d3.js学习笔记

Posted wlbreath

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了d3.js学习笔记相关的知识,希望对你有一定的参考价值。

很早之前就知道d3,当初看到它的时候,第一反应就是“我去,怎么这么炫酷“,但是一直没有学(自己太懒了),最近可能是五月病犯了,不想看书,不想写代码,不想看论文,不想写论文,虽然什么事情都不想做,不过还是找点事情做吧,那就学学d3呗,说不定将来什么时候就用到了呢。

这篇博客主要从源码的角度去学习,所以对于没有接触过d3的朋友,请先看完下面的资料。

学习资料

学习嘛,当然得找到好的资料咯,下面是我昨天看的一些文章,在d3的github上都能够找到,毕竟最好的学习资料就是官网的文档、教程和源代码了。

下面是我现在看的文章,每篇文章都不长,而且写的都很好,值的推荐,如果快的话一天就看完了。

简单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是如何工作的。

对于动画的部分,因为还没有分析,所以这里就不写了,会在以后的博客中写的。

还是老话,希望每天能够进步一点,对了,本人现在在合肥读书,暑假的时候希望能找一份靠谱的前端实习工作,觉得我还可以的话,把我带走吧,哈哈哈哈。

以上是关于d3.js学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

d3.js学习笔记

d3.js学习笔记

d3.js学习笔记—— Transition

d3.js学习笔记—— Transition

精通D3.js学习笔记基础的函数

精通D3.js学习笔记比例尺和坐标