Skip to content

Commit 85bc085

Browse files
committed
refactor(vortex-web): render treemap to leaves with a path header
The one-level drill view hid the blocks (a dominant field was just one big empty tile). Render the full nested tree down to the leaves again so every physical block is visible, but label only leaf blocks — parent names come from a single path header at the top, which shows the path to the block under the cursor (e.g. "root / amount / data / [4]"). Because containers carry no inline label, nothing overlaps. Click a block to select it (flat blocks expand in place to reveal their buffers); click a path segment to focus that subtree. Signed-off-by: Robert <robert@spiraldb.com>
1 parent 0ac9772 commit 85bc085

6 files changed

Lines changed: 42 additions & 50 deletions

File tree

vortex-web/README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ A web UI for exploring Vortex data files, built with React, TypeScript, Tailwind
44

55
## Treemap Explorer
66

7-
The **Treemap** view (toggle in the header, next to the theme picker) is a
8-
drill-down map of a file's physical blocks. The box shows the current layout's
9-
blocks, sized by byte footprint and coloured by dtype to match the swimlane and
10-
detail treemap; a header bar at the top is the current path. Click a container
11-
block to drill into it, and click a segment of the path header to drill back
12-
out. It follows the app theme — dark by default, matching explore.vortex.dev —
13-
and the light theme via the theme picker.
7+
The **Treemap** view (toggle in the header, next to the theme picker) renders a
8+
file's physical blocks all the way down to the leaves: layouts nest, flat
9+
layouts subdivide into their array-encoding buffers, and every block is sized by
10+
its byte footprint and coloured by dtype to match the swimlane and detail
11+
treemap. Only leaf blocks are labelled inline, so labels never collide; a header
12+
bar at the top is the path to the block under the cursor. Click a path segment
13+
to focus that subtree, and click a flat block to expand its buffers in place. It
14+
follows the app theme — dark by default, matching explore.vortex.dev — and the
15+
light theme via the theme picker.
1416

1517
![Treemap explorer overview](docs/img/treemap-explorer-overview.png)
1618

-30.6 KB
Loading
47 KB
Loading
44.2 KB
Loading

vortex-web/src/components/explorer/BlockTreemap.tsx

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,22 @@ function subtreeBytes(node: LayoutTreeNode, segmentMap: Map<number, SegmentMapEn
6464
);
6565
}
6666

67-
/** The child blocks to show inside a node, one level deep. Each tile aggregates
68-
* its own subtree's bytes; drilling re-roots the box at it. Flat / array layouts
69-
* expose their array-encoding children; other layouts hide them until expanded. */
70-
function childTiles(node: LayoutTreeNode, segmentMap: Map<number, SegmentMapEntry>): TreeNode[] {
67+
/** Build the full nested treemap for a subtree so every physical block is
68+
* visible. Flat / array layouts expose their array-encoding children; other
69+
* layouts hide array children until expanded. Tiles are coloured by dtype. */
70+
function buildTree(node: LayoutTreeNode, segmentMap: Map<number, SegmentMapEntry>): TreeNode {
7171
const isArray = node.isArrayNode ?? false;
7272
const isFlatOrArray = isArray || node.encoding === 'vortex.flat';
7373
const children = isFlatOrArray ? node.children : node.children.filter((c) => !c.isArrayNode);
74-
return children.map((c) => ({
75-
name: getNodeDisplayName(c),
76-
nodeId: c.id,
77-
color: DTYPE_COLORS[getDtypeCategory(c.dtype)],
78-
bytes: Math.max(subtreeBytes(c, segmentMap), 1),
79-
layoutNode: c,
80-
}));
74+
const base = {
75+
name: getNodeDisplayName(node),
76+
nodeId: node.id,
77+
color: DTYPE_COLORS[getDtypeCategory(node.dtype)],
78+
bytes: Math.max(subtreeBytes(node, segmentMap), 1),
79+
layoutNode: node,
80+
};
81+
if (children.length === 0) return base;
82+
return { ...base, children: children.map((c) => buildTree(c, segmentMap)) };
8183
}
8284

8385
interface ThemeColors {
@@ -103,12 +105,6 @@ interface Tooltip {
103105
y: number;
104106
}
105107

106-
/** A node is drillable if it has child blocks, or is a flat layout whose array
107-
* encoding tree can still be expanded. */
108-
function isDrillable(node: LayoutTreeNode): boolean {
109-
return node.children.length > 0 || isFlatLayout(node);
110-
}
111-
112108
export function BlockTreemap({
113109
root,
114110
segments,
@@ -133,18 +129,7 @@ export function BlockTreemap({
133129
// Current drill root (the box shows this node's blocks). Falls back to the
134130
// file root if the drilled node disappears.
135131
const drillNode = useMemo(() => findNodeById(root, drillId) ?? root, [root, drillId]);
136-
const tree = useMemo<TreeNode>(
137-
() => ({
138-
name: getNodeDisplayName(drillNode),
139-
nodeId: drillNode.id,
140-
color: DTYPE_COLORS[getDtypeCategory(drillNode.dtype)],
141-
bytes: 1,
142-
layoutNode: drillNode,
143-
children: childTiles(drillNode, segmentMap),
144-
}),
145-
[drillNode, segmentMap],
146-
);
147-
const drillPath = useMemo(() => findPathToNode(root, drillNode.id), [root, drillNode.id]);
132+
const tree = useMemo(() => buildTree(drillNode, segmentMap), [drillNode, segmentMap]);
148133

149134
// Track the box size (area below the path header).
150135
useEffect(() => {
@@ -193,6 +178,11 @@ export function BlockTreemap({
193178
const activeHover = localHover ?? hoveredNodeId;
194179
const hoveredNode = activeHover ? findNodeById(root, activeHover) : null;
195180

181+
// Path shown in the header: to the hovered block while hovering (so any block
182+
// can be identified), otherwise to the focused drill node.
183+
const headerNode = hoveredNode ?? drillNode;
184+
const headerPath = useMemo(() => findPathToNode(root, headerNode.id), [root, headerNode.id]);
185+
196186
/** Deepest tile (excluding the drill root itself) containing a point. */
197187
const hitTest = useCallback(
198188
(px: number, py: number): RectNode | null => {
@@ -251,18 +241,19 @@ export function BlockTreemap({
251241
const hit = hitTest(p.px, p.py);
252242
if (!hit) return;
253243
const node = hit.data.layoutNode;
254-
if (isDrillable(node)) drillTo(node);
255-
else onSelectNode(node.id);
244+
onSelectNode(node.id);
245+
// Expand a flat block in place so its array buffers render down to leaves.
246+
if (isFlatLayout(node) && !node.children.some((c) => c.isArrayNode)) onExpand?.(node.id);
256247
},
257-
[hitTest, localPoint, drillTo, onSelectNode],
248+
[hitTest, localPoint, onSelectNode, onExpand],
258249
);
259250

260251
return (
261252
<div className="flex flex-col w-full h-full">
262253
{/* Path header — the box's title bar showing the current location. */}
263254
<div className="flex items-center gap-1 flex-shrink-0 h-6 px-2 border-b border-vortex-grey-light/40 dark:border-white/[0.06] bg-vortex-grey-lightest dark:bg-white/[0.03] text-[11px] font-mono overflow-hidden">
264-
{drillPath.map((node, i) => {
265-
const isLast = i === drillPath.length - 1;
255+
{headerPath.map((node, i) => {
256+
const isLast = i === headerPath.length - 1;
266257
return (
267258
<span key={node.id} className="flex items-center gap-1 min-w-0 flex-shrink-0">
268259
{i > 0 && <span className="text-vortex-grey-dark opacity-40">/</span>}
@@ -274,16 +265,17 @@ export function BlockTreemap({
274265
<button
275266
className="text-vortex-grey-dark hover:text-vortex-light-blue truncate"
276267
onClick={() => drillTo(node)}
268+
title={`Focus ${getNodeDisplayName(node)}`}
277269
>
278270
{getNodeDisplayName(node)}
279271
</button>
280272
)}
281273
</span>
282274
);
283275
})}
284-
{hoveredNode && hoveredNode.id !== drillNode.id && (
276+
{hoveredNode && (
285277
<span className="ml-auto flex-shrink-0 text-vortex-grey-dark truncate">
286-
{getNodeDisplayName(hoveredNode)} · {shortEncoding(hoveredNode.encoding)}
278+
{shortEncoding(hoveredNode.encoding)} · {hoveredNode.dtype}
287279
</span>
288280
)}
289281
</div>
@@ -310,7 +302,6 @@ export function BlockTreemap({
310302
const isLeaf = !n.children || n.children.length === 0;
311303
const isHovered = d.nodeId === activeHover;
312304
const isSelected = selectedSubtreeIds.has(d.nodeId);
313-
const drillable = isDrillable(d.layoutNode);
314305
const maxChars = Math.floor((w - 6) / 6);
315306
const label = maxChars < 2 ? '' : truncate(d.name, maxChars);
316307

@@ -326,9 +317,9 @@ export function BlockTreemap({
326317
stroke={isHovered ? theme.highlight : theme.border}
327318
strokeWidth={isHovered ? 2 : isLeaf ? 0.5 : 1}
328319
/>
329-
{/* Every tile is one level deep, so its label never collides
330-
with nested content. A › marks a block you can drill into. */}
331-
{label && h > 14 && (
320+
{/* Only leaf blocks are labelled, so labels never collide with
321+
nested content. Parent names come from the path header. */}
322+
{isLeaf && label && h > 14 && (
332323
<text
333324
x={n.x0 + 4}
334325
y={n.y0 + 12}
@@ -337,10 +328,9 @@ export function BlockTreemap({
337328
fontFamily="'Geist Mono', monospace"
338329
>
339330
{label}
340-
{drillable ? ' ›' : ''}
341331
</text>
342332
)}
343-
{w > 50 && h > 28 && (
333+
{isLeaf && w > 50 && h > 28 && (
344334
<text
345335
x={n.x0 + 4}
346336
y={n.y0 + 23}

vortex-web/src/components/explorer/TreemapExplorer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function TreemapExplorer() {
5858
</div>
5959
))}
6060
<span className="ml-auto flex-shrink-0">
61-
click a block to drill in · click the path to go back
61+
click a block to select · click the path to focus a subtree
6262
</span>
6363
</div>
6464

0 commit comments

Comments
 (0)