Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add StackedTree Layout #173

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}