在web开发中咱们会花大量的时间用于dom的操做上,通常状况下咱们会选择第三方库如jQuery来替代原生的方法,由于原生的方法在操做上会使代码大量重复冗余而不易操做。d3一样提供了一套本身的方法来很方便的对dom进行操做,像修改样式、注册事件等等均可以经过它来完成。html
d3的selection主要用于直接对DOM进行操做,如设置属性、修改样式等等,同时它能够和data join(数据链接)这一强大的功能结合起来对元素和元素上绑定的数据进行操做。 selection中的方法计算后返回当前selection,这样能够进行方法的链式调用。因为经过这种方式调用方法会使得每行的代码很长,所以按照约定:若该方法返回的是当前的selection,则使用四个空格进行缩进;若方法返回的是新的selection,则使用两个空格进行缩进。node
元素的选择经过两种方法select
和selectAll
来实现,前者只返回第一个匹配的元素,然后者返回全部匹配元素。 因为以后全部的操做都是在selection上进行,而该对象则是经过Selection构造函数获得的,源码以下:web
function Selection(groups, parents) {
this._groups = groups;
this._parents = parents;
}
复制代码
能够看出,selection对象包含两个基本属性_groups
和_parents
,前者用于存储结点组,然后者则存储结点的父节点信息。数组
var p = d3.selectAll('div')
.select('p');
复制代码
该方法对selection中的每一个元素进行查找,选择其中第一个匹配selector的子元素,源码以下:bash
/*
* Selection的select方法
* 经过select方法选择时,若不存在元素,则会在数组中将该位置留出(赋值为null)用于以后插入时使用。
*/
function selection_select(select) {
if (typeof select !== "function") select = selector(select);
//当select是函数时,直接调用该函数,并依次对该函数传入data信息、当前的索引和当前的结点group,同时将函数的this设置为当前dom对象
for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
//当node中有data信息时,node的子元素也添加该data信息
if ("__data__" in node) subnode.__data__ = node.__data__;
subgroup[i] = subnode;
}
}
}
//将父级的_parents属性做为子元素的_parents属性
return new Selection(subgroups, this._parents);
}
复制代码
若selector为选择器字符串时,则会先调用selector方法将其转化为函数,源码以下:app
function selector(selector) {
return selector == null ? none$2 : function() {
//只返回第一个选中元素
return this.querySelector(selector);
};
}
复制代码
能够看出,其内部调用的是js的原生方法querySelector
。 上述代码对select参数进行处理后,使得其转化为函数,在后来的循环中调用时,经过select.call
进行调用,传入的参数依次为结点的__data__属性值,结点在该结点组中的索引,该结点组。 有如下几点值得注意:dom
__data__
属性则会对匹配的子元素也设置该属性。_parents
值并不会改变。var p = d3.selectAll('div')
.selectAll('p');
复制代码
该方法对selection中的每一个元素进行查找,选择其中匹配selector的子元素,返回的selection中的元素根据其父结点进行对应的分组,源码以下:svg
/*
* 对selection进行selectAll计算会改变selection结构,parents也会改变
*/
function selection_selectAll(select) {
if (typeof select !== "function") select = selectorAll(select);
for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
if (node = group[i]) {
subgroups.push(select.call(node, node.__data__, i, group));
parents.push(node);
}
}
}
return new Selection(subgroups, parents);
}
复制代码
上述代码能够看出,对node调用select方法后,查找到的结果存入subgroups中,同时将node做为父结点存入parents数组中,使得结点与父结点一一对应,最终返回新的selection。函数
var red = d3.selectAll('p')
.filter('.red')
复制代码
将使得filter为true的元素构形成新的selection并返回,源码以下:ui
//filter方法对当前selection进行过滤,保留知足条件的元素
function selection_filter(match) {
if (typeof match !== "function") match = matcher$1(match);
for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
subgroup.push(node);
}
}
}
return new Selection(subgroups, this._parents);
}
复制代码
若match
不为函数,则经过matcher$1
函数对其进行处理,其源码以下:
var matcher = function(selector) {
return function() {
//Element.matches(s),若是元素能经过s选择器选择到则返回true;不然返回false
return this.matches(selector);
};
};
复制代码
可看出matcher
函数内部是调用原生的Element.matches
方法实现。
var circle = svg.selectAll("circle").data(data) // UPDATE
.style("fill", "blue");
circle.exit().remove(); // EXIT
circle.enter().append("circle") // ENTER
.style("fill", "green")
.merge(circle) // ENTER + UPDATE
.style("stroke", "black");
复制代码
该方法将两个selection进行合并成一个新的selection并返回,源码以下:
function selection_merge(selection) {
//新的selection的_.groups长度和groups0相同,合并时只在m范围内计算
for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
//groups0数组大小不变,只有在group0[i]不存在,group1[i]存在时才选择group1[i]
if (node = group0[i] || group1[i]) {
merge[i] = node;
}
}
}
// 若m1 < m0,则将groups0剩余的复制过来
for (; j < m0; ++j) {
merges[j] = groups0[j];
}
return new Selection(merges, this._parents);
}
复制代码
该方法实际上至关于对this selection中的空元素进行填充。
在选择元素以后可使用selection的方法来修改元素,如样式、属性等。
var p = d3.selectAll('p')
.attr('class', 'red');
复制代码
对selection中的元素以指定的name和value设置属性,并返回当前selection,源码以下:
function selection_attr(name, value) {
var fullname = namespace(name);
if (arguments.length < 2) {
//获得selection中第一个存在的元素
var node = this.node();
return fullname.local
? node.getAttributeNS(fullname.space, fullname.local)
: node.getAttribute(fullname);
}
return this.each((value == null
? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === "function"
? (fullname.local ? attrFunctionNS : attrFunction)
: (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));
}
复制代码
若只有name参数时,则返回第一个存在的元素的name属性值,调用的是原生的Element.getAttribute
方法。 当有两个参数时,调用selection.each
方法对selection中的每一个元素进行操做,其源码以下:
function selection_each(callback) {
for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
if (node = group[i]) callback.call(node, node.__data__, i, group);
}
}
return this;
}
复制代码
若value值为函数,则将name和value传入attrFunction函数中进行处理。
function attrFunction(name, value) {
return function() {
var v = value.apply(this, arguments);
if (v == null) this.removeAttribute(name);
else this.setAttribute(name, v);
};
}
复制代码
在selection.each
方法中,将参数传入value函数中,根据value返回结果选择设置和删除属性操做。
var p = d3.selectAll('p')
.classed('red warn', true);
复制代码
对selection中的元素设置类名,源码以下:
// 当value为真值时,在全部元素类名中添加name;不然删除name
function selection_classed(name, value) {
var names = classArray(name + "");
// 当只有name参数时,判断该selection对象的_groups里第一个存在的结点是否包含全部的name的类名,若是是则返回true;不然,返回false
if (arguments.length < 2) {
var list = classList(this.node()), i = -1, n = names.length;
while (++i < n) if (!list.contains(names[i])) return false;
return true;
}
return this.each((typeof value === "function"
? classedFunction : value
? classedTrue
: classedFalse)(names, value));
}
复制代码
其中classArray
方法是将类名字符串拆分红数组:
// 将类名拆分红数组,如'button button-warn' => ['button', 'button-warn']
function classArray(string) {
return string.trim().split(/^|\s+/);
}
复制代码
var p = d3.selectAll('p')
.style('color', 'red');
复制代码
该方法对selection中的元素设置样式,源码以下:
// 设置selection的样式,注意样式的单位问题
function selection_style(name, value, priority) {
var node;
return arguments.length > 1
? this.each((value == null
? styleRemove : typeof value === "function"
? styleFunction
: styleConstant)(name, value, priority == null ? "" : priority))
: window(node = this.node())
.getComputedStyle(node, null)
.getPropertyValue(name);
}
复制代码
从上述代码能够看出,获取样式是经过window.getComputedStyle(element).getPropertyValue(name)
来获得(该方法获得的值是只读的),而删除样式则是经过element.style.removeProperty(name)
来实现,设置属性经过element.style.setProperty(name, value)
来实现。
var checkbox = d3.selectAll('input[type=checkbox]')
.property('checked', 'checked');
复制代码
该方法设置一些特殊的属性。
function selection_property(name, value) {
return arguments.length > 1
? this.each((value == null
? propertyRemove : typeof value === "function"
? propertyFunction
: propertyConstant)(name, value))
: this.node()[name];
}
function propertyRemove(name) {
return function() {
delete this[name];
};
}
function propertyConstant(name, value) {
return function() {
this[name] = value;
};
}
function propertyFunction(name, value) {
return function() {
var v = value.apply(this, arguments);
if (v == null) delete this[name];
else this[name] = v;
};
}
复制代码
内部经过直接修改元素的属性来实现。
该方法对selection中的全部元素设置文本内容,同时会替换掉元素中的子元素,源码以下:
function textRemove() {
this.textContent = "";
}
function textConstant(value) {
return function() {
this.textContent = value;
};
}
function textFunction(value) {
return function() {
var v = value.apply(this, arguments);
this.textContent = v == null ? "" : v;
};
}
// 设置元素的textContent属性,该属性返回的是元素内的纯文本内容,不包含结点标签(但包含标签内的文本)
function selection_text(value) {
return arguments.length
? this.each(value == null
? textRemove : (typeof value === "function"
? textFunction
: textConstant)(value))
: this.node().textContent;
}
复制代码
上述代码是经过element.textContent
方法来获取和修改文本内容。
对selection中的全部元素设置innerHTML。方法同上述selection.text
相似,只是经过element.innerHTML
来修改元素内的全部内容。
对selection中的元素添加新的元素。
/*
* selection的append方法
* 该方法返回新的Selection对象,
*/
function selection_append(name) {
var create = typeof name === "function" ? name : creator(name);
return this.select(function() {
//arguments是传入当前匿名函数的参数
return this.appendChild(create.apply(this, arguments));
});
}
function creatorInherit(name) {
return function() {
//ownerDocument返回当前document对象
var document = this.ownerDocument,
uri = this.namespaceURI;
return uri === xhtml && document.documentElement.namespaceURI === xhtml
? document.createElement(name)//建立dom对象
: document.createElementNS(uri, name);
};
}
复制代码
该方法中调用到了selection.select
方法,因为element.appendChild
方法返回的是该子结点,所以返回的新的selection包含的是全部添加的子结点。
对selection中的元素插入新的元素,同上述selection.append
方法相似,只是内部使用element.insertBefore
方法实现。
根据compare
函数对selection中的元素进行排序,排好序后按照排序结果对dom进行排序,返回排序后新建立的selection对象。
function selection_sort(compare) {
if (!compare) compare = ascending$2;
function compareNode(a, b) {
// 比较结点的data大小
return a && b ? compare(a.__data__, b.__data__) : !a - !b;
}
// copy一份selection中的_groups包含的结点
for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
if (node = group[i]) {
sortgroup[i] = node;
}
}
// 调用array的sort方法
sortgroup.sort(compareNode);
}
return new Selection(sortgroups, this._parents).order();
}
// 递增
function ascending$2(a, b) {
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}
复制代码
若没有compare参数,则默认以递增的方式排序。同时排序是首先比较元素中的__data__
属性值的大小,对selection排好序后调用order方法。
按照selection中每组内元素的顺序对dom进行排序
// 对dom结点进行排序
function selection_order() {
for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
if (node = group[i]) {
// 将node移至next的前面,并将node赋值给next
if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
next = node;
}
}
}
return this;
}
复制代码
链接数据是将数据绑定到selection对象上,其实是将数据存储到__data__
属性中,这样以后对selection的操做过程当中即可以直接使用绑定好的数据。主要要理解update
、enter
和exit
,可参考文章Thinking With Joins。
该方法将指定的data数组绑定到选中的元素上,返回的selection包含成功绑定数据的元素,也叫作updata selection
,源码以下:
function selection_data(value, key) {
//当value为假值时,将selection全部元素的__data__属性以数组形式返回
if (!value) {
data = new Array(this.size()), j = -1;
this.each(function(d) { data[++j] = d; });
return data;
}
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;
if (typeof value !== "function") value = constant$4(value);
for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = value.call(parent, parent && parent.__data__, j, parents),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// 对enter结点设置_next属性,存储其索引以后的第一个update结点
for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) i1 = i0 + 1;
while (!(next = updateGroup[i1]) && ++i1 < dataLength);
previous._next = next || null;
}
}
}
// 将enter和exit存入update selection的属性中
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
}
复制代码
经过上述代码能够看到,对每组group绑定的是相同的data数据。 当没有key参数时,绑定数据使用的是bindIndex
方法,按照索引一次绑定。
function bindIndex(parent, group, enter, update, exit, data) {
var i = 0,
node,
groupLength = group.length,
dataLength = data.length;
/*
* 将data数据绑定到node,并将该node存入update数组中
* 将剩余的data数据存入enter数组中
*/
for (; i < dataLength; ++i) {
if (node = group[i]) {
node.__data__ = data[i];
update[i] = node;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// 将剩余的node存入exit数组中
for (; i < groupLength; ++i) {
if (node = group[i]) {
exit[i] = node;
}
}
}
复制代码
若含有key参数,则调用bindKey
方法来绑定数据。
function bindKey(parent, group, enter, update, exit, data, key) {
var i,
node,
nodeByKeyValue = {},
groupLength = group.length,
dataLength = data.length,
keyValues = new Array(groupLength),
keyValue;
// 对group中每一个结点计算keyValue,若是以后的结点含有与前面结点相同的keyValue则将该结点存入exit数组中
for (i = 0; i < groupLength; ++i) {
if (node = group[i]) {
keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
if (keyValue in nodeByKeyValue) {
exit[i] = node;
} else {
nodeByKeyValue[keyValue] = node;
}
}
}
// 对每个data计算keyValue,若是该keyValue已存在nodeByKeyValue数组中,则将其对应的node存入update数组且绑定data数据;不然将data存入enter中
for (i = 0; i < dataLength; ++i) {
keyValue = keyPrefix + key.call(parent, data[i], i, data);
if (node = nodeByKeyValue[keyValue]) {
update[i] = node;
node.__data__ = data[i];
nodeByKeyValue[keyValue] = null;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// 将剩余的没有绑定数据的结点存入exit数组
for (i = 0; i < groupLength; ++i) {
if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
exit[i] = node;
}
}
}
复制代码
返回selections中的enter selection,即selection._enter
的结果。
function sparse(update) {
return new Array(update.length);
}
// selection的enter方法
function selection_enter() {
return new Selection(this._enter || this._groups.map(sparse), this._parents);
}
复制代码
若selection没有_enter
属性,即没有进行过data
操做,则建立空的数组。
返回selection中的exit selection,即selection._exit
的结果。
function selection_exit() {
return new Selection(this._exit || this._groups.map(sparse), this._parents);
}
复制代码
对selection中的每一个元素设置绑定数据,该方法并不会影响到enter
和exit
的值。
function selection_datum(value) {
return arguments.length
? this.property("__data__", value)
: this.node().__data__;
}
复制代码
可见其调用selection.property
方法来设置__data__
属性。 常常会使用该方法来进行HTML5 data属性的访问,如
selection.datum(function() {return this.dataset});
复制代码
element.dataset
是原生方法,返回的是元素绑定的全部data属性。
该方法用于对selection中的元素添加或者移除事件。
function selection_on(typename, value, capture) {
var typenames = parseTypenames$1(typename + ""), i, n = typenames.length, t;
// 若是只有typename参数,根据type和name值来找到selection中第一个存在的元素的__on属性中对应的value值。
if (arguments.length < 2) {
var on = this.node().__on;
if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
for (i = 0, o = on[j]; i < n; ++i) {
if ((t = typenames[i]).type === o.type && t.name === o.name) {
return o.value;
}
}
}
return;
}
// 若是value为真值,则添加事件;不然移除事件。
on = value ? onAdd : onRemove;
if (capture == null) capture = false;
for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
return this;
}
复制代码
首先是对typename参数进行处理,使用的是parseTypenames
函数。
// 对typenames根据空格来划分红数组,并根据分离后的字符串中的'.'来将该字符串分割为type和name部分,如:'click.foo click.bar' => [{type: 'click', name: 'foo'}, {type: 'click', name: 'bar'}]
function parseTypenames$1(typenames) {
return typenames.trim().split(/^|\s+/).map(function(t) {
var name = "", i = t.indexOf(".");
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
return {type: t, name: name};
});
}
复制代码
根据value是否为真值来选择添加或者移除事件。
// 添加事件函数
function onAdd(typename, value, capture) {
var wrap = filterEvents.hasOwnProperty(typename.type) ? filterContextListener : contextListener;
return function(d, i, group) {
var on = this.__on, o, listener = wrap(value, i, group);
if (on) for (var j = 0, m = on.length; j < m; ++j) {
// 若是新事件的type和name和以前已绑定的事件相同,则移除以前的事件并绑定新的事件
if ((o = on[j]).type === typename.type && o.name === typename.name) {
this.removeEventListener(o.type, o.listener, o.capture);
this.addEventListener(o.type, o.listener = listener, o.capture = capture);
o.value = value;
return;
}
}
// 添加事件,并将事件信息存入selection.__on属性中
this.addEventListener(typename.type, listener, capture);
o = {type: typename.type, name: typename.name, value: value, listener: listener, capture: capture};
if (!on) this.__on = [o];
else on.push(o);
};
}
复制代码
// 移除事件函数
function onRemove(typename) {
return function() {
var on = this.__on;
if (!on) return;
for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
//对结点移除事件
this.removeEventListener(o.type, o.listener, o.capture);
} else {
//修改结点的__on属性值
on[++i] = o;
}
}
if (++i) on.length = i;
//on为空则删除__on属性
else delete this.__on;
};
}
复制代码
对selection中的元素分派指定类型的自定义事件,其中parameters可能包含如下内容:
//分派事件
function selection_dispatch(type, params) {
return this.each((typeof params === "function"
? dispatchFunction
: dispatchConstant)(type, params));
}
function dispatchConstant(type, params) {
return function() {
return dispatchEvent(this, type, params);
};
}
function dispatchFunction(type, params) {
return function() {
return dispatchEvent(this, type, params.apply(this, arguments));
};
}
复制代码
dispatchEvent
函数以下:
//建立自定义事件并分派给指定元素
function dispatchEvent(node, type, params) {
var window$$ = window(node),
event = window$$.CustomEvent;
if (event) {
event = new event(type, params);
} else {
//该方法已被废弃
event = window$$.document.createEvent("Event");
if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
else event.initEvent(type, false, false);
}
node.dispatchEvent(event);
}
复制代码
存储当前事件,在调用事件监听器的时候设置,处理函数执行完毕后重置,能够获取其中包含的事件信息如:event.pageX
。
该方法调用指定的监听器。
//调用指定的监听器
function customEvent(event1, listener, that, args) {
//记录当前事件
var event0 = exports.event;
event1.sourceEvent = exports.event;
exports.event = event1;
try {
return listener.apply(that, args);
} finally {
//监听器执行完后恢复事件
exports.event = event0;
}
}
复制代码
返回当前当前事件相对于指定容器的x和y坐标。
function mouse(node) {
var event = sourceEvent();
//若是是触摸事件,返回Touch对象
if (event.changedTouches) event = event.changedTouches[0];
return point$5(node, event);
}
复制代码
point$5
方法用于计算坐标。
function point$5(node, event) {
//若node是svg元素,则获取svg容器
var svg = node.ownerSVGElement || node;
if (svg.createSVGPoint) {
//建立SVGPoint对象
var point = svg.createSVGPoint();
//将事件相对客户端的x和y坐标赋值给point对象
point.x = event.clientX, point.y = event.clientY;
//进行坐标转换
point = point.matrixTransform(node.getScreenCTM().inverse());
return [point.x, point.y];
}
var rect = node.getBoundingClientRect();
//返回event事件相对于容器的坐标
return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
}
复制代码
用于selection的一些高级操做。
为每个选中的元素调用指定的函数。
//selection的each方法
function selection_each(callback) {
for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
//经过call方式调用回调函数
if (node = group[i]) callback.call(node, node.__data__, i, group);
}
}
return this;
}
复制代码
将selection和其余参数传入指定函数中执行,并返回当前selection,这和直接链式调用function(selection)
相同。
function selection_call() {
var callback = arguments[0];
arguments[0] = this;
callback.apply(null, arguments);
return this;
}
复制代码
d3的局部变量能够定义与data独立开来的局部状态,它的做用域是dom元素。 其构造函数和原型方法以下:
function Local() {
this._ = "@" + (++nextId).toString(36);
}
Local.prototype = local.prototype = {
constructor: Local,
get: function(node) {
var id = this._;
while (!(id in node)) if (!(node = node.parentNode)) return;
return node[id];
},
set: function(node, value) {
return node[this._] = value;
},
remove: function(node) {
return this._ in node && delete node[this._];
},
toString: function() {
return this._;
}
};
复制代码