本篇大綱:Enter / Update / Exit 狀態、增減資料數量與DOM元素不匹配的方法、神奇的d
今天要來講資料綁定的第二個部分:根據資料與 DOM元素匹配的數量去增減DOM元素~
在講解怎麼增加或減少DOM元素之前,我們要先來看看這張圖
只要一提到 D3 的資料綁定,一定會解說到這張圖,相信不少人也都看過~因為這個是D3 讓我們能專注在資料上的核心概念。
由於使用selection.data
的時候,資料會跟DOM元素一一配對並綁定,因此也會出現資料較多或DOM元素較多的情況,因此如果兩者的數量不匹配時,D3就把這些情況分成三種狀態
update
資料。enter
資料。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元素與綁定的資料這邊可以看到,無論是 _enter、_exit 或是 _groups,它們陣列的數量都一樣是四個
接著我們分別展開 _enter 跟 _exit,發現裡面都是寫 [empty X 4],代表所有的資料都跟 DOM 元素搭配好了,沒有多餘的資料或是多餘的DOM元素
最後展開_group,會看到所有綁定資料的DOM元素。如果一一將元素展開,找到裡面的 _data_,就知道該DOM元素綁定的是哪筆資料
而這些有與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元素可以搭配的資料
繼續將這個陣列展開後,可以看到是第3、4、5的資料沒有搭配到,這幾個資料的值分別是“比”、“較”、“多”,這些沒跟DOM元素綁定的資料就會被歸到 enter 資料。
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元素沒有資料可以綁定
展開這個陣列後,發現是第3跟第4個DOM元素沒有資料綁定,這些沒跟資料綁定的DOM元素就是 exit 資料
看完了D3對資料與DOM元素搭配的狀態後,接著就輪到增減資料數量與DOM元素不匹配的方法
上場啦!
這個分類的方法總共有三項
我們一樣會先看官網解說,並搭配範例一一來講解這三個方法
selection.enter( )
官方文件上寫著:這個方法會返還一個 enter selection,這個 enter selection 用來抓出缺失的DOM元素,以搭配多餘的data資料。
上面這段的意思是,剛剛我們已經先看到,當資料多餘DOM元素時,在_enter
會呈現出沒被綁定的資料,接著我們就要用 enter() 這個方法去創建DOM元素來搭配這些資料
一旦抓到缺少DOM元素搭配的資料後,我們就會用 .append( )的方法,把缺少的DOM元素加上去,如此一來每筆資料就都能搭配到對應的DOM元素了
const enterData = ["資", "料", "比", "較", "多"];
const eData = d3.selectAll('.enterData')
.data(enterData)
.enter()
.append('p')
.attr('class', 'enterData')
這麼一來,缺失的三個 DOM 元素就會被加上去了
selection.exit( )
接著我們看到 exit () 的官方解說:
這個方法會返還一個 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元素
接著,用 exit() 抓出多餘的DOM 元素後,搭配 remove() 的方法把多餘的 DOM 元素刪除
const exitData = ["資", "料", "少"];
const exData = d3.selectAll('.exitData').data(exitData).exit().remove()
console.log('exData', exData);
畫面上就只剩下三個 DOM 元素了
selection.join( )
最後我們看到的是 join() 這個方法。這個方法其實是個更方便的方法,它結合了 exter()、exit() 跟其他方法,讓我們能更快速簡單的增減元素
先看到官方文件的解釋:這個方法可以增加、移除或重新排列元素的順序,藉以搭配資料
這個的意思就是,當你需要同時處理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
看上述的解釋就很清楚啦~當我們使用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 元素,改成呈現我們自訂的文字
這些只是 callback function 當參數的小應用,等到後面的篇章帶到繪製圖表後,就能看到更多更複雜的應用方式
上面講了這麼多當資料數量不匹配時,要如何去增減DOM節點,這時的大家的心裡是否會出現一個疑問:到底什麼時候資料會這樣變動?上面的方法實際上要怎麼應用呢?
這邊我們就來做個小範例吧,透過範例更能了解增減DOM元素的方法要怎麼實際應用~這個範例是滑動選取不同的範圍時,資料會更新,並且呈現在下方的柱狀圖表也會更新
先來看到程式碼:畫面上有一個 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軸位置
}
這樣一來,我們就能順利呈現綁定資料的圖表了
太好了完成!結束掰掰 (想得美)! 乍看之下一切都沒問題,但如果你開始移動 range bar,就會發現:奇怪,怎麼資料明明更新了,但我的圖表卻沒有更新呢?
這是因為,一旦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軸位置
}
這樣一來圖表就能隨著新的資料正確更新了
但是!又出現一個問題(有完沒完啊~),如果我們把範圍縮小,就會驚訝的發現DOM元素並沒有減少,而且還綁定著原本的資料!
這是因為我們並沒有把沒被新資料綁定的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 、 Github Page,可以實際操作一次看看資料跟圖表的變化哦~
"但如果換成使用join的話,就可以一次併在一起寫" 這一段的範例使用.join()若沒有傳值,我自己試過似乎會報錯 (我是用Angular 10)
方便請問錯誤訊息是什麼嗎? 我用原生JS寫能正常運作,不確定是不是受到框架的影響有些地方不同
原因是因為ng 是限制必須要使用 typescript 去編程, 而.join 這個方法他的參數型別並沒有允許使用undefined 值, 所以就是型別錯誤囉~
原來是受到 typescript 的影響~
他文件有提到.join 的第一個參數並不是optional:
https://github.com/d3/d3-selection/blob/main/README.md#selection_join
selection.join(enter[, update][, exit])
但是他的源碼沒有做型別的防呆, 所以原生的才可以pass
https://github.com/d3/d3-selection/blob/main/src/selection/join.js
我用angular做有遇到一個問題想請教,
就是每調整一次range bar...
下面line bar不是換成新的一組,而是再生出一組line bar排在下面...
原本的line bar沒有被蓋掉...
這可能是什麼原因呢XD?
發現要在生圖表前先移除原本的svg?
嗨嗨,不用呀,應該是你綁定元素的方式不太正確~有程式碼可以給我看嗎?