Skip to content

Commit 99fd836

Browse files
authored
fix(profiles): Fix treemap and sunburst display issues (#1203)
## Summary Fix two issues with the Profiles tab under trace views: 1. **Sunburst chart showing byte sizes (e.g., '42kb')**: The nanovis library interprets the `size` field as bytes. Added an overlay to cover the center text and display proper sample counts instead. 2. **Treemap and sunburst colors appearing white/invisible**: rendering issue ## Changes - Flamegraph: Renders immediately on load (triple rAF + direct draw call) - Treemap: Proper dimensions with explicit pixel sizing - Sunburst: Square aspect ratio with centered sample count overlay - Added sunburst center overlay showing sample counts - Fixed tooltip to show 'Samples' instead of 'Total Time'
1 parent f8173b3 commit 99fd836

File tree

4 files changed

+115
-12
lines changed

4 files changed

+115
-12
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@spotlightjs/spotlight": patch
3+
---
4+
5+
Fix profile visualization issues in trace views:
6+
- Update frame colors to use vibrant, high-contrast colors for better visibility
7+
- Add custom nanovis palette for Spotlight's dark theme
8+
- Fix sunburst center text showing bytes instead of sample counts
9+
- Fix treemap visibility with proper color contrast
10+

packages/spotlight/src/ui/index.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
--color-primary-900: #312e81;
1616
--color-primary-950: #1e1b4b;
1717

18-
--color-frame-application: #d0d1f5;
19-
--color-frame-system: #ffe0e4;
20-
--color-frame-unknown: #f3f4f6;
18+
--color-frame-application: #6366f1;
19+
--color-frame-system: #f87171;
20+
--color-frame-unknown: #9ca3af;
2121
}
2222

2323
ul.tree {

packages/spotlight/src/ui/telemetry/components/traces/TraceDetails/components/TraceProfileTree.tsx

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1720
type 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+
3650
export 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>

packages/spotlight/src/ui/telemetry/constants/profile.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const FRAME_COLOR = {
2-
application: "#d0d1f5",
3-
system: "#ffe0e4",
4-
unknown: "#f3f4f6",
2+
application: "#6366f1", // Indigo - matches primary-500
3+
system: "#f87171", // Red-400 - distinct system color
4+
unknown: "#9ca3af", // Gray-400
55
};
66

77
export const FRAME_TYPES = [

0 commit comments

Comments
 (0)