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
okay this actually resolves the re-renders
  • Loading branch information
nicholas-codecov committed Mar 3, 2025
commit 74ccc0cb217c38695575ad26b23ff3747d104380
361 changes: 186 additions & 175 deletions src/ui/SunburstChart/SunburstChart.jsx
Original file line number Diff line number Diff line change
@@ -67,25 +67,29 @@ function SunburstChart({
const radius = width / 6

// Creates a function for creating arcs representing files and folders.
const createDrawArcFunction = (parentSpan) =>
Sentry.startSpan({ name: 'SunburstChart.drawArc', parentSpan }, () =>
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))
function createDrawArcFunction(parentSpan) {
return Sentry.startSpan(
{ name: 'SunburstChart.drawArc', parentSpan },
() =>
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 createColorFunction = (parentSpan) =>
Sentry.startSpan({ name: 'SunburstChart.color', parentSpan }, () =>
function createColorFunction(parentSpan) {
return Sentry.startSpan({ name: 'SunburstChart.color', parentSpan }, () =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we use yaml set color indication range for this? seems like no

Copy link
Contributor Author

@nicholas-codecov nicholas-codecov Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not no, it would be really funky with the interpolation and how we handle it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha okay

scaleSequential()
.domain([colorDomainMin, colorDomainMax])
.interpolator(colorRange)
.clamp(true)
)
}

// Tracks previous location for rendering .. in the breadcrumb.
let previous
@@ -98,185 +102,192 @@ function SunburstChart({
.append('g')
.attr('transform', `translate(${width / 2},${width / 2})`)

const renderSunburst = Sentry.startSpan(
{ name: 'SunburstChart.renderSunburst' },
(renderSunburstSpan) => {
const drawArc = createDrawArcFunction(renderSunburstSpan)
const color = createColorFunction(renderSunburstSpan)
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 = 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))
)
// 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 renderSunburst() {
Sentry.startSpan(
{ name: 'SunburstChart.renderSunburst' },
(renderSunburstSpan) => {
const drawArc = createDrawArcFunction(renderSunburstSpan)
const color = createColorFunction(renderSunburstSpan)
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 = 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))
)
// 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 hoveredRoot(_event, node) {
if (previous) {
reactHoverCallback({ target: previous, type: 'folder' })
return
}
reactHoverCallback({ target: node, type: 'folder' })
}
// 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' })
})

function reactClickCallback({ target, type }) {
if (target?.ancestors) {
// Create a string from the root data down to the current item
const filePath = target
// Create a11y label / mouse hover tooltip
const formatTitle = (d) => {
const coverage = formatData(d.data.value)
const filePath = d
.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,
})
return `${filePath}\n${coverage}% coverage`
}
}

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,
})
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)
}
}

const changeLocation = Sentry.startSpan(
{
name: 'SunburstChart.handleArcsUpdate',
parentSpan: renderSunburstSpan,
},
(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)
function hoveredRoot(_event, node) {
if (previous) {
reactHoverCallback({ target: previous, type: 'folder' })
return
}
reactHoverCallback({ target: node, type: 'folder' })
}

// 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)
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,
})
}
}

// update the cords for the node
d.current = { x0, y0, x1, y1 }
})
)
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,
})
}
}
)
}
)

function changeLocation(node) {
Sentry.startSpan(
{
name: 'SunburstChart.handleArcsUpdate',
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 }
})
)
}
}
)
}
}
)
}

renderSunburst()