/** * 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.axis = new Axis(); this.lines = []; } /** * plot y versus x as line * * @param 1darray x x data * @param 1darray y y data * @param String linestyle style of the line (can be "solid", * "dotted", "dashdot", "dashed" or "" * for no line) * @param String linecolor color of the line (can be "#abcdef", * "rgb(x,y,z)" or a name of a color. * @param String markerstyle style of the marker used for each * point (can be "circle", "point", * "small", "cross" or "" for no * marker) * @param String markercolor color of the marker (same * definition than linecolor) * @param Number markersize size of the marker */ plot( x , y , linestyle = 'solid' , linecolor = 'black' , markerstyle = 'circle', markercolor = 'black' , markersize = 5 , ) { let line = new Line( x , y , linestyle = linestyle , linecolor = linecolor , markerstyle = markerstyle, markercolor = markercolor, markersize = markersize , ); this.lines.push(line); } /** * render the axes to the canvas * * @param ctx CanvasRenderingContext2D Context of the canvas * @param view 2darray Window where to draw the canvas */ draw(ctx, view) { 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 n_view = this.title.draw( ctx , view, ); n_view = this.axis.draw( ctx , n_view, [ [x_min, y_min], [x_max, y_max] ], ); for (const index_line in this.lines) { this.lines[index_line].draw( ctx , n_view, [ [x_min, y_min], [x_max, y_max], ] , ); } ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.setLineDash([]); ctx.strokeRect( n_view[0][0], n_view[0][1], n_view[1][0] - n_view[0][0], n_view[1][1] - n_view[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 String linestyle style of the line (can be "solid", * "dotted", "dashdot", "dashed" or "" * for no line) * @property String linecolor color of the line (can be "#abcdef", * "rgb(x,y,z)" or a name of a color. * @property String markerstyle style of the marker used for each * point (can be "circle", "point", * "small", "cross" or "" for no * marker) * @property String markercolor color of the marker (same * definition than linecolor) * @property Number markersize size of the marker */ class Line { /** * @param 1darray x xdata * @param 1darray y ydata * @param string linestyle style of the line (can be "solid", * "dotted", "dashdot", "dashed" or "" * for no line) * @param string linecolor color of the line (can be "#abcdef", * "rgb(x,y,z)" or a name of a color. * @param string markerstyle style of the marker used for each * point (can be "circle", "point", * "small", "cross" or "" for no * marker) * @param string markercolor color of the marker (same * definition than linecolor) * @param Number markersize size of the marker */ constructor( x , y , linestyle = 'solid' , linecolor = 'black' , markerstyle = 'circle' , markercolor = 'black' , markersize = 5 , ) { this.x = x ; this.y = y ; this.linestyle = linestyle ; this.linecolor = linecolor ; this.markerstyle = markerstyle; this.markercolor = markercolor; this.markersize = markersize ; } /** * 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) { return box[0][0] + box_width * ( element - view[0][0] ) / x_delta; } ); let y_coordinates = this.y.map( function (element) { return box[0][1] + box_height - ( // starting top left box_height * ( element - view[0][1] ) / y_delta ); } ); if (this.linestyle != '') { this.drawLine( ctx, x_coordinates, y_coordinates, ); } if (this.markerstyle != '') { this.drawPoints( ctx , x_coordinates, y_coordinates, ); } } /** * draw a line in the given context at the given positions * * @param CanvasRenderingContext2d ctx context of the canvas * @param 1darray x_coordinates X coordinates of the point * @param 1darray y_coordinates Y coordinates of the point */ drawLine(ctx, x_coordinates, y_coordinates) { ctx.beginPath(); ctx.strokeStyle = this.linecolor; ctx.moveTo( x_coordinates[0], y_coordinates[0], ); if (this.linestyle === 'solid') { ctx.setLineDash([]); } else if (this.linestyle === 'dashed') { ctx.setLineDash([15, 5]); } else if (this.linestyle === 'dotted') { ctx.setLineDash([2,2]); } else if (this.linestyle === 'dashdot') { ctx.setLineDash([15,2,2,2]); } else { throw _('the style of the line is not correctly defined'); } for (const index in x_coordinates) { ctx.lineTo( x_coordinates[index], y_coordinates[index], ); } ctx.stroke(); } /** * draw points in the given context at the given positions * * @param CanvasRenderingContext2d ctx context of the canvas * @param 1darray x_coordinates x coordinate of the point * @param 1darray y_coordinates y coordinate of the point */ drawPoints(ctx, x_coordinates, y_coordinates) { for (const index in x_coordinates) { let x = x_coordinates[index]; let y = y_coordinates[index]; ctx.beginPath(); if (this.markerstyle == 'circle') { ctx.fillStyle = this.markercolor; ctx.ellipse( x , y , this.markersize, this.markersize, 0 , 0 , 2*Math.PI , ); ctx.fill(); } else if (this.markerstyle == 'point') { ctx.fillStyle = this.markercolor; ctx.ellipse( x , y , 0.5*this.markersize, 0.5*this.markersize, 0 , 0 , 2*Math.PI , ); ctx.fill(); } else if (this.markerstyle == 'small') { ctx.fillStyle = this.markercolor; ctx.ellipse( x , y , 0.1*this.markersize, 0.1*this.markersize, 0 , 0 , 2*Math.PI , ); ctx.fill(); } else if (this.markerstyle == 'cross') { ctx.setLineDash([]); ctx.strokeStyle = this.markercolor; ctx.moveTo( x - this.markersize, y - this.markersize, ); ctx.lineTo( x + this.markersize, y + this.markersize, ); ctx.stroke(); ctx.beginPath(); ctx.moveTo( x + this.markersize, y - this.markersize, ); ctx.lineTo( x - this.markersize, y + this.markersize, ); ctx.stroke(); } else { throw _('the style of the marker is not correctly defined'); } } } } /** * 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 box int Window where to write the title on * * @return plot_box int New windows for the plot */ draw(ctx, box) { ctx.font = this.font.toString(); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; let width = ctx.measureText(this.content).width; if (width > box[1][0] - box[0][0] + 2 * this.margin) { ctx.fillText( this.content , box[0][0] , this.font.size + this.margin, box[1][0] - box[0][0], ); } else { let text_position = box[0][0] + ( box[1][0] - box[0][0] - width ) / 2 ctx.fillText( this.content, text_position, this.font.size + this.margin, ); } return [ [ box[0][0], box[0][1] + this.font.size + 2 * this.margin, ], [box[1][0], box[1][1]], ]; } } /** * Axis (x and y) of the axes (not separated xaxis and yaxis) * * @property number_x_tick int Number of x axis tick * @property number_y_tick int Number of y axis tick * @property int size_tick Size of the tick * @property int margin Size of the margin */ class Axis { /** * @param number_x_tick int Number of x axis tick * @param number_y_tick int Number of y axis tick * @param size_tick int Size of the tick * @param margin int Size of the margin * @param text_height int Height of the text label */ constructor( number_x_tick = 4, number_y_tick = 4, size_tick = 5, margin = 15, text_height = 15, ) { this.number_x_tick = number_x_tick; this.number_y_tick = number_y_tick; this.size_tick = size_tick; this.margin = margin; this.text_height = text_height; } /** * draw xaxis and yaxis to canvas * * @param ctx CanvasRenderingContext2D Context of canvas * @param window 2darray Window where to draw the canvas * @param view 2darray Window where to draw the canvas with * the real value */ draw(ctx, window, view) { ctx.strokeStyle = 'black'; ctx.fillStyle = 'black'; let index = 0; let start_pos = window[0][1]; let padding = ( window[1][1] - window[0][1] - this.margin - this.text_height - this.size_tick ) / this.number_y_tick; let step = (view[1][1] - view[0][1]) / this.number_y_tick; let width = 2 * this.text_height; while (index <= this.number_y_tick) { let current_y = start_pos + index * padding; let text = ( view[1][1] - index * step ).toString().slice(0,3); let text_position = current_y + this.text_height / 2 ctx.beginPath(); ctx.moveTo( window[0][0] + this.margin + width, current_y, ); ctx.lineTo( window[0][0] + this.margin + this.size_tick + width, current_y, ); ctx.stroke(); ctx.fillText( text, window[0][0] + this.margin, text_position, ); current_y += padding; index += 1; } index = 0; start_pos = window[0][0] + this.margin + this.size_tick + width; padding = ( window[1][0] - this.margin - start_pos ) / this.number_x_tick; step = (view[1][0] - view[0][0]) / this.number_x_tick; while (index <= this.number_x_tick) { let current_x = start_pos + padding * index; let text = ( view[0][0] + index * step ).toString().slice(0,3); let text_width = ctx.measureText(text).width; let text_position = current_x - text_width / 2; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo( current_x, window[1][1] - this.margin - this.text_height, ); ctx.lineTo( current_x, window[1][1] - this.margin - this.size_tick - this.text_height, ); ctx.stroke(); ctx.fillText( text , text_position, window[1][1] - this.margin, ); index += 1; } return [ [window[0][0] + this.margin + this.size_tick + width, window[0][1]], [window[1][0] - this.margin , window[1][1] - this.text_height - this.margin - this.size_tick], ]; } } /** * 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 ); }