Skip to content

Commit 6653b0e

Browse files
author
Markus Opolka
committed
Add StackedTree Layout
- Based on Bisson and Blanch (2012) > Bisson, G., & Blanch, R. (2012, July). > Improving visualization of large hierarchical clustering. > In 2012 16th International Conference on Information Visualisation (pp. 220-228). IEEE.
1 parent 75c84e8 commit 6653b0e

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var treemap = d3.treemap();
3131
* [Cluster](#cluster)
3232
* [Tree](#tree)
3333
* [Treemap](#treemap) ([Treemap Tiling](#treemap-tiling))
34+
* [Stacked Tree](#stackedtree)
3435
* [Partition](#partition)
3536
* [Pack](#pack)
3637

@@ -493,6 +494,54 @@ Like [d3.treemapSquarify](#treemapSquarify), except preserves the topology (node
493494
494495
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)
495496
497+
### Stacked Tree
498+
499+
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.
500+
501+
<a name="stackedtree" href="#stackedtree">#</a> d3.<b>stackedtree</b>() · [Source](https://github.com/d3/d3-hierarchy/blob/master/src/stackedtree.js), [Examples](https://observablehq.com/@martialblog/d3-stacked-tree)
502+
503+
Creates a new stacked tree layout with default settings.
504+
505+
<a name="_stackedtree" href="#_stackedtree">#</a> <i>stackedtree</i>(<i>root</i>)
506+
507+
Lays out the specified *root* [hierarchy](#hierarchy), assigning the following properties on *root* and its descendants:
508+
509+
* *node*.x - the *x*-coordinate of the node
510+
* *node*.y - the *y*-coordinate of the node
511+
512+
<a name="stackedtree_size" href="#stackedtree_size">#</a> <i>stackedtree</i>.<b>size</b>([<i>size</i>])
513+
514+
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.
515+
516+
<a name="stackedtree_nodeSize" href="#stackedtree_nodeSize">#</a> <i>stackedtree</i>.<b>nodeSize</b>([<i>size</i>])
517+
518+
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⟩.
519+
520+
<a name="stackedtree_separation" href="#stackedtree_separation">#</a> <i>stackedtree</i>.<b>separation</b>([<i>separation</i>])
521+
522+
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:
523+
524+
```js
525+
function separation(a, b) {
526+
return a.parent == b.parent ? 0 : 1;
527+
}
528+
```
529+
530+
<a name="stackedtree_stacking" href="#stackedtree_stacking">#</a> <i>stackedtree</i>.<b>stacking</b>([<i>stacking</i>])
531+
532+
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:
533+
534+
```js
535+
function stacking(a, b, n) {
536+
// With n being the length of the longest leaf array
537+
return a.parent === b.parent ? 1 / n : 0;
538+
}
539+
```
540+
541+
<a name="stackedtree_ratio" href="#stackedtree_ratio">#</a> <i>stackedtree</i>.<b>ratio</b>([<i>ratio</i>])
542+
543+
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.
544+
496545
### Partition
497546
498547
[<img alt="Partition" src="https://raw.githubusercontent.com/d3/d3-hierarchy/master/img/partition.png">](https://observablehq.com/@d3/icicle)

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export {default as pack} from "./pack/index.js";
44
export {default as packSiblings} from "./pack/siblings.js";
55
export {default as packEnclose} from "./pack/enclose.js";
66
export {default as partition} from "./partition.js";
7+
export {default as stackedtree} from "./stackedtree.js";
78
export {default as stratify} from "./stratify.js";
89
export {default as tree} from "./tree.js";
910
export {default as treemap} from "./treemap/index.js";

src/stackedtree.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
function defaultSeparation(a, b) {
2+
return a.parent === b.parent ? 0 : 1;
3+
}
4+
5+
function defaultStacking(a, b, n) {
6+
return a.parent === b.parent ? 1 / n : 0;
7+
}
8+
9+
function meanX(children) {
10+
return children.reduce(meanXReduce, 0) / children.length;
11+
}
12+
13+
function meanXReduce(x, c) {
14+
return x + c.x;
15+
}
16+
17+
function maxY(children) {
18+
return children.reduce(maxYReduce, 1);
19+
}
20+
21+
function maxYReduce(y, c) {
22+
return Math.max(y, c.y);
23+
}
24+
25+
function leafLeft(node) {
26+
var children;
27+
while (children = node.children) node = children[0];
28+
return node;
29+
}
30+
31+
function leafRight(node) {
32+
var children;
33+
while (children = node.children) node = children[children.length - 1];
34+
return node;
35+
}
36+
37+
export default function() {
38+
var separation = defaultSeparation,
39+
stacking = defaultStacking,
40+
ratio = 1,
41+
dx = 1,
42+
dy = 1,
43+
nodeSize = false;
44+
45+
function stackedtree(root) {
46+
var previousNode,
47+
stackHeight = 1,
48+
y = 0,
49+
x = 0;
50+
51+
// Find longest children array to calculate stacking distance
52+
root.each(function(node){
53+
var leaves = node.children;
54+
stackHeight = leaves ? Math.max(node.children.length, stackHeight) : stackHeight;
55+
})
56+
57+
// First walk, computing the initial x & y values.
58+
root.eachAfter(function(node) {
59+
60+
// TODO: Is this flexible enough?
61+
// Resetting y for new stack
62+
y = previousNode && previousNode.parent !== node.parent ? 0 : y;
63+
64+
var children = node.children;
65+
if (children) {
66+
node.x = meanX(children);
67+
node.y = ratio + maxY(children);
68+
} else {
69+
node.x = previousNode ? x += separation(node, previousNode) : 0;
70+
node.y = previousNode ? y += stacking(node, previousNode, stackHeight) : 0;
71+
previousNode = node;
72+
}
73+
});
74+
75+
var left = leafLeft(root),
76+
right = leafRight(root),
77+
x0 = left.x - separation(left, right) / 2,
78+
x1 = right.x + separation(right, left) / 2;
79+
80+
// Second walk, normalizing x & y to the desired size.
81+
return root.eachAfter(nodeSize ? function(node) {
82+
node.x = (node.x - root.x) * dx;
83+
node.y = (root.y - node.y) * dy;
84+
} : function(node) {
85+
node.x = (node.x - x0) / (x1 - x0) * dx;
86+
node.y = (1 - (root.y ? node.y / root.y : 1)) * dy;
87+
});
88+
}
89+
90+
stackedtree.separation = function(x) {
91+
return arguments.length ? (separation = x, stackedtree) : separation;
92+
};
93+
94+
stackedtree.stacking = function(y) {
95+
return arguments.length ? (stacking = y, stackedtree) : stacking;
96+
};
97+
98+
stackedtree.ratio = function(x) {
99+
// TODO: This a good solution?
100+
// Tree-to-Stack Ratio from 0 to 1 (default: 1)
101+
// Lower value means less emphasis on the tree, more on the stacks.
102+
return arguments.length ? (ratio = x, stackedtree) : ratio;
103+
};
104+
105+
stackedtree.size = function(x) {
106+
return arguments.length ? (nodeSize = false, dx = +x[0], dy = +x[1], stackedtree) : (nodeSize ? null : [dx, dy]);
107+
};
108+
109+
stackedtree.nodeSize = function(x) {
110+
return arguments.length ? (nodeSize = true, dx = +x[0], dy = +x[1], stackedtree) : (nodeSize ? [dx, dy] : null);
111+
};
112+
113+
return stackedtree;
114+
}

0 commit comments

Comments
 (0)