@massimo-cassandro/minimo 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Raccolta di utilità condivise per la gestione dei grafici.
3
+ */
4
+
5
+ /**
6
+ * Tronca un numero a un numero specifico di decimali.
7
+ * @param {number} number - Il numero da troncare.
8
+ * @param {number} [decimals=4] - Il numero di decimali da mantenere.
9
+ * @returns {number} Il numero troncato.
10
+ */
11
+ export function truncateDecimal(number, decimals = 4) {
12
+ return +parseFloat((number).toFixed(decimals));
13
+ }
14
+
15
+ /**
16
+ * Converte un angolo da gradi in radianti.
17
+ * @param {number} degrees_angle - L'angolo in gradi.
18
+ * @returns {number} L'angolo in radianti.
19
+ */
20
+ export const toRadians = degrees_angle => (degrees_angle * Math.PI) / 180.0;
21
+
22
+ /**
23
+ * Trasforma le coordinate polari (centro + raggio) in coordinate cartesiane (x, y).
24
+ * @param {number} degrees_angle - L'angolo in gradi.
25
+ * @param {number} cx - Coordinata X del centro.
26
+ * @param {number} cy - Coordinata Y del centro.
27
+ * @param {number} circleRadius - Il raggio del cerchio.
28
+ * @returns {{x: number, y: number}} Le coordinate cartesiane risultanti.
29
+ */
30
+ export function polarToCartesian(degrees_angle, cx, cy, circleRadius) {
31
+ const radians = toRadians(degrees_angle);
32
+
33
+ return {
34
+ x: truncateDecimal(Math.abs(cx + circleRadius * Math.cos(radians))),
35
+ y: truncateDecimal(Math.abs(cy - circleRadius * Math.sin(radians)))
36
+ };
37
+
38
+ }
39
+
40
+ // come il precedente, ma restituisce una stringa
41
+ export function polarToCartesianString(degrees_angle, cx, cy, circleRadius) {
42
+ const {x,y} = polarToCartesian(degrees_angle, cx, cy, circleRadius);
43
+
44
+ return `${x},${y}`;
45
+ }
46
+
47
+ /**
48
+ * Concatena i nomi delle classi filtrando i valori falsy.
49
+ * @param {...any} args - I nomi delle classi o condizioni.
50
+ * @returns {string} Una stringa di nomi di classi separati da spazio.
51
+ */
52
+ export function classnames(...args) {
53
+ return args.filter(Boolean).join(' ');
54
+ }
55
+
56
+ /**
57
+ * Concatena i nomi delle classi filtrando i valori falsy; restituisce null se il risultato è una stringa vuota.
58
+ * @param {...any} args - I nomi delle classi o condizioni.
59
+ * @returns {string|null} Una stringa di nomi di classi o null.
60
+ */
61
+ export function classnamesNull(...args) {
62
+ return args.filter(Boolean).join(' ') || null;
63
+ }
64
+
65
+ /**
66
+ * Calcola le coordinate per un elemento `path` di tipo arco (arc).
67
+ * Lo `start_angle` è il punto di inizio e `percentage` è la lunghezza percentuale dell'arco rispetto a `max_angle`.
68
+ *
69
+ * @param {Object} options - Opzioni di configurazione.
70
+ * @param {number} [options.start_angle=0] - Angolo di partenza in gradi.
71
+ * @param {number|null} options.percentage - Lunghezza dell'arco in percentuale (da 0 a 1).
72
+ * @param {number} [options.max_arc=360] - L'estensione massima dell'arco in gradi.
73
+ * @param {Object} options.center - Coordinate del centro.
74
+ * @param {number|null} options.center.x - Coordinata X del centro.
75
+ * @param {number|null} options.center.y - Coordinata Y del centro.
76
+ * @param {number|null} options.radius - Raggio dell'arco.
77
+ * @param {boolean} [options.clockwise=true] - Direzione oraria se true.
78
+ * @returns {{arc_length: number, start: {x: number, y: number}, end: {x: number, y: number}}} Coordinate di inizio, fine e lunghezza.
79
+ */
80
+ export function calcArcCoords(options) {
81
+ const default_options = {
82
+ start_angle: 0,
83
+ percentage: null,
84
+ max_arc: 360,
85
+ center: { x: null, y: null },
86
+ radius: null,
87
+ clockwise: true
88
+ };
89
+
90
+ options = { ...default_options, ...options };
91
+
92
+ try {
93
+ if (options.percentage == null ||
94
+ options.center?.x == null ||
95
+ options.center?.y == null ||
96
+ options.start_angle == null ||
97
+ options.radius == null ||
98
+ options.max_arc == null
99
+ ) {
100
+ throw 'Errore parametri in `calcArcCoords`';
101
+ }
102
+ } catch (e) {
103
+ // eslint-disable-next-line no-console
104
+ console.error(e);
105
+ }
106
+
107
+ // Angolo dell'arco corrispondente alla percentuale data
108
+ let arc_length = options.max_arc * options.percentage,
109
+ end_angle = options.start_angle + (options.clockwise ? -arc_length : arc_length);
110
+
111
+ if (options.percentage === 1) {
112
+ end_angle -= .001; // Per evitare errori di arrotondamento nel disegno dell'arco completo
113
+ }
114
+
115
+ return {
116
+ arc_length: arc_length,
117
+ start: polarToCartesian(options.start_angle, options.center, options.radius),
118
+ end: polarToCartesian(end_angle, options.center, options.radius),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Calcola l'attributo `d` per un elemento SVG path che rappresenta un arco.
124
+ * Accetta le stesse opzioni di `calcArcCoords`.
125
+ * * @param {Object} options - Opzioni per il calcolo dell'arco.
126
+ * @returns {string} La stringa da utilizzare nell'attributo `d` del path SVG.
127
+ */
128
+ export function calcAttrD(options) {
129
+ const points = calcArcCoords(options);
130
+
131
+ return [
132
+ `M${points.start.x},${points.start.y}`,
133
+ `A${options.radius},${options.radius}`,
134
+ 0, // xAxisRotation
135
+ (points.arc_length <= 180 ? 0 : 1), // largeArcFlag
136
+ options.clockwise && points.arc_length < 360 ? 1 : 0, // sweepFlag
137
+ `${points.end.x},${points.end.y}`
138
+ ].join(' ');
139
+ }
140
+
141
+ /**
142
+ * Recupera un elemento DOM da un selettore stringa o lo restituisce se è già un elemento.
143
+ * @param {string|Element|null} container - Il selettore CSS o l'elemento DOM.
144
+ * @returns {Element|null} L'elemento DOM o null.
145
+ */
146
+ export function getElementFromContainer(container) {
147
+ if (!container) {
148
+ return null;
149
+ } else if (typeof container === 'string') {
150
+ return document.querySelector(container);
151
+ } else if (container instanceof Element) {
152
+ return container;
153
+ } else {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Analizza il contenitore e restituisce informazioni sulle sue dimensioni, eseguendo alcune azioni di base.
160
+ * @param {Object} params - Parametri di input.
161
+ * @param {string|Element|null} [params.container=null] - Il contenitore del grafico.
162
+ * @param {number|null} [params.width=null] - Larghezza predefinita (opzionale).
163
+ * @param {number|null} [params.height=null] - Altezza predefinita (opzionale).
164
+ * @returns {[Element|null, number|null, number|null]} Un array contenente l'elemento, la larghezza e l'altezza.
165
+ */
166
+ export function parseContainer({ container = null, width = null, height = null }) {
167
+ const containerElement = getElementFromContainer(container);
168
+
169
+ // Calcolo delle dimensioni del grafico
170
+ width = width || (containerElement ? containerElement.clientWidth : null);
171
+ height = height || (containerElement ? containerElement.clientHeight : null);
172
+
173
+ return [containerElement, width, height];
174
+ }
@@ -0,0 +1,25 @@
1
+ import { default_colors } from './default-chart-colors.js';
2
+ import { createSvgCanvas } from './create-svg-canvas.js';
3
+
4
+ export class MinimoCharts {
5
+ constructor(customParams = {}) {
6
+ this.defaults = {
7
+ colors: customParams.defaults?.colors ?? Object.values(default_colors),
8
+ font: {
9
+ size: 14,
10
+ weight: 400,
11
+ fill: '#999',
12
+ ...customParams.defaults?.font
13
+ }
14
+ };
15
+
16
+
17
+ this.debug = customParams.debug ?? false;
18
+ this.createSvgCanvas = (customParams.createSvgCanvas ?? createSvgCanvas).bind(this);
19
+
20
+ this.sharedParams = {}; // per condivisione parametri tra grafici nella stessa applicazione
21
+ }
22
+
23
+ setSharedParams(key, value = null) { this.sharedParams[key] = value; }
24
+ getSharedParams(key) { return this.sharedParams[key]; }
25
+ }
@@ -0,0 +1,25 @@
1
+ import { SVG } from '@svgdotjs/svg.js';
2
+
3
+
4
+ export function createSvgCanvas(container, debug = false) {
5
+
6
+ try {
7
+
8
+ const chartInstance = this;
9
+
10
+ if(!container) {
11
+ throw 'Missing container';
12
+ }
13
+
14
+ const svgCanvas = SVG().addTo(container);
15
+
16
+ if(debug || chartInstance.debug) {
17
+ svgCanvas.addClass('debug');
18
+ }
19
+ return svgCanvas;
20
+
21
+ } catch(e) {
22
+ console.error( 'Charts → createSvgCanvas', e ); // eslint-disable-line
23
+ }
24
+
25
+ }
@@ -0,0 +1,54 @@
1
+ import { textToSvgPath } from './svg-text-to-path.js';
2
+ import { SVG, Text } from '@svgdotjs/svg.js';
3
+
4
+ export async function createTextEl (chartInstance, {
5
+ text,
6
+ textFill = '#000',
7
+ fontAttr = {},
8
+ fontFilePath = null,
9
+ parentElement = null, // se null viene restituito l'elemento generato (text o path)
10
+ textToPath = false, // bypassa l'impostazione globale, per usi particolari
11
+ }) {
12
+
13
+ let textEl;
14
+
15
+
16
+ if(text == null) {
17
+ throw '`createTextEl`: missing `text`';
18
+ }
19
+
20
+ // Utilizza i defaults dell'istanza (grazie al bind nel costruttore)
21
+ // Se fontAttr è nullo, usa un oggetto vuoto per evitare errori di spread
22
+ fontAttr = { ...chartInstance.defaults.font, ...(fontAttr ?? {}), fill: textFill };
23
+
24
+ if(chartInstance.textToPath || textToPath) {
25
+
26
+ if(!fontFilePath) {
27
+ throw `‘createTextEl’: missing ‘fontFilePath’ for text ‘${text}’`;
28
+ }
29
+
30
+ // Qui recuperiamo sia il tracciato che la stringa SVG (come nel tuo originale)
31
+ const { pathData, pathElementString } = await textToSvgPath(fontFilePath, text, fontAttr.size ?? 16);
32
+
33
+ if(parentElement) {
34
+ textEl = parentElement.path(pathData);
35
+ } else {
36
+ // Nota: utilizzando path con pathData direttamente su SVG() a volte dà errore in svg.js 3.x
37
+ textEl = SVG(pathElementString);
38
+ }
39
+
40
+ textEl.attr({ fill: textFill });
41
+
42
+ } else {
43
+ // Modalità testo standard (non tracciato)
44
+ if(parentElement) {
45
+ textEl = parentElement.plain(text);
46
+ } else {
47
+ textEl = new Text().plain(text);
48
+ }
49
+
50
+ textEl.font(fontAttr).attr({ lengthAdjust: 'spacingAndGlyphs' });
51
+ }
52
+
53
+ return textEl;
54
+ }
@@ -0,0 +1,17 @@
1
+
2
+ export const default_colors = {
3
+ azzurro : '#2387e2',
4
+ rosaScuro : '#fc1484',
5
+ giallo : '#f5a623',
6
+ verdeAcqua : '#6dbebf',
7
+ lavanda : '#9894fd',
8
+ carminio : '#af373e',
9
+ verdeElettrico : '#54f25a',
10
+ verdeBlu : '#167bac',
11
+ acqua : '#00e6fb',
12
+ bluElettrico : '#0b16e9',
13
+ oliva : '#9dbf6d',
14
+ violaElettrico : '#c614fc',
15
+ lime : '#d9f440',
16
+ rosaChiaro : '#fea4f2',
17
+ };
@@ -0,0 +1,96 @@
1
+ import { G } from '@svgdotjs/svg.js';
2
+ import { createTextEl } from './create-text-element.js';
3
+
4
+ export async function legenda(chartInstance, {
5
+
6
+ /** altezza legenda */
7
+ height = 20,
8
+
9
+ /** spazio tra gli elementi della legenda */
10
+ elements_gap = 20,
11
+
12
+ /** dimensioni delle forme della legenda */
13
+ lines_width = 20,
14
+ circles_radius = 20,
15
+
16
+ /** attributi svg.js dei font del testo della legenda (null utilizza default) */
17
+ fontAttrs = null,
18
+
19
+ /** percorso font file per textToPath */
20
+ fontFilePath = null,
21
+
22
+ /** elementi della legenda */
23
+ items = [],
24
+
25
+
26
+ }) {
27
+
28
+ /**
29
+ elementi di default di items
30
+ */
31
+ const items_defaults = {
32
+
33
+ shape: 'line', // line || circle
34
+ attrs: {}, // attributi svg.js di shape
35
+ text: '__label__', // testo della voce
36
+ };
37
+
38
+ try {
39
+
40
+
41
+ if(!items.length) {
42
+ throw 'nessuna voce in `items`';
43
+ }
44
+
45
+ items = items.map(i => ({...items_defaults, ...i}));
46
+
47
+ const legendaGroup = new G();
48
+
49
+
50
+ let x = 0; // posizionamento x
51
+ for await (const voce of items) {
52
+
53
+ const thisGroup = legendaGroup.group();
54
+
55
+ if(voce.shape === 'line') {
56
+
57
+ thisGroup.line(
58
+ x, height / 2,
59
+ x + lines_width, height / 2
60
+ )
61
+ .attr(voce.attrs);
62
+ x = x + lines_width;
63
+
64
+ } else if (voce.shape === 'circle') {
65
+ thisGroup.circle(circles_radius)
66
+ .attr(voce.attrs)
67
+ .move(x, (height - circles_radius) / 2);
68
+
69
+ x = x + circles_radius;
70
+
71
+ } else {
72
+ throw `parametro 'shape' non corretto in una delle voci (${voce.shape})`;
73
+ }
74
+
75
+ // testo
76
+ const labelEl = await createTextEl(chartInstance, {
77
+ text: voce.text,
78
+ textFill: fontAttrs.fill?? '#000',
79
+ fontAttr: fontAttrs,
80
+ fontFilePath: fontFilePath,
81
+ parentElement: thisGroup
82
+ }),
83
+ labelBbox = labelEl.bbox();
84
+
85
+ labelEl.move(x + 5, (height - labelBbox.height) / 2);
86
+
87
+ x = legendaGroup.bbox().width + elements_gap;
88
+ }
89
+
90
+ return legendaGroup;
91
+
92
+ } catch(e) {
93
+ console.error( 'MinimoCharts → legenda', e ); // eslint-disable-line
94
+ }
95
+
96
+ }
@@ -0,0 +1,95 @@
1
+ // prima elaborazione e controlli delle impostazioni comuni a tutti i grafici con assi
2
+ // NB non nel prototypoe della classe, va importato dove richiesto
3
+ /*
4
+ l'argomento params è un oggetto che deve contenere i parametri con cui vengono effettuati i vari calcoli:
5
+ width, height, ecc... (vedi default)
6
+
7
+ values è un array flat contentente tutti i valori da rappresentare
8
+
9
+ restituisce un offetto `parsed` con i nuovi parametri calcolati
10
+ */
11
+
12
+ export function parseStdAxisOpts(params = {} ) {
13
+
14
+ try {
15
+
16
+
17
+
18
+ const parsed = {};
19
+
20
+ // default
21
+ params = {
22
+ max_value: null,
23
+ min_value: null,
24
+ values: [], // array flat dei valori per il calcolo di maxValues e minValues, qualora fossero null
25
+ legenda: null,
26
+ padding: 0, // spazio intorno ai bordi del grafico
27
+ width: null, // width e height non possono essere null, vanno verificate prima
28
+ height: null,
29
+ eti_asseX_height: null, // non può essere null, va verificato prima
30
+ eti_asseY_width: null, // non può essere null, va verificato prima
31
+ stepY_count: null, // non può essere null, va verificato prima
32
+ stepX_count: null, // non può essere null, va verificato prima
33
+ ticksLength: 0,
34
+
35
+ ...params
36
+ };
37
+
38
+ ['width', 'height', 'eti_asseX_height', 'eti_asseY_width', 'stepY_count', 'stepX_count', 'ticksLength']
39
+ .forEach(key => {
40
+ if(params[key] == null) {
41
+ throw `‘${key}’ non può essere null`;
42
+ }
43
+ });
44
+
45
+
46
+
47
+ // =>> PARAMETRI calcolati
48
+ //---------------
49
+
50
+ // calcolo valore massimo dei dati se non già impostato in precedenza
51
+ if(params.max_value == null) {
52
+ parsed.max_value = Math.max(0, ...params.values.flat());
53
+ } else {
54
+ parsed.max_value = params.max_value;
55
+ }
56
+
57
+ // calcolo valore minimo dei dati se non già impostato in precedenza
58
+ if(params.min_value == null) {
59
+ parsed.min_value = Math.min(parsed.max_value, ...params.values.flat().filter(v => v != null));
60
+ } else {
61
+ parsed.min_value = params.min_value;
62
+ }
63
+
64
+ const legenda_height = params.legenda.height?? 0;
65
+ const gap_legenda = legenda_height? 10 : 0; // spazio tra legenda e grafico
66
+
67
+ // =>> chart area
68
+ // coordinate e dimensioni del rettangolo interno all'svg entro cui viene disegnato il grafico
69
+ // i lati sx e inferiore corrispondono agli assi
70
+ parsed.chart_area = {
71
+ top : legenda_height + gap_legenda + params.padding, // coordinata y superiore
72
+ right : params.width - params.padding, // coordinata x dx
73
+ bottom : params.height - params.eti_asseX_height - params.ticksLength - params.padding, // coordinata y inferiore
74
+ left : params.eti_asseY_width + params.ticksLength + params.padding // coordinata x sx
75
+ };
76
+ parsed.chart_area.width = parsed.chart_area.right - parsed.chart_area.left;
77
+ parsed.chart_area.height = parsed.chart_area.bottom - parsed.chart_area.top;
78
+
79
+ // valore di ogni step asse y
80
+ // (usato per il calcolo delle etichette dell'asse Y)
81
+ parsed.stepY_value = (parsed.max_value - parsed.min_value) / params.stepY_count;
82
+
83
+ // lunghezza di ogni step asse y
84
+ parsed.stepY_size = parsed.chart_area.height / params.stepY_count;
85
+
86
+
87
+ parsed.stepX_size = parsed.chart_area.width / (params.stepX_count - 1);
88
+
89
+
90
+ return parsed;
91
+
92
+ } catch(e) {
93
+ console.error( 'MinimoCharts → parseStdOpts', e ); // eslint-disable-line
94
+ }
95
+ }
@@ -0,0 +1,45 @@
1
+ // https://stackblitz.com/edit/vitejs-vite-acphht?file=main.js
2
+
3
+ import opentype from 'opentype.js';
4
+
5
+ /*
6
+ // Example usage:
7
+ const fontUrl = './Rubik Moonrocks.ttf';
8
+ const text = 'Hello, World!';
9
+ const fontSize = 16; // in points
10
+ textToSvgPath(fontUrl, text, fontSize)
11
+ .then(({ svgPathData, svg }) => {
12
+ //console.log(svgPathData); // SVG path data string
13
+ console.log(svg); // SVG element as a string
14
+ })
15
+ .catch((err) => console.error(err));
16
+ */
17
+
18
+ // Function to load the font and convert text to SVG path
19
+ // vedi ada-frontend/ada-charts/test/textToPath-test.mjs
20
+
21
+ export async function textToSvgPath(fontUrl, text, fontSize) {
22
+ try {
23
+ const font = await opentype.load(fontUrl);
24
+
25
+ // Define the x and y coordinates where the text should start
26
+ const x = 0;
27
+ const y = 50;
28
+
29
+ // Get a path representing the text
30
+ const path = font.getPath(String(text), x, y, fontSize);
31
+
32
+ // Convert the path to an SVG path data string
33
+ const pathData = path.toPathData();
34
+
35
+ // Alternatively, convert the path to an SVG
36
+ const pathElementString = path.toSVG();
37
+
38
+ return { pathData, pathElementString };
39
+
40
+ } catch (err) {
41
+ // eslint-disable-next-line no-console
42
+ console.error('textToSvgPath - font loading error:', err);
43
+ throw err;
44
+ }
45
+ }