D3.js is a JavaScript library for manipulating documents based on data.
就像我之前文章提到的,D3js 給自己的定位並不是圖表,如官網所言,他是資料驅動dom。能理解這一點,就能將之靈活運用到各自場景。比如,給普通table的<td/>
加上資料背景色變成資料透視表;給文字font-size繫結資料,變成簡易詞雲;或者你就是要畫一些資料指標,等等。在這些操作中,首先要用到的就是將dom與資料關聯起來,並對dom進行增刪改。那麼enter 與 exit 兩個函式就是起到這個作用。(如果在react或者vue中,你可以理解為dom的diff,只是在d3中我們是顯式地直接操作dom)
enter 與 exit 與 update
code depend d3 version: v5 github.com/d3/d3-selec…
首先我們先理解一下概念:假設集合 collectionA,集合 collectionB,判斷二者之間是不是有交集 equalBy 。
這張圖,初學d3的同學都見過。可能解釋的比較少的是中間的equalBy部分。collectionA 即指上一次繪製所棒定的資料,如果上次未繫結資料即[undefine, undefine ....],如果沒有圖形就是空陣列[]。collectionB 是我們要重新整理檢視的新資料集合。
現在我們來看程式碼,這是官網的demo如下:
const circle = svg.selectAll("circle").data(data) // UPDATE
.style("fill", "blue");
circle.exit().remove(); // EXIT
circle = circle.enter().append("circle") // ENTER
.style("fill", "green")
.merge(circle) // ENTER + UPDATE
.style("stroke", "black");
複製程式碼
其實這裡有個預設選項 即上圖提到的equalBy。 上面的程式碼我們把預設的equalBy補齊如下:
const equalBy = (d, i) => i; // 根據索引判斷元素是否為同一個元素
const circle = svg.selectAll("circle").data(data, equalBy) // UPDATE
.style("fill", "blue");
circle.exit().remove(); // EXIT
circle = circle.enter().append("circle") // ENTER
.style("fill", "green")
.merge(circle) // ENTER + UPDATE
.style("stroke", "black");
複製程式碼
所以,在沒有指定equalBy的時候,是根據索引判斷元素是否為同一個元素。
const collectionA = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 3, text: 3 }];
const collectionB = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 4, text: 4 }, { id: 5, text: 5 }];
複製程式碼
_但我們正常更新資料時候,equal(collectionA[2],collectionB[2]) == false ,但你未設定equalBy的時候,即預設index為標記,這裡就認為是同一個元素 ,equal(collectionA[2],collectionB[2]) == true(就好像一個程式設計師,10歲的和30歲的他,他的身份證號沒有變,只是頭髮可能因為寫程式碼剩的不太多,他的特徵屬性發生了變化),所以它屬於update部分。
機智的你肯定可以想到,那麼如果我給資料集每個物件一個身份證。
const equalBy = obj => obj.id;
const circle = svg.selectAll("circle").data(data, equalBy) // UPDATE
.style("fill", "blue");
複製程式碼
這時候對比屬於enter exit 還是update 則是根據 obj id是不是還是舊的那個。(也就是,判斷昨天的你和今天的你是否是一個人,是跟據你的身份證id來判斷)。
我們來個實踐:
// 虛擬碼
const equalBy = (d, i) => i; // 為設定,即d3預設規則 selection.data(data)
const collectionA = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 3, text: 3 }];
const collectionB = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 4, text: 4 }, { id: 5, text: 5 }];
// enter() = [{ id: 5, text: 5 }]
// exit() = []
// update = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 4, text: 4 }]
const equalBy = obj => obj.id; // 自定義設定規則 selection.data(data, equalBy)
const collectionA = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 3, text: 3 }];
const collectionB = [{ id: 1, text: 1 }, { id: 2, text: 2 }, { id: 4, text: 4 }, { id: 5, text: 5 }];
// enter() = [{ id: 4, text: 4 }, { id: 5, text: 5 }]
// exit() divsext: 3 }]
// update = [{ id: 1, text: 1 }, { id: 2, text: 2 x}]
複製程式碼
結合react元件中如何使用
enter exit update 其實就是對dom元素的增刪該,這很容易讓我們聯想到react或者vue。這裡我以react為例來說說。
首先,equalBy 相當於react元件key (reactjs.org/docs/lists-… 即標識這個元件的身份id。通常對於exit我們會做刪除操作,enter與update 做render component操作(假設我們的需求僅管理dom,複雜操作本文暫不提,後續獨立篇幅)。
那麼 exit 在react中我們並不需要做什麼,資料不存在,那麼自然就被銷燬。上訴的兩種寫法相當於如下程式碼:
class Demo extens React.Component {
render() {
const { data } =this.props;
return (<div>
{data.map((obj,i) => <div ref={c => (c.__data__ = obj)} key={i}>obj.text</div>)}
</div>)
}
}
class Demo extens React.Component {
render() {
const { data } =this.props;
return (<div>
{data.map((obj,i) => <div ref={c => (c.__data__ = obj)} key={obj.id}>obj.text</div>)}
</div>)
}
}
複製程式碼
細心的你可能發現了一行程式碼ref={c => (c.__data__ = obj)}
我們用react元件建立的元素,如果後續需要使用d3-selection 繼續做一系列操作,比如可以在componensDidUpdate後執行d3-transition動畫等等,我們要做的就是將資料關聯到dom中去。
實際上,d3js 就是通過dom的propties __data__
來關聯資料的(注:dom的propties與attribute 的區別)。所以我們可以在ref 中獲取dom例項,並賦值掛載。
附錄
d3js: d3js.org/
demo工具:beta.observablehq.com/
demo地址: beta.observablehq.com/@leannechn/…
react keys: reactjs.org/docs/lists-…