diff --git a/Dockerfile b/Dockerfile index 79d895c..bed6763 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN cd $(npm root -g)/npm \ && npm install fs-extra \ && sed -i -e s/graceful-fs/fs-extra/ -e s/fs\.rename/fs.move/ ./lib/utils/rename.js RUN npm install -g grunt-cli mocha-cli -RUN npm install chart.js@"<=2.4.*" +RUN npm install chart.js@"<=2.6.*" # Output debug logs in test output ENV DEBUG=chartjs-node* # FILES FOR BUILD diff --git a/README.md b/README.md index 5dbb046..b2b5b03 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,37 @@ var chartJsOptions = { type: 'pie', data: myChartData, options: myChartOptions -} +}; ``` -[Read here](http://www.chartjs.org/docs/#advanced-usage-creating-plugins) to see what plugins you can write. In the context of drawing static images, ``beforeDraw`` and/or ``afterDraw`` methods makes most sense to implement. +[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. [Read here](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D) to see which methods are available for the ``ctx`` object. + +## Adding custom charts + +To use custom charts, also use the ``options`` object to add your chart config and controller, like so: +```js +var myChartOptions = { + charts: [{ + type: 'custom', + baseType: 'bar', + controller: { + draw: function(ease) {}, + ... + }, + defaults: { + ... + }, + }] +} + +var chartJsOptions = { + type: 'custom', + data: myChartData, + options: myChartOptions +}; +``` + + +[Read here](http://www.chartjs.org/docs/latest/developers/charts.html) to see how to write custom charts. \ No newline at end of file diff --git a/index.js b/index.js index 90c2e3d..fd0c2dc 100644 --- a/index.js +++ b/index.js @@ -72,6 +72,16 @@ class ChartjsNode { if (configuration.options.plugins) { Chartjs.pluginService.register(configuration.options.plugins); } + if (configuration.options.charts) { + configuration.options.charts.forEach(chart => { + Chartjs.defaults[chart.type] = chart.defaults || {}; + if (chart.baseType) { + Chartjs.controllers[chart.type] = Chartjs.controllers[chart.baseType].extend(chart.controller); + } else { + Chartjs.controllers[chart.type] = Chartjs.DatasetController.extend(chart.controller); + } + }); + } this._disableDynamicChartjsSettings(configuration); this._canvas = BbPromise.promisifyAll(window.document.getElementById('myChart')); diff --git a/test/index.js b/test/index.js index e81323c..cfb91cd 100644 --- a/test/index.js +++ b/test/index.js @@ -8,130 +8,370 @@ const stream = require('stream'); /** * Schedule compute test. Tests a variety of schedules to validate correctness */ -function createChart() { - var chartNode = new ChartjsNode(600, 600); - return chartNode.drawChart({ - type: 'bar', - data: { - labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], - datasets: [{ - label: '# of Votes', - data: [12, 19, 3, 5, 2, 3], - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1 - }] - }, - options: { - responsive: false, - width: 400, - height: 400, - animation: false, - scales: { - yAxes: [{ - ticks: { - beginAtZero:true - } - }] - }, - tooltips: { - mode: 'label' - } - } - }) - .then(() => { - return chartNode; - }); -} describe('chartjs', function () { - describe('#destroy', function () { - it('should destroy the in-memory window', function () { - return createChart() - .then(chartNode => { - chartNode.destroy(); - // check if there are window properties to destroy from node global object - assert(!chartNode._windowPropertiesToDestroy); - assert(!chartNode._window); - debug('Sucessfully destroyed in-memory window properties'); + + function runScenarios(chartConfig) { + describe('#destroy', function () { + it('should destroy the in-memory window', function () { + var chartNode = new ChartjsNode(600, 600); + return chartNode.drawChart(chartConfig) + .then(() => { + chartNode.destroy(); + // check if there are window properties to destroy from node global object + assert(!chartNode._windowPropertiesToDestroy); + assert(!chartNode._window); + debug('Sucessfully destroyed in-memory window properties'); + }); }); }); - }); - describe('#drawChart', function () { - it('should draw the chart to a file', function () { - return createChart() - .then(chartNode => { - assert.ok(chartNode); - return chartNode.writeImageToFile('image/png', './testimage.png'); - }) - .then(() => { - assert(fs.existsSync('./testimage.png')); - // clean up - fs.unlinkSync('./testimage.png'); - debug('Sucessfully wrote image to a file'); + describe('#drawChart', function () { + it('should draw the chart to a file', function () { + var chartNode = new ChartjsNode(600, 600); + return chartNode.drawChart(chartConfig) + .then(() => { + assert.ok(chartNode); + return chartNode.writeImageToFile('image/png', './testimage.png'); + }) + .then(() => { + assert(fs.existsSync('./testimage.png')); + // clean up + fs.unlinkSync('./testimage.png'); + debug('Sucessfully wrote image to a file'); + }); }); - }); - it('should draw the chart to a buffer', function () { - return createChart() - .then(chartNode => { - assert.ok(chartNode); - return chartNode.getImageBuffer('image/png'); - }) - .then(buffer => { - assert(buffer.length > 1); - assert(buffer instanceof Buffer); - debug('Sucessfully wrote image to a Buffer'); + it('should draw the chart to a buffer', function () { + var chartNode = new ChartjsNode(600, 600); + return chartNode.drawChart(chartConfig) + .then(() => { + assert.ok(chartNode); + return chartNode.getImageBuffer('image/png'); + }) + .then(buffer => { + assert(buffer.length > 1); + assert(buffer instanceof Buffer); + debug('Sucessfully wrote image to a Buffer'); + }); }); - }); - it('should draw the chart to a stream', function () { - return createChart() - .then(chartNode => { - assert.ok(chartNode); - return chartNode.getImageStream('image/png'); - }) - .then(imageStream => { - assert(imageStream.stream instanceof stream.Readable); - var length = imageStream.length; - var readLength = 0; - return new Promise((resolve, reject) => { - imageStream.stream.on('data', d => { - readLength += d.length; - if (readLength === length) { - debug('Sucessfully wrote image to a Readable stream'); - resolve(); - } + it('should draw the chart to a stream', function () { + var chartNode = new ChartjsNode(600, 600); + return chartNode.drawChart(chartConfig) + .then(() => { + assert.ok(chartNode); + return chartNode.getImageStream('image/png'); + }) + .then(imageStream => { + assert(imageStream.stream instanceof stream.Readable); + var length = imageStream.length; + var readLength = 0; + return new Promise((resolve, reject) => { + imageStream.stream.on('data', d => { + readLength += d.length; + if (readLength === length) { + debug('Sucessfully wrote image to a Readable stream'); + resolve(); + } + }); + setTimeout(() => { + debug('length: ' + length); + debug('readLength: ' + readLength); + reject('Failed to read complete chart image stream in time'); + }, 1000); + }); }); - setTimeout(() => { - debug('length: ' + length); - debug('readLength: ' + readLength); - reject('Failed to read complete chart image stream in time'); - }, 1000); - }); }); - }); - it('should return the image as data url', function () { - return createChart() - .then(chartNode => { - assert.ok(chartNode); - return chartNode.getImageDataUrl('image/png'); - }) - .then(imageData => { - assert(imageData.length > 1); - debug('Sucessfully wrote image to a Readable stream'); + it('should return the image as data url', function () { + var chartNode = new ChartjsNode(600, 600); + return chartNode.drawChart(chartConfig) + .then(() => { + assert.ok(chartNode); + return chartNode.getImageDataUrl('image/png'); + }) + .then(imageData => { + assert(imageData.length > 1); + debug('Sucessfully wrote image to a Readable stream'); + }); }); }); + } + + var testPlugin = { + beforeDraw: function (chartController, easingValue, pluginOptions) { + var ctx = chartController.chart.ctx; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = '10px Georgia'; + ctx.fillText(pluginOptions.text, 0, 0); + return true; + } + }; + var testPluginOptions = { + testPlugin: { + text: 'Hello World' + } + }; + var testchart = { + type: 'custom', + controller: { + addElements: function () {}, + addElementAndReset: function () {}, + buildOrUpdateElements: function () {}, + getMeta: function () {}, + transition: function () {}, + initialize: function (chart) { + this._chart = chart; + }, + draw: function () { + var options = this._chart.config.options; + var ctx = this._chart.ctx; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = options.customFont; + ctx.fillText('Hello World!', 0, 0); + return true; + } + }, + defaults: { + customFont: '10px Georgia' + } + }; + + describe('with no charts or plugins,', function () { + + var chartConfig = { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: false, + width: 400, + height: 400, + animation: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + tooltips: { + mode: 'label' + } + } + }; + runScenarios(chartConfig); + }); + + describe('with no charts but plugins,', function () { + + var chartConfig = { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: false, + width: 400, + height: 400, + animation: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + tooltips: { + mode: 'label' + }, + plugins: testPluginOptions + }, + plugins: [testPlugin] + }; + runScenarios(chartConfig); + }); + + describe('with charts but no plugins,', function () { + + describe('bar chart', function () { + + var chartConfig = { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: false, + width: 400, + height: 400, + animation: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + tooltips: { + mode: 'label' + }, + charts: [testchart] + } + }; + runScenarios(chartConfig); + }); + + describe('custom chart', function () { + + var chartConfig = { + type: 'custom', + data: { + datasets: [{ + label: '', + data: [] + }] + }, + options: { + charts: [testchart] + } + }; + runScenarios(chartConfig); + }); + }); + + describe('with charts and plugins,', function () { + + describe('bar chart', function () { + + var chartConfig = { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255,99,132,1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + responsive: false, + width: 400, + height: 400, + animation: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + tooltips: { + mode: 'label' + }, + charts: [testchart], + plugins: testPluginOptions + }, + plugins: [testPlugin] + }; + runScenarios(chartConfig); + }); + + describe('custom chart', function () { + + var chartConfig = { + type: 'custom', + data: { + datasets: [{ + label: '', + data: [] + }] + }, + options: { + charts: [testchart], + plugins: testPluginOptions + }, + plugins: [testPlugin] + }; + runScenarios(chartConfig); + }); }); });