D3 源代碼解構

D3是一個數據可視化的javascript庫,相對於highchart和echarts專一圖表可視化的庫,D3更適合作大數據處理的可視化,它只提供基礎的可視化功能,靈活而豐富的接口讓咱們能開發出各式各樣的圖表。javascript

D3代碼版本:「3.5.17」html

D3的代碼骨架比較簡潔,相比jquery來講更適合閱讀,你能夠很舒服地自上而下的看下去而不用看到一個新的函數發現聲明在千里以外,而後在代碼中跳來跳去。java

內部代碼流水線

  • 基本的數學計算:最小最大、均值中值方差、偏分值……node

  • 各類集合類型: map、set、nest……jquery

  • 集合的操做、方法: text、html、append、insert、removegit

  • d3的dragginggithub

  • 圖形操做算法

  • ……json

自執行匿名函數

首先是典型的自執行匿名函數,對外提供接口,隱藏實現方式,實現私有變量等等功能。數組

!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的骨架後,接下來能夠深刻到代碼函數實現中去研究其中奧妙。

相關文章
相關標籤/搜索