Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions packages/sunburst/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"@nivo/tooltip": "workspace:*",
"@types/d3-hierarchy": "^3.1.7",
"d3-hierarchy": "^3.1.2",
"@types/d3-scale": "^4.0.8",
"d3-scale": "^4.0.2",
"lodash": "^4.17.21"
},
"peerDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/sunburst/src/Sunburst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const InnerSunburst = <RawDatum,>({
data,
id = defaultProps.id,
value = defaultProps.value,
innerRadius = defaultProps.innerRadius,
renderRootNode = defaultProps.renderRootNode,
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think we should use the same rendering as for the arcs for the root node, it generates a shape that doesn't really make sense:

CleanShot 2025-04-18 at 21 34 05@2x

Which means it cannot have a color, interactivity doesn't work...

Copy link
Author

Choose a reason for hiding this comment

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

I think there was a bug, innerRadius was used instead of innerRadiusOffset
here
this resulted in 0.4 radius.

Fixed now, but, please, let me know if you implied a different approach / mode edits

Copy link
Owner

Choose a reason for hiding this comment

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

It's a bit better, but I think using an arc doesn't work very well tbh.
When enabling/disabling the root node, we can clearly see that the shape for the root node isn't a circle but more like a donut, since it's an arc of 360 degrees.
While the donut shape isn't necessarily critical, the impact it has on the position of the root node label is IMHO, the text should be at the center.
An option (and a bit of a hack) would be to adjust computeArcCenter so that it handles arcs of 360 deg with an inner radius of 0 differently (and arcLabelsRadiusOffset should probably be ignored for this case). That's probably the easiest option.

Copy link
Owner

Choose a reason for hiding this comment

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

If we go for this approach, we should test the position of the text in the root node, and also make sure that a ring of 360 deg still behave as before.

valueFormat,
cornerRadius = defaultProps.cornerRadius,
layers = defaultProps.layers as SunburstLayer<RawDatum>[],
Expand Down Expand Up @@ -74,6 +76,8 @@ const InnerSunburst = <RawDatum,>({
valueFormat,
radius,
cornerRadius,
innerRadius,
renderRootNode,
colors,
colorBy,
inheritColorFromParent,
Expand Down
24 changes: 20 additions & 4 deletions packages/sunburst/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from 'react'
import { partition as d3Partition, hierarchy as d3Hierarchy } from 'd3-hierarchy'
import { scaleRadial as d3ScaleRadial } from 'd3-scale'
import cloneDeep from 'lodash/cloneDeep.js'
import sortBy from 'lodash/sortBy.js'
import { usePropertyAccessor, useValueFormatter } from '@nivo/core'
Expand All @@ -22,6 +23,8 @@ export const useSunburst = <RawDatum>({
valueFormat,
radius,
cornerRadius = defaultProps.cornerRadius,
innerRadius = defaultProps.innerRadius,
renderRootNode = defaultProps.renderRootNode,
colors = defaultProps.colors,
colorBy = defaultProps.colorBy,
inheritColorFromParent = defaultProps.inheritColorFromParent,
Expand All @@ -33,6 +36,8 @@ export const useSunburst = <RawDatum>({
valueFormat?: DataProps<RawDatum>['valueFormat']
radius: number
cornerRadius?: SunburstCommonProps<RawDatum>['cornerRadius']
innerRadius?: SunburstCommonProps<RawDatum>['innerRadius']
renderRootNode?: SunburstCommonProps<RawDatum>['renderRootNode']
colors?: SunburstCommonProps<RawDatum>['colors']
colorBy?: SunburstCommonProps<RawDatum>['colorBy']
inheritColorFromParent?: SunburstCommonProps<RawDatum>['inheritColorFromParent']
Expand All @@ -58,8 +63,10 @@ export const useSunburst = <RawDatum>({
const hierarchy = d3Hierarchy(clonedData).sum(getValue)

const partition = d3Partition<RawDatum>().size([2 * Math.PI, radius * radius])
// exclude root node
const descendants = partition(hierarchy).descendants().slice(1)
// exclude root node if renderRootNode is false
const descendants = renderRootNode
? partition(hierarchy).descendants()
: partition(hierarchy).descendants().slice(1)

const total = hierarchy.value ?? 0

Expand All @@ -69,6 +76,12 @@ export const useSunburst = <RawDatum>({
// are going to be computed first
const sortedNodes = sortBy(descendants, 'depth')

const innerRadiusOffset = radius * Math.min(innerRadius, 1)

const maxDepth = Math.max(...sortedNodes.map(n => n.depth))
Copy link
Owner

Choose a reason for hiding this comment

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

As nodes are already sorted, we can simply get the depth of the last one, should be faster for larger datasets. We just need to handle the case where there are no nodes.


const radiusScale = d3ScaleRadial().domain([0, maxDepth]).range([innerRadiusOffset, radius])

return sortedNodes.reduce<ComputedDatum<RawDatum>[]>((acc, descendant) => {
const id = getId(descendant.data)
// d3 hierarchy node value is optional by default as it depends on
Expand All @@ -79,12 +92,13 @@ export const useSunburst = <RawDatum>({
const value = descendant.value!
const percentage = (100 * value) / total
const path = descendant.ancestors().map(ancestor => getId(ancestor.data))
const isRootNode = renderRootNode && descendant.depth === 0

const arc: Arc = {
startAngle: descendant.x0,
endAngle: descendant.x1,
innerRadius: Math.sqrt(descendant.y0),
outerRadius: Math.sqrt(descendant.y1),
innerRadius: isRootNode ? 0 : radiusScale(descendant.depth - 1),
outerRadius: isRootNode ? innerRadiusOffset : radiusScale(descendant.depth),
}

let parent: ComputedDatum<RawDatum> | undefined
Expand Down Expand Up @@ -119,6 +133,8 @@ export const useSunburst = <RawDatum>({
}, [
data,
radius,
innerRadius,
renderRootNode,
getValue,
getId,
valueFormat,
Expand Down
2 changes: 2 additions & 0 deletions packages/sunburst/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const defaultProps = {
id: 'id',
value: 'value',
cornerRadius: 0,
innerRadius: 0.4,
renderRootNode: false,
layers: ['arcs', 'arcLabels'] as SunburstLayerId[],
colors: { scheme: 'nivo' } as unknown as OrdinalColorScaleConfig,
colorBy: 'id' as const,
Expand Down
2 changes: 2 additions & 0 deletions packages/sunburst/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export type SunburstCommonProps<RawDatum> = {
margin?: Box
cornerRadius: number
theme: PartialTheme
innerRadius: number
renderRootNode: boolean
colors: OrdinalColorScaleConfig<Omit<ComputedDatum<RawDatum>, 'color' | 'fill'>>
colorBy: 'id' | 'depth'
inheritColorFromParent: boolean
Expand Down
29 changes: 29 additions & 0 deletions packages/sunburst/tests/Sunburst.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,35 @@
expect(layer.exists()).toBeTruthy()
expect(layer.prop('arcGenerator').cornerRadius()()).toEqual(3)
})

it('should render the root node correctly when innerRadius and renderRootNode are set', () => {
const width = 400
const height = 400
const innerRadiusProp = 0.6

// Assuming default margins { top: 0, right: 0, bottom: 0, left: 0 }
// as per Sunburst defaultProps
const chartRadius = Math.min(width, height) / 2
const expectedRootOuterRadius = chartRadius * innerRadiusProp

const wrapper = mount(
<Sunburst
width={width}
height={height}
data={sampleData} // sampleData has a root with id: 'root'
innerRadius={innerRadiusProp}
renderRootNode
/>
)

const rootArcShape = wrapper.find(ArcShape).filterWhere(n => n.prop('datum').depth === 0)

Check failure on line 205 in packages/sunburst/tests/Sunburst.test.tsx

View workflow job for this annotation

GitHub Actions / Tests

Replace `.find(ArcShape)` with `⏎················.find(ArcShape)⏎················`
expect(rootArcShape.exists()).toBe(true)

const rootDatum = rootArcShape.prop('datum')
expect(rootDatum.id).toEqual('root') // Verify we got the correct root node
expect(rootDatum.arc.innerRadius).toEqual(0)
expect(rootDatum.arc.outerRadius).toEqual(expectedRootOuterRadius)
})
})

describe('colors', () => {
Expand Down
Loading
Loading