d3 使用记录: Selection

假设有这么一个需求: 给定一组数据,然后要绘制一个可交互的,可动态变化,并拥有良好性能的图形。这个图形可能是条形图,散点图,树或是其他任何形式。限你两个星期做出来 --咳咳..

也许为了能快速应对,会对当前的某一个图具体的去画出来(自己也这么搞过)。但毕竟只是权宜之计,有没有一种方式可以满足上述需求呢? d3 便是一个答案

可交互: 可以为图形的任意位置,添加任意类型的事件交互,也许是键盘按键,或是鼠标按下/滚动

动态变化: 数据的更替,图形也即需要对应刷新;

      这种场景我们可以把它再细分:

      新增的数据就在图形中加一个

      不要的数据就对应删掉

      已经有的数据,但值有变化,更新它的值

良好性能: 分片 + 异步 + 缓存, 都用上

图形: 不画就行了,用的人去画。大不了提供一些图行算法

这里要说的 Selection - data,就是数据和图形元素的关联关系。从一个例子出发

var selection = d3.selectAll('.bar').data([2,3,4,5,6]);
selection
    .enter()
        .append('div')
    .merge(selection)
        .attr('class', 'bar')
        .style('background', 'green')
        .style('width', '200px')
        .style('height', '40px')
        .style('margin-bottom', '2px')
d3.selectAll('.bar').data([2,3,4,5,6]) 这一串代码便完成了图形和数据的关联

  selection.enter() 选中新增的数据节点
  selection.exit() 选中删除的数据节点
  selection.merge(selection2) 将选择集 selection2合并到 selection, 这个过程不会对 selection 插入新节点, 只会执行替换
  selection.attr() 遍历 selection的节点, 对每一个节点设置属性
  selection.style() 遍历 selection的节点, 对每一个节点设置样式

简单介绍了它的作用,下面来细说下它的实现。

d3.selectAll('.bar') 实际调用的

  var root = [null];

//
d3.selectAll --> selectAll
// 该方法返回一个 Selection 实例对象, 创建一个拥有 _groups, _parents属性的对象
// _groups: 保存着选择的子节点,
// 如果传递的 selector 是字符串,返回该字符串选中的 dom节点,
// 如果没有传递 selector, 返回空数组,
// 如果传递的是其他, 返回传递的对象本身
// _parents 保存着选择的父节点,
// 如果传递的 selector 是字符串, 返回 document,
// 如果传递的 selector 不是字符串, 返回 null
// 所以例子返回的是一个 _parents 为 document, _groups 为所有类名为 .bar的 NodeList 所组成的一个对象
function selectAll(selector) {
    return typeof selector === "string"
        ? new Selection([document.querySelectorAll(selector)], [document.documentElement])
        : new Selection([selector == null ? [] : selector], root);
}

// 关于 Selection, 实例本身只有 _groups, _parents 两个属性, 原型对象中有操作数据,操作 dom节点等方法 function Selection(groups, parents) { this._groups = groups; this._parents = parents; } Selection.prototype = selection.prototype = { constructor: Selection, select: selection_select, selectAll: selection_selectAll, filter: selection_filter, data: selection_data, enter: selection_enter, exit: selection_exit, join: selection_join, merge: selection_merge,
append: selection_append, ... }
最终返回的一个  { _groups: [NodeList], _parents: [document] } 对象
d3.selectAll('.bar').data([2,3,4,5,6]) 也即调用的 Selection原型上的 selection_data 方法;
function constant$1(x) {
    return function() {
        return x;
    };
}
function selection_data(value, key) {
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$1(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为一个函数, 这时候就会拿去调用父元素, 将调用函数的返回结果作为 data值 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); // Now connect the enter nodes to their following update node, such that // appendChild can insert the materialized enter node before this node, // rather than at the end of the parent node. 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; } } } update = new Selection(update, parents); update._enter = enter; update._exit = exit; return update; }
函数最终返回一个新的 Selection实例,新实例的 _parent属性保持不变, _groups属性被替换成了待更新的所有节点 update;
并且为新实例额外创建了 _enter, _exit 属性,分别保存着新增加的节点和多出来的节点

上述代码中首先定义了
update 为一个长度和子节点同样长度的空数组, 然后再重新赋值为一个和给定数据(假定为 dataList)同样长度的空数组,
enter 为一个长度和子节点同样长度的空数组, 然后再重新赋值为一个和给定数据同样长度的空数组,
exit 为一个长度和子节点 _groups同样长度的空数组,然后再重新赋值为一个和 _groups同样长度的空数组,
注意的是, 当我们使用常规的 d3.selectAll('.bar') 方式创建出来的 Selection实例, _groups属性是 [QuerySelectAll('.bar')] 的一个二维数组, 因此 上面的循环也只会走一次;
var m = groups.length, 
    update = new Array(m), 
    enter = new Array(m), 
    exit = new Array(m)


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);
因为这里我们并没有传入第二个参数,所以 bind即为 bindIndex函数,也即
function bindIndex(parent, group, enter, update, exit, data) {
  var i = 0,
      node,
      groupLength = group.length,
      dataLength = data.length;

  // Put any non-null nodes that fit into update.
  // Put any null nodes into enter.
  // Put any remaining data into enter.
  for (; i < dataLength; ++i) {
    if (node = group[i]) {
      node.__data__ = data[i];
      update[i] = node;
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // Put any non-null nodes that don’t fit into exit.
  for (; i < groupLength; ++i) {
    if (node = group[i]) {
      exit[i] = node;
    }
  }
}
 
_groups    dataList
遍历 dataList,
如果 _groups里面有节点,update插入当前节点, 并更新节点属性 __data__
如果 _groups里面没有节点, enter插入一个新的 EnterNode实例
以 dataList结束点为起点, 开始遍历 _groups
如果有节点, exit插入当前节点
由此步骤, 就拿到了 要新增的节点, 待更新的节点, 和多出来的节点

selection_data 方法最后, 给新实例对象添加 _enter, _exit两个属性, 分别保存着 新增的节点, 多出来的节点;
自此, 便完成了数据和节点的关联过程。

而 selection[enter | exit], 也即是返回一个以 [_enter | _exit] 为 _groups(子节点), 以本身父节点为 _parent(父节点)的一个新的 Selection实例。


 额外说一下 merge, join; 

  merge对应的源码:

function selection_merge(selection) {
  // _groups => [nodeList] 一般情况是一个二维数组, 因此这里只会迭代一次
// 取本身和待合并的节点的交集, 创建一个交集组成的空数组
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) {
// 对 nodeList的迭代, 创建一个跟本身同样长度的空数组, merges[i] = new Array(n), 并且创建一个 merge变量引用该数组
for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
   // 在本身 nodeList和待合并对象 nodeList上取值, 优先取本身, 再取待合并对象; 如果都没有则跳过
if (node = group0[i] || group1[i]) { merge[i] = node; } } } // 以上次循环的结束点为起点, 以本身子节点的长度为终点, 将未合并操作的节点加进来 for (; j < m0; ++j) { merges[j] = groups0[j]; } return new Selection(merges, this._parents); }

方法最终返回一个以 对象本身节点与待合并对象节点 "合并操作"后的结果 -> 作为节点, 以对象本身父节点为父节点, 组成的一个新的 Selection对象;

由于这一个判断

if (node = group0[i] || group1[i]) {

这会导致一个现象: 对已存在的节点进行合并 merge(newData)之后再重绘时, 节点不生效(可以直接绘制不 merge)。 在使用时这一点是需要注意的,比如:

var arr = [20,23,199,23,12,350,22,10]
var oldSelect = d3.selectAll('.v-bar').data(arr);
oldSelect.enter()
             .append('div')
             .style('width', '100px')
             .attr("class", 'v-bar')
             .text(d=>'old' + d)


var newArr = ['23', 123, 111, 112, 22];
var newSelect = d3.selectAll('.v-bar').data(newArr);
newSelect.text(v
=> 'new' + v) newSelect.exit().remove()

// var newSelect = d3.selectAll('.v-bar').data(newArr).merge(oldSelect)
// 如果使用的 merge, 根据上面的代码分析, 返回的是一个包含 newArr.length 个节点且数据绑定为 newArr的选择集
// 并不会返回个带有 _exit属性, 或 _enter属性 Selection对象, 因此如果接着用 exit(), enter() 是无法生效的

join对应的源码

function selection_join(onenter, onupdate, onexit) {
  var enter = this.enter(), update = this, exit = this.exit();

  enter = typeof onenter === "function" ? onenter(enter) : enter.append(onenter + "");
  if (onupdate != null) update = onupdate(update);
  if (onexit == null) exit.remove(); else onexit(exit);
  return enter && update ? enter.merge(update).order() : update;
}

根据源码推测它的用法: 同时接受三个参数

    onenter: 可以传入一个字符串, 便捷的插入dom节点; 也可以是一个函数, 对新增的节点执行操作

    onupdate: 如果传了值则只能是一个函数, 并将自身作为参数,函数调用的返回结果会作为一个 Selection , 和新增的节点组成的 Selection进行合并。

    onexit: 如果传了值会作为函数调用, 并将多余的节点作为参数; 如果没传值多余的节点将被删除

方法最终返回一个新的经过排序后的 Selection或者本身。

根据上述 数据和图形的关联介绍, 可以发现 d3所建立的 数据-图形 的关联概念 是很通用的。

我们可以操作 svg来绘图, d3操作 dom非常友好;

同样也可以用 d3.selectAll(null) 的方式来作为一个载体, 同样通过 [enter | exit] 的方式来选择 '绘图对象', 通过对 '绘图对象'的更新去重绘画布,在 canvas上同样可以适用; --自己没有这样实验过

原文地址:https://www.cnblogs.com/liuyingde/p/13785494.html