├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── LICENSE.d3
├── README.md
├── ci
└── deploy
├── demo
├── .babelrc
├── css
│ └── main.scss
├── index.html
└── js
│ ├── Demo.js
│ └── app.js
├── index.js
├── package-lock.json
├── package.json
├── src
├── AreaChart.js
├── Axis.js
├── Bar.js
├── BarChart.js
├── Chart.js
├── DonutChart.js
├── Legend.js
├── LineChart.js
├── NodeChart.js
├── Path.js
├── PieChart.js
├── SparklineChart.js
├── Tooltip.js
└── helpers.js
├── test
├── AreaChart-test.js
├── Axis-test.js
├── Bar-test.js
├── BarChart-test.js
├── Chart-test.js
├── DonutChart-test.js
├── Legend-test.js
├── LineChart-test.js
├── NodeChart-test.js
├── Path-test.js
├── PieChart-test.js
├── SparklineChart-test.js
└── Tooltip-test.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "syntax-class-properties",
5 | "transform-class-properties"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": [
4 | "react"
5 | ],
6 | "env": {
7 | "browser": true,
8 | "node": true,
9 | "es6": true
10 | },
11 | "globals": {
12 | "__DEV__": true,
13 | "__SERVER__": true
14 | },
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "rules": {
19 | // Strict mode
20 | "strict": [2, "never"],
21 |
22 | // Code style
23 | "indent": [2, 2, {"SwitchCase": 1}],
24 | "quotes": [2, "single"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 |
7 | # Runtime data
8 | pids
9 | *.pid
10 | *.seed
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # transpiled source code
19 | lib
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Compiled binary addons (http://nodejs.org/api/addons.html)
28 | build/Release
29 |
30 | # Dependency directory
31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
32 | node_modules
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # es6 source code
2 | src
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Jive Software
4 |
5 | Portions Copyright (c) 2015 Neri Marschik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/LICENSE.d3:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2015, Michael Bostock
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * The name Michael Bostock may not be used to endorse or promote products
15 | derived from this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-d3-charts
2 | A D3 charting library for React
3 |
4 | * Line charts, Bar charts, Sparkline Charts, Donut Charts, Pie Charts, Node Charts, and Area Charts (more are coming soon)
5 | * ES6 support
6 | * Grid lines
7 | * Legends
8 | * Tooltips
9 | * Tests!
10 | * Demo app
11 |
12 | ## installation
13 |
14 | ```shell
15 | $ npm install react-d3-charts
16 | ```
17 |
18 | ## usage
19 |
20 | Check out the demo
21 | ```shell
22 | $ npm run demo
23 | $ open http://localhost:3000
24 | ```
25 |
26 | There also tests that you should probably look at.
27 |
28 | ## code layout
29 | The es6 source code is in the /src directory.
30 |
31 | The published code is transpiled from /src to the /lib directory by running the build task (see below).
32 |
33 | The build task is automatically called when the module is published (see package.json -> scripts -> prepublish).
34 |
35 | The root index.js imports code from the /lib directory allowing end-users to seamlessly use the module without requiring babel or some other transpiler.
36 |
37 | ## tasks
38 |
39 | run the tests
40 | ```shell
41 | $ npm test
42 | ```
43 |
44 | generate test coverage report
45 | ```shell
46 | $ npm test --coverage
47 | ```
48 |
49 | build the module for distribution
50 | ```shell
51 | $ npm run build
52 | ```
53 |
54 | ## resources
55 | * This started out initially as a fork of [react-d3-components](https://github.com/codesuki/react-d3-components)
56 | * Color sorting provided by [sc-color](https://www.npmjs.com/package/sc-color)
57 |
--------------------------------------------------------------------------------
/ci/deploy:
--------------------------------------------------------------------------------
1 | #!/bin/sh -eux
2 | npm i
3 | npm run build
4 | npm publish
--------------------------------------------------------------------------------
/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "syntax-class-properties",
5 | "transform-class-properties"
6 | ]
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/demo/css/main.scss:
--------------------------------------------------------------------------------
1 | @import "~susy/sass/susy";
2 | $susy: (
3 | columns: 12,
4 | gutters: 1/4,
5 | gutter-position: 'after',
6 | global-box-sizing: border-box,
7 | /*
8 | debug: (
9 | image: show,
10 | color: rgba(#66b3dc, 0.25),
11 | output: background,
12 | toggle: top right
13 | )
14 | */
15 | );
16 |
17 | $container-style: 'fluid';
18 | $body-background-color: #fff;
19 |
20 | $chart-background-color: #fff;
21 | $chart-min-height: 320px;
22 | $spark-chart-min-height: 200px;
23 | $chart-heading-font-size: 18px;
24 | $chart-axis-line-color: rgba(170,170,170,0.5);
25 |
26 | $tab-background-color: #e5e5e5;
27 |
28 | *,
29 | *:before,
30 | *:after {
31 | -webkit-box-sizing: border-box;
32 | -moz-box-sizing: border-box;
33 | box-sizing: border-box;
34 | }
35 |
36 | body {
37 | background-color: $body-background-color;
38 | margin: 30px 0;
39 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
40 | font-weight: 300;
41 | }
42 |
43 | .tabs-navigation {
44 | padding: 0 50px;
45 | }
46 |
47 | .tabs-menu {
48 | display: table;
49 | list-style: none;
50 | padding: 0;
51 | margin: 0;
52 | }
53 |
54 | .tabs-menu-item {
55 | float: left;
56 | margin-right: 20px;
57 | }
58 |
59 | .tabs-menu-item a {
60 | cursor: pointer;
61 | display: block;
62 | padding: 0 10px;
63 | height: 50px;
64 | line-height: 50px;
65 | border-bottom: 0;
66 | color: #a9a9a9;
67 | }
68 |
69 | .tabs-menu-item:not(.is-active) a:hover,
70 | .tabs-menu-item.is-active a {
71 | background: $tab-background-color;
72 | border: 1px solid #ddd;
73 | border-top: 2px solid #3498db;
74 | border-bottom: 0;
75 | color: #333;
76 | }
77 |
78 | .tab-panel {
79 | padding: 50px 0;
80 | background-color: $tab-background-color;
81 | box-shadow: 0 1px 3px rgba(0,0,0,.25);
82 | overflow: auto;
83 | }
84 |
85 | div.charts {
86 | @include container(90%);
87 | }
88 |
89 | section.chart {
90 | box-shadow: 0 1px 3px rgba(0,0,0,.25);
91 | border-radius: 5px;
92 | padding: 10px;
93 | overflow: hidden;
94 | margin-bottom: 30px;
95 | h1 {
96 | font-size: $chart-heading-font-size;
97 | font-weight: 300;
98 | text-align: center;
99 | }
100 | &.last {
101 | margin-right: 0;
102 | }
103 |
104 | svg {
105 | margin: 0 auto;
106 | display: block;
107 | .axis {
108 | .tick {
109 | line {
110 | stroke: $chart-axis-line-color;
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
117 | section.chart {
118 | min-height: $chart-min-height;
119 |
120 | background-color: $chart-background-color;
121 |
122 | &.spark {
123 | min-height: $spark-chart-min-height;
124 | svg {
125 | box-shadow: 0 1px 3px rgba(0,0,0,.25);
126 | }
127 | }
128 | }
129 |
130 | div.tip {
131 | dl {
132 | min-width:10em;
133 | box-shadow: 0 1px 3px rgba(0,0,0,.25);
134 | border-radius: 5px;
135 | font-size: smaller;
136 | background-color: #fff;
137 | border: 3px double #ccc;
138 | padding: 0.5em;
139 | }
140 | dt {
141 | float: left;
142 | clear: left;
143 | text-align: left;
144 | font-weight: bold;
145 | }
146 | dt:after {
147 | content: ":";
148 | }
149 | dd {
150 | /*margin: 0 0 0 110px;*/
151 | /*padding: 0 0 0.5em 0;*/
152 | }
153 | }
154 |
155 | @media (min-width: 768px){
156 | section.chart{
157 | @include span(6 of 12);
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/js/Demo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tabs from "react-simpletabs";
3 | import BarChart from '../../src/BarChart.js';
4 | import axios from 'axios';
5 |
6 | class Demo extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 |
12 | barDataCPU_Utilization :[{
13 | label: 'cpu-utilization',
14 | values: [{x: '1', y: 0}]
15 | }],
16 | barDataCPU_Interupts:[{
17 | label: 'cpu-interupts',
18 | values: [{x: '1', y: 0}]
19 | }],
20 | barDataMem_Percent :[{
21 | label: 'mem',
22 | values: [{x: '1', y: 0}]
23 | }]
24 | }
25 | }
26 |
27 | fetchCpuData() {
28 |
29 | var _this = this;
30 | axios.get('http://34.239.233.186:61208/api/3/cpu')
31 | .then(function (response) {
32 | console.log('CPU Response : ',response.data);
33 | if(localStorage.getItem('cpu-records')){
34 | var arr = JSON.parse(localStorage.getItem('cpu-records'));
35 | arr.push(response.data);
36 | localStorage.setItem('cpu-records',JSON.stringify(arr));
37 | }else{
38 | var arr = [];
39 | arr.push(response.data);
40 | localStorage.setItem('cpu-records',JSON.stringify(arr));
41 | }
42 | var cpu_records = JSON.parse(localStorage.getItem('cpu-records'));
43 | _this.setState(state=>{
44 | console.log('All cpu records : ',cpu_records);
45 | state.barDataCPU_Utilization[0].values = [];
46 | state.barDataCPU_Interupts[0].values = [];
47 | for(var i=0;i{
81 | console.log('All MemRecords:',mem_records);
82 | state.barDataMem_Percent[0].values = [];
83 |
84 | for(var i=0;i
117 |
118 | - x
119 | - { x }
120 | - y0
121 | - {y0}
122 | - total
123 | - {total}
124 | - dataLabel
125 | - {dataLabel}
126 |
127 | );
128 | };
129 |
130 | const toolTipOffset = {top: 10, left: 10};
131 |
132 | return (
133 |
134 |
135 |
136 |
137 | CPU Chart - Utilization
138 |
146 | Child
147 |
148 |
149 |
150 | CPU Chart - Interupts
151 |
159 | Child
160 |
161 |
162 |
163 | Memory Chart - Percent
164 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | );
185 | }
186 | }
187 |
188 | export default Demo;
--------------------------------------------------------------------------------
/demo/js/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import '../css/main.scss';
5 | import Demo from './Demo';
6 |
7 | ReactDOM.render(
8 |
9 | ,
10 | document.getElementById('demo')
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | BarChart: require('./lib/BarChart').default,
3 | LineChart: require('./lib/LineChart').default,
4 | SparklineChart: require('./lib/SparklineChart').default,
5 | AreaChart: require('./lib/AreaChart').default,
6 | DonutChart: require('./lib/DonutChart').default,
7 | NodeChart: require('./lib/NodeChart').default
8 | };
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-d3-charts",
3 | "version": "1.0.47",
4 | "description": "A D3 charting library for React",
5 | "main": "index.js",
6 | "scripts": {
7 | "clean": "rm -rf ./lib/*",
8 | "build": "npm run clean && ./node_modules/.bin/babel src --out-dir lib/",
9 | "demo": "webpack-dev-server --content-base demo --hot --progress --colors --port 3000",
10 | "prepublish": "npm run build",
11 | "start": "npm run demo",
12 | "test": "babel-node ./node_modules/.bin/babel-istanbul test _mocha -- --recursive 'test/**/*.js'"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/jivesoftware/react-d3-charts.git"
17 | },
18 | "publishConfig": {
19 | "registry": "http://nexus-int.eng.jiveland.com/content/repositories/npm-internal"
20 | },
21 | "keywords": [
22 | "react",
23 | "d3",
24 | "charts"
25 | ],
26 | "author": "Matthew Page",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/jivesoftware/react-d3-charts/issues"
30 | },
31 | "homepage": "https://github.com/jivesoftware/react-d3-charts#readme",
32 | "devDependencies": {
33 | "babel-cli": "^6.26.0",
34 | "babel-core": "^6.26.3",
35 | "babel-eslint": "^4.1.6",
36 | "babel-istanbul": "^0.12.2",
37 | "babel-loader": "^6.2.1",
38 | "babel-plugin-syntax-class-properties": "^6.3.13",
39 | "babel-plugin-transform-class-properties": "^6.24.1",
40 | "babel-preset-es2015": "^6.24.1",
41 | "babel-preset-react": "^6.3.13",
42 | "chai": "^3.4.1",
43 | "css-loader": "^0.23.1",
44 | "enzyme": "^2.4.1",
45 | "eslint-plugin-react": "^3.15.0",
46 | "jsdom": "^7.2.2",
47 | "mocha": "^2.3.4",
48 | "node-sass": "^4.9.3",
49 | "react-addons-test-utils": "^15.4.1",
50 | "react-hot-loader": "^1.3.0",
51 | "react-simpletabs": "^0.7.0",
52 | "sass-loader": "^7.0.3",
53 | "sinon": "^1.17.2",
54 | "style-loader": "^0.13.0",
55 | "susy": "^2.2.12",
56 | "webpack": "^1.13.2",
57 | "webpack-dev-server": "^1.11.0"
58 | },
59 | "dependencies": {
60 | "axios": "^0.18.0",
61 | "d3": "^3.5.16",
62 | "lodash": "^4.17.10",
63 | "react": "^15.4.1",
64 | "react-dom": "^15.4.1",
65 | "sc-color": "^0.4.0",
66 | "shortid": "^2.2.6"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/AreaChart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import d3 from 'd3/d3.min.js';
4 | import Chart from './Chart';
5 | import Axis from './Axis';
6 | import Path from './Path';
7 | import Tooltip from './Tooltip';
8 | import * as helpers from './helpers.js';
9 |
10 | class AreaChart extends Component {
11 |
12 | static propTypes = {
13 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
14 | className: PropTypes.string,
15 | colorScale: PropTypes.func,
16 | data: PropTypes.oneOfType([
17 | PropTypes.object,
18 | PropTypes.array
19 | ]).isRequired,
20 | height: PropTypes.number.isRequired,
21 | interpolate: PropTypes.string,
22 | label: PropTypes.func,
23 | legend: PropTypes.object,
24 | margin: PropTypes.shape({
25 | top: PropTypes.number,
26 | bottom: PropTypes.number,
27 | left: PropTypes.number,
28 | right: PropTypes.number
29 | }),
30 | stroke: PropTypes.func,
31 | tooltipHtml: PropTypes.func,
32 | tooltipMode: PropTypes.oneOf(['mouse', 'element', 'fixed']),
33 | tooltipClassName: PropTypes.string,
34 | tooltipContained: PropTypes.bool,
35 | tooltipOffset: PropTypes.objectOf(PropTypes.number),
36 | values: PropTypes.func,
37 | width: PropTypes.number.isRequired,
38 | xAxis: PropTypes.object,
39 | yAxis: PropTypes.object,
40 | x: PropTypes.func,
41 | y: PropTypes.func,
42 | y0: PropTypes.func
43 | };
44 |
45 | static defaultProps = {
46 | className: 'chart',
47 | colorScale: d3.scale.category20(),
48 | data: {label: 'No data available', values: [{x: 'No data available', y: 1}]},
49 | interpolate: 'linear',
50 | label: stack => { return stack.label; },
51 | margin: {top: 0, bottom: 0, left: 0, right: 0},
52 | stroke: d3.scale.category20(),
53 | tooltipMode: 'mouse',
54 | tooltipOffset: {top: -35, left: 0},
55 | tooltipClassName: null,
56 | tooltipHtml: null,
57 | tooltipContained: false,
58 | values: stack => { return stack.values; },
59 | x: e => { return e.x; },
60 | y: e => { return e.y; },
61 | y0: () => { return 0; }
62 | };
63 |
64 | constructor(props) {
65 | super(props);
66 | this.state = {
67 | tooltip: {
68 | hidden: true
69 | }
70 | };
71 | }
72 |
73 | componentDidMount() {
74 | this._svg_node = ReactDOM.findDOMNode(this).getElementsByTagName('svg')[0];
75 | }
76 |
77 | componentWillMount() {
78 | helpers.calculateInner(this, this.props);
79 | helpers.arrayify(this, this.props);
80 | helpers.makeScales(this, this.props);
81 | helpers.addTooltipMouseHandlers(this);
82 | }
83 |
84 | componentWillReceiveProps(nextProps) {
85 | helpers.calculateInner(this, nextProps);
86 | helpers.arrayify(this, nextProps);
87 | helpers.makeScales(this, nextProps);
88 | }
89 |
90 | _tooltipHtml(d, position) {
91 | const {x, y0, y, values } = this.props;
92 | const [xScale, yScale] = [this._xScale, this._yScale];
93 |
94 | const xValueCursor = xScale.invert(position[0]);
95 |
96 | const xBisector = d3.bisector(e => { return x(e); }).right;
97 | let xIndex = xBisector(values(d[0]), xScale.invert(position[0]));
98 | xIndex = (xIndex === values(d[0]).length) ? xIndex - 1: xIndex;
99 |
100 | const xIndexRight = xIndex === values(d[0]).length ? xIndex - 1: xIndex;
101 | const xValueRight = x(values(d[0])[xIndexRight]);
102 |
103 | const xIndexLeft = xIndex === 0 ? xIndex : xIndex - 1;
104 | const xValueLeft = x(values(d[0])[xIndexLeft]);
105 |
106 | if (Math.abs(xValueCursor - xValueRight) < Math.abs(xValueCursor - xValueLeft)) {
107 | xIndex = xIndexRight;
108 | } else {
109 | xIndex = xIndexLeft;
110 | }
111 |
112 | const yValueCursor = yScale.invert(position[1]);
113 |
114 | const yBisector = d3.bisector(e => { return y0(values(e)[xIndex]) + y(values(e)[xIndex]); }).left;
115 | let yIndex = yBisector(d, yValueCursor);
116 | yIndex = (yIndex === d.length) ? yIndex - 1: yIndex;
117 |
118 | const yValue = y(values(d[yIndex])[xIndex]);
119 | const yValueCumulative = y0(values(d[d.length - 1])[xIndex]) + y(values(d[d.length - 1])[xIndex]);
120 |
121 | const xValue = x(values(d[yIndex])[xIndex]);
122 |
123 | const xPos = xScale(xValue);
124 | const yPos = yScale(y0(values(d[yIndex])[xIndex]) + yValue);
125 |
126 | return [this.props.tooltipHtml(yValue, yValueCumulative, xValue), xPos, yPos];
127 | }
128 |
129 | render() {
130 | const {
131 | height,
132 | legend,
133 | width,
134 | margin,
135 | colorScale,
136 | interpolate,
137 | stroke,
138 | values,
139 | label,
140 | x,
141 | y,
142 | y0,
143 | xAxis,
144 | yAxis
145 | } = this.props;
146 |
147 | const [
148 | data,
149 | innerWidth,
150 | innerHeight,
151 | xScale,
152 | yScale,
153 | //xIntercept,
154 | //yIntercept
155 | ] = [
156 | this._data,
157 | this._innerWidth,
158 | this._innerHeight,
159 | this._xScale,
160 | this._yScale,
161 | //this._xIntercept,
162 | //this._yIntercept
163 | ];
164 |
165 | const line = d3.svg.line()
166 | .x(function(e) { return xScale(x(e)); })
167 | .y(function(e) { return yScale(y0(e) + y(e)); })
168 | .interpolate(interpolate);
169 |
170 | const area = d3.svg.area()
171 | .x(function(e) { return xScale(x(e)); })
172 | .y0(function(e) { return yScale(yScale.domain()[0] + y0(e)); })
173 | .y1(function(e) { return yScale(y0(e) + y(e)); })
174 | .interpolate(interpolate);
175 |
176 | const areas = data.map((stack, index) => {
177 | return (
178 |
187 | );
188 | });
189 |
190 | data.map(stack => {
191 | return (
192 | );
197 | });
198 |
199 | return (
200 |
201 |
202 | {areas}
203 |
204 |
205 | { this.props.children }
206 |
207 |
208 |
209 | );
210 | }
211 |
212 | }
213 |
214 | export default AreaChart;
215 |
--------------------------------------------------------------------------------
/src/Axis.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import _ from 'lodash';
3 |
4 | class Axis extends Component {
5 |
6 | static propTypes = {
7 | height: PropTypes.number,
8 | width: PropTypes.number,
9 | tickArguments: PropTypes.array,
10 | tickValues: PropTypes.array,
11 | tickFormat: PropTypes.func, //format tick label text
12 | tickFilter: PropTypes.func, //filter ticks before elements are created
13 | innerTickSize: PropTypes.number,
14 | tickPadding: PropTypes.number,
15 | outerTickSize: PropTypes.number,
16 | scale: PropTypes.func.isRequired,
17 | className: PropTypes.string,
18 | zero: PropTypes.number,
19 | orientation: PropTypes.oneOf(['top', 'bottom', 'left', 'right']).isRequired,
20 | label: PropTypes.string,
21 | staggerLabels: PropTypes.bool,
22 | gridLines: PropTypes.bool,
23 | visible: PropTypes.bool,
24 | };
25 |
26 | static defaultProps = {
27 | tickArguments: [10],
28 | tickValues: null,
29 | tickFormat: null,
30 | innerTickSize: 6,
31 | tickPadding: 3,
32 | outerTickSize: 6,
33 | className: 'axis',
34 | zero: 0,
35 | label: '',
36 | staggerLabels: false,
37 | gridLines: false,
38 | visible: true
39 | };
40 |
41 | _getTranslateString() {
42 | const {
43 | orientation,
44 | height,
45 | width,
46 | zero
47 | } = this.props;
48 |
49 | if (orientation === 'top') {
50 | return `translate(0, ${zero})`;
51 | }
52 | if (orientation === 'bottom') {
53 | return `translate(0, ${zero === 0 ? height : zero})`;
54 | }
55 | if (orientation === 'left') {
56 | return `translate(${zero}, 0)`;
57 | }
58 | if (orientation === 'right') {
59 | return `translate(${zero === 0 ? width : zero}, 0)`;
60 | }
61 | return '';
62 | }
63 |
64 | render() {
65 | const {
66 | height,
67 | width,
68 | tickArguments,
69 | tickValues,
70 | tickFormat,
71 | innerTickSize,
72 | tickPadding,
73 | outerTickSize,
74 | scale,
75 | orientation,
76 | className,
77 | zero,
78 | label,
79 | staggerLabels,
80 | gridLines,
81 | visible
82 | } = this.props;
83 |
84 | if (!visible) {
85 | return ;
86 | }
87 |
88 | let ticks;
89 |
90 | if (!tickValues){
91 | ticks = scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain();
92 | } else {
93 | ticks = tickValues;
94 | }
95 |
96 | let tickFormatter = tickFormat;
97 |
98 | if (!tickFormatter) {
99 | if (scale.tickFormat) {
100 | tickFormatter = scale.tickFormat.apply(scale, tickArguments);
101 | } else {
102 | tickFormatter = x => { return x; };
103 | }
104 | }
105 |
106 | // TODO: is there a cleaner way? removes the 0 tick if axes are crossing
107 | if (zero !== height && zero !== width && zero !== 0) {
108 | ticks = ticks.filter((element) => { return element === 0 ? false : true;});
109 | }
110 |
111 | const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;
112 |
113 | const sign = orientation === 'top' || orientation === 'left' ? -1 : 1;
114 |
115 | const range = this._d3ScaleRange(scale);
116 |
117 | const activeScale = scale.rangeBand ? e => { return scale(e) + scale.rangeBand() / 2; } : scale;
118 |
119 | let transform, x, y, x2, y2, dy, textAnchor, d, labelElement;
120 | if (orientation === 'bottom' || orientation === 'top') {
121 | transform = 'translate({}, 0)';
122 | x = 0;
123 | y = sign * tickSpacing;
124 | x2 = 0;
125 | y2 = gridLines ? (-1 * height) : (sign * innerTickSize);
126 | dy = sign < 0 ? '0em' : '.71em';
127 | textAnchor = 'middle';
128 | d = `M${range[0]}, ${sign * outerTickSize}V0H${range[1]}V${sign * outerTickSize}`;
129 |
130 | labelElement = {label};
131 | } else {
132 | transform = 'translate(0, {})';
133 | x = sign * tickSpacing;
134 | y = 0;
135 | x2 = gridLines ? width : (sign * innerTickSize);
136 | y2 = 0;
137 | dy = '.32em';
138 | textAnchor = sign < 0 ? 'end' : 'start';
139 | d = `M${sign * outerTickSize}, ${range[0]}H0V${range[1]}H${sign * outerTickSize}`;
140 |
141 | labelElement = {label};
142 | }
143 |
144 | const tickElements = _.compact(ticks.map((tick, index) => {
145 | const position = activeScale(tick);
146 | const translate = transform.replace('{}', position);
147 | const formatted = tickFormatter(tick);
148 | const tickClasses = ['tick'];
149 | let offset = 0;
150 | if (index % 2){
151 | tickClasses.push('even');
152 | if (staggerLabels){
153 | offset = 20;
154 | }
155 | } else {
156 | tickClasses.push('odd');
157 | }
158 |
159 | if (formatted.length < 1){
160 | tickClasses.push('hidden');
161 | }
162 | if (_.isFunction(this.props.tickFilter)){
163 | if (!this.props.tickFilter.call(scale, tick, formatted, index)){
164 | return null;
165 | }
166 | }
167 | return (
168 |
169 |
170 |
171 | {tickFormatter(tick)}
172 |
173 | );
174 | }));
175 |
176 | const pathElement = ;
177 |
178 | const axisBackground = ;
179 |
180 | return (
181 |
182 | {axisBackground}
183 | {tickElements}
184 | {pathElement}
185 | {labelElement}
186 |
187 | );
188 | }
189 |
190 | _d3ScaleExtent(domain) {
191 | const start = domain[0];
192 | const stop = domain[domain.length - 1];
193 | return start < stop ? [start, stop] : [stop, start];
194 | }
195 |
196 | _d3ScaleRange(scale) {
197 | return scale.rangeExtent ? scale.rangeExtent() : this._d3ScaleExtent(scale.range());
198 | }
199 | }
200 |
201 | export default Axis;
202 |
--------------------------------------------------------------------------------
/src/Bar.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | class Bar extends Component {
4 |
5 | static propTypes = {
6 | className: PropTypes.string,
7 | width: PropTypes.number.isRequired,
8 | height: PropTypes.number.isRequired,
9 | x: PropTypes.number.isRequired,
10 | y: PropTypes.number.isRequired,
11 | fill: PropTypes.string.isRequired,
12 | data: PropTypes.oneOfType([
13 | PropTypes.array,
14 | PropTypes.object
15 | ]).isRequired,
16 | onMouseMove: PropTypes.func,
17 | onMouseLeave: PropTypes.func
18 | };
19 |
20 | static defaultProps = {
21 | className: 'bar'
22 | };
23 |
24 | render() {
25 | const {
26 | className,
27 | x,
28 | y,
29 | width,
30 | height,
31 | fill,
32 | data,
33 | onMouseMove,
34 | onMouseLeave
35 | } = this.props;
36 |
37 | return (
38 | { onMouseMove(e, data); } }
46 | onMouseLeave={ e => { onMouseLeave(e); } }
47 | />
48 | );
49 | }
50 | }
51 |
52 | export default Bar;
53 |
--------------------------------------------------------------------------------
/src/BarChart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import d3 from 'd3/d3.min.js';
3 | import Chart from './Chart';
4 | import Axis from './Axis';
5 | import Bar from './Bar';
6 | import Tooltip from './Tooltip';
7 | import * as helpers from './helpers.js';
8 | import ReactDOM from 'react-dom';
9 |
10 | class BarChart extends Component {
11 |
12 | static propTypes = {
13 | barPadding: PropTypes.number,
14 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
15 | className: PropTypes.string,
16 | colorScale: PropTypes.func,
17 | data: PropTypes.oneOfType([
18 | PropTypes.object,
19 | PropTypes.array
20 | ]).isRequired,
21 | groupedBars: PropTypes.bool,
22 | height: PropTypes.number.isRequired,
23 | label: PropTypes.func,
24 | legend: PropTypes.object,
25 | margin: PropTypes.shape({
26 | top: PropTypes.number,
27 | bottom: PropTypes.number,
28 | left: PropTypes.number,
29 | right: PropTypes.number
30 | }),
31 | offset: PropTypes.string,
32 | tooltipHtml: PropTypes.func,
33 | tooltipMode: PropTypes.oneOf(['mouse', 'element', 'fixed']),
34 | tooltipClassName: PropTypes.string,
35 | tooltipContained: PropTypes.bool,
36 | tooltipOffset: PropTypes.objectOf(PropTypes.number),
37 | values: PropTypes.func,
38 | width: PropTypes.number.isRequired,
39 | x: PropTypes.func,
40 | xAxis: PropTypes.object,
41 | xScale: PropTypes.func,
42 | y: PropTypes.func,
43 | yAxis: PropTypes.object,
44 | y0: PropTypes.func,
45 | yScale: PropTypes.func
46 | };
47 |
48 | static defaultProps = {
49 | barPadding: 0.5,
50 | className: 'chart',
51 | colorScale: d3.scale.category20(),
52 | data: {label: 'No data available', values: [{x: 'No data available', y: 1}]},
53 | groupedBars: false,
54 | label: stack => { return stack.label; },
55 | margin: {top: 0, bottom: 0, left: 0, right: 0},
56 | offset: 'zero',
57 | order: 'default',
58 | tooltipMode: 'mouse',
59 | tooltipOffset: {top: -35, left: 0},
60 | tooltipClassName: null,
61 | tooltipHtml: null,
62 | tooltipContained: false,
63 | values: stack => { return stack.values; },
64 | x: e => { return e.x; },
65 | xScale: null,
66 | y: e => { return e.y; },
67 | y0: e => { return e.y0; },
68 | yScale: null
69 | };
70 |
71 | constructor(props) {
72 | super(props);
73 | this.state = {
74 | tooltip: {
75 | hidden: true
76 | }
77 | };
78 | }
79 |
80 | componentDidMount() {
81 | this._svg_node = ReactDOM.findDOMNode(this).getElementsByTagName('svg')[0];
82 | }
83 |
84 | componentWillMount() {
85 | helpers.calculateInner(this, this.props);
86 | helpers.arrayify(this, this.props);
87 | this._stackData(this.props);
88 | helpers.makeScales(this, this.props);
89 | helpers.addTooltipMouseHandlers(this);
90 | }
91 |
92 | componentWillReceiveProps(nextProps) {
93 | helpers.calculateInner(this, nextProps);
94 | helpers.arrayify(this, nextProps);
95 | this._stackData(nextProps);
96 | helpers.makeScales(this, nextProps);
97 | }
98 |
99 | _stackData(props) {
100 | const { offset, order, x, y, values } = props;
101 |
102 | const stack = d3.layout.stack()
103 | .offset(offset)
104 | .order(order)
105 | .x(x)
106 | .y(y)
107 | .values(values);
108 |
109 | this._data = stack(this._data);
110 | }
111 |
112 | _tooltipHtml(d /*, position*/) {
113 | const xScale = this._xScale;
114 | const yScale = this._yScale;
115 |
116 | const x = this.props.x(d);
117 | const y0 = this.props.y0(d);
118 |
119 | let i, j;
120 | const midPoint = xScale.rangeBand() / 2;
121 | const xPos = midPoint + xScale(x);
122 |
123 | const topStack = this._data[this._data.length - 1].values;
124 | let topElement = null;
125 |
126 | for (i = 0; i < topStack.length; i++) {
127 | if (this.props.x(topStack[i]) === x) {
128 | topElement = topStack[i];
129 | break;
130 | }
131 | }
132 | const yPos = yScale(this.props.y0(topElement) + this.props.y(topElement));
133 |
134 | let datum, value, total, valuesLen, dataLabel;
135 | const dataLen = this._data.length;
136 | total = 0;
137 | for (i=0; i {
189 | return values(stack).map((e, index) => {
190 | return (
191 | );
202 | });
203 | });
204 | } else {
205 | bars = data.map(stack => {
206 | return values(stack).map((e, index) => {
207 | return (
208 | );
219 | });
220 | });
221 | }
222 |
223 | return (
224 |
225 |
226 |
227 |
228 | {bars}
229 | { this.props.children }
230 |
231 |
232 |
233 | );
234 | }
235 |
236 | }
237 |
238 | export default BarChart;
239 |
--------------------------------------------------------------------------------
/src/Chart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import Legend from './Legend';
3 | import _ from 'lodash';
4 |
5 | class Chart extends Component {
6 |
7 | static propTypes = {
8 | className: PropTypes.string,
9 | height: PropTypes.number.isRequired,
10 | width: PropTypes.number.isRequired,
11 | legend: PropTypes.object,
12 | defs: PropTypes.arrayOf(PropTypes.object),
13 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
14 | margin: PropTypes.shape({
15 | top: PropTypes.number,
16 | bottom: PropTypes.number,
17 | left: PropTypes.number,
18 | right: PropTypes.number
19 | }).isRequired
20 | };
21 |
22 | static defaultProps = {
23 | className: 'chart',
24 | defs: [],
25 | legend: {
26 | },
27 | margin: {
28 | top: 0,
29 | bottom: 0,
30 | left: 0,
31 | right: 0
32 | }
33 | };
34 |
35 | constructor(props){
36 | super(props);
37 | this.state = {
38 | legendX: props.margin.left,
39 | legendY: props.margin.top + props.height
40 | };
41 | }
42 |
43 | componentDidMount() {
44 | this.alignLegend();
45 | }
46 |
47 | componentDidUpdate(prevProps) {
48 | if (!_.isEqual(this.props, prevProps)){
49 | this.alignLegend();
50 | }
51 | }
52 |
53 | hasLegend(){
54 | if (_.isPlainObject(this.props.legend)){
55 | return _.isArray(this.props.legend.data) && this.props.legend.data.length > 0;
56 | }
57 | return false;
58 | }
59 |
60 | alignLegend() {
61 | if (this.hasLegend()){
62 |
63 | let legendX, legendY;
64 |
65 | const chart = d3.select(this.refs.chart);
66 | const legend = chart.select('g.legend');
67 | const bbox = legend.node().getBBox();
68 | const align = _.isString(this.props.legend.align) ? this.props.legend.align : 'left';
69 | const position = _.isString(this.props.legend.position) ? this.props.legend.position : 'bottom';
70 |
71 | switch (align.toLowerCase()){
72 | case 'right':
73 | legendX = (this.props.width - this.props.margin.right) - bbox.width;
74 | break;
75 | case 'center':
76 | legendX = (this.props.width / 2) - (bbox.width / 2);
77 | break;
78 | case 'left':
79 | default:
80 | legendX = this.props.margin.left;
81 | break;
82 | }//end switch
83 |
84 | switch (position){
85 | case 'top':
86 | legendY = this.props.margin.top;
87 | break;
88 | case 'bottom':
89 | default:
90 | legendY = this.props.height;
91 | break;
92 | }
93 | this.setState({ legendX: legendX, legendY: legendY });
94 | }
95 | }
96 |
97 | render() {
98 | const { width, height, margin, viewBox, preserveAspectRatio, children } = this.props;
99 |
100 | let legendOffset = 0;
101 |
102 | const hasLegend = this.hasLegend();
103 |
104 | if (hasLegend){
105 | legendOffset += 50;
106 | }
107 |
108 | return (
109 |
110 |
117 |
118 | );
119 | }
120 |
121 | }
122 |
123 | export default Chart;
124 |
--------------------------------------------------------------------------------
/src/DonutChart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import d3 from 'd3/d3.min.js';
4 | import Chart from './Chart';
5 | import Path from './Path';
6 | import Tooltip from './Tooltip';
7 | import * as helpers from './helpers.js';
8 | import _ from 'lodash';
9 |
10 | class DonutChart extends Component {
11 |
12 | static propTypes = {
13 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
14 | className: PropTypes.string,
15 | colorScale: PropTypes.func,
16 | cornerRadius: PropTypes.number,
17 | data: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
18 | height: PropTypes.number.isRequired,
19 | innerRadius: PropTypes.number,
20 | legend: PropTypes.object,
21 | outerRadius: PropTypes.number,
22 | margin: PropTypes.shape({
23 | top: PropTypes.number,
24 | bottom: PropTypes.number,
25 | left: PropTypes.number,
26 | right: PropTypes.number
27 | }),
28 | padRadius: PropTypes.string,
29 | sort: PropTypes.any,
30 | tooltipHtml: PropTypes.func,
31 | tooltipMode: PropTypes.oneOf(['mouse', 'element', 'fixed']),
32 | tooltipClassName: PropTypes.string,
33 | tooltipContained: PropTypes.bool,
34 | tooltipOffset: PropTypes.objectOf(PropTypes.number),
35 | values: PropTypes.func,
36 | width: PropTypes.number.isRequired,
37 | x: PropTypes.func,
38 | xScale: PropTypes.func,
39 | y: PropTypes.func,
40 | yScale: PropTypes.func
41 | };
42 |
43 | static defaultProps = {
44 | className: 'chart',
45 | colorScale: d3.scale.category20(),
46 | data: [],
47 | innerRadius: null,
48 | margin: {top: 0, bottom: 0, left: 0, right: 0},
49 | outerRadius: null,
50 | padRadius: 'auto',
51 | cornerRadius: 0,
52 | sort: undefined,
53 | tooltipMode: 'mouse',
54 | tooltipOffset: {top: -35, left: 0},
55 | tooltipClassName: null,
56 | tooltipHtml: null,
57 | tooltipContained: false,
58 | values: stack => {
59 | return stack.values;
60 | },
61 | x: e => { return e.x; },
62 | y: e => { return e.y; },
63 | y0: () => { return 0; }
64 | };
65 |
66 | constructor(props) {
67 | super(props);
68 | this.state = {
69 | tooltip: {
70 | hidden: true
71 | }
72 | };
73 | }
74 |
75 | componentDidMount() {
76 | this._svg_node = ReactDOM.findDOMNode(this).getElementsByTagName('svg')[0];
77 | }
78 |
79 | componentWillMount() {
80 | helpers.calculateInner(this, this.props);
81 | //helpers.arrayify(this, this.props);
82 | helpers.makeScales(this, this.props);
83 | helpers.addTooltipMouseHandlers(this);
84 | }
85 |
86 | componentWillReceiveProps(nextProps) {
87 | helpers.calculateInner(this, nextProps);
88 | //helpers.arrayify(this, nextProps);
89 | helpers.makeScales(this, nextProps);
90 | }
91 |
92 | _tooltipHtml(d) {
93 | const html = this.props.tooltipHtml(d.x, d.y);
94 | return [html, 0, 0];
95 | }
96 |
97 | _midAngle(d){
98 | return d.startAngle + (d.endAngle - d.startAngle)/2;
99 | }
100 |
101 | render() {
102 | const {
103 | data,
104 | width,
105 | height,
106 | legend,
107 | margin,
108 | colorScale,
109 | padRadius,
110 | cornerRadius,
111 | sort,
112 | x,
113 | y,
114 | values
115 | } = this.props;
116 |
117 | const [
118 | innerWidth,
119 | innerHeight,
120 | ] = [
121 | this._innerWidth,
122 | this._innerHeight
123 | ];
124 |
125 | let innerRadius = this.props.innerRadius;
126 | let outerRadius = this.props.outerRadius;
127 |
128 | let pie = d3.layout.pie().value(e => {
129 | return y(e);
130 | });
131 |
132 | if (typeof sort !== 'undefined') {
133 | pie = pie.sort(sort);
134 | }
135 |
136 | const radius = Math.min(innerWidth, innerHeight) / 2;
137 | if (!innerRadius) {
138 | innerRadius = radius * 0.8;
139 | }
140 |
141 | if (!outerRadius) {
142 | outerRadius = radius * 0.4;
143 | }
144 |
145 | const translation = `translate(${innerWidth/2}, ${innerHeight/2})`;
146 | let wedges;
147 |
148 | if (_.isPlainObject(data) && _.isArray(data.values) && data.values.length > 0){
149 | const arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius).padRadius(padRadius).cornerRadius(cornerRadius);
150 | const vals = values(data);
151 |
152 | const pieData = pie(vals);
153 |
154 | wedges = pieData.map((e, index) => {
155 | const d = arc(e);
156 | return (
157 | {
164 | this.handleMouseMove(evt, e.data);
165 | }}
166 | onMouseLeave={this.handleMouseLeave.bind(this)}
167 | data={data}
168 | />
169 | );
170 | });
171 | }
172 |
173 | return (
174 |
175 |
176 |
177 | {wedges}
178 |
179 | { this.props.children }
180 |
181 |
182 |
183 | );
184 | }
185 | }
186 |
187 | export default DonutChart;
188 |
--------------------------------------------------------------------------------
/src/Legend.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import d3 from 'd3/d3.min.js';
3 | import color from 'sc-color';
4 | import _ from 'lodash';
5 |
6 | class Legend extends Component {
7 |
8 | static propTypes = {
9 | className: PropTypes.string,
10 | cellsClassName: PropTypes.string,
11 | cellClassName: PropTypes.string,
12 | cellTextClassName: PropTypes.string,
13 | symbolType: PropTypes.oneOf(['circle', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-up']),
14 | symbolSize: PropTypes.number,
15 | symbolOffset: PropTypes.number,
16 | symbolPosition: PropTypes.string,
17 | wrapText: PropTypes.bool,
18 | x: PropTypes.number,
19 | y: PropTypes.number,
20 | defaultSymbolColor: PropTypes.string,
21 | data: PropTypes.arrayOf(PropTypes.object).isRequired
22 | };
23 |
24 | static defaultProps = {
25 | className: 'legend',
26 | cellsClassName: 'cells',
27 | cellClassName: 'cell',
28 | cellTextClassName: 'label',
29 | symbolType: 'circle',
30 | symbolPosition: 'left',
31 | symbolSize: 80,
32 | symbolOffset: 20,
33 | wrapText: false,
34 | x: 0,
35 | y: 0,
36 | defaultSymbolColor: '#000000'
37 | };
38 |
39 | componentDidMount() {
40 | this.updateLegendText();
41 | }
42 |
43 | componentDidUpdate(prevProps) {
44 | if (!_.isEqual(this.props, prevProps)){
45 | this.updateLegendText();
46 | }
47 | }
48 |
49 | updateLegendText() {
50 | const cellClassName = this.props.cellClassName;
51 | const symbolOffset = this.props.symbolOffset;
52 | const symbolPosition = this.props.symbolPosition.toLowerCase();
53 |
54 | const wrapper = function(text, width){
55 | text.each(function(){
56 | const t = d3.select(this);
57 | if (!t.attr('data-wrapped')){
58 | //split words on space, slashes, underscores, or dashes
59 | const words = t.text().split(/[\s\/_-]+/).reverse();
60 | if (words.length > 0){
61 | let line = [];
62 | let word;
63 | let lineNumber = 0;
64 | const lineHeight = 1.1; // ems
65 | const x = (symbolPosition === 'top') ? '0' : '0.5em';
66 | const y = (symbolPosition === 'top') ? '0' : '0.4em';
67 | const dy = 0;
68 | let textLen = 0;
69 | let tspan = t.text(null).append('tspan').attr('x', x).attr('y', y).attr('dy', dy + 'em');
70 | while (word = words.pop()) {
71 | line.push(word);
72 | tspan.text(line.join(' '));
73 | textLen = tspan.node().getComputedTextLength();
74 | if (textLen > width) {
75 | if (line.length > 1){
76 | line.pop();
77 | }
78 | tspan.text(line.join(' '));
79 | line = [word];
80 | t.attr('data-wrapped', 'true');
81 | tspan = t.append('tspan').attr('x', x).attr('y', y).attr('dy', (lineNumber * lineHeight) + dy + 'em').text(word);
82 | lineNumber += 1;
83 | }
84 | }
85 | }
86 | }
87 | });
88 | };
89 |
90 | const leg = d3.select(this.refs.legend);
91 | if (this.props.wrapText){
92 | leg.selectAll('.cell text').call(wrapper, d3.scale.ordinal().domain([]).range([]).rangeBand());
93 | }
94 |
95 | let textWidthOffset = 0;
96 | const cells = leg.selectAll('g.' + cellClassName);
97 | cells.attr('transform', function (d, index) {
98 | const cell = d3.select(this);
99 | const symbol = cell.select('path');
100 | const text = cell.select('text');
101 | const symbolBox = symbol.node().getBBox();
102 | const textWidth = text.node().getBBox().width;
103 | if (symbolPosition === 'top'){
104 | symbol.attr('transform', `translate(${ textWidth / 2 }, 0)`);
105 | text.attr('transform', `translate(0, ${ symbolBox.height + symbolOffset })`);
106 | }
107 | const translate = `translate(${ (symbolBox.width + symbolOffset * index) + textWidthOffset }, 0)`;
108 | textWidthOffset += textWidth;
109 | return translate;
110 | });
111 | }
112 |
113 | render() {
114 | const cells = this.props.data.map((obj, index) =>{
115 | const symbolPathData = d3.svg.symbol().type(this.props.symbolType).size(this.props.symbolSize)();
116 | const keys = Object.keys(obj);
117 | const symbolColor = keys.length ? color(obj[keys[0]]).html() : color(this.props.defaultSymbolColor).html();
118 | return (
119 |
120 |
121 |
122 | { keys[0] }
123 |
124 |
125 | );
126 | });
127 |
128 | const legendTransform=`translate(${this.props.x}, ${this.props.y})`;
129 | return (
130 |
131 |
132 | { cells }
133 |
134 |
135 | );
136 | }
137 | }
138 |
139 | export default Legend;
140 |
--------------------------------------------------------------------------------
/src/LineChart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import d3 from 'd3/d3.min.js';
3 | import Chart from './Chart';
4 | import Axis from './Axis';
5 | import Path from './Path';
6 | import Tooltip from './Tooltip';
7 | import * as helpers from './helpers.js';
8 | import ReactDOM from 'react-dom';
9 |
10 | class LineChart extends Component {
11 |
12 | static propTypes = {
13 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
14 | className: PropTypes.string,
15 | colorScale: PropTypes.func,
16 | data: PropTypes.oneOfType([
17 | PropTypes.object,
18 | PropTypes.array
19 | ]).isRequired,
20 | defined: PropTypes.func,
21 | height: PropTypes.number.isRequired,
22 | interpolate: PropTypes.string,
23 | label: PropTypes.func,
24 | legend: PropTypes.object,
25 | xAxis: PropTypes.object,
26 | yAxis: PropTypes.object,
27 | margin: PropTypes.shape({
28 | top: PropTypes.number,
29 | bottom: PropTypes.number,
30 | left: PropTypes.number,
31 | right: PropTypes.number
32 | }),
33 | shape: PropTypes.string,
34 | shapeColor: PropTypes.string,
35 | stroke: PropTypes.object,
36 | tooltipHtml: PropTypes.func,
37 | tooltipMode: PropTypes.oneOf(['mouse', 'element', 'fixed']),
38 | tooltipClassName: PropTypes.string,
39 | tooltipContained: PropTypes.bool,
40 | tooltipOffset: PropTypes.objectOf(PropTypes.number),
41 | values: PropTypes.func,
42 | width: PropTypes.number.isRequired,
43 | x: PropTypes.func,
44 | xScale: PropTypes.func,
45 | y: PropTypes.func,
46 | y0: PropTypes.func,
47 | yScale: PropTypes.func
48 | };
49 |
50 | static defaultProps = {
51 | className: 'chart',
52 | colorScale: d3.scale.category20(),
53 | data: {label: 'No data available', values: [{x: 'No data available', y: 1}]},
54 | interpolate: 'linear',
55 | label: stack => { return stack.label; },
56 | defined: () => true,
57 | margin: {top: 0, bottom: 0, left: 0, right: 0},
58 | shape: 'circle',
59 | stroke: {},
60 | tooltipMode: 'mouse',
61 | tooltipOffset: {top: -35, left: 0},
62 | tooltipClassName: null,
63 | tooltipHtml: null,
64 | tooltipContained: false,
65 | values: stack => { return stack.values; },
66 | x: e => { return e.x; },
67 | xScale: null,
68 | y: e => { return e.y; },
69 | y0: () => { return 0; },
70 | yScale: null
71 | };
72 |
73 | constructor(props) {
74 | super(props);
75 | this.state = {
76 | tooltip: {
77 | hidden: true
78 | }
79 | };
80 | }
81 |
82 | componentDidMount() {
83 | this._svg_node = ReactDOM.findDOMNode(this).getElementsByTagName('svg')[0];
84 | }
85 |
86 | componentWillMount() {
87 | helpers.calculateInner(this, this.props);
88 | helpers.arrayify(this, this.props);
89 | helpers.makeScales(this, this.props);
90 | helpers.addTooltipMouseHandlers(this);
91 | }
92 |
93 | componentWillReceiveProps(nextProps) {
94 | helpers.calculateInner(this, nextProps);
95 | helpers.arrayify(this, nextProps);
96 | helpers.makeScales(this, nextProps);
97 | }
98 |
99 | /*
100 | The code below supports finding the data values for the line closest to the mouse cursor.
101 | Since it gets all events from the Rect overlaying the Chart the tooltip gets shown everywhere.
102 | For now I don't want to use this method.
103 | */
104 | _tooltipHtml(data, position) {
105 |
106 | const { x, y, values, label } = this.props;
107 | const [xScale, yScale] = [this._xScale, this._yScale];
108 |
109 | const xValueCursor = xScale.invert(position[0]);
110 | const yValueCursor = yScale.invert(position[1]);
111 |
112 | const xBisector = d3.bisector(e => { return x(e); }).left;
113 |
114 | const valuesAtX = data.map(function(stack) {
115 | const idx = xBisector(values(stack), xValueCursor);
116 |
117 | const indexRight = idx === values(stack).length ? idx - 1 : idx;
118 | const valueRight = x(values(stack)[indexRight]);
119 |
120 | const indexLeft = idx === 0 ? idx : idx - 1;
121 | const valueLeft = x(values(stack)[indexLeft]);
122 |
123 | let index;
124 | if (Math.abs(xValueCursor - valueRight) < Math.abs(xValueCursor - valueLeft)) {
125 | index = indexRight;
126 | } else {
127 | index = indexLeft;
128 | }
129 |
130 | return { label: label(stack), value: values(stack)[index] };
131 | });
132 |
133 | valuesAtX.sort((a, b) => { return y(a.value) - y(b.value); });
134 |
135 | const yBisector = d3.bisector(e => { return y(e.value); }).left;
136 | const yIndex = yBisector(valuesAtX, yValueCursor);
137 |
138 | const yIndexRight = yIndex === valuesAtX.length ? yIndex - 1 : yIndex;
139 | const yIndexLeft = yIndex === 0 ? yIndex : yIndex - 1;
140 |
141 | const yValueRight = y(valuesAtX[yIndexRight].value);
142 | const yValueLeft = y(valuesAtX[yIndexLeft].value);
143 |
144 | let index;
145 | if (Math.abs(yValueCursor - yValueRight) < Math.abs(yValueCursor - yValueLeft)) {
146 | index = yIndexRight;
147 | } else {
148 | index = yIndexLeft;
149 | }
150 |
151 | this._tooltipData = valuesAtX[index];
152 |
153 | const html = this.props.tooltipHtml(valuesAtX[index].label, valuesAtX[index].value);
154 |
155 | const xPos = xScale(valuesAtX[index].value.x);
156 | const yPos = yScale(valuesAtX[index].value.y);
157 | return [html, xPos, yPos];
158 | }
159 |
160 | render() {
161 | const {
162 | height,
163 | width,
164 | margin,
165 | colorScale,
166 | interpolate,
167 | defined,
168 | stroke,
169 | values,
170 | label,
171 | x,
172 | y,
173 | xAxis,
174 | yAxis,
175 | shape,
176 | shapeColor,
177 | legend
178 | } = this.props;
179 |
180 | const [
181 | data,
182 | innerWidth,
183 | innerHeight,
184 | xScale,
185 | yScale,
186 | xIntercept,
187 | yIntercept
188 | ] = [
189 | this._data,
190 | this._innerWidth,
191 | this._innerHeight,
192 | this._xScale,
193 | this._yScale,
194 | this._xIntercept,
195 | this._yIntercept
196 | ];
197 |
198 | const handleMouseMove = this.handleMouseMove.bind(this);
199 | const handleMouseLeave = this.handleMouseLeave.bind(this);
200 |
201 | const line = d3.svg.line()
202 | .x(function(e) { return xScale(x(e)); })
203 | .y(function(e) { return yScale(y(e)); })
204 | .interpolate(interpolate)
205 | .defined(defined);
206 |
207 | let tooltipSymbol;
208 | if (!this.state.tooltip.hidden) {
209 | const symbol = d3.svg.symbol().type(shape);
210 | const symbolColor = shapeColor ? shapeColor : colorScale(this._tooltipData.label);
211 | const translate = this._tooltipData ? `translate(${xScale(x(this._tooltipData.value))}, ${yScale(y(this._tooltipData.value))})` : '';
212 | tooltipSymbol = this.state.tooltip.hidden ? null : ;
213 | }
214 |
215 | const sizeId = innerWidth + 'x' + innerHeight;
216 |
217 | const lines = data.map(function(stack, index){
218 | return (
219 |
232 | );
233 | });
234 |
235 | return (
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | {lines}
249 |
250 |
251 | { this.props.children }
252 | {tooltipSymbol}
253 |
254 |
255 |
);
256 | }
257 |
258 | }
259 |
260 | export default LineChart;
261 |
--------------------------------------------------------------------------------
/src/NodeChart.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import d3 from 'd3/d3.min.js';
4 | import Chart from './Chart';
5 | import Tooltip from './Tooltip';
6 | import * as helpers from './helpers.js';
7 | import _ from 'lodash';
8 | import shortid from 'shortid';
9 |
10 | class NodeChart extends Component {
11 |
12 | static propTypes = {
13 | children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]),
14 | className: PropTypes.string,
15 | colorScale: PropTypes.func,
16 | data: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
17 | height: PropTypes.number.isRequired,
18 | innerNodeOffset: PropTypes.number,
19 | layout: PropTypes.string,
20 | legend: PropTypes.object,
21 | margin: PropTypes.shape({
22 | top: PropTypes.number,
23 | bottom: PropTypes.number,
24 | left: PropTypes.number,
25 | right: PropTypes.number
26 | }),
27 | defaultNodeRadius: PropTypes.number,
28 | maxNodeRadius: PropTypes.number,
29 | minNodeRadius: PropTypes.number,
30 | labelNodes: PropTypes.bool,
31 | onNodeClick: PropTypes.func,
32 | scaleNodesByValue: PropTypes.bool,
33 | tooltipHtml: PropTypes.func,
34 | tooltipMode: PropTypes.oneOf(['mouse', 'element', 'fixed']),
35 | tooltipClassName: PropTypes.string,
36 | tooltipContained: PropTypes.bool,
37 | tooltipOffset: PropTypes.objectOf(PropTypes.number),
38 | values: PropTypes.func,
39 | width: PropTypes.number.isRequired,
40 | x: PropTypes.func,
41 | xScale: PropTypes.func,
42 | y: PropTypes.func,
43 | yScale: PropTypes.func
44 | };
45 |
46 | static defaultProps = {
47 | className: 'chart',
48 | colorScale: d3.scale.category20(),
49 | data: [],
50 | innerNodeOffset: 6,
51 | layout: 'radial',
52 | margin: {top: 0, bottom: 0, left: 0, right: 0},
53 | defaultNodeRadius: 15,
54 | maxNodeRadius: 50,
55 | minNodeRadius: 10,
56 | scaleNodesByValue: false,
57 | labelNodes: true,
58 | tooltipMode: 'mouse',
59 | tooltipOffset: {top: -35, left: 0},
60 | tooltipClassName: null,
61 | tooltipHtml: null,
62 | tooltipContained: false,
63 | values: stack => {
64 | return stack.values;
65 | },
66 | x: e => { return e.x; },
67 | y: e => { return e.y; },
68 | y0: () => { return 0; }
69 | };
70 |
71 | constructor(props) {
72 | super(props);
73 |
74 | this.uniqueId = shortid.generate();
75 | this.state = {
76 | tree: this._buildTree(props),
77 | tooltip: {
78 | hidden: true
79 | }
80 | };
81 | }
82 |
83 | componentDidMount() {
84 | const svgs = ReactDOM.findDOMNode(this).getElementsByTagName('svg');
85 | this._svg_node = svgs[0];
86 | const self = this;
87 | this._drag = d3.behavior.drag().on('drag', function() {
88 | const evt = d3.event;
89 | if (evt.sourceEvent.buttons || evt.type === 'drag'){
90 | self._handleDrag(this, evt.dx, evt.dy);
91 | }
92 | });
93 |
94 | this._setupDrag();
95 | }
96 |
97 | componentWillMount() {
98 | helpers.calculateInner(this, this.props);
99 | helpers.addTooltipMouseHandlers(this);
100 | }
101 |
102 | componentWillReceiveProps(nextProps) {
103 | helpers.calculateInner(this, nextProps);
104 | const tooltip = this.state.tooltip;
105 | this.setState({
106 | tree: this._buildTree(nextProps),
107 | tooltip: tooltip
108 | });
109 | }
110 |
111 | componentDidUpdate(){
112 | this._setupDrag();
113 | }
114 |
115 | _radial(center, radius, scaleRadius, len){
116 | return function(node, index){
117 | const D2R = Math.PI / 180;
118 | const startAngle = 90;
119 | const displacement = (len-1)>=12 ? 30 : 360/(len-1);
120 | const currentAngle = startAngle + (displacement * index);
121 | const currentAngleRadians = currentAngle * D2R;
122 | const radialPoint = {
123 | x: center.x + radius * Math.cos(currentAngleRadians),
124 | y: center.y + radius * Math.sin(currentAngleRadians)
125 | };
126 | node.x += (radialPoint.x - node.x);
127 | node.y += (radialPoint.y - node.y);
128 | if (scaleRadius){
129 | node.radius = scaleRadius(node.value);
130 | //console.log('assigned node', node.value, node.radius);
131 | }
132 | };
133 | }
134 |
135 | _values(nodes) {
136 | const nodeValues = [];
137 | _.forEach(nodes, function(node){
138 | nodeValues.push(node.value);
139 | });
140 | nodeValues.sort(function(a, b){
141 | return b - a;
142 | });
143 | //console.log('sorted', nodeValues, nodeValues.length);
144 | return nodeValues;
145 | }
146 |
147 | _makeScale(nodeValues, minNodeRadius, maxNodeRadius, defaultNodeRadius) {
148 | const largestNodeRadius = nodeValues[0];
149 | const smallestNodeRadius = nodeValues[nodeValues.length - 1];
150 | const rangeSize = largestNodeRadius - smallestNodeRadius;
151 | if (rangeSize > 0){
152 | return function(value){
153 | const adjustedValue = value - smallestNodeRadius;
154 | return minNodeRadius + Math.floor((maxNodeRadius/rangeSize) * adjustedValue);
155 | };
156 | }
157 | return function(){
158 | return defaultNodeRadius;
159 | };
160 | }
161 |
162 | _collide(node) {
163 | const nr = node.radius,
164 | nx1 = node.x - nr,
165 | nx2 = node.x + nr,
166 | ny1 = node.y - nr,
167 | ny2 = node.y + nr;
168 | return function(quad, x1, y1, x2, y2) {
169 | if (quad.point && (quad.point !== node)) {
170 | let x = node.x - quad.point.x;
171 | let y = node.y - quad.point.y;
172 | let l = Math.sqrt(x * x + y * y);
173 | const r = node.radius + quad.point.radius;
174 | if (l < r) {
175 | l = (l - r) / l;
176 | node.x -= x *= l;
177 | node.y -= y *= l;
178 | quad.point.x += x;
179 | quad.point.y += y;
180 | }
181 | }
182 | return x1 > nx2
183 | || x2 < nx1
184 | || y1 > ny2
185 | || y2 < ny1;
186 | };
187 | }
188 |
189 | _buildTree(props){
190 | if (!props){
191 | return {};
192 | }
193 |
194 | const innerWidth = props.width - props.margin.left - props.margin.right;
195 | const innerHeight = props.height - props.margin.top - props.margin.bottom;
196 | const center = {
197 | x: innerWidth / 2,
198 | y: innerHeight / 2
199 | };
200 |
201 | const diameter = Math.min(innerHeight, innerWidth);
202 | const size = [innerWidth, innerHeight];
203 | const tree = d3.layout.tree().size(size);
204 | const nodes = tree.nodes(_.cloneDeep(props.data));
205 | let i, scaleRadius;
206 |
207 | if (props.scaleNodesByValue){
208 | const nodeValues = this._values(nodes);
209 | if (nodeValues.length > 0){
210 | scaleRadius = this._makeScale(nodeValues, props.minNodeRadius, props.maxNodeRadius, props.defaultNodeRadius);
211 | }
212 | }
213 |
214 | let chartRadius = (diameter / 2);
215 |
216 | if (props.layout === 'radial'){
217 | const len = nodes.length;
218 | let radial = this._radial(center, chartRadius, scaleRadius, len, props.width, props.height);
219 |
220 | //put the first node in the center
221 | nodes[0].x = center.x;
222 | nodes[0].y = center.y;
223 | nodes[0].radius = scaleRadius ? scaleRadius(nodes[0].value) : props.defaultNodeRadius;
224 |
225 | //distribute the around the center like the hours on a clock
226 | for (i = 1; i < len; ++i){
227 | if (((i-1) % 12) === 0){
228 | //distribute the next go round with a shorter radius
229 | chartRadius = Math.max(0, chartRadius) - (chartRadius * 0.25) - props.innerNodeOffset;
230 | radial = this._radial(center, chartRadius, scaleRadius, len, props.width, props.height);
231 | }
232 | nodes[i].radius = props.defaultNodeRadius;
233 | radial(nodes[i], i, scaleRadius);
234 | }
235 |
236 | //detect and fix collisions
237 | const q = d3.geom.quadtree(nodes);
238 | i = 0;
239 | while (++i < len) {
240 | q.visit(this._collide(nodes[i]));
241 | }
242 | }
243 | const links = tree.links(nodes);
244 | return {
245 | nodes: nodes,
246 | links: links
247 | };
248 | }
249 |
250 | _tooltipHtml(node, position) {
251 | const html = this.props.tooltipHtml(node.name, node.value, position);
252 | return [html, 0, 0];
253 | }
254 |
255 |
256 | _setupDrag(){
257 | setTimeout(() => {
258 | const nodes = d3.selectAll('.node');
259 | nodes.call(this._drag);
260 | }, 100);
261 | }
262 |
263 | _handleDrag(node, dx, dy){
264 | const nodeIndex = node.getAttribute('data-node-index');
265 | const tree = this.state.tree;
266 | const nodes = tree.nodes;
267 | const tooltip = { hidden: true };
268 | const len = nodes.length;
269 | if (nodeIndex > -1 && nodeIndex < len){
270 | nodes[nodeIndex].x += dx;
271 | nodes[nodeIndex].y += dy;
272 | this.setState({
273 | tree: tree,
274 | tooltip: tooltip
275 | });
276 | }
277 | }
278 |
279 | _handleClick(evt, node){
280 | if (_.isPlainObject(this.state.tooltip) && _.isBoolean(this.state.tooltip.hidden) && (!this.state.tooltip.hidden)){
281 | //tooltips are hidden when dragging so we only register a click if not dragging
282 | if (this.props.onNodeClick){
283 | this.props.onNodeClick(evt, node);
284 | }
285 | }
286 | }
287 |
288 | _createLink(link, index){
289 | return (
290 |
300 | );
301 | }
302 |
303 | _imageId(index){
304 | return `${this.uniqueId}-node-image-${index}`;
305 | }
306 |
307 | _createPattern(node, index){
308 | const hasImage = _.isString(node.imageUrl);
309 | if (hasImage){
310 | return (
311 |
312 |
319 |
320 | );
321 | }
322 | return '';
323 | }
324 |
325 | _createNode(node, index){
326 | const hasImage = _.isString(node.imageUrl);
327 | return (
328 | {
337 | this._handleClick(evt, node);
338 | }}
339 | onMouseMove={ (evt) => {
340 | this.handleMouseMove(evt, node);
341 | }}
342 | onMouseLeave={this.handleMouseLeave.bind(this)}
343 | />
344 | );
345 | }
346 |
347 | _createLabels(node, index){
348 | if (node.hideLabel){
349 | return null;
350 | }
351 | return (
352 |
358 | { node.label || node.name }
359 |
360 | );
361 | }
362 |
363 | render() {
364 | const {
365 | width,
366 | height,
367 | legend,
368 | margin
369 | } = this.props;
370 |
371 | let patterns = [];
372 | let links = [];
373 | let nodes = [];
374 | let labels = [];
375 |
376 | const tree = this.state.tree;
377 | if (tree && _.isArray(tree.nodes) && tree.nodes.length > 0 && _.isArray(tree.links) && tree.links.length > 0){
378 | links = tree.links.map(this._createLink.bind(this));
379 | patterns = _.compact(tree.nodes.map(this._createPattern.bind(this)));
380 | nodes = tree.nodes.map(this._createNode.bind(this));
381 | if (this.props.labelNodes){
382 | labels = _.compact(tree.nodes.map(this._createLabels.bind(this)));
383 | }
384 | }
385 |
386 | return (
387 |
388 |
389 | {links}
390 | {nodes}
391 | {labels}
392 | { this.props.children }
393 |
394 |
395 |
396 | );
397 | }
398 | }
399 |
400 | export default NodeChart;
401 |
402 |
--------------------------------------------------------------------------------
/src/Path.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | class Path extends Component {
4 |
5 | static propTypes = {
6 | className: PropTypes.string,
7 | stroke: PropTypes.string.isRequired,
8 | strokeLinecap: PropTypes.string,
9 | strokeWidth: PropTypes.string,
10 | strokeDasharray: PropTypes.string,
11 | fill: PropTypes.string,
12 | d: PropTypes.string.isRequired,
13 | data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
14 | onMouseMove: PropTypes.func,
15 | onMouseLeave: PropTypes.func,
16 | style: PropTypes.object
17 | };
18 |
19 | static defaultProps = {
20 | className: 'path',
21 | fill: 'none',
22 | strokeWidth: '2',
23 | strokeLinecap: 'butt',
24 | strokeDasharray: 'none'
25 | };
26 |
27 | render() {
28 | const {
29 | className,
30 | stroke,
31 | strokeWidth,
32 | strokeLinecap,
33 | strokeDasharray,
34 | fill,
35 | d,
36 | style,
37 | data,
38 | onMouseMove,
39 | onMouseLeave
40 | } = this.props;
41 |
42 | return (
43 | { onMouseMove(evt, data); } }
51 | onMouseLeave={ evt => { onMouseLeave(evt); } }
52 | style={style}
53 | />
54 | );
55 | }
56 | }
57 |
58 | export default Path;
59 |
--------------------------------------------------------------------------------
/src/PieChart.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import DonutChart from './DonutChart';
3 | import _ from 'lodash';
4 |
5 | /* Pie charts are just donut charts without a hole */
6 | class PieChart extends Component {
7 |
8 | render() {
9 | const props = _.omit(this.props, ['outerRadius']);
10 | props.outerRadius = 0.1;
11 | return (
12 |
13 | );
14 | }
15 |
16 | }
17 |
18 | export default PieChart;
19 |
20 |
--------------------------------------------------------------------------------
/src/SparklineChart.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import LineChart from './LineChart';
3 | import _ from 'lodash';
4 |
5 | class SparklineChart extends Component {
6 |
7 | render() {
8 | const props = _.omit(this.props, ['xAxis', 'yAxis']);
9 | props.xAxis = props.yAxis = { visible: false };
10 | return (
11 |
12 | );
13 | }
14 |
15 | }
16 |
17 | export default SparklineChart;
18 |
19 |
--------------------------------------------------------------------------------
/src/Tooltip.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | class Tooltip extends Component {
4 |
5 | static propTypes = {
6 | top: PropTypes.number.isRequired,
7 | left: PropTypes.number.isRequired,
8 | html: PropTypes.node,
9 | hidden: PropTypes.bool,
10 | translate: PropTypes.number,
11 | className: PropTypes.string
12 | };
13 |
14 | static defaultProps = {
15 | top: 150,
16 | left: 100,
17 | html: '',
18 | translate: 50,
19 | className: 'tooltip'
20 | };
21 |
22 | render() {
23 | const {top, left, hidden, html, translate} = this.props;
24 |
25 | const style = {
26 | display: hidden ? 'none' : 'block',
27 | position: 'fixed',
28 | top: top,
29 | left: left,
30 | transform: `translate(-${translate}%, 0)`,
31 | pointerEvents: 'none'
32 | };
33 |
34 | return ( {html}
);
35 | }
36 |
37 | }
38 |
39 | export default Tooltip;
40 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import d3 from 'd3/d3.min.js';
3 |
4 | export function calculateInner(component, props) {
5 | const { height, width, margin } = props;
6 | component._innerHeight = height - margin.top - margin.bottom;
7 | component._innerWidth = width - margin.left - margin.right;
8 | }
9 |
10 | export function arrayify(component, props) {
11 | const isEmptyObject = _.isPlainObject(props.data) && _.keys(props.data).length < 1;
12 | const isEmptyArray = _.isArray(props.data) && props.data.length < 1;
13 | if (props.data === null || isEmptyObject || isEmptyArray) {
14 | component._data = [{
15 | label: 'No data available',
16 | values: [{x: 'No data available', y: 1}]
17 | }];
18 | } else if (!Array.isArray(props.data)) {
19 | component._data = [props.data];
20 | } else {
21 | component._data = props.data;
22 | }
23 | }
24 |
25 | export function makeLinearXScale(component, props) {
26 | const {x, values} = props;
27 | const [ data, innerWidth ] = [component._data, component._innerWidth];
28 |
29 | const extents =
30 | d3.extent(
31 | Array.prototype.concat.apply([],
32 | data.map(stack => {
33 | return values(stack).map(e => {
34 | return x(e);
35 | });
36 | })));
37 |
38 | const scale = d3.scale.linear()
39 | .domain(extents)
40 | .range([0, innerWidth]);
41 |
42 | const zero = d3.max([0, scale.domain()[0]]);
43 | const xIntercept = scale(zero);
44 |
45 | return [scale, xIntercept];
46 | }
47 |
48 | export function makeOrdinalXScale(component, props) {
49 | const { x, values } = props;
50 | const barPadding = _.isNumber(props.barPadding) ? props.barPadding : 0.5;
51 | const [ data, innerWidth ] = [component._data, component._innerWidth];
52 | let scale;
53 |
54 | if (_.isFunction(values) && _.isArray(data) && data.length > 0){
55 | scale = d3.scale.ordinal().domain(values(data[0]).map(e => { return x(e); })).rangeRoundBands([0, innerWidth], barPadding);
56 | } else {
57 | scale = d3.scale.ordinal();
58 | }
59 | return [scale, 0];
60 | }
61 |
62 | export function makeTimeXScale(component, props) {
63 | const { x, values } = props;
64 | const [ data, innerWidth ] = [component._data, component._innerWidth];
65 |
66 | const minDate = d3.min(values(data[0]), x);
67 |
68 | const maxDate = d3.max(values(data[0]), x);
69 |
70 | const scale = d3.time.scale()
71 | .domain([minDate, maxDate])
72 | .range([0, innerWidth]);
73 |
74 | return [scale, 0];
75 | }
76 |
77 | export function makeLinearYScale(component, props) {
78 | const { y, y0, values, groupedBars } = props;
79 | const [ data, innerHeight ] = [component._data, component._innerHeight];
80 |
81 | let extents =
82 | d3.extent(
83 | Array.prototype.concat.apply([],
84 | data.map(stack => {
85 | return values(stack).map(e => {
86 | if (groupedBars) {
87 | return y(e);
88 | }
89 | return y0(e) + y(e);
90 | });
91 | })));
92 |
93 | extents = [d3.min([0, extents[0]]), extents[1]];
94 |
95 | const scale = d3.scale.linear().domain(extents).range([innerHeight, 0]);
96 |
97 | const zero = d3.max([0, scale.domain()[0]]);
98 | const yIntercept = scale(zero);
99 |
100 | return [scale, yIntercept];
101 | }
102 |
103 | export function makeOrdinalYScale(component) {
104 | const [ innerHeight ] = [component._data, component._innerHeight];
105 | const scale = d3.scale.ordinal().range([innerHeight, 0]);
106 | const yIntercept = scale(0);
107 |
108 | return [scale, yIntercept];
109 | }
110 |
111 | export function makeXScale(component, props) {
112 | const { x, values } = props;
113 | const data = component._data;
114 |
115 | if (_.isFunction(x) && _.isFunction(values) && _.isArray(data) && data.length > 0){
116 | if (typeof (x(values(data[0])[0])) === 'number') {
117 | return makeLinearXScale(component, props);
118 | }
119 |
120 | if (typeof x(values(data[0])[0]).getMonth === 'function') {
121 | return makeTimeXScale(component, props);
122 | }
123 | }
124 | return makeOrdinalXScale(component, props);
125 | }
126 |
127 | export function makeYScale(component, props) {
128 | const { y, values } = props;
129 | const data = component._data;
130 |
131 | if (_.isFunction(y) && _.isFunction(values) && _.isArray(data) && data.length > 0){
132 | if (typeof y(values(data[0])[0]) === 'number') {
133 | return makeLinearYScale(component, props);
134 | }
135 | }
136 | return makeOrdinalYScale(component, props);
137 | }
138 |
139 | export function makeScales(component, props) {
140 | const { xScale, xIntercept, yScale, yIntercept } = props;
141 |
142 | if (!xScale) {
143 | [component._xScale, component._xIntercept] = makeXScale(component, props);
144 | } else {
145 | [component._xScale, component._xIntercept] = [xScale, xIntercept];
146 | }
147 |
148 | if (!yScale) {
149 | [component._yScale, component._yIntercept] = makeYScale(component, props);
150 | } else {
151 | [component._yScale, component._yIntercept] = [yScale, yIntercept];
152 | }
153 | }
154 |
155 | export function addTooltipMouseHandlers(component) {
156 | component.handleMouseLeave = function(e) {
157 | if (!this.props.tooltipHtml) {
158 | return;
159 | }
160 |
161 | e.preventDefault();
162 |
163 | this.setState({
164 | tooltip: {
165 | hidden: true
166 | }
167 | });
168 | };
169 | component.handleMouseMove = function(e, data) {
170 | if (!this.props.tooltipHtml) {
171 | return;
172 | }
173 |
174 | e.preventDefault();
175 |
176 | const {
177 | margin,
178 | tooltipMode,
179 | tooltipOffset,
180 | tooltipContained
181 | } = this.props;
182 |
183 | const svg = this._svg_node;
184 | let position;
185 | if (svg.createSVGPoint) {
186 | let point = svg.createSVGPoint();
187 | point.x = e.clientX;
188 | point.y = e.clientY;
189 | point = point.matrixTransform(svg.getScreenCTM().inverse());
190 | position = [point.x - margin.left, point.y - margin.top];
191 | } else {
192 | const rect = svg.getBoundingClientRect();
193 | position = [e.clientX - rect.left - svg.clientLeft - margin.left, e.clientY - rect.top - svg.clientTop - margin.top];
194 | }
195 |
196 | const [html, xPos, yPos] = this._tooltipHtml(data, position);
197 | const svgTop = svg.getBoundingClientRect().top + margin.top;
198 | const svgLeft = svg.getBoundingClientRect().left + margin.left;
199 |
200 | let top = 0;
201 | let left = 0;
202 |
203 | if (tooltipMode === 'fixed') {
204 | top = svgTop + tooltipOffset.top;
205 | left = svgLeft + tooltipOffset.left;
206 | } else if (tooltipMode === 'element') {
207 | top = svgTop + yPos + tooltipOffset.top;
208 | left = svgLeft + xPos + tooltipOffset.left;
209 | } else { // mouse
210 | top = e.clientY + tooltipOffset.top;
211 | left = e.clientX + tooltipOffset.left;
212 | }
213 |
214 | function lerp(t, a, b) {
215 | return (1 - t) * a + t * b;
216 | }
217 |
218 | let translate = 50;
219 |
220 | if (tooltipContained) {
221 | const t = position[0] / svg.getBoundingClientRect().width;
222 | translate = lerp(t, 0, 100);
223 | }
224 |
225 | this.setState({
226 | tooltip: {
227 | top: top,
228 | left: left,
229 | hidden: false,
230 | html: html,
231 | translate: translate
232 | }
233 | });
234 | };
235 | }
236 |
237 |
--------------------------------------------------------------------------------
/test/AreaChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import AreaChart from '../src/AreaChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | });
19 |
20 | it('renders a with some data', function(){
21 | const data = [ {
22 | label: 'Apple',
23 | values: [{x: 0, y: 2}, {x: 1.3, y: 5}, {x: 3, y: 6}, {x: 3.5, y: 6.5}, {x: 4, y: 6}, {x: 4.5, y: 6}, {x: 5, y: 7}, {x: 5.5, y: 8}]
24 | }];
25 | const wrapper = shallow();
26 | const chart = wrapper.find('.chart');
27 | expect(chart).to.have.length(1);
28 | const props = chart.props();
29 | expect(props.height).to.equal(height);
30 | expect(props.width).to.equal(width);
31 | const html = wrapper.html();
32 | expect(html).to.match(/g class="x axis"/);
33 | expect(html).to.match(/line x2=/);
34 | });
35 |
36 | it('should not blow up if no data is passed to it', function(){
37 | const wrapper = shallow();
38 | const chart = wrapper.find('.chart');
39 | expect(chart).to.have.length(1);
40 | expect(wrapper.html()).to.contain('No data available');
41 | });
42 |
43 | it('should not blow up if an empty array of data is passed to it', function(){
44 | const wrapper = shallow();
45 | const chart = wrapper.find('.chart');
46 | expect(chart).to.have.length(1);
47 | expect(wrapper.html()).to.contain('No data available');
48 | });
49 |
50 | it('should not blow up if an empty object of data is passed to it', function(){
51 | const wrapper = shallow();
52 | const chart = wrapper.find('.chart');
53 | expect(chart).to.have.length(1);
54 | expect(wrapper.html()).to.contain('No data available');
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/Axis-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import Axis from '../src/Axis';
5 | import d3 from 'd3/d3.min.js';
6 | import sinon from 'sinon';
7 |
8 | describe('', function() {
9 |
10 | function testAxis(scale, orientation, tickFormat=null, tickFilter=null){
11 | const wrapper = shallow();
12 | expect(wrapper.find('g.axis')).to.have.length(1);
13 | expect(wrapper.find('g.tick.even').length).to.be.gt(1);
14 | expect(wrapper.find('g.tick.odd').length).to.be.gt(1);
15 | expect(wrapper.find('path.domain')).to.have.length(1);
16 | expect(wrapper.find('text.axis.label')).to.have.length(1);
17 | return wrapper;
18 | }
19 |
20 | it('renders an component', function() {
21 | const scale = d3.scale.linear();
22 | testAxis(scale, 'top');
23 | testAxis(scale, 'bottom');
24 | testAxis(scale, 'left');
25 | testAxis(scale, 'right');
26 | });
27 |
28 | it('should support a custom tick format', function(){
29 | const scale = d3.scale.linear();
30 | const format = d3.format(',.0f');
31 | const tickFormatter = function(x){ return format(x); };
32 | testAxis(scale, 'top', tickFormatter);
33 | });
34 |
35 | it('should add a hidden class to any axes with an empty label', function(){
36 | const scale = d3.scale.linear();
37 | const tickFormatter = function(){ return ''; };
38 | const wrapper = testAxis(scale, 'top', tickFormatter);
39 | expect(wrapper.find('.tick.hidden').length).to.be.gt(1);
40 | });
41 |
42 | it('should support a custom tick filter', function(){
43 | const scale = d3.scale.linear();
44 | const tickFilter = sinon.spy(function(){ return true; });
45 | testAxis(scale, 'top', null, tickFilter);
46 | });
47 |
48 | });
49 |
--------------------------------------------------------------------------------
/test/Bar-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import sinon from 'sinon';
5 | import Bar from '../src/Bar';
6 |
7 | describe('', function() {
8 |
9 | const width = 100;
10 | const height = 200;
11 | const x = 0;
12 | const y = 10;
13 | const fill = '#0000';
14 | const data = [ ];
15 |
16 | it('renders a component', function() {
17 | const wrapper = shallow();
18 |
19 | const rect = wrapper.find('rect.bar');
20 | expect(rect).to.have.length(1);
21 | const props = rect.props();
22 | expect(props.x).to.equal(x);
23 | expect(props.y).to.equal(y);
24 | expect(props.width).to.equal(width);
25 | expect(props.height).to.equal(height);
26 | expect(props.fill).to.equal(fill);
27 | });
28 |
29 | it('supports mouse events', function() {
30 | const mouseEvent = { pageX: x, pageY: y };
31 | const handleMouseMove = sinon.spy();
32 | const handleMouseLeave = sinon.spy();
33 | data.push({ foo: 'bar' });
34 | const wrapper = shallow();
35 | const bar = wrapper.find('rect.bar');
36 | bar.simulate('mouseMove', mouseEvent);
37 | bar.simulate('mouseLeave', mouseEvent);
38 | expect(handleMouseMove.called).to.be.true;
39 | expect(handleMouseMove.args[0][0]).to.equal(mouseEvent);
40 | expect(handleMouseMove.args[0][1]).to.equal(data);
41 | expect(handleMouseLeave.calledOnce).to.be.true;
42 | expect(handleMouseLeave.args[0][0]).to.equal(mouseEvent);
43 | });
44 |
45 | });
46 |
47 |
--------------------------------------------------------------------------------
/test/BarChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import BarChart from '../src/BarChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function() {
11 | const data = [];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | });
19 |
20 | it('renders a with some data', function(){
21 | const data = [{
22 | label: 'Fruits',
23 | values: [{x: 'Apple', y: 10}, {x: 'Peaches', y: 4}, {x: 'Pumpkin Pie', y: 3}]
24 | }];
25 | const wrapper = shallow();
26 | const chart = wrapper.find('.chart');
27 | expect(chart).to.have.length(1);
28 | const props = chart.props();
29 | expect(props.height).to.equal(height);
30 | expect(props.width).to.equal(width);
31 | const html = wrapper.html();
32 | expect(html).to.match(/g class="x axis"/);
33 | expect(html).to.match(/rect class="bar"/);
34 | });
35 |
36 | it('should not blow up if no data is passed to it', function(){
37 | const wrapper = shallow();
38 | const chart = wrapper.find('.chart');
39 | expect(chart).to.have.length(1);
40 | expect(wrapper.html()).to.contain('No data available');
41 | });
42 |
43 | it('should not blow up if an empty array of data is passed to it', function(){
44 | const wrapper = shallow();
45 | const chart = wrapper.find('.chart');
46 | expect(chart).to.have.length(1);
47 | expect(wrapper.html()).to.contain('No data available');
48 | });
49 |
50 | it('should not blow up if an empty object of data is passed to it', function(){
51 | const wrapper = shallow();
52 | const chart = wrapper.find('.chart');
53 | expect(chart).to.have.length(1);
54 | expect(wrapper.html()).to.contain('No data available');
55 | });
56 | });
57 |
58 |
--------------------------------------------------------------------------------
/test/Chart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, render } from 'enzyme';
3 | import { expect } from 'chai';
4 | import Chart from '../src/Chart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 | const margin = {
10 | top: 0,
11 | left: 0,
12 | bottom: 0,
13 | right: 0
14 | };
15 |
16 | function testChartWithLegend(position){
17 | return function(){
18 | const legend = {
19 | position: position,
20 | data: [
21 | { 'apple': '#dddddd' },
22 | { 'peach': '#cdcdcd' }
23 | ]
24 | };
25 | const wrapper = render();
26 | expect(wrapper.find('g.legend')).to.have.length(position === 'both' ? 2 : 1);
27 | expect(wrapper.find('g.cells')).to.have.length(position === 'both' ? 2 : 1);
28 | expect(wrapper.find('g.cell')).to.have.length(position === 'both' ? 4 : 2);
29 | expect(wrapper.find('text.label')).to.have.length(position === 'both' ? 4 : 2);
30 | };
31 | }
32 |
33 | it('renders a component', function() {
34 | const wrapper = shallow();
35 | const chart = wrapper.find('svg');
36 | expect(chart).to.have.length(1);
37 | const props = chart.props();
38 | expect(props.height).to.equal(height);
39 | expect(props.height).to.equal(height);
40 | expect(props.width).to.equal(width);
41 | });
42 |
43 | it('renders a with a top legend', testChartWithLegend('top'));
44 | it('renders a with a bottom legend', testChartWithLegend('bottom'));
45 |
46 | });
47 |
--------------------------------------------------------------------------------
/test/DonutChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import DonutChart from '../src/DonutChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | });
19 |
20 | it('renders a with some data', function(){
21 | const data = {
22 | label: 'Apple',
23 | values: [{x: 'Apple', y: 10}, {x: 'Peaches', y: 4}, {x: 'Pumpkin', y: 3}]
24 | };
25 | const wrapper = shallow();
26 | const chart = wrapper.find('.chart');
27 | expect(chart).to.have.length(1);
28 | const props = chart.props();
29 | expect(props.height).to.equal(height);
30 | expect(props.width).to.equal(width);
31 | const html = wrapper.html();
32 | expect(html).to.match(/path class="arc"/);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/Legend-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import Legend from '../src/Legend';
5 |
6 | describe('', function() {
7 |
8 | it('renders an component', function() {
9 | const data = [
10 | { 'apple': '#dddddd' },
11 | { 'peach': '#cdcdcd' }
12 | ];
13 | const wrapper = shallow();
14 | expect(wrapper.find('g.legend')).to.have.length(1);
15 | expect(wrapper.find('g.cells')).to.have.length(1);
16 | expect(wrapper.find('g.cell')).to.have.length(2);
17 | expect(wrapper.find('text.label')).to.have.length(2);
18 | });
19 |
20 | });
21 |
22 |
--------------------------------------------------------------------------------
/test/LineChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import LineChart from '../src/LineChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [ { label: 'apples', values: [{ x: 0, y: 0 }] } ];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | const html = wrapper.html();
19 | expect(html).to.match(/g class="x axis"/);
20 | expect(html).to.match(/g class="y axis"/);
21 | expect(html).to.match(/path class="line"/);
22 | });
23 |
24 | it('should not blow up if no data is passed to it', function(){
25 | const wrapper = shallow();
26 | const chart = wrapper.find('.chart');
27 | expect(chart).to.have.length(1);
28 | expect(wrapper.html()).to.contain('No data available');
29 | });
30 |
31 | it('should not blow up if an empty array of data is passed to it', function(){
32 | const wrapper = shallow();
33 | const chart = wrapper.find('.chart');
34 | expect(chart).to.have.length(1);
35 | expect(wrapper.html()).to.contain('No data available');
36 | });
37 |
38 | it('should not blow up if an empty object of data is passed to it', function(){
39 | const wrapper = shallow();
40 | const chart = wrapper.find('.chart');
41 | expect(chart).to.have.length(1);
42 | expect(wrapper.html()).to.contain('No data available');
43 | });
44 |
45 | });
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test/NodeChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import NodeChart from '../src/NodeChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | });
19 |
20 | it('renders a with some data', function(){
21 |
22 | const nodeTree = {
23 | name: 'A',
24 | value: 20,
25 | imageUrl: 'https://placekitten.com/30/30',
26 | children: [
27 | {
28 | name: 'B',
29 | value: 15,
30 | imageUrl: 'https://placekitten.com/30/30',
31 | children: [
32 | {
33 | name: 'E',
34 | value: 9
35 | }
36 | ]
37 | },
38 | {
39 | name: 'C',
40 | value: 25,
41 | imageUrl: 'https://placekitten.com/30/30',
42 | children: null
43 | }
44 | ]
45 | };
46 |
47 | const wrapper = shallow();
48 | const chart = wrapper.find('.chart');
49 | expect(chart).to.have.length(1);
50 | const props = chart.props();
51 | expect(props.height).to.equal(height);
52 | expect(props.width).to.equal(width);
53 | const html = wrapper.html();
54 | expect(html).to.match(/circle class="node"/);
55 | });
56 |
57 | });
58 |
59 |
60 |
--------------------------------------------------------------------------------
/test/Path-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import sinon from 'sinon';
5 | import Path from '../src/Path';
6 | import d3 from 'd3/d3.min.js';
7 |
8 | describe('', function() {
9 |
10 | const stroke= 'gray';
11 | const data=[ {x: 0, y: 2}, {x: 1.3, y: 5}, {x: 3, y: 6}, {x: 3.5, y: 6.5}, {x: 4, y: 6}, {x: 4.5, y: 6}, {x: 5, y: 7}, {x: 5.5, y: 8} ];
12 | const line = d3.svg.line().x((d) => { return d.x; }).y((d) => { return d.y; }).interpolate('basis');
13 | const d = line(data);
14 |
15 | it('renders a component', function() {
16 | const wrapper = shallow();
17 | const path = wrapper.find('path.path');
18 | expect(path).to.have.length(1);
19 | const props = path.props();
20 | expect(props.d).to.equal(d);
21 | expect(props.stroke).to.equal(stroke);
22 | });
23 |
24 | it('supports mouse events', function() {
25 | const mouseEvent = { pageX: 0, pageY: 2};
26 | const handleMouseMove = sinon.spy();
27 | const handleMouseLeave = sinon.spy();
28 | const wrapper = shallow();
29 | const path = wrapper.find('path.path');
30 | path.simulate('mouseMove', mouseEvent);
31 | path.simulate('mouseLeave', mouseEvent);
32 | expect(handleMouseMove.called).to.be.true;
33 | expect(handleMouseMove.args[0][0]).to.equal(mouseEvent);
34 | expect(handleMouseMove.args[0][1]).to.equal(data);
35 | expect(handleMouseLeave.calledOnce).to.be.true;
36 | expect(handleMouseLeave.args[0][0]).to.equal(mouseEvent);
37 | });
38 |
39 | });
40 |
41 |
--------------------------------------------------------------------------------
/test/PieChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import PieChart from '../src/PieChart';
5 |
6 | describe('', function() {
7 | const height = 100;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [];
12 | const wrapper = shallow();
13 | const chart = wrapper.find('.chart');
14 | expect(chart).to.have.length(1);
15 | const props = chart.props();
16 | expect(props.height).to.equal(height);
17 | expect(props.width).to.equal(width);
18 | });
19 |
20 | it('renders a with some data', function(){
21 | const data = {
22 | label: 'Apple',
23 | values: [{x: 'Apple', y: 10}, {x: 'Peaches', y: 4}, {x: 'Pumpkin', y: 3}]
24 | };
25 | const wrapper = shallow();
26 | const chart = wrapper.find('.chart');
27 | expect(chart).to.have.length(1);
28 | const props = chart.props();
29 | expect(props.height).to.equal(height);
30 | expect(props.width).to.equal(width);
31 | const html = wrapper.html();
32 | expect(html).to.match(/path class="arc"/);
33 | });
34 |
35 | });
36 |
37 |
--------------------------------------------------------------------------------
/test/SparklineChart-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import SparklineChart from '../src/SparklineChart';
5 |
6 | describe('', function() {
7 | const height = 50;
8 | const width = 100;
9 |
10 | it('renders a component', function(){
11 | const data = [ { label: 'apples', values: [{ x: 0, y: 0 }] } ];
12 | const wrapper = shallow();
13 | const html = wrapper.html();
14 | expect(html).to.not.match(/g class="x axis"/);
15 | expect(html).to.not.match(/g class="y axis"/);
16 | expect(html).to.match(/path class="line"/);
17 | });
18 | });
19 |
20 |
--------------------------------------------------------------------------------
/test/Tooltip-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 | import Tooltip from '../src/Tooltip';
5 |
6 | describe('', function(){
7 |
8 | const top = 0;
9 | const left = 0;
10 |
11 | it('renders a component', function() {
12 | const wrapper = shallow();
13 | const tip = wrapper.find('div.tooltip');
14 | expect(tip).to.have.length(1);
15 | const props = tip.props();
16 | expect(props.style.top).to.equal(top);
17 | expect(props.style.left).to.equal(left);
18 | });
19 |
20 | it('should be possible to customize the tooltip class', function(){
21 | const wrapper = shallow();
22 | let tip = wrapper.find('div.tooltip');
23 | expect(tip).to.have.length(0);
24 | tip = wrapper.find('div.trolltip');
25 | expect(tip).to.have.length(1);
26 | });
27 | });
28 |
29 |
30 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | module.exports = {
3 | entry: [
4 | 'webpack/hot/only-dev-server',
5 | './demo/js/app.js'
6 | ],
7 | output: {
8 | path: './demo',
9 | filename: 'bundle.js'
10 | },
11 | module: {
12 | loaders: [
13 | { test: /\.js?$/, loaders: ['react-hot', 'babel'], exclude: /node_modules/ },
14 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'},
15 | { test: /\.css$/, loader: 'style!css' },
16 | { test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader' }
17 | ]
18 | },
19 | resolve: {
20 | extensions: ['', '.js', '.json' ]
21 | },
22 | plugins: [
23 | new webpack.NoErrorsPlugin()
24 | ]
25 | };
26 |
--------------------------------------------------------------------------------