前端编程提高之旅(八)—-D3.js数据可视化data join解析
最后更新于:2022-04-01 09:37:05
**[D3.js](http://d3js.org/)作为一门轻型的可视化类库,非常便于将数据与web界面元素绑定,实现可视化。乐帝d3.js入门是大体看了一遍[《d3js数据可视化实战》](http://download.csdn.net/detail/loganyang123/6781653)这本书,D3操作非常类似于jquery的使用,具体体现在两点:**
- **选择器模块都采用CSS3标准**
- **方法可以链式调用**
**有了jquery使用基础,相信再加上以上书籍的例子,调试很容易上手使用D3.js,乐帝目前认为D3.js与jquery区别在于:D3.js独有的数据结构概念及对SVG操作方便的实现。而深入理解D3原理,以上皮毛的理解就不够用了。**
**通过阅读上述书籍乐帝将D3内容划了几大块来分开理解:**
**DOM操作包括:**
- **选择器模块(select、selectAll等方法)及如何实现链式调用。**
- **节点模块(append、remove等方法实现)**
- **样式模块(attr、style等方法实现)**
**数据绑定相关方法:**
- **data方法**
- **enter、exit等方法**
**比例尺:**
- **值域与输出域的实现**
**更新、过渡、动画方法:**
- **transition方法及连带duration、ease、delay等方法**
**事件:**
- **即绑定事件**
**而后通读[API](https://github.com/mbostock/d3/wiki/API-Reference)发现对于理解D3.js实现机理有一些关键概念,这里关键概念涉及selection、data join、group、transition等。而乐帝读API最大心得在于,在没有一个大体概念时,千万不要去触碰源码,还是按部就班读API吧,读不懂把文档翻译一遍就懂了,乐帝是这么做的。**
**这篇文章乐帝主要想讨论[data join](http://download.csdn.net/detail/yingyiledi/8074405),但在讨论之前,还是需要补充下基本概念。**
**D3.js独有关键概念是selection,乐帝将其翻译成元素集。它表示从当前文档获取的元素数组。它定义了一种数据结构,或者说是对原有dom文档数据结构的修改。有了元素集之后,就可以对元素执行常规操作了,诸如属性、样式、参数、文档内容等。**
**selection存在的意义在于,是它将页面元素与数据实现绑定连接,连接的数据又可以产生enter和exit子元素集,因此能够反映数据的变化,用于添加或移除元素。**
**D3支持方法链,操作方法返回值是元素集,从这一句话,我们知道不管如何具体实现链式操作,我们知道返回的是元素集就够了。**
**乐帝最近思考,整个web世界甚至整个世界,由两种力量驱动:数据与人。而某些高深思想或许能把人也归结为数据。D3是这两种力量的典型,数据驱动展现,人驱动交互。脱离人与数据,D3的代码,只是一堆分离的函数而已。D3紧紧拥抱数据,这就是其最大特点。**
**selection介绍完了,我们开始介绍data join概念。**
**由上所述,D3是数据驱动,而元素集是沟通数据与元素的集合,而data join的概念又是代表数据与元素结合三种状态的概念,由此看来,data join在D3中属于核心概念。**
**[《以data join概念思考》](http://bost.ocks.org/mike/join/)这篇文章给出了对data join比较简明易懂的陈述。**
**在D3中任何时候,数据与页面元素都有三种关系:数据与元素绑定时,即一一对应,这样构成的selection状态叫update selection;没有元素与之对应的数据构成的selection叫enter selection;没有数据与之对应的元素构成的selection叫exit selection,它代表将要被移除的元素集。**
**如下图:**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e43c8327.jpg)**
**从字面上理解,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容器是空的。**
****
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e43d6e4b.jpg)**
**然后上述空元素集与数据结合,构成新的元素集,包含enter、update、exit三种状态。因为元素集是空的,所以update和exit元素集为空,而enter状态的元素集,则包括五个占位符元素。**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e43e56d1.jpg)
selection.enter后,返回enter元素集,此时为五个绑定数据的对象。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e43f1bb8.jpg)
**最后append("circle")一步,使得enter元素集实现与元素一一对应,没错构造成了上述的update元素集状态。**
**由上述例子不难看出,给页面对象添加子对象不用for循环,而是采取data join的概念,用意在于,在静态展现的基础上,对update及exit做微小改动,就可以使它实现动态展现。这就意味着你可以看实时数据,允许数据集合的交互行为及温和过渡效果展现。**
**任何时候运行代码,都会重新计算data join,从而保证数据与元素预期的关系。**
**data join允许我们队指定状态进行操作,比如,可是设置常数值在enter上,而不是update上,通过重新选择元素最小化改变原有dom,大大提升渲染效率。**
**下面乐帝展示一个[对各个状态data join操作](http://bl.ocks.org/mbostock/5779690#index.html)的例子:**
**起始HTML:**
~~~
<div>update</div>
<div>exit</div>
~~~
**起始D3代码:**
~~~
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 三个状态:**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e440ec8a.jpg)**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e4424d05.jpg)**
**如上图,不难分析得到,此时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();//移除节点
});
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e4435dfe.jpg)
**不难得出结论,是原有两个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);
});
~~~
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e448b9b6.jpg)**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e449b9bb.jpg)**
**第三步对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文本内容的集合,经过运行如下:**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e44a8f29.jpg)**
**与之上比较不难发现,这里update元素集有一个元素,enter元素集有一个enter状态元素。另看exit元素集:**
**![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-24_56cd5e44b8116.jpg)**
**exit及enter都少了一个对应状态的对象,这里乐帝猜测d3应该是将每个键值函数返回数据进行了查重操作,将两个“update”文本数据合并成一个update状态元素了。**
**执行1.exit代码,此时只有exit文本的div被移除。执行2.update代码时,update文本的div被设置为背景为橘黄色。最后执行3.enter代码时,背景色为绿色的div被加入到文档。**