├── .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 | 111 | { this.props.defs } 112 | {children} 113 | { 114 | hasLegend && 115 | } 116 | 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 | --------------------------------------------------------------------------------