Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: Render only two levels of the sunburst #3783

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
re-instument optimized Sunburst with Sentry
  • Loading branch information
nicholas-codecov committed Mar 3, 2025
commit a0b9798a75a14d83ad3c3a19c211f9f2bcd3968e
371 changes: 200 additions & 171 deletions src/ui/SunburstChart/SunburstChart.jsx
Original file line number Diff line number Diff line change
@@ -67,24 +67,26 @@ function SunburstChart({
const radius = width / 6

// Creates a function for creating arcs representing files and folders.
const drawArc = Sentry.startSpan({ name: 'SunburstChart.drawArc' }, () => {
return arc()
.startAngle((d) => d.x0)
.endAngle((d) => d.x1)
.padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius((d) => d.y0 * radius)
.outerRadius((d) => Math.max(d.y0 * radius, d.y1 * radius - 1))
})
const createDrawArcFunction = (parentSpan) =>
Sentry.startSpan({ name: 'SunburstChart.drawArc', parentSpan }, () => {
return arc()
.startAngle((d) => d.x0)
.endAngle((d) => d.x1)
.padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius((d) => d.y0 * radius)
.outerRadius((d) => Math.max(d.y0 * radius, d.y1 * radius - 1))
})

// A color function you can pass a number from 0-100 to and get a color back from the specified color range
// Ex color(10.4)
const color = Sentry.startSpan({ name: 'SunburstChart.color' }, () => {
return scaleSequential()
.domain([colorDomainMin, colorDomainMax])
.interpolator(colorRange)
.clamp(true)
})
const createColorFunction = (parentSpan) =>
Sentry.startSpan({ name: 'SunburstChart.color', parentSpan }, () => {
return scaleSequential()
.domain([colorDomainMin, colorDomainMax])
.interpolator(colorRange)
.clamp(true)
})

// Tracks previous location for rendering .. in the breadcrumb.
let previous
@@ -97,167 +99,194 @@ function SunburstChart({
.append('g')
.attr('transform', `translate(${width / 2},${width / 2})`)

function renderArcs() {
const nodesToRender = selectedNode
.descendants()
.slice(1)
.filter((d) => d.depth <= selectedNode.depth + 2)

// Renders an arc per data point in the correct location. (Pieces of the circle that add up to a circular graph)
const path = g
.append('g')
.selectAll('path')
.data(nodesToRender)
.join('path')
.attr('fill', (d) => color(d?.data?.value || 0))
// If data point is a file fade the background color a bit.
.attr('fill-opacity', (d) => (d.children ? 1 : 0.6))
.attr('pointer-events', () => 'auto')
.attr('d', (d) => drawArc(d.current))

// Events for folders
path
.filter((d) => d.children)
.style('cursor', 'pointer')
.on('click', clickedFolder)
.on('mouseover', function (_event, p) {
select(this).attr('fill-opacity', 0.6)
reactHoverCallback({ target: p, type: 'folder' })
})
.on('mouseout', function (_event, _node) {
select(this).attr('fill-opacity', 1)
})

// Events for file
path
.filter((d) => !d.children)
.style('cursor', 'pointer')
.on('click', function (_event, node) {
reactClickCallback({ target: node, type: 'file' })
})
.on('mouseover', function (_event, node) {
select(this).attr('fill-opacity', 0.6)
reactHoverCallback({ target: node, type: 'file' })
})

// Create a11y label / mouse hover tooltip
const formatTitle = (d) => {
const coverage = formatData(d.data.value)
const filePath = d
.ancestors()
.map((d) => d.data.name)
.reverse()
.join('/')

return `${filePath}\n${coverage}% coverage`
}

path.append('title').text((d) => formatTitle(d))

// White circle in the middle. Act's as a "back"
g.append('circle')
.datum(selectedNode.parent)
.attr('r', radius)
.attr('class', 'fill-none')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('cursor', (d) => (d ? 'pointer' : 'default'))
.on('click', clickedFolder)
.on('mouseover', hoveredRoot)

g.append('text')
.datum(selectedNode.parent)
.text('..')
// if the parent exists (i.e. not root), show the text
.attr('fill-opacity', (d) => (d ? 1 : 0))
.attr('text-anchor', 'middle')
.attr('class', 'text-7xl fill-ds-gray-quinary select-none')
.attr('cursor', 'pointer')
.on('click', clickedFolder)
.on('mouseover', hoveredRoot)

function clickedFolder(_event, node) {
reactClickCallback({ target: node, type: 'folder' })
changeLocation(node)
}

function hoveredRoot(_event, node) {
if (previous) {
reactHoverCallback({ target: previous, type: 'folder' })
return
}
reactHoverCallback({ target: node, type: 'folder' })
}

function reactClickCallback({ target, type }) {
if (target?.ancestors) {
// Create a string from the root data down to the current item
const filePath = target
.ancestors()
.map((d) => d.data.name)
.slice(0, -1)
.reverse()
.join('/')

// callback to parent component with a path, the data node, and raw d3 data
// (just in case we need it for the second iteration to listen to location changes and direct to the correct folder.)
clickHandler.current({
path: filePath,
data: target.data,
target,
type,
})
}
}
const renderSunburst = () =>
Sentry.startSpan(
{ name: 'SunburstChart.renderSunburst' },
(renderSunburstSpan) => {
const nodesToRender = selectedNode
.descendants()
.slice(1)
.filter((d) => d.depth <= selectedNode.depth + 2)

const drawArc = createDrawArcFunction(renderSunburstSpan)
const color = createColorFunction(renderSunburstSpan)

// Renders an arc per data point in the correct location. (Pieces of the circle that add up to a circular graph)
const path = Sentry.startSpan(
{
name: 'SunburstChart.renderArcs',
parentSpan: renderSunburstSpan,
},
() =>
g
.append('g')
.selectAll('path')
.data(nodesToRender)
.join('path')
.attr('fill', (d) => color(d?.data?.value || 0))
// If data point is a file fade the background color a bit.
.attr('fill-opacity', (d) => (d.children ? 1 : 0.6))
.attr('pointer-events', () => 'auto')
.attr('d', (d) => drawArc(d.current))
)

function reactHoverCallback({ target, type }) {
if (target?.ancestors) {
// Create a string from the root data down to the current item
const filePath = target
.ancestors()
.map((d) => d.data.name)
.slice(0, -1)
.reverse()
.join('/')

// callback to parent component with a path, the data node, and raw d3 data
// (just in case we need it for the second iteration to listen to location changes and direct to the correct folder.)
hoverHandler.current({
path: filePath,
data: target.data,
target,
type,
})
}
}
// Events for folders
path
.filter((d) => d.children)
.style('cursor', 'pointer')
.on('click', clickedFolder)
.on('mouseover', function (_event, p) {
select(this).attr('fill-opacity', 0.6)
reactHoverCallback({ target: p, type: 'folder' })
})
.on('mouseout', function (_event, _node) {
select(this).attr('fill-opacity', 1)
})

function changeLocation(node) {
// Because you can move two layers at a time previous !== parent
previous = node

if (node) {
// Update the selected node
setSelectedNode(
node.each((d) => {
// determine x0 and y0
const x0Min = Math.min(1, (d.x0 - node.x0) / (node.x1 - node.x0))
const x0 = Math.max(0, x0Min) * 2 * Math.PI
const y0 = Math.max(0, d.y0 - node.depth)

// determine x1 and y1
const x1Min = Math.min(1, (d.x1 - node.x0) / (node.x1 - node.x0))
const x1 = Math.max(0, x1Min) * 2 * Math.PI
const y1 = Math.max(0, d.y1 - node.depth)

// update the cords for the node
d.current = { x0, y0, x1, y1 }
// Events for file
path
.filter((d) => !d.children)
.style('cursor', 'pointer')
.on('click', function (_event, node) {
reactClickCallback({ target: node, type: 'file' })
})
)
.on('mouseover', function (_event, node) {
select(this).attr('fill-opacity', 0.6)
reactHoverCallback({ target: node, type: 'file' })
})

// Create a11y label / mouse hover tooltip
const formatTitle = (d) => {
const coverage = formatData(d.data.value)
const filePath = d
.ancestors()
.map((d) => d.data.name)
.reverse()
.join('/')

return `${filePath}\n${coverage}% coverage`
}

path.append('title').text((d) => formatTitle(d))

// White circle in the middle. Act's as a "back"
g.append('circle')
.datum(selectedNode.parent)
.attr('r', radius)
.attr('class', 'fill-none')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('cursor', (d) => (d ? 'pointer' : 'default'))
.on('click', clickedFolder)
.on('mouseover', hoveredRoot)

g.append('text')
.datum(selectedNode.parent)
.text('..')
// if the parent exists (i.e. not root), show the text
.attr('fill-opacity', (d) => (d ? 1 : 0))
.attr('text-anchor', 'middle')
.attr('class', 'text-7xl fill-ds-gray-quinary select-none')
.attr('cursor', 'pointer')
.on('click', clickedFolder)
.on('mouseover', hoveredRoot)

function clickedFolder(_event, node) {
reactClickCallback({ target: node, type: 'folder' })
changeLocation(node)
}

function hoveredRoot(_event, node) {
if (previous) {
reactHoverCallback({ target: previous, type: 'folder' })
return
}
reactHoverCallback({ target: node, type: 'folder' })
}

function reactClickCallback({ target, type }) {
if (target?.ancestors) {
// Create a string from the root data down to the current item
const filePath = target
.ancestors()
.map((d) => d.data.name)
.slice(0, -1)
.reverse()
.join('/')

// callback to parent component with a path, the data node, and raw d3 data
// (just in case we need it for the second iteration to listen to location changes and direct to the correct folder.)
clickHandler.current({
path: filePath,
data: target.data,
target,
type,
})
}
}

function reactHoverCallback({ target, type }) {
if (target?.ancestors) {
// Create a string from the root data down to the current item
const filePath = target
.ancestors()
.map((d) => d.data.name)
.slice(0, -1)
.reverse()
.join('/')

// callback to parent component with a path, the data node, and raw d3 data
// (just in case we need it for the second iteration to listen to location changes and direct to the correct folder.)
hoverHandler.current({
path: filePath,
data: target.data,
target,
type,
})
}
}

const changeLocation = (node) =>
Sentry.startSpan(
{
name: 'SunburstChart.changeLocation',
parentSpan: renderSunburstSpan,
},
() => {
// Because you can move two layers at a time previous !== parent
previous = node

if (node) {
// Update the selected node
setSelectedNode(
node.each((d) => {
// determine x0 and y0
const x0Min = Math.min(
1,
(d.x0 - node.x0) / (node.x1 - node.x0)
)
const x0 = Math.max(0, x0Min) * 2 * Math.PI
const y0 = Math.max(0, d.y0 - node.depth)

// determine x1 and y1
const x1Min = Math.min(
1,
(d.x1 - node.x0) / (node.x1 - node.x0)
)
const x1 = Math.max(0, x1Min) * 2 * Math.PI
const y1 = Math.max(0, d.y1 - node.depth)

// update the cords for the node
d.current = { x0, y0, x1, y1 }
})
)
}
}
)
}
}
}
)

renderArcs()
renderSunburst()

return () => {
// On cleanup remove the root DOM generated by D3
Loading
Oops, something went wrong.