@@ -12,6 +12,9 @@ interface NanovisVisualization {
1212 events : {
1313 on ( event : "select" | "hover" , callback : ( node : NanovisTreeNode ) => void ) : void ;
1414 } ;
15+ resize ( ) : void ;
16+ invalidate ( ) : void ;
17+ draw ( ) : void ;
1518}
1619
1720type VisualizationType = "flame" | "treemap" | "sunburst" ;
@@ -33,12 +36,25 @@ const FlamegraphLegend = () => {
3336 ) ;
3437} ;
3538
39+ /**
40+ * Formats a sample count for display.
41+ * Uses "k" suffix for thousands to keep the display compact.
42+ */
43+ function formatSampleCount ( count : number ) : string {
44+ if ( count >= 1000 ) {
45+ return `${ ( count / 1000 ) . toFixed ( 1 ) } k samples` ;
46+ }
47+ return `${ count } samples` ;
48+ }
49+
3650export default function TraceProfileTree ( { profile } : TraceProfileTreeProps ) {
3751 const containerRef = useRef < HTMLDivElement > ( null ) ;
3852 const visualizationRef = useRef < NanovisVisualization | null > ( null ) ;
53+ const resizeObserverRef = useRef < ResizeObserver | null > ( null ) ;
3954 const [ visualizationType , setVisualizationType ] = useState < VisualizationType > ( "flame" ) ;
4055 const [ hoveredNode , setHoveredNode ] = useState < NanovisTreeNode | null > ( null ) ;
4156 const [ mousePosition , setMousePosition ] = useState ( { x : 0 , y : 0 } ) ;
57+ const [ treeData , setTreeData ] = useState < NanovisTreeNode | null > ( null ) ;
4258
4359 useEffect ( ( ) => {
4460 const handleMouseMove = ( e : MouseEvent ) => {
@@ -49,6 +65,7 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
4965 if ( ! containerRef . current || ! profile ) return ;
5066
5167 const tree = await convertSentryProfileToNormalizedTree ( profile ) ;
68+ setTreeData ( tree ) ;
5269
5370 const nanovisModule = await import ( "nanovis" ) ;
5471 const { Flamegraph, Treemap, Sunburst } = nanovisModule ;
@@ -58,8 +75,18 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
5875 visualizationRef . current = null ;
5976 }
6077
78+ // Custom palette for Spotlight's dark theme
6179 const options = {
6280 getColor : ( node : TreeNode < unknown > ) => node . color ,
81+ palette : {
82+ text : "#e0e7ff" , // primary-100 for better visibility
83+ fg : "#fff" ,
84+ bg : "#1e1b4b" , // primary-950
85+ stroke : "#4338ca" , // primary-700
86+ fallback : "#9ca3af" , // Gray-400 fallback
87+ hover : "#ffffff33" , // Semi-transparent white for hover
88+ shadow : "#00000066" , // Semi-transparent black for shadows
89+ } ,
6390 } ;
6491
6592 let visualization : NanovisVisualization ;
@@ -86,12 +113,60 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
86113 } ) ;
87114
88115 if ( containerRef . current ) {
89- containerRef . current . appendChild ( visualization . el ) ;
116+ const container = containerRef . current ;
117+
118+ container . appendChild ( visualization . el ) ;
119+
120+ // Helper to update visualization dimensions (called on resize)
121+ // Only modifies visualization.el dimensions, NOT container dimensions
122+ const updateVisualization = ( ) => {
123+ if ( ! container || ! visualization ) return ;
124+
125+ // Get content area dimensions (excluding padding)
126+ const style = getComputedStyle ( container ) ;
127+ const paddingX = Number . parseFloat ( style . paddingLeft ) + Number . parseFloat ( style . paddingRight ) ;
128+ const paddingY = Number . parseFloat ( style . paddingTop ) + Number . parseFloat ( style . paddingBottom ) ;
129+ const contentWidth = container . clientWidth - paddingX ;
130+ const contentHeight = container . clientHeight - paddingY ;
131+
132+ if ( visualizationType === "treemap" ) {
133+ visualization . el . style . width = `${ contentWidth } px` ;
134+ visualization . el . style . height = `${ Math . max ( contentHeight , 400 ) } px` ;
135+ } else if ( visualizationType === "sunburst" ) {
136+ // Sunburst is square - use the smaller of width or a max height
137+ const size = Math . min ( contentWidth , window . innerHeight * 0.7 ) ;
138+ visualization . el . style . width = `${ size } px` ;
139+ visualization . el . style . height = `${ size } px` ;
140+ } else {
141+ // Flamegraph just needs width, calculates its own height
142+ visualization . el . style . width = `${ contentWidth } px` ;
143+ }
144+
145+ visualization . resize ( ) ;
146+ visualization . draw ( ) ;
147+ } ;
148+
149+ // Set up ResizeObserver to handle container resizes
150+ resizeObserverRef . current = new ResizeObserver ( ( ) => {
151+ updateVisualization ( ) ;
152+ } ) ;
153+ resizeObserverRef . current . observe ( container ) ;
154+
155+ // Initial sizing after DOM is ready
156+ requestAnimationFrame ( ( ) => {
157+ requestAnimationFrame ( ( ) => {
158+ requestAnimationFrame ( ( ) => {
159+ updateVisualization ( ) ;
160+ } ) ;
161+ } ) ;
162+ } ) ;
90163 }
91164 window . addEventListener ( "mousemove" , handleMouseMove ) ;
92165 } ) ( ) ;
93166
94167 return ( ) => {
168+ resizeObserverRef . current ?. disconnect ( ) ;
169+ resizeObserverRef . current = null ;
95170 if ( visualizationRef . current ) {
96171 visualizationRef . current . el . remove ( ) ;
97172 visualizationRef . current = null ;
@@ -128,8 +203,8 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
128203 } ) ;
129204
130205 return (
131- < div className = "w-full h-full relative p-4" >
132- < div className = "mb-4" >
206+ < div className = "w-full h-full flex flex-col p-4" >
207+ < div className = "mb-4 flex-shrink-0 " >
133208 < div className = "flex items-center justify-between mb-2" >
134209 < h3 className = "text-lg font-semibold text-primary-200" > Profile</ h3 >
135210 < div className = "flex items-center space-x-2" >
@@ -157,12 +232,29 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
157232 </ p >
158233 </ div >
159234 < FlamegraphLegend />
160- < div onMouseLeave = { ( ) => setHoveredNode ( null ) } >
235+ < div onMouseLeave = { ( ) => setHoveredNode ( null ) } className = "flex-1 min-h-0 mt-4" >
161236 < div
162237 ref = { containerRef }
163- className = "w-full border border-primary-700 rounded-md overflow-auto p-2 my-4"
238+ className = { cn (
239+ "border border-primary-700 rounded-md p-2 relative" ,
240+ // Flamegraph: full width, auto height, allow scroll for deep trees
241+ visualizationType === "flame" && "w-full overflow-auto" ,
242+ // Treemap: full width and height, no scrollbars (we control exact size)
243+ visualizationType === "treemap" && "w-full h-full min-h-[400px] overflow-hidden" ,
244+ // Sunburst: shrink-wrap to content (w-fit) and center, so overlay positions correctly
245+ visualizationType === "sunburst" && "mx-auto overflow-hidden" ,
246+ ) }
164247 { ...mouseTrackingProps }
165248 >
249+ { /* Sunburst center overlay - container is square so 50%/50% centers correctly */ }
250+ { visualizationType === "sunburst" && treeData && (
251+ < div
252+ className = "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary-950 px-3 py-1 rounded-md pointer-events-none z-10"
253+ style = { { minWidth : "80px" , textAlign : "center" } }
254+ >
255+ < span className = "text-primary-100 font-bold text-sm" > { formatSampleCount ( treeData . sampleCount ) } </ span >
256+ </ div >
257+ ) }
166258 { hoveredNode && (
167259 < div
168260 className = "bg-primary-900 border-primary-400 absolute flex flex-col min-w-[200px] rounded-lg border p-3 shadow-lg z-50"
@@ -174,7 +266,8 @@ export default function TraceProfileTree({ profile }: TraceProfileTreeProps) {
174266 >
175267 < span className = "text-primary-200 font-semibold" > { hoveredNode . text } </ span >
176268 < span className = "text-primary-400 text-xs" > { hoveredNode . subtext } </ span >
177- < span className = "text-primary-400 text-xs" > Total Time: { hoveredNode . size } </ span >
269+ { /* Use sampleCount if available, fall back to size (sampleCount may be lost during normalization on child nodes) */ }
270+ < span className = "text-primary-400 text-xs" > Samples: { hoveredNode . sampleCount ?? hoveredNode . size } </ span >
178271 </ div >
179272 ) }
180273 </ div >
0 commit comments