Skip to content

Commit

Permalink
Add StackedTree Layout
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
martialblog committed Oct 10, 2020
1 parent 75c84e8 commit 6653b0e
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.
<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)
Creates a new stacked tree layout with default settings.
<a name="_stackedtree" href="#_stackedtree">#</a> <i>stackedtree</i>(<i>root</i>)
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
<a name="stackedtree_size" href="#stackedtree_size">#</a> <i>stackedtree</i>.<b>size</b>([<i>size</i>])
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.
<a name="stackedtree_nodeSize" href="#stackedtree_nodeSize">#</a> <i>stackedtree</i>.<b>nodeSize</b>([<i>size</i>])
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⟩.
<a name="stackedtree_separation" href="#stackedtree_separation">#</a> <i>stackedtree</i>.<b>separation</b>([<i>separation</i>])
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;
}
```
<a name="stackedtree_stacking" href="#stackedtree_stacking">#</a> <i>stackedtree</i>.<b>stacking</b>([<i>stacking</i>])
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;
}
```
<a name="stackedtree_ratio" href="#stackedtree_ratio">#</a> <i>stackedtree</i>.<b>ratio</b>([<i>ratio</i>])
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
[<img alt="Partition" src="https://raw.githubusercontent.com/d3/d3-hierarchy/master/img/partition.png">](https://observablehq.com/@d3/icicle)
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
114 changes: 114 additions & 0 deletions src/stackedtree.js
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 6653b0e

Please sign in to comment.