diff --git a/README.md b/README.md index f07a2c40..ea5474ec 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ var treemap = d3.treemap(); * [Cluster](#cluster) * [Tree](#tree) * [Treemap](#treemap) ([Treemap Tiling](#treemap-tiling)) +* [Stacked Tree](#stackedtree) * [Partition](#partition) * [Pack](#pack) @@ -493,6 +494,54 @@ Like [d3.treemapSquarify](#treemapSquarify), except preserves the topology (node Specifies the desired aspect ratio of the generated rectangles. The *ratio* must be specified as a number greater than or equal to one. Note that the orientation of the generated rectangles (tall or wide) is not implied by the ratio; for example, a ratio of two will attempt to produce a mixture of rectangles whose *width*:*height* ratio is either 2:1 or 1:2. (However, you can approximately achieve this result by generating a square treemap at different dimensions, and then [stretching the treemap](https://observablehq.com/@d3/stretched-treemap) to the desired aspect ratio.) Furthermore, the specified *ratio* is merely a hint to the tiling algorithm; the rectangles are not guaranteed to have the specified aspect ratio. If not specified, the aspect ratio defaults to the golden ratio, φ = (1 + sqrt(5)) / 2, per [Kong *et al.*](http://vis.stanford.edu/papers/perception-treemaps) +### Stacked Tree + +The **stacked tree layout** produces a dendrogram-like diagram based on Bisson and Blanch (2012). Stacked trees are a more compact version of the [cluster](#cluster) layout, useful for very large hierarchical clusters. + +# d3.stackedtree() · [Source](https://github.com/d3/d3-hierarchy/blob/master/src/stackedtree.js), [Examples](https://observablehq.com/@martialblog/d3-stacked-tree) + +Creates a new stacked tree layout with default settings. + +# stackedtree(root) + +Lays out the specified *root* [hierarchy](#hierarchy), assigning the following properties on *root* and its descendants: + +* *node*.x - the *x*-coordinate of the node +* *node*.y - the *y*-coordinate of the node + +# stackedtree.size([size]) + +If *size* is specified, sets this stacked tree layout’s size to the specified two-element array of numbers [*width*, *height*] and returns this stacked tree layout. If *size* is not specified, returns the current layout size, which defaults to [1, 1]. A layout size of null indicates that a [node size](#stackedtree_nodeSize) will be used instead. + +# stackedtree.nodeSize([size]) + +If *size* is specified, sets this stackedtree layout’s node size to the specified two-element array of numbers [*width*, *height*] and returns this stackedtree layout. If *size* is not specified, returns the current node size, which defaults to null. A node size of null indicates that a [layout size](#stackedtree_size) will be used instead. When a node size is specified, the root node is always positioned at ⟨0, 0⟩. + +# stackedtree.separation([separation]) + +If *separation* is specified, sets the separation accessor to the specified function and returns this stackedtree layout. If *separation* is not specified, returns the current separation accessor, which defaults to: + +```js +function separation(a, b) { + return a.parent == b.parent ? 0 : 1; +} +``` + +# stackedtree.stacking([stacking]) + +If *stacking* is specified, sets the stacking accessor to the specified function and returns this stackedtree layout. If *stacking* is not specified, returns the current stacking accessor, which defaults to: + +```js +function stacking(a, b, n) { + // With n being the length of the longest leaf array + return a.parent === b.parent ? 1 / n : 0; +} +``` + +# stackedtree.ratio([ratio]) + +If *ratio* is specified, sets the tree-to-stack ratio. Meaning, the lower the ratio the lower the focus on the tree. The ratio must be specified as a number between 0 and 1. If *ratio* is not specified, returns the current ratio, which defaults to: 1. + ### Partition [Partition](https://observablehq.com/@d3/icicle) diff --git a/src/index.js b/src/index.js index cd4cca39..e52f0b18 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ export {default as pack} from "./pack/index.js"; export {default as packSiblings} from "./pack/siblings.js"; export {default as packEnclose} from "./pack/enclose.js"; export {default as partition} from "./partition.js"; +export {default as stackedtree} from "./stackedtree.js"; export {default as stratify} from "./stratify.js"; export {default as tree} from "./tree.js"; export {default as treemap} from "./treemap/index.js"; diff --git a/src/stackedtree.js b/src/stackedtree.js new file mode 100644 index 00000000..8c028984 --- /dev/null +++ b/src/stackedtree.js @@ -0,0 +1,114 @@ +function defaultSeparation(a, b) { + return a.parent === b.parent ? 0 : 1; +} + +function defaultStacking(a, b, n) { + return a.parent === b.parent ? 1 / n : 0; +} + +function meanX(children) { + return children.reduce(meanXReduce, 0) / children.length; +} + +function meanXReduce(x, c) { + return x + c.x; +} + +function maxY(children) { + return children.reduce(maxYReduce, 1); +} + +function maxYReduce(y, c) { + return Math.max(y, c.y); +} + +function leafLeft(node) { + var children; + while (children = node.children) node = children[0]; + return node; +} + +function leafRight(node) { + var children; + while (children = node.children) node = children[children.length - 1]; + return node; +} + +export default function() { + var separation = defaultSeparation, + stacking = defaultStacking, + ratio = 1, + dx = 1, + dy = 1, + nodeSize = false; + + function stackedtree(root) { + var previousNode, + stackHeight = 1, + y = 0, + x = 0; + + // Find longest children array to calculate stacking distance + root.each(function(node){ + var leaves = node.children; + stackHeight = leaves ? Math.max(node.children.length, stackHeight) : stackHeight; + }) + + // First walk, computing the initial x & y values. + root.eachAfter(function(node) { + + // TODO: Is this flexible enough? + // Resetting y for new stack + y = previousNode && previousNode.parent !== node.parent ? 0 : y; + + var children = node.children; + if (children) { + node.x = meanX(children); + node.y = ratio + maxY(children); + } else { + node.x = previousNode ? x += separation(node, previousNode) : 0; + node.y = previousNode ? y += stacking(node, previousNode, stackHeight) : 0; + previousNode = node; + } + }); + + var left = leafLeft(root), + right = leafRight(root), + x0 = left.x - separation(left, right) / 2, + x1 = right.x + separation(right, left) / 2; + + // Second walk, normalizing x & y to the desired size. + return root.eachAfter(nodeSize ? function(node) { + 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; + }); + } + + stackedtree.separation = function(x) { + return arguments.length ? (separation = x, stackedtree) : separation; + }; + + stackedtree.stacking = function(y) { + return arguments.length ? (stacking = y, stackedtree) : stacking; + }; + + stackedtree.ratio = function(x) { + // TODO: This a good solution? + // Tree-to-Stack Ratio from 0 to 1 (default: 1) + // Lower value means less emphasis on the tree, more on the stacks. + return arguments.length ? (ratio = x, stackedtree) : ratio; + }; + + stackedtree.size = function(x) { + return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], stackedtree) : (nodeSize ? null : [dx, dy]); + }; + + stackedtree.nodeSize = function(x) { + return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], stackedtree) : (nodeSize ? [dx, dy] : null); + }; + + return stackedtree; +}