├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dist ├── d3kit-es.js ├── d3kit.js ├── d3kit.js.map └── d3kit.min.js ├── docs ├── Developing.md ├── Gallery.md ├── Getting-started.md ├── Versioning.md ├── api │ ├── AbstractChart.md │ ├── AbstractPlate.md │ ├── CanvasChart.md │ ├── CanvasPlate.md │ ├── Chartlet.md │ ├── DivPlate.md │ ├── Helper.md │ ├── HybridChart.md │ ├── LayerOrganizer.md │ ├── SvgChart.md │ ├── SvgPlate.md │ └── index.md └── index.md ├── examples ├── index.html ├── src │ ├── CanvasExample.js │ ├── HybridExample.js │ ├── SvgExample.js │ ├── main.js │ └── util.js ├── style.css └── vendor │ ├── milligram.min.css │ └── normalize.css ├── karma.conf.js ├── package.json ├── resources └── skeleton.png ├── rollup.config.examples.js ├── rollup.config.js ├── src ├── Base.js ├── Base.spec.js ├── chartlet.js ├── chartlet.spec.js ├── charts │ ├── AbstractChart.js │ ├── AbstractChart.spec.js │ ├── CanvasChart.js │ ├── CanvasChart.spec.js │ ├── HybridChart.js │ ├── HybridChart.spec.js │ ├── SvgChart.js │ └── SvgChart.spec.js ├── helper.js ├── helper.spec.js ├── layerOrganizer.js ├── layerOrganizer.spec.js ├── main.js ├── main.spec.js └── plates │ ├── AbstractPlate.js │ ├── AbstractPlate.spec.js │ ├── CanvasPlate.js │ ├── CanvasPlate.spec.js │ ├── DivPlate.js │ ├── DivPlate.spec.js │ ├── SvgPlate.js │ └── SvgPlate.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "env" : { 4 | "test": { 5 | "plugins": [ 6 | ["istanbul", { 7 | "exclude": ["src/**/*.spec.js", "node_modules/**/*"] 8 | }] 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "mocha" 5 | ], 6 | "rules": { 7 | "mocha/no-exclusive-tests": "error" 8 | }, 9 | globals: { 10 | describe: true, 11 | it: true, 12 | expect: true, 13 | before: true 14 | } 15 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | coverage 4 | npm-debug.log 5 | examples/dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.0" 4 | - "5.0" 5 | cache: 6 | directories: 7 | - travis_phantomjs 8 | before_install: 9 | # Upgrade PhantomJS to v2.1.1. 10 | - "export PHANTOMJS_VERSION=2.1.1" 11 | - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH" 12 | - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi" 13 | - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi" 14 | - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi" 15 | - "phantomjs --version" 16 | before_script: 17 | - export DISPLAY=:99.0 18 | - sh -e /etc/init.d/xvfb start 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change logs 2 | 3 | ## v3.x.x 4 | 5 | ### 3.2.0 6 | - Add `HybridChart` 7 | - Implement plating systems with `SvgPlate`, `CanvasPlate` and `DivPlate`. 8 | 9 | ### 3.1.3 10 | - Fix issue with CanvasChart when setting style `width` and `height` by adding `'px'`. 11 | 12 | ### 3.1.2 13 | - Revert `options.offset` format from object `{ x, y }` to array `[x, y]` for consistency with v1-2. 14 | 15 | ### 3.1.1 16 | 17 | - Use existing `fitOptions` when calling `.fit()` with no argument. 18 | - Fix issue with fit watching when it does not resize as expected. 19 | 20 | ### 3.1.0 21 | 22 | Change from using `DEFAULT_OPTIONS` variable to store default options to static function `.getDefaultOptions()` that creates and returns a new Object. This resolve issues when the value in the options is not plain Object (for example, a scale) and multiple chart instances try to access and modify the default value (scale). 23 | 24 | ### 3.0.0 25 | 26 | Rewrite the chart abstraction in es6 and split `Skeleton` into `SvgChart` and `CanvasChart`, both extends from `AbstractChart`. The resize/auto-resize logic are revisited and published as another library called `slimfit`. (d3Kit wraps and includes slimfit by default.) 27 | 28 | ## v2.x.x 29 | 30 | ### 2.0.0 (2016-08-16) 31 | 32 | Make d3Kit compatible with D3 v4. Key changes are due to: 33 | 34 | - Removal of `d3.functor` and `d3.rebind`. Implement helper functions as replacements. 35 | - API changes for `d3.dispatch`. Now use `dispatch.call('x', ...)` instead of `dispatch.x(...)` 36 | 37 | The npm package also remove `d3` from `dependencies` and add `d3-selection` and `d3-dispatch` to `peerDependencies` instead. 38 | 39 | In terms of development, switch from grunt to gulp and webpack and prepare to migrate each module to es2015. 40 | 41 | ## v1.x.x 42 | 43 | ### 1.1.0 (2016-04-07) 44 | 45 | Add an option to select tag type for LayerOrganizer 46 | 47 | ```javascript 48 | new LayerOrganizer(container); //will create layers as by default 49 | new LayerOrganizer(container, 'div'); // will create layers as
50 | ``` 51 | 52 | ### 1.0.11 (2016-02-23) 53 | 54 | Change main file to point to `d3kit.min.js` instead of `d3kit.js` 55 | 56 | ### 1.0.10 (2016-02-17) 57 | 58 | Update D3 version in the dependencies to 3.5.16 59 | 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Recommendations and requirements for how to best contribute to **d3Kit**. As always, thanks for contributing, and we hope these guidelines make it easier and shed some light on our approach and processes. 2 | 3 | ### Key branches 4 | - `master` is the latest released version 5 | - `dev` is where development happens and all pull requests should be submitted 6 | 7 | ### Versioning 8 | 9 | **d3Kit** comforms to the [Semantic Versioning](http://semver.org/) standard. 10 | 11 | ### Pull requests 12 | - Submit pull requests against the `dev` branch 13 | - Try not to pollute your pull request with unintended changes. Please keep them simple and small. 14 | 15 | ### License 16 | By contributing your code, you agree to license your contributions under the terms of the MIT license: 17 | https://github.com/twitter/d3kit/blob/master/LICENSE 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Twitter, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3Kit 2 | 3 | [![status: retired](https://opensource.twitter.dev/status/retired.svg)](https://opensource.twitter.dev/status/#retired) 4 | 5 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] 6 | [![CDNJS version](https://img.shields.io/cdnjs/v/d3kit.svg)](https://cdnjs.com/libraries/d3kit) 7 | 8 | **d3Kit** provides thin scaffold for creating *reusable* and *responsive* charts with [D3](https://github.com/mbostock/d3). 9 | It aims to relieve you from the same groundwork tasks you found yourself doing again and again. 10 | 11 | [Introduction slides](http://d3kit.surge.sh) | 12 | [Getting started guide](docs/Getting-started.md) | 13 | [API Reference](docs/api/index.md) | 14 | [All Documentation](docs/TableOfContent.md) 15 | 16 | For developers who have tried d3Kit v1-2, d3Kit v3 was rewritten to support D3 v4, consider several new use cases (``, for example) and use ES6 class for the implementation, making every chart can be extended easily. 17 | Documentation of version 1-2 can be found [here](https://github.com/twitter/d3kit/tree/be7a37738fb5661c84920faf7f1a981025fe4993/docs) 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm install d3kit --save 23 | ``` 24 | 25 | See [getting start guide](docs/Getting-started.md) for more details. 26 | 27 | ## Examples 28 | 29 | Here are a few examples of d3Kit in action: 30 | 31 | * [Using d3Kit to scaffold \](http://bl.ocks.org/kristw/09d462027bb50e80cec0c53c0856e663) 32 | * [Using d3Kit to scaffold \](http://bl.ocks.org/kristw/2cc83b10a1677a16f6448a5108b322a1) 33 | * [SVG Scatterplot](http://bl.ocks.org/kristw/4628672d57f5fe822bb4c84b682abb6e) 34 | * [Canvas Scatterplot](http://bl.ocks.org/kristw/840bd7750742458f20f00749c13e6241) 35 | * [Simple bar graph](http://bl.ocks.org/kristw/12991fb0fec6e9287980902bfb746ef7) - show how to use d3Kit for scaffolding svg with D3 margin convention. 36 | * [Reusable bar graph](http://bl.ocks.org/kristw/f57f88f6f60d2ef22992d7866ba2933f) - forked from the bar chart above and make it a reusable component. 37 | 38 | 39 | ## Why should you use d3Kit? 40 | 41 | ### Avoid coding basic building blocks again and again. 42 | 43 | 😫 You are tired of copying the boilerplate `d3.select('body').append('svg')...` from D3 examples. 44 | 45 | **Solution:** There is `SvgChart` for that. 46 | 47 | 😫 You want to create a chart on `` but never remember how to handle different screen resolution (retina display). 48 | 49 | **Solution:** There is `CanvasChart` for that. 50 | 51 | 🤔 You want to use `` and `` together. 52 | 53 | **Solution:** There is `HybridChart` for that. 54 | 55 | 😫 You use [D3's margin convention](http://bl.ocks.org/mbostock/3019563) and are tired of copy pasting from [Mike's block](http://bl.ocks.org/mbostock/3019563). 56 | 57 | **Solution:** All `SvgChart`, `CanvasChart` and `HybridChart` extends `AbstractChart`, which was built based on the margin convention. 58 | 59 | ### Reusable charts 60 | 61 | 🤔 You want to create a *reusable* chart in D3. 62 | 63 | 😫 You want to create a *responsive* chart, but are tired of listening to window resize or manually polling for element size. 64 | 65 | 🤔 You want to make a responsive chart that maintains aspect ratio. 66 | 67 | **Solution:** Create a chart extends from `SvgChart`, `CanvasChart`, `HybridChart` or `AbstractChart` then you get all of the above handled. 68 | 69 | 🤔 You are familiar with creating charts in D3 and want to adapt them easily into React or angular components. 70 | 71 | **Solution:** Currently there are [react-d3kit](https://github.com/kristw/react-d3kit) and [angular-d3kit-adapter](https://github.com/kristw/angular-d3kit-adapter) that can convert charts written in d3Kit into React and Angular 1 components, respectively, in a few lines of code. 72 | 73 | ## What is d3Kit? 74 | 75 | The core of d3Kit are base classes for creating a chart. Currently there are `SvgChart`, `CanvasChart` and `HybridChart`. All are extended from `AbstractChart`. 76 | 77 | ### AbstractChart 78 | 79 | * takes a target container (usually a `
`) and helps you build a chart inside. 80 | * encapsulates [D3's margin convention](http://bl.ocks.org/mbostock/3019563). The dimension of each chart is defined by `width`, `height` and `margin`. 81 | * `chart.width()` get/set the total width (including margin) 82 | * `chart.height()` get/set the total height (including margin) 83 | * `chart.margin()` get/set the margin 84 | * `chart.getInnerWidth()` returns width excluding margin. This is usually used as the boundary of the x-axis. 85 | * `chart.getInnerHeight()` returns height excluding margin. This is usually used as the boundary of the y-axis. 86 | * can resize the chart to be percentage of a container and/or maintain aspect ratio 87 | * `chart.fit(fitOptions:Object)` Calling this function with single argument will resize the chart to fit into the container once. Please refer to [slimfit documentation](https://github.com/kristw/slimfit) for `fitOptions`. 88 | * can listen to resize (either window or element) and update the chart size to fit container. 89 | * `chart.fit(fitOptions:Object, watchOptions:Boolean/Object)` Calling with two arguments, such as `chart.fit({...}, true)` or `chart.fit({...}, {...})`, will enable watching. Please refer to [slimfit documentation](https://github.com/kristw/slimfit) for `fitOptions` and `watchOptions` 90 | * `chart.stopFitWatcher()` will disable the watcher. 91 | * dispatches event `resize` when the chart is resized. 92 | * `chart.on('resize', listener)` is then use to register what to do after the chart is resized. 93 | * defines two main input channels `.data(...)` and `.options(...)` and dispatches event `data` and `options` when they are changed, respectively. 94 | * `chart.data(data)` get/set data. 95 | * `chart.options(options)` get/merge options 96 | * `chart.on('data', listener)` 97 | * `chart.on('options', listener)` 98 | * assumes little about how you implement a chart. You can extends the class and implements it the way you want. 99 | 100 | Most of the time you will not need to access `AbstractChart` directly, but you will use one of its children: `SvgChart`, `CanvasChart` or `HybridChart`. 101 | 102 | ### SvgChart 103 | 104 | This class creates `` boilerplate inside the container. 105 | 106 | ### CanvasChart 107 | 108 | While `SvgChart` creates necessary element for building chart with ``. This class creates `` inside the container. It also handles different screen resolution for you (retina display vs. standard display). 109 | 110 | ### HybridChart 111 | 112 | Thought about using `` and `` in combination? Here it is. A `HybridChart` creates both `` and `` inside the container. 113 | 114 | ### Build your own chart with `plates` 115 | 116 | If `SvgChart`, `CanvasChart` or `HybridChart` does not fit your need yet, you can create your own. 117 | 118 | Under the hood, d3Kit use its "plating" system to wrap different type of components (``, ``, etc.). The current implementation includes three types of plates: `SvgPlate`, `CanvasPlate` and `DivPlate`. 119 | 120 | Think of `AbstractChart` as a container. **Any resizing done to the chart will be applied to the plates in it by d3Kit.** This abstraction helps you think of a chart as one piece and not to worry about how to keep track of each children size. Then you can just focus on what to drawn on svg or canvas based on the current dimension of the chart. 121 | 122 | * An `SvgChart` is an `AbstractChart` that has an `SvgPlate` in it. 123 | * A `CanvasChart` is an `AbstractChart` that has a `CanvasPlate` in it. 124 | * A `HybridChart`, as you may guess, is an `AbstractChart` that has two plates (`CanvasPlate` and `SvgPlate`) in it. 125 | 126 | Now if you want to create a chart with multiple canvases and svg, just create a new subclass. 127 | 128 | ```javascript 129 | import { AbstractChart, CanvasPlate, SvgPlate } from 'd3kit'; 130 | 131 | class CustomChart extends AbstractChart { 132 | constructor(selector, ...options) { 133 | super(selector, ...options); 134 | 135 | this.addPlate('canvas1', new CanvasPlate()); 136 | // now access D3 selection of this element 137 | // via this.plates.canvas1.getSelection() 138 | 139 | this.addPlate('canvas2', new CanvasPlate()); 140 | // now access D3 selection of this element 141 | // via this.plates.canvas2.getSelection() 142 | 143 | this.addPlate('svg', new SvgPlate()); 144 | // now access D3 selection of this element 145 | // via this.plates.svg.getSelection() 146 | 147 | this.updateDimensionNow(); 148 | } 149 | } 150 | ``` 151 | 152 | Once you call 153 | 154 | ```javascript 155 | new CustomChart('#my-chart'); 156 | ``` 157 | 158 | This will be created. 159 | 160 | ```html 161 |
162 |
163 | 164 | 165 | 166 |
167 |
168 | ``` 169 | 170 | ## Other features 171 | 172 | ### LayerOrganizer 173 | 174 | This was created from a habit of creating many ``s inside the root ``. 175 | 176 | #### Input 177 | 178 | ```html 179 | 180 | 181 | 182 | ``` 183 | 184 | ```javascript 185 | const layers = new LayerOrganizer(d3.selection('.container')); 186 | layers.create(['content', 'x-axis', 'y-axis']); 187 | ``` 188 | 189 | #### Output 190 | 191 | ```html 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | ``` 203 | 204 | All `SvgChart` includes `chart.layers` by default, which is `new LayerOrganizer(chart.container)`. 205 | 206 | There are more features. [Read more here.](docs/LayerOrganizer.md) 207 | 208 | ### Chartlet 209 | 210 | d3Kit v1-2 also helps you create reusable subcomponent (a.k.a. Chartlet). We have not ported it to v3 yet. 211 | 212 | ## Documentation 213 | 214 | Want to learn more? Follow these links. 215 | 216 | * [Getting started guide](docs/Getting-started.md) 217 | * [API Reference](docs/api/index.md) 218 | * [All Documentation](docs/TableOfContent.md) 219 | 220 | ## Appendix 221 | 222 | A diagram explaining D3's margin convention 223 | 224 |

225 | 226 | 227 | 228 |

229 | 230 | ## Authors 231 | 232 | * Robert Harris / [@trebor](https://twitter.com/trebor) 233 | * Krist Wongsuphasawat / [@kristw](https://twitter.com/kristw) 234 | 235 | ## License 236 | 237 | © 2015-2017 [Twitter, Inc.](https://twitter.com) MIT License 238 | 239 | [npm-image]: https://badge.fury.io/js/d3kit.svg 240 | [npm-url]: https://npmjs.org/package/d3kit 241 | [travis-image]: https://travis-ci.org/twitter/d3kit.svg?branch=master 242 | [travis-url]: https://travis-ci.org/twitter/d3kit 243 | [daviddm-image]: https://david-dm.org/twitter/d3kit.svg?theme=shields.io 244 | [daviddm-url]: https://david-dm.org/twitter/d3kit 245 | -------------------------------------------------------------------------------- /dist/d3kit.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("d3-selection"),require("d3-dispatch")):"function"==typeof define&&define.amd?define(["exports","d3-selection","d3-dispatch"],e):e(t.d3Kit=t.d3Kit||{},t.d3,t.d3)}(this,function(t,e,n){"use strict";function i(t){var e="undefined"==typeof t?"undefined":x(t);return null!=t&&("object"==e||"function"==e)}function r(t){var e=i(t)?T.call(t):"";return e==N||e==C||e==R}function a(t){return null!=t&&"object"==("undefined"==typeof t?"undefined":x(t))}function s(t){return"symbol"==("undefined"==typeof t?"undefined":x(t))||a(t)&&$.call(t)==G}function o(t){if("number"==typeof t)return t;if(s(t))return z;if(i(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=i(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(q,"");var n=U.test(t);return n||K.test(t)?V(t.slice(2),n?2:8):B.test(t)?z:+t}function h(t,e,n){function r(e){var n=g,i=p;return g=p=void 0,_=e,m=t.apply(i,n)}function a(t){return _=t,y=setTimeout(u,e),b?r(t):m}function s(t){var n=t-w,i=t-_,r=e-n;return O?Q(r,v-i):r}function h(t){var n=t-w,i=t-_;return void 0===w||n>=e||0>n||O&&i>=v}function u(){var t=L();return h(t)?l(t):void(y=setTimeout(u,s(t)))}function l(t){return y=void 0,k&&g?r(t):(g=p=void 0,m)}function c(){void 0!==y&&clearTimeout(y),_=0,g=w=p=y=void 0}function f(){return void 0===y?m:l(L())}function d(){var t=L(),n=h(t);if(g=arguments,p=this,w=t,n){if(void 0===y)return a(w);if(O)return y=setTimeout(u,e),r(w)}return void 0===y&&(y=setTimeout(u,e)),m}var g,p,v,m,y,w,_=0,b=!1,O=!1,k=!0;if("function"!=typeof t)throw new TypeError(Z);return e=o(e)||0,i(n)&&(b=!!n.leading,O="maxWait"in n,v=O?J(o(n.maxWait)||0,e):v,k="trailing"in n?!!n.trailing:k),d.cancel=c,d.flush=f,d}function u(t,e,n){var r=!0,a=!0;if("function"!=typeof t)throw new TypeError(X);return i(n)&&(r="leading"in n?!!n.leading:r,a="trailing"in n?!!n.trailing:a),h(t,e,{leading:r,maxWait:e,trailing:a})}function l(t){return null==t?"":String(t).replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")}function c(t){return null==t?"\\s":t.source?t.source:"["+l(t)+"]"}function f(t,e){if(null==t)return"";if(!e&&Y)return Y.call(t);var n=c(e),i=new RegExp("^"+n+"+|"+n+"+$","g");return String(t).replace(i,"")}function d(t){return f(t).replace(/([A-Z])/g,"-$1").replace(/[-_\s]+/g,"-").toLowerCase()}function g(t){t=t||{};for(var e=1;e-1){var n=function(){var t=+e.replace("%","")/100;return{v:function(e,n){return n*t}}}();if("object"===("undefined"==typeof n?"undefined":x(n)))return n.v}return function(){return+e.replace("px","")}}function D(t){function e(t,e){var n=arguments.length<=2||void 0===arguments[2]?"":arguments[2],i=e.split("."),r=void 0,a=void 0;i.length>1?(a=i[0].length>0?i[0]:h,r=i[1]):(a=h,r=i[0]);var s=""+n+r;if(u.hasOwnProperty(s))throw new Error("invalid or duplicate layer id: "+s);var o=d(r)+"-layer",l=t.append(a).classed(o,!0);return u[s]=l,l}function n(t,r){var a=arguments.length<=2||void 0===arguments[2]?"":arguments[2];if(Array.isArray(r))return r.map(function(e){return n(t,e,a)});if(i(r)){var s=Object.keys(r),o=W(s,1),h=o[0],u=e(t,h,a);return n(u,r[h],""+a+h+"/"),u}return e(t,r,a)}function r(e){return n(t,e)}function a(t){return Array.isArray(t)?t.map(r):r(t)}function s(t){return u[t]}function o(t){return!!u[t]}var h=arguments.length<=1||void 0===arguments[1]?"g":arguments[1],u={};return{create:a,get:s,has:o}}var x="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},E=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},A=function(){function t(t,e){for(var n=0;ni;i++)n[i]=arguments[i];if(1===n.length){var a=n[0],s=r(a)?a():a;if(s instanceof t)this.width=s.width,this.height=s.height;else if(O(s))this.width=s.clientWidth,this.height=s.clientHeight;else if(Array.isArray(s))this.width=s[0],this.height=s[1];else{if(!(_(s)&&_(s.width)&&_(s.height))){var o=new Error("Unsupported input. Must be either\n DOMNode, Array or Object with field width and height,\n or a function that returns any of the above.");throw o.value=a,o}this.width=s.width,this.height=s.height}}else{var h=n[0],u=n[1];this.width=h,this.height=u}}return A(t,[{key:"isEqual",value:function(e){if(e instanceof t)return this.width===e.width&&this.height===e.height;var n=new t(e);return this.width===n.width&&this.height===n.height}},{key:"toArray",value:function(){return[this.width,this.height]}},{key:"toObject",value:function(){return{width:this.width,height:this.height}}}]),t}(),nt=function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];E(this,t);var n=e||{},i=n.mode,r=void 0===i?t.MODE_BASIC:i,a=n.width,s=void 0===a?"100%":a,o=n.height,h=void 0===o?null:o,u=n.ratio,l=void 0===u?1:u,c=n.maxWidth,f=void 0===c?null:c,d=n.maxHeight,g=void 0===d?null:d;r===t.MODE_ASPECT_RATIO?(this.wFn=k(f),this.hFn=k(g),this.options={mode:r,ratio:l,maxWidth:f,maxHeight:g}):(this.wFn=k(s),this.hFn=k(h),this.options={mode:r,width:s,height:h})}return A(t,[{key:"fit",value:function(){var e=arguments.length<=0||void 0===arguments[0]?w("box"):arguments[0],n=arguments.length<=1||void 0===arguments[1]?w("container"):arguments[1],i=new et(e),r=i.width,a=i.height,s=new et(n),o=s.width,h=s.height,u=void 0;if(this.options.mode===t.MODE_ASPECT_RATIO){var l=this.options.ratio,c=this.wFn(o,o),f=this.hFn(h,h),d=Math.floor(l*f);u=c>=d?new et(d,f):new et(c,Math.floor(c/l))}else u=new et(this.wFn(r,o),this.hFn(a,h));return{dimension:u,changed:!u.isEqual(i)}}}]),t}();nt.MODE_BASIC="basic",nt.MODE_ASPECT_RATIO="aspectRatio";var it=function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];E(this,t);var n=e||{},i=n.mode,r=void 0===i?t.MODE_WINDOW:i,a=n.target,s=void 0===a?null:a,o=n.interval,h=void 0===o?200:o;r!==t.MODE_POLLING||s||w("options.target"),this.mode=r,this.target=s,this.interval=h,this.check=this.check.bind(this),this.throttledCheck=u(this.check,this.interval),this.isWatching=!1,this.listeners={change:[]}}return A(t,[{key:"hasTargetChanged",value:function(){if(!this.target)return!0;var t=new et(this.target);return this.currentDim&&t.isEqual(this.currentDim)?!1:(this.currentDim=t,!0)}},{key:"check",value:function(){return this.hasTargetChanged()&&this.dispatch("change",this.currentDim),this}},{key:"dispatch",value:function(t){for(var e=this,n=arguments.length,i=Array(n>1?n-1:0),r=1;n>r;r++)i[r-1]=arguments[r];return this.listeners[t].forEach(function(t){return t.apply(e,i)}),this}},{key:"on",value:function(t,e){return-1===this.listeners[t].indexOf(e)&&this.listeners[t].push(e),this}},{key:"off",value:function(t,e){return this.listeners[t]=this.listeners[t].filter(function(t){return t!==e}),this}},{key:"start",value:function(){return this.isWatching||(this.target&&(this.currentDim=new et(this.target)),this.mode===t.MODE_WINDOW?window.addEventListener("resize",this.throttledCheck):this.mode===t.MODE_POLLING&&(this.intervalId=window.setInterval(this.check,this.interval)),this.isWatching=!0),this}},{key:"stop",value:function(){return this.isWatching&&(this.mode===t.MODE_WINDOW?window.removeEventListener("resize",this.throttledCheck):this.mode===t.MODE_POLLING&&this.intervalId&&(window.clearInterval(this.intervalId),this.intervalId=null),this.isWatching=!1),this}},{key:"destroy",value:function(){return this.stop(),this.listeners.change=[],this}}]),t}();it.MODE_WINDOW="window",it.MODE_POLLING="polling";var rt=function(t){function e(){var t=arguments.length<=0||void 0===arguments[0]?w("box"):arguments[0],n=arguments.length<=1||void 0===arguments[1]?w("container"):arguments[1],i=arguments[2],r=arguments[3];E(this,e);var a=P(this,Object.getPrototypeOf(e).call(this,r)),s=new nt(i);return a.fit=function(){return s.fit(t,n)},a}return j(e,t),A(e,[{key:"check",value:function(){if(this.hasTargetChanged()){var t=this.fit(),e=t.changed,n=t.dimension;e&&this.dispatch("change",n)}return this}}]),e}(it),at=function(){function t(){E(this,t);for(var e=arguments.length,n=Array(e),i=0;e>i;i++)n[i]=arguments[i];var r=g.apply(void 0,[this.constructor.getDefaultOptions()].concat(n));this._state={width:r.initialWidth,height:r.initialHeight,options:r},this._updateDimension=h(this._updateDimension.bind(this),1)}return A(t,null,[{key:"getDefaultOptions",value:function(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];return g.apply(void 0,[{initialWidth:720,initialHeight:500,margin:{top:30,right:30,bottom:30,left:30},offset:[.5,.5],pixelRatio:window.devicePixelRatio||1}].concat(e))}}]),A(t,[{key:"copyDimension",value:function(t){if(t){var e=t._state,n=e.width,i=e.height,r=t._state.options,a=r.offset,s=r.margin,o=r.pixelRatio;g(this._state,{width:n,height:i,options:{offset:a.concat(),margin:s,pixelRatio:o}}),this._updateDimension()}return this}},{key:"width",value:function(){if(0===arguments.length)return this._state.width;var t=Math.floor(+(arguments.length<=0?void 0:arguments[0]));return t!==this._state.width&&(this._state.width=t,this._updateDimension()),this}},{key:"height",value:function(){if(0===arguments.length)return this._state.height;var t=Math.floor(+(arguments.length<=0?void 0:arguments[0]));return t!==this._state.height&&(this._state.height=t,this._updateDimension()),this}},{key:"dimension",value:function(){if(0===arguments.length)return[this._state.width,this._state.height];var t=arguments.length<=0?void 0:arguments[0],e=W(t,2),n=e[0],i=e[1];return this.width(n).height(i),this}},{key:"margin",value:function(){if(0===arguments.length)return this._state.options.margin;var t=this._state.options.margin,e=p({},this._state.options.margin,arguments.length<=0?void 0:arguments[0]),n=Object.keys(e).some(function(n){return t[n]!==e[n]});return n&&(this._state.options.margin=e,this._updateDimension()),this}},{key:"offset",value:function(){if(0===arguments.length)return this._state.options.offset;var t=arguments.length<=0?void 0:arguments[0],e=W(this._state.options.offset,2),n=e[0],i=e[1],r=W(t,2),a=r[0],s=r[1];return(n!==a||i!==s)&&(this._state.options.offset=t,this._updateDimension()),this}},{key:"pixelRatio",value:function(){if(0===arguments.length)return this._state.options.pixelRatio;var t=+(arguments.length<=0?void 0:arguments[0]);return t!==this._state.options.pixelRatio&&(this._state.options.pixelRatio=t,this._updateDimension()),this}},{key:"_updateDimension",value:function(){return this}},{key:"updateDimensionNow",value:function(){return this._updateDimension(),this._updateDimension.flush(),this}}]),t}(),st=function(t){function r(t){var n;E(this,r);for(var i=arguments.length,a=Array(i>1?i-1:0),s=1;i>s;s++)a[s-1]=arguments[s];var o=P(this,(n=Object.getPrototypeOf(r)).call.apply(n,[this].concat(a)));p(o._state,{innerWidth:0,innerHeight:0,fitOptions:null,data:null,plates:[]}),o.container=e.select(t),o.container.style("line-height",0),o.chartRoot=o.container.append("div").classed("d3kit-chart-root",!0).style("display","inline-block").style("position","relative").style("line-height",0),o.plates={};var u=o.constructor.getCustomEventNames();return o.setupDispatcher(u),o._dispatchData=h(o._dispatchData.bind(o),1),o._dispatchOptions=h(o._dispatchOptions.bind(o),1),o}return j(r,t),A(r,null,[{key:"getCustomEventNames",value:function(){return[]}}]),A(r,[{key:"addPlate",value:function(t,e,n){if(this.plates[t])throw new Error("Plate with this name already exists",t);return this._state.plates.push(e),this.plates[t]=e,n?e:(e.getSelection().classed("d3kit-plate",!0).style("position","absolute").style("top",0).style("left",0),this.chartRoot.append(function(){return e.getNode()}),this)}},{key:"removePlate",value:function(t){var e=this.plates[t];if(e){var n=this._state.plates.indexOf(e);n>-1&&this._state.plates.splice(n,1),e.getNode().parentNode===this.chartRoot.node()&&this.chartRoot.node().removeChild(e.getNode()),delete this.plates[t]}return this}},{key:"setupDispatcher",value:function(){var t=arguments.length<=0||void 0===arguments[0]?[]:arguments[0];return this._customEventNames=t,this._eventNames=r.DEFAULT_EVENTS.concat(t),this.dispatcher=n.dispatch.apply(this,this._eventNames),this}},{key:"getCustomEventNames",value:function(){return this._customEventNames}},{key:"getInnerWidth",value:function(){return this._state.innerWidth}},{key:"getInnerHeight",value:function(){return this._state.innerHeight}},{key:"data",value:function(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];if(0===e.length)return this._state.data;var i=e[0];return this._state.data=i,this._dispatchData(),this}},{key:"options",value:function(){for(var t=arguments.length,e=Array(t),n=0;t>n;n++)e[n]=arguments[n];if(0===e.length)return this._state.options;var i=e[0],r=p({},i);return i.margin&&(this.margin(i.margin),delete r.margin),i.offset&&(this.offset(i.offset),delete r.offset),i.pixelRatio&&(this.pixelRatio(i.pixelRatio),delete r.pixelRatio),this._state.options=g(this._state.options,r),this._dispatchOptions(),this}},{key:"_updateDimension",value:function(){var t=this,e=this._state,n=e.width,i=e.height,r=e.plates,a=this._state.options.margin,s=a.top,o=a.right,h=a.bottom,u=a.left;this._state.innerWidth=n-u-o,this._state.innerHeight=i-s-h,this.chartRoot.style("width",n+"px").style("height",i+"px"),r.forEach(function(e){e.copyDimension(t).updateDimensionNow()});var l=this._state,c=l.innerWidth,f=l.innerHeight;return this.dispatcher.apply("resize",this,[n,i,c,f]),this}},{key:"hasData",value:function(){var t=this._state.data;return null!==t&&void 0!==t}},{key:"hasNonZeroArea",value:function(){var t=this._state,e=t.innerWidth,n=t.innerHeight;return e>0&&n>0}},{key:"fit",value:function(t){var e=this,n=arguments.length<=1||void 0===arguments[1]?!1:arguments[1];t&&(this._state.fitOptions=t);var r=new nt(this._state.fitOptions),a=r.fit(this.dimension(),this.container.node()),s=a.changed,o=a.dimension;s&&this.dimension([o.width,o.height]);var h=!!n;return h&&(this.fitWatcher&&this.fitWatcher.destroy(),this.fitWatcher=new rt(function(){return e.dimension()},this.container.node(),this._state.fitOptions,i(n)?n:null).on("change",function(t){return e.dimension([t.width,t.height])}).start()),this}},{key:"stopFitWatcher",value:function(){return this.fitWatcher&&(this.fitWatcher.destroy(),this.fitWatcher=null),this}},{key:"_dispatchData",value:function(){return this.dispatcher.call("data",this,this._state.data),this}},{key:"_dispatchOptions",value:function(){return this.dispatcher.call("options",this,this._state.options),this}},{key:"on",value:function(t,e){return this.dispatcher.on(t,e),this}},{key:"off",value:function(t){return this.dispatcher.on(t,null),this}},{key:"dispatchAs",value:function(t){var e=this;return function(){for(var n=arguments.length,i=Array(n),r=0;n>r;r++)i[r]=arguments[r];e.dispatcher.apply(t,e,i)}}},{key:"destroy",value:function(){var t=this;return this._eventNames.forEach(function(e){t.off(e)}),this.stopFitWatcher(),this}}]),r}(at);st.DEFAULT_EVENTS=["data","options","resize"];var ot=function(t){function n(t){var i;E(this,n);for(var r=arguments.length,a=Array(r>1?r-1:0),s=1;r>s;s++)a[s-1]=arguments[s];var o=P(this,(i=Object.getPrototypeOf(n)).call.apply(i,[this].concat(a)));return o.node=t,o.selection=e.select(o.node),o}return j(n,t),A(n,[{key:"getNode",value:function(){return this.node}},{key:"getSelection",value:function(){return this.selection}}]),n}(at),ht=function(t){function e(){var t;E(this,e);for(var n=arguments.length,i=Array(n),r=0;n>r;r++)i[r]=arguments[r];return P(this,(t=Object.getPrototypeOf(e)).call.apply(t,[this,document.createElement("canvas")].concat(i)))}return j(e,t),A(e,[{key:"getContext2d",value:function(){var t=(this.width(),this.height(),this.pixelRatio()),e=this.margin(),n=e.top,i=e.left,r=this.offset(),a=W(r,2),s=a[0],o=a[1],h=this.node.getContext("2d");return h.setTransform(1,0,0,1,0,0),h.scale(t,t),h.translate(i+s,n+o),h}},{key:"clear",value:function(){var t=this.width(),e=this.height(),n=this.pixelRatio(),i=this.node.getContext("2d");return i.setTransform(1,0,0,1,0,0),i.scale(n,n),i.clearRect(0,0,t,e),this}},{key:"_updateDimension",value:function(){var t=this.width(),e=this.height(),n=this.pixelRatio();return this.node.setAttribute("width",t*n),this.node.setAttribute("height",e*n),this.node.style.width=t+"px",this.node.style.height=e+"px",this}}]),e}(ot),ut=function(t){function e(t){var n;E(this,e);for(var i=arguments.length,r=Array(i>1?i-1:0),a=1;i>a;a++)r[a-1]=arguments[a];var s=P(this,(n=Object.getPrototypeOf(e)).call.apply(n,[this,t].concat(r)));return s.addPlate("canvas",new ht),s.canvas=s.plates.canvas.getSelection(),s.updateDimensionNow(),s}return j(e,t),A(e,[{key:"getContext2d",value:function(){return this.plates.canvas.getContext2d()}},{key:"clear",value:function(){return this.plates.canvas.clear(),this}}]),e}(st),lt=function(t){function e(){var t;E(this,e);for(var n=arguments.length,i=Array(n),r=0;n>r;r++)i[r]=arguments[r];var a=P(this,(t=Object.getPrototypeOf(e)).call.apply(t,[this,document.createElementNS("http://www.w3.org/2000/svg","svg")].concat(i)));return a.rootG=a.selection.append("g"),a.layers=new D(a.rootG),a}return j(e,t),A(e,[{key:"_updateDimension",value:function(){var t=this.width(),e=this.height(),n=this.margin(),i=n.top,r=n.left,a=this.offset(),s=W(a,2),o=s[0],h=s[1];return this.selection.attr("width",t).attr("height",e),this.rootG.attr("transform","translate("+(r+o)+","+(i+h)+")"),this}}]),e}(ot),ct=function(t){function e(t){var n;E(this,e);for(var i=arguments.length,r=Array(i>1?i-1:0),a=1;i>a;a++)r[a-1]=arguments[a];var s=P(this,(n=Object.getPrototypeOf(e)).call.apply(n,[this,t].concat(r)));s.addPlate("svg",new lt);var o=s.plates.svg;return s.svg=o.getSelection(),s.rootG=o.rootG,s.layers=o.layers,s.updateDimensionNow(),s}return j(e,t),e}(ut),ft=function(t){function e(t){var n;E(this,e);for(var i=arguments.length,r=Array(i>1?i-1:0),a=1;i>a;a++)r[a-1]=arguments[a];var s=P(this,(n=Object.getPrototypeOf(e)).call.apply(n,[this,t].concat(r)));s.addPlate("svg",new lt);var o=s.plates.svg;return s.svg=o.getSelection(),s.rootG=o.rootG,s.layers=o.layers,s.updateDimensionNow(),s}return j(e,t),e}(st),dt=function(t){function e(){var t;E(this,e);for(var n=arguments.length,i=Array(n),r=0;n>r;r++)i[r]=arguments[r];return P(this,(t=Object.getPrototypeOf(e)).call.apply(t,[this,document.createElement("div")].concat(i)))}return j(e,t),A(e,[{key:"_updateDimension",value:function(){var t=this.width(),e=this.height(),n=this.margin();return this.node.style.width=t-n.left-n.right+"px",this.node.style.height=e-n.top-n.bottom+"px",this.node.style.marginLeft=n.left+"px",this.node.style.marginRight=n.right+"px",this.node.style.marginTop=n.top+"px",this.node.style.marginBottom=n.bottom+"px",this}}]),e}(ot);t.helper=tt,t.AbstractChart=st,t.CanvasChart=ut,t.HybridChart=ct,t.SvgChart=ft,t.AbstractPlate=ot,t.CanvasPlate=ht,t.DivPlate=dt,t.SvgPlate=lt,t.LayerOrganizer=D,Object.defineProperty(t,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /docs/Developing.md: -------------------------------------------------------------------------------- 1 | > [Docs](./index.md) ▸ Development ▸ **Developing d3Kit** 2 | 3 | ### One-time setup 4 | 5 | 1) Install [node.js](http://nodejs.org/) 6 | 7 | 2) Load development tool and javascript dependencies: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | ### Test 14 | 15 | ```bash 16 | # Run this command to test once. 17 | npm run test 18 | # Or run this command to test and retest when files are changed. 19 | npm run tdd 20 | ``` 21 | 22 | Test coverage will be reported on the command line and also generated as html to ```coverage``` directory. 23 | -------------------------------------------------------------------------------- /docs/Gallery.md: -------------------------------------------------------------------------------- 1 | > [Docs](./index.md) ▸ **Gallery** 2 | 3 | ** TODO: This page has not been updated for v3 yet. ** 4 | 5 | ## Examples 6 | 7 | ##### Skeleton 8 | * [d3Kit.Skeleton](http://bl.ocks.org/kristw/7eef5cb21f3dfc1c0a4c) 9 | * [Dot in a box](http://bl.ocks.org/treboresque/f839966214cf66627df6) 10 | * [Bubble Chart](http://bl.ocks.org/kristw/75999459f1a34e05d580) 11 | * [Bar Chart](http://bl.ocks.org/kristw/9ecc2d17061cadbe3707) compared to Mike Bostock's original [Bar Chart](http://bl.ocks.org/mbostock/3885304) 12 | 13 | ##### Factory 14 | * [Sunburst Tree](http://bl.ocks.org/treboresque/211c0b6fadd0e3a2dd05) 15 | * [Reusable Bubble Chart](http://bl.ocks.org/kristw/d8b15dd09a4c3510621c) 16 | 17 | ##### Chartlet 18 | * [Circle Chartlet](http://bl.ocks.org/treboresque/0f01e42fb3c9268d7105) 19 | * [Face Chartlet](http://bl.ocks.org/treboresque/6cc9d948be0635d88990) 20 | * [Sierpinski Chartlet](http://bl.ocks.org/treboresque/28476a3ae1297af52d95) 21 | 22 | ##### Advanced 23 | 24 | * [Fetal growth chart](http://bl.ocks.org/kristw/762e219e34808e4f50a4) 25 | 26 | ## Projects using d3Kit 27 | 28 | * [Premier League: Where are your club's followers?](https://interactive.twitter.com/premierleague) -------------------------------------------------------------------------------- /docs/Getting-started.md: -------------------------------------------------------------------------------- 1 | > [Docs](./index.md) ▸ **Getting started** 2 | 3 | ### Download the library 4 | 5 | You can get the package from npm or bower. 6 | It depends on `d3-selection` and `d3-dispatch` from [D3.js](http://d3js.org/) v4. 7 | 8 | #### npm 9 | 10 | ```bash 11 | npm install d3 d3kit --save 12 | # or to be more specific 13 | npm install d3-selection d3-dispatch d3kit --save 14 | ``` 15 | 16 | #### bower 17 | 18 | ```bash 19 | bower install d3 d3kit --save 20 | ``` 21 | 22 | ### Add to your project 23 | 24 | **d3Kit** was packaged as UMD (Universal Module Definition). You can use it via: 25 | 26 | #### Option 1: Global (Simple script tag) 27 | 28 | ```html 29 | 30 | 31 | ``` 32 | 33 | d3Kit will be available as `d3Kit`. 34 | 35 | #### Option 2: ES6 import 36 | 37 | ```javascript 38 | import { SvgChart, CanvasChart } from 'd3kit'; 39 | ``` 40 | 41 | #### Option 3: AMD (requirejs) 42 | 43 | ```javascript 44 | require.config({ 45 | paths: { 46 | d3: 'path/to/d3', 47 | d3Kit: 'path/to/d3Kit' 48 | } 49 | }); 50 | require(['d3', 'd3Kit'], function(d3, d3Kit) { 51 | // do something 52 | }); 53 | ``` 54 | 55 | #### Option 4: commonjs (browserify) 56 | 57 | ```javascript 58 | var d3Kit = require('d3kit'); 59 | ``` -------------------------------------------------------------------------------- /docs/Versioning.md: -------------------------------------------------------------------------------- 1 | > [Docs](./index.md) ▸ Development ▸ **Versioning** 2 | 3 | ## Versioning 4 | 5 | ```bash 6 | # Choose from one of these 7 | npm version patch 8 | npm version minor 9 | npm version major 10 | # Check package version and size. 11 | # If everything looks good, then publish 12 | npm publish 13 | ``` -------------------------------------------------------------------------------- /docs/api/AbstractChart.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **AbstractChart** 2 | 3 | # AbstractChart 4 | 5 | An AbstractChart does all the basic groundwork for you before creating any chart. (This was revised and improved from the `Skeleton` in d3Kit v1-2.) 6 | 7 | ## Constructor 8 | 9 | ```javascript 10 | const chart = new AbstractChart(container[, options]); 11 | ``` 12 | 13 | However, never call this constructor directly, but use [SvgChart](SvgChart.md) or [CanvasChart](CanvasChart.md) instead. 14 | 15 | * **container** can be anything you can pass to `d3.select()`. For example: 16 | 17 | * *CSS Selector string* - A chart will be created within the first element that matches this CSS selector. Example usage: `new AbstractChart('#chart')` 18 | * *DOM element* - A chart will be created within this element. Usually a `
` is passed as an argument. Example usage: `new AbstractChart(document.getElementById('chart))` 19 | 20 | * **options:Object** will override default options similar to calling ```chart.options(options)``` later. 21 | 22 | ## Fields 23 | 24 | ### Public 25 | 26 | # chart.**container** 27 | 28 | Return a D3 selection of the container. 29 | 30 | # chart.**dispatcher** 31 | 32 | Return the chart's event dispatcher. This dispatcher is `d3.dispatch`. 33 | 34 | ### Private 35 | 36 | There are private fields named begin with underscore (`_`). Please do not touch them unless you really know what you are doing. 37 | 38 | ## Functions 39 | 40 | For getter/setter function, if a value is passed these functions set the specified value to the variable, otherwise these functions return the current value for that variable. 41 | 42 | ### Event handling 43 | 44 | # *(static)* AbstractChart.**getCustomEventNames**() 45 | 46 | Return the names of custom events that an object of this class can dispatch (other than `resize`, `data` and `options` that are included with every AbstractChart). 47 | 48 | # chart.**getCustomEventNames**() 49 | 50 | Return the names of custom events that this chart can dispatch. 51 | 52 | # chart.**on**(*eventName:String*, *listener:Function*) 53 | 54 | Add an event listener to an event from this chart. Similar to [d3.dispatch.on](https://github.com/mbostock/d3/wiki/Internals#dispatch_on). 55 | 56 | # chart.**off**(*eventName:String*, *listener:Function*) 57 | 58 | Remove event listener. 59 | 60 | # chart.**setupDispatcher**(*customEventNames:Array*) 61 | 62 | Setup the dispatcher to include the specified custom event names. 63 | 64 | # chart.**dispatchAs**(*name:String*) 65 | 66 | Returns an event handler that will capture all arguments and dispatch as event `name`. 67 | 68 | #### Events 69 | 70 | By default, the chart can dispatch these events out-of-the-box. 71 | 72 | # event: **data** 73 | 74 | dispatched whenever the data are set via ```chart.data(value)```. Note that it the chart does not watch for changes in the data. 75 | 76 | ```javascript 77 | chart.on('data', function(data){ ... }) 78 | ``` 79 | 80 | # event: **options** 81 | 82 | dispatched whenever the options are set via ```chart.options(value)```. Note that it the chart does not watch for changes in the options Object. 83 | 84 | ```javascript 85 | chart.on('options', function(options){ ... }) 86 | ``` 87 | 88 | # event: **resize** 89 | 90 | dispatched whenever the dimension of the chart is changed. This could be due to changes in *width*, *height*, *margin* or *offset*. 91 | 92 | ```javascript 93 | chart.on('resize', function(info){ ... }) 94 | ``` 95 | 96 | ```info``` is an Array ```[width, height, innerWidth, innerHeight]``` 97 | 98 | ### Data Handling 99 | 100 | # *(static)* AbstractChart.**getDefaultOptions**() 101 | 102 | Create and return a default `options` Object. Overwrite this function when extending the class to modify default options. For example: 103 | 104 | ```javascript 105 | class CanvasChart extends AbstractChart { 106 | static getDefaultOptions() { 107 | return deepExtend( 108 | super.getDefaultOptions(), 109 | { 110 | pixelRatio: window.devicePixelRatio, 111 | } 112 | ); 113 | } 114 | ... 115 | } 116 | ``` 117 | 118 | # chart.**data**([*data:Any*]) 119 | 120 | Get/Set the data for this chart. ```data``` can be any value. 121 | 122 | Calling ```chart.data(value)``` will make the chart dispatch event *data*. 123 | 124 | # chart.**hasData**() 125 | 126 | Return true if ```chart.data()``` is not null and not undefined. 127 | 128 | # chart.**options**([*options:Object*]) 129 | 130 | Get/Set the options for this chart. The input options will merge with current options and override any field with the same key. When the chart was created, these are the default options: 131 | 132 | ```javascript 133 | // AbstractChart.getDefaultOptions() 134 | { 135 | margin: {top: 30, right: 30, bottom: 30, left: 30}, 136 | offset: [0.5, 0.5], 137 | initialWidth: 720, 138 | initialHeight: 500 139 | }; 140 | ``` 141 | 142 | Calling ```chart.options(value)``` will make the chart dispatch event *options*. 143 | 144 | ### Size Handling 145 | 146 | # chart.**dimension**([*dimension:Array*]) 147 | 148 | Syntactic sugar for getting/setting both width and height at the same time. 149 | 150 | * When called without argument will return Array `[width, height]`. 151 | * When called with argument will set both width and height and dispatch *resize* event. 152 | 153 | # chart.**fit**([*fitOptions:Object*[, *watchOptions:Object*]) 154 | 155 | * Calling this function without any argument will resize the chart to fit into the container once using default settings (width = 100%, height <= container), or previous settings if `.fit(fitOptions)` has been called with more than one argument before. 156 | * Calling this function with single argument will resize the chart to fit into the container once using the specified `fitOptions`. 157 | * Calling with two arguments, such as `chart.fit({...}, true)` or `chart.fit({...}, {...})`, will create a `fitWatcher` that watch for size changes and auto-resize to fit. To kill the `fitWatcher`, call `chart.stopFitWatcher()`. 158 | 159 | Please refer to [slimfit documentation](https://github.com/kristw/slimfit) for `fitOptions` and `watchOptions` 160 | 161 | # chart.**getInnerHeight**() 162 | 163 | Return the height of the chart, less the top and bottom margin values. 164 | 165 | ```javascript 166 | innerHeight = chart.height() - chart.options().margin.top - chart.options().margin.bottom; 167 | ``` 168 | 169 | # chart.**getInnerWidth**() 170 | 171 | Return the width of the chart, less the left and right margin values. 172 | 173 | ```javascript 174 | innerWidth = chart.width() - chart.options().margin.left - chart.options().margin.right; 175 | ``` 176 | 177 | # chart.**hasNonZeroArea**() 178 | 179 | Return true if ```inner width * inner height > 0``` 180 | 181 | # chart.**height**([*value:Number*]) 182 | 183 | Get/Set the total height for this chart. Calling ```chart.height(value)``` will make the chart dispatch event *resize*. 184 | 185 | # chart.**margin**([*margin:Object*,[*, doNotDispatch:Boolean*]]) 186 | 187 | Get/Set the margin for this chart. The input margin will merge with current margin and override any field with the same key. The ```margin``` object can have up to four keys: *top*, *bottom*, *left* and *right*. 188 | 189 | Calling ```chart.margin(value)``` will make the chart dispatch event *resize*. 190 | 191 | # chart.**offset**([*offset:Array*]) 192 | 193 | Get/Set the offset for this chart. By default the root `````` will have half-pixel offset [0.5, 0.5], which is a small trick to provide sharp edges. See the second answer in this [StackOverflow question](http://stackoverflow.com/questions/7589650/drawing-grid-with-jquery-svg-produces-2px-lines-instead-of-1px) for more explanation. However, if this become an issue and you would like to remove the offset, you can override it. ```offset``` is an Array ```[xOffset, yOffset]``` 194 | 195 | Calling ```chart.offset(value)``` will make the chart dispatch event *resize*. 196 | 197 | # chart.**stopFitWatcher**() 198 | 199 | Stop the watcher. 200 | 201 | # chart.**updateDimensionNow**() 202 | 203 | Force the chart to recompute the dimension immediately. This is a synchronous operation while other sizing functions are asynchronous. 204 | 205 | For example, 206 | 207 | ```javascript 208 | // Other size functions are asynchronous 209 | const chart = new SvgChart('#container', { initialWidth: 400 }); 210 | chart.width(800); 211 | console.log(chart.container.clientWidth); // 400 212 | chart.on('resize', () => { 213 | console.log(chart.container.clientWidth); // 800 214 | }); 215 | ``` 216 | 217 | ```javascript 218 | // Force update with .updateDimensionNow() 219 | const chart = new SvgChart('#container', { initialWidth: 400 }); 220 | chart.width(800).updateDimensionNow(); 221 | console.log(chart.container.clientWidth); // 800 222 | ``` 223 | 224 | # chart.**width**([*value:Number*]) 225 | 226 | Get/Set the total width for this chart. Calling ```chart.width(value)``` will make the chart dispatch event *resize*. 227 | 228 | ### Plate functions 229 | 230 | # chart.**addPlate**(*name:String*, *plate:AbstractPlate*[, *doNotAppend:Boolean*]) 231 | 232 | Add a plate to this chart with the given name. If `doNotAppend` is true, will not append the plate node to this chart node (only keep in memory). 233 | 234 | # chart.**removePlate**(*name:String*) 235 | 236 | Remove a plate with the specified name. 237 | 238 | ### Other functions 239 | 240 | # chart.**destroy**() 241 | 242 | Kill all event listeners and watchers. Useful for cleaning up when the chart is not needed anymore. 243 | -------------------------------------------------------------------------------- /docs/api/AbstractPlate.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **AbstractPlate** 2 | 3 | # AbstractPlate 4 | 5 | ## Constructor 6 | 7 | ```javascript 8 | const plate = new AbstractPlate(node, options); 9 | ``` 10 | 11 | ## Functions 12 | 13 | # chart.**getNode**() 14 | 15 | returns a DOM node represented by this plate 16 | 17 | # chart.**getSelection**() 18 | 19 | returns a D3 selection of the DOM node represented by this plate 20 | -------------------------------------------------------------------------------- /docs/api/CanvasChart.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **CanvasChart** 2 | 3 | # CanvasChart 4 | 5 | This class extends from [AbstractChart](AbstractChart.md) and therefore inherits all fields and functions. In addition, this class also creates `` in side the container. 6 | 7 | ## Examples 8 | 9 | ### A. Scaffold and create something quickly 10 | 11 | ```html 12 |
13 | ``` 14 | 15 | ```javascript 16 | import { SvgChart } from 'd3kit'; 17 | const chart = new CanvasChart('#chart0', { 18 | initialWidth: 720, 19 | initialHeight: 500, 20 | margin: { top: 30, right: 30, bottom: 30, left: 30 } 21 | }); 22 | ``` 23 | 24 | The output will looks like this. 25 | 26 | ```html 27 | 28 |
29 | 30 | 31 | 32 |
33 | ``` 34 | 35 | So you can draw on the canvas 36 | 37 | ```javascript 38 | const ctx = chart.getContext2d(); 39 | ctx.fillRect(10, 10, 10, 10); 40 | ``` 41 | 42 | ### B. Create a reusable chart 43 | 44 | ```javascript 45 | import { CanvasChart, helper } from 'd3kit'; 46 | import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; 47 | import { extent } from 'd3-array'; 48 | 49 | class CanvasBubbleChart extends CanvasChart { 50 | // Define default options for this chart 51 | static getDefaultOptions() { 52 | return helper.deepExtend( 53 | super.getDefaultOptions(), 54 | { 55 | margin: {top: 60, right: 60, bottom: 60, left: 60}, 56 | initialWidth: 800, 57 | initialHeight: 460 58 | } 59 | ); 60 | } 61 | 62 | /** 63 | * Define the names of custom events that can be dispatched from this chart 64 | * @return {Array[String]} event names 65 | */ 66 | static getCustomEventNames() { 67 | return []; 68 | } 69 | 70 | constructor(selector, options) { 71 | super(selector, options); 72 | 73 | // add custom variables 74 | this.xScale = scaleLinear(); 75 | this.yScale = scaleLinear(); 76 | this.color = scaleOrdinal(schemeCategory10); 77 | 78 | // add basic event listeners 79 | this.visualize = this.visualize.bind(this); 80 | this.on('resize.default', this.visualize); 81 | this.on('data.default', this.visualize); 82 | } 83 | 84 | // You can define a new function for this class. 85 | visualize() { 86 | this.clear(); 87 | 88 | if(!this.hasData()){ 89 | return; 90 | } 91 | 92 | const data = this.data(); 93 | 94 | this.xScale.domain(extent(data, d => d.x)) 95 | .range([0, this.getInnerWidth()]); 96 | this.yScale.domain(extent(data, d => d.y)) 97 | .range([this.getInnerHeight(), 0]); 98 | 99 | const ctx = this.getContext2d(); 100 | data.forEach((d,i) => { 101 | ctx.fillStyle = this.color(i); 102 | ctx.fillRect( 103 | this.xScale(d.x) - d.r, 104 | this.yScale(d.y) - d.r, 105 | d.r * 2, 106 | d.r * 2 107 | ); 108 | }); 109 | 110 | } 111 | } 112 | 113 | export default CanvasBubbleChart; 114 | ``` 115 | 116 | ## Constructor 117 | 118 | ```javascript 119 | const chart = new CanvasChart(container[, options]); 120 | ``` 121 | 122 | * **options:Object** - There is an extra field `pixelRatio` to specify the resolution of the canvas. Default value is `window.devicePixelRatio`. 123 | 124 | ## Fields 125 | 126 | # chart.**canvas** 127 | 128 | Return a D3 selection of `````` element. 129 | 130 | ## Functions 131 | 132 | # chart.**clear**() 133 | 134 | Clear canvas 135 | 136 | # chart.**getContext2d**() 137 | 138 | Return `context2d` that has been adjusted for scaling and margins. 139 | 140 | -------------------------------------------------------------------------------- /docs/api/CanvasPlate.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **CanvasPlate** 2 | 3 | # CanvasPlate 4 | 5 | A plate that wraps `` 6 | 7 | ## Constructor 8 | 9 | ```javascript 10 | const plate = new CanvasPlate(node, options); 11 | ``` 12 | 13 | ## Functions 14 | 15 | # chart.**clear**() 16 | 17 | Clear canvas 18 | 19 | # chart.**getContext2d**() 20 | 21 | Return `context2d` that has been adjusted for scaling and margins. 22 | 23 | #### inherits from [AbstractPlate](AbstractPlate.md) 24 | -------------------------------------------------------------------------------- /docs/api/Chartlet.md: -------------------------------------------------------------------------------- 1 | > [Docs](../../README.md) ▸ [API Reference](index.md) ▸ **Chartlet** 2 | 3 | ** This component was in d3Kit v1-2, but has not been ported to v3 yet ** 4 | 5 | ## d3Kit.Chartlet 6 | 7 | Chartlets provide a way to break large, often entangled, chart code into smaller, reusable components. The [Circle Chartlet](http://bl.ocks.org/treboresque/0f01e42fb3c9268d7105) provides the most simple, stripped down, example. Chartlets can be constructed from other chartlets, or even recursively call themselves - see the [Sierpinski Chartlet](http://bl.ocks.org/treboresque/28476a3ae1297af52d95) example. Chartlets feature: 8 | 9 | * d3 enter/update/exit semantics 10 | * named properties, who's values can are specified in the calling context 11 | * custom events, which can be handled in the calling context 12 | * support for transitions and other asynchronous action 13 | * ability to inherit properties from parent chartlets 14 | 15 | ### How chartlets typically work 16 | 17 | Typically a chart, which is going to use a chartlet, uses the d3 enter/update/exit system to create and position a number of `````` elements in the normal way. The chartlet is called on the selection of those ``````s, and adds addition DOM elements to them. 18 | 19 | ### Creating a charlet: 20 | 21 | The following is taken from the [Circle Chartlet](http://bl.ocks.org/treboresque/0f01e42fb3c9268d7105) example, which creates `````` elements. Internally it implements ```enter()```, ```update()```, and ```exit()``` functions, which are passed into the chartlet constructor. Each of those functions is passed a selection to operate on and a ```done()``` callback function to call when any asynchronous activity has finished. Internally the done method is debounced so it may be called multiple times as shown. If you call ```done()``` directly, pass the ```selection``` into it as the first parameter. 22 | 23 | ```javascript 24 | function CircleChartlet() { 25 | 26 | var events = ['circleClicked']; 27 | 28 | var chartlet = d3Kit.Chartlet(enter, update, exit, events); 29 | 30 | function enter(selection, done) { 31 | selection 32 | .append('circle') 33 | .attr('r', 0) 34 | .attr('fill', 'white') 35 | .on('click', chartlet.getDispatcher().circleClicked); 36 | 37 | done(selection); 38 | } 39 | 40 | function update(selection, done) { 41 | selection.select('circle') 42 | .transition() 43 | .attr('fill', chartlet.property('color')) 44 | .attr('r', chartlet.property('radius')) 45 | .each('end', done); 46 | } 47 | 48 | function exit(selection, done) { 49 | selection.select('circle') 50 | .transition() 51 | .attr('r', 0) 52 | .remove() 53 | .each('end', done); 54 | } 55 | 56 | return chartlet; 57 | }; 58 | ``` 59 | 60 | ### Using a charlet: 61 | 62 | The following code paraphrases [Circle Chartlet](http://bl.ocks.org/treboresque/0f01e42fb3c9268d7105) to show how the chartlet might be used. Note that the ```color``` and ```radius``` properties provide abstracted access to these values inside the chartlet. The the chartlet's ```enter()```, ```update()```, and ```exit()``` methods are called during those phases the containing chart. 63 | 64 | ```javascript 65 | var radiusScale = d3.scale.linear().range([10, 50]); 66 | var colorScale = d3.scale.category20(); 67 | 68 | var circles = CircleChartlet() 69 | .property('radius', function(d, i) {return radiusScale(d.size);}) 70 | .property('color', function(d, i) {return colorScale(i);}); 71 | 72 | var data = d3.range(20).map(function(i) { 73 | return {size: i, x: Math.random(), y: Math.random()}; 74 | }); 75 | 76 | var nodes = chart.getRootG().selectAll('g.node') 77 | .data(data); 78 | 79 | // handle enter case 80 | 81 | nodes.enter() 82 | .append('g') 83 | .classed('node', true) 84 | .call(circles.enter); 85 | 86 | // handle exit case 87 | 88 | nodes.exit() 89 | .call(circles.exit); 90 | 91 | // handle update case 92 | 93 | nodes 94 | .attr('transform', function(d) {return 'translate(' + [xScale(d.x), yScale(d.y)] + ')';}) 95 | .call(circles.update); 96 | ``` 97 | 98 | ## Constructor 99 | 100 | # new **d3Kit.Chartlet(**enterFunction, updateFuncton, exitFunction [, *customEventNames*]**)** 101 | 102 | This creates a new chartlet with the specified **[enterFunction](Chartlet#enter)**, 103 | **[updateFunction](Chartlet#update)** and **[exitFunction](Chartlet#exit)**. An optional list of **customEventNames** may be passed which are used to configure the chartlet's dispatcher. 104 | 105 | ### Getter Functions 106 | 107 | # chartlet.**getDispatcher()** 108 | 109 | Returns the chartlets's internal event dispatcher. This dispatcher is an instance of d3.dispatch. 110 | 111 | # chartlet.**getCustomEvents()** 112 | 113 | Returns the list of custom events which a given chartlet might dispatch. 114 | 115 | # chartlet.**getPropertyValue(***name, datum, datum_index***)** 116 | 117 | Returns the value of a named property. This is typically called from inside the chartlet code, and thus the **datum** and **datum_index** are passed in as parameters as they may be needed to compute the property value. This is the same as calling: 118 | 119 | ```javascript 120 | chartlet.property('foo')(datum, datum_index); 121 | ``` 122 | 123 | ### Getter/Setter Function 124 | 125 | # chartlet.**property(**name, [function_or_value]**)** 126 | 127 | Either gets or sets the function which returns the value for a given chartlet named property. If **function_or_value** is specified, then that property will be set to that function or value. If a naked value is passed, it will be wrapped in a function. In either case the original chartlet will be returned so that calls to **Chartlet.property** may be chained. 128 | 129 | If **function_or_value** is omitted, then the a function will be returned, which when called with a datum and datum_index, will return the value for the name property. 130 | 131 | ### Enter/Update/Exit Functions 132 | 133 | These functions are typically called using d3's [selection.call()](https://github.com/mbostock/d3/wiki/Selections#call) to cause the chartlet code to execute and update the DOM. 134 | 135 | # chartlet.**enter(**selection**)** 136 | 137 | This call performs the actions which will cause the chartlet to add new elements to a chart. 138 | 139 | # chartlet.**update(**selection**)** 140 | 141 | This call performs the actions which will cause the chartlet to update existing elements in a chart. 142 | 143 | # chartlet.**exit(**selection**)** 144 | 145 | This call performs the actions which will cause the chartlet to remove elements from a chart. 146 | 147 | ### Inheritance Functions 148 | 149 | Chartlets can be composed of (aka contain) other chartlets. These functions map properties and events between parent and children chartlets. 150 | 151 | # chartlet.**inheritPropertyFrom(**parent_chartlet, parent_property_name, [child_property_name]**)** 152 | 153 | This function maps **parent_property_name** property from the **parent_chartlet** to the calling chartlet. If specified the property will be renamed to **child_property_name**. 154 | 155 | # chartlet.**inheritPropertiesFrom(**parent_chartlet, parent_property_names, [child_property_names]**)** 156 | 157 | This function maps a list of **parent_property_names** properties from the **parent_chartlet** to the calling chartlet. If specified the properties will be renamed to **child_property_names**. 158 | 159 | # chartlet.**publishEventsTo(**dispatcher**)** 160 | 161 | This function causes the chartlet to dispatch all it's events to the provided dispatcher. Typically you will pass the dispatcher from a parent chartlet to a child chartlet to pass all it's events up stream. 162 | 163 | ```javascript 164 | childChartlet.publishEventsTo(parentChartlet.getDispatcher()); 165 | ``` 166 | 167 | ### Events 168 | 169 | Chartlets dispatch a set of stock events to indicate completion of asynchronous activity during calls to [Chartlet.enter](Chartlet#enter), [Chartlet.update](Chartlet#update) or [Chartlet.exit](Chartlet#exit). This allows calling code to chain action only after such asynchronous activity has completed. Handlers of stock events will be passed the [d3.selection](https://github.com/mbostock/d3/wiki/Selections) that was being operated on. 170 | 171 | Authors may also add custom events as needed for a specific chartlet. Where possible, the handler of custom events should be passed the **datum** and **datum_index** associated with the element which is the source of the event. 172 | 173 | # chartlet.**on(**eventName, handlerFunction**)** 174 | 175 | This function maps a **handlerFunction** to the specified **eventName**. 176 | 177 | # chartlet.**enterDone** 178 | 179 | This event is fired when asynchronous activity in [Chartlet.enter](Chartlet#enter) has completed. The handler of this event will be passed the [d3.selection](https://github.com/mbostock/d3/wiki/Selections) that was being operated on. 180 | 181 | # chartlet.**updateDone** 182 | 183 | This event is fired when asynchronous activity in [Chartlet.update](Chartlet#update) has completed. The handler of this event will be passed the [d3.selection](https://github.com/mbostock/d3/wiki/Selections) that was being operated on. 184 | 185 | # chartlet.**exitDone** 186 | 187 | This event is fired when asynchronous activity in [Chartlet.exit](Chartlet#exit) has completed. The handler of this event will be passed the [d3.selection](https://github.com/mbostock/d3/wiki/Selections) that was being operated on. -------------------------------------------------------------------------------- /docs/api/DivPlate.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **DivPlate** 2 | 3 | # DivPlate 4 | 5 | A plate that wraps `
` 6 | 7 | ## Constructor 8 | 9 | ```javascript 10 | const plate = new DivPlate(node, options); 11 | ``` 12 | 13 | ## Functions 14 | 15 | #### inherits from [AbstractPlate](AbstractPlate.md) 16 | 17 | -------------------------------------------------------------------------------- /docs/api/Helper.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **helper** 2 | 3 | # helper 4 | 5 | These functions were originally collected from multiple places (jQuery, lodash, ...) for usage in d3Kit core library. Since they are also useful for building visualizations in general, we also expose them to the API. 6 | 7 | * [helper.debounce(func, delay)](https://lodash.com/docs/4.16.4#debounce) - returns a debounced function with given delay. 8 | * [helper.deepExtend(dest, src1, src2, ...)](Helper.md#deepExtend) - Recursively merge the contents of two or more objects together into the first object. Works similarly to `jQuery.extend(true, target, obj1, obj2, ...)` 9 | * [helper.extend(dest, src1, src2, ...)](Helper.md#extend) - Merge the contents of two or more objects together into the first object. Works similarly to `jQuery.extend(target, obj1, obj2, ...)` 10 | * [helper.functor(valueOrFunc)](https://github.com/d3/d3-3.x-api-reference/blob/master/Internals#functor) - If value is not a function, returns a function that returns the value. Otherwise returns the function. 11 | * [helper.isObject(value)](https://lodash.com/docs/4.16.4#isObject) - returns `true` if value is an object. 12 | * [helper.isFunction(value)](https://lodash.com/docs/4.16.4#isFunction) - returns `true` if value is a function. 13 | * [helper.kebabCase(string)](https://lodash.com/docs/4.16.4#kebabCase) - converts any string into `kebab-case` 14 | * [helper.throttle(func, delay)](https://lodash.com/docs/4.16.4#throttle) - returns a throttled function with given delay. 15 | -------------------------------------------------------------------------------- /docs/api/HybridChart.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **HybridChart** 2 | 3 | # HybridChart 4 | 5 | This class combines behaviors of [CanvasChart](CanvasChart.md) and [SvgChart](SvgChart.md), so it has both `` and ``. 6 | 7 | ## Constructor 8 | 9 | ```javascript 10 | const chart = new HybridChart(container[, options]); 11 | ``` 12 | 13 | * **options:Object** - There is an extra field `pixelRatio` to specify the resolution of the canvas. Default value is `window.devicePixelRatio`. 14 | 15 | ## Fields 16 | 17 | #### inherits from [CanvasChart](CanvasChart.md) 18 | 19 | #### inherits from [SvgChart](SvgChart.md) 20 | 21 | ## Functions 22 | 23 | #### inherits from [CanvasChart](CanvasChart.md) 24 | -------------------------------------------------------------------------------- /docs/api/LayerOrganizer.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **LayerOrganizer** 2 | 3 | # LayerOrganizer 4 | 5 | A utility for creating layers from a given config. If you have a habit of creating many `````` or other tags to layer your visual elements. This utility will let you create nested layers easily. For example, 6 | 7 | ```javascript 8 | var layers = new d3Kit.LayerOrganizer(d3.select('svg')); 9 | layers.create([{'highlight': 'cursor'}, {'graph': ['axis','content']}]); 10 | ``` 11 | 12 | will give you 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ``` 25 | 26 | and you can access a d3 selection of each layer by its name. 27 | 28 | ```javascript 29 | layers.get('highlight.cursor'); // a d3 selection of 30 | ``` 31 | 32 | This was originally written to support just `` but since version 1.1.0 we have added support for other tags. Just specify the tag name in the constructor. 33 | 34 | ## Functions 35 | 36 | # new **d3Kit.LayerOrganizer**(*container*[,tag]) 37 | 38 | Construct a layer organizer for the specified ```container```. ```container``` is a d3 selection of ``, ``, `
`, etc. You can specify *tag* such as `'div'` if you want to create layers of another thing that is not `` 39 | 40 | # layers.**create**(*config*) 41 | 42 | Create layer(s) of `````` within the container. If ```config``` is 43 | 44 | * *String* - Create one `````` 45 | * *Array* - Create each `````` for each string value in the array. If the value is an Object, will use the Object construction rule below. 46 | * *Object* - For each (key,value) pair, create ``````, then create layer(s) inside the new ```g.[key]-layer``` depends on the type of the value (string, Array or Object). 47 | 48 | # layers.**get**(*name*) 49 | 50 | Retrieve a layer by ```name``` (*string*). If a layer is nested under one or more layer(s), add its parent names as prefixes, separated by dot. For example, ```layers.get('highlight.cursor')``` will return the ```g.cursor-layer``` that is nested under ```g.highlight-layer```. 51 | 52 | 53 | # layers.**has**(*name*) 54 | 55 | Check if there is a layer with the specified ```name``` (*string*). Use the same name format with [layers.get](#get) -------------------------------------------------------------------------------- /docs/api/SvgChart.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **SvgChart** 2 | 3 | # SvgChart 4 | 5 | This class extends from [AbstractChart](AbstractChart.md) and therefore inherits all fields and functions. These additional tasks are performed: 6 | 7 | * Creates `````` in side the container. 8 | * Creates `````` within the `````` using D3's [margin convention](http://bl.ocks.org/mbostock/3019563). This `````` is called the root `````` and can be accessed via `chart.rootG`. 9 | 10 | ## Example Usage 11 | 12 | ### A. Scaffold and create something quickly 13 | 14 | ```html 15 |
16 | ``` 17 | 18 | ```javascript 19 | import { SvgChart } from 'd3kit'; 20 | const chart = new SvgChart('#chart0', { 21 | initialWidth: 720, 22 | initialHeight: 500, 23 | margin: { top: 30, right: 30, bottom: 30, left: 30 }, 24 | offset: [0.5, 0.5] // add little offset for sharp-edge rendering 25 | }); 26 | ``` 27 | 28 | The output will looks like this. 29 | 30 | ```html 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 |
39 | ``` 40 | 41 | So you can append a circle or do anything you usually do with D3. 42 | 43 | ```javascript 44 | chart.rootG.append('circle') 45 | .attr('cx', 10) 46 | .attr('cy', 10) 47 | .attr('r', 5) 48 | ``` 49 | 50 | ### B. Create a reusable chart 51 | 52 | First create a chart by extending `SvgChart`. 53 | 54 | ```javascript 55 | import { SvgChart, helper } from 'd3kit'; 56 | import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; 57 | import { axisLeft, axisBottom } from 'd3-axis'; 58 | import { extent } from 'd3-array'; 59 | 60 | class SvgBubbleChart extends SvgChart { 61 | // Define default options for this chart 62 | static getDefaultOptions() { 63 | return helper.deepExtend( 64 | super.getDefaultOptions(), 65 | { 66 | margin: {top: 60, right: 60, bottom: 60, left: 60}, 67 | initialWidth: 800, 68 | initialHeight: 460 69 | } 70 | ); 71 | } 72 | 73 | /** 74 | * Define the names of custom events that can be dispatched from this chart 75 | * @return {Array[String]} event names 76 | */ 77 | static getCustomEventNames() { 78 | return ['bubbleClick']; 79 | } 80 | 81 | constructor(selector, options) { 82 | super(selector, options); 83 | 84 | // Add custom variables 85 | this.xScale = scaleLinear(); 86 | this.yScale = scaleLinear(); 87 | this.color = scaleOrdinal(schemeCategory10); 88 | this.xAxis = axisBottom().scale(this.xScale); 89 | this.yAxis = axisLeft().scale(this.yScale); 90 | this.xAxisG = this.rootG.append('g'); 91 | this.yAxisG = this.rootG.append('g'); 92 | 93 | // Add basic event listeners 94 | this.visualize = this.visualize.bind(this); 95 | this.on('resize.default', this.visualize); 96 | this.on('data.default', this.visualize); 97 | } 98 | 99 | // You can define a new function for this class. 100 | visualize() { 101 | if(!this.hasData()) return; 102 | 103 | const data = this.data(); 104 | 105 | this.xScale.domain(extent(data, d => d.x)) 106 | .range([0, this.getInnerWidth()]); 107 | this.yScale.domain(extent(data, d => d.y)) 108 | .range([this.getInnerHeight(), 0]); 109 | 110 | this.xAxisG 111 | .attr('transform', `translate(0,${this.getInnerHeight()})`) 112 | .call(this.xAxis); 113 | 114 | this.yAxisG.call(this.yAxis); 115 | 116 | const selection = this.rootG.selectAll('circle') 117 | .data(data); 118 | 119 | selection.exit().remove(); 120 | 121 | const sEnter = selection.enter().append('circle') 122 | .attr('cx', d => this.xScale(d.x)) 123 | .attr('cy', d => this.yScale(d.y)) 124 | .on('click', (...args) => { 125 | this.dispatcher.apply('bubbleClick', this, args); 126 | }); 127 | 128 | selection.merge(sEnter) 129 | .attr('cx', d => this.xScale(d.x)) 130 | .attr('cy', d => this.yScale(d.y)) 131 | .attr('r', d => d.r) 132 | .style('fill', (d,i) => this.color(i)); 133 | } 134 | } 135 | 136 | export default SvgBubbleChart; 137 | ``` 138 | 139 | Then use it 140 | 141 | ```javascript 142 | const chart1 = new SvgBubbleChart('#chart1', { 143 | margin: { top: 20 }, 144 | initialWidth: 300, 145 | initialHeight: 300, 146 | }) 147 | .data(bubbles) 148 | // handle bubbleClick event 149 | .on('bubbleClick', d => { alert(JSON.stringify(d)); }) 150 | // demonstrate auto resizing to maintain 16:9 aspect ratio 151 | .fit({ 152 | mode: 'aspectRatio', 153 | ratio: 16/9, 154 | }, true); 155 | ``` 156 | 157 | ## Constructor 158 | 159 | ```javascript 160 | const chart = new SvgChart(container[, options]); 161 | ``` 162 | 163 | ## Fields 164 | 165 | # chart.**layers** 166 | 167 | Return the chart's internal layer organizer, which is a [LayerOrganizer](LayerOrganizer) that wraps the root ``````. 168 | 169 | # chart.**rootG** 170 | 171 | Return a D3 selection that is the root `````` element, to which you will append the rest of the elements for your chart. 172 | 173 | # chart.**svg** 174 | 175 | Return a D3 selection that is the chart's `````` element. 176 | -------------------------------------------------------------------------------- /docs/api/SvgPlate.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ [API Reference](index.md) ▸ **SvgPlate** 2 | 3 | # SvgPlate 4 | 5 | A plate that wraps `` 6 | 7 | ## Constructor 8 | 9 | ```javascript 10 | const plate = new SvgPlate(node, options); 11 | ``` 12 | 13 | ## Functions 14 | 15 | #### inherits from [AbstractPlate](AbstractPlate.md) 16 | 17 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | > [Docs](../index.md) ▸ **API Reference** 2 | 3 | ## AbstractChart 4 | 5 | ##### Constructor 6 | 7 | const chart = [new AbstractChart(container[, options])](AbstractChart.md#constructor) 8 | 9 | ##### Fields 10 | 11 | * [chart.container](AbstractChart.md#container) - D3 selection of the container. 12 | * [chart.dispatcher](AbstractChart.md#dispatch) - A `d3-dispatch` event dispatcher for this chart. 13 | 14 | ##### Event functions 15 | 16 | * *(static)* [AbstractChart.getCustomEventNames()](AbstractChart.md#static-getCustomEventNames) - return the names of custom events that an object of this class can dispatch (other than `resize`, `data` and `options` that are included with every AbstractChart). 17 | * [chart.getCustomEventNames()](AbstractChart.md#getCustomEventNames) - return the names of custom events that this chart can dispatch (other than `resize`, `data` and `options` that are included with every AbstractChart). 18 | * [chart.on(eventName, listener)](AbstractChart.md#on) - add an event listener to an event from this chart. 19 | * [chart.off(eventName, listener)](AbstractChart.md#off) - remove an event listener from this chart. 20 | * [chart.setupDispatcher(customEventNames)](AbstractChart.md#setupDispatcher) - setup the dispatcher to include the specified custom event names. 21 | * [chart.dispatchAs(name)](AbstractChart.md#dispatchAs) - Returns an event handler that will capture all arguments and dispatch as event `name`. 22 | 23 | ##### Data functions 24 | 25 | * *(static)* [AbstractChart.getDefaultOptions()](AbstractChart.md#static-getDefaultOptions) - create and return a default `options` Object. Overwrite this function when extending the class to modify default options. 26 | * [chart.data([data])](AbstractChart.md#data) - get/set the data. 27 | * [chart.on('data', listener)](AbstractChart.md#event_data) - handle when the data are set. 28 | * [chart.hasData()](AbstractChart.md#hasData) - return `true` if data is not `null` or `undefined`. 29 | * [chart.options([options])](AbstractChart.md#options) - get/set the options. 30 | * [chart.on('options', listener)](AbstractChart.md#event_options) - handle when the options are set. 31 | 32 | ##### Size functions 33 | 34 | * [chart.dimension([dimension])](AbstractChart.md#dimension) - get/set both width and height at the same time. 35 | * [chart.fit(fitOptions[, watchOptions])](AbstractChart.md#fit) - Calling this function with single argument will resize the chart to fit into the container once. Calling with two arguments, such as `chart.fit({...}, true)` or `chart.fit({...}, {...})`, will enable watching. Please refer to [slimfit documentation](https://github.com/kristw/slimfit) for `fitOptions` and `watchOptions` 36 | * [chart.getInnerHeight()](AbstractChart.md#getInnerHeight) - return the height of the chart without margin. 37 | * [chart.getInnerWidth()](AbstractChart.md#getInnerWidth) - return the width of the chart without margin. 38 | * [chart.hasNonZeroArea()](AbstractChart.md#hasNonZeroArea) - return `true` if the inner area (*inner width x inner height*) is more than zero. 39 | * [chart.height([height])](AbstractChart.md#height) - get/set the height. 40 | * [chart.margin([margin])](AbstractChart.md#margin) - get/set the margin. 41 | * [chart.offset([offset])](AbstractChart.md#offset) - get/set the offset. 42 | * [chart.stopFitWatcher()](AbstractChart.md#stopFitWatcher) - stop the watcher. 43 | * [chart.updateDimensionNow()](AbstractChart.md#updateDimensionNow()) - force the chart to recompute the dimension immediately. This is a synchronous operation while other sizing functions are asynchronous. 44 | * [chart.width([width])](AbstractChart.md#width) - get/set the width. 45 | * [chart.on('resize', listener)](AbstractChart.md#event_resize) - handle when the dimension is changed. 46 | 47 | ##### Plate functions 48 | 49 | * [chart.addPlate(name, plate[, doNotAppend])](AbstractChart.md#addPlate) - Add a plate to this chart with the given name. If `doNotAppend` is true, will not append the plate node to this chart node (only keep in memory). 50 | * [chart.removePlate(name)](AbstractChart.md#removePlate) - Remove a plate with the specified name. 51 | 52 | ##### Other functions 53 | 54 | * [chart.destroy()](AbstractChart.md#destroy) - kill all event listeners and watchers. Useful for cleaning up when the chart is not needed anymore. 55 | 56 | ## SvgChart 57 | 58 | ##### Constructor 59 | 60 | const chart = [new SvgChart(container[, options])](SvgChart.md#constructor) 61 | 62 | ##### Fields 63 | 64 | * *inherits from* `AbstractChart` 65 | * [chart.layers](SvgChart.md#layers) - return the LayerOrganizer. 66 | * [chart.rootG](SvgChart.md#rootG) - D3 selection of the root `` element. 67 | * [chart.svg](SvgChart.md#svg) - D3 selection of the `` element. 68 | 69 | ## CanvasChart 70 | 71 | ##### Constructor 72 | 73 | const chart = [new CanvasChart(container[, options])](CanvasChart.md#constructor) 74 | 75 | ##### Fields 76 | 77 | * *inherits from* `AbstractChart` 78 | * [chart.canvas](CanvasChart.md#canvas) - D3 selection of the `` element. 79 | 80 | ##### Functions 81 | 82 | * *inherits from* `AbstractChart` 83 | * [chart.clear()](CanvasChart.md#clear) - clear canvas. 84 | * [chart.getContext2d()](CanvasChart.md#getContext2d) - return a context for drawing on canvas. 85 | 86 | ## HybridChart 87 | 88 | ##### Constructor 89 | 90 | const chart = [new HybridChart(container[, options])](HybridChart.md#constructor) 91 | 92 | ##### Fields 93 | 94 | * *inherits from* `CanvasChart` 95 | * *inherits from* `SvgChart` 96 | 97 | ##### Functions 98 | 99 | * *inherits from* `CanvasChart` 100 | * *inherits from* `SvgChart` 101 | 102 | ## AbstractPlate 103 | 104 | ##### Constructor 105 | 106 | const plate = [new AbstractPlate(node[, options])](AbstractPlate.md#constructor) 107 | 108 | ##### Functions 109 | 110 | * [plate.getNode()](CanvasPlate.md#getNode) - returns a DOM node represented by this plate 111 | * [plate.getSelection()](CanvasPlate.md#getSelection) - returns a D3 selection of the DOM node represented by this plate 112 | 113 | ## CanvasPlate 114 | 115 | ##### Constructor 116 | 117 | const plate = [new CanvasPlate(node[, options])](CanvasPlate.md#constructor) 118 | 119 | ##### Functions 120 | 121 | * *inherits from* `AbstractPlate` 122 | * [plate.clear()](CanvasPlate.md#clear) - clear canvas. 123 | * [plate.getContext2d()](CanvasPlate.md#getContext2d) - return a context for drawing on canvas. 124 | 125 | ## DivPlate 126 | 127 | ##### Constructor 128 | 129 | const plate = [new DivPlate(node[, options])](DivPlate.md#constructor) 130 | 131 | ##### Functions 132 | 133 | * *inherits from* `AbstractPlate` 134 | 135 | ## SvgPlate 136 | 137 | ##### Constructor 138 | 139 | const plate = [new SvgPlate(node[, options])](SvgPlate.md#constructor) 140 | 141 | ##### Functions 142 | 143 | * *inherits from* `AbstractPlate` 144 | 145 | 179 | 180 | ## LayerOrganizer 181 | 182 | ##### Constructor 183 | 184 | const layers = [new LayerOrganizer(container[, defaultTag])](LayerOrganizer.md#constructor) 185 | 186 | ##### Functions 187 | 188 | * [layers.create(config)](LayerOrganizer.md#create) - create layers of element, such as ``````, within the container. 189 | * [layers.get(name)](LayerOrganizer.md#get) - get a layer by name. 190 | * [layers.has(name)](LayerOrganizer.md#has) - check if there is a layer with specified name. 191 | 192 | ## helper 193 | 194 | ##### Functions 195 | 196 | * *(static)* [helper.debounce(func, delay)](https://lodash.com/docs/4.16.4#debounce) 197 | * *(static)* [helper.deepExtend(dest, src1, src2, ...)](Helper.md#deepExtend) 198 | * *(static)* [helper.extend(dest, src1, src2, ...)](Helper.md#extend) 199 | * *(static)* [helper.functor(valueOrFunc)](https://github.com/d3/d3-3.x-api-reference/blob/master/Internals#functor) 200 | * *(static)* [helper.isFunction(value)](https://lodash.com/docs/4.16.4#isFunction) 201 | * *(static)* [helper.isObject(value)](https://lodash.com/docs/4.16.4#isObject) 202 | * *(static)* [helper.kebabCase(string)](https://lodash.com/docs/4.16.4#kebabCase) 203 | * *(static)* [helper.throttle(func, delay)](https://lodash.com/docs/4.16.4#throttle) 204 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | > Docs ▸ **Table of Content** 2 | 3 | ### [Getting started](Getting-started.md) 4 | 5 | ### [API Reference](api/index.md) 6 | 7 | * [Brief version of everything](api/index.md) 8 | * module: [AbstractChart](api/AbstractChart.md) 9 | * module: [CanvasChart](api/CanvasChart.md) 10 | * module: [HybridChart](api/HybridChart.md) 11 | * module: [SvgChart](api/SvgChart.md) 12 | * module: [AbstractPlate](api/AbstractPlate.md) 13 | * module: [CanvasPlate](api/CanvasPlate.md) 14 | * module: [DivPlate](api/DivPlate.md) 15 | * module: [SvgPlate](api/SvgPlate.md) 16 | * module: [LayerOrganizer](api/LayerOrganizer.md) 17 | * module: [helper](api/helper.md) 18 | 19 | ### [Gallery](Gallery.md) 20 | 21 | ### Development 22 | 23 | * [Developing d3Kit](Developing.md) 24 | * [Versioning](Versioning.md) 25 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | d3kit 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 |
32 |
33 |
34 |
35 |

36 | d3Kit 37 |

38 | 39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 |

SvgChart

56 |
57 |

58 | This chart was created entirely in <svg>. 59 |

60 |
61 |
62 |

CanvasChart

63 |
64 |

65 | This chart was drawn entirely on <canvas>. 66 |

67 |
68 |
69 |

HybridChart

70 |
71 |

72 | This axes in this chart was created in <svg> while the rectangles are drawn on <canvas>. 73 |

74 |
75 |
76 |
77 | 78 |

79 |

80 |

81 | 82 |
83 | © 2015-2017. Twitter, Inc. by @trebor and @kristw — MIT License 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/src/CanvasExample.js: -------------------------------------------------------------------------------- 1 | import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; 2 | import { extent } from 'd3-array'; 3 | import { CanvasChart, helper } from '../../src/main.js'; 4 | 5 | export default class CanvasExample extends CanvasChart { 6 | static getDefaultOptions() { 7 | return helper.deepExtend( 8 | super.getDefaultOptions(), 9 | { 10 | margin: {top: 60, right: 60, bottom: 60, left: 60}, 11 | initialWidth: 800, 12 | initialHeight: 460 13 | } 14 | ); 15 | } 16 | 17 | /** 18 | * Define the names of custom events that can be dispatched from this chart 19 | * @return {Array[String]} event names 20 | */ 21 | static getCustomEventNames() { 22 | return []; 23 | } 24 | 25 | constructor(selector, options) { 26 | super(selector, options); 27 | 28 | // add custom variables 29 | this.xScale = scaleLinear(); 30 | this.yScale = scaleLinear(); 31 | this.color = scaleOrdinal(schemeCategory10); 32 | 33 | // add basic event listeners 34 | this.visualize = this.visualize.bind(this); 35 | this.on('resize.default', this.visualize); 36 | this.on('data.default', this.visualize); 37 | } 38 | 39 | // You can define a new function for this class. 40 | visualize() { 41 | this.clear(); 42 | 43 | if(!this.hasData()){ 44 | return; 45 | } 46 | 47 | const data = this.data(); 48 | 49 | this.xScale.domain(extent(data, d => d.x)) 50 | .range([0, this.getInnerWidth()]) 51 | .nice(); 52 | this.yScale.domain(extent(data, d => d.y)) 53 | .range([this.getInnerHeight(), 0]) 54 | .nice(); 55 | 56 | const ctx = this.getContext2d(); 57 | data.forEach((d,i) => { 58 | ctx.fillStyle = this.color(i); 59 | ctx.fillRect( 60 | this.xScale(d.x) - d.r, 61 | this.yScale(d.y) - d.r, 62 | d.r * 2, 63 | d.r * 2 64 | ); 65 | }); 66 | 67 | } 68 | } -------------------------------------------------------------------------------- /examples/src/HybridExample.js: -------------------------------------------------------------------------------- 1 | import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; 2 | import { axisLeft, axisBottom } from 'd3-axis'; 3 | import { extent } from 'd3-array'; 4 | import { HybridChart, helper } from '../../src/main.js'; 5 | 6 | export default class HybridExample extends HybridChart { 7 | static getDefaultOptions() { 8 | return helper.deepExtend( 9 | super.getDefaultOptions(), 10 | { 11 | margin: {top: 60, right: 60, bottom: 60, left: 60}, 12 | initialWidth: 800, 13 | initialHeight: 460 14 | } 15 | ); 16 | } 17 | 18 | /** 19 | * Define the names of custom events that can be dispatched from this chart 20 | * @return {Array[String]} event names 21 | */ 22 | static getCustomEventNames() { 23 | return ['bubbleClick']; 24 | } 25 | 26 | constructor(selector, options) { 27 | super(selector, options); 28 | 29 | // create layers 30 | this.layers.create(['x-axis', 'y-axis']); 31 | 32 | // add custom variables 33 | this.xScale = scaleLinear(); 34 | this.yScale = scaleLinear(); 35 | this.color = scaleOrdinal(schemeCategory10); 36 | this.xAxis = axisBottom().scale(this.xScale); 37 | this.yAxis = axisLeft().scale(this.yScale); 38 | 39 | // add basic event listeners 40 | this.visualize = this.visualize.bind(this); 41 | this.on('resize.default', this.visualize); 42 | this.on('data.default', this.visualize); 43 | } 44 | 45 | // You can define a new function for this class. 46 | visualize() { 47 | if(!this.hasData()){ 48 | this.layers.get('content').selectAll('*').remove(); 49 | return; 50 | } 51 | 52 | const data = this.data(); 53 | 54 | this.xScale.domain(extent(data, d => d.x)) 55 | .range([0, this.getInnerWidth()]) 56 | .nice(); 57 | this.yScale.domain(extent(data, d => d.y)) 58 | .range([this.getInnerHeight(), 0]) 59 | .nice(); 60 | 61 | this.layers.get('x-axis') 62 | .attr('transform', `translate(0,${this.getInnerHeight()})`) 63 | .call(this.xAxis); 64 | 65 | this.layers.get('y-axis') 66 | .call(this.yAxis); 67 | 68 | this.clear(); 69 | 70 | const ctx = this.getContext2d(); 71 | data.forEach((d,i) => { 72 | ctx.fillStyle = this.color(i); 73 | ctx.fillRect( 74 | this.xScale(d.x) - d.r, 75 | this.yScale(d.y) - d.r, 76 | d.r * 2, 77 | d.r * 2 78 | ); 79 | }); 80 | } 81 | } -------------------------------------------------------------------------------- /examples/src/SvgExample.js: -------------------------------------------------------------------------------- 1 | import { scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3-scale'; 2 | import { axisLeft, axisBottom } from 'd3-axis'; 3 | import { extent } from 'd3-array'; 4 | import { SvgChart, helper } from '../../src/main.js'; 5 | 6 | export default class SvgExample extends SvgChart { 7 | static getDefaultOptions() { 8 | return helper.deepExtend( 9 | super.getDefaultOptions(), 10 | { 11 | margin: {top: 60, right: 60, bottom: 60, left: 60}, 12 | initialWidth: 800, 13 | initialHeight: 460 14 | } 15 | ); 16 | } 17 | 18 | /** 19 | * Define the names of custom events that can be dispatched from this chart 20 | * @return {Array[String]} event names 21 | */ 22 | static getCustomEventNames() { 23 | return ['bubbleClick']; 24 | } 25 | 26 | constructor(selector, options) { 27 | super(selector, options); 28 | 29 | // create layers 30 | this.layers.create(['content', 'x-axis', 'y-axis']); 31 | 32 | // add custom variables 33 | this.xScale = scaleLinear(); 34 | this.yScale = scaleLinear(); 35 | this.color = scaleOrdinal(schemeCategory10); 36 | this.xAxis = axisBottom().scale(this.xScale); 37 | this.yAxis = axisLeft().scale(this.yScale); 38 | 39 | // add basic event listeners 40 | this.visualize = this.visualize.bind(this); 41 | this.on('resize.default', this.visualize); 42 | this.on('data.default', this.visualize); 43 | } 44 | 45 | // You can define a new function for this class. 46 | visualize() { 47 | if(!this.hasData()){ 48 | this.layers.get('content').selectAll('*').remove(); 49 | return; 50 | } 51 | 52 | const data = this.data(); 53 | 54 | this.xScale.domain(extent(data, d => d.x)) 55 | .range([0, this.getInnerWidth()]) 56 | .nice(); 57 | this.yScale.domain(extent(data, d => d.y)) 58 | .range([this.getInnerHeight(), 0]) 59 | .nice(); 60 | 61 | this.layers.get('x-axis') 62 | .attr('transform', `translate(0,${this.getInnerHeight()})`) 63 | .call(this.xAxis); 64 | 65 | this.layers.get('y-axis') 66 | .call(this.yAxis); 67 | 68 | const selection = this.layers.get('content').selectAll('circle') 69 | .data(data); 70 | 71 | selection.exit().remove(); 72 | 73 | const sEnter = selection.enter().append('circle') 74 | .attr('cx', d => this.xScale(d.x)) 75 | .attr('cy', d => this.yScale(d.y)) 76 | .on('click', (...args) => { 77 | this.dispatcher.apply('bubbleClick', this, args); 78 | }); 79 | 80 | selection.merge(sEnter) 81 | .attr('cx', d => this.xScale(d.x)) 82 | .attr('cy', d => this.yScale(d.y)) 83 | .attr('r', d => d.r) 84 | .style('fill', (d,i) => this.color(i)); 85 | } 86 | } -------------------------------------------------------------------------------- /examples/src/main.js: -------------------------------------------------------------------------------- 1 | import SvgExample from './SvgExample.js'; 2 | import CanvasExample from './CanvasExample.js'; 3 | import HybridExample from './HybridExample.js'; 4 | import { generateBubbles } from './util.js'; 5 | 6 | //--------------------------------------------------- 7 | // Generate random data 8 | //--------------------------------------------------- 9 | const bubbles = generateBubbles(); 10 | const info = document.querySelector('#info'); 11 | 12 | //--------------------------------------------------- 13 | // Use the bubble chart 14 | //--------------------------------------------------- 15 | 16 | const options = { 17 | margin: { top: 20, left: 30, right: 30, bottom: 40 }, 18 | initialWidth: 300, 19 | initialHeight: 200, 20 | }; 21 | const fitOptions = { 22 | width: '100%', 23 | height: 200 24 | }; 25 | const charts = [ 26 | new SvgExample('#chart', options) 27 | .data(bubbles) 28 | .on('bubbleClick', d => { info.innerHTML = (JSON.stringify(d)); }) 29 | .fit(fitOptions, true), 30 | new CanvasExample('#chart2', options) 31 | .data(bubbles) 32 | .fit(fitOptions, true), 33 | new HybridExample('#chart3', options) 34 | .data(bubbles) 35 | .fit(fitOptions, true) 36 | ]; 37 | 38 | //--------------------------------------------------- 39 | // Buttons 40 | //--------------------------------------------------- 41 | document.querySelector('#data-btn') 42 | .addEventListener('click', () => { 43 | const newData = generateBubbles(); 44 | charts.forEach(chart => { 45 | chart.data(newData); 46 | }); 47 | }); 48 | 49 | document.querySelector('#fit-btn') 50 | .addEventListener('click', () => { 51 | charts.forEach(chart => { 52 | chart.fit(fitOptions); 53 | }); 54 | }); 55 | 56 | let i = 1; 57 | document.querySelector('#resize-btn') 58 | .addEventListener('click', () => { 59 | charts.forEach(chart => { 60 | chart.dimension([200 * i, 100 * i]); 61 | }); 62 | i = i===1 ? 2 : 1; 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /examples/src/util.js: -------------------------------------------------------------------------------- 1 | export function generateBubbles() { 2 | const array = []; 3 | for(let i=0;i<100;i++){ 4 | array.push({ 5 | x: Math.random()*100, 6 | y: Math.random()*100, 7 | r: Math.random()*5+3 8 | }); 9 | } 10 | return array; 11 | } -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | // Overwrite milligram 2 | a { 3 | color: #55acee; 4 | } 5 | 6 | button { 7 | background-color: #55acee; 8 | border-color: #55acee; 9 | } 10 | 11 | input[type="email"]:focus, 12 | input[type="number"]:focus, 13 | input[type="password"]:focus, 14 | input[type="search"]:focus, 15 | input[type="tel"]:focus, 16 | input[type="text"]:focus, 17 | input[type="url"]:focus, 18 | textarea:focus, select:focus { 19 | border-color: #55acee; 20 | outline: 0px; 21 | } 22 | 23 | header { 24 | text-align: center; 25 | margin-top: 20px; 26 | } 27 | 28 | .button-bar { 29 | margin-bottom: 20px; 30 | } 31 | 32 | p { 33 | text-align: justify; 34 | } 35 | 36 | footer{ 37 | text-align: center; 38 | color: #aaa; 39 | font-size: 12px; 40 | font-weight: 300; 41 | padding: 40px 20px; 42 | } 43 | 44 | .column { 45 | overflow: hidden; 46 | } -------------------------------------------------------------------------------- /examples/vendor/milligram.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Milligram v1.2.4 3 | * http://milligram.github.io 4 | * 5 | * Copyright (c) 2016 CJ Patoilo 6 | * Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.0rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:3.0rem;line-height:1.3}h4{font-size:2.4rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}@media (min-width: 40rem){h1{font-size:5.0rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3.0rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /*# sourceMappingURL=milligram.min.css.map */ -------------------------------------------------------------------------------- /examples/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in 7 | * IE on Windows Phone and in iOS. 8 | */ 9 | 10 | /* Document 11 | ========================================================================== */ 12 | 13 | html { 14 | font-family: sans-serif; /* 1 */ 15 | line-height: 1.15; /* 2 */ 16 | -ms-text-size-adjust: 100%; /* 3 */ 17 | -webkit-text-size-adjust: 100%; /* 3 */ 18 | } 19 | 20 | /* Sections 21 | ========================================================================== */ 22 | 23 | /** 24 | * Remove the margin in all browsers (opinionated). 25 | */ 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | /** 32 | * Add the correct display in IE 9-. 33 | */ 34 | 35 | article, 36 | aside, 37 | footer, 38 | header, 39 | nav, 40 | section { 41 | display: block; 42 | } 43 | 44 | /** 45 | * Correct the font size and margin on `h1` elements within `section` and 46 | * `article` contexts in Chrome, Firefox, and Safari. 47 | */ 48 | 49 | h1 { 50 | font-size: 2em; 51 | margin: 0.67em 0; 52 | } 53 | 54 | /* Grouping content 55 | ========================================================================== */ 56 | 57 | /** 58 | * Add the correct display in IE 9-. 59 | * 1. Add the correct display in IE. 60 | */ 61 | 62 | figcaption, 63 | figure, 64 | main { /* 1 */ 65 | display: block; 66 | } 67 | 68 | /** 69 | * Add the correct margin in IE 8. 70 | */ 71 | 72 | figure { 73 | margin: 1em 40px; 74 | } 75 | 76 | /** 77 | * 1. Add the correct box sizing in Firefox. 78 | * 2. Show the overflow in Edge and IE. 79 | */ 80 | 81 | hr { 82 | box-sizing: content-box; /* 1 */ 83 | height: 0; /* 1 */ 84 | overflow: visible; /* 2 */ 85 | } 86 | 87 | /** 88 | * 1. Correct the inheritance and scaling of font size in all browsers. 89 | * 2. Correct the odd `em` font sizing in all browsers. 90 | */ 91 | 92 | pre { 93 | font-family: monospace, monospace; /* 1 */ 94 | font-size: 1em; /* 2 */ 95 | } 96 | 97 | /* Text-level semantics 98 | ========================================================================== */ 99 | 100 | /** 101 | * 1. Remove the gray background on active links in IE 10. 102 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 103 | */ 104 | 105 | a { 106 | background-color: transparent; /* 1 */ 107 | -webkit-text-decoration-skip: objects; /* 2 */ 108 | } 109 | 110 | /** 111 | * Remove the outline on focused links when they are also active or hovered 112 | * in all browsers (opinionated). 113 | */ 114 | 115 | a:active, 116 | a:hover { 117 | outline-width: 0; 118 | } 119 | 120 | /** 121 | * 1. Remove the bottom border in Firefox 39-. 122 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 123 | */ 124 | 125 | abbr[title] { 126 | border-bottom: none; /* 1 */ 127 | text-decoration: underline; /* 2 */ 128 | text-decoration: underline dotted; /* 2 */ 129 | } 130 | 131 | /** 132 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: inherit; 138 | } 139 | 140 | /** 141 | * Add the correct font weight in Chrome, Edge, and Safari. 142 | */ 143 | 144 | b, 145 | strong { 146 | font-weight: bolder; 147 | } 148 | 149 | /** 150 | * 1. Correct the inheritance and scaling of font size in all browsers. 151 | * 2. Correct the odd `em` font sizing in all browsers. 152 | */ 153 | 154 | code, 155 | kbd, 156 | samp { 157 | font-family: monospace, monospace; /* 1 */ 158 | font-size: 1em; /* 2 */ 159 | } 160 | 161 | /** 162 | * Add the correct font style in Android 4.3-. 163 | */ 164 | 165 | dfn { 166 | font-style: italic; 167 | } 168 | 169 | /** 170 | * Add the correct background and color in IE 9-. 171 | */ 172 | 173 | mark { 174 | background-color: #ff0; 175 | color: #000; 176 | } 177 | 178 | /** 179 | * Add the correct font size in all browsers. 180 | */ 181 | 182 | small { 183 | font-size: 80%; 184 | } 185 | 186 | /** 187 | * Prevent `sub` and `sup` elements from affecting the line height in 188 | * all browsers. 189 | */ 190 | 191 | sub, 192 | sup { 193 | font-size: 75%; 194 | line-height: 0; 195 | position: relative; 196 | vertical-align: baseline; 197 | } 198 | 199 | sub { 200 | bottom: -0.25em; 201 | } 202 | 203 | sup { 204 | top: -0.5em; 205 | } 206 | 207 | /* Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Add the correct display in IE 9-. 212 | */ 213 | 214 | audio, 215 | video { 216 | display: inline-block; 217 | } 218 | 219 | /** 220 | * Add the correct display in iOS 4-7. 221 | */ 222 | 223 | audio:not([controls]) { 224 | display: none; 225 | height: 0; 226 | } 227 | 228 | /** 229 | * Remove the border on images inside links in IE 10-. 230 | */ 231 | 232 | img { 233 | border-style: none; 234 | } 235 | 236 | /** 237 | * Hide the overflow in IE. 238 | */ 239 | 240 | svg:not(:root) { 241 | overflow: hidden; 242 | } 243 | 244 | /* Forms 245 | ========================================================================== */ 246 | 247 | /** 248 | * 1. Change the font styles in all browsers (opinionated). 249 | * 2. Remove the margin in Firefox and Safari. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | font-family: sans-serif; /* 1 */ 258 | font-size: 100%; /* 1 */ 259 | line-height: 1.15; /* 1 */ 260 | margin: 0; /* 2 */ 261 | } 262 | 263 | /** 264 | * Show the overflow in IE. 265 | * 1. Show the overflow in Edge. 266 | */ 267 | 268 | button, 269 | input { /* 1 */ 270 | overflow: visible; 271 | } 272 | 273 | /** 274 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 275 | * 1. Remove the inheritance of text transform in Firefox. 276 | */ 277 | 278 | button, 279 | select { /* 1 */ 280 | text-transform: none; 281 | } 282 | 283 | /** 284 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 285 | * controls in Android 4. 286 | * 2. Correct the inability to style clickable types in iOS and Safari. 287 | */ 288 | 289 | button, 290 | html [type="button"], /* 1 */ 291 | [type="reset"], 292 | [type="submit"] { 293 | -webkit-appearance: button; /* 2 */ 294 | } 295 | 296 | /** 297 | * Remove the inner border and padding in Firefox. 298 | */ 299 | 300 | button::-moz-focus-inner, 301 | [type="button"]::-moz-focus-inner, 302 | [type="reset"]::-moz-focus-inner, 303 | [type="submit"]::-moz-focus-inner { 304 | border-style: none; 305 | padding: 0; 306 | } 307 | 308 | /** 309 | * Restore the focus styles unset by the previous rule. 310 | */ 311 | 312 | button:-moz-focusring, 313 | [type="button"]:-moz-focusring, 314 | [type="reset"]:-moz-focusring, 315 | [type="submit"]:-moz-focusring { 316 | outline: 1px dotted ButtonText; 317 | } 318 | 319 | /** 320 | * Change the border, margin, and padding in all browsers (opinionated). 321 | */ 322 | 323 | fieldset { 324 | border: 1px solid #c0c0c0; 325 | margin: 0 2px; 326 | padding: 0.35em 0.625em 0.75em; 327 | } 328 | 329 | /** 330 | * 1. Correct the text wrapping in Edge and IE. 331 | * 2. Correct the color inheritance from `fieldset` elements in IE. 332 | * 3. Remove the padding so developers are not caught out when they zero out 333 | * `fieldset` elements in all browsers. 334 | */ 335 | 336 | legend { 337 | box-sizing: border-box; /* 1 */ 338 | color: inherit; /* 2 */ 339 | display: table; /* 1 */ 340 | max-width: 100%; /* 1 */ 341 | padding: 0; /* 3 */ 342 | white-space: normal; /* 1 */ 343 | } 344 | 345 | /** 346 | * 1. Add the correct display in IE 9-. 347 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 348 | */ 349 | 350 | progress { 351 | display: inline-block; /* 1 */ 352 | vertical-align: baseline; /* 2 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * 1. Correct the inability to style clickable types in iOS and Safari. 404 | * 2. Change font properties to `inherit` in Safari. 405 | */ 406 | 407 | ::-webkit-file-upload-button { 408 | -webkit-appearance: button; /* 1 */ 409 | font: inherit; /* 2 */ 410 | } 411 | 412 | /* Interactive 413 | ========================================================================== */ 414 | 415 | /* 416 | * Add the correct display in IE 9-. 417 | * 1. Add the correct display in Edge, IE, and Firefox. 418 | */ 419 | 420 | details, /* 1 */ 421 | menu { 422 | display: block; 423 | } 424 | 425 | /* 426 | * Add the correct display in all browsers. 427 | */ 428 | 429 | summary { 430 | display: list-item; 431 | } 432 | 433 | /* Scripting 434 | ========================================================================== */ 435 | 436 | /** 437 | * Add the correct display in IE 9-. 438 | */ 439 | 440 | canvas { 441 | display: inline-block; 442 | } 443 | 444 | /** 445 | * Add the correct display in IE. 446 | */ 447 | 448 | template { 449 | display: none; 450 | } 451 | 452 | /* Hidden 453 | ========================================================================== */ 454 | 455 | /** 456 | * Add the correct display in IE 10-. 457 | */ 458 | 459 | [hidden] { 460 | display: none; 461 | } 462 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 'use strict'; 3 | 4 | const babel = require('rollup-plugin-babel'); 5 | const babelrc = require('babelrc-rollup').default; 6 | const nodeResolve = require('rollup-plugin-node-resolve'); 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | 11 | // base path that will be used to resolve all patterns (eg. files, exclude) 12 | basePath: '.', 13 | 14 | // frameworks to use 15 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 16 | frameworks: ['mocha', 'chai'], 17 | 18 | // each file acts as entry point for the webpack configuration 19 | files: [ 20 | // Add the js files so it will trigger watch, 21 | // but do not include them as tests 22 | { pattern: 'src/**/!(*.spec).@(js|jsx)', included: false }, 23 | // Add all files ending in ".spec.js" 24 | // These are the unit test files 25 | 'src/**/*.spec.js', 26 | ], 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | preprocessors: { 31 | 'src/**/*.spec.js': ['rollup'] 32 | }, 33 | 34 | rollupPreprocessor: { 35 | // rollup settings. See Rollup documentation 36 | plugins: [ 37 | nodeResolve(), 38 | babel(babelrc()) 39 | ], 40 | // will help to prevent conflicts between different tests entries 41 | format: 'iife', 42 | sourceMap: 'inline' 43 | }, 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress', 'mocha', 'coverage' 47 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 48 | reporters: ['mocha', 'coverage'], 49 | 50 | coverageReporter: { 51 | reporters: [ 52 | { type: 'text' }, 53 | { type: 'html' } 54 | ] 55 | }, 56 | 57 | // web server port 58 | port: 9876, 59 | 60 | // enable / disable colors in the output (reporters and logs) 61 | colors: true, 62 | 63 | // level of logging 64 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 65 | logLevel: config.LOG_INFO, 66 | 67 | // // enable / disable watching file and executing tests whenever any file changes 68 | // autoWatch: true, 69 | 70 | // start these browsers 71 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 72 | browsers: [ 73 | 'PhantomJS' 74 | // 'Chrome', 75 | // 'Firefox' 76 | ] 77 | 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3kit", 3 | "version": "3.2.0", 4 | "description": "A kit of tools to speed D3 related project development.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/twitter/d3kit" 8 | }, 9 | "keywords": [ 10 | "d3", 11 | "d3kit", 12 | "visualization", 13 | "javascript" 14 | ], 15 | "author": [ 16 | "Krist Wongsuphasawat (http://kristw.yellowpigz.com)", 17 | "Robert Harris " 18 | ], 19 | "license": "MIT", 20 | "main": "dist/d3kit.min.js", 21 | "files": [ 22 | "src/**/*.*", 23 | "dist/*.*" 24 | ], 25 | "dependencies": { 26 | "d3-dispatch": "^1.0.1", 27 | "d3-selection": "^1.0.2" 28 | }, 29 | "devDependencies": { 30 | "babel-eslint": "^6.1.2", 31 | "babel-plugin-external-helpers": "^6.8.0", 32 | "babel-plugin-istanbul": "^2.0.1", 33 | "babel-preset-es2015": "^6.3.13", 34 | "babelrc-rollup": "^3.0.0", 35 | "browser-sync": "~2.14.0", 36 | "chai": "^3.5.0", 37 | "d3": "^4.4.1", 38 | "eslint": "^2.9.0", 39 | "eslint-config-airbnb": "^9.0.1", 40 | "eslint-plugin-import": "^1.12.0", 41 | "eslint-plugin-jsx-a11y": "^1.2.0", 42 | "eslint-plugin-mocha": "^4.7.0", 43 | "eslint-plugin-react": "^5.0.1", 44 | "gh-pages": "^0.11.0", 45 | "karma": "~0.13.15", 46 | "karma-chai": "^0.1.0", 47 | "karma-coverage": "~0.5.3", 48 | "karma-mocha": "^1.1.1", 49 | "karma-mocha-reporter": "^2.1.0", 50 | "karma-phantomjs-launcher": "^1.0.1", 51 | "karma-rollup-plugin": "^0.2.4", 52 | "lodash-es": "^4.16.4", 53 | "mocha": "^3.0.2", 54 | "pkgfiles": "^2.3.0", 55 | "rollup": "^0.34.7", 56 | "rollup-plugin-babel": "^2.6.1", 57 | "rollup-plugin-istanbul": "^1.1.0", 58 | "rollup-plugin-node-resolve": "^2.0.0", 59 | "rollup-watch": "^3.2.2", 60 | "slimfit": "^0.4.2", 61 | "uglifyjs": "^2.4.10" 62 | }, 63 | "scripts": { 64 | "rollup": "rollup -c", 65 | "build": "npm run rollup && uglifyjs dist/d3kit.js -m -c > dist/d3kit.min.js", 66 | "test": "NODE_ENV=test karma start --single-run", 67 | "tdd": "NODE_ENV=test karma start", 68 | "eslint": "eslint --ignore-path .gitignore \"src/**/*.@(js|jsx)\"", 69 | "eslint-fix": "eslint --fix --ignore-path .gitignore \"src/**/*.@(js|jsx)\"", 70 | "preversion": "npm run test", 71 | "version": "npm run build && git add -A dist", 72 | "postversion": "git push ; git push --tags; pkgfiles", 73 | "prepublish": "pkgfiles", 74 | "server": "browser-sync start --server 'examples' --browser \"Google Chrome\" --files \"examples/**/*.*\" --port 3003", 75 | "dev-examples": "rollup -c rollup.config.examples.js -w & npm run server", 76 | "build-examples": "rollup -c rollup.config.examples.js", 77 | "gh-pages": "npm run build-examples && gh-pages -d examples" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter/d3kit/5cd333bd240522c26f4e6f85356b47cfc62a71d1/resources/skeleton.png -------------------------------------------------------------------------------- /rollup.config.examples.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | 5 | export default { 6 | entry: 'examples/src/main.js', 7 | plugins: [ 8 | nodeResolve(), 9 | babel(babelrc()) 10 | ], 11 | dest: 'examples/dist/main.js', 12 | format: 'iife', 13 | sourceMap: true 14 | }; 15 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import babelrc from 'babelrc-rollup'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | 5 | export default { 6 | entry: 'src/main.js', 7 | plugins: [ 8 | nodeResolve(), 9 | babel(babelrc()) 10 | ], 11 | external: ['d3-selection', 'd3-dispatch'], 12 | globals: { 13 | 'd3-selection': 'd3', 14 | 'd3-dispatch': 'd3' 15 | }, 16 | targets: [ 17 | { 18 | dest: 'dist/d3kit.js', 19 | format: 'umd', 20 | moduleName: 'd3Kit', 21 | sourceMap: true 22 | }, 23 | { 24 | dest: 'dist/d3kit-es.js', 25 | format: 'es' 26 | } 27 | ] 28 | }; -------------------------------------------------------------------------------- /src/Base.js: -------------------------------------------------------------------------------- 1 | import { debounce, deepExtend, extend } from './helper.js'; 2 | 3 | class Base { 4 | static getDefaultOptions(...options) { 5 | return deepExtend({ 6 | initialWidth: 720, 7 | initialHeight: 500, 8 | margin: { 9 | top: 30, 10 | right: 30, 11 | bottom: 30, 12 | left: 30, 13 | }, 14 | offset: [0.5, 0.5], 15 | pixelRatio: window.devicePixelRatio || 1, 16 | }, ...options); 17 | } 18 | 19 | constructor(...options) { 20 | const mergedOptions = deepExtend( 21 | this.constructor.getDefaultOptions(), 22 | ...options 23 | ); 24 | 25 | this._state = { 26 | width: mergedOptions.initialWidth, 27 | height: mergedOptions.initialHeight, 28 | options: mergedOptions, 29 | }; 30 | 31 | this._updateDimension = debounce(this._updateDimension.bind(this), 1); 32 | } 33 | 34 | copyDimension(another) { 35 | if (another) { 36 | const { width, height } = another._state; 37 | const { offset, margin, pixelRatio } = another._state.options; 38 | 39 | deepExtend(this._state, { 40 | width, 41 | height, 42 | options: { 43 | offset: offset.concat(), 44 | margin, 45 | pixelRatio, 46 | }, 47 | }); 48 | this._updateDimension(); 49 | } 50 | return this; 51 | } 52 | 53 | width(...args) { 54 | if (args.length === 0) return this._state.width; 55 | const newValue = Math.floor(+args[0]); 56 | if (newValue !== this._state.width) { 57 | this._state.width = newValue; 58 | this._updateDimension(); 59 | } 60 | return this; 61 | } 62 | 63 | height(...args) { 64 | if (args.length === 0) return this._state.height; 65 | const newValue = Math.floor(+args[0]); 66 | if (newValue !== this._state.height) { 67 | this._state.height = newValue; 68 | this._updateDimension(); 69 | } 70 | return this; 71 | } 72 | 73 | dimension(...args) { 74 | if (args.length === 0) { 75 | return [this._state.width, this._state.height]; 76 | } 77 | const [w, h] = args[0]; 78 | this.width(w).height(h); 79 | return this; 80 | } 81 | 82 | margin(...args) { 83 | if (args.length === 0) return this._state.options.margin; 84 | const oldMargin = this._state.options.margin; 85 | const newMargin = extend({}, this._state.options.margin, args[0]); 86 | const changed = Object.keys(newMargin) 87 | .some(field => oldMargin[field] !== newMargin[field]); 88 | if (changed) { 89 | this._state.options.margin = newMargin; 90 | this._updateDimension(); 91 | } 92 | return this; 93 | } 94 | 95 | offset(...args) { 96 | if (args.length === 0) return this._state.options.offset; 97 | const newOffset = args[0]; 98 | const [ox, oy] = this._state.options.offset; 99 | const [nx, ny] = newOffset; 100 | if (ox !== nx || oy !== ny) { 101 | this._state.options.offset = newOffset; 102 | this._updateDimension(); 103 | } 104 | return this; 105 | } 106 | 107 | pixelRatio(...args) { 108 | if (args.length === 0) return this._state.options.pixelRatio; 109 | const newValue = +args[0]; 110 | if (newValue !== this._state.options.pixelRatio) { 111 | this._state.options.pixelRatio = newValue; 112 | this._updateDimension(); 113 | } 114 | return this; 115 | } 116 | 117 | _updateDimension() { 118 | // Intentionally do nothing 119 | // Subclasses can override this function 120 | return this; 121 | } 122 | 123 | updateDimensionNow() { 124 | this._updateDimension(); 125 | this._updateDimension.flush(); 126 | return this; 127 | } 128 | } 129 | 130 | export default Base; 131 | -------------------------------------------------------------------------------- /src/Base.spec.js: -------------------------------------------------------------------------------- 1 | import Base from './Base.js'; 2 | 3 | class TestBase extends Base { 4 | constructor(...args) { 5 | super(...args); 6 | this.counter = { 7 | updateDimension: 0, 8 | }; 9 | } 10 | 11 | _updateDimension() { 12 | this.counter.updateDimension++; 13 | } 14 | } 15 | 16 | describe('Base', () => { 17 | let base; 18 | 19 | it('should exist', () => { 20 | expect(Base).to.exist; 21 | }); 22 | 23 | beforeEach(() => { 24 | base = new TestBase({ 25 | initialWidth: 10, 26 | initialHeight: 10, 27 | margin: { 28 | top: 10, 29 | right: 10, 30 | bottom: 10, 31 | left: 10, 32 | }, 33 | offset: [1, 1], 34 | pixelRatio: 10, 35 | }); 36 | }); 37 | 38 | describe('(static) Base.getDefaultOptions()', () => { 39 | it('should return an options object', () => { 40 | const options = Base.getDefaultOptions(); 41 | expect(options).to.be.an('Object'); 42 | }); 43 | }); 44 | 45 | describe('new Base(options)', () => { 46 | it('should construct an object with the given options', () => { 47 | expect(base).to.exist; 48 | expect(base.width()).to.equal(10); 49 | expect(base.height()).to.equal(10); 50 | expect(base.margin()).to.deep.equal({ top: 10, right: 10, bottom: 10, left: 10 }); 51 | expect(base.pixelRatio()).to.equal(10); 52 | expect(base.offset()).to.deep.equal([1, 1]); 53 | }); 54 | }); 55 | 56 | describe('.dimension([dimension])', () => { 57 | describe('as getter: when called without argument', () => { 58 | it('should return current value', () => { 59 | expect(base.dimension()).to.deep.equal([10, 10]); 60 | }); 61 | }); 62 | describe('as setter: when called with argument', () => { 63 | it('should set value and update dimension if the new value is different from current value', (done) => { 64 | base.dimension([20, 20]); 65 | expect(base.dimension()).to.deep.equal([20, 20]); 66 | window.setTimeout(() => { 67 | expect(base.counter.updateDimension).to.equal(1); 68 | done(); 69 | }, 10); 70 | }); 71 | it('should not update dimension if the new value is equal to current value', (done) => { 72 | base.dimension([10, 10]); 73 | expect(base.dimension()).to.deep.equal([10, 10]); 74 | window.setTimeout(() => { 75 | expect(base.counter.updateDimension).to.equal(0); 76 | done(); 77 | }, 10); 78 | }); 79 | it('should return this', () => { 80 | expect(base.dimension([5, 5])).to.equal(base); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('.margin([margin])', () => { 86 | describe('as getter: when called without argument', () => { 87 | it('should return current value', () => { 88 | expect(base.margin()).to.deep.equal({ left: 10, right: 10, top: 10, bottom: 10 }); 89 | }); 90 | }); 91 | describe('as setter: when called with argument', () => { 92 | it('should set value and update dimension if the new value is different from current value', (done) => { 93 | base.margin({ left: 1, right: 1, top: 1, bottom: 1 }); 94 | expect(base.margin()).to.deep.equal({ left: 1, right: 1, top: 1, bottom: 1 }); 95 | window.setTimeout(() => { 96 | expect(base.counter.updateDimension).to.equal(1); 97 | done(); 98 | }, 10); 99 | }); 100 | it('should accept partial values, not all "top", "left", "bottom", "right" have to be present.', () => { 101 | base.margin({ left: 21 }); 102 | expect(base.margin().left).to.equal(21); 103 | base.margin({ right: 22 }); 104 | expect(base.margin().right).to.equal(22); 105 | base.margin({ top: 23 }); 106 | expect(base.margin().top).to.equal(23); 107 | base.margin({ bottom: 24 }); 108 | expect(base.margin().bottom).to.equal(24); 109 | }); 110 | it('should not update dimension if the new value is equal to current value', (done) => { 111 | base.margin({ left: 10, right: 10, top: 10, bottom: 10 }); 112 | expect(base.margin()).to.deep.equal({ left: 10, right: 10, top: 10, bottom: 10 }); 113 | window.setTimeout(() => { 114 | expect(base.counter.updateDimension).to.equal(0); 115 | done(); 116 | }, 10); 117 | }); 118 | it('should return this', () => { 119 | expect(base.margin({ top: 12 })).to.equal(base); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('.offset([offset])', () => { 125 | describe('as getter: when called without argument', () => { 126 | it('should return current value', () => { 127 | expect(base.offset()).to.deep.equal([1, 1]); 128 | }); 129 | }); 130 | describe('as setter: when called with argument', () => { 131 | it('should set value and update dimension if the new value is different from current value', (done) => { 132 | base.offset([2, 2]); 133 | expect(base.offset()).to.deep.equal([2, 2]); 134 | window.setTimeout(() => { 135 | expect(base.counter.updateDimension).to.equal(1); 136 | done(); 137 | }, 10); 138 | }); 139 | it('should not update dimension if the new value is equal to current value', (done) => { 140 | base.offset([1, 1]); 141 | expect(base.offset()).to.deep.equal([1, 1]); 142 | window.setTimeout(() => { 143 | expect(base.counter.updateDimension).to.equal(0); 144 | done(); 145 | }, 10); 146 | }); 147 | it('should return this', () => { 148 | expect(base.offset([3, 3])).to.equal(base); 149 | }); 150 | }); 151 | }); 152 | 153 | ['width', 'height', 'pixelRatio'].forEach(field => { 154 | describe(`.${field}([${field}]])`, () => { 155 | describe('as getter: when called without argument', () => { 156 | it('should return current value', () => { 157 | expect(base[field]()).to.equal(10); 158 | }); 159 | }); 160 | describe('as setter: when called with argument', () => { 161 | it('should set value and update dimension if the new value is different from current value', (done) => { 162 | base[field](1); 163 | expect(base[field]()).to.equal(1); 164 | window.setTimeout(() => { 165 | expect(base.counter.updateDimension).to.equal(1); 166 | done(); 167 | }, 10); 168 | }); 169 | it('should not update dimension if the new value is equal to current value', (done) => { 170 | base[field](10); 171 | expect(base[field]()).to.equal(10); 172 | window.setTimeout(() => { 173 | expect(base.counter.updateDimension).to.equal(0); 174 | done(); 175 | }, 10); 176 | }); 177 | it('should return this', () => { 178 | expect(base[field](12)).to.equal(base); 179 | }); 180 | }); 181 | }); 182 | }); 183 | 184 | describe('.copyDimension(another)', () => { 185 | it('should copy all fields and update dimension', (done) => { 186 | const state = { 187 | initialWidth: 20, 188 | initialHeight: 20, 189 | margin: { 190 | top: 20, 191 | right: 20, 192 | bottom: 20, 193 | left: 20, 194 | }, 195 | offset: [2, 2], 196 | pixelRatio: 2, 197 | }; 198 | base.copyDimension(new Base(state)); 199 | expect(base.width()).to.equal(state.initialWidth); 200 | expect(base.height()).to.equal(state.initialHeight); 201 | expect(base.pixelRatio()).to.equal(state.pixelRatio); 202 | expect(base.margin()).to.deep.equal(state.margin); 203 | expect(base.offset()).to.deep.equal(state.offset); 204 | 205 | window.setTimeout(() => { 206 | expect(base.counter.updateDimension).to.equal(1); 207 | done(); 208 | }, 10); 209 | }); 210 | it('should do nothing if "another" is not defined', (done) => { 211 | base.copyDimension(null); 212 | base.copyDimension(undefined); 213 | window.setTimeout(() => { 214 | expect(base.counter.updateDimension).to.equal(0); 215 | done(); 216 | }, 10); 217 | }); 218 | it('should return this', () => { 219 | expect(base.copyDimension()).to.equal(base); 220 | }); 221 | }); 222 | 223 | describe('.updateDimensionNow()', () => { 224 | it('should trigger update dimension immediately', () => { 225 | expect(base.counter.updateDimension).to.equal(0); 226 | base.updateDimensionNow(); 227 | expect(base.counter.updateDimension).to.equal(1); 228 | }); 229 | it('should return this', () => { 230 | expect(base.updateDimensionNow()).to.equal(base); 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /src/chartlet.js: -------------------------------------------------------------------------------- 1 | // This module has not been revised yet. 2 | 3 | import { dispatch } from 'd3-dispatch'; 4 | import debounce from 'lodash-es/debounce.js'; 5 | import { functor, rebind } from './helper.js'; 6 | 7 | function NOOP(selection, done) { done(); } 8 | 9 | function Chartlet(enter, update, exit, customEvents) { 10 | update = update || NOOP; 11 | exit = exit || NOOP; 12 | customEvents = customEvents || []; 13 | const _propertyCache = {}; 14 | const _dispatch = dispatch.apply(this, ['enterDone', 'updateDone', 'exitDone'].concat(customEvents)); 15 | 16 | // getter and setter of chartlet properties 17 | 18 | function property(name, value) { 19 | // if functioning as a setter, set property in cache 20 | if (arguments.length > 1) { 21 | _propertyCache[name] = functor(value); 22 | return this; 23 | } 24 | 25 | // functioning as a getter, return property accessor 26 | return functor(_propertyCache[name]); 27 | } 28 | 29 | function getPropertyValue(name, d, i) { 30 | return property(name)(d, i); 31 | } 32 | 33 | function _wrapAction(action, doneHookName) { 34 | return function (selection) { 35 | action(selection, debounce(function (d, i) { 36 | _dispatch.call(doneHookName, this, selection); 37 | }), 5); 38 | }; 39 | } 40 | 41 | function inheritPropertyFrom(chartlet, from, to) { 42 | _propertyCache[to || from] = function (d, i) { return chartlet.property(from)(d, i); }; 43 | return this; 44 | } 45 | 46 | function inheritPropertiesFrom(chartlet, froms, tos) { 47 | froms.forEach(function (from, i) { 48 | inheritPropertyFrom(chartlet, from, tos && i < tos.length ? tos[i] : undefined); 49 | }); 50 | return this; 51 | } 52 | 53 | function publishEventsTo(foreignDispatcher) { 54 | customEvents.forEach(function (event) { 55 | _dispatch.on(event, function () { 56 | const args = Array.prototype.slice.call(arguments); 57 | foreignDispatcher.apply(event, this, args); 58 | }); 59 | }); 60 | return this; 61 | } 62 | 63 | function getCustomEventNames() { 64 | return customEvents; 65 | } 66 | 67 | // exports 68 | const exports = { 69 | // for use by child chartlet 70 | getDispatcher() { return _dispatch; }, 71 | getPropertyValue, 72 | inheritPropertyFrom, 73 | inheritPropertiesFrom, 74 | publishEventsTo, 75 | getCustomEventNames, 76 | 77 | property, 78 | enter: _wrapAction(enter, 'enterDone'), 79 | update: _wrapAction(update, 'updateDone'), 80 | exit: _wrapAction(exit, 'exitDone'), 81 | }; 82 | 83 | // bind events to exports 84 | rebind(exports, _dispatch, 'on'); 85 | 86 | // return exports 87 | return exports; 88 | } 89 | 90 | export default Chartlet; 91 | -------------------------------------------------------------------------------- /src/chartlet.spec.js: -------------------------------------------------------------------------------- 1 | import Chartlet from './chartlet.js'; 2 | 3 | describe('Chartlet', function () { 4 | let enter, update, exit, chartlet; 5 | const customEvents = ['fooEvent']; 6 | let ChildChartlet = null; 7 | let ParentChartlet = null; 8 | 9 | const callback = function (selection, done) { done(); }; 10 | beforeEach(function (done) { 11 | ChildChartlet = function () { 12 | const chartlet = new Chartlet(callback, callback, callback); 13 | chartlet.runTest = function (testFunction) { 14 | testFunction(chartlet); 15 | }; 16 | return chartlet; 17 | }; 18 | 19 | ParentChartlet = function (configureFunction) { 20 | const chartlet = new Chartlet(callback, callback, callback); 21 | const child = new ChildChartlet(); 22 | configureFunction(chartlet, child); 23 | chartlet.runTest = child.runTest; 24 | return chartlet; 25 | }; 26 | 27 | enter = callback; 28 | update = callback; 29 | exit = callback; 30 | chartlet = new Chartlet(enter, update, exit, customEvents); 31 | done(); 32 | }); 33 | 34 | describe('new Chartlet(enter, update, exit, customEvents)', function () { 35 | it('should create a chartlet', function () { 36 | expect(chartlet).to.be.an('Object'); 37 | expect(chartlet).to.include.keys(['property', 'on']); 38 | expect(chartlet.enter).to.be.a('Function'); 39 | expect(chartlet.update).to.be.a('Function'); 40 | expect(chartlet.exit).to.be.a('Function'); 41 | expect(function () { chartlet.enter(); }).to.not.throw(Error); 42 | expect(function () { chartlet.update(); }).to.not.throw(Error); 43 | expect(function () { chartlet.exit(); }).to.not.throw(Error); 44 | expect(chartlet.getCustomEventNames()).to.deep.equal(customEvents); 45 | }); 46 | it('arguments "update", "exit" and "customEvents" are optional', function () { 47 | const comp = new Chartlet(enter); 48 | expect(comp).to.be.an('Object'); 49 | expect(comp).to.include.keys(['property', 'on']); 50 | expect(comp.enter).to.be.a('Function'); 51 | expect(comp.update).to.be.a('Function'); 52 | expect(comp.exit).to.be.a('Function'); 53 | expect(function () { comp.enter(); }).to.not.throw(Error); 54 | expect(function () { comp.update(); }).to.not.throw(Error); 55 | expect(function () { comp.exit(); }).to.not.throw(Error); 56 | }); 57 | }); 58 | 59 | describe('#getDispatcher()', function () { 60 | it('should return a dispatcher', function () { 61 | const dispatcher = chartlet.getDispatcher(); 62 | expect(dispatcher).to.exist; 63 | }); 64 | it('returned dispatcher should handle enter/update/exit events', function () { 65 | const dispatcher = chartlet.getDispatcher(); 66 | expect(dispatcher._).to.include.keys(['enterDone', 'updateDone', 'exitDone'].concat(customEvents)); 67 | }); 68 | }); 69 | 70 | describe('#getPropertyValue(name, d, i)', function () { 71 | it('should return computed value for specified property name, d and i', function () { 72 | const d = { a: 99 }; 73 | const i = 2; 74 | 75 | chartlet.property('foo', 1); 76 | chartlet.property('bar', 'two'); 77 | chartlet.property('baz', function (d, i) { return 3; }); 78 | chartlet.property('qux', function (d, i) { return 'four'; }); 79 | chartlet.property('nux', function (d, i) { return d.a * i; }); 80 | 81 | expect(chartlet.getPropertyValue('foo', d, i)).to.equal(1); 82 | expect(chartlet.getPropertyValue('bar', d, i)).to.equal('two'); 83 | expect(chartlet.getPropertyValue('baz', d, i)).to.equal(3); 84 | expect(chartlet.getPropertyValue('qux', d, i)).to.equal('four'); 85 | expect(chartlet.getPropertyValue('nux', d, i)).to.equal(198); 86 | }); 87 | }); 88 | 89 | describe('#property(name, valueOrFn)', function () { 90 | describe('should act as a getter when called with one argument', function () { 91 | it('should always return a function', function () { 92 | chartlet.property('foo', 1); 93 | expect(chartlet.property('foo')).to.be.a('Function'); 94 | chartlet.property('bar', function () { return 100; }); 95 | expect(chartlet.property('bar')).to.be.a('Function'); 96 | }); 97 | it('should return a function that return undefined for unknown property name', function () { 98 | expect(chartlet.property('unknown name')).to.be.a('Function'); 99 | expect(chartlet.property('unknown name')()).to.equal(undefined); 100 | }); 101 | }); 102 | 103 | describe('should act as a setter when called with two arguments', function () { 104 | it('should set specified property to a functor of given value', function () { 105 | chartlet.property('foo', 1); 106 | expect(chartlet.property('foo')).to.be.a('Function'); 107 | expect(chartlet.property('foo')()).to.equal(1); 108 | chartlet.property('bar', function () { return 100; }); 109 | expect(chartlet.property('bar')).to.be.a('Function'); 110 | expect(chartlet.property('bar')()).to.equal(100); 111 | }); 112 | it('should overwrite previous value when set property with the same name', function () { 113 | chartlet.property('foo', 1); 114 | expect(chartlet.property('foo')()).to.equal(1); 115 | chartlet.property('foo', 100); 116 | expect(chartlet.property('foo')()).to.equal(100); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('#on(eventName, listener)', function () { 122 | it('event "enterDone" should be triggered after chartlet.enter() is completed.', function (done) { 123 | chartlet.on('enterDone', function () { done(); }); 124 | chartlet.enter([]); 125 | }); 126 | it('event "updateDone" should be triggered after chartlet.update() is completed.', function (done) { 127 | chartlet.on('updateDone', function () { done(); }); 128 | chartlet.update([]); 129 | }); 130 | it('event "exitDone" should be triggered after chartlet.exit() is completed.', function (done) { 131 | chartlet.on('exitDone', function () { done(); }); 132 | chartlet.exit([]); 133 | }); 134 | }); 135 | 136 | describe('#inheritPropertyFrom(parentChartlet, parentPropertyName, childPropertyName)', function () { 137 | it('it should cause a child to inherit a parent property', function () { 138 | parent = new ParentChartlet( 139 | function (parent, child) { 140 | child.inheritPropertyFrom(parent, 'foo', 'bar'); 141 | }) 142 | .property('foo', function (d) { return 2 * d; }); 143 | 144 | parent.runTest(function (child) { 145 | expect(child.getPropertyValue('bar', 4)).to.be.equal(8); 146 | }); 147 | }); 148 | 149 | it('it should default to the parent property name', function () { 150 | parent = new ParentChartlet( 151 | function (parent, child) { 152 | child.inheritPropertyFrom(parent, 'foo'); 153 | }) 154 | .property('foo', function (d) { return 2 * d; }); 155 | 156 | parent.runTest(function (child) { 157 | expect(child.getPropertyValue('foo', 4)).to.be.equal(8); 158 | }); 159 | }); 160 | }); 161 | 162 | describe('#inheritProperties(parentChartlet, parentPropertyNames, childPropertyNames)', function () { 163 | it('it should cause a child to inherit many parent properties', function () { 164 | parent = new ParentChartlet( 165 | function (parent, child) { 166 | child.inheritPropertiesFrom(parent, ['foo', 'bar', 'baz'], ['foo-x', 'bar-x', 'baz-x']); 167 | }) 168 | .property('foo', function (d) { return 2 * d; }) 169 | .property('bar', function (d) { return 3 * d; }) 170 | .property('baz', function (d) { return 4 * d; }); 171 | 172 | parent.runTest(function (child) { 173 | expect(child.getPropertyValue('foo-x', 1)).to.be.equal(2); 174 | expect(child.getPropertyValue('bar-x', 1)).to.be.equal(3); 175 | expect(child.getPropertyValue('baz-x', 1)).to.be.equal(4); 176 | }); 177 | }); 178 | 179 | it('it should default to the parent property names', function () { 180 | parent = new ParentChartlet( 181 | function (parent, child) { 182 | child.inheritPropertiesFrom(parent, ['foo', 'bar', 'baz']); 183 | }) 184 | .property('foo', function (d) { return 2 * d; }) 185 | .property('bar', function (d) { return 3 * d; }) 186 | .property('baz', function (d) { return 4 * d; }); 187 | 188 | parent.runTest(function (child) { 189 | expect(child.getPropertyValue('foo', 1)).to.be.equal(2); 190 | expect(child.getPropertyValue('bar', 1)).to.be.equal(3); 191 | expect(child.getPropertyValue('baz', 1)).to.be.equal(4); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('#publishEventsTo(foreignDispatcher)', function () { 197 | it('should map events to a foreignDispatcher', function (done) { 198 | const parent = new Chartlet(callback, callback, callback, ['foo']); 199 | parent.getDispatcher().on('foo', function (value) { 200 | expect(value).to.be.equal(99); 201 | done(); 202 | }); 203 | 204 | const child = new Chartlet(callback, callback, callback, ['foo']) 205 | .publishEventsTo(parent.getDispatcher()); 206 | 207 | child.getDispatcher().call('foo', this, 99); 208 | }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /src/charts/AbstractChart.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import { dispatch } from 'd3-dispatch'; 3 | import FitWatcher from 'slimfit/src/FitWatcher.js'; 4 | import Fitter from 'slimfit/src/Fitter.js'; 5 | import Base from '../Base.js'; 6 | import { debounce, deepExtend, extend, isObject } from '../helper.js'; 7 | 8 | class AbstractChart extends Base { 9 | static getCustomEventNames() { 10 | return []; 11 | } 12 | 13 | constructor(selector, ...options) { 14 | super(...options); 15 | 16 | extend(this._state, { 17 | innerWidth: 0, 18 | innerHeight: 0, 19 | fitOptions: null, 20 | data: null, 21 | plates: [], 22 | }); 23 | 24 | this.container = select(selector); 25 | // Enforce line-height = 0 to fix issue with height resizing 26 | // https://github.com/twitter/d3kit/issues/13 27 | this.container.style('line-height', 0); 28 | 29 | this.chartRoot = this.container.append('div') 30 | .classed('d3kit-chart-root', true) 31 | .style('display', 'inline-block') 32 | .style('position', 'relative') 33 | .style('line-height', 0); 34 | 35 | this.plates = {}; 36 | 37 | const customEvents = this.constructor.getCustomEventNames(); 38 | this.setupDispatcher(customEvents); 39 | 40 | this._dispatchData = debounce(this._dispatchData.bind(this), 1); 41 | this._dispatchOptions = debounce(this._dispatchOptions.bind(this), 1); 42 | } 43 | 44 | addPlate(name, plate, doNotAppend) { 45 | if(this.plates[name]) { 46 | throw new Error('Plate with this name already exists', name); 47 | } 48 | this._state.plates.push(plate); 49 | this.plates[name] = plate; 50 | if (doNotAppend) return plate; 51 | plate.getSelection() 52 | .classed('d3kit-plate', true) 53 | .style('position', 'absolute') 54 | .style('top', 0) 55 | .style('left', 0); 56 | this.chartRoot.append(() => plate.getNode()); 57 | return this; 58 | } 59 | 60 | removePlate(name) { 61 | const plate = this.plates[name]; 62 | if (plate) { 63 | const index = this._state.plates.indexOf(plate); 64 | if (index > -1) { 65 | this._state.plates.splice(index, 1); 66 | } 67 | if (plate.getNode().parentNode === this.chartRoot.node()) { 68 | this.chartRoot.node().removeChild(plate.getNode()); 69 | } 70 | delete this.plates[name]; 71 | } 72 | return this; 73 | } 74 | 75 | setupDispatcher(customEventNames = []) { 76 | this._customEventNames = customEventNames; 77 | this._eventNames = AbstractChart.DEFAULT_EVENTS.concat(customEventNames); 78 | this.dispatcher = dispatch.apply(this, this._eventNames); 79 | return this; 80 | } 81 | 82 | getCustomEventNames() { 83 | return this._customEventNames; 84 | } 85 | 86 | getInnerWidth() { 87 | return this._state.innerWidth; 88 | } 89 | 90 | getInnerHeight() { 91 | return this._state.innerHeight; 92 | } 93 | 94 | data(...args) { 95 | if (args.length === 0) return this._state.data; 96 | const [newData] = args; 97 | this._state.data = newData; 98 | this._dispatchData(); 99 | return this; 100 | } 101 | 102 | options(...args) { 103 | if (args.length === 0) return this._state.options; 104 | const [newOptions] = args; 105 | const copy = extend({}, newOptions); 106 | 107 | if (newOptions.margin) { 108 | this.margin(newOptions.margin); 109 | delete copy.margin; 110 | } 111 | if (newOptions.offset) { 112 | this.offset(newOptions.offset); 113 | delete copy.offset; 114 | } 115 | if (newOptions.pixelRatio) { 116 | this.pixelRatio(newOptions.pixelRatio); 117 | delete copy.pixelRatio; 118 | } 119 | 120 | this._state.options = deepExtend(this._state.options, copy); 121 | 122 | this._dispatchOptions(); 123 | return this; 124 | } 125 | 126 | _updateDimension() { 127 | const { width, height, plates } = this._state; 128 | const { margin } = this._state.options; 129 | const { top, right, bottom, left } = margin; 130 | 131 | this._state.innerWidth = width - left - right; 132 | this._state.innerHeight = height - top - bottom; 133 | 134 | this.chartRoot 135 | .style('width', `${width}px`) 136 | .style('height', `${height}px`); 137 | 138 | plates.forEach(plate => { 139 | plate.copyDimension(this) 140 | .updateDimensionNow(); 141 | }); 142 | 143 | // Dispatch resize event 144 | const { innerWidth, innerHeight } = this._state; 145 | this.dispatcher.apply('resize', this, [width, height, innerWidth, innerHeight]); 146 | 147 | return this; 148 | } 149 | 150 | hasData() { 151 | const { data } = this._state; 152 | return data !== null && data !== undefined; 153 | } 154 | 155 | hasNonZeroArea() { 156 | const { innerWidth, innerHeight } = this._state; 157 | return (innerWidth > 0 && innerHeight > 0); 158 | } 159 | 160 | fit(fitOptions, watchOptions = false) { 161 | if (fitOptions) { 162 | this._state.fitOptions = fitOptions; 163 | } 164 | 165 | // Fit once 166 | const fitter = new Fitter(this._state.fitOptions); 167 | const { changed, dimension } = fitter.fit( 168 | this.dimension(), 169 | this.container.node() 170 | ); 171 | 172 | if (changed) { 173 | this.dimension([dimension.width, dimension.height]); 174 | } 175 | 176 | // Setup watcher 177 | const enable = !!watchOptions; 178 | if (enable) { 179 | if (this.fitWatcher) { 180 | this.fitWatcher.destroy(); 181 | } 182 | this.fitWatcher = new FitWatcher( 183 | // pass getter instead of value 184 | // because the value may change when time the watcher checks 185 | () => this.dimension(), 186 | this.container.node(), 187 | this._state.fitOptions, 188 | isObject(watchOptions) ? watchOptions : null 189 | ) 190 | .on('change', dim => this.dimension([dim.width, dim.height])) 191 | .start(); 192 | } 193 | 194 | return this; 195 | } 196 | 197 | stopFitWatcher() { 198 | if (this.fitWatcher) { 199 | this.fitWatcher.destroy(); 200 | this.fitWatcher = null; 201 | } 202 | return this; 203 | } 204 | 205 | _dispatchData() { 206 | this.dispatcher.call('data', this, this._state.data); 207 | return this; 208 | } 209 | 210 | _dispatchOptions() { 211 | this.dispatcher.call('options', this, this._state.options); 212 | return this; 213 | } 214 | 215 | on(name, listener) { 216 | this.dispatcher.on(name, listener); 217 | return this; 218 | } 219 | 220 | off(name) { 221 | this.dispatcher.on(name, null); 222 | return this; 223 | } 224 | 225 | dispatchAs(name) { 226 | return (...args) => { 227 | this.dispatcher.apply(name, this, args); 228 | }; 229 | } 230 | 231 | destroy() { 232 | this._eventNames.forEach(name => { 233 | this.off(name); 234 | }); 235 | this.stopFitWatcher(); 236 | return this; 237 | } 238 | } 239 | 240 | AbstractChart.DEFAULT_EVENTS = ['data', 'options', 'resize']; 241 | 242 | export default AbstractChart; 243 | -------------------------------------------------------------------------------- /src/charts/AbstractChart.spec.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import AbstractChart from './AbstractChart.js'; 3 | import SvgPlate from '../plates/SvgPlate.js'; 4 | 5 | class CustomChart extends AbstractChart { 6 | static getCustomEventNames() { 7 | return ['custom1', 'custom2']; 8 | } 9 | } 10 | 11 | describe('AbstractChart', () => { 12 | let element, $element, chart; 13 | 14 | beforeEach(() => { 15 | element = document.body.appendChild(document.createElement('div')); 16 | chart = new AbstractChart(element, null); 17 | $element = select(element); 18 | }); 19 | 20 | describe('(static) AbstractChart.getCustomEventNames()', () => { 21 | it('should return an array of custom event names', () => { 22 | const names = AbstractChart.getCustomEventNames(); 23 | expect(names).to.be.an('Array'); 24 | expect(names).to.deep.equal([]); 25 | }); 26 | it('can be overridden by subclass', () => { 27 | const names = CustomChart.getCustomEventNames(); 28 | expect(names).to.be.an('Array'); 29 | expect(names).to.deep.equal(['custom1', 'custom2']); 30 | }); 31 | }); 32 | 33 | describe('new AbstractChart(element, options)', () => { 34 | it('should create a dispatcher as chart.dispatcher', () => { 35 | const dispatcher = chart.dispatcher; 36 | expect(dispatcher).to.be.an('Object'); 37 | expect(dispatcher.call).to.be.a('Function'); 38 | }); 39 | }); 40 | 41 | describe('.addPlate(name, plate [, doNotAppend])', ()=>{ 42 | it('should add a plate to the chart and make plate accessible via chart.plates[name]', ()=>{ 43 | const plate = new SvgPlate(); 44 | chart.addPlate('plate1', plate); 45 | expect(chart.plates.plate1).to.equal(plate); 46 | }); 47 | it('should append plate node to chartRoot', ()=>{ 48 | const plate = new SvgPlate(); 49 | chart.addPlate('plate1', plate); 50 | expect(plate.getNode().parentNode).to.equal(chart.chartRoot.node()); 51 | }); 52 | it('should not append plate node to chartRoot if doNotAppend is true', ()=>{ 53 | const plate = new SvgPlate(); 54 | chart.addPlate('plate1', plate, true); 55 | expect(plate.getNode().parentNode).to.not.equal(chart.chartRoot.node()); 56 | }); 57 | it('should throw error if a plate with this name already exists', ()=>{ 58 | chart.addPlate('plate1', new SvgPlate()); 59 | expect(() => { 60 | chart.addPlate('plate1', new SvgPlate()); 61 | }).to.throw(Error); 62 | }); 63 | }); 64 | 65 | describe('.removePlate(name)', ()=>{ 66 | it('should remove plate from the chart', ()=>{ 67 | const plate = new SvgPlate(); 68 | chart.addPlate('plate1', plate); 69 | chart.removePlate('plate1'); 70 | expect(chart.plates.plate1).to.equal(undefined); 71 | }); 72 | it('should remove plate node from chartRoot', ()=>{ 73 | const plate = new SvgPlate(); 74 | chart.addPlate('plate1', plate); 75 | chart.removePlate('plate1'); 76 | expect(plate.getNode().parentNode).to.not.equal(chart.chartRoot.node()); 77 | }); 78 | it('should do nothing if there is no plate with this name', ()=>{ 79 | expect(() => { 80 | chart.removePlate('plate1'); 81 | }).to.not.throw(Error); 82 | }); 83 | }); 84 | 85 | describe('.getCustomEventNames()', () => { 86 | it('should return an array of custom event names', () => { 87 | const names = chart.getCustomEventNames(); 88 | expect(names).to.be.an('Array'); 89 | expect(names).to.deep.equal([]); 90 | }); 91 | it('should return the same value with the static function by default', () => { 92 | const chart2 = new CustomChart(); 93 | const names = chart2.getCustomEventNames(); 94 | expect(names).to.deep.equal(CustomChart.getCustomEventNames()); 95 | }); 96 | }); 97 | 98 | describe('.getInnerWidth()', () => { 99 | it('should return width of the chart excluding margin', () => { 100 | chart 101 | .width(100) 102 | .options({ 103 | margin: { left: 10, right: 10 }, 104 | }) 105 | .updateDimensionNow(); 106 | expect(chart.getInnerWidth()).to.equal(80); 107 | }); 108 | }); 109 | 110 | describe('.getInnerHeight()', () => { 111 | it('should return height of the chart excluding margin', () => { 112 | chart 113 | .height(100) 114 | .options({ 115 | margin: { top: 10, bottom: 20 }, 116 | }) 117 | .updateDimensionNow(); 118 | expect(chart.getInnerHeight()).to.equal(70); 119 | }); 120 | }); 121 | 122 | describe('.data(data)', () => { 123 | it('should return data when called without argument', () => { 124 | chart.data({ a: 1 }); 125 | expect(chart.data()).to.deep.equal({ a: 1 }); 126 | }); 127 | it('should set data when called with at least one argument', () => { 128 | chart.data('test'); 129 | expect(chart.data()).to.equal('test'); 130 | }); 131 | it('after setting, should dispatch "data" event', done => { 132 | chart.on('data.test', () => { done(); }); 133 | chart.data({ a: 1 }); 134 | }); 135 | }); 136 | 137 | describe('.options(options)', () => { 138 | it('should return options when called without argument', () => { 139 | chart.options({ a: 2 }); 140 | expect(chart.options()).to.include.key('a'); 141 | expect(chart.options().a).to.equal(2); 142 | }); 143 | it('should set options when called with at least one argument', () => { 144 | chart.options({ a: 1 }); 145 | expect(chart.options()).to.include.key('a'); 146 | expect(chart.options().a).to.equal(1); 147 | }); 148 | it('should not overwrite but extend existing options when setting', () => { 149 | chart.options({ a: 1 }); 150 | chart.options({ b: 2 }); 151 | expect(chart.options()).to.include.keys(['a', 'b']); 152 | expect(chart.options().a).to.equal(1); 153 | expect(chart.options().b).to.equal(2); 154 | }); 155 | it('should dispatch "resize" as necessary if margin was included in the options.', (done) => { 156 | chart.on('resize.test', () => { done(); }); 157 | chart.options({ 158 | margin: { top: 12 }, 159 | }); 160 | }); 161 | it('should dispatch "resize" as necessary if offset was included in the options.', (done) => { 162 | chart.on('resize.test', () => { done(); }); 163 | chart.options({ 164 | offset: [4, 4], 165 | }); 166 | }); 167 | it('should dispatch "resize" as necessary if pixelRatio was included in the options.', (done) => { 168 | chart.on('resize.test', () => { done(); }); 169 | chart.options({ 170 | pixelRatio: 3, 171 | }); 172 | }); 173 | it('after setting, should dispatch "options" event', done => { 174 | chart.on('options.test', () => { done(); }); 175 | chart.options({ a: 1 }); 176 | }); 177 | }); 178 | 179 | describe('.dimension(dimension)', () => { 180 | it('after setting, should dispatch "resize" event', done => { 181 | chart.on('resize.test', () => { done(); }); 182 | chart.dimension([150, 150]); 183 | }); 184 | }); 185 | 186 | describe('.margin(margin)', () => { 187 | it('should update innerWidth after setting margin', () => { 188 | chart 189 | .width(100) 190 | .margin({ left: 15, right: 15 }) 191 | .updateDimensionNow(); 192 | 193 | expect(chart.getInnerWidth()).to.equal(70); 194 | }); 195 | it('should update innerHeight after setting margin', () => { 196 | chart 197 | .height(100) 198 | .margin({ top: 10, bottom: 10 }) 199 | .updateDimensionNow(); 200 | 201 | expect(chart.getInnerHeight()).to.equal(80); 202 | }); 203 | it('after setting, should dispatch "resize" event', done => { 204 | chart.on('resize.test', () => { done(); }); 205 | chart.margin({ left: 33 }); 206 | }); 207 | }); 208 | 209 | describe('.offset(offset)', () => { 210 | it('after setting, should dispatch "resize" event', done => { 211 | chart.on('resize.test', () => { done(); }); 212 | chart.offset([3, 3]); 213 | }); 214 | }); 215 | 216 | ['width', 'height', 'pixelRatio'].forEach(field => { 217 | describe(`.${field}(${field})`, () => { 218 | it('after setting, should dispatch "resize" event', done => { 219 | chart.on('resize.test', () => { done(); }); 220 | chart[field](200); 221 | }); 222 | }); 223 | }); 224 | 225 | describe('.fit(fitOptions)', () => { 226 | it('should fit the chart to container as instructed', () => { 227 | $element 228 | .style('width', '500px') 229 | .style('height', '500px'); 230 | chart 231 | .fit({ 232 | width: '100%', 233 | height: '100%', 234 | }) 235 | .updateDimensionNow(); 236 | 237 | expect(chart.dimension()).to.deep.equal([500, 500]); 238 | 239 | chart 240 | .fit({ 241 | width: '50%', 242 | height: '80%', 243 | }) 244 | .updateDimensionNow(); 245 | 246 | expect(chart.dimension()).to.deep.equal([250, 400]); 247 | }); 248 | 249 | it('should be called repeatedly without problem', () => { 250 | chart.fit({ 251 | width: '100%', 252 | }, true); 253 | chart.fit({ 254 | width: '100%', 255 | }, true); 256 | }); 257 | 258 | it('when watch is true, should update dimension if the container size has changed.', (done)=>{ 259 | chart.fit({ 260 | width: '100%', 261 | }, true); 262 | chart.container.node().style.width = '118px'; 263 | setTimeout(() => { 264 | expect(chart.width()).to.equal(118); 265 | done(); 266 | }, 100); 267 | }); 268 | }); 269 | 270 | describe('.stopFitWatcher()', () => { 271 | it('should kill the fitWatcher, if exists', () => { 272 | chart.fit({ 273 | width: '100%', 274 | }, true); 275 | chart.stopFitWatcher(); 276 | expect(chart.fitWatcher).to.not.exist; 277 | }); 278 | it('should return this', () => { 279 | const returnValue = chart.stopFitWatcher(); 280 | expect(returnValue).to.equal(chart); 281 | }); 282 | }); 283 | 284 | describe('.hasData()', () => { 285 | it('should return true when data are not null nor undefined', () => { 286 | chart.data({}); 287 | expect(chart.hasData()).to.be.true; 288 | chart.data({ test: 1 }); 289 | expect(chart.hasData()).to.be.true; 290 | chart.data([]); 291 | expect(chart.hasData()).to.be.true; 292 | chart.data(['test']); 293 | expect(chart.hasData()).to.be.true; 294 | }); 295 | it('should return false when data are null or undefined', () => { 296 | chart.data(null); 297 | expect(chart.hasData()).to.be.false; 298 | chart.data(undefined); 299 | expect(chart.hasData()).to.be.false; 300 | }); 301 | }); 302 | 303 | describe('.hasNonZeroArea()', () => { 304 | it('should return true if innerWidth * innerHeight is more than zero', () => { 305 | chart 306 | .width(80) 307 | .height(50) 308 | .options({ 309 | margin: { top: 10, bottom: 20, left: 10, right: 10 }, 310 | }) 311 | .updateDimensionNow(); 312 | expect(chart.hasNonZeroArea()).to.be.true; 313 | }); 314 | it('should return false otherwise', () => { 315 | chart 316 | .width(20) 317 | .height(30) 318 | .options({ 319 | margin: { top: 10, bottom: 20, left: 10, right: 10 }, 320 | }) 321 | .updateDimensionNow(); 322 | expect(chart.hasNonZeroArea()).to.be.false; 323 | }); 324 | }); 325 | 326 | describe('.destroy()', () => { 327 | it('should unregister all event handlers', (done) => { 328 | const chart2 = new CustomChart(); 329 | chart2.on('custom1', () => { 330 | assert.fail('should not be called'); 331 | }); 332 | chart2.destroy(); 333 | chart2.dispatcher.call('custom1', chart); 334 | setTimeout(() => { 335 | done(); 336 | }, 20); 337 | }); 338 | it('should stop fitWatcher if there is any', () => { 339 | chart.destroy(); 340 | expect(chart.fitWatcher).to.not.exist; 341 | }); 342 | }); 343 | 344 | describe('.dispatchAs(eventName)', () => { 345 | it('should return a function which can be called to dispatch the named event with given arguments', (done) => { 346 | const chart2 = new CustomChart(); 347 | chart2.on('custom1', (a, b, c) => { 348 | expect(a).to.equal(1); 349 | expect(b).to.equal(2); 350 | expect(c).to.equal(3); 351 | done(); 352 | }); 353 | chart2.dispatchAs('custom1')(1, 2, 3); 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /src/charts/CanvasChart.js: -------------------------------------------------------------------------------- 1 | import AbstractChart from './AbstractChart.js'; 2 | import CanvasPlate from '../plates/CanvasPlate.js'; 3 | 4 | class CanvasChart extends AbstractChart { 5 | constructor(selector, ...options) { 6 | super(selector, ...options); 7 | 8 | this.addPlate('canvas', new CanvasPlate()); 9 | this.canvas = this.plates.canvas.getSelection(); 10 | this.updateDimensionNow(); 11 | } 12 | 13 | getContext2d() { 14 | return this.plates.canvas.getContext2d(); 15 | } 16 | 17 | clear() { 18 | this.plates.canvas.clear(); 19 | return this; 20 | } 21 | } 22 | 23 | export default CanvasChart; 24 | -------------------------------------------------------------------------------- /src/charts/CanvasChart.spec.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import CanvasChart from './CanvasChart.js'; 3 | import CanvasPlate from '../plates/CanvasPlate.js'; 4 | 5 | describe('CanvasChart', () => { 6 | let element, $element, $canvas, chart; 7 | 8 | beforeEach(() => { 9 | element = document.body.appendChild(document.createElement('div')); 10 | chart = new CanvasChart(element, null); 11 | }); 12 | 13 | describe('new CanvasChart(element, options)', () => { 14 | it('should create inside the element, which is accessible from chart.canvas', () => { 15 | expect(chart.canvas).to.exist; 16 | expect(chart.canvas.size()).to.be.equal(1); 17 | }); 18 | it('has a CanvasPlate accessible from this.plates.canvas', () => { 19 | expect(chart.plates.canvas).to.be.instanceof(CanvasPlate); 20 | }); 21 | }); 22 | 23 | describe('.getContext2d()', () => { 24 | it('should return context2d from canvas', () => { 25 | const ctx = chart.getContext2d(); 26 | expect(ctx).to.exist; 27 | expect(ctx).to.be.instanceof(CanvasRenderingContext2D); 28 | }); 29 | }); 30 | 31 | describe('.clear()', () => { 32 | it('should clear canvas and return this', () => { 33 | const returnValue = chart.clear(); 34 | expect(returnValue).to.equal(chart); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/charts/HybridChart.js: -------------------------------------------------------------------------------- 1 | import CanvasChart from './CanvasChart.js'; 2 | import SvgPlate from '../plates/SvgPlate.js'; 3 | 4 | class HybridChart extends CanvasChart { 5 | constructor(selector, ...options) { 6 | super(selector, ...options); 7 | 8 | this.addPlate('svg', new SvgPlate()); 9 | const plate = this.plates.svg; 10 | this.svg = plate.getSelection(); 11 | this.rootG = plate.rootG; 12 | this.layers = plate.layers; 13 | this.updateDimensionNow(); 14 | } 15 | } 16 | 17 | export default HybridChart; 18 | -------------------------------------------------------------------------------- /src/charts/HybridChart.spec.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import HybridChart from './HybridChart.js'; 3 | import CanvasPlate from '../plates/CanvasPlate.js'; 4 | import SvgPlate from '../plates/SvgPlate.js'; 5 | 6 | describe('HybridChart', () => { 7 | let element, $element, $canvas, chart; 8 | 9 | beforeEach(() => { 10 | element = document.body.appendChild(document.createElement('div')); 11 | chart = new HybridChart(element, null); 12 | }); 13 | 14 | describe('new HybridChart(element, options)', () => { 15 | it('has a CanvasPlate accessible from this.plates.canvas', () => { 16 | expect(chart.plates.canvas).to.be.instanceof(CanvasPlate); 17 | }); 18 | it('should create inside the container, accessible as chart.canvas', () => { 19 | expect(chart.canvas).to.exist; 20 | expect(chart.container.select('canvas').node()).to.equal(chart.canvas.node()); 21 | }); 22 | 23 | it('has an SvgPlate accessible from this.plates.svg', () => { 24 | expect(chart.plates.svg).to.be.instanceof(SvgPlate); 25 | }); 26 | it('should create inside the container, accessible from chart.svg', () => { 27 | expect(chart.svg).to.exist; 28 | expect(chart.container.select('svg').node()).to.equal(chart.svg.node()); 29 | }); 30 | it('should create inside the above, accessible as chart.rootG', () => { 31 | expect(chart.rootG).to.exist; 32 | expect(chart.svg.select('g').node()).to.equal(chart.rootG.node()); 33 | }); 34 | }); 35 | 36 | describe('.getContext2d()', () => { 37 | it('should return context2d from canvas', () => { 38 | const ctx = chart.getContext2d(); 39 | expect(ctx).to.exist; 40 | expect(ctx).to.be.instanceof(CanvasRenderingContext2D); 41 | }); 42 | }); 43 | 44 | describe('.clear()', () => { 45 | it('should clear canvas and return this', () => { 46 | const returnValue = chart.clear(); 47 | expect(returnValue).to.equal(chart); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/charts/SvgChart.js: -------------------------------------------------------------------------------- 1 | import AbstractChart from './AbstractChart.js'; 2 | import SvgPlate from '../plates/SvgPlate.js'; 3 | 4 | class SvgChart extends AbstractChart { 5 | constructor(selector, ...options) { 6 | super(selector, ...options); 7 | 8 | this.addPlate('svg', new SvgPlate()); 9 | const plate = this.plates.svg; 10 | this.svg = plate.getSelection(); 11 | this.rootG = plate.rootG; 12 | this.layers = plate.layers; 13 | this.updateDimensionNow(); 14 | } 15 | } 16 | 17 | export default SvgChart; 18 | -------------------------------------------------------------------------------- /src/charts/SvgChart.spec.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import SvgChart from './SvgChart.js'; 3 | import SvgPlate from '../plates/SvgPlate.js'; 4 | 5 | describe('SvgChart', () => { 6 | let element, $element, chart; 7 | 8 | beforeEach(() => { 9 | element = document.body.appendChild(document.createElement('div')); 10 | chart = new SvgChart(element, null); 11 | $element = select(element); 12 | }); 13 | 14 | describe('new SvgChart(element, options)', () => { 15 | it('has an SvgPlate accessible from this.plates.svg', () => { 16 | expect(chart.plates.svg).to.be.instanceof(SvgPlate); 17 | }); 18 | it('should create inside the container, accessible from chart.svg', () => { 19 | expect(chart.svg).to.exist; 20 | expect(chart.container.select('svg').node()).to.equal(chart.svg.node()); 21 | }); 22 | it('should create inside the above, accessible as chart.rootG', () => { 23 | expect(chart.rootG).to.exist; 24 | expect(chart.svg.select('g').node()).to.equal(chart.rootG.node()); 25 | }); 26 | }); 27 | 28 | describe('.width(width)', () => { 29 | it('should return width when called without argument', () => { 30 | const w = chart.svg.attr('width'); 31 | expect(chart.width()).to.equal(+w); 32 | }); 33 | it('should set width when called with Number as the first argument', () => { 34 | chart 35 | .width(300) 36 | .updateDimensionNow(); 37 | expect(+chart.svg.attr('width')).to.equal(300); 38 | }); 39 | }); 40 | 41 | describe('.height(height)', () => { 42 | it('should return height when called without argument', () => { 43 | const w = chart.svg.attr('height'); 44 | expect(chart.height()).to.equal(+w); 45 | }); 46 | it('should set height when called with Number as the first argument', () => { 47 | chart 48 | .height(300) 49 | .updateDimensionNow(); 50 | expect(+chart.svg.attr('height')).to.equal(300); 51 | }); 52 | it('should override line-height and set height correctly', () => { 53 | const element2 = document.body.appendChild(document.createElement('div')); 54 | element2.style.lineHeight = 1; 55 | const chart2 = new SvgChart(element2); 56 | chart2 57 | .height(300) 58 | .updateDimensionNow(); 59 | expect(+chart2.svg.attr('height')).to.equal(300); 60 | expect(+chart2.container.node().clientHeight).to.equal(300); 61 | expect(+element2.style.lineHeight).to.equal(0); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | import isFunction from 'lodash-es/isFunction.js'; 2 | import isObject from 'lodash-es/isObject.js'; 3 | 4 | export { isObject, isFunction }; 5 | export { default as debounce } from 'lodash-es/debounce.js'; 6 | export { default as throttle } from 'lodash-es/throttle.js'; 7 | 8 | //--------------------------------------------------- 9 | // From underscore.string 10 | //--------------------------------------------------- 11 | /* jshint ignore:start */ 12 | 13 | const nativeTrim = String.prototype.trim; 14 | 15 | function escapeRegExp(str) { 16 | if (str == null) return ''; 17 | return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); 18 | } 19 | 20 | function defaultToWhiteSpace(characters) { 21 | if (characters == null) { 22 | return '\\s'; 23 | } else if (characters.source) { 24 | return characters.source; 25 | } 26 | return `[${escapeRegExp(characters)}]`; 27 | } 28 | 29 | function trim(str, characters) { 30 | if (str == null) return ''; 31 | if (!characters && nativeTrim) return nativeTrim.call(str); 32 | const chars = defaultToWhiteSpace(characters); 33 | const pattern = new RegExp(`\^${chars}+|${chars}+$`, 'g'); 34 | return String(str).replace(pattern, ''); 35 | } 36 | 37 | export function kebabCase(str) { 38 | return trim(str) 39 | .replace(/([A-Z])/g, '-$1') 40 | .replace(/[-_\s]+/g, '-') 41 | .toLowerCase(); 42 | } 43 | 44 | //--------------------------------------------------- 45 | // From http://youmightnotneedjquery.com/ 46 | //--------------------------------------------------- 47 | 48 | export function deepExtend(out) { 49 | out = out || {}; 50 | 51 | for (let i = 1; i < arguments.length; i++) { 52 | const obj = arguments[i]; 53 | 54 | if (!obj) 55 | continue; 56 | 57 | for (const key in obj) { 58 | if (obj.hasOwnProperty(key)) { 59 | const value = obj[key]; 60 | if (isObject(value) && !Array.isArray(value) && !isFunction(value)) { 61 | out[key] = deepExtend(out[key], value); 62 | } 63 | else 64 | out[key] = value; 65 | } 66 | } 67 | } 68 | 69 | return out; 70 | } 71 | 72 | export function extend(out) { 73 | out = out || {}; 74 | 75 | for (let i = 1; i < arguments.length; i++) { 76 | if (!arguments[i]) 77 | continue; 78 | 79 | for (const key in arguments[i]) { 80 | if (arguments[i].hasOwnProperty(key)) 81 | out[key] = arguments[i][key]; 82 | } 83 | } 84 | 85 | return out; 86 | } 87 | 88 | //--------------------------------------------------- 89 | // From D3 v3 90 | //--------------------------------------------------- 91 | 92 | // Method is assumed to be a standard D3 getter-setter: 93 | // If passed with no arguments, gets the value. 94 | // If passed with arguments, sets the value and returns the target. 95 | function d3Rebind(target, source, method) { 96 | return function () { 97 | const value = method.apply(source, arguments); 98 | return value === source ? target : value; 99 | }; 100 | } 101 | 102 | // Copies a variable number of methods from source to target. 103 | export function rebind(target, source) { 104 | let i = 1, n = arguments.length, method; 105 | while (++i < n) target[method = arguments[i]] = d3Rebind(target, source, source[method]); 106 | return target; 107 | } 108 | 109 | export function functor(v) { 110 | return isFunction(v) ? v : () => v; 111 | } 112 | -------------------------------------------------------------------------------- /src/helper.spec.js: -------------------------------------------------------------------------------- 1 | import * as helper from './helper.js'; 2 | 3 | describe('helper', function () { 4 | describe('.deepExtend(target, src1, src2, ...)', function () { 5 | it('should copy fields from sources into target', function () { 6 | expect(helper.deepExtend({}, { 7 | a: 1, 8 | b: 2, 9 | }, { 10 | b: 3, 11 | c: 4, 12 | })).to.deep.equal({ 13 | a: 1, 14 | b: 3, 15 | c: 4, 16 | }); 17 | 18 | expect(helper.deepExtend({}, { 19 | a: 1, 20 | b: 2, 21 | }, { 22 | b: 3, 23 | c: 4, 24 | }, null)).to.deep.equal({ 25 | a: 1, 26 | b: 3, 27 | c: 4, 28 | }); 29 | }); 30 | 31 | it('should copy arrays and functions correctly from sources into target', function () { 32 | const fn1 = function (d) { return d + 1; }; 33 | const fn2 = function (d) { return d + 2; }; 34 | expect(helper.deepExtend({}, { 35 | a: fn1, 36 | b: [1, 2], 37 | }, { 38 | b: [3, 4], 39 | c: fn2, 40 | })).to.deep.equal({ 41 | a: fn1, 42 | b: [3, 4], 43 | c: fn2, 44 | }); 45 | }); 46 | 47 | it('should perform "deep" copy', function () { 48 | const fn1 = function (d) { return d + 1; }; 49 | const fn2 = function (d) { return d + 2; }; 50 | expect(helper.deepExtend({}, { 51 | a: { d: fn1 }, 52 | b: [1, 2], 53 | c: { f: 3 }, 54 | h: { i: [1, 2, 3], j: [3, 4, 5] }, 55 | }, { 56 | a: { e: 2 }, 57 | b: [3, 4], 58 | c: { f: 4, g: fn2 }, 59 | h: { i: [2, 3, 4], k: [3, 4, 5], l: { m: 2 } }, 60 | })).to.deep.equal({ 61 | a: { d: fn1, e: 2 }, 62 | b: [3, 4], 63 | c: { f: 4, g: fn2 }, 64 | h: { i: [2, 3, 4], j: [3, 4, 5], k: [3, 4, 5], l: { m: 2 } }, 65 | }); 66 | }); 67 | }); 68 | 69 | describe('.extend(target, src1, src2, ...)', function () { 70 | it('should copy fields from sources into target', function () { 71 | expect(helper.extend({}, { 72 | a: 1, 73 | b: 2, 74 | }, { 75 | b: 3, 76 | c: 4, 77 | })).to.deep.equal({ 78 | a: 1, 79 | b: 3, 80 | c: 4, 81 | }); 82 | 83 | expect(helper.extend({}, { 84 | a: 1, 85 | b: 2, 86 | }, { 87 | b: 3, 88 | c: 4, 89 | }, null)).to.deep.equal({ 90 | a: 1, 91 | b: 3, 92 | c: 4, 93 | }); 94 | }); 95 | 96 | it('should copy arrays and functions correctly from sources into target', function () { 97 | const fn1 = function (d) { return d + 1; }; 98 | const fn2 = function (d) { return d + 2; }; 99 | expect(helper.extend({}, { 100 | a: fn1, 101 | b: [1, 2], 102 | }, { 103 | b: [3, 4], 104 | c: fn2, 105 | })).to.deep.equal({ 106 | a: fn1, 107 | b: [3, 4], 108 | c: fn2, 109 | }); 110 | }); 111 | 112 | it('should NOT perform "deep" copy', function () { 113 | const fn1 = function (d) { return d + 1; }; 114 | const fn2 = function (d) { return d + 2; }; 115 | expect(helper.extend({}, { 116 | a: { d: fn1 }, 117 | b: [1, 2], 118 | c: { f: 3 }, 119 | h: { i: [1, 2, 3], j: [3, 4, 5] }, 120 | }, { 121 | a: { e: 2 }, 122 | b: [3, 4], 123 | c: { f: 4, g: fn2 }, 124 | h: { i: [2, 3, 4], k: [3, 4, 5], l: { m: 2 } }, 125 | })).to.deep.equal({ 126 | a: { e: 2 }, 127 | b: [3, 4], 128 | c: { f: 4, g: fn2 }, 129 | h: { i: [2, 3, 4], k: [3, 4, 5], l: { m: 2 } }, 130 | }); 131 | }); 132 | }); 133 | 134 | describe('.isFunction(function)', function () { 135 | it('should return true if the value is a function', function () { 136 | const fn1 = function (d) { return d + 1; }; 137 | function fn2(d) { return d + 2; } 138 | 139 | expect(helper.isFunction(fn1)).to.be.true; 140 | expect(helper.isFunction(fn2)).to.be.true; 141 | }); 142 | it('should return false if the value is not a function', function () { 143 | expect(helper.isFunction(0)).to.be.false; 144 | expect(helper.isFunction(1)).to.be.false; 145 | expect(helper.isFunction(true)).to.be.false; 146 | expect(helper.isFunction('what')).to.be.false; 147 | expect(helper.isFunction(null)).to.be.false; 148 | expect(helper.isFunction(undefined)).to.be.false; 149 | }); 150 | }); 151 | 152 | describe('.kebabCase(str)', function () { 153 | it('should convert input to dash-case', function () { 154 | expect(helper.kebabCase('camelCase')).to.equal('camel-case'); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /src/layerOrganizer.js: -------------------------------------------------------------------------------- 1 | import { isObject, kebabCase } from './helper.js'; 2 | 3 | // EXAMPLE USAGE: 4 | // 5 | // var layers = new d3LayerOrganizer(vis); 6 | // layers.create([ 7 | // {'axis': ['bar', 'mark']}, 8 | // 'glass', 9 | // 'label' 10 | // ]); 11 | // 12 | // Then access the layers via 13 | // layers.get('axis'), 14 | // layers.get('axis/bar'), 15 | // layers.get('axis/mark'), 16 | // layers.get('glass'), 17 | // layers.get('label') 18 | 19 | export default function (mainContainer, defaultTag = 'g') { 20 | const layers = {}; 21 | 22 | function createLayerFromName(container, layerName, prefix = '') { 23 | const chunks = layerName.split('.'); 24 | let name; 25 | let tag; 26 | if (chunks.length > 1) { 27 | tag = chunks[0].length > 0 ? chunks[0] : defaultTag; 28 | name = chunks[1]; 29 | } else { 30 | tag = defaultTag; 31 | name = chunks[0]; 32 | } 33 | 34 | const id = `${prefix}${name}`; 35 | if (layers.hasOwnProperty(id)) { 36 | throw new Error(`invalid or duplicate layer id: ${id}`); 37 | } 38 | const className = `${kebabCase(name)}-layer`; 39 | const layer = container.append(tag) 40 | .classed(className, true); 41 | 42 | layers[id] = layer; 43 | return layer; 44 | } 45 | 46 | function createLayerFromConfig(container, config, prefix = '') { 47 | if (Array.isArray(config)) { 48 | return config 49 | .map(info => createLayerFromConfig(container, info, prefix)); 50 | } else if (isObject(config)) { 51 | const [parentKey] = Object.keys(config); 52 | const parentLayer = createLayerFromName(container, parentKey, prefix); 53 | createLayerFromConfig(parentLayer, config[parentKey], `${prefix}${parentKey}/`); 54 | return parentLayer; 55 | } 56 | 57 | return createLayerFromName(container, config, prefix); 58 | } 59 | 60 | function createLayer(config) { 61 | return createLayerFromConfig(mainContainer, config); 62 | } 63 | 64 | function create(layerNames) { 65 | return Array.isArray(layerNames) 66 | ? layerNames.map(createLayer) 67 | : createLayer(layerNames); 68 | } 69 | 70 | function get(layerName) { 71 | return layers[layerName]; 72 | } 73 | 74 | function has(layerName) { 75 | return !!layers[layerName]; 76 | } 77 | 78 | return { 79 | create, 80 | get, 81 | has, 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/layerOrganizer.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint expr: true */ 2 | 3 | import { select } from 'd3-selection'; 4 | import LayerOrganizer from './layerOrganizer.js'; 5 | 6 | describe('LayerOrganizer', () => { 7 | let container, layers; 8 | before(done => { 9 | container = select('body').append('svg').append('g'); 10 | layers = new LayerOrganizer(container); 11 | done(); 12 | }); 13 | 14 | describe('new LayerOrganizer(container)', () => { 15 | it('should create an organizer', () => { 16 | const l = new LayerOrganizer(container); 17 | expect(l).to.exist; 18 | }); 19 | }); 20 | 21 | describe('.create(config)', () => { 22 | it('should create single layer as given a String "name"', () => { 23 | layers.create('single'); 24 | expect(container.select('g.single-layer').size()).to.be.equal(1); 25 | }); 26 | 27 | it('should create single layer as given a String "tag.name"', () => { 28 | layers.create('div.customTag'); 29 | expect(container.select('div.custom-tag-layer').size()).to.be.equal(1); 30 | }); 31 | 32 | it('should create multiple layers given an array', () => { 33 | layers.create(['a', 'div.b', 'span.c']); 34 | expect(container.select('g.a-layer').size()).to.be.equal(1); 35 | expect(container.select('div.b-layer').size()).to.be.equal(1); 36 | expect(container.select('span.c-layer').size()).to.be.equal(1); 37 | }); 38 | 39 | it('should create nested layers given a plain Object with a String inside', () => { 40 | layers.create({ 'g.d': 'div.e' }); 41 | expect(container.select('g.d-layer').size()).to.be.equal(1); 42 | expect(container.select('g.d-layer div.e-layer').size()).to.be.equal(1); 43 | }); 44 | 45 | it('should create nested layers given a plain Object with an Array inside', () => { 46 | layers.create({ f: ['g', 'h'] }); 47 | expect(container.select('g.f-layer').size()).to.be.equal(1); 48 | expect(container.select('g.f-layer g.g-layer').size()).to.be.equal(1); 49 | expect(container.select('g.f-layer g.h-layer').size()).to.be.equal(1); 50 | }); 51 | 52 | it('should create multiple nested layers given an array of objects', () => { 53 | layers.create([{ 'i': ['x'] }, { 'j': 'x' }, { 'k': ['x', 'y'] }]); 54 | expect(container.select('g.i-layer').size()).to.be.equal(1); 55 | expect(container.select('g.j-layer').size()).to.be.equal(1); 56 | expect(container.select('g.k-layer').size()).to.be.equal(1); 57 | expect(container.select('g.i-layer g.x-layer').size()).to.be.equal(1); 58 | expect(container.select('g.i-layer g.x-layer').size()).to.be.equal(1); 59 | expect(container.select('g.k-layer g.x-layer').size()).to.be.equal(1); 60 | expect(container.select('g.k-layer g.y-layer').size()).to.be.equal(1); 61 | }); 62 | 63 | it('should create multi-level nested layers given a nested plain Object', () => { 64 | layers.create({ 65 | l: [ 66 | 'm', 67 | { 'n': [ 68 | { 'o': ['p'] }, 'q', 69 | ] }, 70 | ], 71 | }); 72 | expect(container.select('g.l-layer').size()).to.be.equal(1); 73 | expect(container.select('g.l-layer g.m-layer').size()).to.be.equal(1); 74 | expect(container.select('g.l-layer g.n-layer').size()).to.be.equal(1); 75 | expect(container.select('g.l-layer g.n-layer g.o-layer').size()).to.be.equal(1); 76 | expect(container.select('g.l-layer g.n-layer g.o-layer g.p-layer').size()).to.be.equal(1); 77 | expect(container.select('g.l-layer g.n-layer g.q-layer').size()).to.be.equal(1); 78 | }); 79 | }); 80 | 81 | describe('.has(name)', () => { 82 | it('should be able to check first-level layer', () => { 83 | expect(layers.has('single')).to.be.true; 84 | expect(layers.has('test')).to.be.false; 85 | }); 86 | it('should be able to check second-level layer', () => { 87 | expect(layers.has('l/m')).to.be.true; 88 | expect(layers.has('l/x')).to.be.false; 89 | }); 90 | it('should be able to check third-level layer', () => { 91 | expect(layers.has('l/n/q')).to.be.true; 92 | expect(layers.has('l/n/x')).to.be.false; 93 | }); 94 | }); 95 | 96 | describe('.get(name)', () => { 97 | it('should be able to get first-level layer', () => { 98 | expect(layers.get('single')).to.exist; 99 | expect(layers.get('test')).to.be.not.exist; 100 | }); 101 | it('should be able to get second-level layer', () => { 102 | expect(layers.get('l/m')).to.exist; 103 | expect(layers.get('l/x')).to.not.exist; 104 | }); 105 | it('should be able to get third-level layer', () => { 106 | expect(layers.get('l/n/o')).to.exist; 107 | expect(layers.get('l/n/x')).to.not.exist; 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as helper from './helper.js'; 2 | export { helper }; 3 | 4 | export { default as AbstractChart } from './charts/AbstractChart.js'; 5 | export { default as CanvasChart } from './charts/CanvasChart.js'; 6 | export { default as HybridChart } from './charts/HybridChart.js'; 7 | export { default as SvgChart } from './charts/SvgChart.js'; 8 | 9 | export { default as AbstractPlate } from './plates/AbstractPlate.js'; 10 | export { default as CanvasPlate } from './plates/CanvasPlate.js'; 11 | export { default as DivPlate } from './plates/DivPlate.js'; 12 | export { default as SvgPlate } from './plates/SvgPlate.js'; 13 | 14 | export { default as LayerOrganizer } from './layerOrganizer.js'; 15 | // export { default as Chartlet } from './chartlet.js'; 16 | -------------------------------------------------------------------------------- /src/main.spec.js: -------------------------------------------------------------------------------- 1 | import * as d3Kit from './main.js'; 2 | 3 | describe('d3Kit', () => { 4 | it('should exist', () => { 5 | expect(d3Kit).to.exist; 6 | }); 7 | 8 | [ 9 | 'AbstractChart', 10 | 'CanvasChart', 11 | 'HybridChart', 12 | 'SvgChart', 13 | 'AbstractPlate', 14 | 'CanvasPlate', 15 | 'DivPlate', 16 | 'SvgPlate', 17 | 'helper', 18 | 'LayerOrganizer', 19 | ].forEach(module => { 20 | it(`should include module ${module}`, () => { 21 | expect(d3Kit[module]).to.exist; 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/plates/AbstractPlate.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import Base from '../Base.js'; 3 | 4 | class AbstractPlate extends Base { 5 | constructor(node, ...options) { 6 | super(...options); 7 | this.node = node; 8 | this.selection = select(this.node); 9 | } 10 | 11 | getNode() { 12 | return this.node; 13 | } 14 | 15 | getSelection() { 16 | return this.selection; 17 | } 18 | } 19 | 20 | export default AbstractPlate; 21 | -------------------------------------------------------------------------------- /src/plates/AbstractPlate.spec.js: -------------------------------------------------------------------------------- 1 | import AbstractPlate from './AbstractPlate.js'; 2 | 3 | describe('AbstractPlate', () => { 4 | let node, plate; 5 | 6 | it('should exist', () => { 7 | expect(AbstractPlate).to.exist; 8 | }); 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div'); 12 | plate = new AbstractPlate(node, { 13 | initialWidth: 5, 14 | initialHeight: 5, 15 | }); 16 | }); 17 | 18 | describe('new AbstractPlate(node, options)', () => { 19 | it('should construct an object with the given parameters', () => { 20 | expect(plate).to.exist; 21 | expect(plate.width()).to.equal(5); 22 | expect(plate.height()).to.equal(5); 23 | }); 24 | }); 25 | 26 | describe('.getNode()', () => { 27 | it('should return the node passed to constructor', () => { 28 | expect(plate.getNode()).to.equal(node); 29 | }); 30 | }); 31 | 32 | describe('.getSelection()', () => { 33 | it('should return selection of the node passed to constructor', () => { 34 | expect(plate.getSelection().node()).to.equal(node); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/plates/CanvasPlate.js: -------------------------------------------------------------------------------- 1 | import AbstractPlate from './AbstractPlate.js'; 2 | 3 | class CanvasPlate extends AbstractPlate { 4 | constructor(...options) { 5 | super(document.createElement('canvas'), ...options); 6 | } 7 | 8 | getContext2d() { 9 | const width = this.width(); 10 | const height = this.height(); 11 | const pixelRatio = this.pixelRatio(); 12 | const { top, left } = this.margin(); 13 | const [x, y] = this.offset(); 14 | 15 | const ctx = this.node.getContext('2d'); 16 | ctx.setTransform(1, 0, 0, 1, 0, 0); 17 | ctx.scale(pixelRatio, pixelRatio); 18 | ctx.translate(left + x, top + y); 19 | return ctx; 20 | } 21 | 22 | clear() { 23 | const width = this.width(); 24 | const height = this.height(); 25 | const pixelRatio = this.pixelRatio(); 26 | 27 | const ctx = this.node.getContext('2d'); 28 | ctx.setTransform(1, 0, 0, 1, 0, 0); 29 | ctx.scale(pixelRatio, pixelRatio); 30 | ctx.clearRect(0, 0, width, height); 31 | return this; 32 | } 33 | 34 | _updateDimension() { 35 | const width = this.width(); 36 | const height = this.height(); 37 | const pixelRatio = this.pixelRatio(); 38 | 39 | this.node.setAttribute('width', width * pixelRatio); 40 | this.node.setAttribute('height', height * pixelRatio); 41 | this.node.style.width = `${width}px`; 42 | this.node.style.height = `${height}px`; 43 | 44 | return this; 45 | } 46 | } 47 | 48 | export default CanvasPlate; 49 | -------------------------------------------------------------------------------- /src/plates/CanvasPlate.spec.js: -------------------------------------------------------------------------------- 1 | import CanvasPlate from './CanvasPlate.js'; 2 | import LayerOrganizer from '../layerOrganizer.js'; 3 | 4 | describe('CanvasPlate', () => { 5 | let plate; 6 | 7 | it('should exist', () => { 8 | expect(CanvasPlate).to.exist; 9 | }); 10 | 11 | beforeEach(() => { 12 | plate = new CanvasPlate({ 13 | initialWidth: 100, 14 | initialHeight: 100, 15 | margin: { 16 | top: 10, 17 | right: 10, 18 | bottom: 10, 19 | left: 10, 20 | }, 21 | offset: [1, 1], 22 | pixelRatio: 2, 23 | }); 24 | }); 25 | 26 | describe('constructor(params)', () => { 27 | it('should construct a plate that contains ', () => { 28 | expect(plate).to.exist; 29 | expect(plate.getNode().tagName.toLowerCase()).to.equal('canvas'); 30 | }); 31 | }); 32 | 33 | describe('.getContext2d()', () => { 34 | it('should return context', () => { 35 | const ctx = plate.getContext2d(); 36 | expect(ctx).to.exist; 37 | }); 38 | 39 | it('should adjust scale and translation', () => { 40 | const ctx = plate.getContext2d(); 41 | ctx.fillStyle = 'rgb(44,44,44)'; 42 | ctx.fillRect(0, 0, 10, 10); 43 | const outside = ctx.getImageData(21, 21, 1, 1).data; 44 | expect(outside[0]).to.equal(0); 45 | expect(outside[1]).to.equal(0); 46 | expect(outside[2]).to.equal(0); 47 | const inside = ctx.getImageData(22, 22, 1, 1).data; 48 | expect(inside[0]).to.equal(44); 49 | expect(inside[1]).to.equal(44); 50 | expect(inside[2]).to.equal(44); 51 | }); 52 | }); 53 | 54 | describe('.clear()', () => { 55 | it('should clear canvas', () => { 56 | // fill first 57 | const ctx = plate.getContext2d(); 58 | ctx.fillStyle = 'rgb(44,44,44)'; 59 | ctx.fillRect(0, 0, 10, 10); 60 | const before = ctx.getImageData(22, 22, 1, 1).data; 61 | expect(before[0]).to.equal(44); 62 | expect(before[1]).to.equal(44); 63 | expect(before[2]).to.equal(44); 64 | 65 | // then clear 66 | plate.clear(); 67 | const after = ctx.getImageData(22, 22, 1, 1).data; 68 | expect(after[0]).to.equal(0); 69 | expect(after[1]).to.equal(0); 70 | expect(after[2]).to.equal(0); 71 | }); 72 | }); 73 | 74 | describe('.updateDimensionNow()', () => { 75 | it('should update dimension', () => { 76 | plate.updateDimensionNow(); 77 | const canvas = plate.getSelection(); 78 | expect(+canvas.attr('width')).to.equal(200); 79 | expect(+canvas.attr('height')).to.equal(200); 80 | expect(canvas.node().style.width).to.equal('100px'); 81 | expect(canvas.node().style.height).to.equal('100px'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/plates/DivPlate.js: -------------------------------------------------------------------------------- 1 | import AbstractPlate from './AbstractPlate.js'; 2 | 3 | class DivPlate extends AbstractPlate { 4 | constructor(...options) { 5 | super(document.createElement('div'), ...options); 6 | } 7 | 8 | _updateDimension() { 9 | const width = this.width(); 10 | const height = this.height(); 11 | const margin = this.margin(); 12 | 13 | this.node.style.width = `${width - margin.left - margin.right}px`; 14 | this.node.style.height = `${height - margin.top - margin.bottom}px`; 15 | this.node.style.marginLeft = `${margin.left}px`; 16 | this.node.style.marginRight = `${margin.right}px`; 17 | this.node.style.marginTop = `${margin.top}px`; 18 | this.node.style.marginBottom = `${margin.bottom}px`; 19 | 20 | return this; 21 | } 22 | } 23 | 24 | export default DivPlate; 25 | -------------------------------------------------------------------------------- /src/plates/DivPlate.spec.js: -------------------------------------------------------------------------------- 1 | import DivPlate from './DivPlate.js'; 2 | 3 | describe('DivPlate', () => { 4 | let plate; 5 | 6 | it('should exist', () => { 7 | expect(DivPlate).to.exist; 8 | }); 9 | 10 | beforeEach(() => { 11 | plate = new DivPlate({ 12 | initialWidth: 100, 13 | initialHeight: 100, 14 | margin: { 15 | top: 10, 16 | right: 10, 17 | bottom: 10, 18 | left: 10, 19 | }, 20 | offset: [1, 1], 21 | pixelRatio: 10, 22 | }); 23 | }); 24 | 25 | describe('constructor(params)', () => { 26 | it('should construct a plate that contains ', () => { 27 | expect(plate).to.exist; 28 | expect(plate.getNode().tagName.toLowerCase()).to.equal('div'); 29 | }); 30 | }); 31 | 32 | describe('.updateDimensionNow()', () => { 33 | it('should update
dimension', () => { 34 | plate.updateDimensionNow(); 35 | const div = plate.getNode(); 36 | expect(div.style.width).to.equal('80px'); 37 | expect(div.style.height).to.equal('80px'); 38 | expect(div.style.marginLeft).to.equal('10px'); 39 | expect(div.style.marginRight).to.equal('10px'); 40 | expect(div.style.marginTop).to.equal('10px'); 41 | expect(div.style.marginBottom).to.equal('10px'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/plates/SvgPlate.js: -------------------------------------------------------------------------------- 1 | import AbstractPlate from './AbstractPlate.js'; 2 | import LayerOrganizer from '../layerOrganizer.js'; 3 | 4 | class SvgPlate extends AbstractPlate { 5 | constructor(...options) { 6 | super(document.createElementNS('http://www.w3.org/2000/svg', 'svg'), ...options); 7 | this.rootG = this.selection.append('g'); 8 | this.layers = new LayerOrganizer(this.rootG); 9 | } 10 | 11 | _updateDimension() { 12 | const width = this.width(); 13 | const height = this.height(); 14 | const { top, left } = this.margin(); 15 | const [x, y] = this.offset(); 16 | 17 | this.selection 18 | .attr('width', width) 19 | .attr('height', height); 20 | 21 | this.rootG.attr( 22 | 'transform', 23 | `translate(${left + x},${top + y})` 24 | ); 25 | 26 | return this; 27 | } 28 | } 29 | 30 | export default SvgPlate; 31 | -------------------------------------------------------------------------------- /src/plates/SvgPlate.spec.js: -------------------------------------------------------------------------------- 1 | import SvgPlate from './SvgPlate.js'; 2 | import LayerOrganizer from '../layerOrganizer.js'; 3 | 4 | describe('SvgPlate', () => { 5 | let plate; 6 | 7 | it('should exist', () => { 8 | expect(SvgPlate).to.exist; 9 | }); 10 | 11 | beforeEach(() => { 12 | plate = new SvgPlate({ 13 | initialWidth: 100, 14 | initialHeight: 100, 15 | margin: { 16 | top: 10, 17 | right: 10, 18 | bottom: 10, 19 | left: 10, 20 | }, 21 | offset: [1, 1], 22 | pixelRatio: 10, 23 | }); 24 | }); 25 | 26 | describe('constructor(params)', () => { 27 | it('should construct a plate that contains ', () => { 28 | expect(plate).to.exist; 29 | expect(plate.getNode().tagName.toLowerCase()).to.equal('svg'); 30 | }); 31 | it('should create root inside ', () => { 32 | expect(plate.getSelection().select('g').size()).to.equal(1); 33 | expect(plate.rootG.node().tagName).to.equal('g'); 34 | }); 35 | it('should create layer organizer', () => { 36 | expect(plate.layers).to.exist; 37 | }); 38 | }); 39 | 40 | describe('.updateDimensionNow()', () => { 41 | it('should update dimension', () => { 42 | plate.updateDimensionNow(); 43 | const svg = plate.getSelection(); 44 | expect(+svg.attr('width')).to.equal(100); 45 | expect(+svg.attr('height')).to.equal(100); 46 | expect(plate.rootG.attr('transform')).to.equal('translate(11,11)'); 47 | }); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------