238 lines
6.9 KiB
JavaScript
238 lines
6.9 KiB
JavaScript
|
import {Node} from "./hierarchy/index.js";
|
|||
|
|
|||
|
function defaultSeparation(a, b) {
|
|||
|
return a.parent === b.parent ? 1 : 2;
|
|||
|
}
|
|||
|
|
|||
|
// function radialSeparation(a, b) {
|
|||
|
// return (a.parent === b.parent ? 1 : 2) / a.depth;
|
|||
|
// }
|
|||
|
|
|||
|
// This function is used to traverse the left contour of a subtree (or
|
|||
|
// subforest). It returns the successor of v on this contour. This successor is
|
|||
|
// either given by the leftmost child of v or by the thread of v. The function
|
|||
|
// returns null if and only if v is on the highest level of its subtree.
|
|||
|
function nextLeft(v) {
|
|||
|
var children = v.children;
|
|||
|
return children ? children[0] : v.t;
|
|||
|
}
|
|||
|
|
|||
|
// This function works analogously to nextLeft.
|
|||
|
function nextRight(v) {
|
|||
|
var children = v.children;
|
|||
|
return children ? children[children.length - 1] : v.t;
|
|||
|
}
|
|||
|
|
|||
|
// Shifts the current subtree rooted at w+. This is done by increasing
|
|||
|
// prelim(w+) and mod(w+) by shift.
|
|||
|
function moveSubtree(wm, wp, shift) {
|
|||
|
var change = shift / (wp.i - wm.i);
|
|||
|
wp.c -= change;
|
|||
|
wp.s += shift;
|
|||
|
wm.c += change;
|
|||
|
wp.z += shift;
|
|||
|
wp.m += shift;
|
|||
|
}
|
|||
|
|
|||
|
// All other shifts, applied to the smaller subtrees between w- and w+, are
|
|||
|
// performed by this function. To prepare the shifts, we have to adjust
|
|||
|
// change(w+), shift(w+), and change(w-).
|
|||
|
function executeShifts(v) {
|
|||
|
var shift = 0,
|
|||
|
change = 0,
|
|||
|
children = v.children,
|
|||
|
i = children.length,
|
|||
|
w;
|
|||
|
while (--i >= 0) {
|
|||
|
w = children[i];
|
|||
|
w.z += shift;
|
|||
|
w.m += shift;
|
|||
|
shift += w.s + (change += w.c);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// If vi-’s ancestor is a sibling of v, returns vi-’s ancestor. Otherwise,
|
|||
|
// returns the specified (default) ancestor.
|
|||
|
function nextAncestor(vim, v, ancestor) {
|
|||
|
return vim.a.parent === v.parent ? vim.a : ancestor;
|
|||
|
}
|
|||
|
|
|||
|
function TreeNode(node, i) {
|
|||
|
this._ = node;
|
|||
|
this.parent = null;
|
|||
|
this.children = null;
|
|||
|
this.A = null; // default ancestor
|
|||
|
this.a = this; // ancestor
|
|||
|
this.z = 0; // prelim
|
|||
|
this.m = 0; // mod
|
|||
|
this.c = 0; // change
|
|||
|
this.s = 0; // shift
|
|||
|
this.t = null; // thread
|
|||
|
this.i = i; // number
|
|||
|
}
|
|||
|
|
|||
|
TreeNode.prototype = Object.create(Node.prototype);
|
|||
|
|
|||
|
function treeRoot(root) {
|
|||
|
var tree = new TreeNode(root, 0),
|
|||
|
node,
|
|||
|
nodes = [tree],
|
|||
|
child,
|
|||
|
children,
|
|||
|
i,
|
|||
|
n;
|
|||
|
|
|||
|
while (node = nodes.pop()) {
|
|||
|
if (children = node._.children) {
|
|||
|
node.children = new Array(n = children.length);
|
|||
|
for (i = n - 1; i >= 0; --i) {
|
|||
|
nodes.push(child = node.children[i] = new TreeNode(children[i], i));
|
|||
|
child.parent = node;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
(tree.parent = new TreeNode(null, 0)).children = [tree];
|
|||
|
return tree;
|
|||
|
}
|
|||
|
|
|||
|
// Node-link tree diagram using the Reingold-Tilford "tidy" algorithm
|
|||
|
export default function() {
|
|||
|
var separation = defaultSeparation,
|
|||
|
dx = 1,
|
|||
|
dy = 1,
|
|||
|
nodeSize = null;
|
|||
|
|
|||
|
function tree(root) {
|
|||
|
var t = treeRoot(root);
|
|||
|
|
|||
|
// Compute the layout using Buchheim et al.’s algorithm.
|
|||
|
t.eachAfter(firstWalk), t.parent.m = -t.z;
|
|||
|
t.eachBefore(secondWalk);
|
|||
|
|
|||
|
// If a fixed node size is specified, scale x and y.
|
|||
|
if (nodeSize) root.eachBefore(sizeNode);
|
|||
|
|
|||
|
// If a fixed tree size is specified, scale x and y based on the extent.
|
|||
|
// Compute the left-most, right-most, and depth-most nodes for extents.
|
|||
|
else {
|
|||
|
var left = root,
|
|||
|
right = root,
|
|||
|
bottom = root;
|
|||
|
root.eachBefore(function(node) {
|
|||
|
if (node.x < left.x) left = node;
|
|||
|
if (node.x > right.x) right = node;
|
|||
|
if (node.depth > bottom.depth) bottom = node;
|
|||
|
});
|
|||
|
var s = left === right ? 1 : separation(left, right) / 2,
|
|||
|
tx = s - left.x,
|
|||
|
kx = dx / (right.x + s + tx),
|
|||
|
ky = dy / (bottom.depth || 1);
|
|||
|
root.eachBefore(function(node) {
|
|||
|
node.x = (node.x + tx) * kx;
|
|||
|
node.y = node.depth * ky;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
return root;
|
|||
|
}
|
|||
|
|
|||
|
// Computes a preliminary x-coordinate for v. Before that, FIRST WALK is
|
|||
|
// applied recursively to the children of v, as well as the function
|
|||
|
// APPORTION. After spacing out the children by calling EXECUTE SHIFTS, the
|
|||
|
// node v is placed to the midpoint of its outermost children.
|
|||
|
function firstWalk(v) {
|
|||
|
var children = v.children,
|
|||
|
siblings = v.parent.children,
|
|||
|
w = v.i ? siblings[v.i - 1] : null;
|
|||
|
if (children) {
|
|||
|
executeShifts(v);
|
|||
|
var midpoint = (children[0].z + children[children.length - 1].z) / 2;
|
|||
|
if (w) {
|
|||
|
v.z = w.z + separation(v._, w._);
|
|||
|
v.m = v.z - midpoint;
|
|||
|
} else {
|
|||
|
v.z = midpoint;
|
|||
|
}
|
|||
|
} else if (w) {
|
|||
|
v.z = w.z + separation(v._, w._);
|
|||
|
}
|
|||
|
v.parent.A = apportion(v, w, v.parent.A || siblings[0]);
|
|||
|
}
|
|||
|
|
|||
|
// Computes all real x-coordinates by summing up the modifiers recursively.
|
|||
|
function secondWalk(v) {
|
|||
|
v._.x = v.z + v.parent.m;
|
|||
|
v.m += v.parent.m;
|
|||
|
}
|
|||
|
|
|||
|
// The core of the algorithm. Here, a new subtree is combined with the
|
|||
|
// previous subtrees. Threads are used to traverse the inside and outside
|
|||
|
// contours of the left and right subtree up to the highest common level. The
|
|||
|
// vertices used for the traversals are vi+, vi-, vo-, and vo+, where the
|
|||
|
// superscript o means outside and i means inside, the subscript - means left
|
|||
|
// subtree and + means right subtree. For summing up the modifiers along the
|
|||
|
// contour, we use respective variables si+, si-, so-, and so+. Whenever two
|
|||
|
// nodes of the inside contours conflict, we compute the left one of the
|
|||
|
// greatest uncommon ancestors using the function ANCESTOR and call MOVE
|
|||
|
// SUBTREE to shift the subtree and prepare the shifts of smaller subtrees.
|
|||
|
// Finally, we add a new thread (if necessary).
|
|||
|
function apportion(v, w, ancestor) {
|
|||
|
if (w) {
|
|||
|
var vip = v,
|
|||
|
vop = v,
|
|||
|
vim = w,
|
|||
|
vom = vip.parent.children[0],
|
|||
|
sip = vip.m,
|
|||
|
sop = vop.m,
|
|||
|
sim = vim.m,
|
|||
|
som = vom.m,
|
|||
|
shift;
|
|||
|
while (vim = nextRight(vim), vip = nextLeft(vip), vim && vip) {
|
|||
|
vom = nextLeft(vom);
|
|||
|
vop = nextRight(vop);
|
|||
|
vop.a = v;
|
|||
|
shift = vim.z + sim - vip.z - sip + separation(vim._, vip._);
|
|||
|
if (shift > 0) {
|
|||
|
moveSubtree(nextAncestor(vim, v, ancestor), v, shift);
|
|||
|
sip += shift;
|
|||
|
sop += shift;
|
|||
|
}
|
|||
|
sim += vim.m;
|
|||
|
sip += vip.m;
|
|||
|
som += vom.m;
|
|||
|
sop += vop.m;
|
|||
|
}
|
|||
|
if (vim && !nextRight(vop)) {
|
|||
|
vop.t = vim;
|
|||
|
vop.m += sim - sop;
|
|||
|
}
|
|||
|
if (vip && !nextLeft(vom)) {
|
|||
|
vom.t = vip;
|
|||
|
vom.m += sip - som;
|
|||
|
ancestor = v;
|
|||
|
}
|
|||
|
}
|
|||
|
return ancestor;
|
|||
|
}
|
|||
|
|
|||
|
function sizeNode(node) {
|
|||
|
node.x *= dx;
|
|||
|
node.y = node.depth * dy;
|
|||
|
}
|
|||
|
|
|||
|
tree.separation = function(x) {
|
|||
|
return arguments.length ? (separation = x, tree) : separation;
|
|||
|
};
|
|||
|
|
|||
|
tree.size = function(x) {
|
|||
|
return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], tree) : (nodeSize ? null : [dx, dy]);
|
|||
|
};
|
|||
|
|
|||
|
tree.nodeSize = function(x) {
|
|||
|
return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], tree) : (nodeSize ? [dx, dy] : null);
|
|||
|
};
|
|||
|
|
|||
|
return tree;
|
|||
|
}
|