UNPKG

d3-flextree

Version:

Flexible tree layout algorithm that allows for variable node sizes.

1,891 lines (1,770 loc) 48 kB
(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