/** * 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; this.ctx.beginPath(); this.ctx.strokeStyle = 'black'; this.ctx.setLineDash([]); this.ctx.strokeRect( start_x, 0 , end_x, this.ctx.canvas.height ); const x_min = get_ext_array( axes.lines.map( (element) => element.x.reduce( (a, b) => Math.min(a, b), Infinity ) ) , Math.min, 1 ); const y_min = get_ext_array( axes.lines.map( (element) => element.y.reduce( (a, b) => Math.min(a, b), Infinity ) ) , Math.min, 1 ); const x_max = get_ext_array( axes.lines.map( (element) => element.x.reduce( (a, b) => Math.max(a, b), -Infinity ) ) ); const y_max = get_ext_array( axes.lines.map( (element) => element.y.reduce( (a, b) => Math.max(a, b), -Infinity ) ) ); for (const index_line in axes.lines) { axes.lines[index_line].draw( this.ctx , [[start_x, 0],[end_x, this.ctx.canvas.height]], [[x_min, y_min], [x_max, y_max]] ); } 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 = 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); } } /** * 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(); } } /** * 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 ); }