D3原始碼解讀系列之Hierarchies

arlendp2012發表於2019-11-01

d3的hierarchy模組用於層級圖的計算,會將輸入的資料計算並轉換成指定的層級格式提供給開發者使用。為了表示這種資料間的層級關係,該模組在內部使用了四叉樹這種資料結構。

Hierarchy

用於計算層級資料,在層級圖中使用。

d3.hierarchy

/**
   * 處理層級資料
   * @param  {object} data     輸入的資料
   * @param  {function} children 用於獲取data中的children資料的函式
   * @return {object}          處理後的層級資料
   */
function hierarchy(data, children) {
    var root = new Node(data),
        valued = +data.value && (root.value = data.value),
        node,
        nodes = [root],
        child,
        childs,
        i,
        n;

    if (children == null) children = defaultChildren;
    //先處理父節點,後處理子節點,構造成node物件
    while (node = nodes.pop()) {
      if (valued) node.value = +node.data.value;
      if ((childs = children(node.data)) && (n = childs.length)) {
        node.children = new Array(n);
        for (i = n - 1; i >= 0; --i) {
          nodes.push(child = node.children[i] = new Node(childs[i]));
          child.parent = node;
          child.depth = node.depth + 1;
        }
      }
    }

    return root.eachBefore(computeHeight);
}
複製程式碼

node用於表示hierarchy中的節點物件。

/* node建構函式
   * data: 該node相關聯的資料
   * depth: 該節點所在的層級數,根節點為0,子節點逐漸遞增
   * height: 該節點與其最遠的子節點之間的距離,葉子節點為0
   * parent: 該節點的父節點
   */
function Node(data) {
    this.data = data;
    this.depth =
    this.height = 0;
    this.parent = null;
}
複製程式碼

node的原型方法:

//返回當前節點的所有父級節點,以當前節點開始逐級向上查詢直至根節點
  function node_ancestors() {
    var node = this, nodes = [node];
    while (node = node.parent) {
      nodes.push(node);
    }
    return nodes;
  }
  //返回當前節點的所有子節點,包括當前節點
  function node_descendants() {
    var nodes = [];
    this.each(function(node) {
      nodes.push(node);
    });
    return nodes;
  }
  //返回當前節點包含的所有葉子節點
  function node_leaves() {
    var leaves = [];
    this.eachBefore(function(node) {
      if (!node.children) {
        leaves.push(node);
      }
    });
    return leaves;
  }
  //以節點的父節點和本身構成一個新的物件,返回包含所有該種物件的陣列
  function node_links() {
    var root = this, links = [];
    root.each(function(node) {
      if (node !== root) { // Don’t include the root’s parent, if any.
        links.push({source: node.parent, target: node});
      }
    });
    return links;
  }
複製程式碼

根據node的不同的遍歷方式而有不同的方法

//廣度優先???
  function node_each(callback) {
    var node = this, current, next = [node], children, i, n;
    do {
      current = next.reverse(), next = [];
      while (node = current.pop()) {
        callback(node), children = node.children;
        if (children) for (i = 0, n = children.length; i < n; ++i) {
          next.push(children[i]);
        }
      }
    } while (next.length);
    return this;
  }
  // 先處理回撥函式,後訪問子節點
  function node_eachBefore(callback) {
    var node = this, nodes = [node], children, i;
    while (node = nodes.pop()) {
      callback(node), children = node.children;
      if (children) for (i = children.length - 1; i >= 0; --i) {
        nodes.push(children[i]);
      }
    }
    return this;
  }
  //先訪問所有節點,之後逐個執行回撥
  function node_eachAfter(callback) {
    var node = this, nodes = [node], next = [], children, i, n;
    while (node = nodes.pop()) {
      next.push(node), children = node.children;
      if (children) for (i = 0, n = children.length; i < n; ++i) {
        nodes.push(children[i]);
      }
    }
    while (node = next.pop()) {
      callback(node);
    }
    return this;
  }
複製程式碼

而在這些遍歷方法的基礎上構造出來的方法

//通過value函式對每個節點的data進行計算,節點的value值為其自己的value值加上所有子節點的value值之和。
  function node_sum(value) {
    return this.eachAfter(function(node) {
      var sum = +value(node.data) || 0,
          children = node.children,
          i = children && children.length;
      while (--i >= 0) sum += children[i].value;
      node.value = sum;
    });
  }
  //對所有節點的子節點進行排序,內部呼叫Array的原型鏈方法
  function node_sort(compare) {
    return this.eachBefore(function(node) {
      if (node.children) {
        node.children.sort(compare);
      }
    });
  }
  //計算當前node到end的最短路徑,返回的陣列從當前節點的父節點開始到公共節點,然後到目標節點
  function node_path(end) {
    var start = this,
        ancestor = leastCommonAncestor(start, end),
        nodes = [start];
    //從start開始向上查詢至ancestor
    while (start !== ancestor) {
      start = start.parent;
      nodes.push(start);
    }
    var k = nodes.length;
    while (end !== ancestor) {

      nodes.splice(k, 0, end);
      end = end.parent;
    }
    return nodes;
  }
  //返回最近的相同的祖先節點
  function leastCommonAncestor(a, b) {
    if (a === b) return a;
    var aNodes = a.ancestors(),
        bNodes = b.ancestors(),
        c = null;
    a = aNodes.pop();
    b = bNodes.pop();
    while (a === b) {
      c = a;
      a = aNodes.pop();
      b = bNodes.pop();
    }
    return c;
  }
  //複製一份相同的node
  function node_copy() {
    return hierarchy(this).eachBefore(copyData);
  }
  function copyData(node) {
    node.data = node.data.data;
  }
複製程式碼

Stratify

將資料轉化為層級形式。若資料格式已經是如下形式:

var data = {
        "name": "中國",
        "children": [{
                "name": "浙江",
                "children": [{
                    "name": "杭州"
                }, {
                    "name": "寧波"
                }, {
                    "name": "溫州"
                }, {
                    "name": "紹興"
                }]
            },

            {
                "name": "廣西",
                "children": [{
                    "name": "桂林"
                }, {
                    "name": "南寧"
                }, {
                    "name": "柳州"
                }, {
                    "name": "防城港"
                }]
            }]
    };
複製程式碼

則可以直接傳入上述d3.hierarchy方法來構造層級資料。若不是則用d3.stratify方法來處理,其關鍵部分是'id'和'parentId'方法。

//d3.stratify
function stratify() {
    var id = defaultId,
        parentId = defaultParentId;

    function stratify(data) {
      var d,
          i,
          n = data.length,
          root,
          parent,
          node,
          nodes = new Array(n),
          nodeId,
          nodeKey,
          nodeByKey = {};

      for (i = 0; i < n; ++i) {
        //將data中每個資料構造成node物件
        d = data[i], node = nodes[i] = new Node(d);
        //根據id函式獲取data的id
        if ((nodeId = id(d, i, data)) != null && (nodeId += "")) {
          nodeKey = keyPrefix$1 + (node.id = nodeId);
          nodeByKey[nodeKey] = nodeKey in nodeByKey ? ambiguous : node;
        }
      }

      for (i = 0; i < n; ++i) {
        node = nodes[i], nodeId = parentId(data[i], i, data);
        //parentId為空時認為該node為根節點,但只能存在一個parentId為空的節點
        if (nodeId == null || !(nodeId += "")) {
          if (root) throw new Error("multiple roots");
          root = node;
        } else {
          parent = nodeByKey[keyPrefix$1 + nodeId];
          //如果記錄中沒有parentId,則丟擲異常
          if (!parent) throw new Error("missing: " + nodeId);
          if (parent === ambiguous) throw new Error("ambiguous: " + nodeId);
          //將該節點新增至parentId對應節點的children屬性中
          if (parent.children) parent.children.push(node);
          else parent.children = [node];
          //為node節點新增parent屬性
          node.parent = parent;
        }
      }

      if (!root) throw new Error("no root");
      root.parent = preroot;
      //計算節點的depth和height值
      root.eachBefore(function(node) { node.depth = node.parent.depth + 1; --n; }).eachBefore(computeHeight);
      root.parent = null;
      if (n > 0) throw new Error("cycle");

      return root;
    }
    //設定獲取id的函式
    stratify.id = function(x) {
      return arguments.length ? (id = required(x), stratify) : id;
    };
    //設定獲取父節點id的函式
    stratify.parentId = function(x) {
      return arguments.length ? (parentId = required(x), stratify) : parentId;
    };

    return stratify;
}
複製程式碼

Cluster

用於繪製叢集圖,它會將所有的葉子節點放置在相同的深度,即所有的葉子節點會對齊。這裡返回的結果包含(x, y)座標,即可以直接用於繪製。

//d3.cluster
function cluster() {
    var separation = defaultSeparation,
        dx = 1,
        dy = 1,
        nodeSize = false;

    function cluster(root) {
      var previousNode,
          x = 0;

      root.eachAfter(function(node) {
        var children = node.children;
        if (children) {
          //計算非葉子節點的x、y座標,與其子節點相關
          node.x = meanX(children);
          node.y = maxY(children);
        } else {
          // 如果是葉子節點,則其y座標為0,x座標則根據當前節點與前一個節點是否含有相同的父節點來設定
          node.x = previousNode ? x += separation(node, previousNode) : 0;
          node.y = 0;
          previousNode = node;
        }
      });

      var left = leafLeft(root),
          right = leafRight(root),
          x0 = left.x - separation(left, right) / 2,
          x1 = right.x + separation(right, left) / 2;

      // 根據size大小對節點的x, y座標進行調整
      return root.eachAfter(nodeSize ? function(node) {
        //nodeSize為true時,將root放置於(0, 0)位置
        node.x = (node.x - root.x) * dx;
        node.y = (root.y - node.y) * dy;
      } : function(node) {
        //否則,按比例對節點座標進行調整
        node.x = (node.x - x0) / (x1 - x0) * dx;
        node.y = (1 - (root.y ? node.y / root.y : 1)) * dy;
      });
    }
    //separation用於將相鄰的葉子節點進行分離
    cluster.separation = function(x) {
      return arguments.length ? (separation = x, cluster) : separation;
    };
    //以陣列的形式設定cluster的範圍大小
    cluster.size = function(x) {
      return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? null : [dx, dy]);
    };

    cluster.nodeSize = function(x) {
      return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], cluster) : (nodeSize ? [dx, dy] : null);
    };

    return cluster;
}
複製程式碼

Tree

用於產生樹狀佈局,以根節點位置為基準,逐級對齊。


複製程式碼

Treemap

用於產生矩形式樹狀結構圖。

//d3.treemap
function index$1() {
    var tile = squarify,
        round = false,
        dx = 1,
        dy = 1,
        paddingStack = [0],
        paddingInner = constantZero,
        paddingTop = constantZero,
        paddingRight = constantZero,
        paddingBottom = constantZero,
        paddingLeft = constantZero;

    function treemap(root) {
      root.x0 =
      root.y0 = 0;
      root.x1 = dx;
      root.y1 = dy;
      root.eachBefore(positionNode);
      paddingStack = [0];
      if (round) root.eachBefore(roundNode);
      return root;
    }

    function positionNode(node) {
      var p = paddingStack[node.depth],
          //將有效區域根據padding來縮小
          x0 = node.x0 + p,
          y0 = node.y0 + p,
          x1 = node.x1 - p,
          y1 = node.y1 - p;
      //處理padding過大的情況
      if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
      if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
      node.x0 = x0;
      node.y0 = y0;
      node.x1 = x1;
      node.y1 = y1;
      if (node.children) {
        p = paddingStack[node.depth + 1] = paddingInner(node) / 2;
        //在調整了範圍的基礎上根據特定的padding再次進行調整
        x0 += paddingLeft(node) - p;
        y0 += paddingTop(node) - p;
        x1 -= paddingRight(node) - p;
        y1 -= paddingBottom(node) - p;
        if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
        if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
        tile(node, x0, y0, x1, y1);
      }
    }

    treemap.round = function(x) {
      return arguments.length ? (round = !!x, treemap) : round;
    };

    treemap.size = function(x) {
      return arguments.length ? (dx = +x[0], dy = +x[1], treemap) : [dx, dy];
    };
    //設定tile函式,預設是d3.treemapSquarify,即按黃金分割比進行劃分
    treemap.tile = function(x) {
      return arguments.length ? (tile = required(x), treemap) : tile;
    };
    //同時設定paddingInner和paddingOuter
    treemap.padding = function(x) {
      return arguments.length ? treemap.paddingInner(x).paddingOuter(x) : treemap.paddingInner();
    };

    treemap.paddingInner = function(x) {
      return arguments.length ? (paddingInner = typeof x === "function" ? x : constant$5(+x), treemap) : paddingInner;
    };
    //paddingOuter是由四個方向的padding構成
    treemap.paddingOuter = function(x) {
      return arguments.length ? treemap.paddingTop(x).paddingRight(x).paddingBottom(x).paddingLeft(x) : treemap.paddingTop();
    };

    treemap.paddingTop = function(x) {
      return arguments.length ? (paddingTop = typeof x === "function" ? x : constant$5(+x), treemap) : paddingTop;
    };

    treemap.paddingRight = function(x) {
      return arguments.length ? (paddingRight = typeof x === "function" ? x : constant$5(+x), treemap) : paddingRight;
    };

    treemap.paddingBottom = function(x) {
      return arguments.length ? (paddingBottom = typeof x === "function" ? x : constant$5(+x), treemap) : paddingBottom;
    };

    treemap.paddingLeft = function(x) {
      return arguments.length ? (paddingLeft = typeof x === "function" ? x : constant$5(+x), treemap) : paddingLeft;
    };

    return treemap;
}
複製程式碼

d3.treemap中會對node進行區塊劃分,其中主要是用到tile函式來實現模組劃分的邏輯,預設是d3.treemapSquarify即按黃金分割比進行區塊的分割。

d3.treemapSquarify

通過黃金分割比來對treemap進行分割。

//黃金分割比
var phi = (1 + Math.sqrt(5)) / 2;
//按黃金分割比對treenode區塊進行劃分
var squarify = (function custom(ratio) {

    function squarify(parent, x0, y0, x1, y1) {
      squarifyRatio(ratio, parent, x0, y0, x1, y1);
    }

    squarify.ratio = function(x) {
      return custom((x = +x) > 1 ? x : 1);
    };

    return squarify;
})(phi);

function squarifyRatio(ratio, parent, x0, y0, x1, y1) {
    var rows = [],
        nodes = parent.children,
        row,
        nodeValue,
        i0 = 0,
        i1,
        n = nodes.length,
        dx, dy,
        value = parent.value,
        sumValue,
        minValue,
        maxValue,
        newRatio,
        minRatio,
        alpha,
        beta;

    while (i0 < n) {
      dx = x1 - x0, dy = y1 - y0;
      minValue = maxValue = sumValue = nodes[i0].value;
      //根據長寬比和黃金分割比計算alpha,其值不受子節點影響
      alpha = Math.max(dy / dx, dx / dy) / (value * ratio);
      beta = sumValue * sumValue * alpha;
      minRatio = Math.max(maxValue / beta, beta / minValue);

      // 當ratio不增加時,新增node
      for (i1 = i0 + 1; i1 < n; ++i1) {
        sumValue += nodeValue = nodes[i1].value;
        if (nodeValue < minValue) minValue = nodeValue;
        if (nodeValue > maxValue) maxValue = nodeValue;
        beta = sumValue * sumValue * alpha;
        newRatio = Math.max(maxValue / beta, beta / minValue);
        //不會新增使ratio增加的node,如果不滿足退出迴圈
        if (newRatio > minRatio) { sumValue -= nodeValue; break; }
        minRatio = newRatio;
      }

      // 調整node的範圍並確定分割的方向
      rows.push(row = {value: sumValue, dice: dx < dy, children: nodes.slice(i0, i1)});
      //當node區域長比寬短時
      if (row.dice) treemapDice(row, x0, y0, x1, value ? y0 += dy * sumValue / value : y1);
      //當node區域寬比長短時
      else treemapSlice(row, x0, y0, value ? x0 += dx * sumValue / value : x1, y1);
      //value等於剩餘未分配位置的node的value之和,i0也從未分配的node開始
      value -= sumValue, i0 = i1;
    }

    return rows;
}
複製程式碼

d3.treemapBinary

將treemap區域根據value值大致進行二等分,使兩邊的value值之和儘量相近。

//d3.treemapBinary
function binary(parent, x0, y0, x1, y1) {
    var nodes = parent.children,
        i, n = nodes.length,
        sum, sums = new Array(n + 1);

    for (sums[0] = sum = i = 0; i < n; ++i) {
      sums[i + 1] = sum += nodes[i].value;
    }

    partition(0, n, parent.value, x0, y0, x1, y1);

    function partition(i, j, value, x0, y0, x1, y1) {
      if (i >= j - 1) {
        var node = nodes[i];
        node.x0 = x0, node.y0 = y0;
        node.x1 = x1, node.y1 = y1;
        return;
      }

      var valueOffset = sums[i],
          //加上前面的已經計算過的value值作為偏移量,這樣才能將sums[mid]跟valueTarget進行比較
          valueTarget = (value / 2) + valueOffset,
          k = i + 1,
          hi = j - 1;
      //二分法查詢
      while (k < hi) {
        var mid = k + hi >>> 1;
        if (sums[mid] < valueTarget) k = mid + 1;
        else hi = mid;
      }

      var valueLeft = sums[k] - valueOffset,
          valueRight = value - valueLeft;
      //當矩形較高時,進行上下分割
      if ((y1 - y0) > (x1 - x0)) {
        //根據左右的value和進行座標劃分
        var yk = (y0 * valueRight + y1 * valueLeft) / value;
        partition(i, k, valueLeft, x0, y0, x1, yk);
        partition(k, j, valueRight, x0, yk, x1, y1);
      } 
      //否則進行左右分割
      else {
        var xk = (x0 * valueRight + x1 * valueLeft) / value;
        partition(i, k, valueLeft, x0, y0, xk, y1);
        partition(k, j, valueRight, xk, y0, x1, y1);
      }
    }
}
複製程式碼

Partition

節點以矩形區域的形式展現,節點間的相對位置可以看出其層級關係。同時區塊的大小可以反映value值大小。

//d3.partition
function partition() {
    var dx = 1,
        dy = 1,
        padding = 0,
        round = false;

    function partition(root) {
      var n = root.height + 1;
      root.x0 =
      root.y0 = padding;
      root.x1 = dx;
      root.y1 = dy / n;
      root.eachBefore(positionNode(dy, n));
      if (round) root.eachBefore(roundNode);
      return root;
    }

    function positionNode(dy, n) {
      return function(node) {
        if (node.children) {
          treemapDice(node, node.x0, dy * (node.depth + 1) / n, node.x1, dy * (node.depth + 2) / n);
        }
        var x0 = node.x0,
            y0 = node.y0,
            //這裡減去padding用於與下個兄弟節點分開
            x1 = node.x1 - padding,
            y1 = node.y1 - padding;
        if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
        if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
        node.x0 = x0;
        node.y0 = y0;
        node.x1 = x1;
        node.y1 = y1;
      };
    }

    partition.round = function(x) {
      return arguments.length ? (round = !!x, partition) : round;
    };

    partition.size = function(x) {
      return arguments.length ? (dx = +x[0], dy = +x[1], partition) : [dx, dy];
    };
    //padding用於將節點的相鄰子節點分開
    partition.padding = function(x) {
      return arguments.length ? (padding = +x, partition) : padding;
    };

    return partition;
}
複製程式碼

相關文章