Skip to content

Commit cd34d55

Browse files
authored
feat: Profile sunburst chart (#3785)
1 parent c13213e commit cd34d55

File tree

1 file changed

+114
-95
lines changed

1 file changed

+114
-95
lines changed

src/ui/SunburstChart/SunburstChart.jsx

Lines changed: 114 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Sentry from '@sentry/react'
12
import { interpolate } from 'd3-interpolate'
23
import { scaleSequential } from 'd3-scale'
34
import { select } from 'd3-selection'
@@ -26,38 +27,40 @@ function SunburstChart({
2627
const hoverHandler = useRef(onHover)
2728

2829
// this state stores the root node of the sunburst chart
29-
const [root] = useState(() => {
30-
// go through the data and add `value` to each node
31-
const stack = [data]
32-
const nodeMap = new Map()
33-
34-
// create a new root node with the value of the root node
35-
const result = { ...data, value: selectorHandler.current(data) }
36-
// add the root node to the node map
37-
nodeMap.set(data, result)
38-
39-
// while there are nodes to process, pop the last node from the stack
40-
while (stack.length > 0) {
41-
const node = stack.pop()
42-
const currentNode = nodeMap.get(node)
43-
44-
// if the node has children, process them
45-
if (Array.isArray(node.children)) {
46-
currentNode.children = node.children.map((child) => {
47-
// sad ... some browsers still lack support for structuredClone
48-
const newChild = JSON.parse(JSON.stringify(child))
49-
Object.assign(newChild, { value: selectorHandler.current(child) })
50-
51-
nodeMap.set(child, newChild)
52-
stack.push(child)
53-
return newChild
54-
})
30+
const [root] = useState(() =>
31+
Sentry.startSpan({ name: 'SunburstChart.createRoot' }, () => {
32+
// go through the data and add `value` to each node
33+
const stack = [data]
34+
const nodeMap = new Map()
35+
36+
// create a new root node with the value of the root node
37+
const result = { ...data, value: selectorHandler.current(data) }
38+
// add the root node to the node map
39+
nodeMap.set(data, result)
40+
41+
// while there are nodes to process, pop the last node from the stack
42+
while (stack.length > 0) {
43+
const node = stack.pop()
44+
const currentNode = nodeMap.get(node)
45+
46+
// if the node has children, process them
47+
if (Array.isArray(node.children)) {
48+
currentNode.children = node.children.map((child) => {
49+
// sad ... some browsers still lack support for structuredClone
50+
const newChild = JSON.parse(JSON.stringify(child))
51+
Object.assign(newChild, { value: selectorHandler.current(child) })
52+
53+
nodeMap.set(child, newChild)
54+
stack.push(child)
55+
return newChild
56+
})
57+
}
5558
}
56-
}
5759

58-
// partition the data and add the `current` property to each node
59-
return partitionFn(result).each((d) => (d.current = d))
60-
})
60+
// partition the data and add the `current` property to each node
61+
return partitionFn(result).each((d) => (d.current = d))
62+
})
63+
)
6164

6265
// In this case D3 is handling rendering not React, so useLayoutEffect is used to handle rendering
6366
// and changes outside of the React lifecycle.
@@ -72,20 +75,24 @@ function SunburstChart({
7275
const radius = width / 6
7376

7477
// Creates a function for creating arcs representing files and folders.
75-
const drawArc = arc()
76-
.startAngle((d) => d.x0)
77-
.endAngle((d) => d.x1)
78-
.padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.005))
79-
.padRadius(radius * 1.5)
80-
.innerRadius((d) => d.y0 * radius)
81-
.outerRadius((d) => Math.max(d.y0 * radius, d.y1 * radius - 1))
78+
const drawArc = Sentry.startSpan({ name: 'SunburstChart.drawArc' }, () => {
79+
return arc()
80+
.startAngle((d) => d.x0)
81+
.endAngle((d) => d.x1)
82+
.padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.005))
83+
.padRadius(radius * 1.5)
84+
.innerRadius((d) => d.y0 * radius)
85+
.outerRadius((d) => Math.max(d.y0 * radius, d.y1 * radius - 1))
86+
})
8287

8388
// A color function you can pass a number from 0-100 to and get a color back from the specified color range
8489
// Ex color(10.4)
85-
const color = scaleSequential()
86-
.domain([colorDomainMin, colorDomainMax])
87-
.interpolator(colorRange)
88-
.clamp(true)
90+
const color = Sentry.startSpan({ name: 'SunburstChart.color' }, () => {
91+
return scaleSequential()
92+
.domain([colorDomainMin, colorDomainMax])
93+
.interpolator(colorRange)
94+
.clamp(true)
95+
})
8996

9097
// Tracks previous location for rendering .. in the breadcrumb.
9198
let previous
@@ -99,18 +106,22 @@ function SunburstChart({
99106
.attr('transform', `translate(${width / 2},${width / 2})`)
100107

101108
// Renders an arc per data point in the correct location. (Pieces of the circle that add up to a circular graph)
102-
const path = g
103-
.append('g')
104-
.selectAll('path')
105-
.data(root.descendants().slice(1))
106-
.join('path')
107-
.attr('fill', (d) => color(d?.data?.value || 0))
108-
// If data point is a file fade the background color a bit.
109-
.attr('fill-opacity', (d) =>
110-
arcVisible(d.current) ? (d.children ? 1 : 0.6) : 0
111-
)
112-
.attr('pointer-events', (d) => (arcVisible(d.current) ? 'auto' : 'none'))
113-
.attr('d', (d) => drawArc(d.current))
109+
const path = Sentry.startSpan({ name: 'SunburstChart.renderArcs' }, () =>
110+
g
111+
.append('g')
112+
.selectAll('path')
113+
.data(root.descendants().slice(1))
114+
.join('path')
115+
.attr('fill', (d) => color(d?.data?.value || 0))
116+
// If data point is a file fade the background color a bit.
117+
.attr('fill-opacity', (d) =>
118+
arcVisible(d.current) ? (d.children ? 1 : 0.6) : 0
119+
)
120+
.attr('pointer-events', (d) =>
121+
arcVisible(d.current) ? 'auto' : 'none'
122+
)
123+
.attr('d', (d) => drawArc(d.current))
124+
)
114125

115126
// Events for folders
116127
path
@@ -232,49 +243,55 @@ function SunburstChart({
232243
handleTextUpdate({ current: p, selected, transition: t })
233244
}
234245

235-
function handleArcsUpdate({ current, selected, transition }) {
236-
parent.datum(selected)
237-
238-
// Handle animating in/out of a folder
239-
root.each((d) => {
240-
// determine x0 and y0
241-
const x0Min = Math.min(
242-
1,
243-
(d.x0 - current.x0) / (current.x1 - current.x0)
244-
)
245-
const x0 = Math.max(0, x0Min) * 2 * Math.PI
246-
const y0 = Math.max(0, d.y0 - current.depth)
247-
248-
// determine x1 and y1
249-
const x1Min = Math.min(
250-
1,
251-
(d.x1 - current.x0) / (current.x1 - current.x0)
252-
)
253-
const x1 = Math.max(0, x1Min) * 2 * Math.PI
254-
const y1 = Math.max(0, d.y1 - current.depth)
255-
256-
d.target = { x0, y0, x1, y1 }
257-
})
258-
259-
// Transition the data on all arcs, even the ones that aren’t visible,
260-
// so that if this transition is interrupted, entering arcs will start
261-
// the next transition from the desired position.
262-
path
263-
.transition(transition)
264-
.tween('data', (d) => {
265-
const i = interpolate(d.current, d.target)
266-
return (t) => (d.current = i(t))
267-
})
268-
.filter(function (d) {
269-
return +this.getAttribute('fill-opacity') || arcVisible(d.target)
246+
const handleArcsUpdate = ({ current, selected, transition }) =>
247+
Sentry.startSpan({ name: 'SunburstChart.handleArcsUpdate' }, () => {
248+
parent.datum(selected)
249+
250+
// Handle animating in/out of a folder
251+
Sentry.startSpan({ name: 'SunburstChart.calculateCoordinates' }, () => {
252+
root.each((d) => {
253+
// determine x0 and y0
254+
const x0Min = Math.min(
255+
1,
256+
(d.x0 - current.x0) / (current.x1 - current.x0)
257+
)
258+
const x0 = Math.max(0, x0Min) * 2 * Math.PI
259+
const y0 = Math.max(0, d.y0 - current.depth)
260+
261+
// determine x1 and y1
262+
const x1Min = Math.min(
263+
1,
264+
(d.x1 - current.x0) / (current.x1 - current.x0)
265+
)
266+
const x1 = Math.max(0, x1Min) * 2 * Math.PI
267+
const y1 = Math.max(0, d.y1 - current.depth)
268+
269+
d.target = { x0, y0, x1, y1 }
270+
})
270271
})
271-
.attr('fill-opacity', (d) =>
272-
arcVisible(d.target) ? (d.children ? 1 : 0.6) : 0
273-
)
274-
.attr('pointer-events', (d) => (arcVisible(d.target) ? 'auto' : 'none'))
275272

276-
.attrTween('d', (d) => () => drawArc(d.current))
277-
}
273+
// Transition the data on all arcs, even the ones that aren’t visible,
274+
// so that if this transition is interrupted, entering arcs will start
275+
// the next transition from the desired position.
276+
Sentry.startSpan({ name: 'SunburstChart.transitionArcs' }, () => {
277+
path
278+
.transition(transition)
279+
.tween('data', (d) => {
280+
const i = interpolate(d.current, d.target)
281+
return (t) => (d.current = i(t))
282+
})
283+
.filter(function (d) {
284+
return +this.getAttribute('fill-opacity') || arcVisible(d.target)
285+
})
286+
.attr('fill-opacity', (d) =>
287+
arcVisible(d.target) ? (d.children ? 1 : 0.6) : 0
288+
)
289+
.attr('pointer-events', (d) =>
290+
arcVisible(d.target) ? 'auto' : 'none'
291+
)
292+
.attrTween('d', (d) => () => drawArc(d.current))
293+
})
294+
})
278295

279296
function handleTextUpdate({ current, selected, transition }) {
280297
backText.datum(selected)
@@ -328,4 +345,6 @@ SunburstChart.propTypes = {
328345
colorDomainMax: PropTypes.number,
329346
}
330347

331-
export default SunburstChart
348+
export default Sentry.withProfiler(SunburstChart, {
349+
name: 'SunburstChart',
350+
})

0 commit comments

Comments
 (0)