diff --git a/src/components/axes/Axes.js b/src/components/axes/Axes.js new file mode 100644 index 000000000..e835d8644 --- /dev/null +++ b/src/components/axes/Axes.js @@ -0,0 +1,87 @@ +/* + * 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 } from 'react' +import PropTypes from 'prop-types' +import Axis from './Axis' + +const horizontalPositions = ['top', 'bottom'] +const verticalPositions = ['left', 'right'] +const positions = [...horizontalPositions, ...verticalPositions] + +const axisPropType = PropTypes.shape({ + tickSize: PropTypes.number, + tickPadding: PropTypes.number, + format: PropTypes.func, +}) + +export default class Axes extends Component { + static propTypes = { + xScale: PropTypes.func.isRequired, + yScale: PropTypes.func.isRequired, + + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + + axes: PropTypes.shape({ + top: axisPropType, + right: axisPropType, + bottom: axisPropType, + left: axisPropType, + }).isRequired, + + theme: PropTypes.object.isRequired, + } + + static defaultProps = {} + + render() { + const { + xScale, + yScale, + width, + height, + axes, + theme, + animate, + motionStiffness, + motionDamping, + } = this.props + + return ( + + {positions.map(position => { + if (!axes[position]) return null + + const axis = axes[position] + if (axis.enabled !== undefined && axis.enabled === false) + return null + + const scale = horizontalPositions.includes(position) + ? xScale + : yScale + + return ( + + ) + })} + + ) + } +} diff --git a/src/components/axes/Grid.js b/src/components/axes/Grid.js index 10d44e891..ac81d6264 100644 --- a/src/components/axes/Grid.js +++ b/src/components/axes/Grid.js @@ -23,9 +23,12 @@ export default class Grid extends Component { static propTypes = { width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, + xScale: PropTypes.func, yScale: PropTypes.func, + theme: PropTypes.object.isRequired, + // motion animate: PropTypes.bool.isRequired, motionStiffness: PropTypes.number.isRequired, @@ -43,7 +46,6 @@ export default class Grid extends Component { const { width, height, - scales, xScale, yScale, theme, diff --git a/src/components/axes/index.js b/src/components/axes/index.js index a36805c13..d6017bdb7 100644 --- a/src/components/axes/index.js +++ b/src/components/axes/index.js @@ -7,4 +7,5 @@ * file that was distributed with this source code. */ export Axis from './Axis' +export Axes from './Axes' export Grid from './Grid' diff --git a/src/components/charts/bars/Bar.js b/src/components/charts/bars/Bar.js index 45f494240..79d174b2b 100644 --- a/src/components/charts/bars/Bar.js +++ b/src/components/charts/bars/Bar.js @@ -10,17 +10,11 @@ import { generateStackedBars, } from '../../../lib/charts/bar' import SvgWrapper from '../SvgWrapper' -import Axis from '../../axes/Axis' +import Axes from '../../axes/Axes' import Grid from '../../axes/Grid' import BarItem from './BarItem' import BarItemLabel from './BarItemLabel' -const axisPropType = PropTypes.shape({ - tickSize: PropTypes.number, - tickPadding: PropTypes.number, - format: PropTypes.func, -}) - export default class Bar extends Component { static propTypes = { // data @@ -50,12 +44,7 @@ export default class Bar extends Component { xPadding: PropTypes.number.isRequired, // axes - axes: PropTypes.shape({ - top: axisPropType, - right: axisPropType, - bottom: axisPropType, - left: axisPropType, - }), + axes: PropTypes.object.isRequired, enableGridX: PropTypes.bool.isRequired, enableGridY: PropTypes.bool.isRequired, @@ -81,8 +70,12 @@ export default class Bar extends Component { xPadding: 0.1, enableLabels: true, axes: { - left: {}, - bottom: {}, + left: { + enabled: true, + }, + bottom: { + enabled: true, + }, }, enableGridX: false, enableGridY: true, @@ -187,28 +180,16 @@ export default class Bar extends Component { height={height} xScale={enableGridX ? result.xScale : null} yScale={enableGridY ? result.yScale : null} + {...motionProps} + /> + - {['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 && result.bars.map(d => )} diff --git a/src/components/charts/line/Line.js b/src/components/charts/line/Line.js index 014c89608..99934aea4 100644 --- a/src/components/charts/line/Line.js +++ b/src/components/charts/line/Line.js @@ -8,58 +8,156 @@ */ import React, { Component } from 'react' import PropTypes from 'prop-types' -import _ from 'lodash' -import { curvePropMapping, curvePropType } from '../../../properties/curve' +import { merge } from 'lodash' import { line } from 'd3' +import Nivo, { defaultTheme } from '../../../Nivo' +import { margin as marginPropType } from '../../../PropTypes' +import { getColorRange } from '../../../ColorUtils' +import SvgWrapper from '../SvgWrapper' +import { + generateLines, + generateStackedLines, +} from '../../../lib/charts/line' +import { curvePropMapping, curvePropType } from '../../../properties/curve' +import Axes from '../../axes/Axes' +import Grid from '../../axes/Grid' export default class Line extends Component { static propTypes = { - scales: PropTypes.object.isRequired, + // data + data: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + x: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + y: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + }) + ).isRequired, + }) + ).isRequired, + + stacked: PropTypes.bool.isRequired, curve: curvePropType.isRequired, - color: PropTypes.string.isRequired, + + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + margin: marginPropType, + + // axes + axes: PropTypes.object.isRequired, + enableGridX: PropTypes.bool.isRequired, + enableGridY: PropTypes.bool.isRequired, + + theme: PropTypes.object.isRequired, + colors: PropTypes.any.isRequired, + + // motion + animate: PropTypes.bool.isRequired, + motionStiffness: PropTypes.number.isRequired, + motionDamping: PropTypes.number.isRequired, } static defaultProps = { - scales: {}, + stacked: false, curve: 'linear', - color: '#000', + margin: Nivo.defaults.margin, + axes: { + left: { + enabled: true, + }, + bottom: { + enabled: true, + }, + }, + enableGridX: true, + enableGridY: true, + colors: Nivo.defaults.colorRange, + theme: {}, + animate: true, + motionStiffness: Nivo.defaults.motionStiffness, + motionDamping: Nivo.defaults.motionDamping, } render() { const { data, - scales, - xScale: _xScale, - yScale: _yScale, - x, - y, + stacked, curve, - color, + margin: _margin, + width: _width, + height: _height, + axes, + enableGridX, + enableGridY, + theme: _theme, + colors, + animate, + motionStiffness, + motionDamping, } = this.props - const xScale = scales[_xScale] - const yScale = scales[_yScale] + const margin = Object.assign({}, Nivo.defaults.margin, _margin) + const width = _width - margin.left - margin.right + const height = _height - margin.top - margin.bottom - const getX = _.isFunction(x) ? x : d => d[x] - const getY = _.isFunction(y) ? y : d => d[y] + const theme = merge({}, defaultTheme, _theme) + const color = getColorRange(colors) - const points = data.map(d => ({ - x: xScale(getX(d)), - y: yScale(getY(d)), - })) + const motionProps = { + animate, + motionDamping, + motionStiffness, + } + + let result + if (stacked === true) { + result = generateStackedLines(data, width, height) + } else { + result = generateLines(data, width, height) + } const lineGenerator = line() .x(d => d.x) .y(d => d.y) .curve(curvePropMapping[curve]) + const { xScale, yScale, lines } = result + return ( - + + + + {lines.map(({ id, points }) => ( + + ))} + ) } } diff --git a/src/lib/charts/line/index.js b/src/lib/charts/line/index.js new file mode 100644 index 000000000..e1a9b0830 --- /dev/null +++ b/src/lib/charts/line/index.js @@ -0,0 +1,129 @@ +import { range, max, maxBy, sumBy, uniq } from 'lodash' +import { scalePoint, scaleLinear } from 'd3' + +/** + * Generates X scale. + * + * @param {Array.} data + * @param {number} width + * @returns {Function} + */ +export const getXScale = (data, width) => { + 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 scalePoint().range([0, width]).domain(data[0].data.map(({ x }) => x)) +} + +/** + * Generates Y scale for line chart. + * + * @param {Array.} data + * @param {number} height + * @returns {Function} + */ +export const getYScale = (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 line 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 & lines for line chart. + * + * @param {Array.} data + * @param {number} width + * @param {number} height + * @return {{ xScale: Function, yScale: Function, lines: Array. }} + */ +export const generateLines = (data, width, height) => { + const xScale = getXScale(data, width) + const yScale = getYScale(data, height) + + const lines = data.map(({ id, data: serie }) => ({ + id, + points: serie.map(d => Object.assign({}, d, { + value: d.y, + x: xScale(d.x), + y: yScale(d.y), + })) + })) + + return { xScale, yScale, lines } +} + +/** + * Generates x/y scales & lines for stacked line chart. + * + * @param {Array.} data + * @param {number} width + * @param {number} height + * @return {{ xScale: Function, yScale: Function, lines: Array. }} + */ +export const generateStackedLines = (data, width, height) => { + const xScale = getXScale(data, width) + const yScale = getStackedYScale(data, xScale, height) + + const lines = data.reduce((acc, { id, data: serie }, serieIndex) => { + const previousPoints = serieIndex === 0 ? null : acc[serieIndex - 1].points + + return [ + ...acc, + { + id, + points: serie.map((d, i) => { + if (!previousPoints) { + return Object.assign({}, d, { + value: d.y, + x: d.x, + y: d.y, + }) + } + + return Object.assign({}, d, { + value: d.y, + x: d.x, + y: d.y + previousPoints[i].accY, + }) + }).map(d => ({ + value: d.value, + accY: d.y, + x: xScale(d.x), + y: yScale(d.y), + })) + } + ] + }, []) + + console.log(lines) + + return { xScale, yScale, lines } +}