From a8c872a91e694721bb8a391483f9f0d9e4a15eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Wed, 16 Aug 2017 11:12:15 +0900 Subject: [PATCH] feat(sunburst): add Sunburst component --- README.md | 5 +- package.json | 2 +- src/components/axes/Axis.js | 24 +-- src/components/charts/stream/Stream.js | 41 ++-- .../charts/sunburst/ResponsiveSunburst.js | 18 ++ src/components/charts/sunburst/Sunburst.js | 184 ++++++++++++++++++ src/components/charts/sunburst/SunburstArc.js | 56 ++++++ src/components/charts/sunburst/index.js | 11 ++ src/index.js | 3 +- stories/charts/sunburst.stories.js | 32 +++ 10 files changed, 346 insertions(+), 30 deletions(-) create mode 100644 src/components/charts/sunburst/ResponsiveSunburst.js create mode 100644 src/components/charts/sunburst/Sunburst.js create mode 100644 src/components/charts/sunburst/SunburstArc.js create mode 100644 src/components/charts/sunburst/index.js create mode 100644 stories/charts/sunburst.stories.js diff --git a/README.md b/README.md index d32ccb181..454432fb0 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,10 @@ Several libraries already exist for React d3 integration, but just a few provide - [``](http://nivo.rocks/#/radar) - Stream - [``](http://nivo.rocks/#/stream) - - [``](http://nivo.rocks/#/stream) + - [``](http://nivo.rocks/#/stream) +- Sunburst + - [``](http://nivo.rocks/#/sunburst) + - [``](http://nivo.rocks/#/sunburst) - TreeMap - [``](http://nivo.rocks/#/treemap) - [``](http://nivo.rocks/#/treemap) diff --git a/package.json b/package.json index 96361c960..f561e06b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nivo", - "version": "0.10.0", + "version": "0.11.0", "licenses": [ { "type": "MIT", diff --git a/src/components/axes/Axis.js b/src/components/axes/Axis.js index 2623134ba..1db658d59 100644 --- a/src/components/axes/Axis.js +++ b/src/components/axes/Axis.js @@ -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 => { @@ -255,9 +256,6 @@ Axis.propTypes = { legendOffset: PropTypes.number.isRequired, theme: PropTypes.object.isRequired, - - // motion - ...motionPropTypes, } Axis.defaultProps = { @@ -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) diff --git a/src/components/charts/stream/Stream.js b/src/components/charts/stream/Stream.js index 018a1a15d..a919db981 100644 --- a/src/components/charts/stream/Stream.js +++ b/src/components/charts/stream/Stream.js @@ -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 @@ -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, @@ -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, @@ -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 ) diff --git a/src/components/charts/sunburst/ResponsiveSunburst.js b/src/components/charts/sunburst/ResponsiveSunburst.js new file mode 100644 index 000000000..6d47b3393 --- /dev/null +++ b/src/components/charts/sunburst/ResponsiveSunburst.js @@ -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 => + + {({ width, height }) => } + + +export default ResponsiveSunburst diff --git a/src/components/charts/sunburst/Sunburst.js b/src/components/charts/sunburst/Sunburst.js new file mode 100644 index 000000000..6d73f7281 --- /dev/null +++ b/src/components/charts/sunburst/Sunburst.js @@ -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 ( + + {({ showTooltip, hideTooltip }) => + + + {nodes + .filter(node => node.depth > 0) + .map((node, i) => + + )} + + } + + ) +} + +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) diff --git a/src/components/charts/sunburst/SunburstArc.js b/src/components/charts/sunburst/SunburstArc.js new file mode 100644 index 000000000..1f8ad1e5b --- /dev/null +++ b/src/components/charts/sunburst/SunburstArc.js @@ -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 }) => + + +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( + , + e + ) + }, + })), + pure +) + +export default enhance(SunburstArc) diff --git a/src/components/charts/sunburst/index.js b/src/components/charts/sunburst/index.js new file mode 100644 index 000000000..b9a4b88b8 --- /dev/null +++ b/src/components/charts/sunburst/index.js @@ -0,0 +1,11 @@ +/* + * 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. + */ +export { default as Sunburst } from './Sunburst' +export * from './Sunburst' +export { default as ResponsiveSunburst } from './ResponsiveSunburst' diff --git a/src/index.js b/src/index.js index 6c361cd01..8c14298d5 100644 --- a/src/index.js +++ b/src/index.js @@ -17,9 +17,10 @@ export * from './components/charts/chord' export * from './components/charts/line' export * from './components/charts/pie' export * from './components/charts/radar' +export * from './components/charts/stream' +export * from './components/charts/sunburst' export * from './components/charts/treemap' export * from './components/charts/voronoi' -export * from './components/charts/stream' export * from './components/axes' export * from './components/markers' export * from './constants' diff --git a/stories/charts/sunburst.stories.js b/stories/charts/sunburst.stories.js new file mode 100644 index 000000000..a561bd287 --- /dev/null +++ b/stories/charts/sunburst.stories.js @@ -0,0 +1,32 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { withKnobs, boolean, select } from '@storybook/addon-knobs' +import '../style.css' +import { Sunburst } from '../../src' +import { generateLibTree } from 'nivo-generators' + +const commonProperties = { + width: 600, + height: 600, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + data: generateLibTree(), + identity: 'name', + value: 'loc', + animate: true, +} + +const stories = storiesOf('Sunburst', module) + +stories + .addDecorator(story => +
+ {story()} +
+ ) + .addDecorator(withKnobs) + +stories.add('default', () => ) + +stories.add('with child color modifier', () => + +)