@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.
- package/charts/README.md +42 -0
- package/charts/TODO.md +2 -0
- package/charts/index.js +21 -0
- package/charts/src/bars.js +377 -0
- package/charts/src/goal.js +311 -0
- package/charts/src/helpers/cartesian-axis.js +254 -0
- package/charts/src/helpers/chart-utils.js +174 -0
- package/charts/src/helpers/core.js +25 -0
- package/charts/src/helpers/create-svg-canvas.js +25 -0
- package/charts/src/helpers/create-text-element.js +54 -0
- package/charts/src/helpers/default-chart-colors.js +17 -0
- package/charts/src/helpers/legenda.js +96 -0
- package/charts/src/helpers/parse-std-axis-opts.js +95 -0
- package/charts/src/helpers/svg-text-to-path.js +45 -0
- package/charts/src/lines.js +383 -0
- package/charts/src/rating-display/debug-tools.js +124 -0
- package/charts/src/rating-display/index.js +849 -0
- package/charts/src/rating-display/ratindDisplayAnimation.js +56 -0
- package/charts/src/rating-display/ratingDisplayUtils.js +102 -0
- package/package.json +3 -2
package/charts/README.md
ADDED
|
@@ -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
package/charts/index.js
ADDED
|
@@ -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
|
+
}
|