├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jshintrc ├── .lintstagedrc ├── .npmignore ├── LICENSE ├── LICENSE.d3 ├── README.md ├── config ├── webpack.config.base.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── dist ├── react-d3-components.js ├── react-d3-components.js.map └── react-d3-components.min.js ├── example ├── index.html ├── react-d3-components.js └── style.css ├── package-lock.json ├── package.json └── src ├── AccessorMixin.jsx ├── AreaChart.jsx ├── ArrayifyMixin.jsx ├── Axis.jsx ├── Bar.jsx ├── BarChart.jsx ├── Brush.jsx ├── Chart.jsx ├── DefaultPropsMixin.jsx ├── DefaultScalesMixin.jsx ├── HeightWidthMixin.jsx ├── LineChart.jsx ├── Path.jsx ├── PieChart.jsx ├── ScatterPlot.jsx ├── StackAccessorMixin.jsx ├── StackDataMixin.jsx ├── Tooltip.jsx ├── TooltipMixin.jsx ├── Waveform.jsx └── index.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ 5 | "@babel/preset-stage-0", 6 | { 7 | "decoratorsLegacy": true 8 | } 9 | ], 10 | "@babel/preset-react" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "quotes": [2, "single"], 4 | "linebreak-style": [2, "unix"], 5 | "semi": [2, "always"], 6 | "no-extra-parens": [2, "all"], 7 | "curly": [2, "multi-line"], 8 | "no-multi-spaces": 2, 9 | "array-bracket-spacing": [2, "never"], 10 | "block-spacing": [2, "always"], 11 | "camelcase": 2, 12 | "comma-spacing": [2, { 13 | "before": false, 14 | "after": true 15 | }], 16 | "eol-last": 2, 17 | "func-call-spacing": [2, "never"], 18 | "jsx-quotes": [2, "prefer-double"], 19 | "keyword-spacing": 2, 20 | "no-trailing-spaces": 2, 21 | "semi-spacing": [2, { 22 | "before": false, 23 | "after": true 24 | }], 25 | "space-before-blocks": 2, 26 | "space-before-function-paren": [2, { 27 | "anonymous": "always", 28 | "named": "never" 29 | }], 30 | "space-infix-ops": 2, 31 | "arrow-body-style": [2, "as-needed"], 32 | "arrow-parens": [2, "as-needed"], 33 | "arrow-spacing": 2, 34 | "no-useless-rename": 2, 35 | "no-var": 2, 36 | "object-shorthand": [2, "always", { 37 | "avoidQuotes": true 38 | }], 39 | "prefer-const": 2, 40 | "template-curly-spacing": 2, 41 | "dot-location": [2, "property"], 42 | "no-multiple-empty-lines": [2, { 43 | "max": 1, 44 | "maxEOF": 1 45 | }], 46 | "react/display-name": 2, 47 | "react/jsx-no-duplicate-props": 2, 48 | "react/jsx-no-undef": 2, 49 | "react/jsx-uses-react": 2, 50 | "react/jsx-uses-vars": 2, 51 | "react/no-deprecated": 1, 52 | "react/no-direct-mutation-state": 2, 53 | "react/no-is-mounted": 2, 54 | "react/no-unknown-property": 2, 55 | "react/no-render-return-value": 2, 56 | "react/prop-types": 0, 57 | "react/react-in-jsx-scope": 2, 58 | "react/require-render-return": 2, 59 | "react/self-closing-comp": [2, { 60 | "component": true, 61 | "html": true 62 | }], 63 | "react/jsx-no-bind": [2, { 64 | "ignoreRefs": true, 65 | "allowArrowFunctions": true, 66 | "allowBind": false 67 | }], 68 | "react/jsx-equals-spacing": [2, "never"], 69 | "react/jsx-curly-spacing": [2, "never"] 70 | }, 71 | "parser": "babel-eslint", 72 | "parserOptions": { 73 | "ecmaVersion": 6, 74 | "sourceType": "module", 75 | "ecmaFeatures": { 76 | "jsx": true, 77 | "modules": true 78 | } 79 | }, 80 | "env": { 81 | "browser": true, 82 | "es6": true, 83 | "node": true 84 | }, 85 | "plugins": [ 86 | "react" 87 | ], 88 | "extends": ["eslint:recommended", "prettier"] 89 | } 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .DS_Store/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "browserify": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "funcscope": true, 10 | "immed": true, 11 | "indent": 4, 12 | "latedef": true, 13 | "singleGroups": true, 14 | "undef": true, 15 | "unused": true 16 | } -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/*.jsx": [ 3 | "eslint --fix", 4 | "git add" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | src 3 | .jshintrc 4 | .eslintrc 5 | .editorconfig 6 | .npmignore 7 | webpack.config.min.js 8 | webpack.config.js 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Neri Marschik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Looking for maintainers. If you are interested in maintaining this library please open an issue. 2 | 3 | # react-d3-components 4 | 5 | > D3 Components for React 6 | 7 | Let React have complete control over the DOM even when using D3. This way we can benefit from Reacts Virtual DOM. 8 | 9 | ![charts](http://codesuki.github.io/react-d3-components/charts.png) 10 | 11 | ## Table of Contents 12 | * [Installation](#installation) 13 | * [Development](#development) 14 | * [Description](#description) 15 | * [Documentation](#documentation) 16 | * [Features](#features) 17 | * [Todo](#todo) 18 | * [Changelog](#changelog) 19 | * [Examples](#examples) 20 | * [Bar Chart](#barchart) 21 | * [Brush](#brush) 22 | * [Tooltips](#tooltips) 23 | * [Axis parameters](#axis-parameters) 24 | * [Custom accessors](#custom-accessors) 25 | * [Customization](#overriding-default-parameters) 26 | * [Stacked Bar Chart](#stackedbarchart) 27 | * [Grouped Bar Chart](#groupedbarchart) 28 | * [Scatter, Line and Area Charts](#other-charts) 29 | * [Pie Chart](#piechart) 30 | * [Waveform](#waveform) 31 | 32 | ## Installation 33 | ``` 34 | npm install react-d3-components 35 | ``` 36 | 37 | ## Development 38 | In order to compile the code, from the repository folder, type in your terminal 39 | ``` 40 | npm install & npm run build:js 41 | ``` 42 | This will install the dependencies required and run the build:js. At the end of the process the compiled .js and min.js will be available in the dist folder. 43 | Examples are available in the folder example. 44 | From the root folder type 45 | ``` 46 | python -m SimpleHTTPServer 8000 47 | ``` 48 | to start a webserver, and navigate to http://localhost:8000/example in order to visualize the new example page. 49 | Note that the example page directly points to the dist folder. 50 | 51 | 52 | ## Description 53 | Ideally the library should be usable with minimum configuration. Just put the data in and see the charts. 54 | I try to provide sensible defaults, but since for most use-cases we need to customize D3's parameters they will be made accessible to the user. Most Charts will turn into their stacked variant when given an array as input. 55 | 56 | If you like the project please consider starring and a pull request. I am open for any additions. 57 | 58 | ## Documentation 59 | Documentation is in the [Wiki](https://github.com/codesuki/react-d3-components/wiki). 60 | For quick testing the [examples](#examples) might be enough. 61 | 62 | ## Features 63 | * Brushes 64 | * Tooltips 65 | * Custom accessors to support any data format 66 | * Negative axes 67 | * CSS classes to allow styling 68 | * Bar Chart 69 | * Stacked Bar Chart 70 | * Scatter Plot 71 | * Line Chart 72 | * Area Chart 73 | * Stacked Area Chart 74 | * Pie Plot 75 | 76 | ## Todo 77 | * More Charts 78 | * Animations 79 | * Legend 80 | * Documentation 81 | * Tests 82 | 83 | ## Changelog 84 | * 0.6.6: Fix ticks rendering over bar chart 85 | * 0.6.5: 86 | * Add tickDirection property to Axis 87 | * Add hideLabels property to PieChart 88 | * Add yOrientation property to AreaChart 89 | * Fix Line Chart rendering 90 | * 0.6.4: Fixed react dependency version (was 0.15.0 instead of 15.0.0) 91 | * 0.6.3: Changed react dependency version to >=0.14.0 to allow react 0.15. 92 | * 0.6.2: Fixed build system. Added colorByLabel property to BarChart. 93 | * 0.6.1: Fixed 'BarChart.getDOMNode(...) is deprecated.' 94 | * 0.6.0: Added [Waveform Chart](#waveform). Moved to React 0.14. 95 | * 0.5.2: Fixed default scale for dates 96 | * 0.5.1: Fixed new props not being used by DefaultScalesMixin 97 | * 0.5.0: 98 | * Improved tooltip. (see example below) 99 | * Tooltip now has different modes. 100 | * AreaChart tooltip now contains x-value argument. 101 | * Support for grouped bar charts. (see example below) 102 | * Support to include child elements inside charts. 103 | * Several bug fixes including recent pull requests. 104 | * React is now a peer dependency 105 | * 0.4.8: Fixed bug were graphs don't resize correctly. 106 | * 0.4.7: Moved to React 0.13.1 107 | * 0.4.6: 108 | * Added sort property to PieChart, same usage as d3.Pie.sort(). 109 | * Added support for strokeWidth, strokeDasharray, strokeLinecap to LineChart, can be string or function. 110 | * Small bug fixes. 111 | * 0.4.5: Fixed tooltip not showing when mouse is over tooltip symbol. Tooltip will soon be revamped to allow custom components. 112 | * 0.4.4: Fixed tooltip position inside relative layout containers. Moved to webpack. 113 | * 0.4.3: Fixed tooltip not showing in Safari. 114 | * 0.4.2: Improved LineChart tooltip to show d3.svg.symbol() on nearest data point. Can be customized with shape and shapeColor props. LineChart toolip callback is callback(label, value) now where the format of value depends on your data format, default is {x: x, y: y}. 115 | * 0.4.1: Fixed compatibility issues with Safari and possible other browsers not supporting Number.isFinite(). 116 | * 0.4.0: Added X-Axis Brush. Functioning but might change to improve ease of usage etc. Fixed Axis tickFormat not being set correctly. 117 | * 0.3.6: Fixed not correctly requiring D3. 118 | * 0.3.5: Fixed npm packaging. 119 | * 0.3.4: Charts now render correctly when included in another component. Line chart default axes now meet at 0. 120 | * 0.3.0: Added tooltips and made axis properties accessible. 121 | * 0.2.2: Fixed accessors not being used everywhere 122 | * 0.2.1: Excluded external libraries from build and make it usable as a browser include 123 | * 0.2.0: Custom accessors, stacked charts, default scales 124 | 125 | ## Examples 126 | Check out example/index.html found [here](http://codesuki.github.io/react-d3-components/example.html). 127 | 128 | ### BarChart 129 | ```javascript 130 | var BarChart = ReactD3.BarChart; 131 | 132 | var data = [{ 133 | label: 'somethingA', 134 | values: [{x: 'SomethingA', y: 10}, {x: 'SomethingB', y: 4}, {x: 'SomethingC', y: 3}] 135 | }]; 136 | 137 | React.render( 138 | , 143 | document.getElementById('location') 144 | ); 145 | ``` 146 | 147 | ![barchart](http://codesuki.github.io/react-d3-components/barchart.png) 148 | 149 | ### Brush 150 | With Brushes we can build interactive Graphs. My personal use-case is to select date ranges as shown below and in the example. 151 | The Brush feature is still incomplete, for now only offering a x-Axis Brush but y-Axis will follow soon as well as refactoring. 152 | For now the Brush is rendered in its own SVG, this allows flexible use but might change in the future, or become optional. 153 | Also there is no Brush support for the built-in default Scales. 154 | ```css 155 | .brush .extent { 156 | stroke: #000; 157 | fill-opacity: .125; 158 | shape-rendering: crispEdges; 159 | } 160 | 161 | .brush .background { 162 | fill: #ddd; 163 | } 164 | ``` 165 | ```javascript 166 | var LineChart = ReactD3.LineChart; 167 | var Brush = ReactD3.Brush; 168 | 169 | var SomeComponent = React.createClass({ 170 | getInitialState: function() { 171 | return { 172 | data: {label: '', values: [ 173 | {x: new Date(2015, 2, 5), y: 1}, 174 | {x: new Date(2015, 2, 6), y: 2}, 175 | {x: new Date(2015, 2, 7), y: 0}, 176 | {x: new Date(2015, 2, 8), y: 3}, 177 | {x: new Date(2015, 2, 9), y: 2}, 178 | {x: new Date(2015, 2, 10), y: 3}, 179 | {x: new Date(2015, 2, 11), y: 4}, 180 | {x: new Date(2015, 2, 12), y: 4}, 181 | {x: new Date(2015, 2, 13), y: 1}, 182 | {x: new Date(2015, 2, 14), y: 5}, 183 | {x: new Date(2015, 2, 15), y: 0}, 184 | {x: new Date(2015, 2, 16), y: 1}, 185 | {x: new Date(2015, 2, 16), y: 1}, 186 | {x: new Date(2015, 2, 18), y: 4}, 187 | {x: new Date(2015, 2, 19), y: 4}, 188 | {x: new Date(2015, 2, 20), y: 5}, 189 | {x: new Date(2015, 2, 21), y: 5}, 190 | {x: new Date(2015, 2, 22), y: 5}, 191 | {x: new Date(2015, 2, 23), y: 1}, 192 | {x: new Date(2015, 2, 24), y: 0}, 193 | {x: new Date(2015, 2, 25), y: 1}, 194 | {x: new Date(2015, 2, 26), y: 1} 195 | ]}, 196 | xScale: d3.time.scale().domain([new Date(2015, 2, 5), new Date(2015, 2, 26)]).range([0, 400 - 70]), 197 | xScaleBrush: d3.time.scale().domain([new Date(2015, 2, 5), new Date(2015, 2, 26)]).range([0, 400 - 70]) 198 | }; 199 | }, 200 | 201 | render: function() { 202 | return ( 203 |
204 | 212 |
213 | 222 |
223 |
224 | ); 225 | }, 226 | 227 | _onChange: function(extent) { 228 | this.setState({xScale: d3.time.scale().domain([extent[0], extent[1]]).range([0, 400 - 70])}); 229 | } 230 | }); 231 | ``` 232 | ![brush](http://codesuki.github.io/react-d3-components/brush.png) 233 | 234 | ### Tooltips 235 | You can provide a callback to every chart that will return html for the tooltip. 236 | Depending on the type of chart the callback will receive different parameters that are useful. 237 | 238 | * Bar Chart: label is the first parameter. y0, y of the hovered bar and the total bar height in case of a stacked bar chart. 239 | * Scatter Plot: x, y of the hovered point. 240 | * Pie Chart: label is the first parameter. y of the hovered wedge. 241 | * Area Chart: closest y value to the cursor of the area under the mouse and the cumulative y value in case of a stacked area chart. x value is the third parameter. label is the fourth parameter. 242 | 243 | Example Scatter Plot: 244 | ```javascript 245 | var tooltipScatter = function(x, y) { 246 | return "x: " + x + " y: " + y; 247 | }; 248 | 249 | React.render(, 257 | document.getElementById('scatterplot') 258 | ); 259 | ``` 260 | 261 | Tooltip positioning is influenced by `tooltipOffset` `tooltipContained` and `tooltipMode`, which has 3 options `mouse`, `fixed`, `element`. 262 | 263 | * `mouse` is the default behavior and just follows the mouse 264 | * `fixed` uses `tooltipOffset` as an offset from the top left corner of the svg 265 | * `element` puts the tooltip on top of data points for line/area/scatter charts and on top of bars for the barchart 266 | 267 | `tooltipOffset` is an object with `top` and `left` keys i.e. `{top: 10, left: 10}` 268 | 269 | If `tooltipContained` is true the tooltip will try to stay inside the svg by using `css-transform`. 270 | 271 | ![tooltip](http://codesuki.github.io/react-d3-components/tooltip.png) 272 | 273 | ### Axis parameters 274 | All D3 axis parameters can optionally be provided to the chart. For detailed explanation please check the documentation. 275 | ```javascript 276 | 277 | React.render(, 286 | document.getElementById('linechart')); 287 | ``` 288 | 289 | The following are the default values. 290 | ```javascript 291 | { 292 | tickArguments: [10], 293 | tickValues: null, 294 | tickFormat: x => { return x; }, 295 | innerTickSize: 6, 296 | tickPadding: 3, 297 | outerTickSize: 6, 298 | className: "axis", 299 | zero: 0, 300 | label: "" 301 | } 302 | ``` 303 | 304 | ### Custom Accessors 305 | ```javascript 306 | data = [{ 307 | customLabel: 'somethingA', 308 | customValues: [[0, 3], [1.3, -4], [3, 7], [-3.5, 8], [4, 7], [4.5, 7], [5, -7.8]] 309 | }]; 310 | 311 | var labelAccessor = function(stack) { return stack.customLabel; }; 312 | var valuesAccessor = function(stack) { return stack.customValues; }; 313 | var xAccessor = function(element) { return element[0]; }; 314 | var yAccessor = function(element) { return element[1]; }; 315 | 316 | React.render(, 326 | document.getElementById('location')); 327 | ``` 328 | 329 | ### Overriding default parameters 330 | All Charts provide defaults for scales, colors, etc... 331 | If you want to use your own scale just pass it to the charts constructor. 332 | 333 | The scales are normal D3 objects, their documentation can be found [here](https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md) and [here](https://github.com/d3/d3-3.x-api-reference/blob/master/Quantitative-Scales.md). 334 | 335 | There are more parameters like barPadding, strokeWidth, fill, opacity, etc. please check the documentation for details. 336 | 337 | ```javascript 338 | var xScale = d3.scale.ordinal(); //... + set it up appropriately 339 | var yScale = d3.scale.linear(); 340 | var colorScale = d3.scale.category20(); 341 | 342 | 350 | ``` 351 | 352 | #### LineChart stroke style 353 | You can customize the line style of LineCharts with CSS or if you want to have more control over how each line in your dataset gets rendered you can use the stroke property of LineChart as follows. Note that you do not have to set all the properties in the object. 354 | 355 | ```javascript 356 | var dashFunc = function(label) { 357 | if (label == "somethingA") { 358 | return "4 4 4"; 359 | } 360 | if (label == "somethingB") { 361 | return "3 4 3"; 362 | } 363 | } 364 | 365 | var widthFunc = function(label) { 366 | if (label == "somethingA") { 367 | return "4"; 368 | } 369 | if (label == "somethingB") { 370 | return "2"; 371 | } 372 | } 373 | 374 | var linecapFunc = function(label) { 375 | if (label == "somethingA") { 376 | return "round"; 377 | } 378 | } 379 | 380 | React.render(, 391 | document.getElementById('linechart') 392 | ); 393 | ``` 394 | ![strokestyle](http://codesuki.github.io/react-d3-components/strokestyle.png) 395 | 396 | ### StackedBarChart 397 | ```javascript 398 | var BarChart = ReactD3.BarChart; 399 | 400 | data = [ 401 | { 402 | label: 'somethingA', 403 | values: [{x: 'SomethingA', y: 10}, {x: 'SomethingB', y: 4}, {x: 'SomethingC', y: 3}] 404 | }, 405 | { 406 | label: 'somethingB', 407 | values: [{x: 'SomethingA', y: 6}, {x: 'SomethingB', y: 8}, {x: 'SomethingC', y: 5}] 408 | }, 409 | { 410 | label: 'somethingC', 411 | values: [{x: 'SomethingA', y: 6}, {x: 'SomethingB', y: 8}, {x: 'SomethingC', y: 5}] 412 | } 413 | ]; 414 | 415 | React.render(, 420 | document.getElementById('location')); 421 | ``` 422 | 423 | ![stackedbarchart](http://codesuki.github.io/react-d3-components/stackedbarchart.png) 424 | 425 | ### Grouped Bar Chart 426 | ```javascript 427 | var BarChart = ReactD3.BarChart; 428 | 429 | data = [ 430 | { 431 | label: 'somethingA', 432 | values: [{x: 'SomethingA', y: 10}, {x: 'SomethingB', y: 4}, {x: 'SomethingC', y: 3}] 433 | }, 434 | { 435 | label: 'somethingB', 436 | values: [{x: 'SomethingA', y: 6}, {x: 'SomethingB', y: 8}, {x: 'SomethingC', y: 5}] 437 | }, 438 | { 439 | label: 'somethingC', 440 | values: [{x: 'SomethingA', y: 6}, {x: 'SomethingB', y: 8}, {x: 'SomethingC', y: 5}] 441 | } 442 | ]; 443 | 444 | React.render(, 450 | document.getElementById('location')); 451 | ``` 452 | 453 | ![groupedbarchart](http://codesuki.github.io/react-d3-components/groupedbarchart.png) 454 | 455 | ### Other Charts 456 | ```javascript 457 | var ScatterPlot = ReactD3.ScatterPlot; 458 | var LineChart = ReactD3.LineChart; 459 | var AreaChart = ReactD3.AreaChart; 460 | 461 | data = [ 462 | { 463 | label: 'somethingA', 464 | 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}] 465 | }, 466 | { 467 | label: 'somethingB', 468 | values: [{x: 0, y: 3}, {x: 1.3, y: 4}, {x: 3, y: 7}, {x: 3.5, y: 8}, {x: 4, y: 7}, {x: 4.5, y: 7}, {x: 5, y: 7.8}, {x: 5.5, y: 9}] 469 | } 470 | ]; 471 | 472 | React.render(, 477 | document.getElementById('location')); 478 | 479 | React.render(, 484 | document.getElementById('location') 485 | ); 486 | 487 | React.render(, 493 | document.getElementById('location') 494 | ); 495 | ``` 496 | 497 | ![scatterplot](http://codesuki.github.io/react-d3-components/scatterplot.png) 498 | 499 | ![linechart](http://codesuki.github.io/react-d3-components/linechart.png) 500 | 501 | ![areachart](http://codesuki.github.io/react-d3-components/areachart.png) 502 | 503 | ### PieChart 504 | By default d3 sorts the PieChart but you can use the sort property to pass a custom comparator or null to disable sorting. 505 | 506 | ```javascript 507 | var PieChart = ReactD3.PieChart; 508 | 509 | var data = { 510 | label: 'somethingA', 511 | values: [{x: 'SomethingA', y: 10}, {x: 'SomethingB', y: 4}, {x: 'SomethingC', y: 3}] 512 | }; 513 | 514 | var sort = null; // d3.ascending, d3.descending, func(a,b) { return a - b; }, etc... 515 | 516 | React.render(, 523 | document.getElementById('location') 524 | ); 525 | ``` 526 | 527 | ![piechart](http://codesuki.github.io/react-d3-components/piechart.png) 528 | 529 | ### Waveform 530 | The waveform chart displays a sequence of values as if they were part of an audio waveform. 531 | The values are centered on the horizontal axis and reflected along the horizontal origin. 532 | For now only values in the range [0,1] are supported. 533 | 534 | The graph can accept a colorScale parameter, that is an array of values in the range [0,width], where width is the width of the graph. 535 | Following an example of gradient from white to black for a waveform of width 200. 536 | 537 | ```javascript 538 | colorScale={ d3.scale.linear() 539 | .domain([0,200]) 540 | .range(['#fff','#000'])} 541 | ``` 542 | The graph is responsive and adopts a viewBox strategy to resize the graph maintaining its proportions. 543 | We also adopt subSampling in order to maintain the graph rapresentation of the waveform. 544 | As it is now each bar needs to have a minimum width of 1px, as well as 1px space between to adjacent bars. 545 | In order to allow this, we subsample the input data in order to have exactly a maximum of width/2 elements. 546 | 547 | It is therefore a good strategy to select the width of the graph to be twice the length of the dataset. The viewBox responsiveness will then resize the graph to the width of the container. 548 | If the samples are less than half of the space available we just display them with a width >1px. Space between bars is increased in width as well. 549 | 550 | ![waveform](http://codesuki.github.io/react-d3-components/waveform.png) 551 | 552 | 553 | 554 | ## Credits 555 | This library uses parts of [D3.js](https://github.com/mbostock/d3). 556 | -------------------------------------------------------------------------------- /config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, '../src/index.jsx'), 5 | output: { 6 | library: 'ReactD3', 7 | libraryTarget: 'umd', 8 | path: path.resolve(__dirname, '../dist') 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /.jsx$/, 14 | loader: 'babel-loader', 15 | include: path.resolve(__dirname, '../src') 16 | } 17 | ] 18 | }, 19 | resolve: { 20 | extensions: ['.js', '.jsx'] 21 | }, 22 | externals: { 23 | d3: true, 24 | react: 'React', 25 | 'react-dom': 'ReactDOM' 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | 3 | module.exports = merge(require('./webpack.config.base'), { 4 | mode: 'development', 5 | devtool: 'eval', 6 | output: { 7 | filename: 'react-d3-components.js' 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const merge = require('webpack-merge'); 3 | const { 4 | optimize: { OccurrenceOrderPlugin, AggressiveMergingPlugin } 5 | } = require('webpack'); 6 | 7 | module.exports = merge(require('./webpack.config.base'), { 8 | mode: 'production', 9 | devtool: 'source-map', 10 | output: { 11 | filename: 'react-d3-components.min.js', 12 | sourceMapFilename: 'react-d3-components.js.map' 13 | }, 14 | plugins: [ 15 | new OccurrenceOrderPlugin(), 16 | new AggressiveMergingPlugin(), 17 | new UglifyJsPlugin({ 18 | sourceMap: true, 19 | uglifyOptions: { 20 | compress: { 21 | warnings: false 22 | } 23 | } 24 | }) 25 | ] 26 | }); 27 | -------------------------------------------------------------------------------- /dist/react-d3-components.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React"),require("d3"),require("ReactDOM")):"function"==typeof define&&define.amd?define(["React","d3","ReactDOM"],t):"object"==typeof exports?exports.ReactD3=t(require("React"),require("d3"),require("ReactDOM")):e.ReactD3=t(e.React,e.d3,e.ReactDOM)}(window,function(e,t,n){return function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=19)}([function(e,t,n){e.exports=n(21)()},function(t,n){t.exports=e},function(e,t,n){"use strict";var a=n(1),r=n(24);if(void 0===a)throw Error("create-react-class could not find the React object. If you are using script tags, make sure that React is being loaded before create-react-class.");var i=(new a.Component).updater;e.exports=r(a.Component,a.isValidElement,i)},function(e,n){e.exports=t},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=o(n(1)),r=o(n(0)),i=o(n(2));function o(e){return e&&e.__esModule?e:{default:e}}var l=r.default.number,s=r.default.shape,u=(0,i.default)({displayName:"Chart",propTypes:{height:l.isRequired,width:l.isRequired,margin:s({top:l,bottom:l,left:l,right:l}).isRequired},render:function(){var e=this.props,t=e.width,n=e.height,r=e.margin,i=e.viewBox,o=e.preserveAspectRatio,l=e.children;return a.default.createElement("svg",{ref:"svg",width:t,height:n,viewBox:i,preserveAspectRatio:o},a.default.createElement("g",{transform:"translate(".concat(r.left,", ").concat(r.top,")")},l))}});t.default=u},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={componentWillMount:function(){this._calculateInner(this.props)},componentWillReceiveProps:function(e){this._calculateInner(e)},_calculateInner:function(e){var t=e.height,n=e.width,a=e.margin;this._innerHeight=t-a.top-a.bottom,this._innerWidth=n-a.left-a.right}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=i(n(0)),r=i(n(3));function i(e){return e&&e.__esModule?e:{default:e}}var o=a.default.oneOfType,l=a.default.object,s=a.default.array,u=a.default.shape,c=a.default.func,d=a.default.number,f={propTypes:{data:o([l,s]).isRequired,height:d.isRequired,width:d.isRequired,margin:u({top:d,bottom:d,left:d,right:d}),xScale:c,yScale:c,colorScale:c},getDefaultProps:function(){return{data:{label:"No data available",values:[{x:"No data available",y:1}]},margin:{top:0,bottom:0,left:0,right:0},xScale:null,yScale:null,colorScale:r.default.scale.category20()}}};t.default=f},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=i(n(0)),r=i(n(17));function i(e){return e&&e.__esModule?e:{default:e}}var o=a.default.func,l=a.default.oneOf,s=a.default.bool,u=a.default.objectOf,c=a.default.number,d={propTypes:{tooltipHtml:o,tooltipMode:l(["mouse","element","fixed"]),tooltipContained:s,tooltipOffset:u(c)},getInitialState:function(){return{tooltip:{hidden:!0}}},getDefaultProps:function(){return{tooltipMode:"mouse",tooltipOffset:{top:-35,left:0},tooltipHtml:null,tooltipContained:!1}},componentDidMount:function(){this._svgNode=r.default.findDOMNode(this).getElementsByTagName("svg")[0]},onMouseEnter:function(e,t){if(this.props.tooltipHtml){e.preventDefault();var n,a=this.props,r=a.margin,i=a.tooltipMode,o=a.tooltipOffset,l=a.tooltipContained,s=this._svgNode;if(s.createSVGPoint){var u=s.createSVGPoint();u.x=e.clientX,u.y=e.clientY,n=[(u=u.matrixTransform(s.getScreenCTM().inverse())).x-r.left,u.y-r.top]}else{var c=s.getBoundingClientRect();n=[e.clientX-c.left-s.clientLeft-r.left,e.clientY-c.top-s.clientTop-r.top]}var d=function(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=[],a=!0,r=!1,i=void 0;try{for(var o,l=e[Symbol.iterator]();!(a=(o=l.next()).done)&&(n.push(o.value),!t||n.length!==t);a=!0);}catch(e){r=!0,i=e}finally{try{a||null==l.return||l.return()}finally{if(r)throw i}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}(this._tooltipHtml(t,n),3),f=d[0],p=d[1],h=d[2],m=s.getBoundingClientRect().top+r.top,v=s.getBoundingClientRect().left+r.left,y=0,g=0;"fixed"===i?(y=m+o.top,g=v+o.left):"element"===i?(y=m+h+o.top,g=v+p+o.left):(y=e.clientY+o.top,g=e.clientX+o.left);var x=50;l&&(x=function(e,t,n){return 0*(1-e)+100*e}(n[0]/s.getBoundingClientRect().width)),this.setState({tooltip:{top:y,left:g,hidden:!1,html:f,translate:x}})}},onMouseLeave:function(e){this.props.tooltipHtml&&(e.preventDefault(),this.setState({tooltip:{hidden:!0}}))}};t.default=d},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=o(n(1)),r=o(n(0)),i=o(n(2));function o(e){return e&&e.__esModule?e:{default:e}}var l=r.default.array,s=r.default.func,u=r.default.oneOf,c=r.default.number,d=r.default.string,f=(0,i.default)({displayName:"Axis",propTypes:{tickArguments:l,tickValues:l,tickFormat:s,tickDirection:u(["horizontal","vertical","diagonal"]),innerTickSize:c,tickPadding:c,outerTickSize:c,scale:s.isRequired,className:d,zero:c,orientation:u(["top","bottom","left","right"]).isRequired,label:d},getDefaultProps:function(){return{tickArguments:[10],tickValues:null,tickFormat:null,tickDirection:"horizontal",innerTickSize:6,tickPadding:3,outerTickSize:6,className:"axis",zero:0,label:""}},_getTranslateString:function(){var e=this.props,t=e.orientation,n=e.height,a=e.width,r=e.zero;return"top"===t?"translate(0, ".concat(r,")"):"bottom"===t?"translate(0, ".concat(0==r?n:r,")"):"left"===t?"translate(".concat(r,", 0)"):"right"===t?"translate(".concat(0==r?a:r,", 0)"):""},render:function(){var e=this.props,t=e.height,n=e.tickArguments,r=e.tickValues,i=e.tickDirection,o=e.innerTickSize,l=e.tickPadding,s=e.outerTickSize,u=e.scale,c=e.orientation,d=e.zero,f=this.props,p=f.width,h=f.className,m=f.label,v=this.props.tickFormat,y=null==r?u.ticks?u.ticks.apply(u,n):u.domain():r;v||(v=u.tickFormat?u.tickFormat.apply(u,n):function(e){return e}),d!=t&&d!=p&&0!=d&&(y=y.filter(function(e){return 0!=e}));var g,x,_,E,M,b,S,R,D,w=Math.max(o,0)+l,P="top"===c||"left"===c?-1:1,N=this._d3ScaleRange(u),k=u.rangeBand?function(e){return u(e)+u.rangeBand()/2}:u,O=0;"bottom"===c||"top"===c?(g="translate({}, 0)",x=0,_=P*w,E=0,M=P*o,b=P<0?"0em":".71em",S="middle",R="M".concat(N[0],", ").concat(P*s,"V0H").concat(N[1],"V").concat(P*s),"vertical"===i?(O=-90,x=-w,_=-o,S="end"):"diagonal"===i&&(O=-60,x=-w,_=0,S="end"),D=a.default.createElement("text",{className:"".concat(h," label"),textAnchor:"end",x:p,y:-6},m)):(g="translate(0, {})",x=P*w,_=0,E=P*o,M=0,b=".32em",S=P<0?"end":"start",R="M".concat(P*s,", ").concat(N[0],"H0V").concat(N[1],"H").concat(P*s),"vertical"===i?(O=-90,x-=P*w,_=-(w+o),S="middle"):"diagonal"===i&&(O=-60,x-=P*w,_=-(w+o),S="middle"),D=a.default.createElement("text",{className:"".concat(h," label"),textAnchor:"end",y:6,dy:"left"===c?".75em":"-1.25em",transform:"rotate(-90)"},m));var A=y.map(function(e,t){var n=k(e),r=g.replace("{}",n);return a.default.createElement("g",{key:"".concat(e,".").concat(t),className:"tick",transform:r},a.default.createElement("line",{x2:E,y2:M,stroke:"#aaa"}),a.default.createElement("text",{x:x,y:_,dy:b,textAnchor:S,transform:"rotate(".concat(O,")")},v(e)))}),j=a.default.createElement("path",{className:"domain",d:R,fill:"none",stroke:"#aaa"}),T=a.default.createElement("rect",{className:"axis-background",fill:"none"});return a.default.createElement("g",{ref:"axis",className:h,transform:this._getTranslateString(),style:{shapeRendering:"crispEdges"}},T,A,j,D)},_d3ScaleExtent:function(e){var t=e[0],n=e[e.length-1];return th/2?(p[0].values=function(e,t){for(var n=[],a=e.length,r=a/t,i=0;i=10*Math.PI/180;return a.default.createElement("g",{key:"".concat(o(t.data),".").concat(l(t.data),".").concat(n),className:"arc"},a.default.createElement(_,{data:t.data,fill:i(o(t.data)),d:r(t),onMouseEnter:s,onMouseLeave:u}),!c&&!!t.value&&d&&e.renderLabel(t))});return a.default.createElement("g",null,d)},midAngle:function(e){return e.startAngle+(e.endAngle-e.startAngle)/2}}),M=(0,i.default)({displayName:"PieChart",mixins:[u.default,c.default,d.default,f.default],propTypes:{innerRadius:v,outerRadius:v,labelRadius:v,padRadius:h,cornerRadius:v,sort:x,hideLabels:y},getDefaultProps:function(){return{innerRadius:null,outerRadius:null,labelRadius:null,padRadius:"auto",cornerRadius:0,sort:void 0,hideLabels:!1}},_tooltipHtml:function(e){return[this.props.tooltipHtml(this.props.x(e),this.props.y(e)),0,0]},render:function(){var e=this.props,t=e.data,n=e.width,r=e.height,i=e.margin,u=e.viewBox,c=e.preserveAspectRatio,d=e.colorScale,f=e.padRadius,p=e.cornerRadius,h=e.sort,m=e.x,v=e.y,y=e.values,g=e.hideLabels,x=this.props,_=x.innerRadius,M=x.outerRadius,b=x.labelRadius,S=this._innerWidth,R=this._innerHeight,D=o.default.layout.pie().value(function(e){return v(e)});void 0!==h&&(D=D.sort(h));var w=Math.min(S,R)/2;_||(_=.8*w),M||(M=.4*w),b||(b=.9*w);var P=o.default.svg.arc().innerRadius(_).outerRadius(M).padRadius(f).cornerRadius(p),N=o.default.svg.arc().innerRadius(b).outerRadius(b),k=D(y(t)),O="translate(".concat(S/2,", ").concat(R/2,")");return a.default.createElement("div",null,a.default.createElement(l.default,{height:r,width:n,margin:i,viewBox:u,preserveAspectRatio:c},a.default.createElement("g",{transform:O},a.default.createElement(E,{width:S,height:R,colorScale:d,pie:k,arc:P,outerArc:N,radius:w,x:m,y:v,onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave,hideLabels:g})),this.props.children),a.default.createElement(s.default,this.state.tooltip))}});t.default=M},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=v(n(1)),r=v(n(0)),i=v(n(2)),o=v(n(3)),l=v(n(4)),s=v(n(8)),u=v(n(9)),c=v(n(6)),d=v(n(5)),f=v(n(10)),p=v(n(14)),h=v(n(11)),m=v(n(7));function v(e){return e&&e.__esModule?e:{default:e}}function y(){return(y=Object.assign||function(e){for(var t=1;tthis.state.xExtent[1]?(this.setState({xExtent:[this.state.xExtent[1],a],xExtentDomain:null}),this._resizeDir="e"):this.setState({xExtent:[a,this.state.xExtent[1]],xExtentDomain:null}):"e"==this._resizeDir&&(a 2 | 3 | 4 | React D3 Components Examples 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 489 | 490 | 491 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | } 4 | 5 | div { 6 | float: left; 7 | } 8 | #waveformchart svg { 9 | position: absolute; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | #waveformchart { 14 | width: 100%; 15 | height: 140px; 16 | background-color: #333; 17 | position: relative; 18 | margin-bottom: 20px; 19 | } 20 | .axis { 21 | fill: none; 22 | stroke: #000; 23 | shape-rendering: crispEdges; 24 | } 25 | 26 | .axis text { 27 | font: 11px sans-serif; 28 | fill: #000; 29 | stroke: none; 30 | font-weight: bold; 31 | } 32 | 33 | .axis line { 34 | stroke: #ccc; 35 | } 36 | 37 | .axis path { 38 | stroke: #ccc; 39 | } 40 | #waveformchart .bar{ 41 | -ms-transform: scale(1,1); /* IE 9 */ 42 | -webkit-transform: scale(1,1); /* Safari */ 43 | transform: scale(1,1); 44 | } 45 | 46 | #waveformchart .bar:hover { 47 | height: 100%; 48 | -ms-transform: scale(2,100); /* IE 9 */ 49 | -webkit-transform: scale(2,100); /* Safari */ 50 | transform: scale(2,100); 51 | -ms-transform-origin: 50% 50%; /* IE 9 */ 52 | -webkit-transform-origin: 50% 50%; /* Chrome, Safari, Opera */ 53 | transform-origin: 50% 50%; 54 | } 55 | 56 | .bar:hover { 57 | fill: #E55; 58 | } 59 | 60 | .dot:hover { 61 | fill: #E55; 62 | } 63 | 64 | .area:hover { 65 | fill: #E55; 66 | } 67 | 68 | .line { 69 | padding: 10px; 70 | } 71 | 72 | .line:hover { 73 | stroke: #E55; 74 | } 75 | 76 | .arc path:hover { 77 | fill: #E55; 78 | } 79 | 80 | .arc text { 81 | font: 11px sans-serif; 82 | fill: #000; 83 | stroke: none; 84 | font-weight: bold; 85 | } 86 | 87 | .tooltip { 88 | padding: 3px; 89 | border: 2px solid; 90 | border-radius: 4px; 91 | background-color: #eee; 92 | opacity: 0.6; 93 | justify-content: center; 94 | align-items: center; 95 | } 96 | 97 | .brush .extent { 98 | stroke: #000; 99 | fill-opacity: .125; 100 | shape-rendering: crispEdges; 101 | } 102 | 103 | .brush .background { 104 | fill: #ddd; 105 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-d3-components", 3 | "version": "0.9.1", 4 | "description": "D3 components for React", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/codesuki/react-d3-components.git" 9 | }, 10 | "keywords": [ 11 | "d3", 12 | "react", 13 | "graph", 14 | "visualization", 15 | "chart", 16 | "react-component" 17 | ], 18 | "author": "Neri Marschik (http://www.cyberagent.co.jp)", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/codesuki/react-d3-components/issues" 22 | }, 23 | "homepage": "https://github.com/codesuki/react-d3-components", 24 | "peerDependencies": { 25 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0", 26 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" 27 | }, 28 | "dependencies": { 29 | "create-react-class": "^15.6.2", 30 | "d3": "^3.5.3", 31 | "prop-types": "^15.6.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.0.0-beta.49", 35 | "@babel/core": "^7.0.0-beta.49", 36 | "@babel/preset-env": "^7.0.0-beta.49", 37 | "@babel/preset-react": "^7.0.0-beta.49", 38 | "@babel/preset-stage-0": "^7.0.0-beta.49", 39 | "babel-eslint": "^8.2.3", 40 | "babel-loader": "^8.0.0-beta", 41 | "eslint": "^4.19.1", 42 | "eslint-config-prettier": "^2.9.0", 43 | "eslint-plugin-prettier": "^2.6.0", 44 | "eslint-plugin-react": "^7.9.1", 45 | "husky": "^0.14.3", 46 | "lint-staged": "^7.1.3", 47 | "npm-run-all": "^4.1.3", 48 | "webpack": "^4.12.0", 49 | "webpack-cli": "^3.0.3", 50 | "webpack-merge": "^4.1.2" 51 | }, 52 | "scripts": { 53 | "prepublish": "npm run build", 54 | "postpublish": "rm -r ./lib", 55 | "build": "npm-run-all --parallel build:*", 56 | "build:cmj": "babel -c -d ./lib ./src/", 57 | "build:umd-dev": "webpack --config config/webpack.config.dev.js", 58 | "build:umd-prod": "webpack --config config/webpack.config.prod.js", 59 | "lint": "eslint src/*.jsx --fix", 60 | "precommit": "lint-staged" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AccessorMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const { func } = PropTypes; 4 | 5 | const AccessorMixin = { 6 | propTypes: { 7 | label: func, 8 | values: func, 9 | x: func, 10 | y: func, 11 | y0: func 12 | }, 13 | 14 | getDefaultProps() { 15 | return { 16 | label: stack => stack.label, 17 | values: stack => stack.values, 18 | x: e => e.x, 19 | y: e => e.y, 20 | y0: () => 0 21 | }; 22 | } 23 | }; 24 | 25 | export default AccessorMixin; 26 | -------------------------------------------------------------------------------- /src/AreaChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | import d3 from 'd3'; 5 | 6 | import Chart from './Chart'; 7 | import Axis from './Axis'; 8 | import Path from './Path'; 9 | import Tooltip from './Tooltip'; 10 | 11 | import DefaultPropsMixin from './DefaultPropsMixin'; 12 | import HeightWidthMixin from './HeightWidthMixin'; 13 | import ArrayifyMixin from './ArrayifyMixin'; 14 | import StackAccessorMixin from './StackAccessorMixin'; 15 | import StackDataMixin from './StackDataMixin'; 16 | import DefaultScalesMixin from './DefaultScalesMixin'; 17 | import TooltipMixin from './TooltipMixin'; 18 | 19 | const { array, func, string } = PropTypes; 20 | 21 | const DataSet = createReactClass({ 22 | propTypes: { 23 | data: array.isRequired, 24 | area: func.isRequired, 25 | line: func.isRequired, 26 | colorScale: func.isRequired, 27 | stroke: func.isRequired 28 | }, 29 | 30 | render() { 31 | const { 32 | data, 33 | area, 34 | colorScale, 35 | values, 36 | label, 37 | onMouseEnter, 38 | onMouseLeave 39 | } = this.props; 40 | 41 | const areas = data.map((stack, index) => 42 | 52 | ); 53 | 54 | return {areas}; 55 | } 56 | }); 57 | 58 | const AreaChart = createReactClass({ 59 | mixins: [ 60 | DefaultPropsMixin, 61 | HeightWidthMixin, 62 | ArrayifyMixin, 63 | StackAccessorMixin, 64 | StackDataMixin, 65 | DefaultScalesMixin, 66 | TooltipMixin 67 | ], 68 | 69 | propTypes: { 70 | interpolate: string, 71 | stroke: func 72 | }, 73 | 74 | getDefaultProps() { 75 | return { 76 | interpolate: 'linear', 77 | stroke: d3.scale.category20() 78 | }; 79 | }, 80 | 81 | _tooltipHtml(d, position) { 82 | const { x, y0, y, values, label } = this.props; 83 | const xScale = this._xScale; 84 | const yScale = this._yScale; 85 | 86 | const xValueCursor = xScale.invert(position[0]); 87 | 88 | const xBisector = d3.bisector(e => x(e)).right; 89 | let xIndex = xBisector(values(d[0]), xScale.invert(position[0])); 90 | xIndex = xIndex == values(d[0]).length ? xIndex - 1 : xIndex; 91 | 92 | const xIndexRight = xIndex == values(d[0]).length ? xIndex - 1 : xIndex; 93 | const xValueRight = x(values(d[0])[xIndexRight]); 94 | 95 | const xIndexLeft = xIndex == 0 ? xIndex : xIndex - 1; 96 | const xValueLeft = x(values(d[0])[xIndexLeft]); 97 | 98 | if ( 99 | Math.abs(xValueCursor - xValueRight) < 100 | Math.abs(xValueCursor - xValueLeft) 101 | ) { 102 | xIndex = xIndexRight; 103 | } else { 104 | xIndex = xIndexLeft; 105 | } 106 | 107 | const yValueCursor = yScale.invert(position[1]); 108 | 109 | const yBisector = d3.bisector( 110 | e => y0(values(e)[xIndex]) + y(values(e)[xIndex]) 111 | ).left; 112 | let yIndex = yBisector(d, yValueCursor); 113 | yIndex = yIndex == d.length ? yIndex - 1 : yIndex; 114 | 115 | const yValue = y(values(d[yIndex])[xIndex]); 116 | const yValueCumulative = 117 | y0(values(d[d.length - 1])[xIndex]) + 118 | y(values(d[d.length - 1])[xIndex]); 119 | 120 | const xValue = x(values(d[yIndex])[xIndex]); 121 | 122 | const xPos = xScale(xValue); 123 | const yPos = yScale(y0(values(d[yIndex])[xIndex]) + yValue); 124 | 125 | return [ 126 | this.props.tooltipHtml( 127 | yValue, 128 | yValueCumulative, 129 | xValue, 130 | label(d[yIndex]) 131 | ), 132 | xPos, 133 | yPos 134 | ]; 135 | }, 136 | 137 | render() { 138 | const { 139 | height, 140 | width, 141 | margin, 142 | viewBox, 143 | preserveAspectRatio, 144 | colorScale, 145 | interpolate, 146 | stroke, 147 | values, 148 | label, 149 | x, 150 | y, 151 | y0, 152 | xAxis, 153 | yAxis, 154 | yOrientation 155 | } = this.props; 156 | 157 | const data = this._data; 158 | const innerWidth = this._innerWidth; 159 | const innerHeight = this._innerHeight; 160 | const xScale = this._xScale; 161 | const yScale = this._yScale; 162 | 163 | const line = d3.svg 164 | .line() 165 | .x(e => xScale(x(e))) 166 | .y(e => yScale(y0(e) + y(e))) 167 | .interpolate(interpolate); 168 | 169 | const area = d3.svg 170 | .area() 171 | .x(e => xScale(x(e))) 172 | .y0(e => yScale(yScale.domain()[0] + y0(e))) 173 | .y1(e => yScale(y0(e) + y(e))) 174 | .interpolate(interpolate); 175 | 176 | return ( 177 |
178 | 185 | 196 | 204 | 212 | {this.props.children} 213 | 214 | 215 |
216 | ); 217 | } 218 | }); 219 | 220 | export default AreaChart; 221 | -------------------------------------------------------------------------------- /src/ArrayifyMixin.jsx: -------------------------------------------------------------------------------- 1 | const ArrayifyMixin = { 2 | componentWillMount() { 3 | this._arrayify(this.props); 4 | }, 5 | 6 | componentWillReceiveProps(nextProps) { 7 | this._arrayify(nextProps); 8 | }, 9 | 10 | _arrayify(props) { 11 | if (props.data === null) { 12 | this._data = [ 13 | { 14 | label: 'No data available', 15 | values: [{ x: 'No data available', y: 1 }] 16 | } 17 | ]; 18 | } else if (!Array.isArray(props.data)) { 19 | this._data = [props.data]; 20 | } else { 21 | this._data = props.data; 22 | } 23 | } 24 | }; 25 | 26 | export default ArrayifyMixin; 27 | -------------------------------------------------------------------------------- /src/Axis.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const { array, func, oneOf, number, string } = PropTypes; 6 | 7 | const Axis = createReactClass({ 8 | propTypes: { 9 | tickArguments: array, 10 | tickValues: array, 11 | tickFormat: func, 12 | tickDirection: oneOf(['horizontal', 'vertical', 'diagonal']), 13 | innerTickSize: number, 14 | tickPadding: number, 15 | outerTickSize: number, 16 | scale: func.isRequired, 17 | className: string, 18 | zero: number, 19 | orientation: oneOf(['top', 'bottom', 'left', 'right']).isRequired, 20 | label: string 21 | }, 22 | 23 | getDefaultProps() { 24 | return { 25 | tickArguments: [10], 26 | tickValues: null, 27 | tickFormat: null, 28 | tickDirection: 'horizontal', 29 | innerTickSize: 6, 30 | tickPadding: 3, 31 | outerTickSize: 6, 32 | className: 'axis', 33 | zero: 0, 34 | label: '' 35 | }; 36 | }, 37 | 38 | _getTranslateString() { 39 | const { orientation, height, width, zero } = this.props; 40 | 41 | if (orientation === 'top') { 42 | return `translate(0, ${zero})`; 43 | } else if (orientation === 'bottom') { 44 | return `translate(0, ${zero == 0 ? height : zero})`; 45 | } else if (orientation === 'left') { 46 | return `translate(${zero}, 0)`; 47 | } else if (orientation === 'right') { 48 | return `translate(${zero == 0 ? width : zero}, 0)`; 49 | } else { 50 | return ''; 51 | } 52 | }, 53 | 54 | render() { 55 | const { 56 | height, 57 | tickArguments, 58 | tickValues, 59 | tickDirection, 60 | innerTickSize, 61 | tickPadding, 62 | outerTickSize, 63 | scale, 64 | orientation, 65 | zero 66 | } = this.props; 67 | 68 | const { width, className, label } = this.props; 69 | let { tickFormat } = this.props; 70 | 71 | let ticks = 72 | tickValues == null 73 | ? scale.ticks 74 | ? scale.ticks.apply(scale, tickArguments) 75 | : scale.domain() 76 | : tickValues; 77 | 78 | if (!tickFormat) { 79 | if (scale.tickFormat) { 80 | tickFormat = scale.tickFormat.apply(scale, tickArguments); 81 | } else { 82 | tickFormat = x => x; 83 | } 84 | } 85 | 86 | // TODO: is there a cleaner way? removes the 0 tick if axes are crossing 87 | if (zero != height && zero != width && zero != 0) { 88 | ticks = ticks.filter(element => element != 0); 89 | } 90 | 91 | const tickSpacing = Math.max(innerTickSize, 0) + tickPadding; 92 | 93 | const sign = orientation === 'top' || orientation === 'left' ? -1 : 1; 94 | 95 | const range = this._d3ScaleRange(scale); 96 | 97 | const activeScale = scale.rangeBand 98 | ? e => scale(e) + scale.rangeBand() / 2 99 | : scale; 100 | 101 | let transform, 102 | x, 103 | y, 104 | x2, 105 | y2, 106 | dy, 107 | textAnchor, 108 | d, 109 | labelElement, 110 | tickRotation = 0; 111 | if (orientation === 'bottom' || orientation === 'top') { 112 | transform = 'translate({}, 0)'; 113 | x = 0; 114 | y = sign * tickSpacing; 115 | x2 = 0; 116 | y2 = sign * innerTickSize; 117 | dy = sign < 0 ? '0em' : '.71em'; 118 | textAnchor = 'middle'; 119 | d = `M${range[0]}, ${sign * outerTickSize}V0H${range[1]}V${sign * 120 | outerTickSize}`; 121 | if (tickDirection === 'vertical') { 122 | tickRotation = -90; 123 | x = -tickSpacing; 124 | y = -innerTickSize; 125 | textAnchor = 'end'; 126 | } else if (tickDirection === 'diagonal') { 127 | tickRotation = -60; 128 | x = -tickSpacing; 129 | y = 0; 130 | textAnchor = 'end'; 131 | } 132 | 133 | labelElement = 134 | 140 | {label} 141 | 142 | ; 143 | } else { 144 | transform = 'translate(0, {})'; 145 | x = sign * tickSpacing; 146 | y = 0; 147 | x2 = sign * innerTickSize; 148 | y2 = 0; 149 | dy = '.32em'; 150 | textAnchor = sign < 0 ? 'end' : 'start'; 151 | d = `M${sign * outerTickSize}, ${range[0]}H0V${range[1]}H${sign * 152 | outerTickSize}`; 153 | if (tickDirection === 'vertical') { 154 | tickRotation = -90; 155 | x -= sign * tickSpacing; 156 | y = -(tickSpacing + innerTickSize); 157 | textAnchor = 'middle'; 158 | } else if (tickDirection === 'diagonal') { 159 | tickRotation = -60; 160 | x -= sign * tickSpacing; 161 | y = -(tickSpacing + innerTickSize); 162 | textAnchor = 'middle'; 163 | } 164 | 165 | labelElement = 166 | 173 | {label} 174 | 175 | ; 176 | } 177 | 178 | const tickElements = ticks.map((tick, index) => { 179 | const position = activeScale(tick); 180 | const translate = transform.replace('{}', position); 181 | return ( 182 | 187 | 188 | 195 | {tickFormat(tick)} 196 | 197 | 198 | ); 199 | }); 200 | 201 | const pathElement = 202 | 203 | ; 204 | 205 | const axisBackground = ; 206 | 207 | return ( 208 | 214 | {axisBackground} 215 | {tickElements} 216 | {pathElement} 217 | {labelElement} 218 | 219 | ); 220 | }, 221 | 222 | _d3ScaleExtent(domain) { 223 | const start = domain[0]; 224 | const stop = domain[domain.length - 1]; 225 | return start < stop ? [start, stop] : [stop, start]; 226 | }, 227 | 228 | _d3ScaleRange(scale) { 229 | return scale.rangeExtent 230 | ? scale.rangeExtent() 231 | : this._d3ScaleExtent(scale.range()); 232 | } 233 | }); 234 | 235 | export default Axis; 236 | -------------------------------------------------------------------------------- /src/Bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const { number, string, array, object, func, oneOfType } = PropTypes; 6 | 7 | const Bar = createReactClass({ 8 | propTypes: { 9 | width: number.isRequired, 10 | height: number.isRequired, 11 | x: number.isRequired, 12 | y: number.isRequired, 13 | fill: string.isRequired, 14 | data: oneOfType([array, object]).isRequired, 15 | onMouseEnter: func, 16 | onMouseLeave: func 17 | }, 18 | 19 | render() { 20 | const { 21 | x, 22 | y, 23 | width, 24 | height, 25 | fill, 26 | data, 27 | onMouseEnter, 28 | onMouseLeave 29 | } = this.props; 30 | 31 | return ( 32 | onMouseEnter(e, data)} 40 | onMouseLeave={e => onMouseLeave(e)} 41 | /> 42 | ); 43 | } 44 | }); 45 | 46 | export default Bar; 47 | -------------------------------------------------------------------------------- /src/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | import Chart from './Chart'; 6 | import Axis from './Axis'; 7 | import Bar from './Bar'; 8 | import Tooltip from './Tooltip'; 9 | 10 | import DefaultPropsMixin from './DefaultPropsMixin'; 11 | import HeightWidthMixin from './HeightWidthMixin'; 12 | import ArrayifyMixin from './ArrayifyMixin'; 13 | import StackAccessorMixin from './StackAccessorMixin'; 14 | import StackDataMixin from './StackDataMixin'; 15 | import DefaultScalesMixin from './DefaultScalesMixin'; 16 | import TooltipMixin from './TooltipMixin'; 17 | 18 | const { array, func } = PropTypes; 19 | 20 | const DataSet = createReactClass({ 21 | propTypes: { 22 | data: array.isRequired, 23 | xScale: func.isRequired, 24 | yScale: func.isRequired, 25 | colorScale: func.isRequired, 26 | values: func.isRequired, 27 | label: func.isRequired, 28 | x: func.isRequired, 29 | y: func.isRequired, 30 | y0: func.isRequired 31 | }, 32 | 33 | render() { 34 | const { 35 | data, 36 | xScale, 37 | yScale, 38 | colorScale, 39 | values, 40 | label, 41 | x, 42 | y, 43 | y0, 44 | onMouseEnter, 45 | onMouseLeave, 46 | groupedBars, 47 | colorByLabel 48 | } = this.props; 49 | 50 | let bars; 51 | if (groupedBars) { 52 | bars = data.map((stack, serieIndex) => 53 | values(stack).map((e, index) => { 54 | const yVal = y(e) < 0 ? yScale(0) : yScale(y(e)); 55 | return ( 56 | 70 | ); 71 | }) 72 | ); 73 | } else { 74 | bars = data.map(stack => 75 | values(stack).map((e, index) => { 76 | const color = colorByLabel 77 | ? colorScale(label(stack)) 78 | : colorScale(x(e)); 79 | const yVal = 80 | y(e) < 0 ? yScale(y0(e)) : yScale(y0(e) + y(e)); 81 | return ( 82 | 95 | ); 96 | }) 97 | ); 98 | } 99 | 100 | return {bars}; 101 | } 102 | }); 103 | 104 | const BarChart = createReactClass({ 105 | mixins: [ 106 | DefaultPropsMixin, 107 | HeightWidthMixin, 108 | ArrayifyMixin, 109 | StackAccessorMixin, 110 | StackDataMixin, 111 | DefaultScalesMixin, 112 | TooltipMixin 113 | ], 114 | 115 | getDefaultProps() { 116 | return { 117 | colorByLabel: true 118 | }; 119 | }, 120 | 121 | _tooltipHtml(d) { 122 | const xScale = this._xScale; 123 | const yScale = this._yScale; 124 | 125 | const html = this.props.tooltipHtml( 126 | this.props.x(d), 127 | this.props.y0(d), 128 | this.props.y(d) 129 | ); 130 | 131 | const midPoint = xScale.rangeBand() / 2; 132 | const xPos = midPoint + xScale(this.props.x(d)); 133 | 134 | const topStack = this._data[this._data.length - 1].values; 135 | let topElement = null; 136 | 137 | // TODO: this might not scale if dataset is huge. 138 | // consider pre-computing yPos for each X 139 | for (let i = 0; i < topStack.length; i++) { 140 | if (this.props.x(topStack[i]) === this.props.x(d)) { 141 | topElement = topStack[i]; 142 | break; 143 | } 144 | } 145 | const yPos = yScale( 146 | this.props.y0(topElement) + this.props.y(topElement) 147 | ); 148 | 149 | return [html, xPos, yPos]; 150 | }, 151 | 152 | render() { 153 | const { 154 | xAxis, 155 | yAxis, 156 | height, 157 | width, 158 | margin, 159 | viewBox, 160 | preserveAspectRatio, 161 | colorScale, 162 | values, 163 | label, 164 | y, 165 | y0, 166 | x, 167 | groupedBars, 168 | colorByLabel, 169 | tickFormat 170 | } = this.props; 171 | 172 | const data = this._data; 173 | const innerWidth = this._innerWidth; 174 | const innerHeight = this._innerHeight; 175 | const xScale = this._xScale; 176 | const yScale = this._yScale; 177 | const yIntercept = this._yIntercept; 178 | 179 | return ( 180 |
181 | 188 | 203 | 213 | 222 | {this.props.children} 223 | 224 | 225 |
226 | ); 227 | } 228 | }); 229 | 230 | export default BarChart; 231 | -------------------------------------------------------------------------------- /src/Brush.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | import Chart from './Chart'; 6 | import Axis from './Axis'; 7 | 8 | import HeightWidthMixin from './HeightWidthMixin'; 9 | 10 | // Adapted for React from https://github.com/mbostock/d3/blob/master/src/svg/brush.js 11 | // TODO: Add D3 License 12 | const _d3SvgBrushCursor = { 13 | n: 'ns-resize', 14 | e: 'ew-resize', 15 | s: 'ns-resize', 16 | w: 'ew-resize', 17 | nw: 'nwse-resize', 18 | ne: 'nesw-resize', 19 | se: 'nwse-resize', 20 | sw: 'nesw-resize' 21 | }; 22 | 23 | const _d3SvgBrushResizes = [ 24 | ['n', 'e', 's', 'w', 'nw', 'ne', 'se', 'sw'], 25 | ['e', 'w'], 26 | ['n', 's'], 27 | [] 28 | ]; 29 | 30 | // TODO: add y axis support 31 | const Brush = createReactClass({ 32 | mixins: [HeightWidthMixin], 33 | 34 | getInitialState() { 35 | return { 36 | resizers: _d3SvgBrushResizes[0], 37 | xExtent: [0, 0], 38 | yExtent: [0, 0], 39 | xExtentDomain: undefined, 40 | yExtentDomain: undefined 41 | }; 42 | }, 43 | 44 | getDefaultProps() { 45 | return { 46 | xScale: null, 47 | yScale: null 48 | }; 49 | }, 50 | 51 | componentWillMount() { 52 | this._extent(this.props.extent); 53 | 54 | this.setState({ 55 | resizers: 56 | _d3SvgBrushResizes[ 57 | !this.props.xScale << 1 | !this.props.yScale 58 | ] 59 | }); 60 | }, 61 | 62 | componentWillReceiveProps(nextProps) { 63 | // when is used inside a component 64 | // we should not set the extent prop on every redraw of the parent, because it will 65 | // stop us from actually setting the extent with the brush. 66 | if (nextProps.xScale !== this.props.xScale) { 67 | this._extent(nextProps.extent, nextProps.xScale); 68 | this.setState({ 69 | resizers: 70 | _d3SvgBrushResizes[ 71 | !this.props.xScale << 1 | !this.props.yScale 72 | ] 73 | }); 74 | } 75 | }, 76 | 77 | render() { 78 | // TODO: remove this.state this.props 79 | const xRange = this.props.xScale 80 | ? this._d3ScaleRange(this.props.xScale) 81 | : null; 82 | const yRange = this.props.yScale 83 | ? this._d3ScaleRange(this.props.yScale) 84 | : null; 85 | 86 | const background = 87 | 96 | ; 97 | 98 | // TODO: it seems like actually we can have both x and y scales at the same time. need to find example. 99 | 100 | let extent; 101 | if (this.props.xScale) { 102 | extent = 103 | 111 | ; 112 | } 113 | 114 | const resizers = this.state.resizers.map(e => 115 | { 123 | this._onMouseDownResizer(event, e); 124 | }} 125 | > 126 | 136 | 137 | ); 138 | 139 | const { 140 | height, 141 | width, 142 | margin, 143 | viewBox, 144 | preserveAspectRatio 145 | } = this.props; 146 | 147 | return ( 148 |
149 | 156 | 161 | {background} 162 | {extent} 163 | {resizers} 164 | 165 | 173 | {this.props.children} 174 | 175 |
176 | ); 177 | }, 178 | 179 | // TODO: Code duplicated in TooltipMixin.jsx, move outside. 180 | _getMousePosition(e) { 181 | const svg = ReactDOM.findDOMNode(this).getElementsByTagName('svg')[0]; 182 | let position; 183 | if (svg.createSVGPoint) { 184 | let point = svg.createSVGPoint(); 185 | point.x = e.clientX; 186 | point.y = e.clientY; 187 | point = point.matrixTransform(svg.getScreenCTM().inverse()); 188 | position = [ 189 | point.x - this.props.margin.left, 190 | point.y - this.props.margin.top 191 | ]; 192 | } else { 193 | const rect = svg.getBoundingClientRect(); 194 | position = [ 195 | e.clientX - rect.left - svg.clientLeft - this.props.margin.left, 196 | e.clientY - rect.top - svg.clientTop - this.props.margin.left 197 | ]; 198 | } 199 | 200 | return position; 201 | }, 202 | 203 | _onMouseDownBackground(e) { 204 | e.preventDefault(); 205 | const range = this._d3ScaleRange(this.props.xScale); 206 | const point = this._getMousePosition(e); 207 | 208 | const size = this.state.xExtent[1] - this.state.xExtent[0]; 209 | 210 | range[1] -= size; 211 | 212 | const min = Math.max(range[0], Math.min(range[1], point[0])); 213 | this.setState({ xExtent: [min, min + size] }); 214 | }, 215 | 216 | // TODO: use constants instead of strings 217 | _onMouseDownExtent(e) { 218 | e.preventDefault(); 219 | this._mouseMode = 'drag'; 220 | 221 | const point = this._getMousePosition(e); 222 | const distanceFromBorder = point[0] - this.state.xExtent[0]; 223 | 224 | this._startPosition = distanceFromBorder; 225 | }, 226 | 227 | _onMouseDownResizer(e, dir) { 228 | e.preventDefault(); 229 | this._mouseMode = 'resize'; 230 | this._resizeDir = dir; 231 | }, 232 | 233 | _onDrag(e) { 234 | const range = this._d3ScaleRange(this.props.xScale); 235 | const point = this._getMousePosition(e); 236 | 237 | const size = this.state.xExtent[1] - this.state.xExtent[0]; 238 | 239 | range[1] -= size; 240 | 241 | const min = Math.max( 242 | range[0], 243 | Math.min(range[1], point[0] - this._startPosition) 244 | ); 245 | 246 | this.setState({ xExtent: [min, min + size], xExtentDomain: null }); 247 | }, 248 | 249 | _onResize(e) { 250 | const range = this._d3ScaleRange(this.props.xScale); 251 | const point = this._getMousePosition(e); 252 | // Don't let the extent go outside of its limits 253 | // TODO: support clamp argument of D3 254 | const min = Math.max(range[0], Math.min(range[1], point[0])); 255 | 256 | if (this._resizeDir == 'w') { 257 | if (min > this.state.xExtent[1]) { 258 | this.setState({ 259 | xExtent: [this.state.xExtent[1], min], 260 | xExtentDomain: null 261 | }); 262 | this._resizeDir = 'e'; 263 | } else { 264 | this.setState({ 265 | xExtent: [min, this.state.xExtent[1]], 266 | xExtentDomain: null 267 | }); 268 | } 269 | } else if (this._resizeDir == 'e') { 270 | if (min < this.state.xExtent[0]) { 271 | this.setState({ 272 | xExtent: [min, this.state.xExtent[0]], 273 | xExtentDomain: null 274 | }); 275 | this._resizeDir = 'w'; 276 | } else { 277 | this.setState({ 278 | xExtent: [this.state.xExtent[0], min], 279 | xExtentDomain: null 280 | }); 281 | } 282 | } 283 | }, 284 | 285 | _onMouseMove(e) { 286 | e.preventDefault(); 287 | 288 | if (this._mouseMode == 'resize') { 289 | this._onResize(e); 290 | } else if (this._mouseMode == 'drag') { 291 | this._onDrag(e); 292 | } 293 | }, 294 | 295 | _onMouseUp(e) { 296 | e.preventDefault(); 297 | 298 | this._mouseMode = null; 299 | 300 | this.props.onChange(this._extent()); 301 | }, 302 | 303 | _extent(z, xScale) { 304 | const x = xScale || this.props.xScale; 305 | const y = this.props.yScale; 306 | 307 | let { xExtent, yExtent, xExtentDomain, yExtentDomain } = this.state; 308 | 309 | let x0, x1, y0, y1, t; 310 | 311 | // Invert the pixel extent to data-space. 312 | if (!arguments.length) { 313 | if (x) { 314 | if (xExtentDomain) { 315 | x0 = xExtentDomain[0], x1 = xExtentDomain[1]; 316 | } else { 317 | x0 = xExtent[0], x1 = xExtent[1]; 318 | if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1); 319 | if (x1 < x0) t = x0, x0 = x1, x1 = t; 320 | } 321 | } 322 | if (y) { 323 | if (yExtentDomain) { 324 | y0 = yExtentDomain[0], y1 = yExtentDomain[1]; 325 | } else { 326 | y0 = yExtent[0], y1 = yExtent[1]; 327 | if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1); 328 | if (y1 < y0) t = y0, y0 = y1, y1 = t; 329 | } 330 | } 331 | return x && y ? [[x0, y0], [x1, y1]] : x ? [x0, x1] : y && [y0, y1]; 332 | } 333 | 334 | // Scale the data-space extent to pixels. 335 | if (x) { 336 | x0 = z[0], x1 = z[1]; 337 | if (y) x0 = x0[0], x1 = x1[0]; 338 | xExtentDomain = [x0, x1]; 339 | if (x.invert) x0 = x(x0), x1 = x(x1); 340 | if (x1 < x0) t = x0, x0 = x1, x1 = t; 341 | if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [x0, x1]; // copy-on-write 342 | } 343 | if (y) { 344 | y0 = z[0], y1 = z[1]; 345 | if (x) y0 = y0[1], y1 = y1[1]; 346 | yExtentDomain = [y0, y1]; 347 | if (y.invert) y0 = y(y0), y1 = y(y1); 348 | if (y1 < y0) t = y0, y0 = y1, y1 = t; 349 | if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [y0, y1]; // copy-on-write 350 | } 351 | 352 | this.setState({ xExtent, yExtent, xExtentDomain, yExtentDomain }); 353 | }, 354 | 355 | _empty() { 356 | return ( 357 | !!this.props.xScale && 358 | this.state.xExtent[0] == this.state.xExtent[1] || 359 | !!this.props.yScale && 360 | this.state.yExtent[0] == this.state.yExtent[1] 361 | ); 362 | }, 363 | 364 | // TODO: Code duplicated in Axis.jsx, move outside. 365 | _d3ScaleExtent(domain) { 366 | const start = domain[0]; 367 | const stop = domain[domain.length - 1]; 368 | return start < stop ? [start, stop] : [stop, start]; 369 | }, 370 | 371 | _d3ScaleRange(scale) { 372 | return scale.rangeExtent 373 | ? scale.rangeExtent() 374 | : this._d3ScaleExtent(scale.range()); 375 | } 376 | }); 377 | 378 | export default Brush; 379 | -------------------------------------------------------------------------------- /src/Chart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const { number, shape } = PropTypes; 6 | 7 | const Chart = createReactClass({ 8 | propTypes: { 9 | height: number.isRequired, 10 | width: number.isRequired, 11 | margin: shape({ 12 | top: number, 13 | bottom: number, 14 | left: number, 15 | right: number 16 | }).isRequired 17 | }, 18 | 19 | render() { 20 | const { 21 | width, 22 | height, 23 | margin, 24 | viewBox, 25 | preserveAspectRatio, 26 | children 27 | } = this.props; 28 | 29 | return ( 30 | 37 | 38 | {children} 39 | 40 | 41 | ); 42 | } 43 | }); 44 | 45 | export default Chart; 46 | -------------------------------------------------------------------------------- /src/DefaultPropsMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import d3 from 'd3'; 3 | 4 | const { oneOfType, object, array, shape, func, number } = PropTypes; 5 | 6 | const DefaultPropsMixin = { 7 | propTypes: { 8 | data: oneOfType([object, array]).isRequired, 9 | height: number.isRequired, 10 | width: number.isRequired, 11 | margin: shape({ 12 | top: number, 13 | bottom: number, 14 | left: number, 15 | right: number 16 | }), 17 | xScale: func, 18 | yScale: func, 19 | colorScale: func 20 | }, 21 | 22 | getDefaultProps() { 23 | return { 24 | data: { 25 | label: 'No data available', 26 | values: [{ x: 'No data available', y: 1 }] 27 | }, 28 | margin: { top: 0, bottom: 0, left: 0, right: 0 }, 29 | xScale: null, 30 | yScale: null, 31 | colorScale: d3.scale.category20() 32 | }; 33 | } 34 | }; 35 | 36 | export default DefaultPropsMixin; 37 | -------------------------------------------------------------------------------- /src/DefaultScalesMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import d3 from 'd3'; 3 | 4 | const { number } = PropTypes; 5 | 6 | const DefaultScalesMixin = { 7 | propTypes: { 8 | barPadding: number 9 | }, 10 | 11 | getDefaultProps() { 12 | return { 13 | barPadding: 0.5 14 | }; 15 | }, 16 | 17 | componentWillMount() { 18 | this._makeScales(this.props); 19 | }, 20 | 21 | componentWillReceiveProps(nextProps) { 22 | this._makeScales(nextProps); 23 | }, 24 | 25 | _makeScales(props) { 26 | const { xScale, xIntercept, yScale, yIntercept } = props; 27 | 28 | if (!xScale) { 29 | [this._xScale, this._xIntercept] = this._makeXScale(props); 30 | } else { 31 | [this._xScale, this._xIntercept] = [xScale, xIntercept]; 32 | } 33 | 34 | if (!yScale) { 35 | [this._yScale, this._yIntercept] = this._makeYScale(props); 36 | } else { 37 | [this._yScale, this._yIntercept] = [yScale, yIntercept]; 38 | } 39 | }, 40 | 41 | _makeXScale(props) { 42 | const { x, values } = props; 43 | const data = this._data; 44 | 45 | if (typeof x(values(data[0])[0]) === 'number') { 46 | return this._makeLinearXScale(props); 47 | } else if (typeof x(values(data[0])[0]).getMonth === 'function') { 48 | return this._makeTimeXScale(props); 49 | } else { 50 | return this._makeOrdinalXScale(props); 51 | } 52 | }, 53 | 54 | _makeLinearXScale(props) { 55 | const { x, values } = props; 56 | const data = this._data; 57 | 58 | const extentsData = data.map(stack => values(stack).map(e => x(e))); 59 | const extents = d3.extent( 60 | Array.prototype.concat.apply([], extentsData) 61 | ); 62 | 63 | const scale = d3.scale 64 | .linear() 65 | .domain(extents) 66 | .range([0, this._innerWidth]); 67 | 68 | const zero = d3.max([0, scale.domain()[0]]); 69 | const xIntercept = scale(zero); 70 | 71 | return [scale, xIntercept]; 72 | }, 73 | 74 | _makeOrdinalXScale(props) { 75 | const { x, values, barPadding } = props; 76 | 77 | const scale = d3.scale 78 | .ordinal() 79 | .domain(values(this._data[0]).map(e => x(e))) 80 | .rangeRoundBands([0, this._innerWidth], barPadding); 81 | 82 | return [scale, 0]; 83 | }, 84 | 85 | _makeTimeXScale(props) { 86 | const { x, values } = props; 87 | 88 | const minDate = d3.min(values(this._data[0]), x); 89 | const maxDate = d3.max(values(this._data[0]), x); 90 | 91 | const scale = d3.time 92 | .scale() 93 | .domain([minDate, maxDate]) 94 | .range([0, this._innerWidth]); 95 | 96 | return [scale, 0]; 97 | }, 98 | 99 | _makeYScale(props) { 100 | const { y, values } = props; 101 | const data = this._data; 102 | 103 | if (typeof y(values(data[0])[0]) === 'number') { 104 | return this._makeLinearYScale(props); 105 | } else { 106 | return this._makeOrdinalYScale(props); 107 | } 108 | }, 109 | 110 | _makeLinearYScale(props) { 111 | const { y, y0, values, groupedBars } = props; 112 | 113 | const extentsData = this._data.map(stack => 114 | values(stack).map(e => groupedBars ? y(e) : y0(e) + y(e)) 115 | ); 116 | let extents = d3.extent(Array.prototype.concat.apply([], extentsData)); 117 | 118 | extents = [d3.min([0, extents[0]]), extents[1]]; 119 | 120 | const scale = d3.scale 121 | .linear() 122 | .domain(extents) 123 | .range([this._innerHeight, 0]); 124 | 125 | const zero = d3.max([0, scale.domain()[0]]); 126 | const yIntercept = scale(zero); 127 | 128 | return [scale, yIntercept]; 129 | }, 130 | 131 | _makeOrdinalYScale() { 132 | const scale = d3.scale.ordinal().range([this._innerHeight, 0]); 133 | 134 | const yIntercept = scale(0); 135 | 136 | return [scale, yIntercept]; 137 | } 138 | }; 139 | 140 | export default DefaultScalesMixin; 141 | -------------------------------------------------------------------------------- /src/HeightWidthMixin.jsx: -------------------------------------------------------------------------------- 1 | const HeightWidthMixin = { 2 | componentWillMount() { 3 | this._calculateInner(this.props); 4 | }, 5 | 6 | componentWillReceiveProps(nextProps) { 7 | this._calculateInner(nextProps); 8 | }, 9 | 10 | _calculateInner(props) { 11 | const { height, width, margin } = props; 12 | 13 | this._innerHeight = height - margin.top - margin.bottom; 14 | this._innerWidth = width - margin.left - margin.right; 15 | } 16 | }; 17 | 18 | export default HeightWidthMixin; 19 | -------------------------------------------------------------------------------- /src/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | import d3 from 'd3'; 5 | 6 | import Chart from './Chart'; 7 | import Axis from './Axis'; 8 | import Path from './Path'; 9 | import Tooltip from './Tooltip'; 10 | 11 | import DefaultPropsMixin from './DefaultPropsMixin'; 12 | import HeightWidthMixin from './HeightWidthMixin'; 13 | import ArrayifyMixin from './ArrayifyMixin'; 14 | import AccessorMixin from './AccessorMixin'; 15 | import DefaultScalesMixin from './DefaultScalesMixin'; 16 | import TooltipMixin from './TooltipMixin'; 17 | 18 | const { array, func, string } = PropTypes; 19 | 20 | const DataSet = createReactClass({ 21 | propTypes: { 22 | data: array.isRequired, 23 | line: func.isRequired, 24 | colorScale: func.isRequired 25 | }, 26 | 27 | render() { 28 | const { 29 | width, 30 | height, 31 | data, 32 | line, 33 | strokeWidth, 34 | strokeLinecap, 35 | strokeDasharray, 36 | colorScale, 37 | values, 38 | label, 39 | onMouseEnter, 40 | onMouseLeave 41 | } = this.props; 42 | 43 | const sizeId = width + 'x' + height; 44 | 45 | const lines = data.map((stack, index) => 46 | 71 | ); 72 | 73 | /* 74 | The below is needed in case we want to show the tooltip no matter where on the chart the mouse is. 75 | Not sure if this should be used. 76 | */ 77 | return ( 78 | 79 | 80 | 81 | 82 | 83 | 84 | {lines} 85 | { 92 | onMouseEnter(evt, data); 93 | }} 94 | onMouseLeave={evt => { 95 | onMouseLeave(evt); 96 | }} 97 | /> 98 | 99 | ); 100 | } 101 | }); 102 | 103 | const LineChart = createReactClass({ 104 | mixins: [ 105 | DefaultPropsMixin, 106 | HeightWidthMixin, 107 | ArrayifyMixin, 108 | AccessorMixin, 109 | DefaultScalesMixin, 110 | TooltipMixin 111 | ], 112 | 113 | propTypes: { 114 | interpolate: string, 115 | defined: func 116 | }, 117 | 118 | getDefaultProps() { 119 | return { 120 | interpolate: 'linear', 121 | defined: () => true, 122 | shape: 'circle', 123 | shapeColor: null, 124 | showCustomLine: false, 125 | lineStructureClassName: 'dot', 126 | customPointColor: 'blue', 127 | customPointShape: 'circle' 128 | }; 129 | }, 130 | 131 | /* 132 | The code below supports finding the data values for the line closest to the mouse cursor. 133 | Since it gets all events from the Rect overlaying the Chart the tooltip gets shown everywhere. 134 | For now I don't want to use this method. 135 | */ 136 | _tooltipHtml(data, position) { 137 | const { x, y, values, label } = this.props; 138 | const xScale = this._xScale; 139 | const yScale = this._yScale; 140 | 141 | const xValueCursor = xScale.invert(position[0]); 142 | const yValueCursor = yScale.invert(position[1]); 143 | 144 | const xBisector = d3.bisector(e => x(e)).left; 145 | const valuesAtX = data.map(stack => { 146 | const idx = xBisector(values(stack), xValueCursor); 147 | 148 | const indexRight = idx === values(stack).length ? idx - 1 : idx; 149 | const valueRight = x(values(stack)[indexRight]); 150 | 151 | const indexLeft = idx === 0 ? idx : idx - 1; 152 | const valueLeft = x(values(stack)[indexLeft]); 153 | 154 | let index; 155 | if ( 156 | Math.abs(xValueCursor - valueRight) < 157 | Math.abs(xValueCursor - valueLeft) 158 | ) { 159 | index = indexRight; 160 | } else { 161 | index = indexLeft; 162 | } 163 | 164 | return { label: label(stack), value: values(stack)[index] }; 165 | }); 166 | 167 | valuesAtX.sort((a, b) => y(a.value) - y(b.value)); 168 | 169 | const yBisector = d3.bisector(e => y(e.value)).left; 170 | const yIndex = yBisector(valuesAtX, yValueCursor); 171 | 172 | const yIndexRight = yIndex === valuesAtX.length ? yIndex - 1 : yIndex; 173 | const yIndexLeft = yIndex === 0 ? yIndex : yIndex - 1; 174 | 175 | const yValueRight = y(valuesAtX[yIndexRight].value); 176 | const yValueLeft = y(valuesAtX[yIndexLeft].value); 177 | 178 | let index; 179 | if ( 180 | Math.abs(yValueCursor - yValueRight) < 181 | Math.abs(yValueCursor - yValueLeft) 182 | ) { 183 | index = yIndexRight; 184 | } else { 185 | index = yIndexLeft; 186 | } 187 | 188 | this._tooltipData = valuesAtX[index]; 189 | 190 | const html = this.props.tooltipHtml( 191 | valuesAtX[index].label, 192 | valuesAtX[index].value 193 | ); 194 | 195 | const xPos = xScale(valuesAtX[index].value.x); 196 | const yPos = yScale(valuesAtX[index].value.y); 197 | 198 | return [html, xPos, yPos]; 199 | }, 200 | 201 | /* 202 | _tooltipHtml(data, position) { 203 | let {x, y0, y, values, label} = this.props; 204 | let [xScale, yScale] = [this._xScale, this._yScale]; 205 | 206 | let xValueCursor = xScale.invert(position[0]); 207 | let yValueCursor = yScale.invert(position[1]); 208 | 209 | let xBisector = d3.bisector(e => { return x(e); }).left; 210 | let xIndex = xBisector(data, xScale.invert(position[0])); 211 | 212 | let indexRight = xIndex == data.length ? xIndex - 1 : xIndex; 213 | let valueRight = x(data[indexRight]); 214 | 215 | let indexLeft = xIndex == 0 ? xIndex : xIndex - 1; 216 | let valueLeft = x(data[indexLeft]); 217 | 218 | let index; 219 | if (Math.abs(xValueCursor - valueRight) < Math.abs(xValueCursor - valueLeft)) { 220 | index = indexRight; 221 | } else { 222 | index = indexLeft; 223 | } 224 | 225 | let yValue = y(data[index]); 226 | let cursorValue = d3.round(yScale.invert(position[1]), 2); 227 | 228 | return this.props.tooltipHtml(yValue, cursorValue); 229 | }, 230 | */ 231 | 232 | /* 233 | stroke, 234 | strokeWidth, 235 | strokeLinecap, 236 | strokeDasharray, 237 | */ 238 | render() { 239 | const { 240 | height, 241 | width, 242 | margin, 243 | viewBox, 244 | preserveAspectRatio, 245 | colorScale, 246 | interpolate, 247 | defined, 248 | stroke, 249 | values, 250 | label, 251 | x, 252 | y, 253 | xAxis, 254 | yAxis, 255 | shape, 256 | shapeColor, 257 | showCustomLine, 258 | lineStructureClassName, 259 | customPointColor, 260 | customPointShape 261 | } = this.props; 262 | 263 | const data = this._data; 264 | const innerWidth = this._innerWidth; 265 | const innerHeight = this._innerHeight; 266 | const xScale = this._xScale; 267 | const yScale = this._yScale; 268 | const xIntercept = this._xIntercept; 269 | const yIntercept = this._yIntercept; 270 | 271 | const line = d3.svg 272 | .line() 273 | .x(e => xScale(x(e))) 274 | .y(e => yScale(y(e))) 275 | .interpolate(interpolate) 276 | .defined(defined); 277 | 278 | let tooltipSymbol = null, 279 | points = null; 280 | if (!this.state.tooltip.hidden) { 281 | const symbol = d3.svg.symbol().type(shape); 282 | const symbolColor = shapeColor 283 | ? shapeColor 284 | : colorScale(this._tooltipData.label); 285 | 286 | const translate = this._tooltipData 287 | ? `translate(${xScale(x(this._tooltipData.value))}, ${yScale( 288 | y(this._tooltipData.value) 289 | )})` 290 | : ''; 291 | tooltipSymbol = this.state.tooltip.hidden ? null : 292 | this.onMouseEnter(evt, data)} 298 | onMouseLeave={evt => this.onMouseLeave(evt)} 299 | /> 300 | ; 301 | } 302 | 303 | if (showCustomLine) { 304 | const translatePoints = function (point) { 305 | return `translate(${xScale(x(point))}, ${yScale(y(point))})`; 306 | }; 307 | 308 | points = data.map(d => 309 | d.values.map((p, i) => 310 | this.onMouseEnter(evt, data)} 317 | onMouseLeave={evt => this.onMouseLeave(evt)} 318 | /> 319 | ) 320 | ); 321 | } 322 | 323 | return ( 324 |
325 | 332 | 341 | 350 | 362 | {this.props.children} 363 | {tooltipSymbol} 364 | {points} 365 | 366 | 367 |
368 | ); 369 | } 370 | }); 371 | 372 | export default LineChart; 373 | -------------------------------------------------------------------------------- /src/Path.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const { string, array } = PropTypes; 6 | 7 | const Path = createReactClass({ 8 | propTypes: { 9 | className: string, 10 | stroke: string.isRequired, 11 | strokeLinecap: string, 12 | strokeWidth: string, 13 | strokeDasharray: string, 14 | fill: string, 15 | d: string.isRequired, 16 | data: array.isRequired 17 | }, 18 | 19 | getDefaultProps() { 20 | return { 21 | className: 'path', 22 | fill: 'none', 23 | strokeWidth: '2', 24 | strokeLinecap: 'butt', 25 | strokeDasharray: 'none' 26 | }; 27 | }, 28 | 29 | render() { 30 | const { 31 | className, 32 | stroke, 33 | strokeWidth, 34 | strokeLinecap, 35 | strokeDasharray, 36 | fill, 37 | d, 38 | style, 39 | data, 40 | onMouseEnter, 41 | onMouseLeave 42 | } = this.props; 43 | 44 | return ( 45 | onMouseEnter(evt, data)} 54 | onMouseLeave={evt => onMouseLeave(evt)} 55 | style={style} 56 | /> 57 | ); 58 | } 59 | }); 60 | 61 | export default Path; 62 | -------------------------------------------------------------------------------- /src/PieChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | import d3 from 'd3'; 5 | 6 | import Chart from './Chart'; 7 | import Tooltip from './Tooltip'; 8 | 9 | import DefaultPropsMixin from './DefaultPropsMixin'; 10 | import HeightWidthMixin from './HeightWidthMixin'; 11 | import AccessorMixin from './AccessorMixin'; 12 | import TooltipMixin from './TooltipMixin'; 13 | 14 | const { string, array, number, bool, func, any } = PropTypes; 15 | 16 | const Wedge = createReactClass({ 17 | propTypes: { 18 | d: string.isRequired, 19 | fill: string.isRequired 20 | }, 21 | 22 | render() { 23 | const { fill, d, data, onMouseEnter, onMouseLeave } = this.props; 24 | 25 | return ( 26 | onMouseEnter(evt, data)} 30 | onMouseLeave={evt => onMouseLeave(evt)} 31 | /> 32 | ); 33 | } 34 | }); 35 | 36 | const DataSet = createReactClass({ 37 | propTypes: { 38 | pie: array.isRequired, 39 | arc: func.isRequired, 40 | outerArc: func.isRequired, 41 | colorScale: func.isRequired, 42 | radius: number.isRequired, 43 | strokeWidth: number, 44 | stroke: string, 45 | fill: string, 46 | opacity: number, 47 | x: func.isRequired, 48 | hideLabels: bool 49 | }, 50 | 51 | getDefaultProps() { 52 | return { 53 | strokeWidth: 2, 54 | stroke: '#000', 55 | fill: 'none', 56 | opacity: 0.3, 57 | hideLabels: false 58 | }; 59 | }, 60 | 61 | renderLabel(wedge) { 62 | const { 63 | arc, 64 | outerArc, 65 | radius, 66 | strokeWidth, 67 | stroke, 68 | fill, 69 | opacity, 70 | x 71 | } = this.props; 72 | 73 | const labelPos = outerArc.centroid(wedge); 74 | labelPos[0] = radius * (this.midAngle(wedge) < Math.PI ? 1 : -1); 75 | 76 | const linePos = outerArc.centroid(wedge); 77 | linePos[0] = radius * 0.95 * (this.midAngle(wedge) < Math.PI ? 1 : -1); 78 | 79 | const textAnchor = this.midAngle(wedge) < Math.PI ? 'start' : 'end'; 80 | 81 | return ( 82 | 83 | 94 | 100 | {x(wedge.data)} 101 | 102 | 103 | ); 104 | }, 105 | 106 | render() { 107 | const { 108 | pie, 109 | arc, 110 | colorScale, 111 | x, 112 | y, 113 | onMouseEnter, 114 | onMouseLeave, 115 | hideLabels 116 | } = this.props; 117 | 118 | const wedges = pie.map((e, index) => { 119 | const labelFits = e.endAngle - e.startAngle >= 10 * Math.PI / 180; 120 | 121 | return ( 122 | 123 | 130 | {!hideLabels && 131 | !!e.value && 132 | labelFits && 133 | this.renderLabel(e)} 134 | 135 | ); 136 | }); 137 | 138 | return {wedges}; 139 | }, 140 | 141 | midAngle(d) { 142 | return d.startAngle + (d.endAngle - d.startAngle) / 2; 143 | } 144 | }); 145 | 146 | const PieChart = createReactClass({ 147 | mixins: [DefaultPropsMixin, HeightWidthMixin, AccessorMixin, TooltipMixin], 148 | 149 | propTypes: { 150 | innerRadius: number, 151 | outerRadius: number, 152 | labelRadius: number, 153 | padRadius: string, 154 | cornerRadius: number, 155 | sort: any, 156 | hideLabels: bool 157 | }, 158 | 159 | getDefaultProps() { 160 | return { 161 | innerRadius: null, 162 | outerRadius: null, 163 | labelRadius: null, 164 | padRadius: 'auto', 165 | cornerRadius: 0, 166 | sort: undefined, 167 | hideLabels: false 168 | }; 169 | }, 170 | 171 | _tooltipHtml(d) { 172 | const html = this.props.tooltipHtml(this.props.x(d), this.props.y(d)); 173 | 174 | return [html, 0, 0]; 175 | }, 176 | 177 | render() { 178 | const { 179 | data, 180 | width, 181 | height, 182 | margin, 183 | viewBox, 184 | preserveAspectRatio, 185 | colorScale, 186 | padRadius, 187 | cornerRadius, 188 | sort, 189 | x, 190 | y, 191 | values, 192 | hideLabels 193 | } = this.props; 194 | 195 | let { innerRadius, outerRadius, labelRadius } = this.props; 196 | 197 | const innerWidth = this._innerWidth; 198 | const innerHeight = this._innerHeight; 199 | 200 | let pie = d3.layout.pie().value(e => y(e)); 201 | 202 | if (typeof sort !== 'undefined') { 203 | pie = pie.sort(sort); 204 | } 205 | 206 | const radius = Math.min(innerWidth, innerHeight) / 2; 207 | if (!innerRadius) { 208 | innerRadius = radius * 0.8; 209 | } 210 | 211 | if (!outerRadius) { 212 | outerRadius = radius * 0.4; 213 | } 214 | 215 | if (!labelRadius) { 216 | labelRadius = radius * 0.9; 217 | } 218 | 219 | const arc = d3.svg 220 | .arc() 221 | .innerRadius(innerRadius) 222 | .outerRadius(outerRadius) 223 | .padRadius(padRadius) 224 | .cornerRadius(cornerRadius); 225 | 226 | const outerArc = d3.svg 227 | .arc() 228 | .innerRadius(labelRadius) 229 | .outerRadius(labelRadius); 230 | 231 | const pieData = pie(values(data)); 232 | 233 | const translation = `translate(${innerWidth / 2}, ${innerHeight / 2})`; 234 | 235 | return ( 236 |
237 | 244 | 245 | 259 | 260 | {this.props.children} 261 | 262 | 263 |
264 | ); 265 | } 266 | }); 267 | 268 | export default PieChart; 269 | -------------------------------------------------------------------------------- /src/ScatterPlot.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | import d3 from 'd3'; 5 | 6 | import Chart from './Chart'; 7 | import Axis from './Axis'; 8 | import Tooltip from './Tooltip'; 9 | 10 | import DefaultPropsMixin from './DefaultPropsMixin'; 11 | import HeightWidthMixin from './HeightWidthMixin'; 12 | import ArrayifyMixin from './ArrayifyMixin'; 13 | import AccessorMixin from './AccessorMixin'; 14 | import DefaultScalesMixin from './DefaultScalesMixin'; 15 | import TooltipMixin from './TooltipMixin'; 16 | 17 | const { array, func, string } = PropTypes; 18 | 19 | const DataSet = createReactClass({ 20 | propTypes: { 21 | data: array.isRequired, 22 | symbol: func.isRequired, 23 | xScale: func.isRequired, 24 | yScale: func.isRequired, 25 | colorScale: func.isRequired, 26 | onMouseEnter: func, 27 | onMouseLeave: func 28 | }, 29 | 30 | render() { 31 | const { 32 | data, 33 | symbol, 34 | xScale, 35 | yScale, 36 | colorScale, 37 | label, 38 | values, 39 | x, 40 | y, 41 | onMouseEnter, 42 | onMouseLeave 43 | } = this.props; 44 | 45 | const circles = data.map(stack => 46 | values(stack).map((e, index) => { 47 | const translate = `translate(${xScale(x(e))}, ${yScale(y(e))})`; 48 | return ( 49 | onMouseEnter(evt, e)} 56 | onMouseLeave={evt => onMouseLeave(evt)} 57 | /> 58 | ); 59 | }) 60 | ); 61 | 62 | return {circles}; 63 | } 64 | }); 65 | 66 | const ScatterPlot = createReactClass({ 67 | mixins: [ 68 | DefaultPropsMixin, 69 | HeightWidthMixin, 70 | ArrayifyMixin, 71 | AccessorMixin, 72 | DefaultScalesMixin, 73 | TooltipMixin 74 | ], 75 | 76 | propTypes: { 77 | rScale: func, 78 | shape: string 79 | }, 80 | 81 | getDefaultProps() { 82 | return { 83 | rScale: null, 84 | shape: 'circle' 85 | }; 86 | }, 87 | 88 | _tooltipHtml(d) { 89 | const html = this.props.tooltipHtml(this.props.x(d), this.props.y(d)); 90 | 91 | const xPos = this._xScale(this.props.x(d)); 92 | const yPos = this._yScale(this.props.y(d)); 93 | 94 | return [html, xPos, yPos]; 95 | }, 96 | 97 | render() { 98 | const { 99 | height, 100 | width, 101 | margin, 102 | viewBox, 103 | preserveAspectRatio, 104 | colorScale, 105 | rScale, 106 | shape, 107 | label, 108 | values, 109 | x, 110 | y, 111 | xAxis, 112 | yAxis 113 | } = this.props; 114 | 115 | const data = this._data; 116 | const innerWidth = this._innerWidth; 117 | const innerHeight = this._innerHeight; 118 | const xScale = this._xScale; 119 | const yScale = this._yScale; 120 | const xIntercept = this._xIntercept; 121 | const yIntercept = this._yIntercept; 122 | 123 | let symbol = d3.svg.symbol().type(shape); 124 | 125 | if (rScale) { 126 | symbol = symbol.size(rScale); 127 | } 128 | 129 | return ( 130 |
131 | 138 | 147 | 156 | 169 | {this.props.children} 170 | 171 | 172 |
173 | ); 174 | } 175 | }); 176 | 177 | export default ScatterPlot; 178 | -------------------------------------------------------------------------------- /src/StackAccessorMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const { func } = PropTypes; 4 | 5 | const StackAccessorMixin = { 6 | propTypes: { 7 | label: func, 8 | values: func, 9 | x: func, 10 | y: func, 11 | y0: func 12 | }, 13 | 14 | getDefaultProps() { 15 | return { 16 | label: stack => stack.label, 17 | values: stack => stack.values, 18 | x: e => e.x, 19 | y: e => e.y, 20 | y0: e => e.y0 21 | }; 22 | } 23 | }; 24 | 25 | export default StackAccessorMixin; 26 | -------------------------------------------------------------------------------- /src/StackDataMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import d3 from 'd3'; 3 | 4 | const { string } = PropTypes; 5 | 6 | const StackDataMixin = { 7 | propTypes: { 8 | offset: string 9 | }, 10 | 11 | getDefaultProps() { 12 | return { 13 | offset: 'zero', 14 | order: 'default' 15 | }; 16 | }, 17 | 18 | componentWillMount() { 19 | this._stackData(this.props); 20 | }, 21 | 22 | componentWillReceiveProps(nextProps) { 23 | this._stackData(nextProps); 24 | }, 25 | 26 | _stackData(props) { 27 | const { offset, order, x, y, values } = props; 28 | 29 | const stack = d3.layout 30 | .stack() 31 | .offset(offset) 32 | .order(order) 33 | .x(x) 34 | .y(y) 35 | .values(values); 36 | 37 | this._data = stack(this._data); 38 | 39 | for (let m = 0; m < values(this._data[0]).length; m++) { 40 | let positiveBase = 0; 41 | let negativeBase = 0; 42 | for (let n = 0; n < this._data.length; n++) { 43 | const value = y(values(this._data[n])[m]); 44 | if (value < 0) { 45 | values(this._data[n])[m].y0 = negativeBase; 46 | negativeBase += value; 47 | } else { 48 | values(this._data[n])[m].y0 = positiveBase; 49 | positiveBase += value; 50 | } 51 | } 52 | } 53 | } 54 | }; 55 | 56 | export default StackDataMixin; 57 | -------------------------------------------------------------------------------- /src/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const { number, node } = PropTypes; 6 | 7 | const Tooltip = createReactClass({ 8 | propTypes: { 9 | top: number.isRequired, 10 | left: number.isRequired, 11 | html: node, 12 | translate: number 13 | }, 14 | 15 | getDefaultProps() { 16 | return { 17 | top: 150, 18 | left: 100, 19 | html: '', 20 | translate: 50 21 | }; 22 | }, 23 | 24 | render() { 25 | const { top, left, hidden, html, translate } = this.props; 26 | 27 | const style = { 28 | display: hidden ? 'none' : 'block', 29 | position: 'fixed', 30 | top, 31 | left, 32 | transform: `translate(-${translate}%, 0)`, 33 | pointerEvents: 'none' 34 | }; 35 | 36 | return ( 37 |
38 | {html} 39 |
40 | ); 41 | } 42 | }); 43 | 44 | export default Tooltip; 45 | -------------------------------------------------------------------------------- /src/TooltipMixin.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | const { func, oneOf, bool, objectOf, number } = PropTypes; 5 | 6 | const TooltipMixin = { 7 | propTypes: { 8 | tooltipHtml: func, 9 | tooltipMode: oneOf(['mouse', 'element', 'fixed']), 10 | tooltipContained: bool, 11 | tooltipOffset: objectOf(number) 12 | }, 13 | 14 | getInitialState() { 15 | return { 16 | tooltip: { 17 | hidden: true 18 | } 19 | }; 20 | }, 21 | 22 | getDefaultProps() { 23 | return { 24 | tooltipMode: 'mouse', 25 | tooltipOffset: { top: -35, left: 0 }, 26 | tooltipHtml: null, 27 | tooltipContained: false 28 | }; 29 | }, 30 | 31 | componentDidMount() { 32 | this._svgNode = ReactDOM.findDOMNode(this).getElementsByTagName( 33 | 'svg' 34 | )[0]; 35 | }, 36 | 37 | onMouseEnter(e, data) { 38 | if (!this.props.tooltipHtml) { 39 | return; 40 | } 41 | 42 | e.preventDefault(); 43 | 44 | const { 45 | margin, 46 | tooltipMode, 47 | tooltipOffset, 48 | tooltipContained 49 | } = this.props; 50 | 51 | const svg = this._svgNode; 52 | let position; 53 | if (svg.createSVGPoint) { 54 | let point = svg.createSVGPoint(); 55 | point.x = e.clientX, point.y = e.clientY; 56 | point = point.matrixTransform(svg.getScreenCTM().inverse()); 57 | position = [point.x - margin.left, point.y - margin.top]; 58 | } else { 59 | const rect = svg.getBoundingClientRect(); 60 | position = [ 61 | e.clientX - rect.left - svg.clientLeft - margin.left, 62 | e.clientY - rect.top - svg.clientTop - margin.top 63 | ]; 64 | } 65 | 66 | const [html, xPos, yPos] = this._tooltipHtml(data, position); 67 | 68 | const svgTop = svg.getBoundingClientRect().top + margin.top; 69 | const svgLeft = svg.getBoundingClientRect().left + margin.left; 70 | 71 | let top = 0; 72 | let left = 0; 73 | 74 | if (tooltipMode === 'fixed') { 75 | top = svgTop + tooltipOffset.top; 76 | left = svgLeft + tooltipOffset.left; 77 | } else if (tooltipMode === 'element') { 78 | top = svgTop + yPos + tooltipOffset.top; 79 | left = svgLeft + xPos + tooltipOffset.left; 80 | } else { 81 | // mouse 82 | top = e.clientY + tooltipOffset.top; 83 | left = e.clientX + tooltipOffset.left; 84 | } 85 | 86 | function lerp(t, a, b) { 87 | return (1 - t) * a + t * b; 88 | } 89 | 90 | let translate = 50; 91 | 92 | if (tooltipContained) { 93 | const t = position[0] / svg.getBoundingClientRect().width; 94 | translate = lerp(t, 0, 100); 95 | } 96 | 97 | this.setState({ 98 | tooltip: { 99 | top, 100 | left, 101 | hidden: false, 102 | html, 103 | translate 104 | } 105 | }); 106 | }, 107 | 108 | onMouseLeave(e) { 109 | if (!this.props.tooltipHtml) { 110 | return; 111 | } 112 | 113 | e.preventDefault(); 114 | 115 | this.setState({ 116 | tooltip: { 117 | hidden: true 118 | } 119 | }); 120 | } 121 | }; 122 | 123 | export default TooltipMixin; 124 | -------------------------------------------------------------------------------- /src/Waveform.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | import Chart from './Chart'; 5 | import Bar from './Bar'; 6 | 7 | import DefaultPropsMixin from './DefaultPropsMixin'; 8 | import HeightWidthMixin from './HeightWidthMixin'; 9 | import ArrayifyMixin from './ArrayifyMixin'; 10 | import StackAccessorMixin from './StackAccessorMixin'; 11 | import StackDataMixin from './StackDataMixin'; 12 | import DefaultScalesMixin from './DefaultScalesMixin'; 13 | import TooltipMixin from './TooltipMixin'; 14 | 15 | const { array, func } = PropTypes; 16 | 17 | // receive array and return a subsampled array of size n 18 | // 19 | // a= the array; 20 | // n= number of sample you want output 21 | const subSample = function (a, n) { 22 | const returnArray = []; 23 | const m = a.length; 24 | const samplingRatio = m / n; 25 | 26 | //just round down for now in case of comma separated 27 | for (let i = 0; i < m; ) { 28 | returnArray.push(a[Math.floor(i)]); 29 | i += samplingRatio; 30 | } 31 | return returnArray; 32 | }; 33 | 34 | const DataSet = createReactClass({ 35 | propTypes: { 36 | data: array.isRequired, 37 | xScale: func.isRequired, 38 | yScale: func.isRequired, 39 | colorScale: func.isRequired, 40 | values: func.isRequired, 41 | label: func.isRequired, 42 | x: func.isRequired, 43 | y: func.isRequired, 44 | y0: func.isRequired 45 | }, 46 | 47 | render() { 48 | const { 49 | data, 50 | yScale, 51 | colorScale, 52 | values, 53 | label, 54 | y, 55 | x0, 56 | onMouseEnter, 57 | onMouseLeave 58 | } = this.props; 59 | 60 | const height = yScale(yScale.domain()[0]); 61 | const bars = data.map(stack => 62 | values(stack).map((e, index) => { 63 | // maps the range [0,1] to the range [0, yDomain] 64 | const yValue = height * y(e); 65 | // center vertically to have upper and lower part of the waveform 66 | const vy = height / 2 - yValue / 2; 67 | //position x(e) * width * 2 because we want equal sapce. 68 | const vx = 2 * x0 * index; 69 | 70 | return ( 71 | 82 | ); 83 | }) 84 | ); 85 | 86 | return {bars}; 87 | } 88 | }); 89 | 90 | const Waveform = createReactClass({ 91 | mixins: [ 92 | DefaultPropsMixin, 93 | HeightWidthMixin, 94 | ArrayifyMixin, 95 | StackAccessorMixin, 96 | StackDataMixin, 97 | DefaultScalesMixin, 98 | TooltipMixin 99 | ], 100 | 101 | getDefaultProps() { 102 | return {}; 103 | }, 104 | 105 | _tooltipHtml(d) { 106 | const [xScale, yScale] = [this._xScale, this._yScale]; 107 | 108 | const html = this.props.tooltipHtml( 109 | this.props.x(d), 110 | this.props.y0(d), 111 | this.props.y(d) 112 | ); 113 | 114 | const midPoint = xScale.rangeBand() / 2; 115 | const xPos = midPoint + xScale(this.props.x(d)); 116 | 117 | const topStack = this._data[this._data.length - 1].values; 118 | let topElement = null; 119 | 120 | // TODO: this might not scale if dataset is huge. 121 | // consider pre-computing yPos for each X 122 | for (let i = 0; i < topStack.length; i++) { 123 | if (this.props.x(topStack[i]) === this.props.x(d)) { 124 | topElement = topStack[i]; 125 | break; 126 | } 127 | } 128 | const yPos = yScale( 129 | this.props.y0(topElement) + this.props.y(topElement) 130 | ); 131 | 132 | return [html, xPos, yPos]; 133 | }, 134 | 135 | render() { 136 | const { 137 | height, 138 | width, 139 | margin, 140 | colorScale, 141 | values, 142 | label, 143 | y, 144 | y0, 145 | x 146 | } = this.props; 147 | 148 | const data = this._data; 149 | const innerWidth = this._innerWidth; 150 | const xScale = this._xScale; 151 | const yScale = this._yScale; 152 | 153 | const preserveAspectRatio = 'none'; 154 | const viewBox = `0 0 ${width} ${height}`; 155 | 156 | // there are two options, if the samples are less than the space available 157 | // we'll stretch the width of bar and inbetween spaces. 158 | // Otherwise we just subSample the dataArray. 159 | let barWidth; 160 | if (data[0].values.length > innerWidth / 2) { 161 | data[0].values = subSample(data[0].values, innerWidth / 2); 162 | barWidth = 1; 163 | } else { 164 | barWidth = innerWidth / 2 / data[0].values.length; 165 | } 166 | 167 | return ( 168 |
169 | 176 | 190 | {this.props.children} 191 | 192 | 193 |
194 | ); 195 | } 196 | }); 197 | 198 | export default Waveform; 199 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as BarChart } from './BarChart'; 2 | export { default as Waveform } from './Waveform'; 3 | export { default as PieChart } from './PieChart'; 4 | export { default as ScatterPlot } from './ScatterPlot'; 5 | export { default as LineChart } from './LineChart'; 6 | export { default as AreaChart } from './AreaChart'; 7 | export { default as Brush } from './Brush'; 8 | export d3 from 'd3'; 9 | --------------------------------------------------------------------------------