D3.js作为一门轻型的可视化类库,非常便于将数据与web界面元素绑定,实现可视化。乐帝d3.js入门是大体看了一遍《d3js数据可视化实战》这本书,D3操作非常类似于jquery的使用,具体体现在两点:
有了jquery使用基础,相信再加上以上书籍的例子,调试很容易上手使用D3.js,乐帝目前认为D3.js与jquery区别在于:D3.js独有的数据结构概念及对SVG操作方便的实现。而深入理解D3原理,以上皮毛的理解就不够用了。
通过阅读上述书籍乐帝将D3内容划了几大块来分开理解:
DOM操作包括:
- 选择器模块(select、selectAll等方法)及如何实现链式调用。
- 节点模块(append、remove等方法实现)
- 样式模块(attr、style等方法实现)
数据绑定相关方法:
比例尺:
更新、过渡、动画方法:
- transition方法及连带duration、ease、delay等方法
事件:
而后通读API发现对于理解D3.js实现机理有一些关键概念,这里关键概念涉及selection、data join、group、transition等。而乐帝读API最大心得在于,在没有一个大体概念时,千万不要去触碰源码,还是按部就班读API吧,读不懂把文档翻译一遍就懂了,乐帝是这么做的。
这篇文章乐帝主要想讨论data join,但在讨论之前,还是需要补充下基本概念。
D3.js独有关键概念是selection,乐帝将其翻译成元素集。它表示从当前文档获取的元素数组。它定义了一种数据结构,或者说是对原有dom文档数据结构的修改。有了元素集之后,就可以对元素执行常规操作了,诸如属性、样式、参数、文档内容等。
selection存在的意义在于,是它将页面元素与数据实现绑定连接,连接的数据又可以产生enter和exit子元素集,因此能够反映数据的变化,用于添加或移除元素。
D3支持方法链,操作方法返回值是元素集,从这一句话,我们知道不管如何具体实现链式操作,我们知道返回的是元素集就够了。
乐帝最近思考,整个web世界甚至整个世界,由两种力量驱动:数据与人。而某些高深思想或许能把人也归结为数据。D3是这两种力量的典型,数据驱动展现,人驱动交互。脱离人与数据,D3的代码,只是一堆分离的函数而已。D3紧紧拥抱数据,这就是其最大特点。
selection介绍完了,我们开始介绍data join概念。
由上所述,D3是数据驱动,而元素集是沟通数据与元素的集合,而data join的概念又是代表数据与元素结合三种状态的概念,由此看来,data join在D3中属于核心概念。
《以data join概念思考》这篇文章给出了对data join比较简明易懂的陈述。
在D3中任何时候,数据与页面元素都有三种关系:数据与元素绑定时,即一一对应,这样构成的selection状态叫update selection;没有元素与之对应的数据构成的selection叫enter selection;没有数据与之对应的元素构成的selection叫exit selection,它代表将要被移除的元素集。
如下图:
从字面上理解,D3对数据的推崇可谓毫无节操,有数据没元素叫enter,有元素没数据叫exit,代表要被踢出去的元素。
下面来看一段代码例子:
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);
首先来看第一句svg.selectAll("circle"),它返回空的元素集,因为SVG容器是空的。
然后上述空元素集与数据结合,构成新的元素集,包含enter、update、exit三种状态。因为元素集是空的,所以update和exit元素集为空,而enter状态的元素集,则包括五个占位符元素。
selection.enter后,返回enter元素集,此时为五个绑定数据的对象。
最后append("circle")一步,使得enter元素集实现与元素一一对应,没错构造成了上述的update元素集状态。
由上述例子不难看出,给页面对象添加子对象不用for循环,而是采取data join的概念,用意在于,在静态展现的基础上,对update及exit做微小改动,就可以使它实现动态展现。这就意味着你可以看实时数据,允许数据集合的交互行为及温和过渡效果展现。
任何时候运行代码,都会重新计算data join,从而保证数据与元素预期的关系。
data join允许我们队指定状态进行操作,比如,可是设置常数值在enter上,而不是update上,通过重新选择元素最小化改变原有dom,大大提升渲染效率。
下面乐帝展示一个对各个状态data join操作的例子:
起始HTML:
var dataset =["enter", "hello"];
var key = function(d) {
return d || this.textContent;
}
var duration = 750;
var div = d3.select("body").selectAll("div")
.data(dataset,key);
此时data join 三个状态:
如上图,不难分析得到,此时data join三状态:update元素集为空,enter元素集已经有数据绑定,exit元素集有两个div元素,在第二张图中innerHTML属性中,发现恰恰是初始化HTML的两个div元素,这里采用了键函数(key)方法,键函数被调用了四次,前两次调用的是已有的div数据调用,后两次则是enter状态元素集数据调用。
接下来对exit元素集操作:
// // 1. exit
var exitTransition = d3.transition().duration(750).each(function() {
div.exit()
.style("background", "red")
.transition()
.style("opacity", 0)
.remove();//移除节点
});
不难得出结论,是原有两个div将要被移除,在页面及内存中清除。
接下来对enter的操作,这里update为空,故第二步update操作并没有实际意义。
// 2. update
var updateTransition = exitTransition.transition().each(function() {
div.transition()
.style("background", "orange");
});
// 3. enter
var enterTransition = updateTransition.transition().each(function() {
div.enter().append("div")
.text(function(d) { return d; })
.style("opacity", 0)
.transition()
.style("background", "green")
.style("opacity", 1);
});
第三步对enter元素集的操作,使其与元素绑定,并设置成绿色。
乐帝在调试期间还发现了另外的情况:
一开始js代码是这样的:
var dataset =["enter", "update"];
var key = function(d) {
return d || this.textContent;
}
var duration = 750;
var div = d3.select("body").selectAll("div")
.data(dataset,key);
即dataset有一个元素与已存在的div文本内容相同,又由于键函数是处理的是dataset与div文本内容的集合,经过运行如下:
与之上比较不难发现,这里update元素集有一个元素,enter元素集有一个enter状态元素。另看exit元素集:
exit及enter都少了一个对应状态的对象,这里乐帝猜测d3应该是将每个键值函数返回数据进行了查重操作,将两个“update”文本数据合并成一个update状态元素了。
执行1.exit代码,此时只有exit文本的div被移除。执行2.update代码时,update文本的div被设置为背景为橘黄色。最后执行3.enter代码时,背景色为绿色的div被加入到文档。