Skip to content

treeFilter option #1611

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 1 commit into from
May 22, 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
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
2 changes: 1 addition & 1 deletion 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 Down
63 changes: 26 additions & 37 deletions src/marks/tree.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {cluster as Cluster} from "d3";
import {isNoneish} from "../options.js";
import {tree as Tree, cluster as Cluster} from "d3";
import {marks} from "../mark.js";
import {isNoneish} from "../options.js";
import {maybeTreeAnchor, treeLink, treeNode} from "../transforms/tree.js";
import {filter} from "../transforms/basic.js";
import {dot} from "./dot.js";
import {link} from "./link.js";
import {text} from "./text.js";
Expand Down Expand Up @@ -32,8 +31,26 @@ export function tree(
...options
} = {}
) {
const {treeLayout = Tree} = options;
if (dx === undefined) dx = maybeTreeAnchor(options.treeAnchor).dx;
if (textAnchor !== undefined) throw new Error("textAnchor is not a configurable tree option");

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

return marks(
link(
data,
Expand All @@ -53,40 +70,12 @@ export function tree(
),
dotDot ? dot(data, treeNode({fill: fill === undefined ? "node:internal" : fill, title, ...options})) : null,
textText != null
? [
text(
data,
filter(
(d) => !d.internal,
treeNode({
text: textText,
fill: fill === undefined ? "currentColor" : fill,
stroke: textStroke,
dx,
dy,
title,
textAnchor: "start",
...options
})
)
),
text(
data,
filter(
"internal",
treeNode({
text: textText,
fill: fill === undefined ? "currentColor" : fill,
stroke: textStroke,
dx: -dx,
dy,
title,
textAnchor: "end",
...options
})
)
)
]
? treeLayout === Tree || treeLayout === Cluster
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just hard-coded this to apply only to the “tidy” tree and cluster layouts (and thereby avoiding it for the indent layout), but we could make this a separate option and use this heuristic as the default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably nicer (for dev exploration at least) to give function implementations of layouts the same "powers" that named layout have. An alternative if we don't want this to be a user-facing option, is to read a special property (.balancedLabels?) on the layout function. (The two layouts from D3 would be tested like you do here.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m in favor of an option if you want to make it configurable (and not some alternative mechanism). Perhaps a textBalanced boolean option? Or you could even override the textAnchor option to be mirrored maybe, but perhaps that’s fancier than it needs to be.

? [
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
23 changes: 15 additions & 8 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,15 +44,9 @@ 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] = {
data: node.data,
name: nodeName(node),
path: nodePath(node),
internal: nodeInternal(node),
depth: node.depth, // nodeDepth(node)
height: node.height // nodeHeight(node)
};
treeData[treeIndex] = node.data;
treeAnchor.position(node, treeIndex, X, Y);
for (const o of outputs) o[output_values][treeIndex] = o[output_evaluate](node);
}
Expand All @@ -73,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 @@ -109,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 @@ -201,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 @@ -229,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 @@ -257,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