├── src ├── index.js ├── constants │ └── propTypes.js └── components │ ├── BarChart │ ├── styles.js │ └── index.js │ ├── Bar │ ├── styles.js │ └── index.js │ └── Grid │ ├── styles.js │ ├── GraduationUnit │ ├── index.js │ └── styles.js │ └── index.js ├── .eslintrc ├── .gitignore ├── package.json └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | import Bar from './components/Bar'; 2 | import BarChart from './components/BarChart'; 3 | import Grid from './components/Grid'; 4 | 5 | export { 6 | Bar, 7 | BarChart, 8 | Grid, 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "classes": true, 6 | }, 7 | "rules": { 8 | "no-use-before-define": [2, "nofunc"], 9 | "id-length": 0, 10 | "max-len": 0, 11 | "arrow-body-style": 0, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/constants/propTypes.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | export const dataSetEntryPropType = PropTypes.shape({ 4 | value: PropTypes.number.isRequired, 5 | }); 6 | 7 | export const dataSetPropType = PropTypes.shape({ 8 | fillColor: PropTypes.string.isRequired, 9 | data: PropTypes.arrayOf(dataSetEntryPropType).isRequired, 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | Pods 25 | 26 | # Android/IJ 27 | # 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | -------------------------------------------------------------------------------- /src/components/BarChart/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import memoize from 'memoizee'; 3 | 4 | export default memoize(({ barSpacing, horizontal }) => { 5 | let barsFlexDirection; 6 | let barSpacingStyle; 7 | let barsSpacingStyle; 8 | 9 | if (horizontal) { 10 | barsFlexDirection = 'column'; 11 | barSpacingStyle = { marginVertical: barSpacing }; 12 | barsSpacingStyle = { paddingVertical: barSpacing }; 13 | } else { 14 | barsFlexDirection = 'row'; 15 | barSpacingStyle = { marginHorizontal: barSpacing }; 16 | barsSpacingStyle = { paddingHorizontal: barSpacing }; 17 | } 18 | 19 | return StyleSheet.create({ 20 | container: {}, 21 | 22 | grid: {}, 23 | 24 | bars: { 25 | flex: 1, 26 | flexDirection: barsFlexDirection, 27 | ...barsSpacingStyle, 28 | }, 29 | bar: { 30 | ...barSpacingStyle, 31 | }, 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-charts", 3 | "version": "3.0.0", 4 | "description": "Delightfully-animated data visualization.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "PrazAs Learning Inc", 10 | "license": "ISC", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/PrazAs/react-native-charts.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "native", 18 | "bar", 19 | "chart", 20 | "react-native", 21 | "react-component" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/PrazAs/react-native-charts/issues" 25 | }, 26 | "homepage": "https://github.com/PrazAs/react-native-charts#readme", 27 | "dependencies": { 28 | "memoizee": "^0.3.9", 29 | "underscore": "^1.8.3" 30 | }, 31 | "devDependencies": { 32 | "babel-eslint": "^5.0.0", 33 | "eslint": "^2.2.0", 34 | "eslint-config-airbnb": "^6.0.1", 35 | "eslint-plugin-react": "^4.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-charts 2 | Configurable, animated react-native charting library– (right now just bar charts). 3 | 4 | ![screen shot 2015-09-02 at 7 23 31 pm](https://cloud.githubusercontent.com/assets/1638987/9647197/8ec828e0-51a8-11e5-8257-35986fa76bf5.png) 5 | 6 | 7 | ### Example 8 | ```javascript 9 | import { BarChart } from 'react-native-charts' 10 | 11 | 40 | ``` 41 | 42 | ### TODO 43 | - [ ] Render labels for BarChart data 44 | - [ ] Other chart types including line graphs because they are awesome `¯\_(ツ)_/¯` 45 | 46 | Pull requests welcome! 47 | -------------------------------------------------------------------------------- /src/components/Bar/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import memoize from 'memoizee'; 3 | 4 | export default memoize(({ fillColor, horizontal, maxValue, value, valueScale }) => { 5 | const maximumFlex = maxValue - value; 6 | const valueFlex = maxValue - maximumFlex; 7 | 8 | let containerFlexDirection; 9 | let containerScale; 10 | let scaleDimension; 11 | 12 | if (horizontal) { 13 | containerFlexDirection = 'row'; 14 | containerScale = -1; 15 | scaleDimension = 'scaleX'; 16 | } else { 17 | containerFlexDirection = 'column'; 18 | containerScale = 1; 19 | scaleDimension = 'scaleY'; 20 | } 21 | 22 | return StyleSheet.create({ 23 | container: { 24 | backgroundColor: 'transparent', 25 | flex: 1, 26 | flexDirection: containerFlexDirection, 27 | transform: [ 28 | { [scaleDimension]: containerScale }, 29 | ], 30 | }, 31 | 32 | value: { 33 | backgroundColor: fillColor, 34 | flex: valueFlex, 35 | transform: [ 36 | { [scaleDimension]: valueScale }, 37 | ], 38 | }, 39 | maximum: { 40 | backgroundColor: 'transparent', 41 | flex: maximumFlex, 42 | }, 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/Grid/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import memoize from 'memoizee'; 3 | 4 | export default memoize(({ horizontal, labelWrapperFlex, unitFlex }) => { 5 | let contentContainerWrapperFlexDirection; 6 | let graduationUnitsFlexDirection; 7 | 8 | if (horizontal) { 9 | contentContainerWrapperFlexDirection = 'column'; 10 | graduationUnitsFlexDirection = 'row'; 11 | } else { 12 | contentContainerWrapperFlexDirection = 'row'; 13 | graduationUnitsFlexDirection = 'column'; 14 | } 15 | 16 | return StyleSheet.create({ 17 | container: { 18 | flex: 1, 19 | }, 20 | 21 | graduationUnits: { 22 | flex: 1, 23 | flexDirection: graduationUnitsFlexDirection, 24 | }, 25 | 26 | contentContainerWrapper: { 27 | backgroundColor: 'transparent', 28 | position: 'absolute', 29 | top: 0, 30 | right: 0, 31 | bottom: 0, 32 | left: 0, 33 | 34 | flexDirection: contentContainerWrapperFlexDirection, 35 | }, 36 | contentContainerLabelWrapperOffset: { 37 | backgroundColor: 'transparent', 38 | flex: labelWrapperFlex, 39 | }, 40 | contentContainer: { 41 | backgroundColor: 'transparent', 42 | flex: unitFlex, 43 | }, 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Grid/GraduationUnit/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import styles from './styles'; 4 | 5 | export default class GraduationUnit extends Component { 6 | static propTypes = { 7 | completeBorder: PropTypes.bool, 8 | horizontal: PropTypes.bool.isRequired, 9 | labelColor: PropTypes.string, 10 | labelWrapperFlex: PropTypes.number, 11 | lineColor: PropTypes.string, 12 | style: View.propTypes.style, 13 | unitFlex: PropTypes.number, 14 | value: PropTypes.number, 15 | }; 16 | 17 | static defaultProps = { 18 | horizontal: false, 19 | }; 20 | 21 | getStyles() { 22 | const { 23 | completeBorder, 24 | horizontal, 25 | labelColor, 26 | labelWrapperFlex, 27 | lineColor, 28 | unitFlex, 29 | } = this.props; 30 | 31 | return styles({ 32 | completeBorder, 33 | horizontal, 34 | labelColor, 35 | labelWrapperFlex, 36 | lineColor, 37 | unitFlex, 38 | }); 39 | } 40 | 41 | renderLabel() { 42 | const { 43 | value, 44 | } = this.props; 45 | 46 | return ( 47 | 48 | 49 | {value} 50 | 51 | 52 | ); 53 | } 54 | 55 | render() { 56 | const { 57 | horizontal, 58 | style, 59 | } = this.props; 60 | 61 | return ( 62 | 63 | {horizontal ? null : this.renderLabel()} 64 | 65 | {horizontal ? this.renderLabel() : null} 66 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Grid/GraduationUnit/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import memoize from 'memoizee'; 3 | 4 | // (Configuration constants) 5 | const DEFAULT_LABEL_COLOR = '#ccc'; 6 | const DEFAULT_LABEL_WRAPPER_FLEX = 1; 7 | const DEFAULT_LINE_COLOR = '#dad9d4'; 8 | const DEFAULT_UNIT_FLEX = 12; 9 | const LABEL_FONT_SIZE = 12; 10 | 11 | export default memoize(({ completeBorder, horizontal, labelColor, labelWrapperFlex, lineColor, unitFlex }) => { 12 | const unitBorderStyles = { 13 | borderColor: lineColor || DEFAULT_LINE_COLOR, 14 | borderWidth: 1, 15 | }; 16 | let containerFlexDirection; 17 | let labelTransformation; 18 | let labelMarginSide; 19 | 20 | if (horizontal) { 21 | containerFlexDirection = 'column'; 22 | labelTransformation = { translateX: 4 }; 23 | labelMarginSide = 'Top'; 24 | unitBorderStyles.borderRightWidth = !completeBorder 25 | ? 0 26 | : unitBorderStyles.borderWidth; 27 | } else { 28 | containerFlexDirection = 'row'; 29 | labelTransformation = { translateY: -(1 + LABEL_FONT_SIZE / 2) }; 30 | labelMarginSide = 'Right'; 31 | unitBorderStyles.borderTopWidth = !completeBorder 32 | ? 0 33 | : unitBorderStyles.borderWidth; 34 | } 35 | 36 | return StyleSheet.create({ 37 | container: { 38 | backgroundColor: 'transparent', 39 | flex: 1, 40 | flexDirection: containerFlexDirection, 41 | }, 42 | 43 | labelWrapper: { 44 | flex: labelWrapperFlex || DEFAULT_LABEL_WRAPPER_FLEX, 45 | }, 46 | label: { 47 | color: labelColor || DEFAULT_LABEL_COLOR, 48 | fontFamily: 'Avenir', 49 | fontSize: 12, 50 | [`margin${labelMarginSide}`]: (LABEL_FONT_SIZE / 2), 51 | textAlign: 'right', 52 | transform: [ 53 | labelTransformation, 54 | ], 55 | }, 56 | 57 | unit: { 58 | ...unitBorderStyles, 59 | flex: unitFlex || DEFAULT_UNIT_FLEX, 60 | }, 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/Bar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Animated, View } from 'react-native'; 3 | import styles from './styles'; 4 | 5 | export default class Bar extends Component { 6 | static propTypes = { 7 | destinationValueScale: PropTypes.number.isRequired, 8 | fillColor: PropTypes.string.isRequired, 9 | horizontal: PropTypes.bool, 10 | initialValueScale: PropTypes.number.isRequired, 11 | maxValue: PropTypes.number.isRequired, 12 | style: View.propTypes.style, 13 | value: PropTypes.number.isRequired, 14 | valueScaleSpringFriction: PropTypes.number.isRequired, 15 | }; 16 | 17 | static defaultProps = { 18 | destinationValueScale: 1, 19 | fillColor: '#00b5ec', 20 | initialValueScale: 0, 21 | valueScaleSpringFriction: 5, 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.state = { 28 | valueScale: new Animated.Value(props.initialValueScale), 29 | }; 30 | } 31 | 32 | componentDidMount() { 33 | this.animateValueScale(); 34 | } 35 | 36 | componentWillUpdate(nextProps) { 37 | const maxValueWillChange = nextProps.maxValue !== this.props.maxValue; 38 | const valueWillChange = nextProps.value !== this.props.value; 39 | const shouldAnimate = maxValueWillChange || valueWillChange; 40 | 41 | // Only animate value scale if relative value changes 42 | if (shouldAnimate) { 43 | this.animateValueScale(); 44 | } 45 | } 46 | 47 | getStyles() { 48 | const { 49 | fillColor, 50 | horizontal, 51 | maxValue, 52 | value, 53 | } = this.props; 54 | 55 | return styles({ 56 | fillColor, 57 | horizontal, 58 | maxValue, 59 | value, 60 | valueScale: this.state.valueScale, 61 | }); 62 | } 63 | 64 | animateValueScale() { 65 | const { 66 | destinationValueScale, 67 | initialValueScale, 68 | valueScaleSpringFriction, 69 | } = this.props; 70 | 71 | // Reset value scale 72 | this.state.valueScale.setValue(initialValueScale); 73 | 74 | // Apply spring animation to value scale to its destination value 75 | Animated.spring(this.state.valueScale, { 76 | friction: valueScaleSpringFriction, 77 | toValue: destinationValueScale, 78 | }).start(); 79 | } 80 | 81 | render() { 82 | const { 83 | style, 84 | } = this.props; 85 | 86 | return ( 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/BarChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { View } from 'react-native'; 3 | import { flatten, max, pluck, zip } from 'underscore'; 4 | import { dataSetPropType } from '../../constants/propTypes'; 5 | import Bar from '../Bar'; 6 | import Grid from '../Grid'; 7 | import styles from './styles'; 8 | 9 | export default class BarChart extends Component { 10 | static propTypes = { 11 | barSize: PropTypes.number, 12 | barSpacing: PropTypes.number, 13 | barStyle: View.propTypes.style, 14 | dataSets: PropTypes.arrayOf(dataSetPropType).isRequired, 15 | graduation: PropTypes.number, 16 | horizontal: PropTypes.bool, 17 | showGrid: PropTypes.bool.isRequired, 18 | style: View.propTypes.style, 19 | }; 20 | 21 | static defaultProps = { 22 | showGrid: true, 23 | }; 24 | 25 | getStyles() { 26 | const { 27 | barSize, 28 | barSpacing, 29 | horizontal, 30 | } = this.props; 31 | 32 | return styles({ 33 | barSize, 34 | barSpacing, 35 | horizontal, 36 | }); 37 | } 38 | 39 | getDataSetsMaxValue() { 40 | const { 41 | dataSets, 42 | } = this.props; 43 | 44 | const dataSetsData = flatten(pluck(dataSets, 'data')); 45 | const dataSetsValues = pluck(dataSetsData, 'value'); 46 | const dataSetsMaxValue = max(dataSetsValues); 47 | 48 | return dataSetsMaxValue; 49 | } 50 | 51 | getGraduation() { 52 | const dataSetsMaxValue = this.getDataSetsMaxValue(); 53 | const calculatedGraduation = Math.ceil(Math.sqrt(dataSetsMaxValue)); 54 | 55 | return this.props.graduation || calculatedGraduation; 56 | } 57 | 58 | getGridMaxValue() { 59 | const dataSetsMaxValue = this.getDataSetsMaxValue(); 60 | const graduation = this.getGraduation(); 61 | const gridMaxValue = Math.ceil(dataSetsMaxValue / graduation) * graduation; 62 | 63 | return gridMaxValue; 64 | } 65 | 66 | renderGrid(children) { 67 | const { 68 | horizontal, 69 | } = this.props; 70 | 71 | const gridMaxValue = this.getGridMaxValue(); 72 | const graduation = this.getGraduation(); 73 | 74 | return ( 75 | 82 | ); 83 | } 84 | 85 | renderBars() { 86 | const { 87 | barStyle, 88 | dataSets, 89 | horizontal, 90 | } = this.props; 91 | 92 | // TODO: Margin/pad datasets... 93 | console.log('TODO: Margin/pad datasets...'); 94 | const gridMaxValue = this.getGridMaxValue(); 95 | const dataSetsBars = dataSets.map(dataSet => { 96 | return dataSet.data.map((data, index) => { 97 | return ( 98 | 106 | ); 107 | }); 108 | }); 109 | const bars = flatten(zip(...dataSetsBars)); 110 | 111 | return ( 112 | 113 | {bars} 114 | 115 | ); 116 | } 117 | 118 | render() { 119 | const { 120 | showGrid, 121 | style, 122 | } = this.props; 123 | 124 | const bars = this.renderBars(); 125 | const chart = showGrid 126 | ? this.renderGrid(bars) 127 | : bars; 128 | 129 | return ( 130 | 131 | {chart} 132 | 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Grid/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { View } from 'react-native'; 3 | import GraduationUnit from './GraduationUnit'; 4 | import styles from './styles'; 5 | 6 | export default class Grid extends Component { 7 | static propTypes = { 8 | content: PropTypes.any.isRequired, 9 | contentContainerStyle: View.propTypes.style, 10 | graduation: PropTypes.number.isRequired, 11 | horizontal: PropTypes.bool.isRequired, 12 | labelWrapperFlex: PropTypes.number.isRequired, 13 | lineColor: PropTypes.string, 14 | maxValue: PropTypes.number.isRequired, 15 | style: View.propTypes.style, 16 | unitFlex: PropTypes.number.isRequired, 17 | }; 18 | 19 | static defaultProps = { 20 | graduation: 1, 21 | labelWrapperFlex: 1, 22 | maxValue: 10, 23 | unitFlex: 12, 24 | }; 25 | 26 | getStyles() { 27 | const { 28 | horizontal, 29 | labelWrapperFlex, 30 | unitFlex, 31 | } = this.props; 32 | 33 | return styles({ 34 | horizontal, 35 | labelWrapperFlex, 36 | unitFlex, 37 | }); 38 | } 39 | 40 | getOrderedGraduationUnitValues() { 41 | const { 42 | horizontal, 43 | graduation, 44 | maxValue, 45 | } = this.props; 46 | 47 | const graduationUnitsCount = graduation > 0 48 | ? Math.ceil(maxValue / graduation) 49 | : 1; 50 | 51 | // Generate an iterable array of length `graduationUnitsCount` and map it 52 | // to graduation unit values... 53 | const graduationUnitValues = Array.apply(null, Array(graduationUnitsCount)) 54 | .map((value, index) => { 55 | return graduation * (index + 1); 56 | }); 57 | 58 | return !horizontal 59 | ? graduationUnitValues.reverse() 60 | : graduationUnitValues; 61 | } 62 | 63 | renderGraduationUnits() { 64 | const { 65 | horizontal, 66 | labelWrapperFlex, 67 | lineColor, 68 | unitFlex, 69 | } = this.props; 70 | 71 | const orderedGraduationUnitValues = this.getOrderedGraduationUnitValues(); 72 | const lastGraduationUnitIndex = orderedGraduationUnitValues.length - 1; 73 | 74 | return ( 75 | 76 | {this.getOrderedGraduationUnitValues().map((value, index) => ( 77 | = lastGraduationUnitIndex 81 | : index === 0)} 82 | horizontal={horizontal} 83 | labelWrapperFlex={labelWrapperFlex} 84 | lineColor={lineColor} 85 | unitFlex={unitFlex} 86 | value={value} 87 | /> 88 | ))} 89 | 90 | ); 91 | } 92 | 93 | renderContentContainer() { 94 | const { 95 | content, 96 | contentContainerStyle, 97 | horizontal, 98 | } = this.props; 99 | 100 | return ( 101 | 102 | {horizontal ? null : } 103 | 104 | {content} 105 | 106 | {horizontal ? : null} 107 | 108 | ); 109 | } 110 | 111 | render() { 112 | const { style } = this.props; 113 | 114 | // TODO: Add container for data set data (bar group) labels... 115 | console.log('TODO: Add container for data set data (bar group) labels...'); 116 | // - Should render with respect to bar locations so to visually align them 117 | return ( 118 | 119 | {this.renderGraduationUnits()} 120 | {this.renderContentContainer()} 121 | 122 | ); 123 | } 124 | } 125 | --------------------------------------------------------------------------------