@massimo-cassandro/minimo 1.2.0 → 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,42 @@
1
+ # Minimo-Charts
2
+
3
+ Genera grafici SVG per browser o registrandoli su disco (tramite node)
4
+
5
+ GEN 2026
6
+ * refactoring per rendere la classe tree-shakeable
7
+
8
+ LUG-NOV 2025
9
+ * Versione alpha
10
+
11
+ Richiede i package:
12
+
13
+ * `@svgdotjs/svg.js`
14
+ * `*opentype.js`
15
+ * `svgdom`
16
+
17
+ Modalità d'utilizzo:
18
+
19
+ * Plain JS (index.js): modalità standard, importa `svg.js` come modulo
20
+ * Node mode (index-node.js): per utilizzo con scripts Node
21
+
22
+ Test ed esempi nella directory `test`.
23
+
24
+
25
+
26
+ ## Uso (JS)
27
+
28
+ ```javascript
29
+ import { MinimoCharts, barsChart } from '@MinimoCharts/index.js';
30
+
31
+ const chartInstance = new MinimoCharts();
32
+
33
+ barsChart(chartInstance, { container: '#chart', values: [...] });
34
+ ```
35
+
36
+
37
+ ## Riferimenti ed esempi:
38
+
39
+ * https://codepen.io/massimo-cassandro/pen/zYRBJyG?editors=1100
40
+ * https://datavizproject.com/data-type/circular-bar-chart/
41
+ * https://datavizcatalogue.com/methods/radial_bar_chart.html
42
+
package/charts/TODO.md ADDED
@@ -0,0 +1,2 @@
1
+ completare
2
+ aggionare la documentazione e aggiungere i test a demo (in _wrk)
@@ -0,0 +1,21 @@
1
+
2
+ import { MinimoCharts } from './src/helpers/core.js';
3
+
4
+ import { bars } from './src/bars.js';
5
+ import { goal } from './src/goal.js';
6
+ import { ratingDisplay } from './src/rating-display/index.js';
7
+ // import { pie } from './src/charts/pie.js';
8
+ // import { hbars } from './src/charts/hbars.js';
9
+ // import { signalBars } from './src/charts/signal-bars.js';
10
+ // import { radialBars } from './src/charts/radial-bars.js';
11
+ // import { settimanaleMensile } from './src/charts/settimanale-mensile.js';
12
+ // import { mcn } from './src/charts/mcn.js';
13
+ // import { lines } from './src/charts/lines.js';
14
+
15
+
16
+ // wrapper
17
+ export const barsChart = (chartInstance, params) => bars.call(chartInstance, params);
18
+ export const goalChart = (chartInstance, params) => goal.call(chartInstance, params);
19
+ export const ratingDisplayChart = (chartInstance, params) => ratingDisplay.call(chartInstance, params);
20
+
21
+ export { MinimoCharts };
@@ -0,0 +1,377 @@
1
+ import { parseStdAxisOpts } from './helpers/parse-std-axis-opts.js';
2
+ import { parseContainer } from './helpers/chart-utils.js';
3
+ import { cartesianAxis } from './helpers/cartesian-axis.js';
4
+ import { legenda } from './helpers/legenda.js';
5
+
6
+ const default_lines_attrs = {
7
+ stroke: '#999',
8
+ 'stroke-width': 1,
9
+ 'shape-rendering': 'crispEdges',
10
+ 'stroke-linecap': 'round',
11
+ fill: 'none'
12
+ };
13
+
14
+ const default_font_attrs = {
15
+ // family: 'sans-serif',
16
+ size: 14,
17
+ // stretch: 'normal',
18
+ // style: 'normal',
19
+ // variant: 'normal',
20
+ weight: 400,
21
+ fill: '#999'
22
+ };
23
+
24
+ const default_params = {
25
+
26
+ /** mostra info utili per il debug */
27
+ debug: false,
28
+
29
+ /** container (selettore o elemento DOM), se null viene restituito il codice SVG */
30
+ container: null,
31
+
32
+ /**
33
+ larghezza e altezza del grafico (px),
34
+ Se il container è presente e i parametro `width` e `height` sono `null`,
35
+ vengono utilizzate le dimensioni del container.
36
+ Se uno dei due valori non è impostato o ricavabile, viene generato un errore.
37
+ NB: il padding del container viene considerato nelle dimensioni del grafico,
38
+ è preferibile non impostarlo.
39
+ Il valore 'auto' di height fa sì che l'altezza dell'svg sia calcolata sulla base
40
+ del numero di barre e dei valori `barsGap` e `barsHeight`
41
+ */
42
+ width: null, // null || <value>
43
+ height: 300, // <value> || null || 'auto'
44
+
45
+ /** attributi facoltativi per l'svg (classe, ecc) */
46
+ svgAttrs: {},
47
+
48
+ /**
49
+ valori numerici per la definizione delle linee
50
+ è un array di array, in cui ogni subarray contiene i valori di una singola serie di barre
51
+ */
52
+ values: [],
53
+
54
+ /** spazio tra una barre e l'altra */
55
+ barsGap: 5,
56
+
57
+ /** spazio tra una serie e l'altra */
58
+ seriesGap: 10,
59
+
60
+ /** bars top corner radius */
61
+ barsCornerRadius: 6,
62
+
63
+ /**
64
+ etichette corrispondenti ai vari valori. Sono posizionate sull'asse X
65
+ */
66
+ xLabels: [],
67
+
68
+ /** spazio extra attorno al bordo */
69
+ padding: 10,
70
+
71
+ /** suddivisioni asse Y */
72
+ stepY_count: 4,
73
+
74
+ /** px, altezza o larghezza ticks (secondo l'orientamento) */
75
+ ticksLength: 8,
76
+
77
+ /**
78
+ opzioni per legenda come da componente legenda sono impostati automaticamente)
79
+ NB: l'elemento `items` viene generato dinamicamente:
80
+ `shape` è sempre impostato su line
81
+ `attrs` è ricavato dagli attributi delle linee dati
82
+
83
+ Per utilizzare le impostazioni di default, è sufficiente impostare i valori `items.text`
84
+ */
85
+ legenda: {},
86
+
87
+ /**
88
+ larghezza area etichette asse y
89
+ */
90
+ yAxisLabelWidth: 60,
91
+
92
+ /** altezza area etichette asse x */
93
+ xAxisLabelHeight: 50,
94
+
95
+ /** titoli assi */
96
+ yAxisTitle: null,
97
+ yAxisTitleAttrs: {...default_font_attrs, size: 18},
98
+ yAxisTitleFontFilePath: null,
99
+
100
+ xAxisTitle: null,
101
+ xAxisTitleAttrs: {...default_font_attrs, size: 18},
102
+ xAxisTitleFontFilePath: null,
103
+
104
+ /** valori minimi e massimi per l'asse Y, se null vengono calcolati automaticamente */
105
+ max_value: null,
106
+ min_value: 0,
107
+
108
+ /**
109
+ classi e attributi dei vari elementi.
110
+ Per gli attributi, ogni variabile è un oggetto di attributi svg.js, es =
111
+
112
+ {
113
+ stroke: '#414141',
114
+ 'stroke-width': 1,
115
+ 'shape-rendering': 'crispEdges',
116
+ class: 'xxxx'
117
+ }
118
+
119
+ */
120
+ axisAttrs: {...default_lines_attrs, 'stroke-width': 2},
121
+ ticksAttrs: default_lines_attrs,
122
+
123
+ /**
124
+ griglia assi x e y, in corrispondenza dei ticks
125
+ */
126
+ showXgrid: false, // griglia verticale
127
+ showYgrid: true, // griglia orizzontale
128
+ gridLinesAttrs: default_lines_attrs,
129
+
130
+ labelXAttrs: default_font_attrs, // contiene gli attributi fonts (come in MinimoCharts.defaults.font) e l'attributo `fill`
131
+ labelXFontFilePath: null, // percorso font per textToPath
132
+
133
+ labelYAttrs: default_font_attrs, // contiene gli attributi fonts (come in MinimoCharts.defaults.font) e l'attributo `fill`
134
+ labelYFontFilePath: null, // percorso font per textToPath
135
+
136
+
137
+
138
+ /** colori delle barre */
139
+ data_colors: null,
140
+
141
+
142
+ /**
143
+ Funzione per formattare le etichette dei valori dell'asse Y.
144
+ Null per nessuna eleborazione.
145
+ La funzione di default assume che il valore sia di tipo numerico
146
+ */
147
+ labelYFormatter: etiValue => Math.ceil(etiValue / 1e3)
148
+ .toLocaleString('it-IT', {
149
+ minimumFractionDigits: 0,
150
+ maximumFractionDigits: 0
151
+ }) +
152
+ (etiValue > 0? '\u2009K' : ''), // \u2009: thin space (https://www.compart.com/en/unicode/U+2009)
153
+
154
+ };
155
+ export async function bars(params) {
156
+
157
+ const chartInstance = this;
158
+
159
+ params = {...default_params, ...params};
160
+ params.data_colors??= chartInstance.defaults.colors;
161
+
162
+ try {
163
+
164
+ // controllo values
165
+ params.values.forEach(subarray => {
166
+ if(!Array.isArray(subarray)) {
167
+ throw 'L’elemento `values` deve essere un array di soli array';
168
+ }
169
+ });
170
+
171
+ const flatValues = params.values.flat(); // utilizzato anche in seguito
172
+ if(flatValues.filter(v => isNaN(v)).length) {
173
+ throw 'Tutti i valori dei sub-array di `values` devono essere numerici';
174
+ }
175
+
176
+
177
+ // container
178
+ [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
179
+
180
+
181
+ if(!params.width || !params.height) {
182
+ throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
183
+ }
184
+
185
+ // =>> legenda
186
+ // costruzione delle opzioni per la legenda
187
+ const legenda_params = {
188
+
189
+ ...{
190
+ /** altezza legenda */
191
+ height: 20,
192
+
193
+ /** spazio tra gli elementi della legenda */
194
+ elements_gap: 25,
195
+
196
+ /** dimensioni delle forme della legenda (NB: solo tondi) */
197
+ circles_radius: 20,
198
+
199
+ /** attributi svg.js dei font del testo della legenda (null utilizza default) */
200
+ fontAttrs: {
201
+ fill: '#333',
202
+ 'font-size': 14,
203
+ 'font-weight': 400
204
+ },
205
+ fontFilePath: null,
206
+ },
207
+
208
+ ...params.legenda
209
+ };
210
+
211
+
212
+ // legenda.items (elementi della legenda)
213
+ legenda_params.items = params.legenda.items.map((item, idx) => {
214
+ return {
215
+ ...item,
216
+ shape: 'circle',
217
+ attrs: {
218
+ ...(item.attrs?? {}),
219
+ fill: params.data_colors[idx]?? '#666',
220
+ }
221
+ };
222
+ });
223
+
224
+
225
+ // numero di step dell'asse X
226
+ const stepX_count = params.values.reduce((prev, curr) => Math.max(prev, curr.length), 0) + 1;
227
+
228
+ const svgCanvas = chartInstance.createSvgCanvas(params.container, params.debug)
229
+ // .size(params.width, params.height)
230
+ .viewbox(0, 0, params.width, params.height)
231
+ .attr({...params.svgAttrs});
232
+
233
+
234
+ // pre-elaborazione e calcolo di alcuni elementi (chartArea)
235
+ // nuove variabili definite da parseStdAxisOpts
236
+ const {
237
+ max_value,
238
+ min_value,
239
+ chart_area,
240
+ stepY_value,
241
+ stepY_size,
242
+ stepX_size
243
+ } = parseStdAxisOpts({
244
+ max_value: params.max_value,
245
+ min_value: params.min_value,
246
+ values: flatValues,
247
+ legenda: legenda_params,
248
+ padding: params.padding,
249
+ width: params.width,
250
+ height: params.height,
251
+ eti_asseX_height: params.xAxisLabelHeight,
252
+ eti_asseY_width: params.yAxisLabelWidth,
253
+ stepY_count: params.stepY_count,
254
+ stepX_count: stepX_count,
255
+ ticksLength: params.ticksLength,
256
+ }); // elaborazione standard
257
+
258
+ // =>> costruzione assi e griglia.
259
+ // Gli argomenti di cartesianAxis sono uguali a quelli di bars
260
+ // con l'aggiunta di quelli calcolati da parseStdAxisOpts
261
+ // non tutti sono necessari ma vengono passati lo stesso per comodità
262
+ const cartesianAxisGroup = await cartesianAxis(chartInstance, {
263
+ ...params,
264
+ xLabelsOnTicks: false, // posiziona l'etichetta al centro di ogni step X
265
+ ...{
266
+ max_value,
267
+ min_value,
268
+ chart_area,
269
+ stepY_value,
270
+ stepY_size,
271
+ stepX_size,
272
+ stepX_count
273
+ }
274
+ });
275
+
276
+ svgCanvas.add(cartesianAxisGroup);
277
+
278
+ // *********************************
279
+ // =>> disegno barre
280
+ // *********************************
281
+
282
+ // calcolo larghezza barre
283
+ const barsWidth = (stepX_size - params.seriesGap - (params.barsGap * (params.values.length - 1))) / params.values.length
284
+ ,barsCornerRadius = Math.min(params.barsCornerRadius, barsWidth / 2) // il reggio dell'angolo non può essere più grandedella barra stessa
285
+ ;
286
+
287
+
288
+ const barsGroup = svgCanvas.group()
289
+ .attr({ 'data-debug-info': params.debug? 'Gruppo barre' : null });
290
+
291
+ let barX = chart_area.left + params.seriesGap / 2;
292
+
293
+ for (let thisStepIdx = 0; thisStepIdx < stepX_count; thisStepIdx++) {
294
+
295
+ const thisStepValues = params.values.reduce((result, curr) => {
296
+ result.push(curr[thisStepIdx]?? null);
297
+ return result;
298
+ }, []);
299
+
300
+ // stampa di ogni barra della serie
301
+ for (let idx = 0; idx < thisStepValues.length; idx++) {
302
+ const itemValue = thisStepValues[idx];
303
+ const isLast = idx === thisStepValues.length - 1;
304
+
305
+ if (itemValue != null) {
306
+ // calcolo coordinata Y e altezza assoluta della barra
307
+ const barY = chart_area.top + chart_area.height - (((itemValue - min_value) * chart_area.height) / (max_value - min_value)),
308
+ barH = chart_area.bottom - barY,
309
+ parteVerticaleBarra = barH - barsCornerRadius;
310
+ let path;
311
+
312
+ if(parteVerticaleBarra > 0) {
313
+
314
+ if(barH <= barsCornerRadius) {
315
+
316
+ const altezzaCornerRadius = barH;
317
+
318
+ path = `M${barX},${chart_area.bottom}` +
319
+ `q0,-${altezzaCornerRadius} ${barsCornerRadius},-${altezzaCornerRadius}` +
320
+ `h${barsWidth - barsCornerRadius * 2}` +
321
+ `q${barsCornerRadius},0 ${barsCornerRadius},${altezzaCornerRadius}` +
322
+ 'z';
323
+
324
+ } else {
325
+ path = `M${barX},${chart_area.bottom}` +
326
+ `v-${parteVerticaleBarra}` +
327
+ `q0,-${barsCornerRadius} ${barsCornerRadius},-${barsCornerRadius}` +
328
+ `h${barsWidth - barsCornerRadius * 2}` +
329
+ `q${barsCornerRadius},0 ${barsCornerRadius},${barsCornerRadius}` +
330
+ `v${parteVerticaleBarra}` +
331
+ 'z';
332
+ }
333
+
334
+ barsGroup.path(path)
335
+ .attr({
336
+ fill: params.data_colors[idx]?? '#666',
337
+ 'data-debug-info': params.debug? `value: ${itemValue}` : null
338
+ });
339
+ }
340
+ }
341
+
342
+ barX += barsWidth + (isLast ? 0 : params.barsGap);
343
+ }
344
+
345
+ barX += params.seriesGap;
346
+ }
347
+
348
+
349
+
350
+ // *********************************
351
+ // =>> Legenda
352
+ // *********************************
353
+ const legendaEl = await legenda(chartInstance, legenda_params);
354
+ legendaEl.attr({ 'data-cfg.debug-info': params.debug? 'Legenda' : null });
355
+ const legendaBbox = legendaEl.bbox();
356
+
357
+ legendaEl.dmove(
358
+ (params.width - legendaBbox.width) / 2,
359
+ params.padding
360
+ );
361
+ legendaEl.y(params.padding);
362
+
363
+ legendaEl.x((params.width - legendaBbox.width) / 2);
364
+
365
+ svgCanvas.add(legendaEl);
366
+
367
+
368
+ if(!params.container) {
369
+ return svgCanvas.svg();
370
+ }
371
+
372
+
373
+ } catch(e) {
374
+ console.error( 'MinimoCharts / bars →', e ); // eslint-disable-line
375
+ }
376
+
377
+ }