From a8c1b7699cd8e2d4a3a5544ba8405da3540feaea Mon Sep 17 00:00:00 2001 From: Colin Meinke Date: Tue, 15 Mar 2016 12:03:23 -0600 Subject: [PATCH] feat(chart): add labels to charts BREAKING CHANGE: now requires react 15... + removal/renaming of props --- examples/barChart/App.js | 35 +++--- examples/barChart/Page.js | 59 ++++++++-- examples/barChart/package.json | 4 +- examples/lineChart/App.js | 87 +++++++------- examples/lineChart/Page.js | 66 +++++++++-- examples/lineChart/package.json | 4 +- package.json | 4 +- src/BarChart.js | 155 +++++++++++++++++++------ src/LineChart.js | 198 ++++++++++++++++++++++++++------ 9 files changed, 462 insertions(+), 150 deletions(-) diff --git a/examples/barChart/App.js b/examples/barChart/App.js index 78de674..37cbe28 100644 --- a/examples/barChart/App.js +++ b/examples/barChart/App.js @@ -5,31 +5,31 @@ const days = [ { title: 'Thursday, 9th March', bars: [ - { value: 3.50 }, - { value: 7.45 }, - { value: 1.27 }, - { value: 1.15 }, - { value: 2.93 }, + { label: 'travel', value: 0.00 }, + { label: 'accomodation', value: 20.25 }, + { label: 'food', value: 20.28 }, + { label: 'drink', value: 7.43 }, + { label: 'tourism', value: 13.50 }, ], }, { title: 'Wednesday, 8th March', bars: [ - { value: 1.92 }, - { value: 1.11 }, - { value: 7.20 }, - { value: 6.34 }, - { value: 3.15 }, + { label: 'travel', value: 13.50 }, + { label: 'accomodation', value: 17.77 }, + { label: 'food', value: 14.63 }, + { label: 'drink', value: 9.47 }, + { label: 'tourism', value: 0.00 }, ], }, { title: 'Tuesday, 7th March', bars: [ - { value: 5.37 }, - { value: 7.32 }, - { value: 0.90 }, - { value: 4.78 }, - { value: 2.75 }, + { label: 'travel', value: 138.88 }, + { label: 'accomodation', value: 21.50 }, + { label: 'food', value: 17.42 }, + { label: 'drink', value: 3.98 }, + { label: 'tourism', value: 0.00 }, ], }, ]; @@ -56,9 +56,12 @@ const App = createClass({ ))} `£${ v.toFixed( 2 )}` } /> ); diff --git a/examples/barChart/Page.js b/examples/barChart/Page.js index d646f9a..c493214 100644 --- a/examples/barChart/Page.js +++ b/examples/barChart/Page.js @@ -8,31 +8,70 @@ const Page = ({ app }) => ( diff --git a/examples/barChart/package.json b/examples/barChart/package.json index 79d1965..a1f37c1 100644 --- a/examples/barChart/package.json +++ b/examples/barChart/package.json @@ -22,8 +22,8 @@ "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "express": "^4.13.4", - "react": "^0.14.7", - "react-dom": "^0.14.7" + "react": "^15.0.0-rc.1", + "react-dom": "^15.0.0-rc.1" }, "description": "SVG charts bar chart example", "devDependencies": { diff --git a/examples/lineChart/App.js b/examples/lineChart/App.js index 88f8934..95b75d0 100644 --- a/examples/lineChart/App.js +++ b/examples/lineChart/App.js @@ -1,47 +1,47 @@ import React, { createClass } from 'react'; import { LineChart } from '../../src'; -const days = [ +const categories = [ { - title: 'Thursday, 9th March', - lines: [ - { - points: [ - { value: 3.50 }, - { value: 7.45 }, - { value: 1.27 }, - { value: 1.15 }, - { value: 2.93 }, - ], - }, + title: 'Accomodation', + points: [ + { label: 'Fri 17', value: 7.65 }, + { label: 'Sat 18', value: 25.50 }, + { label: 'Sun 19', value: 21.55 }, + { label: 'Mon 20', value: 21.55 }, + { label: 'Tue 21', value: 21.55 }, + { label: 'Wed 22', value: 21.55 }, + { label: 'Thu 23', value: 39.82 }, + { label: 'Fri 24', value: 39.82 }, + { label: 'Sat 25', value: 39.82 }, ], }, { - title: 'Wednesday, 8th March', - lines: [ - { - points: [ - { value: 1.92 }, - { value: 1.11 }, - { value: 7.20 }, - { value: 6.34 }, - { value: 3.15 }, - ], - }, + title: 'Food', + points: [ + { label: 'Fri 17', value: 5.46 }, + { label: 'Sat 18', value: 5.71 }, + { label: 'Sun 19', value: 9.79 }, + { label: 'Mon 20', value: 9.03 }, + { label: 'Tue 21', value: 13.52 }, + { label: 'Wed 22', value: 12.50 }, + { label: 'Thu 23', value: 15.56 }, + { label: 'Fri 24', value: 9.18 }, + { label: 'Sat 25', value: 9.44 }, ], }, { - title: 'Tuesday, 7th March', - lines: [ - { - points: [ - { value: 5.37 }, - { value: 7.32 }, - { value: 0.90 }, - { value: 4.78 }, - { value: 2.75 }, - ], - }, + title: 'Drink', + points: [ + { label: 'Fri 17', value: 2.35 }, + { label: 'Sat 18', value: 2.55 }, + { label: 'Sun 19', value: 10.20 }, + { label: 'Mon 20', value: 10.97 }, + { label: 'Tue 21', value: 3.83 }, + { label: 'Wed 22', value: 2.04 }, + { label: 'Thu 23', value: 4.52 }, + { label: 'Fri 24', value: 1.28 }, + { label: 'Sat 25', value: 10.91 }, ], }, ]; @@ -49,13 +49,13 @@ const days = [ const App = createClass({ onChange ( e ) { this.setState({ - day: days[ e.target.value ], + category: categories[ e.target.value ], }); }, getInitialState () { return { - day: days[ 0 ], + category: categories[ 0 ], }; }, @@ -63,14 +63,21 @@ const App = createClass({ return (
`£${ v.toFixed( 2 )}` } + lines={[{ points: this.state.category.points }]} + pointSize={ 18 } + labelSpacing={ 15 } preserveAspectRatio="xMinYMid meet" + title="Travel budget" + valueHeight={ 34 } + valueOffset={ 37 } + valueWidth={ 65 } />
); diff --git a/examples/lineChart/Page.js b/examples/lineChart/Page.js index 6c22910..8a706df 100644 --- a/examples/lineChart/Page.js +++ b/examples/lineChart/Page.js @@ -8,31 +8,77 @@ const Page = ({ app }) => ( diff --git a/examples/lineChart/package.json b/examples/lineChart/package.json index 4bb8d09..77494f0 100644 --- a/examples/lineChart/package.json +++ b/examples/lineChart/package.json @@ -22,8 +22,8 @@ "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "express": "^4.13.4", - "react": "^0.14.7", - "react-dom": "^0.14.7" + "react": "^15.0.0-rc.1", + "react-dom": "^15.0.0-rc.1" }, "description": "SVG charts line chart example", "devDependencies": { diff --git a/package.json b/package.json index 3c9b46c..51305e9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "babel-preset-react": "^6.5.0", "commitizen": "^2.7.2", "cz-conventional-changelog": "^1.1.5", - "react": "^0.14.7", + "react": "^15.0.0-rc.1", "semantic-release": "^4.3.5" }, "keywords": [ @@ -53,7 +53,7 @@ "main": "lib/index.js", "name": "react-svg-chart", "peerDependencies": { - "react": "^0.14.7" + "react": "^15.0.0-rc.1" }, "repository": { "type": "git", diff --git a/src/BarChart.js b/src/BarChart.js index 5fb11e9..52aca99 100644 --- a/src/BarChart.js +++ b/src/BarChart.js @@ -3,65 +3,162 @@ import tween from 'tweening'; const BarChart = createClass({ propTypes: { - barClassName: PropTypes.string, bars: PropTypes.array.isRequired, - chartClassName: PropTypes.string, + barSpacing: PropTypes.number, + className: PropTypes.string, + description: PropTypes.string, + duration: PropTypes.number, easing: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]), + formatValue: PropTypes.func, height: PropTypes.number, + labelSpacing: PropTypes.number, + labelWidth: PropTypes.number, preserveAspectRatio: PropTypes.string, - spacing: PropTypes.number, + showLabels: PropTypes.bool, + title: PropTypes.string, + valueSpacing: PropTypes.number, width: PropTypes.number, }, getDefaultProps () { return { - duration: 750, - easing: 'easeOutBounce', + barSpacing: 10, + className: '', + description: '', + duration: 400, + easing: 'easeInOutQuad', + formatValue: v => v, height: 500, + labelSpacing: 10, + labelWidth: 100, preserveAspectRatio: 'xMidYMid meet', - spacing: 10, + showLabels: true, + title: 'Bar chart', + valueSpacing: 10, width: 800, }; }, getInitialState () { - return { - barHeight: ( this.props.height - this.props.spacing * ( this.props.bars.length - 1 )) / this.props.bars.length, - bars: this.relativeBars( this.props.bars ), - }; + const { bars, labelWidth, showLabels, width } = this.props; + return this.calculateState({ bars, labelWidth, showLabels, width }); }, - componentWillReceiveProps ({ bars }) { - const relativeBars = this.relativeBars( bars ); - if ( JSON.stringify( relativeBars ) !== JSON.stringify( this.state.bars )) { - this.animateBars( this.state.bars, relativeBars ); + componentWillReceiveProps ({ bars, labelWidth, showLabels, width }) { + const { bars: nextBars, ...state } = this.calculateState({ bars, labelWidth, showLabels, width }); + + this.setState( state ); + + if ( JSON.stringify( nextBars ) !== JSON.stringify( this.state.bars )) { + this.animateBars( this.state.bars, nextBars ); } }, render () { + const barHeight = ( this.props.height - this.props.barSpacing * ( this.props.bars.length + 1 )) / this.props.bars.length; + const x = this.props.showLabels ? -this.props.labelWidth : 0; + return ( - { this.state.bars.map(( bar, i ) => ( - + { this.props.title } + + + + { this.props.description } + + + { this.state.bars.map(( bar, i ) => { + const barY = ( barHeight + this.props.barSpacing ) * i + this.props.barSpacing; + const textY = barY + barHeight / 2; + const value = this.props.bars[ i ].value; + const formattedValue = this.props.formatValue( value ); + const valueInBar = bar.value > this.state.x / 2; + + return ( + + + { this.props.showLabels ? + + { bar.label } + : + + { bar.label } + + } + + { formattedValue } + + + ); + })} + + + + + + + - ))} + ); }, + calculateState ({ bars, labelWidth, showLabels, width }) { + const x = showLabels ? width - labelWidth : width; + const scale = ( x / 100 ) / ( Math.max( ...bars.map( b => b.value )) / 100 ); + const relativeBars = bars.map( b => ({ ...b, value: b.value * scale })); + return { bars: relativeBars, scale, x }; + }, + animateBars ( from, to ) { tween({ duration: this.props.duration, @@ -71,12 +168,6 @@ const BarChart = createClass({ next: bars => this.setState({ bars }), }); }, - - relativeBars ( bars ) { - const absolutePercent = this.props.width / 100; - const relativePercent = Math.max( ...bars.map( b => b.value )) / 100; - return bars.map( b => ({ ...b, value: b.value / relativePercent * absolutePercent })); - } }); export default BarChart; diff --git a/src/LineChart.js b/src/LineChart.js index 83f71c9..8163451 100644 --- a/src/LineChart.js +++ b/src/LineChart.js @@ -3,66 +3,205 @@ import tween from 'tweening'; const LineChart = createClass({ propTypes: { - chartClassName: PropTypes.string, + className: PropTypes.string, + description: PropTypes.string, + duration: PropTypes.number, easing: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]), + formatValue: PropTypes.func, height: PropTypes.number, - lineClassName: PropTypes.string, + labelHeight: PropTypes.number, + labelOffset: PropTypes.number, lines: PropTypes.array.isRequired, + pointSize: PropTypes.number, preserveAspectRatio: PropTypes.string, + showLabels: PropTypes.bool, + title: PropTypes.string, + valueHeight: PropTypes.number, + valueOffset: PropTypes.number, + valueRadius: PropTypes.number, + valueWidth: PropTypes.number, width: PropTypes.number, }, getDefaultProps () { return { + className: '', + description: '', duration: 400, easing: 'easeInOutQuad', + formatValue: v => v, height: 500, + labelHeight: 50, + labelOffset: 10, + pointSize: 20, preserveAspectRatio: 'xMidYMid meet', + showLabels: true, + title: 'Line chart', + valueHeight: 30, + valueOffset: 35, + valueRadius: 2, + valueWidth: 60, width: 800, }; }, getInitialState () { - return { - lines: this.relativeLines( this.props.lines ), - spacing: this.props.width / ( this.props.lines.reduce(( p, c ) => { - return Math.max( p, c.points.length ); - }, 0 ) - 1 ), - }; + const { height, labelHeight, labelOffset, lines, showLabels, valueHeight, valueOffset } = this.props; + return this.calculateState({ height, labelHeight, labelOffset, lines, showLabels, valueHeight, valueOffset }); }, - componentWillReceiveProps ({ lines }) { - const relativeLines = this.relativeLines( lines ); - if ( JSON.stringify( relativeLines ) !== JSON.stringify( this.state.lines )) { - this.animateLines( this.state.lines, relativeLines ); + componentWillReceiveProps ({ height, labelHeight, labelOffset, lines, showLabels, valueHeight, valueOffset }) { + const { lines: nextLines, ...state } = this.calculateState({ height, labelHeight, labelOffset, lines, showLabels, valueHeight, valueOffset }) + + this.setState( state ); + + if ( JSON.stringify( nextLines ) !== JSON.stringify( this.state.lines )) { + this.animateLines( this.state.lines, nextLines ); } }, render () { + const pointSpacing = this.props.width / ( this.state.lines.reduce(( p, c ) => Math.max( p, c.points.length ), 0 ) + 1 ); + return ( + + { this.props.title } + + + + { this.props.description } + + + + + + + + + + { this.state.lines.map(({ points }, i ) => ( - ( - `${ this.state.spacing * j },${ p.value }` - )).join( ',' )} - stroke="rgb(241,76,84)" - strokeWidth="5" - /> + > + ( + `${ pointSpacing * ( j + 1 )},${ p.value }` + )).join( ',' )} + stroke="rgb( 0, 0, 0 )" + stroke-linejoin="round" + stroke-width="5" + /> + + { points.map(( p, j ) => { + const x = pointSpacing * ( j + 1 ); + const value = this.props.lines[ i ].points[ j ].value; + const formattedValue = this.props.formatValue( value ); + + return ( + + + + { this.props.showLabels ? + + { p.label } + : + + { p.label } + + } + + + + + { formattedValue } + + + + ); + })} + ))} ); }, + calculateState ({ height, labelHeight, labelOffset, lines, showLabels, valueHeight, valueOffset }) { + const offsetTop = valueOffset + valueHeight / 2; + const offsetBottom = showLabels ? labelOffset + labelHeight / 2 : 0; + const chartHeight = height - offsetTop - offsetBottom; + + const scale = ( chartHeight / 100 ) / ( Math.max( + ...lines.map( l => Math.max( ...l.points.map( p => p.value ))) + ) / 100 ); + + const relativeLines = lines.map( l => ({ + ...l, + points: l.points.map( p => ({ ...p, value: chartHeight - ( p.value * scale )})), + })); + + return { chartHeight, lines: relativeLines, offsetBottom, offsetTop, scale }; + }, + animateLines ( from, to ) { tween({ duration: this.props.duration, @@ -71,19 +210,6 @@ const LineChart = createClass({ to, next: lines => this.setState({ lines }), }); - }, - - relativeLines ( lines ) { - const absolutePercent = this.props.height / 100; - - const relativePercent = Math.max( - ...lines.map( l => Math.max( ...l.points.map( p => p.value ))) - ) / 100; - - return lines.map( l => ({ - ...l, - points: l.points.map( p => ({ ...p, value: this.props.height - ( p.value / relativePercent * absolutePercent )})), - })); } });