@@ -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
8385interface 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-
112108export 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 }
0 commit comments