import { Readable } from 'stream' ; import { Chart as ChartJS , ChartConfiguration , ChartComponentLike } from 'chart.js' ; import { createCanvas , registerFont , Image } from 'canvas' ; import { freshRequire } from './freshRequire' ; import { BackgroundColourPlugin } from './backgroundColourPlugin' ; export type ChartJSNodeCanvasPlugins = { /**
     * Global plugins, see https://www.chartjs.org/docs/latest/developers/plugins.html.
     */ readonly modern ? : ReadonlyArray < string | ChartComponentLike > ; /**
     * This will work for plugins that `require` ChartJS themselves.
     */ readonly requireChartJSLegacy ? : ReadonlyArray < string > ; /**
     * This should work for any plugin that expects a global Chart variable.
     */ readonly globalVariableLegacy ? : ReadonlyArray < string > ; /**
     * This will work with plugins that just return a plugin object and do no specific loading themselves.
     */ readonly requireLegacy ? : ReadonlyArray < string > ; } ; export type ChartCallback = ( chartJS : typeof ChartJS ) => void | Promise < void > ; export type CanvasType = 'pdf' | 'svg' ; export type MimeType = 'image/png' | 'image/jpeg' ; type Canvas = HTMLCanvasElement & { toBuffer ( callback , mimeType ? , config ? ) : void ; toBuffer ( mimeType ? , config ? ) : Buffer ; createPNGStream ( config ? ) : Readable ; createJPEGStream ( config ? ) : Readable ; createPDFStream ( config ? ) : Readable ; } ; export interface ChartJSNodeCanvasOptions { /**
     * The width of the charts to render, in pixels.
     */ readonly width : number ; /**
     * The height of the charts to render, in pixels.
     */ readonly height : number ; /**
     * Optional callback which is called once with a new ChartJS global reference as the only parameter.
     */ readonly chartCallback ? : ChartCallback ; /**
     * Optional canvas type ('PDF' or 'SVG'), see the [canvas pdf doc](https://github.com/Automattic/node-canvas#pdf-output-support).
     */ readonly type ? : CanvasType ; /**
     * Optional plugins to register.
     */ readonly plugins ? : ChartJSNodeCanvasPlugins ; /**
     * Optional background color for the chart, otherwise it will be transparent. Note, this will apply to all charts. See the [fillStyle](https://www.w3schools.com/tags/canvas_fillstyle.asp) canvas API used for possible values.
     */ readonly backgroundColour ? : string ; } export class ChartJSNodeCanvas { private readonly _width : number ; private readonly _height : number ; private readonly _chartJs : typeof ChartJS ; private readonly _createCanvas : typeof createCanvas ; private readonly _registerFont : typeof registerFont ; private readonly _image : typeof Image ; private readonly _type ? : CanvasType ; param options constructor ( options ) { if ( options === null || typeof ( options ) !== 'object' ) { throw new Error ( 'An options parameter object is required' ) ; } if ( ! options . width || typeof ( options . width ) !== 'number' ) { throw new Error ( 'A width option is required' ) ; } if ( ! options . height || typeof ( options . height ) !== 'number' ) { throw new Error ( 'A height option is required' ) ; } this . _width = options . width ; this . _height = options . height ; const canvas = freshRequire ( 'canvas' ) ; this . _createCanvas = canvas . createCanvas ; this . _registerFont = canvas . registerFont ; this . _image = canvas . Image ; this . _type = options . type && options . type . toLowerCase ( ) as CanvasType ; this . _chartJs = this . initialize ( options ) ; } see https param configuration param mimeType public renderToDataURL ( configuration , mimeType = 'image/png' ) { const chart = this . renderChart ( configuration ) ; return new Promise < string > ( ( resolve , reject ) => { if ( ! chart . canvas ) { return reject ( new Error ( 'Canvas is null' ) ) ; } const canvas = chart . canvas as Canvas ; canvas . toDataURL ( mimeType , ( error , png ) => { chart . destroy ( ) ; if ( error ) { return reject ( error ) ; } return resolve ( png ) ; } ) ; } ) ; } see https param configuration param mimeType public renderToDataURLSync ( configuration , mimeType = 'image/png' ) { const chart = this . renderChart ( configuration ) ; if ( ! chart . canvas ) { throw new Error ( 'Canvas is null' ) ; } const canvas = chart . canvas as Canvas ; const dataUrl = canvas . toDataURL ( mimeType ) ; chart . destroy ( ) ; return dataUrl ; } see https param configuration param mimeType public renderToBuffer ( configuration , mimeType = 'image/png' ) { const chart = this . renderChart ( configuration ) ; return new Promise < Buffer > ( ( resolve , reject ) => { if ( ! chart . canvas ) { throw new Error ( 'Canvas is null' ) ; } const canvas = chart . canvas as Canvas ; canvas . toBuffer ( ( error , buffer ) => { chart . destroy ( ) ; if ( error ) { return reject ( error ) ; } return resolve ( buffer ) ; } , mimeType ) ; } ) ; } see https param configuration param mimeType public renderToBufferSync ( configuration , mimeType = 'image/png' ) { const chart = this . renderChart ( configuration ) ; if ( ! chart . canvas ) { throw new Error ( 'Canvas is null' ) ; } const canvas = chart . canvas as Canvas ; const buffer = canvas . toBuffer ( mimeType ) ; chart . destroy ( ) ; return buffer ; } see https param configuration param mimeType public renderToStream ( configuration , mimeType = 'image/png' ) { const chart = this . renderChart ( configuration ) ; if ( ! chart . canvas ) { throw new Error ( 'Canvas is null' ) ; } const canvas = chart . canvas as Canvas ; setImmediate ( ( ) => chart . destroy ( ) ) ; switch ( mimeType ) { case 'image/png' : return canvas . createPNGStream ( ) ; case 'image/jpeg' : return canvas . createJPEGStream ( ) ; case 'application/pdf' : return canvas . createPDFStream ( ) ; default : throw new Error ( `Un-handled mimeType: ${ mimeType }` ) ; } } param path param options example public registerFont ( path , options ) { this . _registerFont ( path , options ) ; } private initialize ( options ) { const chartJs = require ( 'chart.js' ) ; if ( options . plugins ?. requireChartJSLegacy ) { for ( const plugin of options . plugins . requireChartJSLegacy ) { require ( plugin ) ; delete require . cache [ require . resolve ( plugin ) ] ; } } if ( options . plugins ?. globalVariableLegacy ) { ( global as any ) . Chart = chartJs ; for ( const plugin of options . plugins . globalVariableLegacy ) { freshRequire ( plugin ) ; } delete ( global as any ) . Chart ; } if ( options . plugins ?. modern ) { for ( const plugin of options . plugins . modern ) { if ( typeof plugin === 'string' ) { chartJs . register ( freshRequire ( plugin ) ) ; } else { chartJs . register ( plugin ) ; } } } if ( options . plugins ?. requireLegacy ) { for ( const plugin of options . plugins . requireLegacy ) { chartJs . register ( freshRequire ( plugin ) ) ; } } if ( options . chartCallback ) { options . chartCallback ( chartJs ) ; } if ( options . backgroundColour ) { chartJs . register ( new BackgroundColourPlugin ( options . width , options . height , options . backgroundColour ) ) ; } delete require . cache [ require . resolve ( 'chart.js' ) ] ; return chartJs ; } private renderChart ( configuration ) { const canvas = this . _createCanvas ( this . _width , this . _height , this . _type ) ; ( canvas as any ) . style = ( canvas as any ) . style || { } ; configuration . options = configuration . options || { } ; configuration . options . responsive = false ; configuration . options . animation = false as any ; const context = canvas . getContext ( '2d' ) ; ( global as any ) . Image = this . _image ; const chart = new this . _chartJs ( context , configuration ) ; delete ( global as any ) . Image ; return chart ; } }