lichartee/lichartee.js
2024-01-16 01:08:06 +01:00

547 lines
12 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.lines = [];
}
/**
* plot y versus x as lines
*
* @param 1darray<Number> x x data
* @param 1darray<Number> 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<Number> 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<float> x xdata
* @property 1darray<float> 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<float> x xdata
* @param 1darray<float> 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<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, 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<array> Window where to write the title on
*
* @return plot_windows int<array> 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<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
);
}