以 Join 的方式來思考D3.js

ssthouse發表於2018-06-13

宣告

原文連結: 來自 D3作者 Mike Bostock bost.ocks.org/mike/join/

譯文地址: github repository

如果覺得還不錯, 不妨去github給個star ?

Content

打個比方, 你想用D3畫一個 散點圖 , 用每一個svg的circle元素來視覺化你的資料. 你會驚訝的發現: D3居然沒有直接建立多個DOM元素的方法! 怎麼回事?

當然, D3有 append 方法, 你可以用來建立單個元素. 比如:

svg.append("circle")
    .attr("cx", d.x)
    .attr("cy", d.y)
    .attr("r", 2.5);
複製程式碼

但這只是一個圓, 如果你想要建立很多個圓(每一個圓代表一個資料點). 你可能會想到用一個for迴圈來實現 ? 這是非常直觀的想法, 這個想法並沒有什麼錯, 但是在這之前不妨看看D3中是如何實現建立多個元素的:

svg.selectAll("circle")
  .data(data)
  .enter().append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", 2.5);
複製程式碼

上面這段程式碼完美的實現了你想要的效果: 為每一個資料點建立了一個 circle, 用資料點的 xy 屬性作為circle的座標. 但這段程式碼裡面的 selectAll("circle") 是什麼意思? 我們為什麼要 select 我們知道當前並不存在的 circle, 還用這個方法的返回值去建立新的元素?

這段程式碼的思想是: 不要告訴D3如何去做, 而是告訴D3你想要的效果. 你想要circle元素和資料一一對應, 那麼你就不應該告訴D3去建立circle元素, 而是告訴D3: .selectAll("circle") 得到的circle集合應該和 .data(data) 一一對應. 這個思想就叫做 Join.

Join 模型

從上圖中可以看到:

  • 資料集合DOM元素集合 相交產生了中間的 update 集合
  • 沒有DOM元素與之對應的Data產生了左邊的 enter 集合 (也就是缺失DOM元素)
  • 同樣的, 所有沒有資料與之對應的DOM元素產生了右邊的 exit 集合 (也就意味著這些DOM元素將被移除)

現在我們可以再來看看上面那段使用 enter-append 模型的程式碼了:

  1. 首先, svg.selectAll("circle") 返回的是一個空的集合, 因為當前 svg 容器還是空的. 這裡的 svg 是所有後續建立的 circle元素的父節點.
  2. svg.selectAll("circle") 返回的集合接下來和 data 進行 Join 操作, 得到的就是我們上面提到的三個集合: update 集合 , enter 集合 , exit 集合. 因為初始時 Elements集合(也就是circle集合)是空的, 所以 updateexit 集合為空, 而 enter 集合會自動為每一個新的data元素生成一個佔位符.
  3. 預設 .data(data) 返回的是 update 集合, 因為 update 集合為空, 所以我們不對其進行操作, 這裡我們呼叫 .enter() 得到 enter 集合.
  4. 接下來, 對於 enter 集合中的每一個元素, 我們使用 selection.append('circle') (值得注意的是, 對集合的操作會被應用到集合中的每一個元素上去). 這樣就為每一個資料點建立了一個 circle (這些circle都在他們的父節點 svg 中)

Join 的方式來思考意味著, 我們要做的事情僅僅是宣告 DOM集合(比如這裡的 circle 集合) 和資料集合之間的關係, 並且通過處理三個不同狀態的集合 enter, update , exit 來描述這種關係.

你也許會問, 為什麼要用這種方式來進行我的資料視覺化工作呢? 好處在哪? 為什麼我不直接用for迴圈建立所有我想要的元素? 答案是這個思想確實是非常有好處的, 它的優美之處在於它的概括性. 現在我們的程式碼還只是處理了 enter 的部分, 這部分對於展示靜態的資料已經足夠了, 但如果你想進行動態的資料展示, 這種 Join 的方式將大大簡化你的工作, 你只需要對 updateexit 進行很少的操作就能得到你想要的效果. 這也意味著你可以輕鬆的展示實時資料, 能夠為使用者新增動態的互動, 能平滑的切換不同的展示資料集.

下面這段程式碼展示了對於 exitupdate 集合的處理:

var circle = svg.selectAll("circle")
  .data(data);

circle.exit().remove();

circle.enter().append("circle")
    .attr("r", 2.5)
  .merge(circle)
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
複製程式碼

無論什麼時候上面的這段程式碼被執行, 它都將重新計算 Join 並且維護好 DOM元素集合 和 資料集合 之間的對應關係. 如果你的新資料集比之前老的資料集要小, 多餘的DOM元素就會進入 exit 集合, 然後被 remove掉. 如果新的資料集比老的大, 那麼新的資料就將進入 enter 集合, 並建立出新的DOM元素. 如果新的資料集和老的數目相同, 那麼只有 update 集合會被更新座標.

使用 Join 的思想能讓我們的程式碼更加直觀. 你只需要處理好這三種狀態的集合, 而不需要 iffor 來進行復雜的邏輯判斷. 你只需要描述好你的資料集合和DOM集合想要有怎樣的對應關係.

Join 還讓你可以對不同狀態的DOM元素進行不同的操作. 比如, 你可以只對 enter 集合進行操作, 這樣就不會每次都對所有的 DOM元素進行更新, 這能顯著的提升你的資料視覺化作品的渲染效率. 同樣的, 你也可以給指定集合的元素新增動畫效果, 比如給 enter 的元素新增放大進入的效果:

circle.enter().append("circle")
    .attr("r", 0)
  .transition()
    .attr("r", 2.5);
複製程式碼

或者給 exit 的集合新增 縮小隱藏 的效果:

circle.exit().transition()
    .attr("r", 0)
    .remove();
複製程式碼

譯者注:

這裡有一個非常好的實踐 Join 思想的例子(同樣來自D3作者), 不妨看看:

Mike Bostock 的實現

join example

這裡是我對這個例子的實現(也包括一些其他的案例):

想繼續瞭解 D3.js ?

這裡是我的 D3.js資料視覺化 的github 地址, 歡迎 start & fork :tada:

D3-blog

如果覺得不錯的話, 不妨點選下面的連結關注一下 : )

github主頁

知乎專欄

掘金

相關文章