/** * 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 , family, color , }) { 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; } } var LightTheme = { line : { style: 'solid', color: 'black', } , marker: { style: '' , color: 'black', size : 5 , } , title : { font : new Font({ size : 20 , family: 'sans-serif', color : 'black' , }) , margin: 10, } , bordercolor: 'black' , axis : { font : new Font({ size : 15 , family: 'sans-serif', color : 'black' , }) , ticks : { x: { number: 4, } , y: { number: 4, } , size : 5, } , margin: 15, } , }; var DarkTheme = { line : { style: 'solid', color: 'white', } , marker: { style: '' , color: 'white', size : 5 , } , title : { font : new Font({ size : 20 , family: 'sans-serif', color : 'white' , }) , margin: 10, } , bordercolor: 'white' , axis : { font : new Font({ size : 15 , family: 'sans-serif', color : 'white' , }) , ticks : { x: { number: 4, } , y: { number: 4, } , size : 5, } , margin: 15, } , }; var DefaultTheme = LightTheme; if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches ) { DefaultTheme = DarkTheme; } else { DefaultTheme = LightTheme; } /** * 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 Theme theme Theme of managed canvas * @property 1darray list_canvas List of canvas held by this * manager */ class Manager { /** * @param Element parent_element Element holding the canvas * @param Theme theme Theme of managed canvas */ constructor({ parent_element , theme = DefaultTheme, }) { this.parent_element = parent_element; this.theme = theme; 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].after( element_canvas ); } let ctx = element_canvas.getContext('2d'); ctx.canvas.width = width ; ctx.canvas.height = height; let figure = new Figure({}); let canvas = new Canvas({ manager: this , context: ctx , figure : figure, }); canvas.figure.canvas = canvas; 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 Manager manager Manager of this canvas * @property CanvasRenderingContext2D ctx Canvas API interface * @property Figure figure Figure held by this canvas */ class Canvas { /** * @param Manager manager Manager of this canvas * @param CanvasRenderingContext2D ctx Canvas API interface * @param Figure figure Figure held by this canvas */ constructor({manager, context , figure}) { this.manager = manager; this.ctx = context; this.figure = figure ; } /** * render the figure to the canvas */ draw() { if (this.figure instanceof Figure.constructor) { throw _('no_figure') } this.ctx.clearRect( 0 , 0 , this.ctx.canvas.width , this.ctx.canvas.height, ); 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({ ctx: this.ctx, box: [ [start_x, 0 ], [end_x , this.ctx.canvas.height], ] , }); start_x = end_x; } } } /** * a figure hold axes and every plot elements * * @property Canvas canvas Canvas of this figure * @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.canvas = undefined; 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({ figure: this , width : relative_width , height: 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 Figure figure Figure of this Axes * @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 Figure figure Figure of this Axes * @param int width Width of the axes * @param int height Height of the axes */ constructor({ figure , width = 100, height = 100, }) { this.figure = figure; this.width = width ; this.height = height; this.axis = new Axis({ axes: this, }) ; 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 , linecolor , markerstyle, markercolor, markersize , }) { let line = new Line({ axes : this , x : x , y : 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 box 2darray Window where to draw the canvas */ draw({ctx, box}) { 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_box = box; if (this.title !== undefined) { n_box = this.title.draw({ ctx: ctx, box: box, }); } n_box = this.axis.draw({ ctx : ctx , box : n_box , view : [ [x_min, y_min], [x_max, y_max] ], }); for (const index_line in this.lines) { this.lines[index_line].draw({ ctx : ctx , box : n_box , view: [ [x_min, y_min], [x_max, y_max], ] , }); } ctx.beginPath(); ctx.strokeStyle = this.figure.canvas.manager.theme.bordercolor; ctx.setLineDash([]); ctx.strokeRect( n_box[0][0], n_box[0][1], n_box[1][0] - n_box[0][0], n_box[1][1] - n_box[0][1], ); } /** * title setter * * @param content String Content of the title */ set title(content) { this._title = new Title({ axes : this , content: content, }); } /** * title getter */ get title() { return this._title; } } /** * a line, with a color and a line style * * @property Axes axes Axes of this line * @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 Axes axes Axes of this 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({ axes , x , y , linestyle , linecolor , markerstyle, markercolor, markersize , }) { this.axes = axes ; if (linestyle === undefined) { linestyle = this.axes.figure.canvas.manager.theme.line .style; } if (linecolor === undefined) { linecolor = this.axes.figure.canvas.manager.theme.line .color; } if (markerstyle === undefined) { markerstyle = this.axes.figure.canvas.manager.theme.marker .style; } if (markercolor === undefined) { markercolor = this.axes.figure.canvas.manager.theme.marker .color; } if (markersize === undefined) { markersize = this.axes.figure.canvas.manager.theme.marker .size; } 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 : ctx , x_coordinates: x_coordinates, y_coordinates: y_coordinates, }); } if (this.markerstyle != '') { this.drawPoints({ ctx : ctx , x_coordinates: x_coordinates, y_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 _('line_style', {style: this.linestyle}); } 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 _('marker_style', {style: this.markerstyle}); } } } } /** * title of an axes * * @property Axes axes Axes of this title * @property String content Content of the title * @property Font font Font of the title * @property Number margin Margin of the title */ class Title { /** * @param Axes axes Axes of this title * @param String content Content of the title * @param Font font Font of the title * @param Number margin Margin of the title */ constructor({ axes , content , font , margin , }) { this.axes = axes ; if (font === undefined) { font = this.axes.figure.canvas.manager.theme.title.font; } if (margin === undefined) { margin = this.axes.figure.canvas.manager.theme.title.margin; } this.content = content; 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 = this.font.color; 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 Axes axes Axes of the Axis * @property Number number_x_tick Number of x axis tick * @property Number number_y_tick Number of y axis tick * @property Number size_tick Size of the tick * @property Number margin Size of the margin * @property Font font Font of tick labels */ class Axis { /** * @param Axes axes Axes of the Axis * @param Number number_x_tick Number of x axis tick * @param Number number_y_tick Number of y axis tick * @param Number size_tick Size of the tick * @param Number margin Size of the margin * @param Number text_height Height of the text label * @param Font font Font of tick labels */ constructor({ axes , number_x_tick, number_y_tick, size_tick , margin , text_height , font , }) { this.axes = axes ; if (number_x_tick === undefined) { number_x_tick = this.axes.figure.canvas.manager.theme.axis .ticks.x.number; } if (number_y_tick === undefined) { number_y_tick = this.axes.figure.canvas.manager.theme.axis .ticks.y.number; } if (size_tick === undefined) { size_tick = this.axes.figure.canvas.manager.theme.axis .ticks.size; } if (margin === undefined) { margin = this.axes.figure.canvas.manager.theme.axis .margin; } if (text_height === undefined) { text_height = this.axes.figure.canvas.manager.theme.axis .font.size; } if (font === undefined) { font = this.axes.figure.canvas.manager.theme.axis .font; } 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 ; this.font = font ; } /** * draw xaxis and yaxis to canvas * * @param ctx CanvasRenderingContext2D Context of canvas * @param box 2darray Window where to draw the axis * @param view 2darray Window where to draw the canvas with the * real value * @return 2darray New window where to draw the rest of the plot */ draw({ctx, box, view}) { ctx.font = this.font.toString(); ctx.strokeStyle = this.font.color; ctx.fillStyle = this.font.color; let index = 0; let start_pos = box[0][1]; let padding = ( box[1][1] - box[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( box[0][0] + this.margin + width, current_y, ); ctx.lineTo( box[0][0] + this.margin + this.size_tick + width, current_y, ); ctx.stroke(); ctx.fillText( text, box[0][0] + this.margin, text_position, ); current_y += padding; index += 1; } index = 0; start_pos = box[0][0] + this.margin + this.size_tick + width; padding = ( box[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, box[1][1] - this.margin - this.text_height, ); ctx.lineTo( current_x, box[1][1] - this.margin - this.size_tick - this.text_height, ); ctx.stroke(); ctx.fillText( text , text_position, box[1][1] - this.margin, ); index += 1; } return [ [box[0][0] + this.margin + this.size_tick + width, box[0][1]], [box[1][0] - this.margin , box[1][1] - this.text_height - this.margin - this.size_tick], ]; } } /** * state-based interface to lichartee. It provides an implicit, * MATLAB-like, way of plotting. Needs a `
` node in the html * * @property Manager manager Current managed manager */ class jsplot { /** * @param Manager manager Current managed manager * @param Theme theme Theme for managed canvas */ constructor(theme) { this.manager = new Manager({ parent_element: document.getElementsByTagName('main')[0], theme : theme , }); } /** * plot y versus x as lines and/or markers * * @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 , linecolor , markerstyle, markercolor, markersize , }) { if (this.manager.list_canvas.length === 0) { this.manager.add_canvas({ width: 500, height: 500, }); } let canvas = this.manager.list_canvas[ this.manager.list_canvas.length - 1 ]; canvas = this.manager.list_canvas[0]; if (canvas.figure.list_axes.length === 0) { canvas.figure.add_axes({}); } canvas.figure.list_axes[0].plot({ x : x , y : y , linestyle: linestyle , linecolor: linecolor , markerstyle: markerstyle, markercolor: markercolor, markersize: markersize , }); } /** * set a title for the Axes * * @param String content Text to use for the title */ title(content) { if (this.manager.list_canvas.length === 0) { this.manager.add_canvas({ width: 500, height: 500, }); } let canvas = this.manager.list_canvas[0]; if (canvas.figure.list_axes.length === 0) { canvas.figure.add_axes({}); } canvas.figure.list_axes[0].title = content; } /** * render the current canvas and create a new canvas for next a plot */ show() { if (this.manager.list_canvas.length === 0) { this.manager.add_canvas({ width: 500, height: 500, position: 0 , }); } else { this.manager.draw(); this.manager.add_canvas({ width: 500 , height: 500 , position: this.manager.list_canvas.length - 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 ); }