很早之前就知道d3,当初看到它的时候,第一反应就是“我去,怎么这么炫酷“,但是一直没有学(自己太懒了),最近可能是五月病犯了,不想看书,不想写代码,不想看论文,不想写论文,虽然什么事情都不想做,不过还是找点事情做吧,那就学学d3呗,说不定将来什么时候就用到了呢。
这篇博客主要从源码的角度去学习,所以对于没有接触过d3的朋友,请先看完下面的资料。
学习资料
学习嘛,当然得找到好的资料咯,下面是我昨天看的一些文章,在d3的github上都能够找到,毕竟最好的学习资料就是官网的文档、教程和源代码了。
下面是我现在看的文章,每篇文章都不长,而且写的都很好,值的推荐,如果快的话一天就看完了。
- Introduce
- Let’s Make a Bar Chart
- Three Little Circles
- Thinking with Joins
- How Selections Work
- General Update Pattern, Parts I, II & III
- Nested Selections
- Object Constancy
- d3源代码
简单demo
和以前一样,首先展示一下效果:
源代码如下(代码写的比较乱,对于有代码洁癖的请见谅):
<!doctype html>
<html>
<head>
<meta charset=‘utf-8‘>
<style type="text/css">
.enter {
fill: red;
}
.update {
fill: blue;
}
</style>
</head>
<body>
<svg width=‘600‘ height=‘400‘
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg">
</svg>
<script type="text/javascript" src=‘./d3.js‘></script>
<script type="text/javascript">
!function(){
var alphabate = ‘abcdefghijklmnopqrstuvwsyz‘.split(‘‘);
var svg = d3.select(‘svg‘)
.append(‘g‘)
.attr(‘transform‘, ‘translate(0, 100)‘);
function display(data){
var text = svg.selectAll(‘text‘)
.data(data, function(d){
return d;
});
text
.transition()
.duration(750)
.attr(‘class‘, ‘update‘)
.attr(‘y‘, 0)
.attr(‘x‘, function(d, i){
return i * 20;
});
text
.enter()
.append(‘text‘)
.attr(‘class‘, ‘enter‘)
.attr(‘y‘, -50)
.attr(‘x‘, function(d, i){
return i * 20;
})
.transition().duration(750)
.attr(‘y‘, 0);
text
.exit()
.style(‘fill-opacity‘, 1)
.transition()
.duration(750)
.style(‘fill-opacity‘, 0)
.attr(‘y‘, 50)
.remove();
text.text(function(d){
return d;
});
}
setInterval(function(){
var start = parseInt(Math.random() * 26);
var end = parseInt(Math.random() * 26);
if(start == end){
end++;
}else if(start > end){
var temp = start;
start = end;
end = temp;
}
display(alphabate.slice(start, end));
}, 1000);
}();
</script>
</body>
</html>
html代码很简单就是定义了一个svg的标签而已,然后我们引用了d3.js文件,之后就是添加demo的代码了(也就是从30~89行)。首先我们定义了一个alphabate变量,这个变量是用来保存要显示在页面上数据源,之后我们要现实的数据都是通过截取它来获取的。
32~34行代码,首先我们选中文档中定义的svg元素,然后向里面添加一个g元素,然后将g元素通过tranform属性进行了一定的偏移,然后将偏移了的g元素赋给了svg变量(对于jquery比较熟悉的朋友,应该能够很轻松的看懂)。
然后接下来我们定义了一个display函数,这个函数使用现实数据用的。对于这个函数我们在后面进行解释,我们先来看一下76~89行的代码,嗯,其实很简单是不是,就是每隔一秒随机的截取alphabate字符串,然后交给display函数进行显示而已。
让我们来仔细的看一下这个display函数,第34行代码也比较简单,就是选取g元素(svg保存的是g元素的selection)里面的text元素而已(虽然刚开始的时候没有text元素),然后我们进行了一下data join,42~49行对于update状态的元素进行一些属性设置,50~60行的代码对于enter状态的元素设置了一些属性,62~69行代码对于exit状态的元素进行属性设置并删除。
最后对于g元素下面所有的text元素设置内容。
上面的代码虽然很潦草的解释了一下,但是里面有几个地方我们需要好好解释一下的。通过d3选择的元素我们称为selection,然后我们通过.data函数进行data join,这里是最值得我们注意的地方。为什么值得注意呢,因为这个时候.data函数传过去的数据的个数和我们选中的元素在数量上不一定相匹配。比如在我们的37~40行代码,我们通过svg变量保存的g元素的selection选择text元素,然后进行data join。假设第一次display传过来的实参是[‘h’,’i’,’j’,’k’],可是这个时候我们选择的text元素其实为空,那么我们是不是应该添加四个text元素,然后显示我们的数据呢。如果对于元素的个数大于data的个数,我们是不是应该要删除那些多余的元素呢。从而我们将通过select或者selectAll选中的selection中的元素可以分成三类:
- update类型:就是data和元素完全匹配的元素,比如data的大小为3,元素的大小为2,那么匹配的元素就是2个。
- enter类型:就是当data的个数要比元素的个数要大情况下,要补全的元素,我们可以通过enter()来获取,严格的说这里获取的是占位符(一个
{__data__: data}
形式的对象selection),因为在获取的时候实际上并没有元素,我们后面是通过append函数进行添加的 - exit类型:当data个数要比元素个数小是,要删除的那些元素。
说了这么多,是不是感觉很多都不明白呀,嗯,昨天看完那些资料,我也是这种感觉,那我们该怎么办呢,既然开始学了,不能半途而废不是吗,最好的学校资料还是源代码,我们看看源代码就好了。
源码解析
下面我只对一些比较重要的函数进行分析,所以想要获取源码的朋友,可以在 d3获取。
d3.select(d3.selectAll和d3.select类似)
首先我们从d3的select函数开始,因为我们的代码就是从那里开始的,下面是select函数的代码:
d3.select = function(node) {
var group;
if (typeof node === "string") {
group = [ d3_select(node, d3_document) ];
group.parentNode = d3_document.documentElement;
} else {
group = [ node ];
group.parentNode = d3_documentElement(node);
}
return d3_selection([ group ]);
};
对于属性jquery的朋友,肯定十分熟悉jquery 是如果选取元素的$(XXX)
这里的XXX可以是选择器,也可是element。d3的select和jquery差不多,我们可以清楚的看到如果是选择器的话,那么就调用d3_select函数来获取要选择的元素,其中d3_document就是document元素,然后将选择的元素作为group数组的第一个元素,然后指定group的parentNode,其实就是根元素html呗。
我们可以看看d3_select是如何实现的呢,它的代码具体如下:
var d3_select = function(s, n) {
return n.querySelector(s);
}
好简单有没有,就是调用了querySelector
对于d3.select函数传过来的元素为element的情况类似,这里我们就不分析了。
然后我们看看最后一行, return d3_selection([ group ]);
调用了d3_selection函数,然后将其结果给返回。那我们看看这个d3_selection函数到底做了些什么事情。
function d3_selection(groups) {
d3_subclass(groups, d3_selectionPrototype);
return groups;
}
看代码好像是让groups继承d3_selectionPrototype,那么是不是呢,我们看一下d3_subclass函数:
var d3_subclass = {}.__proto__ ? function(object, prototype) {
object.__proto__ = prototype;
} : function(object, prototype) {
for (var property in prototype) object[property] = prototype[property];
};
哈哈还真是的,然后select函数就没有了,有没有很简单的感觉。其实d3.select函数很简单,就是将选择到的元素作为一个group数组的第一个元素,然后让group继承d3_selectionPrototype。
d3_selectionPrototype.append
因为在我们的demo中调用的第二个函数是append,所以我们接下来看看这个函数呗:
d3_selectionPrototype.append = function(name) {
name = d3_selection_creator(name);
return this.select(function() {
return this.appendChild(name.apply(this, arguments));
});
};
函数的第一行调用了d3_selection_creator,这个代码很简单就是通过传入的name来获取创建元素的函数,我们这里获取的就是document.createElement。
然后调用了select函数我们看看select函数干了些什么:
d3_selectionPrototype.select = function(selector) {
var subgroups = [], subgroup, subnode, group, node;
selector = d3_selection_selector(selector);
for (var j = -1, m = this.length; ++j < m; ) {
subgroups.push(subgroup = []);
subgroup.parentNode = (group = this[j]).parentNode;
for (var i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) {
subgroup.push(subnode = selector.call(node, node.__data__, i, j));
if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
} else {
subgroup.push(null);
}
}
}
return d3_selection(subgroups);
};
代码比较难懂,这里我们主要看第3行代码和第9行代码就可以了,第3行代码是通过传过来的函数来获取selector函数,如果selector如果是函数的话,直接返回这个selector,所以这里我们的selector保持不变。第九行其实就是对选择获取的selection元素进行便利,然后将每个选择的元素作为selector函数的this而已,并且传入一些参数,因为这里我们不需要这些参数,所以我们也就不分析了。
通过上面的代码解释我们可以比较清楚的了解到append的函数就是给selection中的每个元素添加子元素呗。
d3_selectionPrototype.data
这个函数使用来进行data join的,下面是这个函数的源代码:
d3_selectionPrototype.data = function(value, key) {
var i = -1, n = this.length, group, node;
if (!arguments.length) {
value = new Array(n = (group = this[0]).length);
while (++i < n) {
if (node = group[i]) {
value[i] = node.__data__;
}
}
return value;
}
function bind(group, groupData) {
var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
if (key) {
var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue;
for (i = -1; ++i < n; ) {
if (node = group[i]) {
if (nodeByKeyValue.has(keyValue = key.call(node, node.__data__, i))) {
exitNodes[i] = node;
} else {
nodeByKeyValue.set(keyValue, node);
}
keyValues[i] = keyValue;
}
}
for (i = -1; ++i < m; ) {
if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) {
enterNodes[i] = d3_selection_dataNode(nodeData);
} else if (node !== true) {
updateNodes[i] = node;
node.__data__ = nodeData;
}
nodeByKeyValue.set(keyValue, true);
}
for (i = -1; ++i < n; ) {
if (i in keyValues && nodeByKeyValue.get(keyValues[i]) !== true) {
exitNodes[i] = group[i];
}
}
} else {
for (i = -1; ++i < n0; ) {
node = group[i];
nodeData = groupData[i];
if (node) {
node.__data__ = nodeData;
updateNodes[i] = node;
} else {
enterNodes[i] = d3_selection_dataNode(nodeData);
}
}
for (;i < m; ++i) {
enterNodes[i] = d3_selection_dataNode(groupData[i]);
}
for (;i < n; ++i) {
exitNodes[i] = group[i];
}
}
enterNodes.update = updateNodes;
enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
enter.push(enterNodes);
update.push(updateNodes);
exit.push(exitNodes);
}
var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
if (typeof value === "function") {
while (++i < n) {
bind(group = this[i], value.call(group, group.parentNode.__data__, i));
}
} else {
while (++i < n) {
bind(group = this[i], value);
}
}
update.enter = function() {
return enter;
};
update.exit = function() {
return exit;
};
return update;
};
这个函数比较长,我们慢慢的来分析。3~11行代码表示,如果没有调用函数时没有传递实参的话,就返回当前selection第一个group中每个元素绑定的data。
12~63行代码定义了一个bind函数,这个函数主要使用来将数据绑定在,并且将selection中的元素划分为update状态、enter状态和exit状态并分开保存。
现在让我们来仔细的分析一下这个函数,这个函数有两个参数,第一个参数时group,就是要绑定数据的元素,第二个是groupData,当然就是我们要绑定的数据了呗。
在第13行定义的updateNodes、enterNodes和exitNodes三个变量是值得我们注意的,这三个函数的大小为groupDate的大小,之所以会这么大,是因为这三个变量最多存储这么多的元素。
第14行代码是判断d3_selectionPrototype.data在调用的时候是否传递了第二个参数key,这个参数是一个function,用来制定group和groupData之间时如何绑定的,默认情况下是通过数组的下标(by-index)来绑定,也就是说group[0]和groupData[0]绑定,group[1]和groupData[1]绑定,以此类推。如果传递了key这个参数的话,就通过key函数返回的值来进行等值匹配,也就是说key函数会对group上的每一个绑定的data进行调用一次,然后返回一个值,然后在用key函数对于groupData的每一个元素进行调用,如果对group绑定参数和groupData绑定的参数一致的话,就将该元素updateNodes里面,对于没有匹配到的group元素就保存到exitNodes里面,对于没有匹配到的groupData元素进行包装一下保存在enterNodes里面。
这里有一个地方是值得注意的,也就是updateNodes、enterNodes和exitNodes三个数组中的每一个元素是对应groupData的每个数据位置的,比如如果group大小为2,groupData大小为4,那么exitNode四个元素都为undefined;updateNodes第一个和第二个元素是绑定了数据的element,数据绑定是通过element.__data__ = data
方式来绑定的,而后面两个元素都为空。enterNodes第一个元素和第二个元素为空,而后面两个元素为包装的data,格式为{__data__: data};
41~56行代码表示group和groupData之间的绑定时通过默认绑定的方式(by-index方式)。
58~61行代码就是简单的把updateNodes、enterNodes和exitNodes保存到update、enter和exit三个selection里,指定每一个group的parentNode。
但是这里有两个地方需要我们注意,第一个就是enterNodes的update属性保存了updateNodes,这个数据是将来客户端代码调用了append函数后,将新添加的元素给绑定到updateNodes里面,这个append函数和上面我们介绍的append的函数不同,这个append的函数我会在后面的了内容进行介绍。
第二个地方需要我们注意的地方是enter这个selection,这个selection和update、exit两个selection不同,因为这个selection保存的元素当前是不存在的,所以这里需要进行另外的处理,所以这个selection继承的就不是之前我们提到的d3_selectionPrototype了,而是d3_selection_enterPrototype。对于上面那个append函数其实就是定义在d3_selection_enterPrototype上的。
让我们在回到d3_selectionPrototype.data函数,后面的代码也比较简单了,就是通过循环来获取selection的group和groupData,然后调用bind函数而已。
d3_selection_enterPrototype.append
我们来看看这个函数是怎么定义的
d3_selection_enterPrototype.append = d3_selectionPrototype.append;
咦,这个不是第一个d3_selectionPrototype.append是一样的吗?我们再看下d3_selectionPrototype.append函数的定义吧。
d3_selectionPrototype.append = function(name) {
name = d3_selection_creator(name);
return this.select(function() {
return this.appendChild(name.apply(this, arguments));
});
};
我们发现第三行代码调用了this.select函数,是不是这个函数不一样呢,让我们看看d3_selectionPrototype是否定义了新的select函数,果然d3_selectionPrototype重新定义了新的select函数:
3_selection_enterPrototype.select = function(selector) {
var subgroups = [], subgroup, subnode, upgroup, group, node;
for (var j = -1, m = this.length; ++j < m; ) {
upgroup = (group = this[j]).update;
subgroups.push(subgroup = []);
subgroup.parentNode = group.parentNode;
for (var i = -1, n = group.length; ++i < n; ) {
if (node = group[i]) {
subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
subnode.__data__ = node.__data__;
} else {
subgroup.push(null);
}
}
}
return d3_selection(subgroups);
};
最重要的代码其实只有3行代码,第3、第9和第10行代码。第3行是通过enterNodes的update属性来获得updateNodes。第9行创建需要的element元素,然后将其保存在updateNodes中。第10行给创建的element元素绑定需要绑定的data。
总结
到现在就粗略的分析了一下我做那个demo所需要的代码,虽然分析的代码比较少,但是却能够很好帮助理解update、enter和exit状态的元素以及data join是如何工作的。
对于动画的部分,因为还没有分析,所以这里就不写了,会在以后的博客中写的。
还是老话,希望每天能够进步一点,对了,本人现在在合肥读书,暑假的时候希望能找一份靠谱的前端实习工作,觉得我还可以的话,把我带走吧,哈哈哈哈。