Day6-D3 资料绑订 Data Binding: 资料状态enter、update、exit

本篇大纲:Enter / Update / Exit 状态、增减资料数量与DOM元素不匹配的方法、神奇的d

今天要来讲资料绑定的第二个部分:根据资料与 DOM元素匹配的数量去增减DOM元素~

Enter / Update / Exit 状态

在讲解怎麽增加或减少DOM元素之前,我们要先来看看这张图
https://ithelp.ithome.com.tw/upload/images/20210918/20134930VyKjVouvnv.jpg

只要一提到 D3 的资料绑定,一定会解说到这张图,相信不少人也都看过~因为这个是D3 让我们能专注在资料上的核心概念。

由於使用selection.data的时候,资料会跟DOM元素一一配对并绑定,因此也会出现资料较多或DOM元素较多的情况,因此如果两者的数量不匹配时,D3就把这些情况分成三种状态

  • update : 如果资料跟DOM元素能够绑定,该笔输入的资料会被归纳为 update 资料。
  • enter : 如果资料没有DOM元素能绑定,该笔输入的资料会被归纳为 enter 资料。
  • exit : 如果没有资料能跟被DOM元素绑定,剩下的元素就会被归纳为 exit 资料。

简单来说就是:

呼叫selection.data()时,会回传一个新的selection物件,里面包含成功系结到元素的资料 (update),同时也会填入enter()、exit(),里面包含多的资料或DOM

如果还是有点似懂非懂也没关系,下面一样直接上范例!

update 资料 ⇒ 资料与DOM元素数量刚好

// html
<p class="updateData"></p>
<p class="updateData"></p>
<p class="updateData"></p>
<p class="updateData"></p>

// js
const updateData = ["资", "料", "刚", "好"];
const upData = d3.selectAll('.updateData').data(updateData)
console.log('upData', upData)

把 upData 印到console後,我们会看到之前说过的 _enter_exit_group

  • _enter 负责处理输入的资料
  • _exit 负责处理搭配的DOM 元素
  • _group 呈现DOM元素与绑定的资料

https://ithelp.ithome.com.tw/upload/images/20210918/20134930XehYLz80ks.jpg

这边可以看到,无论是 _enter、_exit 或是 _groups,它们阵列的数量都一样是四个

接着我们分别展开 _enter 跟 _exit,发现里面都是写 [empty X 4],代表所有的资料都跟 DOM 元素搭配好了,没有多余的资料或是多余的DOM元素
https://ithelp.ithome.com.tw/upload/images/20210918/20134930ehX56nyVLm.jpg

最後展开_group,会看到所有绑定资料的DOM元素。如果一一将元素展开,找到里面的 _data_,就知道该DOM元素绑定的是哪笔资料
https://ithelp.ithome.com.tw/upload/images/20210918/20134930zUS6r0ftN0.jpg
而这些有与DOM元素绑定的资料,就会被归到 update 资料内

enter 资料 ⇒ 资料多

// html
<p class="enterData"></p>
<p class="enterData"></p>

// js
const enterData = ["资", "料", "比", "较", "多"];
const eData = d3.selectAll('.enterData').data(enterData)
console.log('eData', eData)

这边会看到 _enter 跟 _exit 的阵列内数量不相同。展开 _enter 可以看到阵列内虽然有五个值,但前面两个却是enpty。这是因为前两个数值已经让相对应的DOM元素匹配走了,剩下的三个就是没有DOM元素可以搭配的资料
https://ithelp.ithome.com.tw/upload/images/20210918/20134930yIDaFmeHlz.jpg

继续将这个阵列展开後,可以看到是第3、4、5的资料没有搭配到,这几个资料的值分别是“比”、“较”、“多”,这些没跟DOM元素绑定的资料就会被归到 enter 资料。
https://ithelp.ithome.com.tw/upload/images/20210918/20134930RD86tY5BAt.jpg

exit 资料 ⇒ DOM元素多

// html
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>

// js
const exitData = ["资", "料", "少"];
const exData = d3.selectAll('.exitData').data(exitData)
console.log('exData', exData);

再来我们把 exData 印到 console 上看,会发现这次是 _exit 的数值多了两个DOM元素。这代表着有两个DOM元素没有资料可以绑定
https://ithelp.ithome.com.tw/upload/images/20210918/20134930yfKoUWrpJe.jpg

展开这个阵列後,发现是第3跟第4个DOM元素没有资料绑定,这些没跟资料绑定的DOM元素就是 exit 资料
https://ithelp.ithome.com.tw/upload/images/20210918/20134930ZQNJ6DNXMX.jpg


增减资料数量与DOM元素不匹配的方法

看完了D3对资料与DOM元素搭配的状态後,接着就轮到增减资料数量与DOM元素不匹配的方法 上场啦!

这个分类的方法总共有三项

  • selection.enter ( )
  • selection.exit ( )
  • selection.join ( )

我们一样会先看官网解说,并搭配范例一一来讲解这三个方法

selection.enter( )

官方文件上写着:这个方法会返还一个 enter selection,这个 enter selection 用来抓出缺失的DOM元素,以搭配多余的data资料。

上面这段的意思是,刚刚我们已经先看到,当资料多余DOM元素时,在_enter 会呈现出没被绑定的资料,接着我们就要用 enter() 这个方法去创建DOM元素来搭配这些资料
https://ithelp.ithome.com.tw/upload/images/20210918/20134930pVzKsJih6R.jpg

一旦抓到缺少DOM元素搭配的资料後,我们就会用 .append( )的方法,把缺少的DOM元素加上去,如此一来每笔资料就都能搭配到对应的DOM元素了

const enterData = ["资", "料", "比", "较", "多"];
const eData = d3.selectAll('.enterData')
				.data(enterData)
				.enter()
				.append('p')
				.attr('class', 'enterData')

这麽一来,缺失的三个 DOM 元素就会被加上去了
https://ithelp.ithome.com.tw/upload/images/20210918/20134930WLsiSYTTyq.jpg

selection.exit( )

接着我们看到 exit () 的官方解说:
https://ithelp.ithome.com.tw/upload/images/20210918/20134930T18FODcn1Q.jpg
这个方法会返还一个 exit selection,这个 exit selection 用来抓出多余的DOM元素,这些多的 DOM 元素没有资料能搭配。

这段话的意思是,刚刚我们已经先看到,当DOM元素比较多时,在_exit 会呈现出没绑定到资料的DOM元素,接着我们就要用 exit() 这个方法将这几个DOM元素抓出来并删除掉

一开始时,我们有五个DOM元素跟三笔资料

// html
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>
<p class="exitData"></p>


const exitData = ["资", "料", "少"];
const exData = d3.selectAll('.exitData').data(exitData)
console.log('exData', exData);

画面上的 DOM元素
https://ithelp.ithome.com.tw/upload/images/20210918/20134930vpawbY5kLl.jpg

接着,用 exit() 抓出多余的DOM 元素後,搭配 remove() 的方法把多余的 DOM 元素删除

const exitData = ["资", "料", "少"];
const exData = d3.selectAll('.exitData').data(exitData).exit().remove()
console.log('exData', exData);

画面上就只剩下三个 DOM 元素了
https://ithelp.ithome.com.tw/upload/images/20210918/20134930ZbA9dWZmDf.jpg

selection.join( )

最後我们看到的是 join() 这个方法。这个方法其实是个更方便的方法,它结合了 exter()、exit() 跟其他方法,让我们能更快速简单的增减元素

先看到官方文件的解释:这个方法可以增加、移除或重新排列元素的顺序,藉以搭配资料
https://ithelp.ithome.com.tw/upload/images/20210918/20134930iqjhRsvHIM.jpg

这个的意思就是,当你需要同时处理update、enter以及 exit 的资料时,之前需要分开来写,例如下面以update 跟 enter为例

const enterData = ["资", "料", "比", "较", "多"];
const eData = d3.selectAll('.enterData')
								.data(enterData)
								.text(d=>d) // 这边先处理 update 资料
								.enter(). // 这边接着处理 enter 资料
								.append('p')
								.attr('class', 'enterData')
								.text(d=>d)

但如果换成使用join的话,就可以一次并在一起写

const joinData = ['j', 'o', 'i', 'n']
  d3.selectAll('.joinData')
    .data(joinData)
    .join() // 把update 跟 enter 一起处理
    .append('p')
    .attr('class','.joinData')
    .text(d => d);

这样一来就节省了我们需要处理 DOM 元素的工序,也减少写重覆的 code,非常的方便~~


神奇的d:绑定资料後的操作,API 回传 callback function

前面我们已经将资料到DOM元素了,但这样还不算完成,我们接着还要处理想呈现的资料。处理之前,我们先来看看一个神奇的参数 —— d

在看别人写的D3图表程序码时,相信大家很常会看到一个神奇的参数 d 被带入呼叫的 API 中,例如:

const joinData = ['j', 'o', 'i', 'n']
  d3.selectAll('.joinData')
    .data(joinData)
    .join(). // 把update 跟 enter 一起处理
    .append('p')
    .attr('class','.joinData')
    .text(d => d);  // 这个神奇的 d

这到底是什麽东西呢?

这边的d => d其实是callback function 的缩写,以及它所带的参数,本来的写法是这样:

const joinData = ['j', 'o', 'i', 'n']
  d3.selectAll('.joinData')
    .data(joinData)
    .join(). // 把update 跟 enter 一起处理
    .append('p')
    .attr('class','.joinData')
    .text(function(d){return d });  

我们一样来看看官网对这个方法的解释:selection.text() 这个方法里面可以带入参数,如果带入的参数是一个方法,它代表的就是 每一个绑定资料的 selection 实体,而且会按照顺序回传个别 selection 绑定的 data 跟 index

https://ithelp.ithome.com.tw/upload/images/20210918/20134930USZCBTiPBL.jpg

看上述的解释就很清楚啦~当我们使用callback function 去回传 d 时,就能把每个DOM元素绑定的资料选出来,并一一呈现在画面上,这样的方式有点类似map的用法,而且大多数的API 都能使用 callback function 去回传资料

除了一一回传资料之外,我们也可以利用 callback function 去设定一些条件

const joinData = ['j', 'o', 'i', 'n']
  d3.selectAll('.joinData')
    .data(joinData)
    .join(). // 把update 跟 enter 一起处理
    .append('p')
    .attr('class','.joinData')
    .text(function(d){
       if(d ==='o'){
          return '抓到你了'
        } else {
          return d
        }
      }); 

    // 三元运算式的简化写法
    .text(d => d === 'o'? '抓到你了' : d)

这样一来,就能把本来应该要呈现 o 的 DOM 元素,改成呈现我们自订的文字
https://ithelp.ithome.com.tw/upload/images/20210918/20134930NT9cnptiIA.jpg

这些只是 callback function 当参数的小应用,等到後面的篇章带到绘制图表後,就能看到更多更复杂的应用方式


什麽情况下需要处理资料变动?

上面讲了这麽多当资料数量不匹配时,要如何去增减DOM节点,这时的大家的心里是否会出现一个疑问:到底什麽时候资料会这样变动?上面的方法实际上要怎麽应用呢?

这边我们就来做个小范例吧,透过范例更能了解增减DOM元素的方法要怎麽实际应用~这个范例是滑动选取不同的范围时,资料会更新,并且呈现在下方的柱状图表也会更新
https://i.imgur.com/lUzZi9l.gif

先来看到程序码:画面上有一个 input 范围条,我们先建立一个空阵列,当范围条改变时会把随机乱数推进阵列,这个阵列就会成为我的资料集

// html
<div>
  <input class="dataChange" type="range" style="width:100% ; cursor:pointer" name="dataChange" min="0" max="10" value="0">
  <p> data:<span class="showData"></span></p>

  <div class="example">
  </div>
</div>

// JS
const dataChange = document.querySelector('.dataChange')
const showData = document.querySelector('.showData')
let randomData = [] // 先建立空资料阵列
dataChange.addEventListener('change', function(){
  randomData = []  // 每次重选range就清空阵列
  for(i= 0; i< event.target.value; i++){
    let random = Math.floor(Math.random() * 5)
    randomData.push(random) // 塞入随机乱数资料
  }
  showData.innerHTML = randomData

drawDiagram() // 绘制图表的方法
});

设定好我们准备带入的资料阵列後,接着就撰写画图表的 drawDiagram() 方法吧!首先,一样先建立 svg 画布,接着设定画图表的方法

// 建立 svg 画布
  const rangeSelect =  d3.select('.example')
        .append('svg')
        .attr('width', 500)
        .attr('height', 500)

// 制作图表
  const drawDiagram =()=>{

  }

选取页面上所有的 < rect > DOM元素之後,用.data()的方法把我们设定好的 randomData 与 DOM 元素绑定

// 制作图表
  const drawDiagram =()=>{
  // 绑定 update 资料
  let rects = rangeSelect.selectAll('rect')
             .data(randomData)
  }

不过此时,我们的页面上并没有任何 < rect > 的DOM元素,因此这些绑定的资料就都会归到 enter 资料内。我们要使用 enter() 的方法去建立相对应的DOM元素,然後再将这些 DOM 元素加上必要的 style 跟 attr 标签内容

// 制作图表
  const drawDiagram =()=>{
  // 绑定 update 资料
  let rects = rangeSelect.selectAll('rect')
             .data(randomData)
  
  // 用 enter 加上少的DOM元素
  rects.enter()
        .append('rect')
        .attr('width', d => d * 60)
        .attr('height', 50)
        .style('fill', 'blue')
        .attr('x', (d, index) => 0 ) // 设定x位置
        .attr('y', (d, index) => index * 60) // 设定y轴位置
  }

这样一来,我们就能顺利呈现绑定资料的图表了
https://ithelp.ithome.com.tw/upload/images/20210918/20134930oWGQzKYTHj.jpg

太好了完成!结束掰掰 (想得美)! 乍看之下一切都没问题,但如果你开始移动 range bar,就会发现:奇怪,怎麽资料明明更新了,但我的图表却没有更新呢?
https://i.imgur.com/AWKXB27.gif

这是因为,一旦DOM元素跟原本的资料绑定後,虽然你更新了资料,更新的资料也已经跟DOM元素绑定的(绑到_data_上),但却没有更新DOM元素一开始绑定的高度设定呀!因此我们要重新设定 rect 的 width,让它绑定新的资料,才能正确呈现更新後的图表

// 制作图表
  const drawDiagram =()=>{
  // 绑定 update 资料
  let rects = rangeSelect.selectAll('rect')
             .data(randomData)

  // update 更新绑定的资料
  rects.attr('width', d => d * 60)
  
  // 用 enter 加上少的DOM元素
  rects.enter()
        .append('rect')
        .attr('width', d => d * 60)
        .attr('height', 50)
        .style('fill', 'blue')
        .attr('x', (d, index) => 0 ) // 设定x位置
        .attr('y', (d, index) => index * 60) // 设定y轴位置
  }

这样一来图表就能随着新的资料正确更新了
https://i.imgur.com/1oyS94o.gif

但是!又出现一个问题(有完没完啊~),如果我们把范围缩小,就会惊讶的发现DOM元素并没有减少,而且还绑定着原本的资料!
https://i.imgur.com/n60V5tB.gif

这是因为我们并没有把没被新资料绑定的DOM元素删除,因此我们要用 exit() 的方法来移除没绑定新资料的DOM元素

// 制作图表
  const drawDiagram =()=>{
  // 绑定 update 资料
  let rects = rangeSelect.selectAll('rect')
             .data(randomData)

  // update 更新绑定的资料
  rects.attr('width', d => d * 60)
  
  // 用 enter 加上少的DOM元素
  rects.enter()
        .append('rect')
        .attr('width', d => d * 60)
        .attr('height', 50)
        .style('fill', 'blue')
        .attr('x', (d, index) => 0 ) // 设定x位置
        .attr('y', (d, index) => index * 60) // 设定y轴位置
    
  // 用 exit 移除多的 DOM 元素
  rects.exit().remove()
  }

这样一来,我们的图表才算是大功告成!

除了用 enter、update、exit 这些方法外,我们也可以用之前提过的 join() 方法,一次处理完新增、更新、删除

let rects = rangeSelect.selectAll('rect')
             .data(randomData)
             .join(
              enter => enter.append("rect")
                            .attr('width', d => d * 60)
                            .attr('height', 50)
                            .style('fill', 'blue')
                            .attr('x', (d, index) => 0 ) // 设定x位置
                            .attr('y', (d, index) => index * 60), // 设定y轴位置,
              update => update.attr('width', d => d * 60),
              exit => exit.remove()
             )

透过D3的这些API,我们就可以很方便的绘制资料变化後的图表~

以上,就是D3.js重要的资料绑定概念!老天爷啊终於写完了,差点以为今天就是挑战失败的那一日了XD 。这边花如此长篇幅详细讲解,是因为这是d3.js很重要的观念,如果看完後还有不太懂的地方(或是我哪里写错),都欢迎在下面留言一起讨论~

Github Page 图表与 Github 程序码

最後,一样附上本章的程序码与图表 GithubGithub Page,可以实际操作一次看看资料跟图表的变化哦~


<<:  活到老,学到老,Ruby 30 天刷题修行篇第三话

>>:  人脸辨识的流程--人脸侦测

Day25-介接 API(三)Google Calendar(III)OAuth 凭证建立与用 Google Calendar API 建立 Google Meet 会议

大家好~ 今天来实作如何用 Google Calendar API 建立 Google Meet 会...

第29天:档案下载

目前我们完成档案上传的功能,接下来就要进行档案下载 写一个专门下载档案的Action,接受ID参数,...

阅读.evtx文件--关於从16进位看事件纪录这回事

事情来自某天我在找资料的过程中,看到有些大大提供了事件纪录档的文本说明,所以今天要来试着阅读.evt...

【程序】给 23 - 28 岁的你的一封信 转生成恶役菜鸟工程师避免 Bad End 的 30 件事 - 29

来到了铁人赛的29天,扣除掉最後一集的心得,今天算是最後一个主题。 今天的影片和以往不太一样,我事...

Proxmox VE 网路进阶设定 (Bridge、LACP、VLAN)

在规模较大的企业网路中,为了避免单点故障会采用 LACP 的方式将多条线路聚合在一起使用,除了增加...