Skip to content

Commit

Permalink
feat(sunburst): add valueFormat prop and move event handlers to hook
Browse files Browse the repository at this point in the history
  • Loading branch information
wyze authored and plouc committed Dec 2, 2020
1 parent c586676 commit c427350
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 98 deletions.
13 changes: 9 additions & 4 deletions packages/sunburst/src/Sunburst.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
data,
id,
value,
valueFormat,

colors,
childColor,
Expand All @@ -37,13 +38,14 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
role,

// interactivity
tooltipFormat,
isInteractive,
tooltip,

// event handlers
onClick,
onMouseEnter,
onMouseLeave,
onMouseMove,
} = { ...defaultProps, ...props }

const { innerWidth, innerHeight, margin } = useDimensions(width, height, partialMargin)
Expand All @@ -60,8 +62,9 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
cornerRadius,
data,
id,
value,
radius,
value,
valueFormat,
})

const boundDefs = bindDefs(defs, nodes, fill, {
Expand All @@ -82,11 +85,13 @@ const InnerSunburst = <RawDatum,>(props: SvgProps<RawDatum>) => {
arcGenerator={arcGenerator}
borderWidth={borderWidth}
borderColor={borderColor}
tooltipFormat={tooltipFormat}
isInteractive={isInteractive}
tooltip={tooltip}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseMove={onMouseMove}
valueFormat={valueFormat}
/>
))}
{enableSliceLabels && (
Expand All @@ -109,7 +114,7 @@ export const Sunburst = <RawDatum,>({
theme,
...otherProps
}: SvgProps<RawDatum>) => (
<Container theme={theme} {...{ isInteractive, animate, motionConfig }}>
<Container {...{ isInteractive, animate, motionConfig, theme }}>
<InnerSunburst<RawDatum> isInteractive={isInteractive} {...otherProps} />
</Container>
)
39 changes: 4 additions & 35 deletions packages/sunburst/src/SunburstArc.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,20 @@
import React, { useMemo } from 'react'
import { BasicTooltip, useTooltip } from '@nivo/tooltip'
import { animated } from 'react-spring'
import { useAnimatedPath } from '@nivo/core'
import { useEventHandlers } from './hooks'
import { SunburstArcProps } from './types'

export const SunburstArc = <RawDatum,>({
node,
arcGenerator,
borderWidth,
borderColor,
tooltip: _tooltip,
tooltipFormat,
onClick,
onMouseEnter,
onMouseLeave,
...props
}: SunburstArcProps<RawDatum>) => {
const { showTooltipFromEvent, hideTooltip } = useTooltip()

const path = useMemo(() => arcGenerator(node), [arcGenerator, node])
const tooltip = useMemo(
() => (
<BasicTooltip
id={node.data.id}
value={`${node.data.percentage.toFixed(2)}%`}
enableChip={true}
color={node.data.color}
format={tooltipFormat}
renderContent={
typeof _tooltip === 'function'
? _tooltip.bind(null, { ...node.data })
: undefined
}
/>
),
[_tooltip, node.data, tooltipFormat]
)

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

if (!path) {
return null
Expand All @@ -48,16 +26,7 @@ export const SunburstArc = <RawDatum,>({
fill={node.data.fill ?? node.data.color}
stroke={borderColor}
strokeWidth={borderWidth}
onMouseEnter={event => {
onMouseEnter?.(node.data, event)
showTooltipFromEvent(tooltip, event)
}}
onMouseMove={event => showTooltipFromEvent(tooltip, event)}
onMouseLeave={event => {
onMouseLeave?.(node.data, event)
hideTooltip()
}}
onClick={event => onClick?.(node.data, event)}
{...handlers}
/>
)
}
18 changes: 18 additions & 0 deletions packages/sunburst/src/SunburstTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import { BasicTooltip } from '@nivo/tooltip'
import { DataProps, 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}
/>
)
153 changes: 120 additions & 33 deletions packages/sunburst/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,103 @@
import sortBy from 'lodash/sortBy'
import cloneDeep from 'lodash/cloneDeep'
import { getAccessorFor, useTheme } from '@nivo/core'
import React, { createElement, useCallback, useMemo } from 'react'
import { getAccessorFor, useTheme, useValueFormatter } from '@nivo/core'
import { arc } from 'd3-shape'
import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors'
import { useMemo } from 'react'
import { useTooltip } from '@nivo/tooltip'
import { partition as d3Partition, hierarchy as d3Hierarchy } from 'd3-hierarchy'
import { CommonProps, ComputedDatum, DataProps, NormalizedDatum } from './types'
import { CommonProps, ComputedDatum, DataProps, NormalizedDatum, MouseEventHandlers } from './types'

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,
onClick: onClickHandler,
onMouseEnter: onMouseEnterHandler,
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]
)

const onClick = useCallback(
(event: React.MouseEvent<SVGPathElement>) => onClickHandler?.(data, event),
[data, onClickHandler]
)
const onMouseEnter = useCallback(
(event: React.MouseEvent<SVGPathElement>) => {
onMouseEnterHandler?.(data, event)
handleTooltip(event)
},
[data, handleTooltip, onMouseEnterHandler]
)
const onMouseLeave = useCallback(
(event: React.MouseEvent<SVGPathElement>) => {
onMouseLeaveHandler?.(data, event)
hideTooltip()
},
[data, hideTooltip, onMouseLeaveHandler]
)
const onMouseMove = useCallback(
(event: React.MouseEvent<SVGPathElement>) => {
onMouseMoveHandler?.(data, event)
handleTooltip(event)
},
[data, handleTooltip, onMouseMoveHandler]
)

return useMemo(() => {
if (!isInteractive) {
return {
onClick: undefined,
onMouseEnter: undefined,
onMouseLeave: undefined,
onMouseMove: undefined,
}
}

return {
onClick,
onMouseEnter,
onMouseLeave,
onMouseMove,
}
}, [isInteractive, onClick, onMouseEnter, onMouseLeave, onMouseMove])
}

export const useSunburst = <RawDatum extends MaybeColor>({
childColor,
colors,
cornerRadius,
data,
id,
value,
valueFormat,
radius,
}: {
childColor: CommonProps<RawDatum>['childColor']
colors: CommonProps<RawDatum>['colors']
cornerRadius: CommonProps<RawDatum>['cornerRadius']
data: DataProps<RawDatum>['data']
id: DataProps<RawDatum>['id']
id: NonNullable<DataProps<RawDatum>['id']>
radius: number
value: DataProps<RawDatum>['value']
value: NonNullable<DataProps<RawDatum>['value']>
valueFormat: DataProps<RawDatum>['valueFormat']
}) => {
const theme = useTheme()
const getColor = useOrdinalColorScale(colors, 'id')
Expand All @@ -38,43 +108,60 @@ export const useSunburst = <RawDatum extends MaybeColor>({
const getId = useMemo(() => getAccessorFor(id), [id])
const getValue = useMemo(() => getAccessorFor(value), [value])

const formatValue = useValueFormatter(valueFormat)

const nodes = useMemo(() => {
const partition = d3Partition<RawDatum>().size([2 * Math.PI, radius * radius])
const hierarchy = d3Hierarchy(data).sum(getValue)
const descendants = partition(cloneDeep(hierarchy)).descendants()
const total = hierarchy.value ?? 0

return sortBy(partition(cloneDeep(hierarchy)).descendants(), 'depth').reduce<
Array<ComputedDatum<RawDatum>>
>((acc, descendant) => {
// Maybe the types are wrong from d3, but value prop is always present, but types make it optional
const node = {
value: 0,
...pick(descendant, 'x0', 'y0', 'x1', 'y1', 'depth', 'height', 'parent', 'value'),
}
return sortBy(descendants, 'depth').reduce<Array<ComputedDatum<RawDatum>>>(
(acc, descendant) => {
// Maybe the types are wrong from d3, but value prop is always present, but types make it optional
const node = {
value: 0,
...pick(
descendant,
'x0',
'y0',
'x1',
'y1',
'depth',
'height',
'parent',
'value'
),
}

const { value } = node
const id = getId(descendant.data)
const data = {
depth: node.depth,
id,
percentage: (100 * value) / total,
value,
}
const { value } = node
const id = getId(descendant.data)
const data = {
color: descendant.data.color,
data: descendant.data,
depth: node.depth,
formattedValue: formatValue(value),
id,
percentage: (100 * value) / total,
value,
}

const parent = acc.find(
computed => node.parent && computed.data.id === getId(node.parent.data)
)
const parent = acc.find(
computed => node.parent && computed.data.id === getId(node.parent.data)
)

const color =
node.depth === 1 || childColor === 'noinherit'
? getColor(data)
: parent
? getChildColor(parent.data)
: descendant.data.color
const color =
node.depth === 1 || childColor === 'noinherit'
? getColor(data)
: parent
? getChildColor(parent.data)
: descendant.data.color

return [...acc, { ...node, data: { ...data, color, parent } }]
}, [])
}, [data, childColor, getChildColor, getColor, getId, getValue, radius])
return [...acc, { ...node, data: { ...data, color, parent } }]
},
[]
)
}, [radius, data, getValue, getId, formatValue, childColor, getColor, getChildColor])

const arcGenerator = useMemo(
() =>
Expand Down
1 change: 1 addition & 0 deletions packages/sunburst/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Sunburst'
export * from './ResponsiveSunburst'
export * from './hooks'
export * from './props'
export * from './types'
6 changes: 5 additions & 1 deletion packages/sunburst/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SunburstTooltip } from './SunburstTooltip'

export type DefaultSunburstProps = Required<typeof defaultProps>

export const defaultProps = {
Expand All @@ -15,10 +17,12 @@ export const defaultProps = {

// slices labels
enableSliceLabels: false,
sliceLabel: 'value',
sliceLabel: 'formattedValue',
sliceLabelsTextColor: { theme: 'labels.text.fill' },

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

tooltip: SunburstTooltip,
} as const
Loading

0 comments on commit c427350

Please sign in to comment.