index.js
'use strict';
const BbPromise = require('bluebird');
const jsdom = BbPromise.promisifyAll(require('jsdom'));
const fs = BbPromise.promisifyAll(require('fs'));
const streamBuffers = require('stream-buffers');
const EventEmitter = require('events');
jsdom.defaultDocumentFeatures = {
FetchExternalResources: ['script'],
ProcessExternalResources: true
};
class ChartjsNode extends EventEmitter {
/**
* Creates an instance of ChartjsNode.
* @param {number} width The width of the chart canvas.
* @param {number} height The height of the chart canvas.
*/
constructor(width, height, devicePixelRatio) {
super();
this._width = width;
this._height = height;
this._devicePixelRatio = devicePixelRatio || 1;
}
/**
* @returns {Number} the width of the chart/canvas in pixels
*/
get width() {
return this._width;
}
/**
* @returns {Number} the height of the chart/canvas in pixels
*/
get height() {
return this._height;
}
_disableDynamicChartjsSettings(configuration) {
configuration.options.responsive = false;
configuration.options.animation = false;
configuration.options.width = this.width;
configuration.options.height = this.height;
}
/**
* Draws the chart given the Chart.js configuration
*
* @returns {Promise} A promise that will resolve when the chart is completed
*/
drawChart(configuration) {
// ensure we clean up any existing window if drawChart was called more than once.
this.destroy();
return jsdom.envAsync(
`<html>
<body>
<div id="chart-div" style="font-size:12; width:${this.width}; height:${this.height};">
<canvas id="myChart" width=${this.width} height=${this.height}></canvas>
</div>
</body>
</html>`,[]).then(window => {
window.devicePixelRatio = this._devicePixelRatio;
this._window = window;
const canvas = require('canvas');
const canvasMethods = ['HTMLCanvasElement'];
// adding window properties to global (only properties that are not already defined).
this._windowPropertiesToDestroy = [];
Object.keys(window).forEach(property => {
if (typeof global[property] === 'undefined') {
if (typeof global[property] === 'undefined') {
global[property] = window[property];
this._windowPropertiesToDestroy.push(property);
}
}
});
// adding all window.HTMLCanvasElement methods to global.HTMLCanvasElement
canvasMethods.forEach(method =>
global[method] = window[method]
);
global.CanvasRenderingContext2D = canvas.Context2d;
global.navigator = {
userAgent: 'node.js'
};
const Chartjs = require('chart.js');
this.emit('beforeDraw', Chartjs);
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 = window.document.getElementById('myChart');
this._ctx = this._canvas.getContext('2d');
this._chart = new Chartjs(this._ctx, configuration);
return this._chart;
});
}
/**
* Retrives the drawn chart as a stream
*
* @param {String} imageType The image type name. Valid values are image/png image/jpeg
* @returns {Stream} The image as an in-memory stream
*/
getImageStream(imageType) {
return this.getImageBuffer(imageType)
.then(buffer => {
var readableStream = new streamBuffers.ReadableStreamBuffer({
frequency: 10, // in milliseconds.
chunkSize: 2048 // in bytes.
});
readableStream.put(buffer);
readableStream.stop();
return {
stream: readableStream,
length: buffer.length
};
});
}
/**
* Retrives the drawn chart as a buffer
*
* @param {String} imageType The image type name. Valid values are image/png image/jpeg
* @returns {Array} The image as an in-memory buffer
*/
getImageBuffer(imageType) {
return new BbPromise((resolve, reject) => {
this._canvas.toBlob((blob, err) => {
if (err) {
return reject(err);
}
var buffer = jsdom.blobToBuffer(blob);
return resolve(buffer);
}, imageType);
});
}
/**
* Returns image in the form of Data Url
*
* @param {String} imageType The image type name. Valid values are image/png image/jpeg
* @returns {Promise} A promise that resolves when the image is received in the form of data url
*/
getImageDataUrl(imageType) {
return new BbPromise((resolve, reject) => {
this._canvas.toDataURL(imageType,(err, img) => {
if (err) {
return reject(err);
}
return resolve(img);
});
});
}
/**
* Writes chart to a file
*
* @param {String} imageType The image type name. Valid values are image/png image/jpeg
* @returns {Promise} A promise that resolves when the image is written to a file
*/
writeImageToFile(imageType, filePath) {
return this.getImageBuffer(imageType)
.then(buffer => {
return new BbPromise((resolve,reject) => {
var out = fs.createWriteStream(filePath);
out.on('finish', () => {
return resolve();
});
out.on('error', err => {
return reject(err);
});
out.write(buffer);
out.end();
});
});
}
/**
* Destroys the virtual DOM and canvas -- releasing any native resources
*/
destroy() {
if (this._chart) {
this._chart.destroy();
}
if (this._windowPropertiesToDestroy) {
this._windowPropertiesToDestroy.forEach((prop) => {
delete global[prop];
});
}
if (this._window) {
this._window.close();
delete this._window;
}
delete this._windowPropertiesToDestroy;
delete global.navigator;
delete global.CanvasRenderingContext2D;
}
}
module.exports = ChartjsNode;