Skip to content

Commit

Permalink
feat(sunburst): add mouse events and some labels (#880)
Browse files Browse the repository at this point in the history
- Add slice labels at the first level
- Add onClick/onMouseEnter/onMouseLeave events

Co-authored-by: Manuele J Sarfatti <[email protected]>
Co-authored-by: Julian Krieger <[email protected]>
Co-authored-by: Asma Berkani <[email protected]>
Co-authored-by: Neil Kistner <[email protected]>
  • Loading branch information
5 people authored Nov 7, 2020
1 parent b968775 commit 1b3dd8f
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 44 deletions.
62 changes: 57 additions & 5 deletions packages/sunburst/src/Sunburst.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import { noop } from '@nivo/core'
import sortBy from 'lodash/sortBy'
import cloneDeep from 'lodash/cloneDeep'
import compose from 'recompose/compose'
Expand All @@ -17,13 +18,21 @@ import withProps from 'recompose/withProps'
import pure from 'recompose/pure'
import { partition as Partition, hierarchy } from 'd3-hierarchy'
import { arc } from 'd3-shape'
import { withTheme, withDimensions, getAccessorFor, Container, SvgWrapper } from '@nivo/core'
import {
withTheme,
withDimensions,
getAccessorFor,
getLabelGenerator,
Container,
SvgWrapper,
} from '@nivo/core'
import {
getOrdinalColorScale,
ordinalColorsPropType,
inheritedColorPropType,
getInheritedColorGenerator,
} from '@nivo/colors'
import SunburstLabels from './SunburstLabels'
import SunburstArc from './SunburstArc'

const getAncestor = node => {
Expand All @@ -46,14 +55,23 @@ const Sunburst = ({
borderWidth,
borderColor,

tooltipFormat, // eslint-disable-line react/prop-types
tooltip, // eslint-disable-line react/prop-types
// slices labels
enableSlicesLabels,
getSliceLabel,
slicesLabelsSkipAngle,
slicesLabelsTextColor,

// theming
theme, // eslint-disable-line react/prop-types

role,

isInteractive,
tooltipFormat,
tooltip,
onClick,
onMouseEnter,
onMouseLeave,
}) => {
return (
<Container isInteractive={isInteractive} theme={theme} animate={false}>
Expand All @@ -79,9 +97,24 @@ const Sunburst = ({
hideTooltip={hideTooltip}
tooltipFormat={tooltipFormat}
tooltip={tooltip}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
theme={theme}
/>
))}
{enableSlicesLabels && (
<SunburstLabels
nodes={nodes}
theme={theme}
label={getSliceLabel}
skipAngle={slicesLabelsSkipAngle}
textColor={getInheritedColorGenerator(
slicesLabelsTextColor,
'labels.text.fill'
)}
/>
)}
</g>
</SvgWrapper>
)}
Expand Down Expand Up @@ -112,12 +145,20 @@ Sunburst.propTypes = {

childColor: inheritedColorPropType.isRequired,

tooltipFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
tooltip: PropTypes.func,
// slices labels
enableSlicesLabels: PropTypes.bool.isRequired,
getSliceLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
slicesLabelsSkipAngle: PropTypes.number,
slicesLabelsTextColor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

role: PropTypes.string.isRequired,

isInteractive: PropTypes.bool,
tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
tooltip: PropTypes.func,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
}

export const SunburstDefaultProps = {
Expand All @@ -133,7 +174,15 @@ export const SunburstDefaultProps = {
childColor: { from: 'color' },
role: 'img',

// slices labels
enableSlicesLabels: false,
sliceLabel: 'value',
slicesLabelsTextColor: 'theme',

isInteractive: true,
onClick: noop,
onMouseEnter: noop,
onMouseLeave: noop,
}

const enhance = compose(
Expand Down Expand Up @@ -200,6 +249,9 @@ const enhance = compose(
return { nodes }
}
),
withPropsOnChange(['sliceLabel'], ({ sliceLabel }) => ({
getSliceLabel: getLabelGenerator(sliceLabel),
})),
pure
)

Expand Down
87 changes: 56 additions & 31 deletions packages/sunburst/src/SunburstArc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,41 @@ import withPropsOnChange from 'recompose/withPropsOnChange'
import pure from 'recompose/pure'
import { BasicTooltip } from '@nivo/tooltip'

const SunburstArc = ({ node, path, borderWidth, borderColor, showTooltip, hideTooltip }) => (
<path
d={path}
fill={node.data.color}
stroke={borderColor}
strokeWidth={borderWidth}
onMouseEnter={showTooltip}
onMouseMove={showTooltip}
onMouseLeave={hideTooltip}
/>
)
const SunburstArc = ({
node,
path,
borderWidth,
borderColor,
showTooltip,
hideTooltip,
tooltip,
onClick,
onMouseEnter,
onMouseLeave,
}) => {
const handleTooltip = e => showTooltip(tooltip, e)
const handleMouseEnter = e => {
onMouseEnter(node.data, e)
showTooltip(tooltip, e)
}
const handleMouseLeave = e => {
onMouseLeave(node.data, e)
hideTooltip(e)
}

return (
<path
d={path}
fill={node.data.color}
stroke={borderColor}
strokeWidth={borderWidth}
onMouseEnter={handleMouseEnter}
onMouseMove={handleTooltip}
onMouseLeave={handleMouseLeave}
onClick={onClick}
/>
)
}

SunburstArc.propTypes = {
node: PropTypes.shape({
Expand All @@ -37,6 +61,9 @@ SunburstArc.propTypes = {
borderColor: PropTypes.string.isRequired,
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
onClick: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
tooltipFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
tooltip: PropTypes.func,
theme: PropTypes.object.isRequired,
Expand All @@ -46,27 +73,25 @@ const enhance = compose(
withPropsOnChange(['node', 'arcGenerator'], ({ node, arcGenerator }) => ({
path: arcGenerator(node),
})),
withPropsOnChange(['node', 'onClick'], ({ node, onClick }) => ({
onClick: event => onClick(node.data, event),
})),
withPropsOnChange(
['node', 'showTooltip', 'tooltip', 'tooltipFormat', 'theme'],
({ node, showTooltip, tooltip, tooltipFormat, theme }) => ({
showTooltip: e => {
showTooltip(
<BasicTooltip
id={node.data.id}
enableChip={true}
color={node.data.color}
value={`${node.data.percentage.toFixed(2)}%`}
theme={theme}
format={tooltipFormat}
renderContent={
typeof tooltip === 'function'
? tooltip.bind(null, { node, ...node })
: null
}
/>,
e
)
},
['node', 'theme', 'tooltip', 'tooltipFormat'],
({ node, theme, tooltip, tooltipFormat }) => ({
tooltip: (
<BasicTooltip
id={node.data.id}
value={`${node.data.percentage.toFixed(2)}%`}
enableChip={true}
color={node.data.color}
theme={theme}
format={tooltipFormat}
renderContent={
typeof tooltip === 'function' ? tooltip.bind(null, { ...node.data }) : null
}
/>
),
})
),
pure
Expand Down
79 changes: 79 additions & 0 deletions packages/sunburst/src/SunburstLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* This file is part of the nivo project.
*
* Copyright 2016-present, Raphaël Benitte.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types'
import { midAngle, positionFromAngle, radiansToDegrees, labelsThemePropType } from '@nivo/core'

const sliceStyle = {
pointerEvents: 'none',
}

export default class SunburstLabels extends Component {
static propTypes = {
nodes: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
skipAngle: PropTypes.number.isRequired,
textColor: PropTypes.func.isRequired,
theme: PropTypes.shape({
labels: labelsThemePropType.isRequired,
}).isRequired,
}

static defaultProps = {
skipAngle: 0,
}

render() {
const { nodes, label, skipAngle, textColor, theme } = this.props

let centerRadius = false

return (
<Fragment>
{nodes
.filter(node => node.depth === 1)
.map(node => {
if (!centerRadius) {
const innerRadius = Math.sqrt(node.y0)
const outerRadius = Math.sqrt(node.y1)
centerRadius = innerRadius + (outerRadius - innerRadius) / 2
}

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}
>
<text
textAnchor="middle"
style={{
...theme.labels.text,
fill: textColor(node.data, theme),
}}
>
{label(node.data)}
</text>
</g>
)
})}
</Fragment>
)
}
}
36 changes: 31 additions & 5 deletions packages/sunburst/stories/sunburst.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { withKnobs } from '@storybook/addon-knobs'
import { generateLibTree } from '@nivo/generators'
import { Sunburst } from '../src'
Expand Down Expand Up @@ -27,6 +28,16 @@ stories.add('with child colors independent of parent', () => (
<Sunburst {...commonProperties} childColor="noinherit" />
))

const customPalette = ['#ffd700', '#ffb14e', '#fa8775', '#ea5f94', '#cd34b5', '#9d02d7', '#0000ff']

stories.add('with custom colors', () => (
<Sunburst
{...commonProperties}
colors={({ value }) => customPalette[value % (customPalette.length - 1)]}
childColor="noinherit"
/>
))

stories.add('with formatted tooltip value', () => (
<Sunburst
{...commonProperties}
Expand All @@ -36,13 +47,28 @@ stories.add('with formatted tooltip value', () => (
/>
))

stories.add('with custom tooltip', () => (
stories.add('custom tooltip', () => (
<Sunburst
{...commonProperties}
tooltip={({ data: { id, value, color } }) => (
<span style={{ color }}>
{id}: <strong>{value}</strong>
</span>
tooltip={({ id, value, color }) => (
<strong style={{ color }}>
{id}: {value}
</strong>
)}
theme={{
tooltip: {
container: {
background: '#333',
},
},
}}
/>
))

stories.add('enter/leave (check actions)', () => (
<Sunburst
{...commonProperties}
onMouseEnter={action('onMouseEnter')}
onMouseLeave={action('onMouseLeave')}
/>
))
Loading

0 comments on commit 1b3dd8f

Please sign in to comment.