lichartee/lichartee.js

1142 lines
26 KiB
JavaScript

/**
* 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<Canvas> 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<Axes> 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<Line> 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<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 ,
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<Number> 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<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 Axes axes Axes of this 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({
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<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 : 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<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 _('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<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 _('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<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 = 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<Number> Window where to draw the axis
* @param view 2darray<Number> Window where to draw the canvas with the
* real value
* @return 2darray<Number> 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 `<main>` 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<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 ,
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<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
);
}