d3-flextree
Version:
Flexible tree layout algorithm that allows for variable node sizes.
1,891 lines (1,770 loc) • 48 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';
/* eslint-disable complexity, indent */
const type = value => {
const t = typeof value;
return value === null || t === 'undefined' || t === 'number' ||
t === 'string' || t === 'boolean' ? 'scalar'
: t === 'function' ? 'function'
: Array.isArray(value) ? 'array'
: 'object';
};
/* eslint-enable complexity, indent */
// This returns true if the `actual` matches `expected` at every path at
// which expected is defined. If `expected` is a scalar, then `compare` is
// used to determine a match. If `expected` is a function, then that function,
// evaluated against `actual`, is used to determine a match. For
// `compare` or an `expected` function, a return value of
// `true` indicates a match; anything else is considered a mismatch.
// If there are any mismatches, then this returns an array of {path, desc}.
// If `findAll` is false, then the array will have only the first mismatch.
/* eslint-disable complexity */
const deepCompare = compare => {
const _dc = (actual, expected, failFast=true, path=[]) => {
const mismatches = [];
const mismatch = desc => {
mismatches.push({ desc, path: path.slice() });
};
const t = type(expected);
const aType = type(actual);
if (t === 'function') {
const result = expected(actual);
if (result !== true) mismatch(result);
}
else if (t !== aType) {
mismatch(`Expected type ${t}, found ${aType}`);
}
else if (t === 'scalar') {
if (compare(actual, expected) !== true) {
mismatch(`Expected value to match ${expected}, found ${actual}`);
}
}
else if (t === 'array' || t === 'object') {
for (const key of Object.keys(expected)) {
if (!(key in actual)) {
mismatch(`Missing expected key ${key}`);
if (failFast) break;
}
const result = _dc(actual[key], expected[key], failFast, path.concat(key));
if (result !== true) {
mismatches.push(...result);
if (failFast) break;
}
}
}
return mismatches.length === 0 ? true : mismatches;
};
return _dc;
};
/* eslint-enable complexity */
const deepEqual =
deepCompare((actual, expected) => Object.is(actual, expected));
const round6 = number => {
const v = Math.round(1000000 * number) / 1000000;
return Math.abs(v) > 0.000001 ? v : 0;
};
const close = (actual, expected) =>
Object.is(actual, expected) || round6(+actual) === round6(+expected);
const deepClose = deepCompare(close);
class Assertion {
constructor(name, arity, test) {
this.name = name;
this.test = test;
this.arity = arity;
}
get function() {
const func = (...args) => {
const msg =
args.length > this.arity ? ': ' + args[this.arity]
: ', arguments: ' + args;
const pass = this.test(...args);
if (!pass) throw Error(`assertion '${this.name}' failed${msg}`);
};
func.assertion = this;
return func;
}
}
const assertionList = [
['pass', 0, () => true],
['fail', 0, () => false],
['truthy', 1, value => !!value],
['falsy', 1, value => !value],
['true', 1, value => value === true],
['false', 1, value => value === false],
['is', 2, (value, expected) => Object.is(value, expected)],
['not', 2, (value, expected) => !Object.is(value, expected)],
['deepEqual', 2, (value, expected) => deepEqual(value, expected) === true],
['notDeepEqual', 2, (value, expected) => deepEqual(value, expected) !== true],
['close', 2, (value, expected) => close(value, expected)],
['deepClose', 2, (value, expected) => deepClose(value, expected) === true],
['notDeepClose', 2, (value, expected) => deepClose(value, expected) !== true],
].map(data => new Assertion(...data));
var assert = Object.assign({},
...assertionList.map(a => ({ [a.name]: a.function }))
);
const defaults = {
verbose: false,
failFast: false,
log: console.log.bind(console), // eslint-disable-line no-console
error: console.error.bind(console), // eslint-disable-line no-console
};
const tester = opts => {
const options = Object.assign({}, defaults, opts);
const {verbose, failFast, log, error} = options;
const results = [];
let failedCount = 0;
const test = (desc, testFunc) => {
if (failedCount > 0 && failFast) return;
try {
if (verbose) log(`Test ${desc}`);
testFunc(assert);
if (verbose) log('passed');
results.push([desc, true]);
}
catch(err) {
error(`Test ${desc} failed: `, err);
results.push([desc, false]);
failedCount++;
}
};
const done = () => {
if (failedCount === 0) {
log('All passed');
}
else {
if (failFast) error('Failed');
else error(`Failed ${failedCount} of ${results.length}`);
}
if (typeof process === 'undefined') {
console.info(failedCount); // eslint-disable-line no-console
}
else {
process.exit(failedCount);
}
};
Object.assign(test, options, {
results,
done,
});
return test;
};
const tests = test => {
test('deepEqual types', t => {
t.true(deepEqual(0, 0));
t.true(deepEqual(null, null));
t.true(deepEqual(NaN, NaN));
t.true(deepEqual(false, false));
t.true(deepEqual([], []));
t.true(deepEqual({}, {}));
const resultA = deepEqual(5, []);
t.true(Array.isArray(resultA));
t.is(resultA.length, 1);
const ra0 = resultA[0];
t.true('desc' in ra0);
t.is(typeof ra0.desc, 'string');
t.true(ra0.desc.includes('type'));
t.true('path' in ra0);
t.true(Array.isArray(ra0.path));
t.is(ra0.path.length, 0);
t.is(deepEqual([[]], [[]]), true);
});
test('deepEqual values', t => {
const actual = [0, 'a', false, {}];
t.true(deepEqual(actual, [0, 'a', false, {}]));
// test that it only compares keys in expected
t.true(deepEqual(actual, [0, 'a']));
// test expected array missing a middle value
const expA = [0];
expA[2] = false;
t.true(deepEqual(actual, expA));
const resultB = deepEqual(actual, [0, 9, 'a']);
t.true(Array.isArray(resultB) && resultB.length === 1 &&
('desc' in resultB[0]) && typeof resultB[0].desc === 'string' &&
resultB[0].desc.includes('value'));
});
const testArr = () => {
let undefined;
const arr = [];
arr[1] = null;
arr[2] = undefined;
arr[5] = -5;
arr[7] = NaN;
arr[8] = {};
arr[9] = 100;
return arr;
};
const testObj = () => {
const obj = {
a: 123,
'-2': [ 4, true, null ],
'false': {
ca: 7.8,
'class': NaN,
n: [ 7, 8, { yabble: 'blop' } ],
},
cat: 'scratch',
bat: 7,
};
obj[2] = 2;
return obj;
};
test('deepEqual nested', t => {
t.true(deepEqual(testArr(), testArr()), 'test array equal to itself');
const expA = [];
expA[7] = NaN;
t.true(deepEqual(testArr(), expA), 'sparse expected array');
t.true(deepEqual(testObj(), testObj()), 'test object equal to itself');
const expB = testObj();
delete expB['-2'];
delete expB['false'].ca;
t.true(deepEqual(testObj(), expB), 'sparse expected object');
});
test('deepClose', t => {
const actA = testArr();
const expA = testArr();
expA[9] = 100.00000001;
t.true(Array.isArray(deepEqual(actA, expA)), 'arrays close but not equal');
const closeA = deepClose(actA, expA);
t.true(closeA, 'arrays are close');
const actB = testObj();
const expB = testObj();
delete expB.cat;
expB['false'].ca += 0.000000001;
t.true(Array.isArray(deepEqual(actB, expB)), 'objects not equal');
t.true(deepClose(actB, expB), 'objects are close');
});
test('failFast off', t => {
t.true(deepEqual(testArr(), testArr(), false));
const actA = testObj();
const expA = testObj();
expA.bat = 7.01;
expA['false'].n[2].yabble = 'bloop';
const ra = deepEqual(actA, expA, false);
t.is(ra.length, 2);
const ra0 = ra[0];
t.true(ra0.desc.includes('bloop') && ra0.desc.includes('blop'));
const ra0p = ra[0].path;
t.is(ra0p[0], 'false');
t.is(ra0p[1], 'n');
t.is(ra0p[2], '2');
t.is(ra0p[3], 'yabble');
});
test('expected function', t => {
const actA = testObj();
const expA = testObj();
expA['false'].n[2].yabble = str => str.length === 4;
expA.cat = str => str.length === 7;
const resA = deepEqual(actA, expA);
t.true(resA);
const actB = actA;
const expB = testObj();
expB.cat = str => str.length === 6;
const resB = deepEqual(actB, expB);
t.true(Array.isArray(resB));
});
};
function count(node) {
var sum = 0,
children = node.children,
i = children && children.length;
if (!i) sum = 1;
else while (--i >= 0) sum += children[i].value;
node.value = sum;
}
function node_count() {
return this.eachAfter(count);
}
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;
}
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;
});
}
function node_sort(compare) {
return this.eachBefore(function(node) {
if (node.children) {
node.children.sort(compare);
}
});
}
function node_path(end) {
var start = this,
ancestor = leastCommonAncestor(start, end),
nodes = [start];
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;
}
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;
}
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;
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);
}
function node_copy() {
return hierarchy(this).eachBefore(copyData);
}
function defaultChildren(d) {
return d.children;
}
function copyData(node) {
node.data = node.data.data;
}
function computeHeight(node) {
var height = 0;
do node.height = height;
while ((node = node.parent) && (node.height < ++height));
}
function Node(data) {
this.data = data;
this.depth =
this.height = 0;
this.parent = null;
}
Node.prototype = hierarchy.prototype = {
constructor: Node,
count: node_count,
each: node_each,
eachAfter: node_eachAfter,
eachBefore: node_eachBefore,
sum: node_sum,
sort: node_sort,
path: node_path,
ancestors: node_ancestors,
descendants: node_descendants,
leaves: node_leaves,
links: node_links,
copy: node_copy
};
const name = "d3-flextree";
const version = "2.1.2";
const main = "build/d3-flextree.js";
const module$1 = "index";
const author = {"name":"Chris Maloney","url":"http://chrismaloney.org"};
const description = "Flexible tree layout algorithm that allows for variable node sizes.";
const keywords = ["d3","d3-module","layout","tree","hierarchy","d3-hierarchy","plugin","d3-plugin","infovis","visualization","2d"];
const homepage = "https://github.com/klortho/d3-flextree";
const license = "WTFPL";
const repository = {"type":"git","url":"https://github.com/klortho/d3-flextree.git"};
const scripts = {"clean":"rm -rf build demo test","build:demo":"rollup -c --environment BUILD:demo","build:dev":"rollup -c --environment BUILD:dev","build:prod":"rollup -c --environment BUILD:prod","build:test":"rollup -c --environment BUILD:test","build":"rollup -c","lint":"eslint index.js src","test:main":"node test/bundle.js","test:browser":"node test/browser-tests.js","test":"npm-run-all test:*","prepare":"npm-run-all clean build lint test"};
const dependencies = {"d3-hierarchy":"^1.1.5"};
const devDependencies = {"babel-plugin-external-helpers":"^6.22.0","babel-preset-es2015-rollup":"^3.0.0","d3":"^4.13.0","d3-selection-multi":"^1.0.1","eslint":"^4.19.1","jsdom":"^11.6.2","npm-run-all":"^4.1.2","rollup":"^0.55.3","rollup-plugin-babel":"^2.7.1","rollup-plugin-commonjs":"^8.0.2","rollup-plugin-copy":"^0.2.3","rollup-plugin-json":"^2.3.0","rollup-plugin-node-resolve":"^3.0.2","rollup-plugin-uglify":"^3.0.0","uglify-es":"^3.3.9"};
var packageInfo = {
name: name,
version: version,
main: main,
module: module$1,
author: author,
description: description,
keywords: keywords,
homepage: homepage,
license: license,
repository: repository,
scripts: scripts,
dependencies: dependencies,
devDependencies: devDependencies,
"jsnext:main": "index"
};
const {version: version$1} = packageInfo;
const defaults$1 = Object.freeze({
children: data => data.children,
nodeSize: node => node.data.size,
spacing: 0,
});
// Create a layout function with customizable options. Per D3-style, the
// options can be set at any time using setter methods. The layout function
// will compute the tree node positions based on the options in effect at the
// time it is called.
function flextree(options) {
const opts = Object.assign({}, defaults$1, options);
function accessor(name$$1) {
const opt = opts[name$$1];
return typeof opt === 'function' ? opt : () => opt;
}
function layout(tree) {
const wtree = wrap(getWrapper(), tree, node=>node.children);
wtree.update();
return wtree.data;
}
function getFlexNode() {
const nodeSize = accessor('nodeSize');
const spacing = accessor('spacing');
return class FlexNode extends hierarchy.prototype.constructor {
constructor(data) {
super(data);
}
copy() {
const c = wrap(this.constructor, this, node=>node.children);
c.each(node => node.data = node.data.data);
return c;
}
get size() { return nodeSize(this); }
spacing(oNode) { return spacing(this, oNode); }
get nodes() { return this.descendants(); }
get xSize() { return this.size[0]; }
get ySize() { return this.size[1]; }
get top() { return this.y; }
get bottom() { return this.y + this.ySize; }
get left() { return this.x - this.xSize / 2; }
get right() { return this.x + this.xSize / 2; }
get root() {
const ancs = this.ancestors();
return ancs[ancs.length - 1];
}
get numChildren() {
return this.hasChildren ? this.children.length : 0;
}
get hasChildren() { return !this.noChildren; }
get noChildren() { return this.children === null; }
get firstChild() {
return this.hasChildren ? this.children[0] : null;
}
get lastChild() {
return this.hasChildren ? this.children[this.numChildren - 1] : null;
}
get extents() {
return (this.children || []).reduce(
(acc, kid) => FlexNode.maxExtents(acc, kid.extents),
this.nodeExtents);
}
get nodeExtents() {
return {
top: this.top,
bottom: this.bottom,
left: this.left,
right: this.right,
};
}
static maxExtents(e0, e1) {
return {
top: Math.min(e0.top, e1.top),
bottom: Math.max(e0.bottom, e1.bottom),
left: Math.min(e0.left, e1.left),
right: Math.max(e0.right, e1.right),
};
}
};
}
function getWrapper() {
const FlexNode = getFlexNode();
const nodeSize = accessor('nodeSize');
const spacing = accessor('spacing');
return class extends FlexNode {
constructor(data) {
super(data);
Object.assign(this, {
x: 0, y: 0,
relX: 0, prelim: 0, shift: 0, change: 0,
lExt: this, lExtRelX: 0, lThr: null,
rExt: this, rExtRelX: 0, rThr: null,
});
}
get size() { return nodeSize(this.data); }
spacing(oNode) { return spacing(this.data, oNode.data); }
get x() { return this.data.x; }
set x(v) { this.data.x = v; }
get y() { return this.data.y; }
set y(v) { this.data.y = v; }
update() {
layoutChildren(this);
resolveX(this);
return this;
}
};
}
function wrap(FlexClass, treeData, children) {
const _wrap = (data, parent) => {
const node = new FlexClass(data);
Object.assign(node, {
parent,
depth: parent === null ? 0 : parent.depth + 1,
height: 0,
length: 1,
});
const kidsData = children(data) || [];
node.children = kidsData.length === 0 ? null
: kidsData.map(kd => _wrap(kd, node));
if (node.children) {
Object.assign(node, node.children.reduce(
(hl, kid) => ({
height: Math.max(hl.height, kid.height + 1),
length: hl.length + kid.length,
}), node
));
}
return node;
};
return _wrap(treeData, null);
}
Object.assign(layout, {
nodeSize(arg) {
return arguments.length ? (opts.nodeSize = arg, layout) : opts.nodeSize;
},
spacing(arg) {
return arguments.length ? (opts.spacing = arg, layout) : opts.spacing;
},
children(arg) {
return arguments.length ? (opts.children = arg, layout) : opts.children;
},
hierarchy(treeData, children) {
const kids = typeof children === 'undefined' ? opts.children : children;
return wrap(getFlexNode(), treeData, kids);
},
dump(tree) {
const nodeSize = accessor('nodeSize');
const _dump = i0 => node => {
const i1 = i0 + ' ';
const i2 = i0 + ' ';
const {x, y} = node;
const size = nodeSize(node);
const kids = (node.children || []);
const kdumps = (kids.length === 0) ? ' ' :
`,${i1}children: [${i2}${kids.map(_dump(i2)).join(i2)}${i1}],${i0}`;
return `{ size: [${size.join(', ')}],${i1}x: ${x}, y: ${y}${kdumps}},`;
};
return _dump('\n')(tree);
},
});
return layout;
}
flextree.version = version$1;
const layoutChildren = (w, y = 0) => {
w.y = y;
(w.children || []).reduce((acc, kid) => {
const [i, lastLows] = acc;
layoutChildren(kid, w.y + w.ySize);
// The lowest vertical coordinate while extreme nodes still point
// in current subtree.
const lowY = (i === 0 ? kid.lExt : kid.rExt).bottom;
if (i !== 0) separate(w, i, lastLows);
const lows = updateLows(lowY, i, lastLows);
return [i + 1, lows];
}, [0, null]);
shiftChange(w);
positionRoot(w);
return w;
};
// Resolves the relative coordinate properties - relX and prelim --
// to set the final, absolute x coordinate for each node. This also sets
// `prelim` to 0, so that `relX` for each node is its x-coordinate relative
// to its parent.
const resolveX = (w, prevSum, parentX) => {
// A call to resolveX without arguments is assumed to be for the root of
// the tree. This will set the root's x-coord to zero.
if (typeof prevSum === 'undefined') {
prevSum = -w.relX - w.prelim;
parentX = 0;
}
const sum = prevSum + w.relX;
w.relX = sum + w.prelim - parentX;
w.prelim = 0;
w.x = parentX + w.relX;
(w.children || []).forEach(k => resolveX(k, sum, w.x));
return w;
};
// Process shift and change for all children, to add intermediate spacing to
// each child's modifier.
const shiftChange = w => {
(w.children || []).reduce((acc, child) => {
const [lastShiftSum, lastChangeSum] = acc;
const shiftSum = lastShiftSum + child.shift;
const changeSum = lastChangeSum + shiftSum + child.change;
child.relX += changeSum;
return [shiftSum, changeSum];
}, [0, 0]);
};
// Separates the latest child from its previous sibling
/* eslint-disable complexity */
const separate = (w, i, lows) => {
const lSib = w.children[i - 1];
const curSubtree = w.children[i];
let rContour = lSib;
let rSumMods = lSib.relX;
let lContour = curSubtree;
let lSumMods = curSubtree.relX;
let isFirst = true;
while (rContour && lContour) {
if (rContour.bottom > lows.lowY) lows = lows.next;
// How far to the left of the right side of rContour is the left side
// of lContour? First compute the center-to-center distance, then add
// the "spacing"
const dist =
(rSumMods + rContour.prelim) - (lSumMods + lContour.prelim) +
rContour.xSize / 2 + lContour.xSize / 2 +
rContour.spacing(lContour);
if (dist > 0 || (dist < 0 && isFirst)) {
lSumMods += dist;
// Move subtree by changing relX.
moveSubtree$1(curSubtree, dist);
distributeExtra(w, i, lows.index, dist);
}
isFirst = false;
// Advance highest node(s) and sum(s) of modifiers
const rightBottom = rContour.bottom;
const leftBottom = lContour.bottom;
if (rightBottom <= leftBottom) {
rContour = nextRContour(rContour);
if (rContour) rSumMods += rContour.relX;
}
if (rightBottom >= leftBottom) {
lContour = nextLContour(lContour);
if (lContour) lSumMods += lContour.relX;
}
}
// Set threads and update extreme nodes. In the first case, the
// current subtree is taller than the left siblings.
if (!rContour && lContour) setLThr(w, i, lContour, lSumMods);
// In the next case, the left siblings are taller than the current subtree
else if (rContour && !lContour) setRThr(w, i, rContour, rSumMods);
};
/* eslint-enable complexity */
// Move subtree by changing relX.
const moveSubtree$1 = (subtree, distance) => {
subtree.relX += distance;
subtree.lExtRelX += distance;
subtree.rExtRelX += distance;
};
const distributeExtra = (w, curSubtreeI, leftSibI, dist) => {
const curSubtree = w.children[curSubtreeI];
const n = curSubtreeI - leftSibI;
// Are there intermediate children?
if (n > 1) {
const delta = dist / n;
w.children[leftSibI + 1].shift += delta;
curSubtree.shift -= delta;
curSubtree.change -= dist - delta;
}
};
const nextLContour = w => {
return w.hasChildren ? w.firstChild : w.lThr;
};
const nextRContour = w => {
return w.hasChildren ? w.lastChild : w.rThr;
};
const setLThr = (w, i, lContour, lSumMods) => {
const firstChild = w.firstChild;
const lExt = firstChild.lExt;
const curSubtree = w.children[i];
lExt.lThr = lContour;
// Change relX so that the sum of modifier after following thread is correct.
const diff = lSumMods - lContour.relX - firstChild.lExtRelX;
lExt.relX += diff;
// Change preliminary x coordinate so that the node does not move.
lExt.prelim -= diff;
// Update extreme node and its sum of modifiers.
firstChild.lExt = curSubtree.lExt;
firstChild.lExtRelX = curSubtree.lExtRelX;
};
// Mirror image of setLThr.
const setRThr = (w, i, rContour, rSumMods) => {
const curSubtree = w.children[i];
const rExt = curSubtree.rExt;
const lSib = w.children[i - 1];
rExt.rThr = rContour;
const diff = rSumMods - rContour.relX - curSubtree.rExtRelX;
rExt.relX += diff;
rExt.prelim -= diff;
curSubtree.rExt = lSib.rExt;
curSubtree.rExtRelX = lSib.rExtRelX;
};
// Position root between children, taking into account their modifiers
const positionRoot = w => {
if (w.hasChildren) {
const k0 = w.firstChild;
const kf = w.lastChild;
const prelim = (k0.prelim + k0.relX - k0.xSize / 2 +
kf.relX + kf.prelim + kf.xSize / 2 ) / 2;
Object.assign(w, {
prelim,
lExt: k0.lExt, lExtRelX: k0.lExtRelX,
rExt: kf.rExt, rExtRelX: kf.rExtRelX,
});
}
};
// Make/maintain a linked list of the indexes of left siblings and their
// lowest vertical coordinate.
const updateLows = (lowY, index, lastLows) => {
// Remove siblings that are hidden by the new subtree.
while (lastLows !== null && lowY >= lastLows.lowY)
lastLows = lastLows.next;
// Prepend the new subtree.
return {
lowY,
index,
next: lastLows,
};
};
const {version: version$2} = packageInfo;
// Several different ways of representing the same tree.
const treeData = {
default: {
name: 'algert',
size: [1, 1],
children: [
{ name: 'blat',
size: [2, 4],
},
{ name: 'cluckoo',
size: [3, 1],
children: [
{ name: 'dornk',
size: [4, 1],
},
],
},
],
},
customSize: {
name: 'algert',
width: 1, height: 1,
children: [
{ name: 'blat',
width: 2, height: 4,
},
{ name: 'cluckoo',
width: 3, height: 1,
children: [
{ name: 'dornk',
width: 4, height: 1,
},
],
},
],
},
customChildren: {
name: 'algert',
size: [1, 1],
kids: [
{ name: 'blat',
size: [2, 4],
},
{ name: 'cluckoo',
size: [3, 1],
kids: [
{ name: 'dornk',
size: [4, 1],
},
],
},
],
},
allCustom:
[ 'algert', 1, 1,
[ 'blat', 2, 4 ],
[ 'cluckoo', 3, 1,
[ 'dornk', 4, 1 ],
],
],
};
const expected = {
default: {
x: 0, y: 0,
children: [
{ x: -1.75, y: 1 },
{ x: 1.25, y: 1,
children: [
{ x: 1.25, y: 2 },
],
},
],
},
sizeConst: {
x: 0, y: 0,
children: [
{ x: -1, y: 2 },
{ x: 1, y: 2,
children: [
{ x: 1, y: 4 },
],
},
],
},
sizeScaled: {
x: 0, y: 0,
children: [
{ x: -3.5, y: 2 },
{ x: 2.5, y: 2,
children: [
{ x: 2.5, y: 4 },
],
},
],
},
spacingConst: {
x: 0, y: 0,
children: [
{ x: -2, y: 1 },
{ x: 1.5, y: 1,
children: [
{ x: 1.5, y: 2 },
],
},
],
},
spacingFunc: {
x: 0, y: 0,
children: [
{ x: -3.75, y: 1 },
{ x: 3.25, y: 1,
children: [
{ x: 3.25, y: 2 },
],
},
],
},
bothFunc: {
x: 0, y: 0,
children: [
{ x: -5.5, y: 2 },
{ x: 4.5, y: 2,
children: [
{ x: 4.5, y: 4 },
],
},
],
},
};
const getName = node => node.data.name;
const ref = eNode => aNode => getName(aNode) === getName(eNode);
const tests$1 = test => {
// eslint-disable-next-line no-console
const log = test.verbose ? console.log.bind(console) : () => {};
test('exports', t => {
t.is(typeof flextree, 'function');
const layout = flextree();
t.is(typeof layout, 'function');
t.is(flextree.version, version$2);
});
test('flextree hierarchy, defaults', t => {
const layout = flextree();
const tree = layout.hierarchy(treeData.default);
const [algert, blat, cluckoo, dornk] = tree.nodes;
layout(tree);
log(layout.dump(tree));
t.deepClose(algert, {
parent: null, depth: 0, height: 2, length: 4,
size: [1, 1], xSize: 1, ySize: 1,
noChildren: false, hasChildren: true, numChildren: 2,
firstChild: ref(blat), lastChild: ref(cluckoo),
x: 0, y: 0,
top: 0, bottom: 1, left: -0.5, right: 0.5,
nodeExtents: { top: 0, bottom: 1, left: -0.5, right: 0.5 },
extents: { top: 0, bottom: 5, left: -2.75, right: 3.25 },
}, 'algert');
t.deepClose(blat, {
parent: ref(algert), depth: 1, height: 0, length: 1,
size: [2, 4], xSize: 2, ySize: 4,
noChildren: true, hasChildren: false, numChildren: 0,
firstChild: null, lastChild: null,
x: -1.75, y: 1,
top: 1, bottom: 5, left: -2.75, right: -0.75,
nodeExtents: { top: 1, bottom: 5, left: -2.75, right: -0.75 },
extents: { top: 1, bottom: 5, left: -2.75, right: -0.75 },
}, 'blat');
t.deepClose(cluckoo, {
parent: ref(algert), depth: 1, height: 1, length: 2,
size: [3, 1], xSize: 3, ySize: 1,
noChildren: false, hasChildren: true, numChildren: 1,
firstChild: ref(dornk), lastChild: ref(dornk),
x: 1.25, y: 1,
top: 1, bottom: 2, left: -0.25, right: 2.75,
nodeExtents: { top: 1, bottom: 2, left: -0.25, right: 2.75 },
extents: { top: 1, bottom: 3, left: -0.75, right: 3.25 },
}, 'cluckoo');
t.deepClose(dornk, {
parent: ref(cluckoo), depth: 2, height: 0, length: 1,
size: [4, 1], xSize: 4, ySize: 1,
noChildren: true, hasChildren: false, numChildren: 0,
firstChild: null, lastChild: null,
x: 1.25, y: 2,
top: 2, bottom: 3, left: -0.75, right: 3.25,
nodeExtents: { top: 2, bottom: 3, left: -0.75, right: 3.25 },
extents: { top: 2, bottom: 3, left: -0.75, right: 3.25 },
}, 'dornk');
t.deepClose(tree, expected.default);
});
test('d3 hierarchy, all defaults', t => {
const layout = flextree();
const tree = hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('flextree hierarchy, nodeSize constant arg', t => {
const layout = flextree({ nodeSize: [2, 2] });
const tree = layout.hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.sizeConst);
});
test('d3 hierarchy, nodeSize constant chained', t => {
const layout = flextree().nodeSize([2, 2]);
const tree = hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.sizeConst);
});
test('flextree hierarchy, nodeSize accessor arg', t => {
const layout = flextree({
nodeSize: n => [n.data.width, n.data.height],
});
const tree = layout.hierarchy(treeData.customSize);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('flextree hierarchy, nodeSize accessor & scale chained', t => {
const layout = flextree()
.nodeSize(n => [2 * n.data.width, 2 * n.data.height]);
const tree = layout.hierarchy(treeData.customSize);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.sizeScaled);
});
test('d3 hierarchy, nodeSize scale chained', t => {
const layout = flextree()
.nodeSize(n => n.data.size.map(v => 2*v));
const tree = hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.sizeScaled);
});
test('flextree hierarchy, spacing constant chained', t => {
const layout = flextree().spacing(0.5);
const tree = layout.hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.spacingConst);
});
test('d3 hierarchy, spacing constant arg', t => {
const layout = flextree({ spacing: 0.5 });
const tree = hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.spacingConst);
});
test('flextree hierarchy, spacing function arg', t => {
const layout = flextree({
spacing: (n0, n1) => n0.path(n1).length,
});
const tree = layout.hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.spacingFunc);
});
test('d3 hierarchy, spacing function, chained', t => {
const layout = flextree().spacing((n0, n1) => n0.path(n1).length);
const tree = hierarchy(treeData.default);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.spacingFunc);
});
test('flextree hierarchy, nodeSize & spacing function args', t => {
const layout = flextree({
nodeSize: n => [2 * n.data.width, 2 * n.data.height],
spacing: (n0, n1) => n0.path(n1).length,
});
const tree = layout.hierarchy(treeData.customSize);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.bothFunc);
});
test('d3 hierarchy, nodeSize & spacing function args', t => {
const layout = flextree({
nodeSize: n => [2 * n.data.width, 2 * n.data.height],
spacing: (n0, n1) => n0.path(n1).length,
});
const tree = hierarchy(treeData.customSize);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.bothFunc);
});
test('flextree hierarchy, children arg to hierarchy', t => {
const layout = flextree();
const tree = layout.hierarchy(treeData.customChildren, d => d.kids || []);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('flextree hierarchy, children arg to flextree', t => {
const layout = flextree({children: d => d.kids});
const tree = layout.hierarchy(treeData.customChildren);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('flextree hierarchy, children chained', t => {
const layout = flextree()
.children(d => d.kids);
const tree = layout.hierarchy(treeData.customChildren);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('d3 hierarchy, children', t => {
const layout = flextree();
const tree = hierarchy(treeData.customChildren, d => d.kids);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.default);
});
test('flextree hierarchy, all custom args', t => {
const layout = flextree({
nodeSize: n => n.data.slice(1, 3).map(v => 2*v),
spacing: (n0, n1) => n0.path(n1).length,
children: d => {
const kd = d.slice(3);
return kd.length ? kd : null;
},
});
const tree = layout.hierarchy(treeData.allCustom);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.bothFunc);
});
test('flextree hierarchy, all custom chained', t => {
const layout = flextree()
.nodeSize(n => n.data.slice(1, 3).map(v => 2*v))
.spacing((n0, n1) => n0.path(n1).length)
.children(d => {
const kd = d.slice(3);
return kd.length ? kd : null;
});
const tree = layout.hierarchy(treeData.allCustom);
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.bothFunc);
});
test('d3 hierarchy, all custom', t => {
const layout = flextree({
nodeSize: n => n.data.slice(1, 3).map(v => 2*v),
spacing: (n0, n1) => n0.path(n1).length,
});
const tree = hierarchy(treeData.allCustom, d => d.slice(3));
layout(tree);
log(layout.dump(tree));
t.deepClose(tree, expected.bothFunc);
});
// Not recommended.
test('reuse layout with changing options', t => {
const layout = flextree();
const tree0 = layout.hierarchy(treeData.default);
layout(tree0);
log(layout.dump(tree0));
t.deepClose(tree0, expected.default);
// Switch to custom spacing; the layout of the same tree should be different
layout.spacing((n0, n1) => n0.path(n1).length);
layout(tree0);
log(layout.dump(tree0));
t.deepClose(tree0, expected.spacingFunc);
// Switch to custom nodeSize, the custom spacing should remain.
layout.nodeSize(n => n.data.size.map(v => v*2));
layout(tree0);
// Although the new nodeSize is reflected in the layout, it is not stored
// in the tree0 hierarchy itself, so we have to pass an accessor to `dump`.
log(layout.dump(tree0));
t.deepClose(tree0, expected.bothFunc);
// Change the children accessor, and remove custom spacing. This will
// still work, because the hierarchy is not effected.
layout.children(d => d.kids).spacing(0);
layout(tree0);
log(layout.dump(tree0));
t.deepClose(tree0, expected.sizeScaled);
});
test('copy method', t => {
const layout = flextree()
.nodeSize(n => n.data.slice(1, 3).map(v => 2*v))
.spacing((n0, n1) => n0.path(n1).length)
.children(d => {
const kd = d.slice(3);
return kd.length ? kd : null;
});
const tree = layout.hierarchy(treeData.allCustom);
t.true(tree instanceof hierarchy);
const copy = tree.copy();
t.true(copy instanceof hierarchy);
t.is(tree.children[0].data, copy.children[0].data);
layout(tree);
log('tree layout:');
log(layout.dump(tree));
layout(copy);
log('copy layout:');
log(layout.dump(copy));
t.deepClose(copy, expected.bothFunc);
});
};
// `data` specifies node sizes, and
// `expected` specifies the expected x and y coordinates.
var treeSpecs = [
{ desc: 'simplest possible tree',
data: [
100, 100,
],
expected: [
0, 0,
],
customSpacing: [
0, 0,
],
},
{ desc: 'simple tree',
data: [
30, 50,
[ 40, 70,
[ 50, 60 ],
[ 50, 100 ],
],
[ 20, 140,
[ 50, 60 ],
[ 50, 60 ],
],
[ 50, 60,
[ 50, 60 ],
[ 50, 60 ],
],
],
expected: [
0, 0,
[ -82.5, 50,
[ -107.5, 120 ],
[ -57.5, 120 ],
],
[ 17.5, 50,
[ -7.5, 190 ],
[ 42.5, 190 ],
],
[ 77.5, 50,
[ 52.5, 110 ],
[ 102.5, 110 ],
],
],
customSpacing: [
0, 0,
[ -109.5, 50,
[ -140.5, 120 ],
[ -78.5, 120 ],
],
[ 22.5, 50,
[ -8.5, 190 ],
[ 53.5, 190 ],
],
[ 104.5, 50,
[ 73.5, 110 ],
[ 135.5, 110 ],
],
],
},
{ desc: 'another simple tree',
data: [
40, 40,
[ 40, 40 ],
[ 40, 40,
[ 50, 40 ],
[ 50, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
],
[ 40, 40 ],
],
expected: [
0, 0,
[ -40, 40 ],
[ 0, 40,
[ -85, 80 ],
[ -35, 80 ],
[ 10, 80 ],
[ 50, 80 ],
[ 90, 80 ],
],
[ 40, 40 ],
],
customSpacing: [
0, 0,
[ -64, 40 ],
[ 0, 40,
[ -133, 80 ],
[ -59, 80 ],
[ 10, 80 ],
[ 74, 80 ],
[ 138, 80 ],
],
[ 64, 40 ],
],
},
{ desc: 'layout bug, simple case',
data: [
40, 40,
[ 40, 40 ],
[ 40, 40,
[ 100, 40 ],
[ 200, 40 ],
],
],
expected: [
0, 0,
[ -20, 40 ],
[ 20, 40,
[ -80, 80 ],
[ 70, 80 ],
],
],
customSpacing: [
0, 0,
[ -32, 40 ],
[ 32, 40,
[ -80, 80 ],
[ 94, 80 ],
],
],
},
{ desc: 'layout bug, redux',
data: [
40, 40,
[ 40, 40 ],
[ 40, 40,
[ 40, 40,
[ 100, 40 ],
[ 200, 40 ],
],
],
],
expected: [
0, 0,
[ -20, 40 ],
[ 20, 40,
[ 20, 80,
[ -80, 120 ],
[ 70, 120 ],
],
],
],
customSpacing: [
0, 0,
[ -32, 40 ],
[ 32, 40,
[ 32, 80,
[ -80, 120 ],
[ 94, 120 ],
],
],
],
},
{ desc: 'layout bug, third sibling',
data: [
40, 40,
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40,
[ 40, 40,
[ 100, 40 ],
[ 200, 40 ],
],
],
],
expected:
[ 0, 0,
[ -40, 40 ],
[ 0, 40 ],
[ 40, 40,
[ 40, 80,
[ -60, 120 ],
[ 90, 120 ],
],
],
],
customSpacing: [
0, 0,
[ -64, 40 ],
[ 0, 40 ],
[ 64, 40,
[ 64, 80,
[ -48, 120 ],
[ 126, 120 ],
],
],
],
},
// My first, naive attempt to fix the layout bug caused this tree's
// layout to break:
{ desc: 'narrower child',
data: [
20, 20,
[ 20, 40 ],
[ 20, 20,
[ 10, 20 ],
],
],
expected: [
0, 0,
[ -10, 20 ],
[ 10, 20,
[ 10, 40 ],
],
],
customSpacing: [
0, 0,
[ -13, 20 ],
[ 13, 20,
[ 13, 40 ],
],
],
},
{ desc: 'redistributed middle sibling - 0',
data: [
40, 40,
[ 40, 40,
[ 200, 200 ],
],
[ 40, 40 ],
[ 40, 40,
[ 200, 200 ],
],
],
expected: [
0, 0,
[ -100, 40,
[ -100, 80 ],
],
[ 0, 40 ],
[ 100, 40,
[ 100, 80 ],
],
],
customSpacing: [
0, 0,
[ -120, 40,
[ -120, 80 ],
],
[ 0, 40 ],
[ 120, 40,
[ 120, 80 ],
],
],
},
{ desc: 'redistributed middle siblings - 1',
data: [
40, 40,
[ 40, 40,
[ 40, 40,
[ 40, 40,
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
],
],
],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40 ],
[ 40, 40,
[ 40, 40,
[ 200, 200 ],
],
],
],
expected: [
0, 0,
[ -140, 40,
[ -140, 80,
[ -140, 120,
[ -300, 160 ],
[ -260, 160 ],
[ -220, 160 ],
[ -180, 160 ],
[ -140, 160 ],
[ -100, 160 ],
[ -60, 160 ],
[ -20, 160 ],
[ 20, 160 ],
],
],
],
[ -70, 40 ],
[ 0, 40 ],
[ 70, 40 ],
[ 140, 40,
[ 140, 80,
[ 140, 120 ],
],
],
],
customSpacing: [
0, 0,
[ -220, 40,
[ -220, 80,
[ -220, 120,
[ -476, 160 ],
[ -412, 160 ],
[ -348, 160 ],
[ -284, 160 ],
[ -220, 160 ],
[ -156, 160 ],
[ -92, 160 ],
[ -28, 160 ],
[ 36, 160 ],
],
],
],
[ -110, 40 ],
[ 0, 40 ],
[ 110, 40 ],
[ 220, 40,
[ 220, 80,
[ 220, 120 ],
],
],
],
},
{ desc: 'test extreme data',
data: [
40, 40,
[ 40, 40,
[ 40, 100,
[ 200, 40 ],
],
],
[ 40, 40,
[ 40, 40 ],
[ 40, 200 ],
[ 40, 40 ],
],
[ 40, 40,
[ 40, 100,
[ 200, 40 ],
],
],
],
expected: [
0, 0,
[ -120, 40,
[ -120, 80,
[ -120, 180 ],
],
],
[ 0, 40,
[ -40, 80 ],
[ 0, 80 ],
[ 40, 80 ],
],
[ 120, 40,
[ 120, 80,
[ 120, 180 ],
],
],
],
customSpacing: [
0, 0,
[ -168, 40,
[ -168, 80,
[ -168, 180 ],
],
],
[ 0, 40,
[ -64, 80 ],
[ 0, 80 ],
[ 64, 80 ],
],
[ 168, 40,
[ 168, 80,
[ 168, 180 ],
],
],
],
},
{ desc: 'test redistributing children, from van der Ploeg figure 6',
data: [
10, 10,
[ 10, 15,
[ 50, 10 ],
[ 50, 10,
[ 50, 10 ],
[ 50, 10 ],
],
],
[ 10, 10 ],
[ 10, 10 ],
[ 10, 20 ],
[ 10, 10 ],
[ 10, 30,
[ 50, 10 ],
],
],
expected: [
0, 0,
[ -50, 10,
[ -75, 25 ],
[ -25, 25,
[ -50, 35 ],
[ 0, 35 ],
],
],
[ -(26 + 2/3), 10 ],
[ -(3 + 1/3), 10 ],
[ 20, 10 ],
[ 35, 10 ],
[ 50, 10,
[ 50, 40 ],
],
],
customSpacing: [
0, 0,
[ -59, 10,
[ -87, 25 ],
[ -31, 25,
[ -59, 35 ],
[ -3, 35 ],
],
],
[ -33, 10 ],
[ -7, 10 ],
[ 19, 10 ],
[ 39, 10 ],
[ 59, 10,
[ 59, 40 ],
],
],
},
{ desc: 'modified figure 6 tree',
data: [
10, 10,
[ 10, 15,
[ 50, 10 ],
[ 50, 10,
[ 50, 10 ],
[ 50, 10 ],
],
],
[ 10, 10 ],
[ 10, 10 ],
[ 10, 20 ],
[ 10, 10 ],
],
expected: [
0, 0,
[ -32.5, 10,
[ -57.5, 25 ],
[ -7.5, 25,
[ -32.5, 35 ],
[ 17.5, 35 ],
],
],
[ -(14 + 1/6), 10 ],
[ 4 + 1/6, 10 ],
[ 22.5, 10 ],
[ 32.5, 10 ],
],
customSpacing: [
0, 0,
[ -41, 10,
[ -69, 25 ],
[ -13, 25,
[ -41, 35 ],
[ 15, 35 ],
],
],
[ -19, 10 ],
[ 3, 10 ],
[ 25, 10 ],
[ 41, 10 ],
],
},
{ desc: 'mirror image of figure 6 tree',
data: [
10, 10,
[ 10, 30,
[ 50, 10 ],
],
[ 10, 10 ],
[ 10, 20 ],
[ 10, 10 ],
[ 10, 10 ],
[ 10, 15,
[ 50, 12,
[ 50, 10 ],
[ 50, 8 ],
],
[ 50, 10 ],
],
],
expected: [
0, 0,
[ -50, 10,
[ -50, 40 ],
],
[ -35, 10 ],
[ -20, 10 ],
[ 3 + 1/3, 10 ],
[ 26 + 2/3, 10 ],
[ 50, 10,
[ 25, 25,
[ 0, 37 ],
[ 50, 37 ],
],
[ 75, 25 ],
],
],
customSpacing: [
0, 0,
[ -59, 10,
[ -59, 40 ],
],
[ -39, 10 ],
[ -19, 10 ],
[ 7, 10 ],
[ 33, 10 ],
[ 59, 10,
[ 31, 25,
[ 3, 37 ],
[ 59, 37 ],
],
[ 87, 25 ],
],
],
},
];
const expCoords = spec => Object.assign(
{ x: spec[0], y: spec[1] },
spec.length > 2 ? { children: spec.slice(2).map(expCoords) } : null
);
const tests$2 = test => {
treeSpecs.forEach((spec, i) => {
test('tree-' + i, t => {
const tree = hierarchy(spec.data, d => d.slice(2));
const layout = flextree({
nodeSize: n => n.data.slice(0, 2),
});
layout(tree);
const e = expCoords(spec.expected);
t.deepClose(tree, e);
});
});
};
const test = tester({
verbose: false,
failFast: true,
});
tests(test);
tests$1(test);
tests$2(test);
test.done();
})));
//# sourceMappingURL=bundle.js.map