├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── src ├── main.js ├── App.vue ├── components │ └── index.vue └── v-charts │ └── index.js ├── LICENSE ├── README.md └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | d3vue 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import * as d3 from 'd3' 6 | import charts from './v-charts' 7 | 8 | Vue.config.productionTip = false 9 | Vue.use(charts); 10 | 11 | Object.defineProperty(Vue.prototype, '$d3', {value: d3}); 12 | 13 | /* eslint-disable no-new */ 14 | new Vue({ 15 | el: '#app', 16 | components: { App }, 17 | template: '' 18 | }) 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brian Greig 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **If you are currently using d3vue we are halting updates on this project. We have created a new plugin that more deeply integrates with Vue using a standalone component. Check out our Github page for [v-chart-plugin](https://github.com/ignoreintuition/v-chart-plugin)** 2 | 3 | # D3 + Vue: a D3 Plugin for VueJS 4 | ![logo](https://user-images.githubusercontent.com/5210420/38968715-af63c158-435a-11e8-9d8f-c0960975e3a8.png) 5 | 6 | > D3 integration with Vue.js 7 | 8 | ![d3vue](https://user-images.githubusercontent.com/5210420/39558150-42b743ee-4e5a-11e8-95ad-155d2b5378f2.png) 9 | 10 | d3vue is a plugin for VueJS 2 that allows you to take data from your Vue instance and bind that data to a D3 v4 data visualization. d3vue uses the v4 merge syntax so when you can call the same function in your lifecycle events (i.e. mounted, beforeUpdate). The function signature is: 11 | 12 | ``` 13 | this.$helpers.chart.barChart(this.$d3, this.dataSet, this.options); 14 | ``` 15 | - this.$d3 is a reference to the d3 instances 16 | - this.dataSet is an array of objects from your instances 17 | - this.options includes 18 | - options.selector: selector name to place the graph. 19 | - options.metric: value you are measuring. 20 | - options.dim: value you will be categorizing the data by. 21 | - options.width: width of the chart. 22 | - options.height: height of the chart. 23 | - options.title: chart title 24 | 25 | Other functions are: 26 | ``` 27 | this.$helpers.chart.pieChart(...) 28 | this.$helpers.chart.lineGraph(...) 29 | this.$helpers.chart.scatterPlot(...) 30 | 31 | ``` 32 | 33 | ## Build Setup 34 | 35 | ``` bash 36 | # install dependencies 37 | npm install 38 | 39 | # serve with hot reload at localhost:8080 40 | npm run dev 41 | 42 | # build for production with minification 43 | npm run build 44 | 45 | # build for production and view the bundle analyzer report 46 | npm run build --report 47 | ``` 48 | 49 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3vue", 3 | "version": "1.0.0", 4 | "description": "D3 integration with Vue.js", 5 | "author": "Brian Greig ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 10 | "start": "npm run dev", 11 | "build": "node build/build.js" 12 | }, 13 | "dependencies": { 14 | "atob": "^2.1.2", 15 | "d3": "^5.0.0", 16 | "macaddress": "^0.2.9", 17 | "node-macaddress": "^0.2.4", 18 | "randomatic": "^3.1.0", 19 | "url-parse": "^1.4.3", 20 | "vue": "^2.5.2" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.2", 24 | "babel-core": "^6.22.1", 25 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 26 | "babel-loader": "^7.1.1", 27 | "babel-plugin-syntax-jsx": "^6.18.0", 28 | "babel-plugin-transform-runtime": "^6.22.0", 29 | "babel-plugin-transform-vue-jsx": "^3.5.0", 30 | "babel-preset-env": "^1.3.2", 31 | "babel-preset-stage-2": "^6.22.0", 32 | "chalk": "^2.0.1", 33 | "copy-webpack-plugin": "^4.0.1", 34 | "css-loader": "^0.28.0", 35 | "extract-text-webpack-plugin": "^3.0.0", 36 | "file-loader": "^1.1.4", 37 | "friendly-errors-webpack-plugin": "^1.6.1", 38 | "html-webpack-plugin": "^2.30.1", 39 | "node-notifier": "^5.1.2", 40 | "optimize-css-assets-webpack-plugin": "^3.2.0", 41 | "ora": "^1.2.0", 42 | "portfinder": "^1.0.13", 43 | "postcss-import": "^11.0.0", 44 | "postcss-loader": "^2.0.8", 45 | "postcss-url": "^7.2.1", 46 | "rimraf": "^2.6.0", 47 | "semver": "^5.3.0", 48 | "shelljs": "^0.7.6", 49 | "uglifyjs-webpack-plugin": "^1.1.1", 50 | "url-loader": "^0.5.8", 51 | "vue-loader": "^13.3.0", 52 | "vue-style-loader": "^3.0.1", 53 | "vue-template-compiler": "^2.5.2", 54 | "webpack": "^3.6.0", 55 | "webpack-bundle-analyzer": "^3.3.2", 56 | "webpack-dev-server": "^3.1.11", 57 | "webpack-merge": "^4.1.0" 58 | }, 59 | "engines": { 60 | "node": ">= 6.0.0", 61 | "npm": ">= 3.0.0" 62 | }, 63 | "browserslist": [ 64 | "> 1%", 65 | "last 2 versions", 66 | "not ie <= 8" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/d3-vue-example/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 83 | 84 | 85 | 111 | -------------------------------------------------------------------------------- /src/v-charts/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install: function(Vue) { 3 | Vue.prototype.$helpers = { 4 | chart: { 5 | d3: {}, 6 | ds: {}, 7 | /** 8 | * $helpers.chart.barChart 9 | * bind data to a bar graph. 10 | * @param {string} d3 - reference to d3 object. 11 | * @param {string} ds - dataset for the graph. 12 | * @param {Object} options - options for bar graph. 13 | * @param {string} options.selector - selector name to place the graph. 14 | * @param {string} options.metric - value you are measuring. 15 | * @param {string} options.dim - value you will be categorizing the data by. 16 | * @param {string} options.width - width of the chart. 17 | * @param {string} options.height - height of the chart. 18 | * @param {string} options.title - title of the chart. 19 | */ 20 | barChart: function(d3, ds, options) { 21 | var metric = options.metric; 22 | var svg = this.init(d3, ds, options.selector); 23 | var offset = options.title ? 20 : 0; 24 | var g = svg.selectAll('rect') 25 | .data(this.ds); 26 | 27 | var maxVal = Math.max.apply(Math, this.ds.map(function(o) { 28 | return o[options.metric]; 29 | })); 30 | 31 | var yScale = this.d3.scaleLinear() 32 | .domain([0, maxVal]) 33 | .range([options.height, 0]); 34 | 35 | var yAxis = this.d3.axisLeft() 36 | .scale(yScale); 37 | 38 | var xScale = this.initOrdinalScale(options.dim, options.width); 39 | var xAxis = this.d3.axisBottom() 40 | .scale(xScale) 41 | 42 | svg.selectAll('g').remove(); 43 | if (options.title) this.addTitle(options.title, svg, options.width); 44 | 45 | g.enter() 46 | .append('rect') 47 | .merge(g) 48 | .attr('class', 'bar') 49 | .attr('width', (d, i) => { 50 | return (options.width / this.ds.length) - 1 51 | }) 52 | .attr('height', d => { 53 | return options.height - yScale(d[options.metric]) 54 | }) 55 | .attr('x', (d, i) => { 56 | return (i * (options.width / this.ds.length)) + 60 57 | }) 58 | .attr('y', d => { 59 | return yScale(d[options.metric]); 60 | }) 61 | .on('mouseover', d => { 62 | this.addTooltip(d, svg, 63 | this.d3.mouse(this.d3.event.currentTarget)[0], 64 | this.d3.mouse(this.d3.event.currentTarget)[1], metric) 65 | }) 66 | .on('mouseout', d => { 67 | this.removeTooltip(svg); 68 | }) 69 | .attr('transform', 'translate(0,' + offset + ')'); 70 | 71 | this.drawAxis(options.height, svg, xAxis, yAxis, offset); 72 | g.exit().remove(); 73 | }, 74 | 75 | /** 76 | * $helpers.chart.lineChart 77 | * bind data to a line graph. 78 | * @param {string} d3 - reference to d3 object. 79 | * @param {string} ds - dataset for the graph. 80 | * @param {Object} options - options for bar graph. 81 | * @param {string} options.selector - selector name to place the graph. 82 | * @param {string} options.metric - value you are measuring. 83 | * @param {string} options.dim - value you will be categorizing the data by. 84 | * @param {string} options.width - width of the chart. 85 | * @param {string} options.height - height of the chart. 86 | * @param {string} options.title - title of the chart. 87 | */ 88 | lineChart: function(d3, ds, options) { 89 | var metric = options.metric; 90 | var svg = this.init(d3, ds, options.selector); 91 | var offset = options.title ? 20 : 0; 92 | var maxVal = Math.max.apply(Math, this.ds.map(function(o) { 93 | return o[options.metric]; 94 | })); 95 | 96 | var minVal = Math.min.apply(Math, this.ds.map(function(o) { 97 | return o[options.metric]; 98 | })); 99 | 100 | var yScale = this.d3.scaleLinear() 101 | .domain([minVal, maxVal]) 102 | .range([options.height, 0]); 103 | 104 | var yAxis = this.d3.axisLeft() 105 | .scale(yScale); 106 | 107 | var xScale = this.initOrdinalScale(options.dim, options.width); 108 | var xAxis = this.d3.axisBottom() 109 | .scale(xScale) 110 | 111 | var lineFunction = this.d3.line() 112 | .x(function(d, i) { 113 | return xScale(d[options.dim]) + 60; 114 | }) 115 | .y(function(d) { 116 | return yScale(d[options.metric]); 117 | }) 118 | 119 | svg.selectAll('path').remove(); 120 | svg.selectAll('g').remove(); 121 | 122 | 123 | if (options.title) this.addTitle(options.title, svg, options.width); 124 | 125 | svg.append('path') 126 | .datum(this.ds) 127 | .attr('fill', 'none') 128 | .attr('stroke', '#ffab00') 129 | .attr('stroke-width', 3) 130 | .attr('d', lineFunction) 131 | .attr('transform', 'translate(0,' + offset + ')'); 132 | 133 | this.drawAxis(options.height, svg, xAxis, yAxis, offset); 134 | 135 | svg.exit().remove(); 136 | }, 137 | /** 138 | * $helpers.chart.pieChart 139 | * bind data to a pie chart. 140 | * @param {string} d3 - reference to d3 object. 141 | * @param {string} ds - dataset for the graph. 142 | * @param {Object} options - options for bar graph. 143 | * @param {string} options.selector - selector name to place the graph. 144 | * @param {string} options.metric - value you are measuring. 145 | * @param {string} options.dim - value you will be categorizing the data by. 146 | * @param {string} options.width - width of the chart. 147 | * @param {string} options.height - height of the chart. 148 | * @param {string} options.title - title of the chart. 149 | */ 150 | pieChart: function(d3, ds, options) { 151 | var metric = options.metric; 152 | var svg = this.init(d3, ds, options.selector); 153 | var radius = options.height > options.width ? (options.width - options.width * 0.1) / 2 : (options.height - options.height * 0.1) / 2; 154 | var offset = options.title ? 20 : 0; 155 | 156 | var pie = this.d3.pie() 157 | .sort(null) 158 | .value(function(ds) { 159 | return ds[options.metric]; 160 | }); 161 | 162 | var path = this.d3.arc() 163 | .outerRadius(radius - 10) 164 | .innerRadius(25); 165 | 166 | var arc = svg.selectAll('.arc') 167 | .data(pie(ds)) 168 | 169 | var color = d3.scaleOrdinal() 170 | .range(['#4D4D4D', '#5DA5DA', '#FAA43A', '#60BD68', '#F17CB0', 171 | '#B2912F', '#B276B2', '#DECF3F', '#F15854' 172 | ]) 173 | if (options.title) this.addTitle(options.title, svg, options.width); 174 | 175 | arc.enter() 176 | .append('g') 177 | .attr('transform', 'translate(' + options.width / 2 + ',' + options.height / 2 + ')') 178 | .append('path') 179 | .merge(arc) 180 | .attr('class', 'arc') 181 | .attr('d', path) 182 | .attr('fill', function(d, i) { 183 | return color(i); 184 | }) 185 | .on('mouseover', d => { 186 | this.addTooltip(d.data, svg, options.width, 0, metric) 187 | }) 188 | .on('mouseout', d => { 189 | this.removeTooltip(svg); 190 | }) 191 | .attr('transform', 'translate(0,' + offset + ')'); 192 | 193 | arc.exit().remove(); 194 | 195 | }, 196 | /** 197 | * $helpers.chart.scatterPlot 198 | * bind data to a scatter plot. 199 | * @param {string} d3 - reference to d3 object. 200 | * @param {string} ds - dataset for the graph. 201 | * @param {Object} options - options for bar graph. 202 | * @param {string} options.selector - selector name to place the graph. 203 | * @param {string} options.metric - value you are measuring. 204 | * @param {string} options.dim - value you will be categorizing the data by. 205 | * @param {string} options.width - width of the chart. 206 | * @param {string} options.height - height of the chart. 207 | * @param {string} options.title - title of the chart. 208 | */ 209 | scatterPlot: function(d3, ds, options) { 210 | var metric = options.metric; 211 | var svg = this.init(d3, ds, options.selector); 212 | var offset = options.title ? 20 : 0; 213 | var maxVal = Math.max.apply(Math, this.ds.map(function(o) { 214 | return o[options.metric]; 215 | })); 216 | 217 | var minVal = Math.min.apply(Math, this.ds.map(function(o) { 218 | return o[options.metric]; 219 | })); 220 | 221 | var maxVal2 = Math.max.apply(Math, this.ds.map(function(o) { 222 | return o[options.metric2]; 223 | })); 224 | 225 | var minVal2 = Math.min.apply(Math, this.ds.map(function(o) { 226 | return o[options.metric2]; 227 | })); 228 | 229 | var g = svg.selectAll('circle') 230 | .data(this.ds); 231 | 232 | var yScale = this.d3.scaleLinear() 233 | .domain([minVal, maxVal]) 234 | .range([options.height, 0]); 235 | 236 | var yAxis = this.d3.axisLeft() 237 | .scale(yScale); 238 | 239 | var xScale = this.d3.scaleLinear() 240 | .domain([minVal2, maxVal2]) 241 | .range([0, options.width]); 242 | 243 | var xAxis = this.d3.axisBottom() 244 | .scale(xScale) 245 | 246 | svg.selectAll('g').remove(); 247 | 248 | if (options.title) this.addTitle(options.title, svg, options.width); 249 | 250 | g.enter() 251 | .append('circle') 252 | .attr('r', '4') 253 | .attr('class', 'point') 254 | .merge(g) 255 | .attr('cx', (d, i) => { 256 | return (xScale(d[options.metric2])) + 60 257 | }) 258 | .attr('cy', d => { 259 | return yScale(d[options.metric]); 260 | }) 261 | .attr('transform', 'translate(0,' + offset + ')') 262 | 263 | this.drawAxis(options.height, svg, xAxis, yAxis, offset); 264 | 265 | svg.exit().remove(); 266 | }, 267 | 268 | /* Helper Function */ 269 | init: function(d3, ds, selector) { 270 | this.d3 = d3; 271 | this.ds = ds; 272 | return this.d3.select(selector) 273 | }, 274 | 275 | initOrdinalScale: function(dim, width) { 276 | var domainArr = []; 277 | var rangeArr = []; 278 | 279 | this.ds.forEach((t) => { 280 | domainArr.push(t[dim]) 281 | }) 282 | this.ds.forEach((t, i) => { 283 | rangeArr.push(width * i / this.ds.length) 284 | }) 285 | 286 | var xScale = this.d3.scaleOrdinal() 287 | .domain(domainArr) 288 | .range(rangeArr); 289 | return xScale; 290 | }, 291 | 292 | drawAxis: function(height, svg, xAxis, yAxis, offset) { 293 | offset = offset || 0; 294 | svg.append('g') 295 | .attr('transform', 'translate(50,' + offset + ')') 296 | .call(yAxis); 297 | 298 | svg.append('g') 299 | .attr('transform', 'translate(70,' + (height + offset + 5) + ')') 300 | .call(xAxis); 301 | }, 302 | 303 | addTooltip: function(d, svg, x, y, v) { 304 | svg.append('text') 305 | .attr('x', x) 306 | .attr('y', y) 307 | .attr('class', 'tt') 308 | .text(d.name + ': ' + d[v]); 309 | }, 310 | 311 | removeTooltip: function(svg) { 312 | svg.selectAll('.tt').remove(); 313 | }, 314 | 315 | addTitle: function(t, svg, w) { 316 | svg.append('text') 317 | .attr('x', w / 2) 318 | .attr('text-anchor', 'middle') 319 | .attr('y', 0) 320 | .text(t); 321 | } 322 | } 323 | } 324 | } 325 | } 326 | --------------------------------------------------------------------------------