├── 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 | 
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 |
--------------------------------------------------------------------------------