Skip to content

Commit

Permalink
refactor(sunburst): respond to PR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
wyze authored and plouc committed Dec 2, 2020
1 parent 66edc5a commit 5d5e71e
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 92 deletions.
51 changes: 30 additions & 21 deletions packages/sunburst/src/Sunburst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
onMouseMove,
} = { ...defaultProps, ...props }

const { innerWidth, innerHeight, margin } = useDimensions(width, height, partialMargin)
const { innerHeight, innerWidth, margin, outerHeight, outerWidth } = useDimensions(
width,
height,
partialMargin
)

const { centerX, centerY, radius } = useMemo(() => {
const radius = Math.min(innerWidth, innerHeight) / 2
Expand All @@ -67,35 +71,40 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
valueFormat,
})

const filteredNodes = useMemo(() => nodes.filter(node => node.depth > 0), [nodes])

const boundDefs = bindDefs(defs, nodes, fill, {
dataKey: 'data',
colorKey: 'data.color',
targetKey: 'data.fill',
})

return (
<SvgWrapper width={width} height={height} defs={boundDefs} margin={margin} role={role}>
<SvgWrapper
width={outerWidth}
height={outerHeight}
defs={boundDefs}
margin={margin}
role={role}
>
<g transform={`translate(${centerX}, ${centerY})`}>
{nodes
.filter(node => node.depth > 0)
.map(node => (
<SunburstArc
key={node.data.id}
node={node}
arcGenerator={arcGenerator}
borderWidth={borderWidth}
borderColor={borderColor}
isInteractive={isInteractive}
tooltip={tooltip}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
valueFormat={valueFormat}
/>
))}
{filteredNodes.map(node => (
<SunburstArc<RawDatum>
key={node.data.id}
node={node}
arcGenerator={arcGenerator}
borderWidth={borderWidth}
borderColor={borderColor}
isInteractive={isInteractive}
tooltip={tooltip}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
/>
))}
{enableSliceLabels && (
<SunburstLabels
<SunburstLabels<RawDatum>
nodes={nodes}
label={sliceLabel}
skipAngle={sliceLabelsSkipAngle}
Expand Down
22 changes: 13 additions & 9 deletions packages/sunburst/src/SunburstArc.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import { animated } from 'react-spring'
import { useAnimatedPath } from '@nivo/core'
import React from 'react'
import { animated, to, useSpring } from 'react-spring'
import { useMotionConfig } from '@nivo/core'
import { useEventHandlers } from './hooks'
import { SunburstArcProps } from './types'

Expand All @@ -11,18 +11,22 @@ export const SunburstArc = <RawDatum,>({
borderColor,
...props
}: SunburstArcProps<RawDatum>) => {
const path = useMemo(() => arcGenerator(node), [arcGenerator, node])
const { animate, config: springConfig } = useMotionConfig()

const animatedPath = useAnimatedPath(path ?? '')
const handlers = useEventHandlers({ data: node.data, ...props })

if (!path) {
return null
}
const { x0, x1, y0, y1 } = useSpring({
x0: node.x0,
x1: node.x1,
y0: node.y0,
y1: node.y1,
config: springConfig,
immediate: !animate,
})

return (
<animated.path
d={animatedPath}
d={to([x0, x1, y0, y1], (x0, x1, y0, y1) => arcGenerator({ ...node, x0, x1, y0, y1 }))}
fill={node.data.fill ?? node.data.color}
stroke={borderColor}
strokeWidth={borderWidth}
Expand Down
84 changes: 56 additions & 28 deletions packages/sunburst/src/SunburstLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
getLabelGenerator,
radiansToDegrees,
useTheme,
useMotionConfig,
} from '@nivo/core'
import { useInheritedColor } from '@nivo/colors'
import { animated, useTransition } from 'react-spring'
import { SunburstLabelProps } from './types'

const sliceStyle = {
Expand All @@ -24,50 +26,76 @@ export const SunburstLabels = <RawDatum,>({
}: SunburstLabelProps<RawDatum>) => {
const theme = useTheme()
const getTextColor = useInheritedColor(textColor, theme)
const { animate, config: springConfig } = useMotionConfig()

const getLabel = useMemo(() => getLabelGenerator(label), [label])

const { centerRadius, labelNodes } = useMemo(() => {
const labelNodes = nodes.filter(node => node.depth === 1)
const [node] = labelNodes
const labelNodes = useMemo(() => {
const filteredNodes = nodes.filter(node => node.depth === 1)
const [node] = filteredNodes
const innerRadius = Math.sqrt(node.y0)
const outerRadius = Math.sqrt(node.y1)
const centerRadius = innerRadius + (outerRadius - innerRadius) / 2

return { centerRadius, labelNodes }
}, [nodes])

return (
<>
{labelNodes.map(node => {
return filteredNodes
.map(node => {
const startAngle = node.x0
const endAngle = node.x1
const angle = Math.abs(endAngle - startAngle)
const angleDeg = radiansToDegrees(angle)

if (angleDeg <= skipAngle) return null

const middleAngle = midAngle({ startAngle, endAngle }) - Math.PI / 2
const position = positionFromAngle(middleAngle, centerRadius)

return (
<g
key={node.data.id}
transform={`translate(${position.x}, ${position.y})`}
style={sliceStyle}
return { angle, angleDeg, data: node.data, endAngle, middleAngle, position }
})
.filter(node => node.angleDeg > skipAngle)
}, [nodes, skipAngle])

const transition = useTransition(labelNodes, {
key: node => node.data.id,
initial: node => ({
opacity: 1,
transform: `translate(${node.position.x},${node.position.y})`,
}),
from: node => ({
opacity: 0,
transform: `translate(${node.position.x},${node.position.y})`,
}),
enter: node => ({
opacity: 1,
transform: `translate(${node.position.x},${node.position.y})`,
}),
update: node => ({
opacity: 1,
transform: `translate(${node.position.x},${node.position.y})`,
}),
leave: {
opacity: 0,
},
config: springConfig,
immediate: !animate,
})

return (
<>
{transition(({ opacity, transform }, node) => (
<animated.g
key={node.data.id}
transform={transform}
style={{ ...sliceStyle, opacity }}
>
<text
textAnchor="middle"
style={{
...theme.labels.text,
fill: getTextColor(node.data),
}}
>
<text
textAnchor="middle"
style={{
...theme.labels.text,
fill: getTextColor(node.data),
}}
>
{getLabel(node.data)}
</text>
</g>
)
})}
{getLabel(node.data)}
</text>
</animated.g>
))}
</>
)
}
13 changes: 3 additions & 10 deletions packages/sunburst/src/SunburstTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import React from 'react'
import { BasicTooltip } from '@nivo/tooltip'
import { DataProps, NormalizedDatum } from './types'
import { NormalizedDatum } from './types'

export const SunburstTooltip = <RawDatum,>({
color,
id,
formattedValue,
percentage,
valueFormat,
}: NormalizedDatum<RawDatum> & Pick<DataProps<RawDatum>, 'valueFormat'>) => (
<BasicTooltip
id={id}
value={valueFormat ? formattedValue : `${percentage.toFixed(2)}%`}
enableChip={true}
color={color}
/>
}: NormalizedDatum<RawDatum>) => (
<BasicTooltip id={id} value={formattedValue} enableChip={true} color={color} />
)
36 changes: 21 additions & 15 deletions packages/sunburst/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pick from 'lodash/pick'
import sortBy from 'lodash/sortBy'
import cloneDeep from 'lodash/cloneDeep'
import React, { createElement, useCallback, useMemo } from 'react'
Expand All @@ -10,9 +11,6 @@ import { CommonProps, ComputedDatum, DataProps, NormalizedDatum, MouseEventHandl

type MaybeColor = { color?: string }

const pick = <T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> =>
keys.reduce((acc, prop) => ({ ...acc, [prop]: obj[prop] }), {} as Pick<T, K>)

export const useEventHandlers = <RawDatum>({
data,
isInteractive,
Expand All @@ -21,18 +19,16 @@ export const useEventHandlers = <RawDatum>({
onMouseLeave: onMouseLeaveHandler,
onMouseMove: onMouseMoveHandler,
tooltip,
valueFormat,
}: Pick<CommonProps<RawDatum>, 'isInteractive' | 'tooltip'> &
MouseEventHandlers<RawDatum, SVGPathElement> & {
data: NormalizedDatum<RawDatum>
valueFormat?: DataProps<RawDatum>['valueFormat']
}) => {
const { showTooltipFromEvent, hideTooltip } = useTooltip()

const handleTooltip = useCallback(
(event: React.MouseEvent<SVGPathElement>) =>
showTooltipFromEvent(createElement(tooltip, { ...data, valueFormat }), event),
[data, showTooltipFromEvent, tooltip, valueFormat]
showTooltipFromEvent(createElement(tooltip, data), event),
[data, showTooltipFromEvent, tooltip]
)

const onClick = useCallback(
Expand Down Expand Up @@ -100,7 +96,7 @@ export const useSunburst = <RawDatum extends MaybeColor>({
valueFormat: DataProps<RawDatum>['valueFormat']
}) => {
const theme = useTheme()
const getColor = useOrdinalColorScale(colors, 'id')
const getColor = useOrdinalColorScale<NormalizedDatum<RawDatum>>(colors, 'id')
const getChildColor = useInheritedColor(childColor, theme) as (
datum: NormalizedDatum<RawDatum>
) => string
Expand All @@ -121,28 +117,28 @@ export const useSunburst = <RawDatum extends MaybeColor>({
// Maybe the types are wrong from d3, but value prop is always present, but types make it optional
const node = {
value: 0,
...pick(
descendant,
...pick(descendant, [
'x0',
'y0',
'x1',
'y1',
'depth',
'height',
'parent',
'value'
),
'value',
]),
}

const { value } = node
const id = getId(descendant.data)
const percentage = (100 * value) / total
const data = {
color: descendant.data.color,
data: descendant.data,
depth: node.depth,
formattedValue: formatValue(value),
formattedValue: valueFormat ? formatValue(value) : `${percentage.toFixed(2)}%`,
id,
percentage: (100 * value) / total,
percentage,
value,
}

Expand All @@ -161,7 +157,17 @@ export const useSunburst = <RawDatum extends MaybeColor>({
},
[]
)
}, [radius, data, getValue, getId, formatValue, childColor, getColor, getChildColor])
}, [
radius,
data,
getValue,
getId,
valueFormat,
formatValue,
childColor,
getColor,
getChildColor,
])

const arcGenerator = useMemo(
() =>
Expand Down
8 changes: 1 addition & 7 deletions packages/sunburst/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,7 @@ export type SvgProps<RawDatum> = DataProps<RawDatum> &

export type SunburstArcProps<RawDatum> = Pick<
SvgProps<RawDatum>,
| 'onClick'
| 'onMouseEnter'
| 'onMouseLeave'
| 'onMouseMove'
| 'borderWidth'
| 'borderColor'
| 'valueFormat'
'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onMouseMove' | 'borderWidth' | 'borderColor'
> &
Pick<CommonProps<RawDatum>, 'isInteractive' | 'tooltip'> & {
arcGenerator: Arc<any, ComputedDatum<RawDatum>>
Expand Down
4 changes: 2 additions & 2 deletions packages/sunburst/tests/Sunburst.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ describe('Sunburst', () => {
const labels = wrapper.find('SunburstLabels').find('g')
expect(labels).toHaveLength(2)

expect(labels.at(0).find('text').text()).toEqual('110')
expect(labels.at(1).find('text').text()).toEqual('20')
expect(labels.at(0).find('text').text()).toEqual('84.62%')
expect(labels.at(1).find('text').text()).toEqual('15.38%')
})

it('should use formattedValue', () => {
Expand Down

0 comments on commit 5d5e71e

Please sign in to comment.