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;
}
複製程式碼