├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build ├── build.js ├── ci.sh ├── karma.conf.js └── release.sh ├── circle.yml ├── dist ├── vue-charts.common.js ├── vue-charts.js ├── vue-charts.min.js └── vue-charts.min.js.gz ├── examples ├── basic.html ├── events.html ├── geochart.html ├── redraw.html └── sets.html ├── package.json ├── src ├── components │ └── chart.js ├── main.js └── utils │ ├── eventsBinder.js │ ├── googleChartsLoader.js │ ├── index.js │ ├── makeDeferred.js │ └── propsWatcher.js └── test └── unit ├── .eslintrc └── specs ├── index.js └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 'root': true, 3 | 4 | 'env': { 5 | 'browser': true, 6 | 'node': true 7 | }, 8 | 9 | 'globals': { 10 | '_': true, 11 | 'google': true, 12 | 'Vue': true 13 | }, 14 | 15 | 'extends': 'standard' 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | node_modules 4 | .DS_Store 5 | *.log 6 | *.swp 7 | *~ 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | *.yml 5 | bower.json 6 | coverage 7 | config 8 | dist/*.map 9 | lib 10 | test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.1 / 2016-10-24 2 | - Fixed linting errors 3 | 4 | # v0.2.0 / 2016-10-23 5 | - Merged https://github.com/haydenbbickerton/vue-charts/pull/14 6 | - Ready for vue 2 (thanks to [syshen!](https://github.com/syshen)) 7 | 8 | # v0.1.13 / 2016-04-06 9 | - es6 tweaks 10 | 11 | # v0.1.12 / 2016-04-06 12 | - Fixed https://github.com/haydenbbickerton/vue-charts/issues/4 13 | 14 | # v0.1.1 / 2016-04-01 15 | - Fixed https://github.com/haydenbbickerton/vue-charts/issues/3 16 | 17 | # v0.1.0 / 2016-03-09 18 | - Added event handling 19 | 20 | # v0.0.5 / 2016-02-15 21 | - "First" release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Hayden Bickerton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-charts 2 | [![Version](https://img.shields.io/npm/v/vue-charts.svg?style=flat-square)](https://www.npmjs.com/package/vue-charts) 3 | [![Status](https://img.shields.io/circleci/project/haydenbbickerton/vue-charts/master.svg?style=flat-square)](https://circleci.com/gh/haydenbbickerton/vue-charts/tree/master) 4 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com) 5 | [![License](https://img.shields.io/npm/l/vue-charts.svg?style=flat-square)](LICENSE) 6 | 7 | Google Charts plugin for Vue.js 8 | 9 | ## Demo 10 | - [Basic Line Chart](https://haydenbbickerton.github.io/vue-charts/basic.html) 11 | - [Multiple Sets of Data, with auto-update](https://haydenbbickerton.github.io/vue-charts/sets.html) 12 | - [Events](https://haydenbbickerton.github.io/vue-charts/events.html) 13 | - [Redraw on window resize](https://haydenbbickerton.github.io/vue-charts/redraw.html) 14 | 15 | ## Installation 16 | 17 | ```shell 18 | npm install --save-dev vue-charts 19 | ``` 20 | 21 | ### Usage 22 | 23 | ```js 24 | Vue.use(VueCharts) 25 | ``` 26 | ```html 27 | 28 | 34 | ``` 35 | 36 | ## Props 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
NameDefaultTypeDescription
packages['corechart']ArrayGoogle Chart Packages to load.
versioncurrentStringGoogle Chart Version to load.
chart-typeLineChartStringThe type of chart to create.
columnsnone, requiredArrayRequired. Chart columns.
rowsnoneArrayChart rows.
chart-eventsnoneObjectGoogle Charts Events. See Events Example
optionsnoneObjectGoogle Charts Options
92 | 93 | 94 | # Credits 95 | 96 | This plugin is heavily based off of: 97 | 98 | - [vue-plugin-boilerplate](https://github.com/kazupon/vue-plugin-boilerplate) 99 | - [vue-google-maps](https://github.com/GuillaumeLeclerc/vue-google-maps/) 100 | - [react-google-charts](https://github.com/RakanNimer/react-google-charts) 101 | 102 | # License 103 | 104 | [MIT](http://opensource.org/licenses/MIT) 105 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var zlib = require('zlib') 3 | var rollup = require('rollup') 4 | var uglify = require('uglify-js') 5 | var babel = require('rollup-plugin-babel') 6 | var replace = require('rollup-plugin-replace') 7 | var pack = require('../package.json') 8 | var version = process.env.VERSION || pack.version 9 | var banner = 10 | '/*!\n' + 11 | ' * ' + pack.name + ' v' + version + '\n' + 12 | ' * (c) ' + new Date().getFullYear() + ' ' + pack.author.name + '\n' + 13 | ' * Released under the ' + pack.license + ' License.\n' + 14 | ' */' 15 | 16 | // update main file 17 | var main = fs 18 | .readFileSync('src/main.js', 'utf-8') 19 | .replace(/plugin\.version = '[\d\.]+'/, "plugin.version = '" + pack.version + "'") 20 | fs.writeFileSync('src/main.js', main) 21 | 22 | // CommonJS build. 23 | // this is used as the "main" field in package.json 24 | // and used by bundlers like Webpack and Browserify. 25 | rollup.rollup({ 26 | entry: 'src/main.js', 27 | plugins: [ 28 | babel({ 29 | presets: ['es2015-rollup'] 30 | }) 31 | ] 32 | }) 33 | .then(function (bundle) { 34 | return write('dist/' + pack.name + '.common.js', bundle.generate({ 35 | format: 'cjs', 36 | banner: banner 37 | }).code) 38 | }) 39 | // Standalone Dev Build 40 | .then(function () { 41 | return rollup.rollup({ 42 | entry: 'src/main.js', 43 | plugins: [ 44 | replace({ 45 | 'process.env.NODE_ENV': "'development'" 46 | }), 47 | babel({ 48 | presets: ['es2015-rollup'] 49 | }) 50 | ] 51 | }) 52 | .then(function (bundle) { 53 | return write('dist/' + pack.name + '.js', bundle.generate({ 54 | format: 'umd', 55 | banner: banner, 56 | moduleName: classify(pack.name) 57 | }).code) 58 | }) 59 | }) 60 | .then(function () { 61 | // Standalone Production Build 62 | return rollup.rollup({ 63 | entry: 'src/main.js', 64 | plugins: [ 65 | replace({ 66 | 'process.env.NODE_ENV': "'production'" 67 | }), 68 | babel({ 69 | presets: ['es2015-rollup'] 70 | }) 71 | ] 72 | }) 73 | .then(function (bundle) { 74 | var code = bundle.generate({ 75 | format: 'umd', 76 | moduleName: classify(pack.name) 77 | }).code 78 | var minified = banner + '\n' + uglify.minify(code, { 79 | fromString: true 80 | }).code 81 | return write('dist/' + pack.name + '.min.js', minified) 82 | }) 83 | .then(zip) 84 | }) 85 | .catch(logError) 86 | 87 | function toUpper (_, c) { 88 | return c ? c.toUpperCase() : '' 89 | } 90 | 91 | const classifyRE = /(?:^|[-_\/])(\w)/g 92 | function classify (str) { 93 | return str.replace(classifyRE, toUpper) 94 | } 95 | 96 | function write (dest, code) { 97 | return new Promise(function (resolve, reject) { 98 | fs.writeFile(dest, code, function (err) { 99 | if (err) return reject(err) 100 | console.log(blue(dest) + ' ' + getSize(code)) 101 | resolve() 102 | }) 103 | }) 104 | } 105 | 106 | function zip () { 107 | return new Promise(function (resolve, reject) { 108 | fs.readFile('dist/' + pack.name + '.min.js', function (err, buf) { 109 | if (err) return reject(err) 110 | zlib.gzip(buf, function (err, buf) { 111 | if (err) return reject(err) 112 | write('dist/' + pack.name + '.min.js.gz', buf).then(resolve) 113 | }) 114 | }) 115 | }) 116 | } 117 | 118 | function getSize (code) { 119 | return (code.length / 1024).toFixed(2) + 'kb' 120 | } 121 | 122 | function logError (e) { 123 | console.log(e) 124 | } 125 | 126 | function blue (str) { 127 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m' 128 | } 129 | -------------------------------------------------------------------------------- /build/ci.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | if [ -z "$CI_PULL_REQUEST" ] 3 | then 4 | npm run lint 5 | npm run unit 6 | else 7 | npm test 8 | fi -------------------------------------------------------------------------------- /build/karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | browsers: ['PhantomJS'], 6 | frameworks: ['mocha', 'sinon-chai'], 7 | files: [ 8 | '../node_modules/babel-polyfill/dist/polyfill.js', 9 | '../test/unit/specs/index.js' 10 | ], 11 | preprocessors: { 12 | '../test/unit/specs/index.js': ['webpack', 'sourcemap'] 13 | }, 14 | webpack: { 15 | devtool: 'source-map', 16 | resolve: { 17 | alias: { 18 | 'src': path.resolve(__dirname, '../src') 19 | } 20 | }, 21 | module: { 22 | loaders: [{ 23 | test: /\.js$/, 24 | exclude: /node_modules|vue\/dist/, 25 | loader: 'babel', 26 | query: { 27 | presets: ['es2015'], 28 | plugins: [ 29 | ['babel-plugin-espower'] 30 | ] 31 | } 32 | }], 33 | postLoaders: [{ 34 | test: /\.json$/, 35 | loader: 'json' 36 | }, { 37 | test: /\.js$/, 38 | exclude: /test|node_modules|vue\/dist/, 39 | loader: 'istanbul-instrumenter' 40 | }] 41 | } 42 | }, 43 | webpackMiddleware: { 44 | noInfo: true 45 | }, 46 | browserDisconnectTimeout: 5000, 47 | reporters: [ 48 | 'mocha', 'coverage' 49 | ], 50 | coverageReporter: { 51 | reporters: [ 52 | {type: 'lcov', dir: '../test/unit/coverage'}, 53 | {type: 'text-summary', dir: '../test/unit/coverage'}] 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /build/release.sh: -------------------------------------------------------------------------------- 1 | # Lifted from https://github.com/vuejs/vue-router/blob/e37a1ce49fa01016bec9f88584de2a89ad3881ef/build/release.sh 2 | 3 | set -e 4 | echo "Enter release version: " 5 | read VERSION 6 | 7 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 8 | echo # (optional) move to a new line 9 | if [[ $REPLY =~ ^[Yy]$ ]] 10 | then 11 | echo "Releasing $VERSION ..." 12 | 13 | # run tests 14 | npm test 2>/dev/null 15 | 16 | # build 17 | VERSION=$VERSION npm run build 18 | 19 | # # commit 20 | git add -A 21 | git commit -m "[build] $VERSION" 22 | npm version $VERSION --message "[release] $VERSION" 23 | 24 | # # publish 25 | git push origin refs/tags/v$VERSION 26 | git push 27 | npm publish 28 | fi -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 5 4 | 5 | general: 6 | branches: 7 | only: 8 | - master 9 | 10 | test: 11 | override: 12 | - bash ./build/ci.sh -------------------------------------------------------------------------------- /dist/vue-charts.common.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-charts v0.2.1 3 | * (c) 2016 Hayden Bickerton 4 | * Released under the MIT License. 5 | */ 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | _ = 'default' in _ ? _['default'] : _; 10 | 11 | /* 12 | This lets us resolve the promise outside the 13 | promise function itself. 14 | */ 15 | function makeDeferred() { 16 | var resolvePromise = null; 17 | var rejectPromise = null; 18 | 19 | var promise = new Promise(function (resolve, reject) { 20 | resolvePromise = resolve; 21 | rejectPromise = reject; 22 | }); 23 | 24 | return { 25 | promise: promise, 26 | resolve: resolvePromise, 27 | reject: rejectPromise 28 | }; 29 | } 30 | 31 | function eventsBinder(vue, googleChart, events) { 32 | // Loop through our events, create a listener for them, and 33 | // attach our callback function to that event. 34 | for (var event in events) { 35 | var eventName = event; 36 | var eventCallback = events[event]; 37 | 38 | if (eventName === 'ready') { 39 | // The chart is already ready, so this event missed it's chance. 40 | // We'll call it manually. 41 | eventCallback(); 42 | } else { 43 | google.visualization.events.addListener(googleChart, eventName, eventCallback); 44 | } 45 | } 46 | } 47 | 48 | var isLoading = false; 49 | var isLoaded = false; 50 | 51 | // Our main promise 52 | var googlePromise = makeDeferred(); 53 | 54 | function googleChartsLoader() { 55 | var packages = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['corechart']; 56 | var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'current'; 57 | var mapsApiKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 58 | var language = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'en'; 59 | 60 | if (!Array.isArray(packages)) { 61 | throw new TypeError('packages must be an array'); 62 | } 63 | 64 | if (version !== 'current' && typeof version !== 'number' && version !== 'upcoming') { 65 | throw new TypeError('version must be a number, "upcoming" or "current"'); 66 | } 67 | 68 | // Google only lets you load it once, so we'll only run once. 69 | if (isLoading || isLoaded) { 70 | return googlePromise.promise; 71 | } 72 | 73 | isLoading = true; 74 | 75 | var script = document.createElement('script'); 76 | script.setAttribute('src', 'https://www.gstatic.com/charts/loader.js'); 77 | 78 | script.onreadystatechange = script.onload = function () { 79 | // After the 'loader.js' is loaded, load our version and packages 80 | var options = { 81 | packages: packages, 82 | language: language 83 | }; 84 | 85 | if (mapsApiKey) { 86 | options['mapsApiKey'] = mapsApiKey; 87 | } 88 | 89 | google.charts.load(version, options); 90 | 91 | // After we've loaded Google Charts, resolve our promise 92 | google.charts.setOnLoadCallback(function () { 93 | isLoading = false; 94 | isLoaded = true; 95 | googlePromise.resolve(); 96 | }); 97 | }; 98 | 99 | // Insert our script into the DOM 100 | document.getElementsByTagName('head')[0].appendChild(script); 101 | 102 | return googlePromise.promise; 103 | } 104 | 105 | function propsWatcher(vue, props) { 106 | /* 107 | Watch our props. Every time they change, redraw the chart. 108 | */ 109 | _.each(props, function (_ref, attribute) { 110 | var type = _ref.type; 111 | 112 | vue.$watch(attribute, function () { 113 | vue.drawChart(); 114 | }, { 115 | deep: _.isObject(type) 116 | }); 117 | }); 118 | } 119 | 120 | var chartDeferred = makeDeferred(); 121 | 122 | var props = { 123 | packages: { 124 | type: Array, 125 | default: function _default() { 126 | return ['corechart']; 127 | } 128 | }, 129 | version: { 130 | default: 'current' 131 | }, 132 | mapsApiKey: { 133 | default: false 134 | }, 135 | language: { 136 | type: String, 137 | default: 'en' 138 | }, 139 | chartType: { 140 | type: String, 141 | default: function _default() { 142 | return 'LineChart'; 143 | } 144 | }, 145 | chartEvents: { 146 | type: Object, 147 | default: function _default() { 148 | return {}; 149 | } 150 | }, 151 | columns: { 152 | required: true, 153 | type: Array 154 | }, 155 | rows: { 156 | type: Array, 157 | default: function _default() { 158 | return []; 159 | } 160 | }, 161 | options: { 162 | type: Object, 163 | default: function _default() { 164 | return { 165 | chart: { 166 | title: 'Chart Title', 167 | subtitle: 'Subtitle' 168 | }, 169 | hAxis: { 170 | title: 'X Label' 171 | }, 172 | vAxis: { 173 | title: 'Y Label' 174 | }, 175 | width: '400px', 176 | height: '300px', 177 | animation: { 178 | duration: 500, 179 | easing: 'out' 180 | } 181 | }; 182 | } 183 | } 184 | }; 185 | 186 | var Chart = { 187 | name: 'vue-chart', 188 | props: props, 189 | render: function render(h) { 190 | var self = this; 191 | return h('div', { class: 'vue-chart-container' }, [h('div', { 192 | attrs: { 193 | id: self.chartId, 194 | class: 'vue-chart' 195 | } 196 | })]); 197 | }, 198 | data: function data() { 199 | return { 200 | chart: null, 201 | /* 202 | We put the uid in the DOM element so the component can be used multiple 203 | times in the same view. Otherwise Google Charts will only make one chart. 204 | The X is prepended because there must be at least 205 | 1 character in id - https://www.w3.org/TR/html5/dom.html#the-id-attribute 206 | */ 207 | chartId: 'X' + this._uid, 208 | wrapper: null, 209 | dataTable: [], 210 | hiddenColumns: [] 211 | }; 212 | }, 213 | 214 | events: { 215 | redrawChart: function redrawChart() { 216 | this.drawChart(); 217 | } 218 | }, 219 | mounted: function mounted() { 220 | var self = this; 221 | googleChartsLoader(self.packages, self.version, self.mapsApiKey, self.language).then(self.drawChart).then(function () { 222 | // we don't want to bind props because it's a kind of "computed" property 223 | var watchProps = props; 224 | delete watchProps.bounds; 225 | 226 | // watching properties 227 | propsWatcher(self, watchProps); 228 | 229 | // binding events 230 | eventsBinder(self, self.chart, self.chartEvents); 231 | }).catch(function (error) { 232 | throw error; 233 | }); 234 | }, 235 | 236 | methods: { 237 | /** 238 | * Initialize the datatable and add the initial data. 239 | * 240 | * @link https://developers.google.com/chart/interactive/docs/reference#DataTable 241 | * @return object 242 | */ 243 | buildDataTable: function buildDataTable() { 244 | var self = this; 245 | 246 | var dataTable = new google.visualization.DataTable(); 247 | 248 | _.each(self.columns, function (value) { 249 | dataTable.addColumn(value); 250 | }); 251 | 252 | if (!_.isEmpty(self.rows)) { 253 | dataTable.addRows(self.rows); 254 | } 255 | 256 | return dataTable; 257 | }, 258 | 259 | 260 | /** 261 | * Update the datatable. 262 | * 263 | * @return void 264 | */ 265 | updateDataTable: function updateDataTable() { 266 | var self = this; 267 | 268 | // Remove all data from the datatable. 269 | self.dataTable.removeRows(0, self.dataTable.getNumberOfRows()); 270 | self.dataTable.removeColumns(0, self.dataTable.getNumberOfColumns()); 271 | 272 | // Add 273 | _.each(self.columns, function (value) { 274 | self.dataTable.addColumn(value); 275 | }); 276 | 277 | if (!_.isEmpty(self.rows)) { 278 | self.dataTable.addRows(self.rows); 279 | } 280 | }, 281 | 282 | 283 | /** 284 | * Initialize the wrapper 285 | * 286 | * @link https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class 287 | * 288 | * @return object 289 | */ 290 | buildWrapper: function buildWrapper(chartType, dataTable, options, containerId) { 291 | var wrapper = new google.visualization.ChartWrapper({ 292 | chartType: chartType, 293 | dataTable: dataTable, 294 | options: options, 295 | containerId: containerId 296 | }); 297 | 298 | return wrapper; 299 | }, 300 | 301 | 302 | /** 303 | * Build the chart. 304 | * 305 | * @return void 306 | */ 307 | buildChart: function buildChart() { 308 | var self = this; 309 | 310 | // If dataTable isn't set, build it 311 | var dataTable = _.isEmpty(self.dataTable) ? self.buildDataTable() : self.dataTable; 312 | 313 | self.wrapper = self.buildWrapper(self.chartType, dataTable, self.options, self.chartId); 314 | 315 | // Set the datatable on this instance 316 | self.dataTable = self.wrapper.getDataTable(); 317 | 318 | // After chart is built, set it on this instance and resolve the promise. 319 | google.visualization.events.addOneTimeListener(self.wrapper, 'ready', function () { 320 | self.chart = self.wrapper.getChart(); 321 | chartDeferred.resolve(); 322 | }); 323 | }, 324 | 325 | 326 | /** 327 | * Draw the chart. 328 | * 329 | * @return Promise 330 | */ 331 | drawChart: function drawChart() { 332 | var self = this; 333 | 334 | // We don't have any (usable) data, or we don't have columns. We can't draw a chart without those. 335 | if (!_.isEmpty(self.rows) && !_.isObjectLike(self.rows) || _.isEmpty(self.columns)) { 336 | return; 337 | } 338 | 339 | if (_.isNull(self.chart)) { 340 | // We haven't built the chart yet, so JUST. DO. IT! 341 | self.buildChart(); 342 | } else { 343 | // Chart already exists, just update the data 344 | self.updateDataTable(); 345 | } 346 | 347 | // Chart has been built/Data has been updated, draw the chart. 348 | self.wrapper.draw(); 349 | 350 | // Return promise. Resolves when chart finishes loading. 351 | return chartDeferred.promise; 352 | } 353 | } 354 | }; 355 | 356 | function install(Vue) { 357 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 358 | 359 | Vue.component('vue-chart', Chart); 360 | } 361 | 362 | module.exports = install; -------------------------------------------------------------------------------- /dist/vue-charts.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-charts v0.2.1 3 | * (c) 2016 Hayden Bickerton 4 | * Released under the MIT License. 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('lodash')) : 8 | typeof define === 'function' && define.amd ? define(['lodash'], factory) : 9 | global.VueCharts = factory(global._); 10 | }(this, function (_) { 'use strict'; 11 | 12 | _ = 'default' in _ ? _['default'] : _; 13 | 14 | /* 15 | This lets us resolve the promise outside the 16 | promise function itself. 17 | */ 18 | function makeDeferred() { 19 | var resolvePromise = null; 20 | var rejectPromise = null; 21 | 22 | var promise = new Promise(function (resolve, reject) { 23 | resolvePromise = resolve; 24 | rejectPromise = reject; 25 | }); 26 | 27 | return { 28 | promise: promise, 29 | resolve: resolvePromise, 30 | reject: rejectPromise 31 | }; 32 | } 33 | 34 | function eventsBinder(vue, googleChart, events) { 35 | // Loop through our events, create a listener for them, and 36 | // attach our callback function to that event. 37 | for (var event in events) { 38 | var eventName = event; 39 | var eventCallback = events[event]; 40 | 41 | if (eventName === 'ready') { 42 | // The chart is already ready, so this event missed it's chance. 43 | // We'll call it manually. 44 | eventCallback(); 45 | } else { 46 | google.visualization.events.addListener(googleChart, eventName, eventCallback); 47 | } 48 | } 49 | } 50 | 51 | var isLoading = false; 52 | var isLoaded = false; 53 | 54 | // Our main promise 55 | var googlePromise = makeDeferred(); 56 | 57 | function googleChartsLoader() { 58 | var packages = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['corechart']; 59 | var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'current'; 60 | var mapsApiKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 61 | var language = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'en'; 62 | 63 | if (!Array.isArray(packages)) { 64 | throw new TypeError('packages must be an array'); 65 | } 66 | 67 | if (version !== 'current' && typeof version !== 'number' && version !== 'upcoming') { 68 | throw new TypeError('version must be a number, "upcoming" or "current"'); 69 | } 70 | 71 | // Google only lets you load it once, so we'll only run once. 72 | if (isLoading || isLoaded) { 73 | return googlePromise.promise; 74 | } 75 | 76 | isLoading = true; 77 | 78 | var script = document.createElement('script'); 79 | script.setAttribute('src', 'https://www.gstatic.com/charts/loader.js'); 80 | 81 | script.onreadystatechange = script.onload = function () { 82 | // After the 'loader.js' is loaded, load our version and packages 83 | var options = { 84 | packages: packages, 85 | language: language 86 | }; 87 | 88 | if (mapsApiKey) { 89 | options['mapsApiKey'] = mapsApiKey; 90 | } 91 | 92 | google.charts.load(version, options); 93 | 94 | // After we've loaded Google Charts, resolve our promise 95 | google.charts.setOnLoadCallback(function () { 96 | isLoading = false; 97 | isLoaded = true; 98 | googlePromise.resolve(); 99 | }); 100 | }; 101 | 102 | // Insert our script into the DOM 103 | document.getElementsByTagName('head')[0].appendChild(script); 104 | 105 | return googlePromise.promise; 106 | } 107 | 108 | function propsWatcher(vue, props) { 109 | /* 110 | Watch our props. Every time they change, redraw the chart. 111 | */ 112 | _.each(props, function (_ref, attribute) { 113 | var type = _ref.type; 114 | 115 | vue.$watch(attribute, function () { 116 | vue.drawChart(); 117 | }, { 118 | deep: _.isObject(type) 119 | }); 120 | }); 121 | } 122 | 123 | var chartDeferred = makeDeferred(); 124 | 125 | var props = { 126 | packages: { 127 | type: Array, 128 | default: function _default() { 129 | return ['corechart']; 130 | } 131 | }, 132 | version: { 133 | default: 'current' 134 | }, 135 | mapsApiKey: { 136 | default: false 137 | }, 138 | language: { 139 | type: String, 140 | default: 'en' 141 | }, 142 | chartType: { 143 | type: String, 144 | default: function _default() { 145 | return 'LineChart'; 146 | } 147 | }, 148 | chartEvents: { 149 | type: Object, 150 | default: function _default() { 151 | return {}; 152 | } 153 | }, 154 | columns: { 155 | required: true, 156 | type: Array 157 | }, 158 | rows: { 159 | type: Array, 160 | default: function _default() { 161 | return []; 162 | } 163 | }, 164 | options: { 165 | type: Object, 166 | default: function _default() { 167 | return { 168 | chart: { 169 | title: 'Chart Title', 170 | subtitle: 'Subtitle' 171 | }, 172 | hAxis: { 173 | title: 'X Label' 174 | }, 175 | vAxis: { 176 | title: 'Y Label' 177 | }, 178 | width: '400px', 179 | height: '300px', 180 | animation: { 181 | duration: 500, 182 | easing: 'out' 183 | } 184 | }; 185 | } 186 | } 187 | }; 188 | 189 | var Chart = { 190 | name: 'vue-chart', 191 | props: props, 192 | render: function render(h) { 193 | var self = this; 194 | return h('div', { class: 'vue-chart-container' }, [h('div', { 195 | attrs: { 196 | id: self.chartId, 197 | class: 'vue-chart' 198 | } 199 | })]); 200 | }, 201 | data: function data() { 202 | return { 203 | chart: null, 204 | /* 205 | We put the uid in the DOM element so the component can be used multiple 206 | times in the same view. Otherwise Google Charts will only make one chart. 207 | The X is prepended because there must be at least 208 | 1 character in id - https://www.w3.org/TR/html5/dom.html#the-id-attribute 209 | */ 210 | chartId: 'X' + this._uid, 211 | wrapper: null, 212 | dataTable: [], 213 | hiddenColumns: [] 214 | }; 215 | }, 216 | 217 | events: { 218 | redrawChart: function redrawChart() { 219 | this.drawChart(); 220 | } 221 | }, 222 | mounted: function mounted() { 223 | var self = this; 224 | googleChartsLoader(self.packages, self.version, self.mapsApiKey, self.language).then(self.drawChart).then(function () { 225 | // we don't want to bind props because it's a kind of "computed" property 226 | var watchProps = props; 227 | delete watchProps.bounds; 228 | 229 | // watching properties 230 | propsWatcher(self, watchProps); 231 | 232 | // binding events 233 | eventsBinder(self, self.chart, self.chartEvents); 234 | }).catch(function (error) { 235 | throw error; 236 | }); 237 | }, 238 | 239 | methods: { 240 | /** 241 | * Initialize the datatable and add the initial data. 242 | * 243 | * @link https://developers.google.com/chart/interactive/docs/reference#DataTable 244 | * @return object 245 | */ 246 | buildDataTable: function buildDataTable() { 247 | var self = this; 248 | 249 | var dataTable = new google.visualization.DataTable(); 250 | 251 | _.each(self.columns, function (value) { 252 | dataTable.addColumn(value); 253 | }); 254 | 255 | if (!_.isEmpty(self.rows)) { 256 | dataTable.addRows(self.rows); 257 | } 258 | 259 | return dataTable; 260 | }, 261 | 262 | 263 | /** 264 | * Update the datatable. 265 | * 266 | * @return void 267 | */ 268 | updateDataTable: function updateDataTable() { 269 | var self = this; 270 | 271 | // Remove all data from the datatable. 272 | self.dataTable.removeRows(0, self.dataTable.getNumberOfRows()); 273 | self.dataTable.removeColumns(0, self.dataTable.getNumberOfColumns()); 274 | 275 | // Add 276 | _.each(self.columns, function (value) { 277 | self.dataTable.addColumn(value); 278 | }); 279 | 280 | if (!_.isEmpty(self.rows)) { 281 | self.dataTable.addRows(self.rows); 282 | } 283 | }, 284 | 285 | 286 | /** 287 | * Initialize the wrapper 288 | * 289 | * @link https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class 290 | * 291 | * @return object 292 | */ 293 | buildWrapper: function buildWrapper(chartType, dataTable, options, containerId) { 294 | var wrapper = new google.visualization.ChartWrapper({ 295 | chartType: chartType, 296 | dataTable: dataTable, 297 | options: options, 298 | containerId: containerId 299 | }); 300 | 301 | return wrapper; 302 | }, 303 | 304 | 305 | /** 306 | * Build the chart. 307 | * 308 | * @return void 309 | */ 310 | buildChart: function buildChart() { 311 | var self = this; 312 | 313 | // If dataTable isn't set, build it 314 | var dataTable = _.isEmpty(self.dataTable) ? self.buildDataTable() : self.dataTable; 315 | 316 | self.wrapper = self.buildWrapper(self.chartType, dataTable, self.options, self.chartId); 317 | 318 | // Set the datatable on this instance 319 | self.dataTable = self.wrapper.getDataTable(); 320 | 321 | // After chart is built, set it on this instance and resolve the promise. 322 | google.visualization.events.addOneTimeListener(self.wrapper, 'ready', function () { 323 | self.chart = self.wrapper.getChart(); 324 | chartDeferred.resolve(); 325 | }); 326 | }, 327 | 328 | 329 | /** 330 | * Draw the chart. 331 | * 332 | * @return Promise 333 | */ 334 | drawChart: function drawChart() { 335 | var self = this; 336 | 337 | // We don't have any (usable) data, or we don't have columns. We can't draw a chart without those. 338 | if (!_.isEmpty(self.rows) && !_.isObjectLike(self.rows) || _.isEmpty(self.columns)) { 339 | return; 340 | } 341 | 342 | if (_.isNull(self.chart)) { 343 | // We haven't built the chart yet, so JUST. DO. IT! 344 | self.buildChart(); 345 | } else { 346 | // Chart already exists, just update the data 347 | self.updateDataTable(); 348 | } 349 | 350 | // Chart has been built/Data has been updated, draw the chart. 351 | self.wrapper.draw(); 352 | 353 | // Return promise. Resolves when chart finishes loading. 354 | return chartDeferred.promise; 355 | } 356 | } 357 | }; 358 | 359 | function install(Vue) { 360 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 361 | 362 | Vue.component('vue-chart', Chart); 363 | } 364 | 365 | return install; 366 | 367 | })); -------------------------------------------------------------------------------- /dist/vue-charts.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-charts v0.2.1 3 | * (c) 2016 Hayden Bickerton 4 | * Released under the MIT License. 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("lodash")):"function"==typeof define&&define.amd?define(["lodash"],t):e.VueCharts=t(e._)}(this,function(e){"use strict";function t(){var e=null,t=null,a=new Promise(function(a,r){e=a,t=r});return{promise:a,resolve:e,reject:t}}function a(e,t,a){for(var r in a){var n=r,i=a[r];"ready"===n?i():google.visualization.events.addListener(t,n,i)}}function r(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["corechart"],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"current",a=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"en";if(!Array.isArray(e))throw new TypeError("packages must be an array");if("current"!==t&&"number"!=typeof t&&"upcoming"!==t)throw new TypeError('version must be a number, "upcoming" or "current"');if(o||u)return s.promise;o=!0;var n=document.createElement("script");return n.setAttribute("src","https://www.gstatic.com/charts/loader.js"),n.onreadystatechange=n.onload=function(){var n={packages:e,language:r};a&&(n.mapsApiKey=a),google.charts.load(t,n),google.charts.setOnLoadCallback(function(){o=!1,u=!0,s.resolve()})},document.getElementsByTagName("head")[0].appendChild(n),s.promise}function n(t,a){e.each(a,function(a,r){var n=a.type;t.$watch(r,function(){t.drawChart()},{deep:e.isObject(n)})})}function i(e){arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e.component("vue-chart",d)}e="default"in e?e.default:e;var o=!1,u=!1,s=t(),c=t(),l={packages:{type:Array,default:function(){return["corechart"]}},version:{default:"current"},mapsApiKey:{default:!1},language:{type:String,default:"en"},chartType:{type:String,default:function(){return"LineChart"}},chartEvents:{type:Object,default:function(){return{}}},columns:{required:!0,type:Array},rows:{type:Array,default:function(){return[]}},options:{type:Object,default:function(){return{chart:{title:"Chart Title",subtitle:"Subtitle"},hAxis:{title:"X Label"},vAxis:{title:"Y Label"},width:"400px",height:"300px",animation:{duration:500,easing:"out"}}}}},d={name:"vue-chart",props:l,render:function(e){var t=this;return e("div",{class:"vue-chart-container"},[e("div",{attrs:{id:t.chartId,class:"vue-chart"}})])},data:function(){return{chart:null,chartId:"X"+this._uid,wrapper:null,dataTable:[],hiddenColumns:[]}},events:{redrawChart:function(){this.drawChart()}},mounted:function(){var e=this;r(e.packages,e.version,e.mapsApiKey,e.language).then(e.drawChart).then(function(){var t=l;delete t.bounds,n(e,t),a(e,e.chart,e.chartEvents)}).catch(function(e){throw e})},methods:{buildDataTable:function(){var t=this,a=new google.visualization.DataTable;return e.each(t.columns,function(e){a.addColumn(e)}),e.isEmpty(t.rows)||a.addRows(t.rows),a},updateDataTable:function(){var t=this;t.dataTable.removeRows(0,t.dataTable.getNumberOfRows()),t.dataTable.removeColumns(0,t.dataTable.getNumberOfColumns()),e.each(t.columns,function(e){t.dataTable.addColumn(e)}),e.isEmpty(t.rows)||t.dataTable.addRows(t.rows)},buildWrapper:function(e,t,a,r){var n=new google.visualization.ChartWrapper({chartType:e,dataTable:t,options:a,containerId:r});return n},buildChart:function(){var t=this,a=e.isEmpty(t.dataTable)?t.buildDataTable():t.dataTable;t.wrapper=t.buildWrapper(t.chartType,a,t.options,t.chartId),t.dataTable=t.wrapper.getDataTable(),google.visualization.events.addOneTimeListener(t.wrapper,"ready",function(){t.chart=t.wrapper.getChart(),c.resolve()})},drawChart:function(){var t=this;if((e.isEmpty(t.rows)||e.isObjectLike(t.rows))&&!e.isEmpty(t.columns))return e.isNull(t.chart)?t.buildChart():t.updateDataTable(),t.wrapper.draw(),c.promise}}};return i}); -------------------------------------------------------------------------------- /dist/vue-charts.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haydenbbickerton/vue-charts/f8bb783dd2476d854389678d0abe45b35ad8014b/dist/vue-charts.min.js.gz -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Charts Basic Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Charts Basic Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /examples/geochart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Charts Basic Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/redraw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Charts Redraw Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/sets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Charts Basic Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |

23 | Multiple Data Sets 24 |

25 |
26 | 27 | 36 |
37 |
38 |
39 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-charts", 3 | "description": "Google Charts component for Vue.js", 4 | "version": "0.2.1", 5 | "author": { 6 | "name": "Hayden Bickerton", 7 | "email": "haydenbbickerton@gmail.com" 8 | }, 9 | "homepage": "https://github.com/haydenbbickerton/vue-charts#readme", 10 | "jsnext:main": "src/main.js", 11 | "license": "MIT", 12 | "main": "dist/vue-charts.common.js", 13 | "files": [ 14 | "dist/vue-charts.js", 15 | "dist/vue-charts.min.js", 16 | "dist/vue-charts.common.js", 17 | "src" 18 | ], 19 | "scripts": { 20 | "build": "node build/build.js", 21 | "clean": "rm -rf dist/*", 22 | "lint": "eslint src/** build/**.js", 23 | "unit": "karma start build/karma.conf.js --single-run", 24 | "test": "npm run lint && npm run unit" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/haydenbbickerton/vue-charts.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/haydenbbickerton/vue-charts/issues" 32 | }, 33 | "dependencies": { 34 | "lodash": "^4.3.0", 35 | "vue": "^2.0.3" 36 | }, 37 | "devDependencies": { 38 | "babel-core": "^6.2.1", 39 | "babel-loader": "^6.2.0", 40 | "babel-plugin-espower": "^2.0.0", 41 | "babel-plugin-external-helpers": "^6.8.0", 42 | "babel-polyfill": "^6.7.4", 43 | "babel-preset-es2015": "^6.16.0", 44 | "babel-preset-es2015-rollup": "^1.0.0", 45 | "chai": "^3.5.0", 46 | "chai-as-promised": "^5.3.0", 47 | "chromedriver": "^2.21.2", 48 | "eslint": "^2.9.0", 49 | "eslint-config-standard": "^5.3.1", 50 | "eslint-loader": "^1.1.1", 51 | "eslint-plugin-promise": "^1.1.0", 52 | "eslint-plugin-standard": "^1.3.2", 53 | "function-bind": "^1.1.0", 54 | "istanbul-instrumenter-loader": "^0.1.3", 55 | "json-loader": "^0.5.4", 56 | "karma": "^0.13.22", 57 | "karma-coverage": "^0.5.5", 58 | "karma-mocha": "^0.2.2", 59 | "karma-mocha-reporter": "^2.0.2", 60 | "karma-phantomjs-launcher": "^1.0.0", 61 | "karma-sinon-chai": "^1.2.0", 62 | "karma-sourcemap-loader": "^0.3.7", 63 | "karma-spec-reporter": "0.0.26", 64 | "karma-webpack": "^1.7.0", 65 | "lodash": "^4.3.0", 66 | "lolex": "^1.4.0", 67 | "mocha": "^2.4.5", 68 | "mocha-loader": "^0.7.1", 69 | "nightwatch": "^0.8.18", 70 | "phantomjs-prebuilt": "^2.1.7", 71 | "rollup": "^0.21.1", 72 | "rollup-plugin-babel": "^2.2.0", 73 | "rollup-plugin-replace": "^1.1.0", 74 | "sinon": "^1.17.3", 75 | "sinon-chai": "^2.8.0", 76 | "uglify-js": "^2.6.1", 77 | "webpack": "^1.12.9" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/chart.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { 3 | eventsBinder, 4 | googleChartsLoader as loadCharts, 5 | makeDeferred, 6 | propsWatcher 7 | } from '../utils/index' 8 | 9 | const chartDeferred = makeDeferred() 10 | 11 | let props = { 12 | packages: { 13 | type: Array, 14 | default: () => { 15 | return ['corechart'] 16 | } 17 | }, 18 | version: { 19 | default: 'current' 20 | }, 21 | mapsApiKey: { 22 | default: false 23 | }, 24 | language: { 25 | type: String, 26 | default: 'en' 27 | }, 28 | chartType: { 29 | type: String, 30 | default: () => { 31 | return 'LineChart' 32 | } 33 | }, 34 | chartEvents: { 35 | type: Object, 36 | default: () => { 37 | return {} 38 | } 39 | }, 40 | columns: { 41 | required: true, 42 | type: Array 43 | }, 44 | rows: { 45 | type: Array, 46 | default: () => { 47 | return [] 48 | } 49 | }, 50 | options: { 51 | type: Object, 52 | default: () => { 53 | return { 54 | chart: { 55 | title: 'Chart Title', 56 | subtitle: 'Subtitle' 57 | }, 58 | hAxis: { 59 | title: 'X Label' 60 | }, 61 | vAxis: { 62 | title: 'Y Label' 63 | }, 64 | width: '400px', 65 | height: '300px', 66 | animation: { 67 | duration: 500, 68 | easing: 'out' 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | export default { 76 | name: 'vue-chart', 77 | props: props, 78 | render (h) { 79 | const self = this 80 | return h('div', {class: 'vue-chart-container'}, [ 81 | h('div', { 82 | attrs: { 83 | id: self.chartId, 84 | class: 'vue-chart' 85 | } 86 | }) 87 | ]) 88 | }, 89 | data () { 90 | return { 91 | chart: null, 92 | /* 93 | We put the uid in the DOM element so the component can be used multiple 94 | times in the same view. Otherwise Google Charts will only make one chart. 95 | 96 | The X is prepended because there must be at least 97 | 1 character in id - https://www.w3.org/TR/html5/dom.html#the-id-attribute 98 | */ 99 | chartId: 'X' + this._uid, 100 | wrapper: null, 101 | dataTable: [], 102 | hiddenColumns: [] 103 | } 104 | }, 105 | events: { 106 | redrawChart () { 107 | this.drawChart() 108 | } 109 | }, 110 | mounted () { 111 | let self = this 112 | loadCharts(self.packages, self.version, self.mapsApiKey, self.language) 113 | .then(self.drawChart) 114 | .then(() => { 115 | // we don't want to bind props because it's a kind of "computed" property 116 | const watchProps = props 117 | delete watchProps.bounds 118 | 119 | // watching properties 120 | propsWatcher(self, watchProps) 121 | 122 | // binding events 123 | eventsBinder(self, self.chart, self.chartEvents) 124 | }) 125 | .catch((error) => { 126 | throw error 127 | }) 128 | }, 129 | methods: { 130 | /** 131 | * Initialize the datatable and add the initial data. 132 | * 133 | * @link https://developers.google.com/chart/interactive/docs/reference#DataTable 134 | * @return object 135 | */ 136 | buildDataTable () { 137 | let self = this 138 | 139 | let dataTable = new google.visualization.DataTable() 140 | 141 | _.each(self.columns, (value) => { 142 | dataTable.addColumn(value) 143 | }) 144 | 145 | if (!_.isEmpty(self.rows)) { 146 | dataTable.addRows(self.rows) 147 | } 148 | 149 | return dataTable 150 | }, 151 | 152 | /** 153 | * Update the datatable. 154 | * 155 | * @return void 156 | */ 157 | updateDataTable () { 158 | let self = this 159 | 160 | // Remove all data from the datatable. 161 | self.dataTable.removeRows(0, self.dataTable.getNumberOfRows()) 162 | self.dataTable.removeColumns(0, self.dataTable.getNumberOfColumns()) 163 | 164 | // Add 165 | _.each(self.columns, (value) => { 166 | self.dataTable.addColumn(value) 167 | }) 168 | 169 | if (!_.isEmpty(self.rows)) { 170 | self.dataTable.addRows(self.rows) 171 | } 172 | }, 173 | 174 | /** 175 | * Initialize the wrapper 176 | * 177 | * @link https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class 178 | * 179 | * @return object 180 | */ 181 | buildWrapper (chartType, dataTable, options, containerId) { 182 | let wrapper = new google.visualization.ChartWrapper({ 183 | chartType: chartType, 184 | dataTable: dataTable, 185 | options: options, 186 | containerId: containerId 187 | }) 188 | 189 | return wrapper 190 | }, 191 | 192 | /** 193 | * Build the chart. 194 | * 195 | * @return void 196 | */ 197 | buildChart () { 198 | let self = this 199 | 200 | // If dataTable isn't set, build it 201 | let dataTable = _.isEmpty(self.dataTable) ? self.buildDataTable() : self.dataTable 202 | 203 | self.wrapper = self.buildWrapper(self.chartType, dataTable, self.options, self.chartId) 204 | 205 | // Set the datatable on this instance 206 | self.dataTable = self.wrapper.getDataTable() 207 | 208 | // After chart is built, set it on this instance and resolve the promise. 209 | google.visualization.events.addOneTimeListener(self.wrapper, 'ready', () => { 210 | self.chart = self.wrapper.getChart() 211 | chartDeferred.resolve() 212 | }) 213 | }, 214 | 215 | /** 216 | * Draw the chart. 217 | * 218 | * @return Promise 219 | */ 220 | drawChart () { 221 | let self = this 222 | 223 | // We don't have any (usable) data, or we don't have columns. We can't draw a chart without those. 224 | if ((!_.isEmpty(self.rows) && !_.isObjectLike(self.rows)) || _.isEmpty(self.columns)) { 225 | return 226 | } 227 | 228 | if (_.isNull(self.chart)) { 229 | // We haven't built the chart yet, so JUST. DO. IT! 230 | self.buildChart() 231 | } else { 232 | // Chart already exists, just update the data 233 | self.updateDataTable() 234 | } 235 | 236 | // Chart has been built/Data has been updated, draw the chart. 237 | self.wrapper.draw() 238 | 239 | // Return promise. Resolves when chart finishes loading. 240 | return chartDeferred.promise 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Chart from './components/chart' 2 | 3 | function install (Vue, options = {}) { 4 | Vue.component('vue-chart', Chart) 5 | } 6 | 7 | export default install 8 | -------------------------------------------------------------------------------- /src/utils/eventsBinder.js: -------------------------------------------------------------------------------- 1 | export function eventsBinder (vue, googleChart, events) { 2 | // Loop through our events, create a listener for them, and 3 | // attach our callback function to that event. 4 | for (let event in events) { 5 | let eventName = event 6 | let eventCallback = events[event] 7 | 8 | if (eventName === 'ready') { 9 | // The chart is already ready, so this event missed it's chance. 10 | // We'll call it manually. 11 | eventCallback() 12 | } else { 13 | google.visualization.events.addListener(googleChart, eventName, eventCallback) 14 | } 15 | } 16 | } 17 | 18 | export default eventsBinder 19 | -------------------------------------------------------------------------------- /src/utils/googleChartsLoader.js: -------------------------------------------------------------------------------- 1 | import makeDeferred from './makeDeferred' 2 | let isLoading = false 3 | let isLoaded = false 4 | 5 | // Our main promise 6 | let googlePromise = makeDeferred() 7 | 8 | export function googleChartsLoader (packages = ['corechart'], version = 'current', mapsApiKey = false, language = 'en') { 9 | if (!Array.isArray(packages)) { 10 | throw new TypeError('packages must be an array') 11 | } 12 | 13 | if (version !== 'current' && typeof version !== 'number' && version !== 'upcoming') { 14 | throw new TypeError('version must be a number, "upcoming" or "current"') 15 | } 16 | 17 | // Google only lets you load it once, so we'll only run once. 18 | if (isLoading || isLoaded) { 19 | return googlePromise.promise 20 | } 21 | 22 | isLoading = true 23 | 24 | let script = document.createElement('script') 25 | script.setAttribute('src', 'https://www.gstatic.com/charts/loader.js') 26 | 27 | script.onreadystatechange = script.onload = () => { 28 | // After the 'loader.js' is loaded, load our version and packages 29 | var options = { 30 | packages: packages, 31 | language: language 32 | } 33 | 34 | if (mapsApiKey) { 35 | options['mapsApiKey'] = mapsApiKey 36 | } 37 | 38 | google.charts.load(version, options) 39 | 40 | // After we've loaded Google Charts, resolve our promise 41 | google.charts.setOnLoadCallback(() => { 42 | isLoading = false 43 | isLoaded = true 44 | googlePromise.resolve() 45 | }) 46 | } 47 | 48 | // Insert our script into the DOM 49 | document.getElementsByTagName('head')[0].appendChild(script) 50 | 51 | return googlePromise.promise 52 | } 53 | 54 | export default googleChartsLoader 55 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export {eventsBinder} from './eventsBinder' 2 | export {googleChartsLoader} from './googleChartsLoader' 3 | export {makeDeferred} from './makeDeferred' 4 | export {propsWatcher} from './propsWatcher' 5 | -------------------------------------------------------------------------------- /src/utils/makeDeferred.js: -------------------------------------------------------------------------------- 1 | /* 2 | This lets us resolve the promise outside the 3 | promise function itself. 4 | */ 5 | export function makeDeferred () { 6 | let resolvePromise = null 7 | let rejectPromise = null 8 | 9 | let promise = new Promise((resolve, reject) => { 10 | resolvePromise = resolve 11 | rejectPromise = reject 12 | }) 13 | 14 | return { 15 | promise: promise, 16 | resolve: resolvePromise, 17 | reject: rejectPromise 18 | } 19 | } 20 | 21 | export default makeDeferred 22 | -------------------------------------------------------------------------------- /src/utils/propsWatcher.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export function propsWatcher (vue, props) { 4 | /* 5 | Watch our props. Every time they change, redraw the chart. 6 | */ 7 | _.each(props, ({type: type}, attribute) => { 8 | vue.$watch(attribute, () => { 9 | vue.drawChart() 10 | }, { 11 | deep: _.isObject(type) 12 | }) 13 | }) 14 | } 15 | 16 | export default propsWatcher 17 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 'env': { 3 | 'browser': true, 4 | 'node': true, 5 | 'mocha': true 6 | }, 7 | 8 | 'globals': { 9 | '_': true, 10 | 'google': true, 11 | 'Vue': true, 12 | 'VueCharts': true, 13 | 'isIE': true, 14 | 'isIE9': true, 15 | 'describe': true, 16 | 'it': true, 17 | 'beforeEach': true, 18 | 'afterEach': true, 19 | 'expect': true, 20 | 'spyOn': true, 21 | 'wait': true 22 | }, 23 | 24 | 'extends': 'standard', 25 | 'rules': { 26 | 'padded-blocks': 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/unit/specs/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueCharts from 'src/main' 3 | 4 | Vue.use(VueCharts) 5 | 6 | require('./utils') 7 | -------------------------------------------------------------------------------- /test/unit/specs/utils.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { 4 | googleChartsLoader as loadCharts, 5 | makeDeferred 6 | } from 'src/utils/index' 7 | 8 | chai.use(chaiAsPromised) 9 | const expect = chai.expect 10 | 11 | describe('Utils', function () { 12 | 13 | /** 14 | * makeDeferred 15 | * 16 | */ 17 | describe('Making deferred promises', function () { 18 | let deferred 19 | 20 | beforeEach(() => { 21 | deferred = makeDeferred() 22 | }) 23 | 24 | it('should be a promise', () => { 25 | return expect(deferred.promise).to.be.a('Promise') 26 | }) 27 | 28 | it('can be resolved', () => { 29 | deferred.resolve() 30 | return expect(deferred.promise).to.eventually.be.fulfilled 31 | }) 32 | 33 | it('can be rejected', () => { 34 | deferred.reject() 35 | return expect(deferred.promise).to.eventually.be.rejected 36 | }) 37 | }) 38 | 39 | /** 40 | * googleChartsLoader 41 | * 42 | */ 43 | describe('Loading google charts library', function () { 44 | this.timeout(15000) // Give time for calls to Google API's and whatnot 45 | let chartsLoader 46 | 47 | beforeEach(() => { 48 | chartsLoader = loadCharts(['corechart'], 'current') 49 | }) 50 | 51 | it('packges must be an array', () => { 52 | return expect(() => loadCharts('corechart')).to.throw(TypeError) 53 | }) 54 | 55 | it('version must be a number, or "current"', () => { 56 | return expect(() => loadCharts(['corechart'], 'forty-three')).to.throw(TypeError) 57 | }) 58 | 59 | it('should be a promise', () => { 60 | return expect(chartsLoader).to.be.a('Promise') 61 | }) 62 | 63 | it('can be resolved', () => { 64 | return expect(chartsLoader).to.eventually.be.fulfilled 65 | }) 66 | }) 67 | }) 68 | --------------------------------------------------------------------------------