D3 源代码解构

D3是一个数据可视化的javascript库,相关于highchart和echarts专注图表可视化的库,D3更合适做大数据处置惩罚的可视化,它只供应基础的可视化功用,天真而雄厚的接口让我们能开发出形形色色的图表。

D3代码版本:“3.5.17”

D3的代码骨架比较简约,比拟jquery来讲更合适浏览,你能够很惬意地自上而下的看下去而不必看到一个新的函数发明声明在千里以外,然后在代码中跳来跳去。

内部代码流水线

  • 基础的数学盘算:最小最大、均值中值方差、偏分值……

  • 种种鸠合范例: map、set、nest……

  • 鸠合的操纵、要领: text、html、append、insert、remove

  • d3的dragging

  • 图形操纵

  • ……

自实行匿名函数

起首是典范的自实行匿名函数,对外供应接口,隐蔽完成体式格局,完成私有变量等等功用。

!function() {
   // code here
}()

这里用到的是感叹号,实在和运用括号是一样的作用,就是将函数声明变成函数表达式,以便于函数的自实行挪用,你能够尝尝

function() {
  console.log('no console')
}()

这是因为JS制止函数声明和函数挪用混用,而括号、逻辑运算符(+、-、&&、||)、逗号、new等都能够将函数声明变成函数表达式,然后便能够自实行。有人做过观察关于这些转化的要领哪一个更快,能够检察这篇博客,也许new是最慢的,比拟运用括号是基础最快,感叹号反而机能平常,所以实在用哪一个都没什么区别,固然假如你想省敲一个标记也是能够用感叹号的。

对外暴露私有变量d3

关于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不会对原生数组做切割,而是会返回数组的复制品,然则要注意是浅复制,关于数组中的对象、数组,是纯真的援用,所以对原数组中的对象或数组的变动照样会影响到复制品。

部份代码完成浏览

一段用来测试d3_array的函数,但什么情况下会重写d3_array函数呢?

【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的洗牌要领

  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。

d3.map 关于内置对象

【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轮回中会被疏忽,如许的话就不必再写额外埠前提去推断是不是为内置属性,很棒的完成体式格局。

数据绑定函数data

还记得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去猎取别的两个鸠合。

关于后期debug的萍踪

d3也会有bug的时刻,这个时刻须要对bug举行修复,然后再更新,为了轻易下次找到修正的bug,在代码里面临其举行定名,是很好的做法:

【1167】

var d3_mouse_bug44083 = this.navigator && /WebKit/.test(this.navigator.userAgent) ? -1 : 0;

D3的色彩空间

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、dsv、tsv存储体式格局

看代码的优点之一是能看到许多日常平凡不会用到的接口,然后会主动去相识是干什么的。

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则是以斜杠作为分开符。

d3.geo

【line 2854】

geo是d3的图形处置惩罚完成,应当算是中心代码了,不过到了4.0版本被支解成依靠,而且不再有d3.geo.path了,而是改用d3.geoPath的体式格局去援用。

总结

版本3的d3九千多行代码,版本4的d4则举行了依靠支解,假如悉数依靠引入的话不紧缩就要过16000行了,假如想团体去看骨架的话,版本3是比较清楚的,版本4则合适深切研讨每一部份的完成,因为依靠都支解得很清楚了,而且互相自力开。

开端相识全部d3的骨架后,接下来能够深切到代码函数完成中去研讨个中玄妙。

    原文作者:chenjsh36
    原文地址: https://segmentfault.com/a/1190000005893218
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞