D3是一个数据可视化的javascript库,相对于highchart和echarts专一图表可视化的库,D3更适合作大数据处理的可视化,它只提供基础的可视化功能,灵活而丰富的接口让咱们能开发出各式各样的图表。javascript
D3代码版本:“3.5.17”html
D3的代码骨架比较简洁,相比jquery来讲更适合阅读,你能够很舒服地自上而下的看下去而不用看到一个新的函数发现声明在千里以外,而后在代码中跳来跳去。java
基本的数学计算:最小最大、均值中值方差、偏分值……node
各类集合类型: map、set、nest……jquery
集合的操做、方法: text、html、append、insert、removegit
d3的dragginggithub
图形操做算法
……json
首先是典型的自执行匿名函数,对外提供接口,隐藏实现方式,实现私有变量等等功能。数组
!function() { // code here }()
这里用到的是感叹号,其实和使用括号是同样的做用,就是将函数声明变成函数表达式,以便于函数的自执行调用,你能够试试
function() { console.log('no console') }()
这是由于JS禁止函数声明和函数调用混用,而括号、逻辑运算符(+、-、&&、||)、逗号、new等均可以将函数声明变成函数表达式,而后即可以自执行。有人作过调查关于这些转化的方法哪一个更快,能够查看这篇博客,大概new是最慢的,相比使用括号是基本最快,感叹号反而性能通常,因此其实用哪一个都没什么区别,固然若是你想省敲一个符号也是能够用感叹号的。
对于d3,采用的是建立私有变量对象,而后对它进行扩展,最后对外暴露
var d3 = { version: '3.5.17' }; // code here //... if (typeof define === 'function' && defind.amd) this.d3 = d3, define(d3); else if (typeof module == 'object' && module.exports) module.exports = d3; else this.d3 = d3;
第一种为异步模块加载模式,第二种为同步模块加载或者是ecma6的import机制,第三种则是将d3设置为全局变量,由于匿名自执行函数中,函数的环境就是全局的,因此this == window。
d3的方法是属于d3对象的属性:
d3_xhr( url, mimeType, response, callback) { // code } d3.json = function(url, callback) { return d3_xhr(url, 'application/json', d3_json, callback); }; function d3_json(request) { return JSON.parse(request.responseText); }
不太好的是d3没有在命名上区分哪些是私有函数,哪些是公用函数,不过对于经过建立对象来对外暴露接口的对象来讲,应该也不用去区分吧。
var d3_arraySlice = [].slice, d3_array = function(list) { return d3_arraySlice.call(list); }; var d3_document = this.document;
提取slice方法,使用它来生成数组的副本,slice不会对原生数组作切割,而是会返回数组的复制品,可是要注意是浅复制,对于数组中的对象、数组,是单纯的引用,因此对原数组中的对象或数组的更改仍是会影响到复制品。
【line15】
if (d3_document) { var test = d3_array(d3_document.documentElement.childNodes); console.log(test); try { d3_array(d3_document.documentElement.childNodes)[0].nodeType; } catch (e) { console.log('catch error:', e); d3_array = function(list) { var i = list.length, array = new Array(i); while (i--) array[i] = list[i]; return array; }; } }
由前面咱们能够知道d3_array能够用来获取传入数组的副本,经过try来测试document的子节点的第一个子元素,通常就是header这个元素,咱们经过查询w3c能够知道nodeType为1,表示html element,感受应该是测试是不是浏览器环境,若是不是的话,就换成本身写的函数的意思吗?仍是为了兼容一些少数的浏览器呢?
【line 30】
if (d3_document) { try { d3_document.createElement("DIV").style.setProperty("opacity", 0, ""); } catch (error) { var d3_element_prototype = this.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = this.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty; d3_element_prototype.setAttribute = function(name, value) { d3_element_setAttribute.call(this, name, value + ""); }; d3_element_prototype.setAttributeNS = function(space, local, value) { d3_element_setAttributeNS.call(this, space, local, value + ""); }; d3_style_prototype.setProperty = function(name, value, priority) { d3_style_setProperty.call(this, name, value + "", priority); }; } }
暂时不知道是为了跨浏览器仍是跨文档而作的检测,待研究。
【line 53】
d3.min = function(array, f) { var i = -1, n = array.length, a, b; if (arguments.length === 1) { while (++i < n) if ((b = array[i]) != null && b >= b) { a = b; break; } while (++i < n) if ((b = array[i]) != null && a > b) a = b; } else { while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { a = b; break; } while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; } return a; };
首先获取第一个可比较的元素,测试了下,发现对于b >= b,不管b是数字、字符串、数组甚至是对象都是能够比较的,那么什么状况下 b>=b == false呢,对于NaN来讲,不管和哪一个数字比较,都是false的,可是对于Infinity却返回真,是个点。因此应该是为了排除NaN这种有问题的数字。
d3.shuffle = function(array, i0, i1) { if ((m = arguments.length) < 3) { i1 = array.length; if (m < 2) i0 = 0; } var m = i1 - i0, t, i; while (m) { i = Math.random() * m-- | 0; t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t; console.log(i, m); } return array; };
d3使用的洗牌算法,关于Fisher-Yates shuffle的文章能够参考一下,它的演变思路简单而优雅:
正常的思路是
每次从原数组中随机选择一个元素,判断是否已经被选取,是的话删除并放入新的数组中,不是的话从新选择。
缺点:越到后面重复选择的几率越大,放入新数组的时间越长。
优化
为了防止重复,每次随机选择第m张卡牌,m为待洗牌组从原始长度n逐步递减的值
缺点:每次都要从新获取剩余数组中的卡牌的紧凑数组,实际的效率为n2
再次优化
就地随机洗牌,使用数组的后一部分做为存储新的洗牌后的地方,前一部分为洗牌前的地方,从而将效率提高为n。
【line 291】
function d3_class(ctor, properties) { for (var key in properties) { Object.defineProperty(ctor.prototype, key, { value: properties[key], enumerable: false }); } } d3.map = function(object, f) { var map = new d3_Map(); if (object instanceof d3_Map) { object.forEach(function(key, value) { map.set(key, value); }); } else if (Array.isArray(object)) { var i = -1, n = object.length, o; if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o); } else { for (var key in object) map.set(key, object[key]); } return map; }; function d3_Map() { this._ = Object.create(null); } var d3_map_proto = "__proto__", d3_map_zero = "\x00"; d3_class(d3_Map, { has: d3_map_has, get: function(key) { return this._[d3_map_escape(key)]; }, set: function(key, value) { return this._[d3_map_escape(key)] = value; }, remove: d3_map_remove, keys: d3_map_keys, values: function() { var values = []; for (var key in this._) values.push(this._[key]); return values; }, entries: function() { var entries = []; for (var key in this._) entries.push({ key: d3_map_unescape(key), value: this._[key] }); return entries; }, size: d3_map_size, empty: d3_map_empty, forEach: function(f) { for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]); } });
关于enumerable
在这里,使用d3_Map来做为对象的构造函数,d3_class来封装类,这里调用了Object.defineProperty来设置属性和值,这里有一个enumerable: false的属性,它将该属性的可枚举性设置为false,使得该属性在通常的遍历中(for...in...)等中没法被获取,可是仍是能够经过obj.key直接获取到,若是须要获取对象自身的全部属性,无论enumerable的值,可使用 Object.getOwnPropertyNames 方法。
为何要设置这个属性呢?咱们能够看到对d3_Map构造对象时,引入了一些原生内置的方法,其中有一个叫作empty的方法用来判断后来设置的属性是否为空,咱们来看看这个函数的实现:
function d3_map_empty() { for (var key in this._) return false; return true; }
看完以后再结合上面提到的enumerable设置为false的属性在for循环中会被忽略,这样的话就不用再写额外地条件去判断是否为内置属性,很棒的实现方式。
还记得D3独特的将数据和图形领域联系起来的方式吗?进入(enter)--更新(update)--退出(exit)
模式。
【line 832】
d3.selectAll('div') .data(dataSet) .enter() .append('div') ; d3.selectAll('div') .data(data) .style('width', function(d) { return d + 'px'; }) ; d3.selectAll('div') .data(newDataSet) .exit() .remove() ;
这里涉及到了三个函数,data、enter、exit,每次进行操做前咱们须要先调用data对数据进行绑定,而后再调用enter或者exit对图形领域进行操做,那么内部实现原理是怎么样的呢,看完下面这段代码就恍然大悟了:
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; };
数据绑定函数data最终返回了变量update,这个变量update一开始为一个空集合,它拥有d3的集合操做方法,而后data函数经过调用bind函数对传入的参数进行逐项绑定,得到update集合做为自己,以及enter集合和exit集合,最后在update上绑定了函数enter和exit,使得用户在调用data后,能够再次调用enter和exit去获取另外两个集合。
d3也会有bug的时候,这个时候须要对bug进行修复,而后再更新,为了方便下次找到修改的bug,在代码里面对其进行命名,是很好的作法:
【1167】
var d3_mouse_bug44083 = this.navigator && /WebKit/.test(this.navigator.userAgent) ? -1 : 0;
D3支持五种颜色表示方式,除了咱们经常接触了rgb、hsl外,还有lab、hcl、cubehelix,它们之间均可以转化为rgb,内部的实现方式值得参考:
【line 1582】
function d3_hsl_rgb(h, s, l) { var m1, m2; h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h; s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s; l = l < 0 ? 0 : l > 1 ? 1 : l; m2 = l <= .5 ? l * (1 + s) : l + s - l * s; m1 = 2 * l - m2; function v(h) { if (h > 360) h -= 360; else if (h < 0) h += 360; if (h < 60) return m1 + (m2 - m1) * h / 60; if (h < 180) return m2; if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; return m1; } function vv(h) { return Math.round(v(h) * 255); } return new d3_rgb(vv(h + 120), vv(h), vv(h - 120)); }
看代码的好处之一是能看到不少平时不会用到的接口,而后会主动去了解是干什么的。
csv格式
在文本数据处理和传输过程当中,咱们经常遇到把多个字段经过分隔符链接在一块儿的需求,如采用著名的CSV格式(comma-separated values)。CSV文件的每一行是一条记录(record),每一行的各个字段经过逗号','分隔。
dsv格式
因为逗号和双引号这两个特殊字符的存在,咱们不能简单地经过字符串的split操做对CSV文件进行解析,而必须进行CSV语法分析。虽然咱们能够经过库的形式进行封装,或者直接采用现成的库,但毕竟各类平台下库的丰富程度差别很大,这些库和split、join这样的简单字符串操做相比也更加复杂。为此,咱们在CSV格式的基础上设计了一种DSV (double separated values)格式。DSV格式的主要设计目的就是为了简化CSV语法,生成和解析只须要replace, join, split这3个基本的字符串操做,而不须要进行语法分析。
DSV的语法很是简单,只包括如下两点:
经过双竖线'||'做为字段分隔符
把字段值中的'|'替换为'_|'进行转义
tsv格式
TSV 是Tab-separated values的缩写,即制表符分隔值。
查询网上关于这三种格式的定义是如上所示,不过d3的实现不太同样,dsv是能够定义为任何一种分隔符,可是分隔符只能为长度为1的字符,csv是以半角符逗号做为分割符,tsv则是以斜杠做为分隔符。
【line 2854】
geo是d3的图形处理实现,应该算是核心代码了,不过到了4.0版本被分割成依赖,而且再也不有d3.geo.path了,而是改用d3.geoPath的方式去引用。
版本3的d3九千多行代码,版本4的d4则进行了依赖分割,若是所有依赖引入的话不压缩就要过16000行了,若是想总体去看骨架的话,版本3是比较清晰的,版本4则适合深刻研究每一部分的实现,由于依赖都分割得很清晰了,而且相互独立开。
初步了解整个d3的骨架后,接下来能够深刻到代码函数实现中去研究其中奥妙。