• 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上同样可以适用; --自己没有这样实验过

  • 相关阅读:
    N 种仅仅使用 HTML/CSS 实现各类进度条的方式
    使用 CSS 轻松实现一些高频出现的奇形怪状按钮
    通过jdb命令连接远程调试的方法
    (转)Python中asyncio与aiohttp入门教程
    (转)从0到1,Python异步编程的演进之路
    (转)python︱用asyncio、aiohttp实现异步及相关案例
    (转)Python 如何仅用5000 行代码,实现强大的 logging 模块?
    (转)python的dict()字典数据类型的方法详解以及案例使用
    (转)Python Async/Await入门指南
    (转)Python运维自动化psutil 模块详解(超级详细)
  • 原文地址:https://www.cnblogs.com/liuyingde/p/13785494.html
Copyright © 2020-2023  润新知