807 lines
18 KiB
JavaScript
807 lines
18 KiB
JavaScript
/**
|
|
* 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<Canvas> 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<Axes> 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<Line> 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<Number> x x data
|
|
* @param 1darray<Number> 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<Number> 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<float> x xdata
|
|
* @property 1darray<float> 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<float> x xdata
|
|
* @param 1darray<float> 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<int> box box where to draw the line
|
|
* @param 2dlist<float> 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<Number> x_coordinates X coordinates of the point
|
|
* @param 1darray<Number> 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<Number> x_coordinates x coordinate of the point
|
|
* @param 1darray<Number> 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<array> Window where to write the title on
|
|
*
|
|
* @return plot_box int<array> 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<Number> Window where to draw the canvas
|
|
* @param view 2darray<Number> 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<number> 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
|
|
);
|
|
}
|