├── .drone.yml ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── img ├── .DS_Store └── chartjsnode.png ├── index.js ├── package.json └── test └── index.js /.drone.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: sedouard/node-mongo:latest 3 | commands: 4 | - npm set registry http://10.0.0.5:4873/ 5 | - npm install 6 | environment: 7 | cache: 8 | mount: 9 | - node_modules 10 | - .git 11 | publish: 12 | npm: 13 | username: vmpowerci 14 | password: VMPOWER 15 | email: support@vmpower.io 16 | registry: "http://10.0.0.5:4873" 17 | always_auth: true 18 | when: 19 | branch: master 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowKeywords": [ 4 | "with" 5 | ], 6 | "disallowKeywordsOnNewLine": [ 7 | "else" 8 | ], 9 | "disallowMixedSpacesAndTabs": true, 10 | "disallowMultipleLineBreaks": true, 11 | "disallowNewlineBeforeBlockStatements": true, 12 | "disallowPaddingNewlinesInBlocks": false, 13 | "disallowQuotedKeysInObjects": true, 14 | "disallowSpaceAfterObjectKeys": true, 15 | "disallowSpaceAfterPrefixUnaryOperators": true, 16 | "disallowSpaceBeforeBinaryOperators": [ 17 | ], 18 | "disallowSpaceBeforePostfixUnaryOperators": true, 19 | "disallowSpacesInsideArrayBrackets": true, 20 | "disallowSpacesInsideObjectBrackets": "all", 21 | "disallowSpacesInsideParentheses": true, 22 | "disallowTrailingComma": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowYodaConditions": true, 25 | "requireBlocksOnNewline": 1, 26 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 27 | "requireCapitalizedConstructors": true, 28 | "requireCommaBeforeLineBreak": true, 29 | "requireCurlyBraces": [ 30 | "if", 31 | "else", 32 | "for", 33 | "while", 34 | "do", 35 | "try", 36 | "catch" 37 | ], 38 | "requireDotNotation": true, 39 | "requireLineFeedAtFileEnd": true, 40 | "requireParenthesesAroundIIFE": true, 41 | "requireSpaceAfterBinaryOperators": true, 42 | "requireSpaceAfterKeywords": [ 43 | "else", 44 | "for", 45 | "while", 46 | "do", 47 | "switch", 48 | "case", 49 | "return", 50 | "try", 51 | "function", 52 | "typeof" 53 | ], 54 | "requireSpaceAfterLineComment": "allowSlash", 55 | "requireSpaceBeforeBinaryOperators": true, 56 | "requireSpaceBeforeBlockStatements": true, 57 | "requireSpacesInConditionalExpression": true, 58 | "safeContextKeyword": [ 59 | "self" 60 | ], 61 | "validateIndentation": 4, 62 | "validateQuoteMarks": "'" 63 | } 64 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "node": true, 4 | "browser": true, 5 | "nomen": false, 6 | "bitwise": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "immed": true, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "plusplus": true, 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "trailing": true, 20 | "indent": 4, 21 | "onevar": true, 22 | "white": true, 23 | "quotmark": "single", 24 | "predef": { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | img 2 | 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: nodejs 4 | 5 | services: 6 | - docker 7 | 8 | before_install: 9 | - docker build -t sedouard/node-chartjs . 10 | 11 | script: 12 | - docker run sedouard/node-chartjs 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:5 2 | RUN apt-get update 3 | RUN apt-get install libcairo2-dev libpango1.0-dev libgif-dev libjpeg62-turbo-dev build-essential g++ -y 4 | # Fix bug https://github.com/npm/npm/issues/9863 5 | RUN cd $(npm root -g)/npm \ 6 | && npm install fs-extra \ 7 | && sed -i -e s/graceful-fs/fs-extra/ -e s/fs\.rename/fs.move/ ./lib/utils/rename.js 8 | RUN npm install -g grunt-cli mocha-cli 9 | RUN npm install chart.js@"<=2.6.*" 10 | # Output debug logs in test output 11 | ENV DEBUG=chartjs-node* 12 | # FILES FOR BUILD 13 | ADD ./test ./test 14 | ADD Gruntfile.js ./Gruntfile.js 15 | ADD ./package.json ./package.json 16 | ADD ./index.js ./index.js 17 | ADD ./.jshintrc ./.jshintrc 18 | ADD ./.jscsrc ./.jscsrc 19 | # END FILES FOR BUILD 20 | RUN npm install 21 | CMD grunt test 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt'); 2 | require('load-grunt-tasks')(grunt); 3 | 4 | var files = ['test/**/*.js', 'index.js']; 5 | 6 | grunt.initConfig({ 7 | mochacli: { 8 | options: { 9 | reporter: 'spec', 10 | bail: false 11 | }, 12 | all: ['test/*.js'] 13 | }, 14 | jshint: { 15 | files: files, 16 | options: { 17 | jshintrc: '.jshintrc' 18 | } 19 | }, 20 | jscs: { 21 | files: { 22 | src: files 23 | }, 24 | options: { 25 | config: '.jscsrc', 26 | esnext: true 27 | } 28 | }, 29 | jsbeautifier: { 30 | write: { 31 | files: { 32 | src: files 33 | }, 34 | options: { 35 | config: './.beautifyrc' 36 | } 37 | } 38 | } 39 | }); 40 | grunt.registerTask('test', ['mochacli', 'jshint', 'jscs']); 41 | grunt.registerTask('validate', ['jshint', 'jscs']); 42 | grunt.registerTask('beautify', ['jsbeautifier']); 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Steven Edouard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Chartjs Node Header Image](./img/chartjsnode.png)](http://chartjs-demo.vmpower.io/) 2 | [![Build Status](https://travis-ci.org/vmpowerio/chartjs-node.svg?branch=master)](https://travis-ci.org/vmpowerio/chartjs-node) 3 | [![Code Climate](https://codeclimate.com/github/vmpowerio/chartjs-node/badges/gpa.svg)](https://codeclimate.com/github/vmpowerio/chartjs-node) 4 | 5 | # Chartjs-Node 6 | 7 | A simple library to make it easy to create Chartjs charts in Node.js (server-side). 8 | 9 | This library puts together [jsdom](https://github.com/tmpvar/jsdom), [node-canvas](https://github.com/Automattic/node-canvas) and [chartjs](https://github.com/chartjs/Chart.js) to render Chartjs on the server. 10 | 11 | **[Live Demo](http://chartjs-demo.vmpower.io)** 12 | 13 | ## Getting Started 14 | 15 | ### Peer Dependencies 16 | 17 | You'll need to npm install `chart.js`. This library will pick up the exact version you end up installing. 18 | 19 | ### Cairo 20 | 21 | Before installing this library you'll need to install Cairo for your system. The instructions for the most common platforms can be found [here](https://github.com/Automattic/node-canvas#installation). 22 | 23 | Now you're ready to install the package: 24 | 25 | ``` 26 | npm install chartjs-node 27 | ``` 28 | 29 | ## Creating a Chart 30 | 31 | ```js 32 | const ChartjsNode = require('chartjs-node'); 33 | // 600x600 canvas size 34 | var chartNode = new ChartjsNode(600, 600); 35 | return chartNode.drawChart(chartJsOptions) 36 | .then(() => { 37 | // chart is created 38 | 39 | // get image as png buffer 40 | return chartNode.getImageBuffer('image/png'); 41 | }) 42 | .then(buffer => { 43 | Array.isArray(buffer) // => true 44 | // as a stream 45 | return chartNode.getImageStream('image/png'); 46 | }) 47 | .then(streamResult => { 48 | // using the length property you can do things like 49 | // directly upload the image to s3 by using the 50 | // stream and length properties 51 | streamResult.stream // => Stream object 52 | streamResult.length // => Integer length of stream 53 | // write to a file 54 | return chartNode.writeImageToFile('image/png', './testimage.png'); 55 | }) 56 | .then(() => { 57 | // chart is now written to the file path 58 | // ./testimage.png 59 | }); 60 | ``` 61 | 62 | ## Destroying the Chart 63 | 64 | Each time you create a chart, you will create a new virtual browser `window`. You should call the `destroy` 65 | method to release the native resources or you may leak memory: 66 | 67 | ``` 68 | chartNode.destroy(); 69 | ``` 70 | 71 | ## Global chart reference 72 | 73 | You can access and modify the ChartJS reference before a chart is drawn via an event (`beforeDraw`). ChartjsNode extends [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). 74 | 75 | ```js 76 | var chartNode = new ChartjsNode(600, 600); 77 | chartNode.on('beforeDraw', function (Chartjs) { 78 | //Chartjs.defaults 79 | //Chartjs.pluginService 80 | //Chartjs.scaleService 81 | //Chartjs.layoutService 82 | //Chartjs.helpers 83 | //Chartjs.controllers 84 | //etc 85 | }); 86 | chartNode.drawChart(chartJsOptions) //beforeDraw is called in here 87 | ... 88 | ``` 89 | 90 | ## Adding draw plugins 91 | 92 | To use draw plugins, simply use the ``options`` object to add your plugins, like so: 93 | ```js 94 | var myChartOptions = { 95 | plugins: { 96 | afterDraw: function (chart, easing) { 97 | var self = chart.config; /* Configuration object containing type, data, options */ 98 | var ctx = chart.chart.ctx; /* Canvas context used to draw with */ 99 | ... 100 | } 101 | } 102 | } 103 | 104 | var chartJsOptions = { 105 | type: 'pie', 106 | data: myChartData, 107 | options: myChartOptions 108 | }; 109 | ``` 110 | 111 | [Read here](http://www.chartjs.org/docs/latest/developers/plugins.html) to see what plugins you can write. In the context of drawing static images, ``beforeDraw`` and/or ``afterDraw`` methods makes most sense to implement. 112 | 113 | [Read here](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D) to see which methods are available for the ``ctx`` object. 114 | 115 | ## Adding custom charts 116 | 117 | To use custom charts, also use the ``options`` object to add your chart config and controller, like so: 118 | ```js 119 | var myChartOptions = { 120 | charts: [{ 121 | type: 'custom', 122 | baseType: 'bar', 123 | controller: { 124 | draw: function (ease) {}, 125 | ... 126 | }, 127 | defaults: { 128 | ... 129 | }, 130 | }] 131 | } 132 | 133 | var chartJsOptions = { 134 | type: 'custom', 135 | data: myChartData, 136 | options: myChartOptions 137 | }; 138 | ``` 139 | 140 | [Read here](http://www.chartjs.org/docs/latest/developers/charts.html) to see how to write custom charts. 141 | -------------------------------------------------------------------------------- /img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmpowerio/chartjs-node/8c10855d4c613d860363ce60bd58d822e5846242/img/.DS_Store -------------------------------------------------------------------------------- /img/chartjsnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmpowerio/chartjs-node/8c10855d4c613d860363ce60bd58d822e5846242/img/chartjsnode.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const BbPromise = require('bluebird'); 3 | const jsdom = BbPromise.promisifyAll(require('jsdom')); 4 | const fs = BbPromise.promisifyAll(require('fs')); 5 | const streamBuffers = require('stream-buffers'); 6 | const EventEmitter = require('events'); 7 | jsdom.defaultDocumentFeatures = { 8 | FetchExternalResources: ['script'], 9 | ProcessExternalResources: true 10 | }; 11 | 12 | class ChartjsNode extends EventEmitter { 13 | /** 14 | * Creates an instance of ChartjsNode. 15 | * @param {number} width The width of the chart canvas. 16 | * @param {number} height The height of the chart canvas. 17 | */ 18 | constructor(width, height, devicePixelRatio) { 19 | super(); 20 | this._width = width; 21 | this._height = height; 22 | this._devicePixelRatio = devicePixelRatio || 1; 23 | } 24 | /** 25 | * @returns {Number} the width of the chart/canvas in pixels 26 | */ 27 | get width() { 28 | return this._width; 29 | } 30 | /** 31 | * @returns {Number} the height of the chart/canvas in pixels 32 | */ 33 | get height() { 34 | return this._height; 35 | } 36 | _disableDynamicChartjsSettings(configuration) { 37 | configuration.options.responsive = false; 38 | configuration.options.animation = false; 39 | configuration.options.width = this.width; 40 | configuration.options.height = this.height; 41 | } 42 | /** 43 | * Draws the chart given the Chart.js configuration 44 | * 45 | * @returns {Promise} A promise that will resolve when the chart is completed 46 | */ 47 | drawChart(configuration) { 48 | // ensure we clean up any existing window if drawChart was called more than once. 49 | this.destroy(); 50 | return jsdom.envAsync( 51 | ` 52 | 53 |
54 | 55 |
56 | 57 | `,[]).then(window => { 58 | 59 | window.devicePixelRatio = this._devicePixelRatio; 60 | this._window = window; 61 | const canvas = require('canvas'); 62 | const canvasMethods = ['HTMLCanvasElement']; 63 | 64 | // adding window properties to global (only properties that are not already defined). 65 | this._windowPropertiesToDestroy = []; 66 | Object.keys(window).forEach(property => { 67 | if (typeof global[property] === 'undefined') { 68 | if (typeof global[property] === 'undefined') { 69 | global[property] = window[property]; 70 | this._windowPropertiesToDestroy.push(property); 71 | } 72 | } 73 | }); 74 | 75 | // adding all window.HTMLCanvasElement methods to global.HTMLCanvasElement 76 | canvasMethods.forEach(method => 77 | global[method] = window[method] 78 | ); 79 | 80 | global.CanvasRenderingContext2D = canvas.Context2d; 81 | 82 | global.navigator = { 83 | userAgent: 'node.js' 84 | }; 85 | 86 | const Chartjs = require('chart.js'); 87 | this.emit('beforeDraw', Chartjs); 88 | if (configuration.options.plugins) { 89 | Chartjs.pluginService.register(configuration.options.plugins); 90 | } 91 | if (configuration.options.charts) { 92 | configuration.options.charts.forEach(chart => { 93 | Chartjs.defaults[chart.type] = chart.defaults || {}; 94 | if (chart.baseType) { 95 | Chartjs.controllers[chart.type] = Chartjs.controllers[chart.baseType].extend(chart.controller); 96 | } else { 97 | Chartjs.controllers[chart.type] = Chartjs.DatasetController.extend(chart.controller); 98 | } 99 | }); 100 | } 101 | 102 | this._disableDynamicChartjsSettings(configuration); 103 | this._canvas = window.document.getElementById('myChart'); 104 | this._ctx = this._canvas.getContext('2d'); 105 | 106 | this._chart = new Chartjs(this._ctx, configuration); 107 | return this._chart; 108 | }); 109 | 110 | } 111 | /** 112 | * Retrives the drawn chart as a stream 113 | * 114 | * @param {String} imageType The image type name. Valid values are image/png image/jpeg 115 | * @returns {Stream} The image as an in-memory stream 116 | */ 117 | getImageStream(imageType) { 118 | return this.getImageBuffer(imageType) 119 | .then(buffer => { 120 | var readableStream = new streamBuffers.ReadableStreamBuffer({ 121 | frequency: 10, // in milliseconds. 122 | chunkSize: 2048 // in bytes. 123 | }); 124 | readableStream.put(buffer); 125 | readableStream.stop(); 126 | return { 127 | stream: readableStream, 128 | length: buffer.length 129 | }; 130 | }); 131 | } 132 | /** 133 | * Retrives the drawn chart as a buffer 134 | * 135 | * @param {String} imageType The image type name. Valid values are image/png image/jpeg 136 | * @returns {Array} The image as an in-memory buffer 137 | */ 138 | getImageBuffer(imageType) { 139 | return new BbPromise((resolve, reject) => { 140 | this._canvas.toBlob((blob, err) => { 141 | if (err) { 142 | return reject(err); 143 | } 144 | var buffer = jsdom.blobToBuffer(blob); 145 | return resolve(buffer); 146 | }, imageType); 147 | }); 148 | } 149 | /** 150 | * Returns image in the form of Data Url 151 | * 152 | * @param {String} imageType The image type name. Valid values are image/png image/jpeg 153 | * @returns {Promise} A promise that resolves when the image is received in the form of data url 154 | */ 155 | getImageDataUrl(imageType) { 156 | return new BbPromise((resolve, reject) => { 157 | this._canvas.toDataURL(imageType,(err, img) => { 158 | if (err) { 159 | return reject(err); 160 | } 161 | return resolve(img); 162 | }); 163 | }); 164 | } 165 | /** 166 | * Writes chart to a file 167 | * 168 | * @param {String} imageType The image type name. Valid values are image/png image/jpeg 169 | * @returns {Promise} A promise that resolves when the image is written to a file 170 | */ 171 | writeImageToFile(imageType, filePath) { 172 | return this.getImageBuffer(imageType) 173 | .then(buffer => { 174 | return new BbPromise((resolve,reject) => { 175 | var out = fs.createWriteStream(filePath); 176 | out.on('finish', () => { 177 | return resolve(); 178 | }); 179 | out.on('error', err => { 180 | return reject(err); 181 | }); 182 | out.write(buffer); 183 | out.end(); 184 | }); 185 | }); 186 | } 187 | /** 188 | * Destroys the virtual DOM and canvas -- releasing any native resources 189 | */ 190 | destroy() { 191 | if (this._chart) { 192 | this._chart.destroy(); 193 | } 194 | 195 | if (this._windowPropertiesToDestroy) { 196 | this._windowPropertiesToDestroy.forEach((prop) => { 197 | delete global[prop]; 198 | }); 199 | } 200 | 201 | if (this._window) { 202 | this._window.close(); 203 | delete this._window; 204 | } 205 | 206 | delete this._windowPropertiesToDestroy; 207 | delete global.navigator; 208 | delete global.CanvasRenderingContext2D; 209 | } 210 | } 211 | module.exports = ChartjsNode; 212 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chartjs-node", 3 | "version": "1.7.1", 4 | "description": "Create Chart.js chart images server-side", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vmpowerio/chartjs-node.git" 15 | }, 16 | "keywords": [ 17 | "Chartjs", 18 | "Charts", 19 | "Plots", 20 | "Graphs" 21 | ], 22 | "author": "sedouard", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/vmpowerio/chartjs-node/issues" 26 | }, 27 | "homepage": "https://github.com/vmpowerio/chartjs-node#readme", 28 | "dependencies": { 29 | "bluebird": "^3.4.6", 30 | "canvas": "1.6.*", 31 | "debug": "^2.2.0", 32 | "jsdom": "9.8.*", 33 | "stream-buffers": "^3.0.1" 34 | }, 35 | "peerDependencies": { 36 | "chart.js": "<=2.7.*" 37 | }, 38 | "devDependencies": { 39 | "grunt": "^1.0.1", 40 | "grunt-contrib-jshint": "^1.0.0", 41 | "grunt-jscs": "^2.8.0", 42 | "grunt-mocha-cli": "^2.1.0", 43 | "load-grunt-tasks": "^3.5.2", 44 | "mocha": "^3.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it*/ 2 | 'use strict'; 3 | const debug = require('debug')('chartjs-node:test'); 4 | const assert = require('assert'); 5 | const ChartjsNode = require('../index.js'); 6 | const fs = require('fs'); 7 | const stream = require('stream'); 8 | /** 9 | * Schedule compute test. Tests a variety of schedules to validate correctness 10 | */ 11 | describe('chartjs', function () { 12 | 13 | function runScenarios(chartConfig) { 14 | describe('#destroy', function () { 15 | it('should destroy the in-memory window', function () { 16 | var chartNode = new ChartjsNode(600, 600); 17 | return chartNode.drawChart(chartConfig) 18 | .then(() => { 19 | chartNode.destroy(); 20 | // check if there are window properties to destroy from node global object 21 | assert(!chartNode._windowPropertiesToDestroy); 22 | assert(!chartNode._window); 23 | debug('Sucessfully destroyed in-memory window properties'); 24 | }); 25 | }); 26 | }); 27 | describe('#drawChart', function () { 28 | it('should draw the chart to a file', function () { 29 | var chartNode = new ChartjsNode(600, 600); 30 | return chartNode.drawChart(chartConfig) 31 | .then(() => { 32 | assert.ok(chartNode); 33 | return chartNode.writeImageToFile('image/png', './testimage.png'); 34 | }) 35 | .then(() => { 36 | assert(fs.existsSync('./testimage.png')); 37 | // clean up 38 | fs.unlinkSync('./testimage.png'); 39 | debug('Sucessfully wrote image to a file'); 40 | }); 41 | }); 42 | it('should draw the chart to a buffer', function () { 43 | var chartNode = new ChartjsNode(600, 600); 44 | return chartNode.drawChart(chartConfig) 45 | .then(() => { 46 | assert.ok(chartNode); 47 | return chartNode.getImageBuffer('image/png'); 48 | }) 49 | .then(buffer => { 50 | assert(buffer.length > 1); 51 | assert(buffer instanceof Buffer); 52 | debug('Sucessfully wrote image to a Buffer'); 53 | }); 54 | }); 55 | it('should draw the chart to a stream', function () { 56 | var chartNode = new ChartjsNode(600, 600); 57 | return chartNode.drawChart(chartConfig) 58 | .then(() => { 59 | assert.ok(chartNode); 60 | return chartNode.getImageStream('image/png'); 61 | }) 62 | .then(imageStream => { 63 | assert(imageStream.stream instanceof stream.Readable); 64 | var length = imageStream.length; 65 | var readLength = 0; 66 | return new Promise((resolve, reject) => { 67 | imageStream.stream.on('data', d => { 68 | readLength += d.length; 69 | if (readLength === length) { 70 | debug('Sucessfully wrote image to a Readable stream'); 71 | resolve(); 72 | } 73 | }); 74 | setTimeout(() => { 75 | debug('length: ' + length); 76 | debug('readLength: ' + readLength); 77 | reject('Failed to read complete chart image stream in time'); 78 | }, 1000); 79 | }); 80 | }); 81 | }); 82 | it('should return the image as data url', function () { 83 | var chartNode = new ChartjsNode(600, 600); 84 | return chartNode.drawChart(chartConfig) 85 | .then(() => { 86 | assert.ok(chartNode); 87 | return chartNode.getImageDataUrl('image/png'); 88 | }) 89 | .then(imageData => { 90 | assert(imageData.length > 1); 91 | debug('Sucessfully wrote image to a Readable stream'); 92 | }); 93 | }); 94 | }); 95 | describe('beforeDraw event', function () { 96 | it('should provide a reference to global Chartjs instance', function () { 97 | var Chartjs = null; 98 | var chartNode = new ChartjsNode(600, 600); 99 | chartNode.on('beforeDraw', function (c) { 100 | Chartjs = c; 101 | }); 102 | return chartNode.drawChart(chartConfig) 103 | .then(() => { 104 | assert.ok(Chartjs); 105 | chartNode.destroy(); 106 | debug('Sucessfully emitted beforeDraw event'); 107 | }); 108 | }); 109 | }); 110 | } 111 | 112 | var testPlugin = { 113 | beforeDraw: function (chartController, easingValue, pluginOptions) { 114 | var ctx = chartController.chart.ctx; 115 | ctx.textAlign = 'center'; 116 | ctx.textBaseline = 'middle'; 117 | ctx.font = '10px Georgia'; 118 | ctx.fillText(pluginOptions.text, 0, 0); 119 | return true; 120 | } 121 | }; 122 | var testPluginOptions = { 123 | testPlugin: { 124 | text: 'Hello World' 125 | } 126 | }; 127 | var testchart = { 128 | type: 'custom', 129 | controller: { 130 | addElements: function () {}, 131 | addElementAndReset: function () {}, 132 | buildOrUpdateElements: function () {}, 133 | getMeta: function () {}, 134 | transition: function () {}, 135 | initialize: function (chart) { 136 | this._chart = chart; 137 | }, 138 | draw: function () { 139 | var options = this._chart.config.options; 140 | var ctx = this._chart.ctx; 141 | ctx.textAlign = 'center'; 142 | ctx.textBaseline = 'middle'; 143 | ctx.font = options.customFont; 144 | ctx.fillText('Hello World!', 0, 0); 145 | return true; 146 | } 147 | }, 148 | defaults: { 149 | customFont: '10px Georgia' 150 | } 151 | }; 152 | 153 | describe('with no charts or plugins,', function () { 154 | 155 | var chartConfig = { 156 | type: 'bar', 157 | data: { 158 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 159 | datasets: [{ 160 | label: '# of Votes', 161 | data: [12, 19, 3, 5, 2, 3], 162 | backgroundColor: [ 163 | 'rgba(255, 99, 132, 0.2)', 164 | 'rgba(54, 162, 235, 0.2)', 165 | 'rgba(255, 206, 86, 0.2)', 166 | 'rgba(75, 192, 192, 0.2)', 167 | 'rgba(153, 102, 255, 0.2)', 168 | 'rgba(255, 159, 64, 0.2)' 169 | ], 170 | borderColor: [ 171 | 'rgba(255,99,132,1)', 172 | 'rgba(54, 162, 235, 1)', 173 | 'rgba(255, 206, 86, 1)', 174 | 'rgba(75, 192, 192, 1)', 175 | 'rgba(153, 102, 255, 1)', 176 | 'rgba(255, 159, 64, 1)' 177 | ], 178 | borderWidth: 1 179 | }] 180 | }, 181 | options: { 182 | responsive: false, 183 | width: 400, 184 | height: 400, 185 | animation: false, 186 | scales: { 187 | yAxes: [{ 188 | ticks: { 189 | beginAtZero: true 190 | } 191 | }] 192 | }, 193 | tooltips: { 194 | mode: 'label' 195 | } 196 | } 197 | }; 198 | runScenarios(chartConfig); 199 | }); 200 | 201 | describe('with no charts but plugins,', function () { 202 | 203 | var chartConfig = { 204 | type: 'bar', 205 | data: { 206 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 207 | datasets: [{ 208 | label: '# of Votes', 209 | data: [12, 19, 3, 5, 2, 3], 210 | backgroundColor: [ 211 | 'rgba(255, 99, 132, 0.2)', 212 | 'rgba(54, 162, 235, 0.2)', 213 | 'rgba(255, 206, 86, 0.2)', 214 | 'rgba(75, 192, 192, 0.2)', 215 | 'rgba(153, 102, 255, 0.2)', 216 | 'rgba(255, 159, 64, 0.2)' 217 | ], 218 | borderColor: [ 219 | 'rgba(255,99,132,1)', 220 | 'rgba(54, 162, 235, 1)', 221 | 'rgba(255, 206, 86, 1)', 222 | 'rgba(75, 192, 192, 1)', 223 | 'rgba(153, 102, 255, 1)', 224 | 'rgba(255, 159, 64, 1)' 225 | ], 226 | borderWidth: 1 227 | }] 228 | }, 229 | options: { 230 | responsive: false, 231 | width: 400, 232 | height: 400, 233 | animation: false, 234 | scales: { 235 | yAxes: [{ 236 | ticks: { 237 | beginAtZero: true 238 | } 239 | }] 240 | }, 241 | tooltips: { 242 | mode: 'label' 243 | }, 244 | plugins: testPluginOptions 245 | }, 246 | plugins: [testPlugin] 247 | }; 248 | runScenarios(chartConfig); 249 | }); 250 | 251 | describe('with charts but no plugins,', function () { 252 | 253 | describe('bar chart', function () { 254 | 255 | var chartConfig = { 256 | type: 'bar', 257 | data: { 258 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 259 | datasets: [{ 260 | label: '# of Votes', 261 | data: [12, 19, 3, 5, 2, 3], 262 | backgroundColor: [ 263 | 'rgba(255, 99, 132, 0.2)', 264 | 'rgba(54, 162, 235, 0.2)', 265 | 'rgba(255, 206, 86, 0.2)', 266 | 'rgba(75, 192, 192, 0.2)', 267 | 'rgba(153, 102, 255, 0.2)', 268 | 'rgba(255, 159, 64, 0.2)' 269 | ], 270 | borderColor: [ 271 | 'rgba(255,99,132,1)', 272 | 'rgba(54, 162, 235, 1)', 273 | 'rgba(255, 206, 86, 1)', 274 | 'rgba(75, 192, 192, 1)', 275 | 'rgba(153, 102, 255, 1)', 276 | 'rgba(255, 159, 64, 1)' 277 | ], 278 | borderWidth: 1 279 | }] 280 | }, 281 | options: { 282 | responsive: false, 283 | width: 400, 284 | height: 400, 285 | animation: false, 286 | scales: { 287 | yAxes: [{ 288 | ticks: { 289 | beginAtZero: true 290 | } 291 | }] 292 | }, 293 | tooltips: { 294 | mode: 'label' 295 | }, 296 | charts: [testchart] 297 | } 298 | }; 299 | runScenarios(chartConfig); 300 | }); 301 | 302 | describe('custom chart', function () { 303 | 304 | var chartConfig = { 305 | type: 'custom', 306 | data: { 307 | datasets: [{ 308 | label: '', 309 | data: [] 310 | }] 311 | }, 312 | options: { 313 | charts: [testchart] 314 | } 315 | }; 316 | runScenarios(chartConfig); 317 | }); 318 | }); 319 | 320 | describe('with charts and plugins,', function () { 321 | 322 | describe('bar chart', function () { 323 | 324 | var chartConfig = { 325 | type: 'bar', 326 | data: { 327 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 328 | datasets: [{ 329 | label: '# of Votes', 330 | data: [12, 19, 3, 5, 2, 3], 331 | backgroundColor: [ 332 | 'rgba(255, 99, 132, 0.2)', 333 | 'rgba(54, 162, 235, 0.2)', 334 | 'rgba(255, 206, 86, 0.2)', 335 | 'rgba(75, 192, 192, 0.2)', 336 | 'rgba(153, 102, 255, 0.2)', 337 | 'rgba(255, 159, 64, 0.2)' 338 | ], 339 | borderColor: [ 340 | 'rgba(255,99,132,1)', 341 | 'rgba(54, 162, 235, 1)', 342 | 'rgba(255, 206, 86, 1)', 343 | 'rgba(75, 192, 192, 1)', 344 | 'rgba(153, 102, 255, 1)', 345 | 'rgba(255, 159, 64, 1)' 346 | ], 347 | borderWidth: 1 348 | }] 349 | }, 350 | options: { 351 | responsive: false, 352 | width: 400, 353 | height: 400, 354 | animation: false, 355 | scales: { 356 | yAxes: [{ 357 | ticks: { 358 | beginAtZero: true 359 | } 360 | }] 361 | }, 362 | tooltips: { 363 | mode: 'label' 364 | }, 365 | charts: [testchart], 366 | plugins: testPluginOptions 367 | }, 368 | plugins: [testPlugin] 369 | }; 370 | runScenarios(chartConfig); 371 | }); 372 | 373 | describe('custom chart', function () { 374 | 375 | var chartConfig = { 376 | type: 'custom', 377 | data: { 378 | datasets: [{ 379 | label: '', 380 | data: [] 381 | }] 382 | }, 383 | options: { 384 | charts: [testchart], 385 | plugins: testPluginOptions 386 | }, 387 | plugins: [testPlugin] 388 | }; 389 | runScenarios(chartConfig); 390 | }); 391 | }); 392 | }); 393 | --------------------------------------------------------------------------------