Skip to content

Commit

Permalink
feat(sunburst): add layers support
Browse files Browse the repository at this point in the history
  • Loading branch information
wyze committed Dec 1, 2020
1 parent 64420d5 commit 65a097d
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 26 deletions.
75 changes: 55 additions & 20 deletions packages/sunburst/src/Sunburst.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useMemo } from 'react'
import React, { Fragment, ReactNode, createElement, useMemo } from 'react'
// @ts-ignore
import { Container, SvgWrapper, useDimensions, bindDefs } from '@nivo/core'
import { SunburstLabels } from './SunburstLabels'
import { SunburstArc } from './SunburstArc'
import { defaultProps } from './props'
import { useSunburst } from './hooks'
import { SvgProps } from './types'
import { useSunburst, useSunburstLayerContext } from './hooks'
import { SvgProps, SunburstLayerId, SunburstLayer } from './types'

const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
const {
Expand All @@ -14,6 +14,8 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
value,
valueFormat,

layers = defaultProps.layers as SunburstLayer<RawDatum>[],

colors,
childColor,

Expand Down Expand Up @@ -79,15 +81,14 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
targetKey: 'data.fill',
})

return (
<SvgWrapper
width={outerWidth}
height={outerHeight}
defs={boundDefs}
margin={margin}
role={role}
>
<g transform={`translate(${centerX}, ${centerY})`}>
const layerById: Record<SunburstLayerId, ReactNode> = {
slices: null,
sliceLabels: null,
}

if (layers.includes('slices')) {
layerById.slices = (
<g key="slices" transform={`translate(${centerX},${centerY})`}>
{filteredNodes.map(node => (
<SunburstArc<RawDatum>
key={node.data.id}
Expand All @@ -103,15 +104,49 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
onMouseMove={onMouseMove}
/>
))}
{enableSliceLabels && (
<SunburstLabels<RawDatum>
nodes={nodes}
label={sliceLabel}
skipAngle={sliceLabelsSkipAngle}
textColor={sliceLabelsTextColor}
/>
)}
</g>
)
}

if (enableSliceLabels && layers.includes('sliceLabels')) {
layerById.sliceLabels = (
<SunburstLabels<RawDatum>
key="sliceLabels"
nodes={nodes}
label={sliceLabel}
skipAngle={sliceLabelsSkipAngle}
textColor={sliceLabelsTextColor}
/>
)
}

const layerContext = useSunburstLayerContext<RawDatum>({
nodes: filteredNodes,
arcGenerator,
centerX,
centerY,
radius,
})

return (
<SvgWrapper
width={outerWidth}
height={outerHeight}
defs={boundDefs}
margin={margin}
role={role}
>
{layers.map((layer, i) => {
if (layerById[layer as SunburstLayerId] !== undefined) {
return layerById[layer as SunburstLayerId]
}

if (typeof layer === 'function') {
return <Fragment key={i}>{createElement(layer, layerContext)}</Fragment>
}

return null
})}
</SvgWrapper>
)
}
Expand Down
38 changes: 36 additions & 2 deletions packages/sunburst/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import sortBy from 'lodash/sortBy'
import cloneDeep from 'lodash/cloneDeep'
import React, { createElement, useCallback, useMemo } from 'react'
import { getAccessorFor, useTheme, useValueFormatter } from '@nivo/core'
import { arc } from 'd3-shape'
import { arc, Arc } from 'd3-shape'
import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors'
import { useTooltip } from '@nivo/tooltip'
import { partition as d3Partition, hierarchy as d3Hierarchy } from 'd3-hierarchy'
import { CommonProps, ComputedDatum, DataProps, NormalizedDatum, MouseEventHandlers } from './types'
import {
CommonProps,
ComputedDatum,
DataProps,
NormalizedDatum,
MouseEventHandlers,
SunburstCustomLayerProps,
} from './types'

type MaybeColor = { color?: string }

Expand Down Expand Up @@ -182,3 +189,30 @@ export const useSunburst = <RawDatum extends MaybeColor>({

return { arcGenerator, nodes }
}

/**
* Memoize the context to pass to custom layers.
*/
export const useSunburstLayerContext = <RawDatum>({
nodes,
arcGenerator,
centerX,
centerY,
radius,
}: {
nodes: ComputedDatum<RawDatum>[]
arcGenerator: Arc<any, ComputedDatum<RawDatum>>
centerX: number
centerY: number
radius: number
}): SunburstCustomLayerProps<RawDatum> =>
useMemo(
() => ({
nodes,
arcGenerator,
centerX,
centerY,
radius,
}),
[nodes, arcGenerator, centerX, centerY, radius]
)
11 changes: 7 additions & 4 deletions packages/sunburst/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { OrdinalColorScaleConfig } from '@nivo/colors'
import { SunburstTooltip } from './SunburstTooltip'

export type DefaultSunburstProps = Required<typeof defaultProps>
import { SunburstLayerId } from './types'

export const defaultProps = {
id: 'id',
value: 'value',

cornerRadius: 0,

colors: { scheme: 'nivo' },
layers: ['slices', 'sliceLabels'] as SunburstLayerId[],

colors: ({ scheme: 'nivo' } as unknown) as OrdinalColorScaleConfig,
borderWidth: 1,
borderColor: 'white',

Expand All @@ -18,11 +20,12 @@ export const defaultProps = {
// slices labels
enableSliceLabels: false,
sliceLabel: 'formattedValue',
sliceLabelsSkipAngle: 0,
sliceLabelsTextColor: { theme: 'labels.text.fill' },

isInteractive: true,
animate: false,
motionConfig: 'gentle',

tooltip: SunburstTooltip,
} as const
}
16 changes: 16 additions & 0 deletions packages/sunburst/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ export type DatumValue = number
export type DatumPropertyAccessor<RawDatum, T> = (datum: RawDatum) => T
export type LabelAccessorFunction<RawDatum> = (datum: RawDatum) => string | number

export type SunburstLayerId = 'slices' | 'sliceLabels'

export interface SunburstCustomLayerProps<RawDatum> {
nodes: ComputedDatum<RawDatum>[]
centerX: number
centerY: number
radius: number
arcGenerator: Arc<any, ComputedDatum<RawDatum>>
}

export type SunburstCustomLayer<RawDatum> = React.FC<SunburstCustomLayerProps<RawDatum>>

export type SunburstLayer<RawDatum> = SunburstLayerId | SunburstCustomLayer<RawDatum>

export interface DataProps<RawDatum> {
data: RawDatum
id?: string | number | DatumPropertyAccessor<RawDatum, DatumId>
Expand Down Expand Up @@ -52,6 +66,8 @@ export interface ComputedDatum<RawDatum> {
}

export type CommonProps<RawDatum> = {
layers: SunburstLayer<RawDatum>[]

margin: Box

cornerRadius: number
Expand Down
23 changes: 23 additions & 0 deletions packages/sunburst/stories/sunburst.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,26 @@ stories.add(
},
}
)

const CenteredMetric = ({ nodes, centerX, centerY }) => {
const total = nodes.reduce((total, datum) => total + datum.value, 0)

return (
<text
x={centerX}
y={centerY}
textAnchor="middle"
dominantBaseline="central"
style={{
fontSize: '42px',
fontWeight: 600,
}}
>
{Number.parseFloat(total).toExponential(2)}
</text>
)
}

stories.add('adding a metric in the center using a custom layer', () => (
<Sunburst {...commonProperties} layers={['slices', 'sliceLabels', CenteredMetric]} />
))
31 changes: 31 additions & 0 deletions packages/sunburst/tests/Sunburst.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,35 @@ describe('Sunburst', () => {
expect(tooltip.text()).toEqual('B')
})
})

describe('layers', () => {
it('should support disabling a layer', () => {
const wrapper = mount(<Sunburst width={400} height={400} data={sampleData} />)
expect(wrapper.find('SunburstArc')).toHaveLength(5)

wrapper.setProps({ layers: ['sliceLabels'] })
expect(wrapper.find('SunburstArc')).toHaveLength(0)
})

it('should support adding a custom layer', () => {
const CustomLayer = () => null

const wrapper = mount(
<Sunburst
width={400}
height={400}
data={sampleData}
layers={['slices', 'sliceLabels', CustomLayer]}
/>
)

const customLayer = wrapper.find(CustomLayer)

expect(customLayer.prop('nodes')).toHaveLength(5)
expect(customLayer.prop('centerX')).toEqual(200)
expect(customLayer.prop('centerY')).toEqual(200)
expect(customLayer.prop('arcGenerator')).toBeDefined()
expect(customLayer.prop('radius')).toEqual(200)
})
})
})
2 changes: 2 additions & 0 deletions website/src/data/components/sunburst/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Sunburst:
link: sunburst--patterns-gradients
- label: drill down to children
link: sunburst--children-drill-down
- label: adding a metric in the center using a custom layer
link: sunburst--adding-a-metric-in-the-center-using-a-custom-layer
description: |
The responsive alternative of this component is
`ResponsiveSunburst`.
28 changes: 28 additions & 0 deletions website/src/data/components/sunburst/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,34 @@ const props = [
step: 1,
},
},
{
key: 'layers',
group: 'Customization',
help: 'Defines the order of layers and add custom layers.',
description: `
You can also use this to insert extra layers
to the chart, the extra layer must be a function.
The layer component which will receive the chart's
context & computed data and must return a valid SVG element
for the \`Sunburst\` component.
The context passed to layers has the following structure:
\`\`\`
{
nodes: ComputedDatum<RawDatum>[],
arcGenerator: Function
centerX: number
centerY: number
radius: number
}
\`\`\`
`,
required: false,
type: 'Array<string | Function>',
defaultValue: defaultProps.layers,
},
{
key: 'isInteractive',
flavors: ['svg'],
Expand Down

0 comments on commit 65a097d

Please sign in to comment.