@@ -15,6 +15,12 @@ import { navigate } from "../lib/routing.js";
1515const treemapWidth = 1200 ;
1616const treemapHeight = 520 ;
1717type TreemapLayoutNode = HierarchyRectangularNode < TreemapNode > ;
18+ const horizontalPadding = 10 ;
19+
20+ type NodeTextContent = {
21+ primary : string | null ;
22+ secondary : string | null ;
23+ } ;
1824
1925const 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
46127const 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