Skip to content

Commit

Permalink
feat(sunburst): add Sunburst component
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte committed Aug 16, 2017
1 parent 33d6f8d commit a8c872a
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 30 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ Several libraries already exist for React d3 integration, but just a few provide
- [`<ResponsiveRadar />`](http://nivo.rocks/#/radar)
- Stream
- [`<Stream />`](http://nivo.rocks/#/stream)
- [`<ResponsiveStream />`](http://nivo.rocks/#/stream)
- [`<ResponsiveStream />`](http://nivo.rocks/#/stream)
- Sunburst
- [`<Sunburst />`](http://nivo.rocks/#/sunburst)
- [`<ResponsiveSunburst />`](http://nivo.rocks/#/sunburst)
- TreeMap
- [`<TreeMap />`](http://nivo.rocks/#/treemap)
- [`<ResponsiveTreeMap />`](http://nivo.rocks/#/treemap)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nivo",
"version": "0.10.0",
"version": "0.11.0",
"licenses": [
{
"type": "MIT",
Expand Down
24 changes: 13 additions & 11 deletions src/components/axes/Axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import compose from 'recompose/compose'
import pure from 'recompose/pure'
import shouldUpdate from 'recompose/shouldUpdate'
import { TransitionMotion, spring } from 'react-motion'
import { motionPropTypes } from '../../props'
import Nivo from '../../Nivo'
import { withMotion } from '../../hocs'
import AxisTick from './AxisTick'

const center = scale => {
Expand Down Expand Up @@ -255,9 +256,6 @@ Axis.propTypes = {
legendOffset: PropTypes.number.isRequired,

theme: PropTypes.object.isRequired,

// motion
...motionPropTypes,
}

Axis.defaultProps = {
Expand All @@ -268,11 +266,15 @@ Axis.defaultProps = {
// legend
legendPosition: 'end',
legendOffset: 0,

// motion
animate: true,
motionStiffness: Nivo.defaults.motionStiffness,
motionDamping: Nivo.defaults.motionDamping,
}

export default pure(Axis)
const enhance = compose(
withMotion(),
shouldUpdate((props, nextProps) => {
//console.log('=> scale', props.scale === nextProps.scale)
return true
}),
pure
)

export default enhance(Axis)
41 changes: 25 additions & 16 deletions src/components/charts/stream/Stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ const stackMax = layers => max(layers.reduce((acc, layer) => [...acc, ...layer.m
const Stream = ({
data,
keys,

order,
offsetType,
xScale,
yScale,
layers,
areaGenerator,

// dimensions
Expand Down Expand Up @@ -73,19 +73,6 @@ const Stream = ({
// stack tooltip
enableStackTooltip,
}) => {
const stack = d3Stack()
.keys(keys)
.offset(stackOffsetFromProp(offsetType))
.order(stackOrderFromProp(order))

const layers = stack(data)

const minValue = stackMin(layers)
const maxValue = stackMax(layers)

const xScale = scalePoint().domain(range(data.length)).range([0, width])
const yScale = scaleLinear().domain([minValue, maxValue]).range([height, 0])

const enhancedLayers = layers.map((points, i) => {
const layer = points.map(([y1, y2], i) => ({
index: i,
Expand Down Expand Up @@ -171,6 +158,10 @@ Stream.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired,
keys: PropTypes.array.isRequired,

stack: PropTypes.func.isRequired,
xScale: PropTypes.func.isRequired,
yScale: PropTypes.func.isRequired,

order: stackOrderPropType.isRequired,
offsetType: stackOffsetPropType.isRequired,
curve: areaCurvePropType.isRequired,
Expand Down Expand Up @@ -233,6 +224,24 @@ const enhance = compose(
withPropsOnChange(['colors'], ({ colors }) => ({
getColor: getColorRange(colors),
})),
withPropsOnChange(['keys', 'offsetType', 'order'], ({ keys, offsetType, order }) => ({
stack: d3Stack()
.keys(keys)
.offset(stackOffsetFromProp(offsetType))
.order(stackOrderFromProp(order)),
})),
withPropsOnChange(['stack', 'data', 'width', 'height'], ({ stack, data, width, height }) => {
const layers = stack(data)

const minValue = stackMin(layers)
const maxValue = stackMax(layers)

return {
layers,
xScale: scalePoint().domain(range(data.length)).range([0, width]),
yScale: scaleLinear().domain([minValue, maxValue]).range([height, 0]),
}
}),
pure
)

Expand Down
18 changes: 18 additions & 0 deletions src/components/charts/sunburst/ResponsiveSunburst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 from 'react'
import ResponsiveWrapper from '../ResponsiveWrapper'
import Sunburst from './Sunburst'

const ResponsiveSunburst = props =>
<ResponsiveWrapper>
{({ width, height }) => <Sunburst width={width} height={height} {...props} />}
</ResponsiveWrapper>

export default ResponsiveSunburst
184 changes: 184 additions & 0 deletions src/components/charts/sunburst/Sunburst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* 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 from 'react'
import PropTypes from 'prop-types'
import { merge, sortBy, cloneDeep } from 'lodash'
import { Motion, TransitionMotion, spring } from 'react-motion'
import compose from 'recompose/compose'
import defaultProps from 'recompose/defaultProps'
import withPropsOnChange from 'recompose/withPropsOnChange'
import withProps from 'recompose/withProps'
import pure from 'recompose/pure'
import { partition as Partition, hierarchy } from 'd3-hierarchy'
import { arc } from 'd3-shape'
import { getInheritedColorGenerator } from '../../../lib/colorUtils'
import { withTheme, withDimensions, withColors } from '../../../hocs'
import { getAccessorFor } from '../../../lib/propertiesConverters'
import Container from '../Container'
import SvgWrapper from '../SvgWrapper'
import SunburstArc from './SunburstArc'

const getAncestor = node => {
if (node.depth === 1) return node
if (node.parent) return getAncestor(node.parent)
return node
}

const Sunburst = ({
nodes,

// dimensions
margin,
centerX,
centerY,
outerWidth,
outerHeight,

arcGenerator,

// border
borderWidth,
borderColor,

// interactivity
isInteractive,
}) => {
return (
<Container isInteractive={isInteractive}>
{({ showTooltip, hideTooltip }) =>
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin}>
<g transform={`translate(${centerX}, ${centerY})`}>
{nodes
.filter(node => node.depth > 0)
.map((node, i) =>
<SunburstArc
key={i}
node={node}
arcGenerator={arcGenerator}
borderWidth={borderWidth}
borderColor={borderColor}
showTooltip={showTooltip}
hideTooltip={hideTooltip}
/>
)}
</g>
</SvgWrapper>}
</Container>
)
}

Sunburst.propTypes = {
data: PropTypes.object.isRequired,
identity: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
getIdentity: PropTypes.func.isRequired, // computed
value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
getValue: PropTypes.func.isRequired, // computed
nodes: PropTypes.array.isRequired, // computed

partition: PropTypes.func.isRequired, // computed

cornerRadius: PropTypes.number.isRequired,
arcGenerator: PropTypes.func.isRequired, // computed

radius: PropTypes.number.isRequired, // computed
centerX: PropTypes.number.isRequired, // computed
centerY: PropTypes.number.isRequired, // computed

// border
borderWidth: PropTypes.number.isRequired,
borderColor: PropTypes.string.isRequired,

childColor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,

// interactivity
isInteractive: PropTypes.bool,
}

export const SunburstDefaultProps = {
identity: 'id',
value: 'value',

cornerRadius: 0,

// border
borderWidth: 1,
borderColor: 'white',

childColor: 'inherit',

// interactivity
isInteractive: true,
}

const enhance = compose(
defaultProps(SunburstDefaultProps),
withTheme(),
withDimensions(),
withColors(),
withProps(({ width, height }) => {
const radius = Math.min(width, height) / 2

const partition = Partition().size([2 * Math.PI, radius * radius])

return { radius, partition, centerX: width / 2, centerY: height / 2 }
}),
withPropsOnChange(['cornerRadius'], ({ cornerRadius }) => ({
arcGenerator: arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.innerRadius(d => Math.sqrt(d.y0))
.outerRadius(d => Math.sqrt(d.y1))
.cornerRadius(cornerRadius),
})),
withPropsOnChange(['identity'], ({ identity }) => ({
getIdentity: getAccessorFor(identity),
})),
withPropsOnChange(['value'], ({ value }) => ({
getValue: getAccessorFor(value),
})),
withPropsOnChange(['data', 'getValue'], ({ data, getValue }) => ({
data: hierarchy(data).sum(getValue),
})),
withPropsOnChange(['childColor'], ({ childColor }) => ({
getChildColor: getInheritedColorGenerator(childColor),
})),
withPropsOnChange(
['data', 'partition', 'getIdentity', 'getChildColor'],
({ data, partition, getIdentity, getColor, getChildColor }) => {
const total = data.value

const nodes = sortBy(partition(cloneDeep(data)).descendants(), 'depth')
nodes.forEach(node => {
const ancestor = getAncestor(node).data

delete node.children
delete node.data.children

Object.assign(node.data, {
id: getIdentity(node.data),
value: node.value,
percentage: 100 * node.value / total,
depth: node.depth,
ancestor,
})

if (node.depth === 1) {
node.data.color = getColor(node.data)
} else if (node.depth > 1) {
node.data.color = getChildColor(node.parent.data)
}
})

return { nodes }
}
),
pure
)

export default enhance(Sunburst)
56 changes: 56 additions & 0 deletions src/components/charts/sunburst/SunburstArc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 from 'react'
import PropTypes from 'prop-types'
import compose from 'recompose/compose'
import withPropsOnChange from 'recompose/withPropsOnChange'
import pure from 'recompose/pure'
import BasicTooltip from '../../tooltip/BasicTooltip'

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}
/>

SunburstArc.propTypes = {
node: PropTypes.shape({}).isRequired,
arcGenerator: PropTypes.func.isRequired,
borderWidth: PropTypes.number.isRequired,
borderColor: PropTypes.string.isRequired,
showTooltip: PropTypes.func.isRequired,
hideTooltip: PropTypes.func.isRequired,
}

const enhance = compose(
withPropsOnChange(['node', 'arcGenerator'], ({ node, arcGenerator }) => ({
path: arcGenerator(node),
})),
withPropsOnChange(['node', 'showTooltip'], ({ node, showTooltip }) => ({
showTooltip: e => {
showTooltip(
<BasicTooltip
id={node.data.id}
enableChip={true}
color={node.data.color}
value={`${node.data.percentage.toFixed(2)}%`}
/>,
e
)
},
})),
pure
)

export default enhance(SunburstArc)
Loading

0 comments on commit a8c872a

Please sign in to comment.