Skip to content

Commit 0a38761

Browse files
committed
Adapt treemap labels to node density
1 parent 045998c commit 0a38761

1 file changed

Lines changed: 118 additions & 16 deletions

File tree

apps/web/src/components/RunTreemap.tsx

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import { navigate } from "../lib/routing.js";
1515
const treemapWidth = 1200;
1616
const treemapHeight = 520;
1717
type TreemapLayoutNode = HierarchyRectangularNode<TreemapNode>;
18+
const horizontalPadding = 10;
19+
20+
type NodeTextContent = {
21+
primary: string | null;
22+
secondary: string | null;
23+
};
1824

1925
const nodePaddingTop = (depth: number): number => {
2026
if (depth === 1) {
@@ -28,19 +34,94 @@ const nodePaddingTop = (depth: number): number => {
2834
return 0;
2935
};
3036

31-
const shouldShowLabel = (node: TreemapLayoutNode): boolean => {
37+
const ellipsize = (value: string, maxChars: number): string | null => {
38+
if (maxChars <= 0) {
39+
return null;
40+
}
41+
42+
if (value.length <= maxChars) {
43+
return value;
44+
}
45+
46+
if (maxChars <= 1) {
47+
return null;
48+
}
49+
50+
return `${value.slice(0, Math.max(1, maxChars - 1)).trimEnd()}…`;
51+
};
52+
53+
const basename = (value: string): string => {
54+
const segments = value.split("/");
55+
return segments.at(-1) ?? value;
56+
};
57+
58+
const compactProcessLabel = (value: string): string => {
59+
const segments = value
60+
.split(" > ")
61+
.map((segment) => segment.trim())
62+
.filter(Boolean);
63+
64+
return segments.at(-1) ?? value;
65+
};
66+
67+
const maxCharactersForWidth = (width: number, charWidth: number): number =>
68+
Math.floor(width / charWidth);
69+
70+
const buildNodeTextContent = (node: TreemapLayoutNode): NodeTextContent => {
3271
const width = node.x1 - node.x0;
3372
const height = node.y1 - node.y0;
73+
const textWidth = Math.max(0, width - horizontalPadding * 2);
3474

3575
if (node.data.kind === "step") {
36-
return width >= 120 && height >= 48;
76+
if (width < 96 || height < 40) {
77+
return { primary: null, secondary: null };
78+
}
79+
80+
return {
81+
primary: ellipsize(node.data.label, maxCharactersForWidth(textWidth, 7.1)),
82+
secondary:
83+
width >= 150 && height >= 56
84+
? ellipsize(
85+
`${formatDurationMs(node.data.valueMs)} process time`,
86+
maxCharactersForWidth(textWidth, 6.2),
87+
)
88+
: null,
89+
};
3790
}
3891

3992
if (node.data.kind === "file") {
40-
return width >= 100 && height >= 42;
93+
if (width < 90 || height < 36) {
94+
return { primary: null, secondary: null };
95+
}
96+
97+
const fileLabel = basename(node.data.filePath ?? node.data.label);
98+
99+
return {
100+
primary: ellipsize(fileLabel, maxCharactersForWidth(textWidth, 6.9)),
101+
secondary:
102+
width >= 136 && height >= 48
103+
? ellipsize(formatDurationMs(node.data.valueMs), maxCharactersForWidth(textWidth, 6.1))
104+
: null,
105+
};
41106
}
42107

43-
return width >= 124 && height >= 48;
108+
if (width < 110 || height < 28) {
109+
return { primary: null, secondary: null };
110+
}
111+
112+
const processLabel = compactProcessLabel(node.data.label);
113+
const primary = ellipsize(
114+
processLabel,
115+
maxCharactersForWidth(textWidth, width >= 180 ? 6.8 : 6.3),
116+
);
117+
118+
return {
119+
primary,
120+
secondary:
121+
primary && width >= 164 && height >= 44
122+
? ellipsize(formatDurationMs(node.data.valueMs), maxCharactersForWidth(textWidth, 6.1))
123+
: null,
124+
};
44125
};
45126

46127
const buildTreemapLayout = (tree: TreemapNode): TreemapLayoutNode => {
@@ -122,17 +203,33 @@ export const TreemapView = ({
122203
role="img"
123204
viewBox={`0 0 ${treemapWidth} ${treemapHeight}`}
124205
>
125-
{renderedNodes.map((node) => {
206+
{renderedNodes.map((node, index) => {
126207
const width = node.x1 - node.x0;
127208
const height = node.y1 - node.y0;
128209
const targetPath = buildNodePath?.(node.data) ?? null;
210+
const text = buildNodeTextContent(node);
211+
const clipPathId = `treemap-clip-${index}`;
129212

130213
return (
131214
<g
132215
className={`treemapNode treemapDepth${node.depth}`}
133216
key={node.data.id}
134217
transform={`translate(${node.x0},${node.y0})`}
135218
>
219+
{text.primary || text.secondary ? (
220+
<defs>
221+
<clipPath id={clipPathId}>
222+
<rect
223+
height={Math.max(0, height - 8)}
224+
rx={node.data.kind === "process" ? 4 : 8}
225+
ry={node.data.kind === "process" ? 4 : 8}
226+
width={Math.max(0, width - horizontalPadding * 2)}
227+
x={horizontalPadding}
228+
y={6}
229+
/>
230+
</clipPath>
231+
</defs>
232+
) : null}
136233
<rect
137234
className={`treemapNodeRect status-${node.data.status}`}
138235
height={Math.max(0, height)}
@@ -164,19 +261,24 @@ export const TreemapView = ({
164261
);
165262
}}
166263
/>
167-
{shouldShowLabel(node) ? (
168-
<text className={`treemapLabel depth-${node.depth}`} x={10} y={20}>
169-
{node.data.label}
170-
</text>
171-
) : null}
172-
{shouldShowLabel(node) && node.data.kind !== "step" ? (
173-
<text className="treemapMeta" x={10} y={38}>
174-
{formatDurationMs(node.data.valueMs)}
264+
{text.primary ? (
265+
<text
266+
className={`treemapLabel depth-${node.depth}`}
267+
clipPath={`url(#${clipPathId})`}
268+
x={horizontalPadding}
269+
y={20}
270+
>
271+
{text.primary}
175272
</text>
176273
) : null}
177-
{shouldShowLabel(node) && node.data.kind === "step" ? (
178-
<text className="treemapMeta" x={10} y={44}>
179-
{formatDurationMs(node.data.valueMs)} process time
274+
{text.secondary ? (
275+
<text
276+
className="treemapMeta"
277+
clipPath={`url(#${clipPathId})`}
278+
x={horizontalPadding}
279+
y={node.data.kind === "step" ? 44 : 38}
280+
>
281+
{text.secondary}
180282
</text>
181283
) : null}
182284
</g>

0 commit comments

Comments
 (0)