diff --git a/src/components/axes/Axis.js b/src/components/axes/Axis.js index 3969b2c15..7a5806e9d 100644 --- a/src/components/axes/Axis.js +++ b/src/components/axes/Axis.js @@ -9,7 +9,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { TransitionMotion, spring } from 'react-motion' -import { max } from 'd3' import Nivo from '../../Nivo' import AxisTick from './AxisTick' diff --git a/src/components/charts/bars/Bar.js b/src/components/charts/bars/Bar.js index c0c0e92db..45f494240 100644 --- a/src/components/charts/bars/Bar.js +++ b/src/components/charts/bars/Bar.js @@ -1,15 +1,19 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import _ from 'lodash' +import { merge } from 'lodash' +import { TransitionMotion, spring } from 'react-motion' import Nivo, { defaultTheme } from '../../../Nivo' import { margin as marginPropType } from '../../../PropTypes' import { getColorRange } from '../../../ColorUtils' +import { + generateGroupedBars, + generateStackedBars, +} from '../../../lib/charts/bar' import SvgWrapper from '../SvgWrapper' import Axis from '../../axes/Axis' import Grid from '../../axes/Grid' import BarItem from './BarItem' import BarItemLabel from './BarItemLabel' -import { scaleBand, scaleLinear, stack } from 'd3' const axisPropType = PropTypes.shape({ tickSize: PropTypes.number, @@ -17,37 +21,6 @@ const axisPropType = PropTypes.shape({ format: PropTypes.func, }) -const getAxis = ( - axes, - scale, - position, - width, - height, - theme, - { animate, motionStiffness, motionDamping } -) => { - if (!axes[position]) return null - - const axis = axes[position] - - return ( - - ) -} - export default class Bar extends Component { static propTypes = { // data @@ -142,125 +115,68 @@ export default class Bar extends Component { const width = _width - margin.left - margin.right const height = _height - margin.top - margin.bottom - const theme = _.merge({}, defaultTheme, _theme) + const theme = merge({}, defaultTheme, _theme) const color = getColorRange(colors) - // determining x scale - const xLengths = _.uniq(data.map(({ data }) => data.length)) - if (xLengths.length > 1) { - throw new Error( - [ - `Found inconsitent data for x,`, - `expecting all series to have same length`, - `but found: ${xLengths.join(', ')}`, - ].join(' ') - ) - } - const xScale = scaleBand() - .rangeRound([0, width]) - .domain(data[0].data.map(({ x }) => x)) - .padding(xPadding) - - // determining y scale, depending on `groupMode` - let maxY - if (groupMode === 'stacked') { - maxY = _.max( - _.range(xLengths).map(i => - _.sumBy(data, serie => serie.data[i].y) - ) - ) - } else if (groupMode === 'grouped') { - maxY = _.maxBy( - data.reduce((acc, serie) => [...acc, ...serie.data], []), - 'y' - ).y - } else { - throw new TypeError( - [ - `'${groupMode}' is not a valid group mode,`, - `must be one of: 'grouped', 'stacked'`, - ].join(' ') - ) + const motionProps = { + animate, + motionDamping, + motionStiffness, } - const yScale = scaleLinear().rangeRound([height, 0]).domain([0, maxY]) - const rects = [] + let result if (groupMode === 'grouped') { - data.forEach(({ id, data: serie }, serieIndex) => { - serie.forEach(d => { - const barWidth = xScale.bandwidth() / data.length - const x = xScale(d.x) + barWidth * serieIndex - const y = yScale(d.y) - const barHeight = height - y - - const value = d.y - - if (barWidth > 0 && barHeight > 0) { - rects.push({ - key: `${id}.${d.x}`, - value, - x, - y, - width: barWidth, - height: barHeight, - color: color(id), - }) - } - }) + result = generateGroupedBars(data, width, height, color, { + xPadding, }) } else if (groupMode === 'stacked') { - const stackedData = data.map(({ id }) => ({ - id, - data: [], - })) - _.range(xLengths).forEach(index => { - data.forEach(({ data: serie }, serieIndex) => { - const d = serie[index] - - let y0 = 0 - let y1 = d.y - if (serieIndex > 0) { - y0 = stackedData[serieIndex - 1].data[index].y1 - y1 = d.y + y0 - } - - stackedData[serieIndex].data[index] = Object.assign({}, d, { - y0, - y1, - }) - }) - }) - - console.log(stackedData) - - stackedData.forEach(({ id, data: serie }) => { - serie.forEach(d => { - const x = xScale(d.x) - const barWidth = xScale.bandwidth() - const y = yScale(d.y1) - const barHeight = yScale(d.y0) - y - - const value = d.y - - if (barWidth > 0 && barHeight > 0) { - rects.push({ - key: `${id}.${d.x}`, - value, - x, - y, - width: barWidth, - height: barHeight, - color: color(id), - }) - } - }) + result = generateStackedBars(data, width, height, color, { + xPadding, }) } - const motionProps = { - animate, - motionDamping, - motionStiffness, + let bars + if (animate === true) { + bars = ( + { + return { + key: bar.key, + data: { + color: bar.color, + value: bar.value, + }, + style: { + x: spring(bar.x, motionProps), + y: spring(bar.y, motionProps), + width: spring(bar.width, motionProps), + height: spring(bar.height, motionProps), + }, + } + })} + > + {interpolatedStyles => + + {interpolatedStyles.map( + ({ key, style, data: { value, color } }) => + + )} + } + + ) + } else { + bars = result.bars.map(d => ) } return ( @@ -269,18 +185,33 @@ export default class Bar extends Component { theme={theme} width={width} height={height} - xScale={enableGridX ? xScale : null} - yScale={enableGridY ? yScale : null} + xScale={enableGridX ? result.xScale : null} + yScale={enableGridY ? result.yScale : null} /> - {['left', 'right'].map(position => - getAxis(axes, yScale, position, width, height, theme, motionProps) - )} - {['top', 'bottom'].map(position => - getAxis(axes, xScale, position, width, height, theme, motionProps) - )} - {rects.map(d => )} + {['top', 'right', 'bottom', 'left'].map(position => { + if (!axes[position]) return null + + const axis = axes[position] + const scale = ['top', 'bottom'].includes(position) + ? result.xScale + : result.yScale + + return ( + + ) + })} + {bars} {enableLabels && - rects.map(d => )} + result.bars.map(d => )} ) } diff --git a/src/components/charts/bars/BarsD3.js b/src/components/charts/bars/BarD3.js similarity index 99% rename from src/components/charts/bars/BarsD3.js rename to src/components/charts/bars/BarD3.js index 3a73a644d..8251e84bf 100644 --- a/src/components/charts/bars/BarsD3.js +++ b/src/components/charts/bars/BarD3.js @@ -16,7 +16,7 @@ import { getColorRange, getColorGenerator } from '../../../ColorUtils' import { margin as marginPropType } from '../../../PropTypes' import decoratorsFromReactChildren from '../../../lib/decoratorsFromReactChildren' -class BarsD3 extends Component { +class BarD3 extends Component { renderD3(props) { const { groupMode, diff --git a/src/components/charts/bars/BarGroup.js b/src/components/charts/bars/BarGroup.js deleted file mode 100644 index b4a2f0c25..000000000 --- a/src/components/charts/bars/BarGroup.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * This file is part of the nivo project. - * - * (c) 2016 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 } from 'react' -import _ from 'lodash' -import PropTypes from 'prop-types' -import ResponsiveWrapper from '../ResponsiveWrapper' -import SvgWrapper from '../SvgWrapper' -import Nivo, { defaultTheme } from '../../../Nivo' -import { margin as marginPropType } from '../../../PropTypes' -import { getColorRange } from '../../../ColorUtils' -import Bar from './Bars' -import { scalesPropType, scalesFromObject } from '../../../lib/scale' - -const MODE_GROUPED = 'group' -const MODE_STACKED = 'stack' - -export default class BarGroup extends Component { - static propTypes = { - scales: scalesPropType, - data: PropTypes.arrayOf(PropTypes.object), - keys: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - ).isRequired, - mode: PropTypes.oneOf([MODE_GROUPED, MODE_STACKED]).isRequired, - colors: PropTypes.any.isRequired, - children: PropTypes.func.isRequired, - margin: marginPropType, - theme: PropTypes.object.isRequired, - } - - static defaultProps = { - mode: 'stack', - colors: Nivo.defaults.colorRange, - theme: {}, - children: () => null, - } - - render() { - return ( - - {({ width: _width, height: _height }) => { - const { - data, - margin: _margin, - keys, - xScale, - x, - yScale, - scales: _scales, - colors, - children, - theme: _theme, - } = this.props - - const margin = Object.assign( - {}, - Nivo.defaults.margin, - _margin - ) - const width = _width - margin.left - margin.right - const height = _height - margin.top - margin.bottom - - const theme = _.merge({}, defaultTheme, _theme) - const scales = scalesFromObject( - _scales, - width, - height, - data - ) - const getColor = getColorRange(colors) - - const barData = data.map(d => - keys.reduce((acc, key, index) => { - let y0 = 0 - let y1 = d[key] - if (index > 0) { - y0 = acc[keys[index - 1]][1] - y1 += y0 - } - - return Object.assign(acc, { - [key]: [y0, y1], - }) - }, d) - ) - - return ( - - {children({ - data, - barData, - scales, - width, - height, - theme, - })} - - {keys.map(key => - - )} - - - ) - }} - - ) - } -} diff --git a/src/components/charts/bars/Bars.js b/src/components/charts/bars/Bars.js deleted file mode 100644 index 4d5bcd46c..000000000 --- a/src/components/charts/bars/Bars.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This file is part of the nivo project. - * - * (c) 2016 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 } from 'react' -import PropTypes from 'prop-types' -import _ from 'lodash' -import Nivo from '../../../Nivo' -import { getColorRange } from '../../../ColorUtils' -import { getAccessorFor } from '../../../lib/propertiesConverters' -import BarItem from './BarItem' -import BarItemLabel from './BarItemLabel' - -export default class Bars extends Component { - static propTypes = { - data: PropTypes.arrayOf(PropTypes.object), - colors: PropTypes.any.isRequired, - scales: PropTypes.object, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - // labels - enableLabels: PropTypes.bool.isRequired, - // motion - animate: PropTypes.bool.isRequired, - motionStiffness: PropTypes.number.isRequired, - motionDamping: PropTypes.number.isRequired, - } - - static defaultProps = { - colors: Nivo.defaults.colorRange, - // labels - enableLabels: true, - // motion - animate: true, - motionStiffness: Nivo.defaults.motionStiffness, - motionDamping: Nivo.defaults.motionDamping, - } - - render() { - const { - data, - scales, - xScale: _xScale, - yScale: _yScale, - height, - colors, - x: _x, - y: _y, - } = this.props - - const xScale = scales[_xScale] - const yScale = scales[_yScale] - - const getXValue = getAccessorFor(_x) - const getYValue = getAccessorFor(_y) - - const getColor = getColorRange(colors) - - const rects = [] - data.forEach((d, i) => { - let x - let y - let barWidth - let barHeight - - const xValue = getXValue(d) - const yValue = getYValue(d) - - if (xScale.bandwidth) { - barWidth = xScale.bandwidth() - x = xScale(xValue) - } else { - x = 0 - barWidth = xScale(xValue) - } - - if (yScale.bandwidth) { - barHeight = yScale.bandwidth() - y = yScale(getYValue(d)) - } else { - if (Array.isArray(yValue)) { - y = yScale(yValue[1]) - barHeight = yScale(yValue[0]) - y - } else { - y = yScale(yValue) - barHeight = height - y - } - } - - if (barWidth > 0 && barHeight > 0) { - rects.push({ - key: `bar.${i}`, - x, - y, - width: barWidth, - height: barHeight, - color: getColor(d), - }) - } - }) - - return ( - - {rects.map(d => )} - - ) - } -} diff --git a/src/components/charts/bars/ResponsiveBarsD3.js b/src/components/charts/bars/ResponsiveBarsD3.js index 51a8f41e9..c09a9b07d 100644 --- a/src/components/charts/bars/ResponsiveBarsD3.js +++ b/src/components/charts/bars/ResponsiveBarsD3.js @@ -1,28 +1,22 @@ /* * This file is part of the nivo project. * - * (c) Raphaël Benitte + * (c) 2016 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 } from 'react' -import PropTypes from 'prop-types' -import Dimensions from 'react-dimensions' -import BarsD3 from './BarsD3' +import ResponsiveWrapper from '../ResponsiveWrapper' +import BarD3 from './BarD3' -class ResponsiveBarsD3 extends Component { +export default class ResponsiveBarD3 extends Component { render() { - const { containerWidth, containerHeight } = this.props - return ( - + + {({ width, height }) => + } + ) } } - -export default Dimensions()(ResponsiveBarsD3) diff --git a/src/components/charts/bars/barsDataSchema.js b/src/components/charts/bars/barsDataSchema.js deleted file mode 100644 index 621f589a3..000000000 --- a/src/components/charts/bars/barsDataSchema.js +++ /dev/null @@ -1,5 +0,0 @@ -const barsDataSchema = { - type: 'array', -} - -export default barsDataSchema diff --git a/src/components/charts/bars/index.js b/src/components/charts/bars/index.js index 0b25ab7a5..fbc4de5bc 100644 --- a/src/components/charts/bars/index.js +++ b/src/components/charts/bars/index.js @@ -7,6 +7,4 @@ * file that was distributed with this source code. */ export Bar from './Bar' -export Bars from './Bars' export ResponsiveBar from './ResponsiveBar' -export BarGroup from './BarGroup' diff --git a/src/lib/charts/bar/index.js b/src/lib/charts/bar/index.js new file mode 100644 index 000000000..4bc2f6f00 --- /dev/null +++ b/src/lib/charts/bar/index.js @@ -0,0 +1,178 @@ +import { range, max, maxBy, sumBy, uniq } from 'lodash' +import { scaleBand, scaleLinear } from 'd3' + +/** + * Generates X scale. + * + * @param {Array.} data + * @param {number} width + * @param {number} padding + * @returns {Function} + */ +export const getXScale = (data, width, padding) => { + const xLengths = uniq(data.map(({ data }) => data.length)) + if (xLengths.length > 1) { + throw new Error( + [ + `Found inconsitent data for x,`, + `expecting all series to have same length`, + `but found: ${xLengths.join(', ')}`, + ].join(' ') + ) + } + + return scaleBand() + .rangeRound([0, width]) + .domain(data[0].data.map(({ x }) => x)) + .padding(padding) +} + +/** + * Generates Y scale for grouped bar chart. + * + * @param {Array.} data + * @param {number} height + * @returns {Function} + */ +export const getGroupedYScale = (data, height) => { + const maxY = maxBy( + data.reduce((acc, serie) => [...acc, ...serie.data], []), + 'y' + ).y + + return scaleLinear().rangeRound([height, 0]).domain([0, maxY]) +} + +/** + * Generates Y scale for stacked bar chart. + * + * @param {Array.} data + * @param {Object} xScale + * @param {number} height + */ +export const getStackedYScale = (data, xScale, height) => { + const maxY = max( + range(xScale.domain().length).map(i => + sumBy(data, serie => serie.data[i].y) + ) + ) + + return scaleLinear().rangeRound([height, 0]).domain([0, maxY]) +} + +/** + * Generates x/y scales & bars for grouped bar chart. + * + * @param {Array.} data + * @param {number} width + * @param {number} height + * @param {Function} color + * @param {number} xPadding + * @return {{ xScale: Function, yScale: Function, bars: Array. }} + */ +export const generateGroupedBars = ( + data, + width, + height, + color, + { xPadding = 0 } = {} +) => { + const xScale = getXScale(data, width, xPadding) + const yScale = getGroupedYScale(data, height) + + const bars = [] + data.forEach(({ id, data: serie }, serieIndex) => { + serie.forEach(d => { + const barWidth = xScale.bandwidth() / data.length + const x = xScale(d.x) + barWidth * serieIndex + const y = yScale(d.y) + const barHeight = height - y + + const value = d.y + + if (barWidth > 0 && barHeight > 0) { + bars.push({ + key: `${id}.${d.x}`, + value, + x, + y, + width: barWidth, + height: barHeight, + color: color(id), + }) + } + }) + }) + + return { xScale, yScale, bars } +} + +/** + * Generates x/y scales & bars for stacked bar chart. + * + * @param {Array.} data + * @param {number} width + * @param {number} height + * @param {Function} color + * @param {number} xPadding + * @return {{ xScale: Function, yScale: Function, bars: Array. }} + */ +export const generateStackedBars = ( + data, + width, + height, + color, + { xPadding = 0 } = {} +) => { + const xScale = getXScale(data, width, xPadding) + const yScale = getStackedYScale(data, xScale, height) + + const stackedData = data.map(({ id }) => ({ + id, + data: [], + })) + + range(xScale.domain().length).forEach((__, index) => { + data.forEach(({ data: serie }, serieIndex) => { + const d = serie[index] + + let y0 = 0 + let y1 = d.y + if (serieIndex > 0) { + y0 = stackedData[serieIndex - 1].data[index].y1 + y1 = d.y + y0 + } + + stackedData[serieIndex].data[index] = Object.assign({}, d, { + y0, + y1, + }) + }) + }) + + const bars = [] + stackedData.forEach(({ id, data: serie }) => { + serie.forEach(d => { + const x = xScale(d.x) + const barWidth = xScale.bandwidth() + const y = yScale(d.y1) + const barHeight = yScale(d.y0) - y + + const value = d.y + + if (barWidth > 0 && barHeight > 0) { + bars.push({ + key: `${id}.${d.x}`, + value, + x, + y, + width: barWidth, + height: barHeight, + color: color(id), + }) + } + }) + }) + + return { xScale, yScale, bars } +}