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-…