Skip to content
Draft
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
36 changes: 36 additions & 0 deletions src/marks/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export class Text extends Mark {
const {
x,
y,
x1,
x2,
y1,
y2,
text = isIterable(data) && isTextual(data) ? identity : indexOf,
frameAnchor,
textAnchor = /right$/i.test(frameAnchor) ? "end" : /left$/i.test(frameAnchor) ? "start" : "middle",
Expand All @@ -65,6 +69,10 @@ export class Text extends Mark {
{
x: {value: x, scale: "x", optional: true},
y: {value: y, scale: "y", optional: true},
x1: {value: x1, scale: "x", optional: true},
y1: {value: y1, scale: "y", optional: true},
x2: {value: x2, scale: "x", optional: true},
y2: {value: y2, scale: "y", optional: true},
fontSize: {value: vfontSize, optional: true},
rotate: {value: numberChannel(vrotate), optional: true},
text: {value: text, filter: nonempty, optional: true}
Expand Down Expand Up @@ -94,6 +102,12 @@ export class Text extends Mark {
const {x: X, y: Y, rotate: R, text: T, title: TL, fontSize: FS} = channels;
const {rotate} = this;
const [cx, cy] = applyFrameAnchor(this, dimensions);
let clipBox = (s) => s;
if (this.clip === "box") {
if (!channels.x1 || !channels.y1 || !channels.x2 || !channels.y2)
throw new Error("box clipping requires x1, y1, x2, and y2 channels.");
clipBox = (selection) => applyClip(selection, channels, 2); // TODO configurable clipInset
}
return create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyIndirectTextStyles, this, T, dimensions)
Expand All @@ -114,11 +128,33 @@ export class Text extends Mark {
)
.call(applyAttr, "font-size", FS && ((i) => FS[i]))
.call(applyChannelStyles, this, channels)
.call(clipBox)
)
.node();
}
}

function applyClip(selection, {x1: X1, x2: X2, y1: Y1, y2: Y2}, clipInset) {
return selection.each(function (i) {
const g = this.ownerDocument.createElementNS(namespaces.svg, "svg");
const x = Math.min(X1[i], X2[i]) + clipInset;
const y = Math.min(Y1[i], Y2[i]) + clipInset;
const w = Math.abs(X1[i] - X2[i]) - 2 * clipInset;
const h = Math.abs(Y1[i] - Y2[i]) - 2 * clipInset;
if (w <= 0 || h <= 0) {
this.parentElement.removeChild(this);
} else {
g.setAttribute("viewBox", `${x} ${y} ${w} ${h}`);
g.setAttribute("x", `${x}`);
g.setAttribute("y", `${y}`);
g.setAttribute("width", `${w}`);
g.setAttribute("height", `${h}`);
this.replaceWith(g);
g.appendChild(this);
}
});
}

export function maybeTextOverflow(textOverflow) {
return textOverflow == null
? null
Expand Down
7 changes: 4 additions & 3 deletions src/marks/treemap.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {ChannelValue} from "../channel.js";
import {Data} from "../mark.js";
import {Markish, Data} from "../mark.js";
import {TreeTransformOptions} from "../transforms/tree.js";
import {Rect, RectOptions} from "./rect.js";
import {RectOptions} from "./rect.js";

// TODO tree channels, e.g., "node:name" | "node:path" | "node:internal"?
export interface TreemapOptions extends RectOptions, TreeTransformOptions {
text?: ChannelValue;
value?: ChannelValue;
}

export function treemap(data?: Data, options?: TreemapOptions): Rect;
export function treemap(data?: Data, options?: TreemapOptions): Markish;
49 changes: 47 additions & 2 deletions src/marks/treemap.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
import {treemapNode} from "../transforms/treemap.js";
import {marks} from "../mark.js";
import {rect} from "./rect.js";
import {text as textMark} from "./text.js";

/** @jsdoc treemap */
export function treemap(
data,
{inset = 0.5, insetTop = inset, insetRight = inset, insetBottom = inset, insetLeft = inset, ...options} = {}
{
inset = 0.5,
insetTop = inset,
insetRight = inset,
insetBottom = inset,
insetLeft = inset,
text = "node:name",
clip = "box",
value,
title = value != null ? "node:title" : "node:name",
...options
} = {}
) {
return rect(data, treemapNode({insetTop, insetRight, insetBottom, insetLeft, ...options}));
const r = rect(
data,
treemapNode({
value,
insetTop,
insetRight,
insetBottom,
insetLeft,
title,
...options
})
);
return text == null
? r
: marks([
r,
textMark(
data,
treemapNode({
value,
insetTop,
insetRight,
insetBottom,
insetLeft,
text,
clip,
...options,
fill: "currentColor",
pointerEvents: "none",
tip: false
})
)
]);
}
6 changes: 5 additions & 1 deletion src/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export function* groupIndex(I, position, mark, channels) {
export function maybeClip(clip) {
if (clip === true) clip = "frame";
else if (clip === false) clip = null;
return maybeKeyword(clip, "clip", ["frame", "sphere"]);
return maybeKeyword(clip, "clip", ["frame", "sphere", "box"]);
}

// Note: may mutate selection.node!
Expand Down Expand Up @@ -351,6 +351,10 @@ function applyClip(selection, mark, dimensions, context) {
.attr("d", geoPath(projection)({type: "Sphere"}));
break;
}
case "box": {
// TODO, see text
// console.warn({selection, mark, dimensions, context});
}
}
// Here we’re careful to apply the ARIA attributes to the outer G element when
// clipping is applied, and to apply the ARIA attributes before any other
Expand Down
22 changes: 22 additions & 0 deletions src/transforms/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ export function maybeNodeValue(value) {
return nodeName;
case "node:path":
return nodePath;
case "node:branch":
return nodeBranch;
case "node:internal":
return nodeInternal;
case "node:external":
Expand All @@ -206,6 +208,10 @@ export function maybeNodeValue(value) {
return nodeDepth;
case "node:height":
return nodeHeight;
case "node:size":
return nodeSize;
case "node:title":
return nodeTitle;
}
throw new Error(`invalid node value: ${value}`);
}
Expand Down Expand Up @@ -244,6 +250,14 @@ function nodePath(node) {
return node.id;
}

function nodeAncestor(node, i) {
return nameof(node.ancestors().at(i)?.id ?? "");
}

function nodeBranch(node) {
return nodeAncestor(node, -2);
}

function nodeName(node) {
return nameof(node.id);
}
Expand All @@ -256,6 +270,14 @@ function nodeHeight(node) {
return node.height;
}

function nodeSize(node) {
return node.size;
}

function nodeTitle(node) {
return `${nameof(node.id)}\n${node.data.size}`;
}

function nodeInternal(node) {
return !!node.children;
}
Expand Down
24 changes: 22 additions & 2 deletions src/transforms/treemap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {stratify, treemap, treemapBinary} from "d3";
import {column, identity, valueof} from "../options.js";
import {max, stratify, treemap, treemapBinary} from "d3";
import {column, identity, mid, valueof} from "../options.js";
import {basic} from "./basic.js";
import {maybeNodeValue, maybeTreeSort, normalizer} from "./tree.js";
import {output_evaluate, output_setValues, output_values, treeOutputs} from "./tree.js";
Expand Down Expand Up @@ -28,6 +28,8 @@ export function treemapNode(options = {}) {
y1: Y1,
x2: X2,
y2: Y2,
x: mid(X1, X2),
y: mid(Y1, Y2),
frameAnchor,
...basic(remainingOptions, (data, facets) => {
const P = normalize(valueof(data, path));
Expand All @@ -41,11 +43,13 @@ export function treemapNode(options = {}) {
const rootof = stratify().path((i) => P[i]);
const layout = treemap().tile(treeTile);
for (const o of outputs) o[output_values] = o[output_setValues]([]);
const sums = [];
for (const facet of facets) {
const treeFacet = [];
const root = rootof(facet.filter((i) => P[i] != null)).each((node) => (node.data = data[node.data]));
if (value) root.sum(value);
else root.count();
sums.push(root.value);
if (treeSort != null) root.sort(treeSort);
layout(root);
for (const node of root.leaves()) {
Expand All @@ -59,6 +63,22 @@ export function treemapNode(options = {}) {
}
treeFacets.push(treeFacet);
}
// normalize facets
if (facets.length > 1) {
const m = max(sums);
for (let i = 0; i < facets.length; ++i) {
const r = Math.sqrt(sums[i] / m);
if (r < 1) {
for (const j of treeFacets[i]) {
X1[j] = 0.5 + r * (X1[j] - 0.5); // centered
X2[j] = 0.5 + r * (X2[j] - 0.5);
Y1[j] = 0.5 + r * (Y1[j] - 0.5);
Y2[j] = 0.5 + r * (Y2[j] - 0.5);
}
}
}
}

return {data: treeData, facets: treeFacets};
}),
...Object.fromEntries(outputs)
Expand Down
Loading