/** * a manager allows to manipulate multiple canvas * * it's different from matplotlib, where the manager can only store one * canvas * * @property Element parent_element DOM element containing every canvas * of this manager * @property 1darray list_canvas List of canvas held by this * manager */ class Manager { /** * @param Element parent_element Element holding the canvas */ constructor(parent_element) { this.parent_element = parent_element; this.list_canvas = []; } /** * add a canvas * * @param int width Width of the added canvas * @param int height Height of the added canvas * @param int position Subjective position of the added canvas */ add_canvas(width = 100, height = 100, position = 0) { let element_canvas = document.createElement('canvas'); let text = document.createTextNode( 'Your browser doesn\'t seem to support canvas, or you ' + 'deactivated them. This is the canvas where the chart ' + 'should be displayed' ); element_canvas.appendChild(text); if (this.parent_element.childElementCount === 0) { this.parent_element.appendChild(element_canvas); } else { this.parent_element.children[position].before( element_canvas ); } let ctx = element_canvas.getContext('2d'); ctx.canvas.width = width ; ctx.canvas.height = height; let figure = new Figure(); let canvas = new Canvas(ctx, figure); this.list_canvas.splice(position, 0, canvas); return canvas; } /** * draw every canvas */ draw() { for (const index in this.list_canvas) { this.list_canvas[index].draw(); } } /** * remove a canvas * * @param int position Position of the canvas to remove */ remove_canvas(position = 0) { document.removeChild(this.list_canvas[position].ctx.canvas); this.list_canvas.splice(position, 1); } } /** * a canvas allows to render a figure * * @property CanvasRenderingContext2D ctx Canvas API interface * @property Figure figure Figure held by this canvas */ class Canvas { /** * @param CanvasRenderingContext2D ctx Canvas API interface * @param Figure figure Figure held by this canvas */ constructor(ctx, figure) { this.ctx = ctx ; this.figure = figure; } /** * render the figure to the canvas */ draw() { if (this.figure instanceof Figure.constructor) { throw _('this canvas does not possess figure to draw') } let start_x = 0; for (const index_axes in this.figure.list_axes) { let axes = this.figure.list_axes[index_axes]; let x_proportion = axes.width/this.figure.width; let end_x = start_x + this.ctx.canvas.width * x_proportion; axes.draw( this.ctx, [ [start_x, 0 ], [end_x , this.ctx.canvas.height], ], ); start_x = end_x; } } } /** * a figure hold axes and every plot elements * * @property int width Total width of this figure (related to each * axes) * @property int height Total height of this figure (related to each * axes) * @property 1darray list_axes List of every axes */ class Figure { constructor() { this.width = 0 ; this.height = 0 ; this.list_axes = []; } /** * add axes to this figure * * @param int relative_width Relative width of the new axes * @param int relative_height Relative height of the new axes * @param int position Position of the axes in the figure */ add_axes(relative_width = 50, relative_height = 50, position = 0) { this.width += relative_width ; this.height += relative_height; let axes = new Axes(relative_width, relative_height); this.list_axes.splice(position, 0, axes); return axes; } /** * remove an axes * * @param int position Position of the axes in the figure to delete */ remove_axes(position = 0) { this.list_axes.splice(position, 1); } } /** * an axes represents one plot in a figure. * * @property int width Width of the axes * @property int height Height of the axes * @property String title Title of the axes * @property 1darray lines List of lines */ class Axes { /** * @param int width Width of the axes * @param int height Height of the axes * @param String title Title of the axes */ constructor( width = 100 , height = 100 , title = '' , ) { this.width = width; this.height = height; this.title = new Title( title ); this.lines = []; } /** * plot y versus x as lines * * @param 1darray x x data * @param 1darray y y data * @param object linestyle Object defining a style for a line. The * structure of the object should be: * { * color: color * style: style * } * where color is a string containing * "rgb(x,y,z)", "#abcdef" or a name of * a color, and style is "solid", "dotted", * "dashdot" or "dashed". Default value is * color = "black" and style = "solid" * @param String label Label that will be displayed in the legend */ plot(x, y, linestyle = {color: "black", style: "solid"}, label = '') { let line = new Line(x, y, linestyle, label); this.lines.push(line); } /** * render the axes to the canvas * * @param ctx CanvasRenderingContext2D Context of the canvas * @param window 2darray Window where to draw the canvas */ draw(ctx, window) { const x_min = get_ext_array( this.lines.map( (element) => element.x.reduce( (a, b) => Math.min(a, b), Infinity ) ) , Math.min, 1 ); const y_min = get_ext_array( this.lines.map( (element) => element.y.reduce( (a, b) => Math.min(a, b), Infinity ) ) , Math.min, 1 ); const x_max = get_ext_array( this.lines.map( (element) => element.x.reduce( (a, b) => Math.max(a, b), -Infinity ) ) ); const y_max = get_ext_array( this.lines.map( (element) => element.y.reduce( (a, b) => Math.max(a, b), -Infinity ) ) ); let plot_window = this.title.draw( ctx , window, ); for (const index_line in this.lines) { this.lines[index_line].draw( ctx , plot_window, [ [x_min, y_min], [x_max, y_max], ] , ); } ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.setLineDash([]); ctx.strokeRect( plot_window[0][0], plot_window[0][1], plot_window[1][0] - plot_window[0][0], plot_window[1][1] - plot_window[0][1], ); } /** * title setter * * @param content String Content of the title */ set title(content) { this._title = new Title(content); } /** * title getter */ get title() { return this._title; } } /** * a line, with a color and a line style * * @property 1darray x xdata * @property 1darray y ydata * @property object linestyle Object defining a style for a line. The * structure of the object should be: * { * color: color * style: style * } * where color is a string containing * "rgb(x,y,z)", "#abcdef" or a name of * a color, and style is "solid", "dotted", * "dashdot" or "dashed". Default value is * color = "black" and style = "solid" * @property String label Label that will be displayed in the legend */ class Line { /** * @param 1darray x xdata * @param 1darray y ydata * @param object linestyle Object defining a style for a line. The * structure of the object should be: * { * color: color * style: style * } * where color is a string containing * "rgb(x,y,z)", "#abcdef" or a name of * a color, and style is "solid", "dotted", * "dashdot" or "dashed". Default value is * color = "black" and style = "solid" * @param String label Label that will be displayed in the legend */ constructor( x , y , linestyle = {color: "black", style: "solid"}, label = '' ) { this.x = x ; this.y = y ; this.linestyle = linestyle ; this.label = label ; } /** * draw the line in the box coordinate * * @param CanvasRenderingContext2d ctx Context of the canvas * @param 2dlist box Box where to draw the line * @param 2dlist view Scale of the box (link value <-> * coordinate */ draw(ctx, box, view) { const x_delta = view[1][0] - view[0][0]; const y_delta = view[1][1] - view[0][1]; const box_width = box[1][0] - box[0][0]; const box_height = box[1][1] - box[0][1]; let x_coordinates = this.x.map( function (element, index, array) { return box[0][0] + box_width * ( element - view[0][0] ) / x_delta; } ); let y_coordinates = this.y.map( function (element, index, array) { return box[0][1] + box_height - ( // starting top left box_height * ( element - view[0][1] ) / y_delta ); } ); if (this.linestyle.style === 'solid') { ctx.setLineDash([]); } else if (this.linestyle.style === 'dashed') { ctx.setLineDash([15, 5]); } else if (this.linestyle.style === 'dotted') { ctx.setLineDash([2,2]); } else if (this.linestyle.style === 'dashdot') { ctx.setLineDash([15,2,2,2]); } else { throw _('the style of the line is not correctly defined'); } ctx.strokeStyle = this.linestyle.color; ctx.beginPath(); ctx.moveTo( x_coordinates[0], y_coordinates[0] ); for (const index in x_coordinates) { ctx.lineTo( x_coordinates[index], y_coordinates[index] ); } ctx.stroke(); } } /** * font object * * @property size int font-size (px) * @property family string font-family * @property color string font color */ class Font { /** * @param size int font-size (px) * @param family string font-family * @param color string font color */ constructor( size = 20 , family = 'sans-serif', color = 'black' , ) { this.size = size; this.family = family; this.color = color; } /** * Convert font to css font proprety's syntax * * @return String */ toString() { return this.size.toString() + 'px ' + self.family; } } /** * title of an axes * * @property content String Content of a title * @property font Font Font of a title */ class Title { /** * @param content Content Content of a title * @param font Font Font of a title */ constructor( content = '' , font = undefined, margin = 5 , ) { this.content = content; if (font === undefined) { font = new Font(); } this.font = font; this.margin = margin; } /** * draw the title in the given windows * * @param ctx CanvasRenderingContext2D Context of the canvas * @param window int Window where to write the title on * * @return plot_windows int New windows for the plot */ draw(ctx, window) { ctx.font = this.font.toString(); let width = ctx.measureText(this.content).width; if (width > window[1][0] - window[0][0] + 2 * this.margin) { ctx.fillText( this.content , window[0][0] , this.font.size + this.margin, window[1][0] - window[0][0], ); } else { let text_position = window[0][0] + ( window[1][0] - window[0][0] - width ) / 2 ctx.fillText( this.content, text_position, this.font.size + this.margin, ); } return [ [ window[0][0], window[0][1] + this.font.size + 2 * this.margin, ], [window[1][0], window[1][1]], ]; } } /** * get max or min of a 1d array of number * * @param 1darray array Array where to get max values * @param function f Math.max or Math.min if we want max or min value * @param int sign -1 if max, 1 if min */ function get_ext_array(array, f = Math.max, sign = -1) { return array.reduce( (a, b) => f(a, b), sign * Infinity ); }