Skip to content

balanced tree #1610

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

Merged
merged 7 commits into from
Jun 15, 2023
Merged
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
19 changes: 15 additions & 4 deletions docs/marks/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function indent() {

# Tree mark

The **tree mark** produces tree diagrams using the [tree transform](../transforms/tree.md). It is a [composite mark](../features/marks.md#marks-marks), consisting of a [link](./link.md) to render links from parent to child, an optional [dot](./dot.md) for nodes, and a [text](./text.md) for node labels. The link mark uses the [treeLink transform](../transforms/tree.md#treelink-options), while the dot and text marks use the [treeNode transform](../transforms/tree.md#treenode-options).
The **tree mark** produces tree diagrams using the [tree transform](../transforms/tree.md). It is a [composite mark](../features/marks.md#marks-marks), consisting of a [link](./link.md) to render links from parent to child, an optional [dot](./dot.md) for nodes, and one or two [text](./text.md) for node labels. The link mark uses the [treeLink transform](../transforms/tree.md#treelink-options), while the dot and text marks use the [treeNode transform](../transforms/tree.md#treenode-options).

For example, here is a little family tree of Greek gods.

Expand All @@ -41,7 +41,8 @@ For example, here is a little family tree of Greek gods.
Plot.plot({
axis: null,
height: 100,
margin: 20,
margin: 10,
marginLeft: 35,
marginRight: 120,
marks: [
Plot.tree(gods, {textStroke: "var(--vp-c-bg)"})
Expand All @@ -63,6 +64,7 @@ As a more complete example, here is a visualization of a software package hierar
Plot.plot({
axis: null,
margin: 10,
marginLeft: 30,
marginRight: 160,
width: 688,
height: 1800,
Expand All @@ -80,6 +82,7 @@ The **treeLayout** option specifies the layout algorithm. The tree mark uses the
Plot.plot({
axis: null,
margin: 10,
marginLeft: 30,
marginRight: 160,
width: 688,
height: 2400,
Expand Down Expand Up @@ -148,11 +151,19 @@ The following options are supported:
* **title** - the text and dot title; defaults to *node:path*
* **text** - the text label; defaults to *node:name*
* **textStroke** - the text stroke; defaults to *white*
* **dx** - the text horizontal offset; defaults to 6 if left-anchored, or -6 if right-anchored
* **textLayout** - the text anchoring layout
* **dx** - the text horizontal offset; defaults to 6
* **dy** - the text vertical offset; defaults to 0

Any additional *options* are passed through to the constituent link, dot, and text marks and their corresponding treeLink or treeNode transform.

The **textLayout** option controls how text labels are anchored to the node. Two layouts are supported:

* *mirrored* - leaf-node labels are left-anchored, and non-leaf nodes right-anchored
* *normal* - all labels are left-anchored

If the **treeLayout** is d3.tree or d3.cluster, the **textLayout** defaults to *mirrored*; otherwise it defaults to *normal*.

## tree(*data*, *options*)

```js
Expand All @@ -167,4 +178,4 @@ Returns a new tree mark with the given *data* and *options*.
Plot.cluster(flare, {path: "name", delimiter: "."})
```

Like [tree](#tree-data-options), except sets the **treeLayout** option to [d3.cluster](https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster), aligning leaf nodes.
Like [tree](#tree-data-options), except sets the **treeLayout** option to [d3.cluster](https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster), aligning leaf nodes, and defaults the **textLayout** option to *mirrored*.
2 changes: 2 additions & 0 deletions docs/transforms/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The treeNode transform will derive output columns for any *options* that have on
* *node:name* - the node’s name (the last part of its path)
* *node:path* - the node’s full, normalized, slash-separated path
* *node:internal* - true if the node is internal, or false for leaves
* *node:external* - true if the node is a leaf, or false for internal nodes
* *node:depth* - the distance from the node to the root
* *node:height* - the distance from the node to its deepest descendant

Expand All @@ -102,6 +103,7 @@ The treeLink transform will likewise derive output columns for any *options* tha
* *node:name* - the child node’s name (the last part of its path)
* *node:path* - the child node’s full, normalized, slash-separated path
* *node:internal* - true if the child node is internal, or false for leaves
* *node:external* - true if the child node is a leaf, or false for internal nodes
* *node:depth* - the distance from the child node to the root
* *node:height* - the distance from the child node to its deepest descendant
* *parent:name* - the parent node’s name (the last part of its path)
Expand Down
12 changes: 10 additions & 2 deletions src/marks/tree.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {DotOptions} from "./dot.js";
import type {LinkOptions} from "./link.js";
import type {TextOptions} from "./text.js";

// TODO tree channels, e.g., "node:name" | "node:path" | "node:internal"?
// TODO tree channels, e.g., "node:name" | "node:path" | "node:internal" | "node:external"?

/** Options for the compound tree mark. */
export interface TreeOptions extends DotOptions, LinkOptions, TextOptions, TreeTransformOptions {
Expand All @@ -19,6 +19,14 @@ export interface TreeOptions extends DotOptions, LinkOptions, TextOptions, TreeT
* atop other marks by creating a halo effect; defaults to *white*.
*/
textStroke?: MarkOptions["stroke"];

/**
* Layout for node labels: if *mirrored*, leaf-node labels are left-anchored,
* and non-leaf nodes right-anchored (with a -dx offset). If *normal*, all
* labels are left-anchored. Defaults to *mirrored* unless a **treeLayout**
* has been specified.
*/
textLayout?: "mirrored" | "normal";
}

/**
Expand All @@ -40,7 +48,7 @@ export function tree(data?: Data, options?: TreeOptions): CompoundMark;
* option, placing leaf nodes of the tree at the same depth. Equivalent to:
*
* ```js
* Plot.tree(data, {...options, treeLayout: d3.cluster})
* Plot.tree(data, {...options, treeLayout: d3.cluster, textLayout: "mirrored"})
* ```
*
* [1]: https://github.com/d3/d3-hierarchy/blob/main/README.md#cluster
Expand Down
51 changes: 36 additions & 15 deletions src/marks/tree.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {cluster as Cluster} from "d3";
import {isNoneish} from "../options.js";
import {cluster as Cluster, tree as Tree} from "d3";
import {marks} from "../mark.js";
import {isNoneish} from "../options.js";
import {maybeTreeAnchor, treeLink, treeNode} from "../transforms/tree.js";
import {dot} from "./dot.js";
import {link} from "./link.js";
import {text} from "./text.js";
import {keyword} from "../options.js";

export function tree(
data,
Expand All @@ -27,14 +28,38 @@ export function tree(
title = "node:path",
dx,
dy,
textAnchor,
treeLayout = Tree,
textLayout = treeLayout === Tree || treeLayout === Cluster ? "mirrored" : "normal",
...options
} = {}
) {
if (dx === undefined) dx = maybeTreeAnchor(options.treeAnchor).dx;
if (textAnchor !== undefined) throw new Error("textAnchor is not a configurable tree option");
textLayout = keyword(textLayout, "textLayout", ["mirrored", "normal"]);

function treeText(textOptions) {
return text(
data,
treeNode({
treeLayout,
text: textText,
fill: fill === undefined ? "currentColor" : fill,
stroke: textStroke,
dx,
dy,
title,
...textOptions,
...options
})
);
}

return marks(
link(
data,
treeLink({
treeLayout,
markerStart,
markerEnd,
stroke: stroke !== undefined ? stroke : fill === undefined ? "node:internal" : fill,
Expand All @@ -48,20 +73,16 @@ export function tree(
...options
})
),
dotDot ? dot(data, treeNode({fill: fill === undefined ? "node:internal" : fill, title, ...options})) : null,
dotDot
? dot(data, treeNode({treeLayout, fill: fill === undefined ? "node:internal" : fill, title, ...options}))
: null,
textText != null
? text(
data,
treeNode({
text: textText,
fill: fill === undefined ? "currentColor" : fill,
stroke: textStroke,
dx,
dy,
title,
...options
})
)
? textLayout === "mirrored"
? [
treeText({textAnchor: "start", treeFilter: "node:external"}),
treeText({textAnchor: "end", treeFilter: "node:internal", dx: -dx})
]
: treeText()
: null
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/transforms/tree.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface TreeTransformOptions {
* * *node:name* - the node’s name (the last part of its path)
* * *node:path* - the node’s full, normalized, slash-separated path
* * *node:internal* - true if the node is internal, or false for leaves
* * *node:external* - true if the node is a leaf, or false for internal nodes
* * *node:depth* - the distance from the node to the root
* * *node:height* - the distance from the node to its deepest descendant
*
Expand All @@ -97,6 +98,7 @@ export function treeNode<T>(options?: T & TreeTransformOptions): Transformed<T>;
* * *node:name* - the child node’s name (the last part of its path)
* * *node:path* - the child node’s full, normalized, slash-separated path
* * *node:internal* - true if the child node is internal, or false for leaves
* * *node:external* - true if the child node is a leaf, or false for external nodes
* * *node:depth* - the distance from the child node to the root
* * *node:height* - the distance from the child node to its deepest descendant
* * *parent:name* - the parent node’s name (the last part of its path)
Expand Down
14 changes: 14 additions & 0 deletions src/transforms/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export function treeNode({
treeSort,
treeSeparation,
treeAnchor,
treeFilter,
...options
} = {}) {
treeAnchor = maybeTreeAnchor(treeAnchor);
treeSort = maybeTreeSort(treeSort);
if (treeFilter != null) treeFilter = maybeNodeValue(treeFilter);
if (frameAnchor === undefined) frameAnchor = treeAnchor.frameAnchor;
const normalize = normalizer(delimiter);
const outputs = treeOutputs(options, maybeNodeValue);
Expand Down Expand Up @@ -42,6 +44,7 @@ export function treeNode({
if (treeSort != null) root.sort(treeSort);
layout(root);
for (const node of root.descendants()) {
if (treeFilter != null && !treeFilter(node)) continue;
treeFacet.push(++treeIndex);
treeData[treeIndex] = node.data;
treeAnchor.position(node, treeIndex, X, Y);
Expand All @@ -66,10 +69,12 @@ export function treeLink({
treeSort,
treeSeparation,
treeAnchor,
treeFilter,
...options
} = {}) {
treeAnchor = maybeTreeAnchor(treeAnchor);
treeSort = maybeTreeSort(treeSort);
if (treeFilter != null) treeFilter = maybeLinkValue(treeFilter);
options = {curve, stroke, strokeWidth, strokeOpacity, ...options};
const normalize = normalizer(delimiter);
const outputs = treeOutputs(options, maybeLinkValue);
Expand Down Expand Up @@ -102,6 +107,7 @@ export function treeLink({
if (treeSort != null) root.sort(treeSort);
layout(root);
for (const {source, target} of root.links()) {
if (treeFilter != null && !treeFilter(target, source)) continue;
treeFacet.push(++treeIndex);
treeData[treeIndex] = target.data;
treeAnchor.position(source, treeIndex, X1, Y1);
Expand Down Expand Up @@ -194,6 +200,8 @@ function maybeNodeValue(value) {
return nodePath;
case "node:internal":
return nodeInternal;
case "node:external":
return nodeExternal;
case "node:depth":
return nodeDepth;
case "node:height":
Expand Down Expand Up @@ -222,6 +230,8 @@ function maybeLinkValue(value) {
return nodePath;
case "node:internal":
return nodeInternal;
case "node:external":
return nodeExternal;
case "node:depth":
return nodeDepth;
case "node:height":
Expand Down Expand Up @@ -250,6 +260,10 @@ function nodeInternal(node) {
return !!node.children;
}

function nodeExternal(node) {
return !node.children;
}

function parentValue(evaluate) {
return (child, parent) => (parent == null ? undefined : evaluate(parent));
}
Expand Down
Loading