iT邦幫忙

2021 iThome 鐵人賽

DAY 6
1
Modern Web

三十天成為D3.js v7 好手系列 第 6

Day6-D3 資料綁訂 Data Binding: 資料狀態enter、update、exit

  • 分享至 

  • xImage
  •  

本篇大綱: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,可以實際操作一次看看資料跟圖表的變化哦~


上一篇
Day5-D3 資料綁定 Data Binding:data( ) 跟 datum( )
下一篇
Day7-D3 不同格式檔資料匯入的API
系列文
三十天成為D3.js v7 好手30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Mizok
iT邦新手 3 級 ‧ 2022-01-25 15:01:06

"但如果換成使用join的話,就可以一次併在一起寫" 這一段的範例使用.join()若沒有傳值,我自己試過似乎會報錯 (我是用Angular 10)

看更多先前的回應...收起先前的回應...
金金 iT邦新手 1 級 ‧ 2022-01-27 18:40:45 檢舉

方便請問錯誤訊息是什麼嗎? 我用原生JS寫能正常運作,不確定是不是受到框架的影響有些地方不同

Mizok iT邦新手 3 級 ‧ 2022-01-27 18:42:42 檢舉

原因是因為ng 是限制必須要使用 typescript 去編程, 而.join 這個方法他的參數型別並沒有允許使用undefined 值, 所以就是型別錯誤囉~

金金 iT邦新手 1 級 ‧ 2022-01-27 18:45:27 檢舉

原來是受到 typescript 的影響~

Mizok iT邦新手 3 級 ‧ 2022-01-27 18:47:15 檢舉

他文件有提到.join 的第一個參數並不是optional:
https://github.com/d3/d3-selection/blob/main/README.md#selection_join

selection.join(enter[, update][, exit]) 
Mizok iT邦新手 3 級 ‧ 2022-01-27 18:53:11 檢舉

但是他的源碼沒有做型別的防呆, 所以原生的才可以pass
https://github.com/d3/d3-selection/blob/main/src/selection/join.js

0
peace&love
iT邦新手 5 級 ‧ 2023-04-05 10:44:57

我用angular做有遇到一個問題想請教,
就是每調整一次range bar...
下面line bar不是換成新的一組,而是再生出一組line bar排在下面...
原本的line bar沒有被蓋掉...
這可能是什麼原因呢XD?

發現要在生圖表前先移除原本的svg?

金金 iT邦新手 1 級 ‧ 2023-04-06 11:33:59 檢舉

嗨嗨,不用呀,應該是你綁定元素的方式不太正確~有程式碼可以給我看嗎?

我要留言

立即登入留言