@massimo-cassandro/minimo 1.2.0 → 1.3.1

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,383 @@
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
+
7
+ const default_lines_attrs = {
8
+ stroke: '#999',
9
+ 'stroke-width': 1,
10
+ 'shape-rendering': 'crispEdges',
11
+ 'stroke-linecap': 'round',
12
+ fill: 'none'
13
+ };
14
+
15
+ const default_font_attrs = {
16
+ // family: 'sans-serif',
17
+ size: 14,
18
+ // stretch: 'normal',
19
+ // style: 'normal',
20
+ // variant: 'normal',
21
+ weight: 400,
22
+ fill: '#999'
23
+ };
24
+
25
+ const default_params = {
26
+
27
+ /** mostra info utili per il debug */
28
+ debug: false,
29
+
30
+ /** container (selettore o elemento DOM), se null viene restituito il codice SVG */
31
+ container: null,
32
+
33
+ /**
34
+ larghezza e altezza del grafico (px),
35
+ Se il container è presente e i parametro `width` e `height` sono `null`,
36
+ vengono utilizzate le dimensioni del container.
37
+ Se uno dei due valori non è impostato o ricavabile, viene generato un errore.
38
+ NB: il padding del container viene considerato nelle dimensioni del grafico,
39
+ è preferibile non impostarlo.
40
+ Il valore 'auto' di height fa sì che l'altezza dell'svg sia calcolata sulla base
41
+ del numero di barre e dei valori `barsGap` e `barsHeight`
42
+ */
43
+ width: null, // null || <value>
44
+ height: 300, // <value> || null || 'auto'
45
+
46
+ /** attributi facoltativi per l'svg (classe, ecc) */
47
+ svgAttrs: {},
48
+
49
+ /**
50
+ valori numerici per la definizione delle linee
51
+ è un array di array, in cui ogni subarray contiene i valori di una singola linea
52
+ l'ordine di rapprsentazione delle linee segue il loro ordine nell'array,
53
+ ovvero l'ultima serie corrisponde alla linea rappresentata per ultima e quindi sopra le altre
54
+ */
55
+ values: [],
56
+
57
+ /**
58
+ etichette corrispondenti ai vari valori. Sono posizionate sull'asse X
59
+ */
60
+ xLabels: [],
61
+
62
+ /** spazio extra attorno al bordo */
63
+ padding: 10,
64
+
65
+ /** suddivisioni asse Y */
66
+ stepY_count: 4,
67
+
68
+ /** px, altezza o larghezza ticks (secondo l'orientamento) */
69
+ ticksLength: 8,
70
+
71
+ /**
72
+ opzioni per legenda come da componente legenda sono impostati automaticamente)
73
+ NB: l'elemento `items` viene generato dinamicamente:
74
+ `shape` è sempre impostato su line
75
+ `attrs` è ricavato dagli attributi delle linee dati
76
+
77
+ Per utilizzare le impostazioni di default, è sufficiente impostare i valori `items.text`
78
+ */
79
+ legenda: {},
80
+
81
+ /**
82
+ larghezza area etichette asse y
83
+ */
84
+ yAxisLabelWidth: 60,
85
+
86
+ /** altezza area etichette asse x */
87
+ xAxisLabelHeight: 50,
88
+
89
+ /** titoli assi */
90
+ yAxisTitle: null,
91
+ yAxisTitleAttrs: {...default_font_attrs, size: 18},
92
+ yAxisTitleFontFilePath: null,
93
+
94
+ xAxisTitle: null,
95
+ xAxisTitleAttrs: {...default_font_attrs, size: 18},
96
+ xAxisTitleFontFilePath: null,
97
+
98
+ /** valori minimi e massimi per l'asse Y, se null vengono calcolati automaticamente */
99
+ max_value: null,
100
+ min_value: null,
101
+
102
+ /**
103
+ classi e attributi dei vari elementi.
104
+ Per gli attributi, ogni variabile è un oggetto di attributi svg.js, es =
105
+
106
+ {
107
+ stroke: '#414141',
108
+ 'stroke-width': 1,
109
+ 'shape-rendering': 'crispEdges',
110
+ class: 'xxxx'
111
+ }
112
+
113
+ */
114
+ axisAttrs: {...default_lines_attrs, 'stroke-width': 2},
115
+ ticksAttrs: default_lines_attrs,
116
+
117
+ /**
118
+ griglia assi x e y, in corrispondenza dei ticks
119
+ */
120
+ showXgrid: true, // griglia verticale
121
+ showYgrid: true, // griglia orizzontale
122
+ gridLinesAttrs: default_lines_attrs,
123
+
124
+ labelXAttrs: default_font_attrs, // contiene gli attributi fonts (come in MinimoCharts.defaults.font) e l'attributo `fill`
125
+ labelXFontFilePath: null, // percorso font per textToPath
126
+
127
+ labelYAttrs: default_font_attrs, // contiene gli attributi fonts (come in MinimoCharts.defaults.font) e l'attributo `fill`
128
+ labelYFontFilePath: null, // percorso font per textToPath
129
+
130
+ /**
131
+ attributi delle varie linee dati
132
+ se l'oggetto `pointsAttrs`, dove presente, non è vuoto,
133
+ viene aggiunto un cerchio ad ogni punto della linea
134
+ (NB non aggiungere `cx` o cy`)
135
+ il valore `r` può essere un numero (valore uguale per tutti i punti) o un array
136
+ in cui ogni valore si apllica alla linea con lo stesso indice
137
+ */
138
+ data_lines_attrs: {
139
+ ...default_lines_attrs,
140
+ 'stroke-width': 2
141
+ },
142
+
143
+ data_points_attrs: {
144
+ r: 4
145
+ },
146
+
147
+ /** colori delle linee */
148
+ data_colors: null,
149
+
150
+ /**
151
+ spessori delle linee
152
+ Se null, viene utilizzato per tutte le linee il valore definito in data_lines_attrs,
153
+ altrimenti è un array, in cui ogni valore corrisponde allo spessore da assegnare
154
+ alla linea con lo stesso indice.
155
+ I valori `undefined` o `null` vengono ricondotti al default `data_lines_attrs['stroke-width']`
156
+ (in sua mancanza a `1`)
157
+ */
158
+ data_lines_widths: null,
159
+
160
+
161
+
162
+ /**
163
+ Funzione per formattare le etichette dei valori dell'asse Y.
164
+ Null per nessuna eleborazione.
165
+ La funzione di default assume che il valore sia di tipo numerico
166
+ */
167
+ labelYFormatter: etiValue => Math.ceil(etiValue / 1e3)
168
+ .toLocaleString('it-IT', {
169
+ minimumFractionDigits: 0,
170
+ maximumFractionDigits: 0
171
+ }) +
172
+ (etiValue > 0? '\u2009K' : ''), // \u2009: thin space (https://www.compart.com/en/unicode/U+2009)
173
+
174
+ };
175
+ export async function lines(params) {
176
+
177
+ const chartInstance = this;
178
+
179
+ params = {...default_params, ...params};
180
+ params.data_colors??= chartInstance.defaults.colors;
181
+
182
+ try {
183
+
184
+ // controllo values
185
+ params.values.forEach(subarray => {
186
+ if(!Array.isArray(subarray)) {
187
+ throw 'L’elemento `values` deve essere un array di soli array';
188
+ }
189
+ });
190
+
191
+ const flatValues = params.values.flat(); // utilizzato anche in seguito
192
+ if(flatValues.filter(v => isNaN(v)).length) {
193
+ throw 'Tutti i valori dei sub-array di `values` devono essere numerici';
194
+ }
195
+
196
+
197
+ // container
198
+ [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
199
+
200
+ if(!params.width || !params.height) {
201
+ throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
202
+ }
203
+
204
+ // =>> ricalcolo data_lines_attrs (utilizzato anche per le linee della legenda)
205
+ const default_line_width = params.data_lines_attrs['stroke-width']?? 1;
206
+
207
+ // il ciclo è basato su params.values per essere certi che il numero di elementi coincida
208
+ params.data_lines_attrs = params.values.map((_, idx) => {
209
+ const thisColor = params.data_colors[idx],
210
+ thisLineWidth = (params.data_lines_widths??[])[idx]?? default_line_width;
211
+
212
+ return {
213
+ ...default_lines_attrs,
214
+ ...params.data_lines_attrs[idx]?? {},
215
+ stroke: thisColor,
216
+ 'stroke-width': thisLineWidth
217
+ };
218
+ });
219
+
220
+
221
+ // =>> legenda
222
+ // costruzione delle opzioni per la legenda
223
+ const opts_legenda = {
224
+
225
+ ...{
226
+ /** altezza legenda */
227
+ height: 20,
228
+
229
+ /** spazio tra gli elementi della legenda */
230
+ elements_gap: 25,
231
+
232
+ /** dimensioni delle forme della legenda (NB: solo linee) */
233
+ lines_width: 20,
234
+
235
+ /** attributi svg.js dei font del testo della legenda (null utilizza default) */
236
+ fontAttrs: {
237
+ fill: '#333',
238
+ 'font-size': 14,
239
+ 'font-weight': 400
240
+ },
241
+ fontFilePath: null,
242
+ },
243
+
244
+ ...params.legenda
245
+ };
246
+
247
+ // legenda.items (elementi della legenda)
248
+ // il ciclo viene eseguito su params.data_lines_attrs per essere certi che il numero di elementi coincida
249
+ // è utilizzato in ordine inverso in modo che il primo elmento della legenda corrisponda alla linea in primo piano
250
+ opts_legenda.items = params.data_lines_attrs.toReversed().map((line_attr, idx) => {
251
+ const thisLegendaItem = params.legenda.items.toReversed()[idx]?? {};
252
+ return {
253
+ ...thisLegendaItem,
254
+ shape: 'line',
255
+ attrs: {
256
+ ...(thisLegendaItem.attrs?? {}),
257
+ ...line_attr
258
+ }
259
+ };
260
+ });
261
+
262
+
263
+ // numero di step dell'asse X
264
+ const stepX_count = params.values.reduce((prev, curr) => Math.max(prev, curr.length), 0);
265
+
266
+ const svgCanvas = chartInstance.createSvgCanvas(params.container, params.debug)
267
+ .size(params.width, params.height)
268
+ .viewbox(0, 0, params.width, params.height)
269
+ .attr({...params.svgAttrs});
270
+
271
+ // pre-elaborazione e calcolo di alcuni elementi (chartArea)
272
+ // nuove variabili definite da parseStdAxisOpts
273
+ const {
274
+ max_value,
275
+ min_value,
276
+ chart_area,
277
+ stepY_value,
278
+ stepY_size,
279
+ stepX_size
280
+ } = parseStdAxisOpts({
281
+ max_value: params.max_value,
282
+ min_value: params.min_value,
283
+ values: flatValues,
284
+ legenda: opts_legenda,
285
+ padding: params.padding,
286
+ width: params.width,
287
+ height: params.height,
288
+ eti_asseX_height: params.xAxisLabelHeight,
289
+ eti_asseY_width: params.yAxisLabelWidth,
290
+ stepY_count: params.stepY_count,
291
+ stepX_count: stepX_count,
292
+ ticksLength: params.ticksLength,
293
+ }); // elaborazione standard
294
+
295
+ // =>> costruzione assi e griglia.
296
+ // Gli argomenti di cartesianAxis sono uguali a quelli di lines
297
+ // con l'aggiunta di quelli calcolati da parseStdAxisOpts
298
+ // non tutti sono necessari ma vengono passati lo stesso per comodità
299
+ const cartesianAxisGroup = await cartesianAxis(chartInstance, {
300
+ ...params,
301
+ ...{
302
+ max_value,
303
+ min_value,
304
+ chart_area,
305
+ stepY_value,
306
+ stepY_size,
307
+ stepX_size,
308
+ stepX_count
309
+ }
310
+ });
311
+
312
+ svgCanvas.add(cartesianAxisGroup);
313
+
314
+
315
+ // =>> disegno linee
316
+ const linesGroup = svgCanvas.group()
317
+ .attr({ 'data-debug-info': params.debug? 'Gruppo Linee' : null });
318
+
319
+ params.values.forEach((lineDataArray, idx) => {
320
+ const thisLineGroup = linesGroup.group()
321
+ .attr({ 'data-debug-info': params.debug? `Gruppo Linea ${idx}` : null }),
322
+ coords = []; // array delle coordinate
323
+
324
+
325
+ lineDataArray.forEach((pointValue, pointIdx) => {
326
+ // trasformazione valore in coordinate
327
+
328
+ const x = chart_area.left + stepX_size * pointIdx,
329
+ y = chart_area.top + chart_area.height - (((pointValue - min_value) * chart_area.height) / (max_value - min_value));
330
+
331
+ coords.push([x,y]);
332
+ });
333
+
334
+ thisLineGroup.polyline(coords).attr(params.data_lines_attrs[idx]);
335
+
336
+ // punti
337
+ const radius = Array.isArray(params.data_points_attrs.r)
338
+ ? params.data_points_attrs.r[idx]
339
+ : params.data_points_attrs.r;
340
+
341
+ coords.forEach((point, coord_idx) => {
342
+
343
+ thisLineGroup.circle()
344
+ .attr({
345
+ ...params.data_points_attrs,
346
+ r: radius,
347
+ fill: params.data_lines_attrs[idx].stroke,
348
+ cx: point[0],
349
+ cy: point[1],
350
+ 'data-debug-info': params.debug? `Valore: ${params.values[idx][coord_idx]}` : null
351
+ });
352
+ });
353
+
354
+
355
+ });
356
+
357
+
358
+ // =>> Legenda
359
+ const legendaEl = await legenda(chartInstance, opts_legenda);
360
+ legendaEl.attr({ 'data-cfg.debug-info': params.debug? 'Legenda' : null });
361
+ const legendaBbox = legendaEl.bbox();
362
+
363
+ legendaEl.dmove(
364
+ (params.width - legendaBbox.width) / 2,
365
+ params.padding
366
+ );
367
+ legendaEl.y(params.padding);
368
+
369
+ legendaEl.x((params.width - legendaBbox.width) / 2);
370
+
371
+ svgCanvas.add(legendaEl);
372
+
373
+
374
+ if(!params.container) {
375
+ return svgCanvas.svg();
376
+ }
377
+
378
+
379
+ } catch(e) {
380
+ console.error( '[MinimoCharts → lines]', e ); // eslint-disable-line
381
+ }
382
+
383
+ }
@@ -0,0 +1,124 @@
1
+ import { G } from '@svgdotjs/svg.js';
2
+ import { polarToCartesian, polarToCartesianString } from '../../chart-utils.js';
3
+
4
+ export function run({
5
+ mainDisplayCenter,
6
+ width,
7
+ height,
8
+ mainRadius,
9
+ // centerGap,
10
+ // scaleEndThickness,
11
+ centroSx,
12
+ rSx,
13
+ centroDx,
14
+ rDx
15
+ }) {
16
+
17
+ const debug_group = new G();
18
+ // centro display e rotazione asticella
19
+ debug_group.attr({
20
+ 'data-debug-info': 'centro rotazione asticella',
21
+ stroke: '#585151',
22
+ fill: 'none',
23
+ 'stroke-width': .5,
24
+ class: 'debug-info'
25
+ });
26
+
27
+ // assi orizzontali e verticali
28
+ debug_group.line({
29
+ x1: 0,
30
+ y1: mainDisplayCenter.y,
31
+ x2: width,
32
+ y2: mainDisplayCenter.y}
33
+ )
34
+ .attr({'data-debug-info': 'Asse orizzontale'});
35
+ debug_group.line({
36
+ x1: mainDisplayCenter.x,
37
+ y1: 0,
38
+ x2: mainDisplayCenter.x,
39
+ y2: height}
40
+ )
41
+ .attr({'data-debug-info': 'Asse verticale'});
42
+
43
+
44
+ debug_group.path(
45
+ `M${polarToCartesianString(210, mainDisplayCenter.x, mainDisplayCenter.y, mainRadius)}`+
46
+ `L${mainDisplayCenter.x},${mainDisplayCenter.y}` +
47
+ `L${polarToCartesianString(330, mainDisplayCenter.x, mainDisplayCenter.y, mainRadius)}`
48
+ ).attr({'data-debug-info': 'Raggi 210° e 330°'});
49
+
50
+ debug_group.path(
51
+ `M${mainDisplayCenter.x},${mainDisplayCenter.y}` +
52
+ `L${polarToCartesianString(30, mainDisplayCenter.x, mainDisplayCenter.y, mainRadius)}`
53
+ ).attr({'data-debug-info': 'raggio 30°'});
54
+
55
+
56
+ // centri circonferenze
57
+ const braccioCrocino = 20;
58
+ const radiusAttrs = {
59
+ 'stroke-width': 2,
60
+ 'stroke-dasharray': '8 3'
61
+ };
62
+
63
+ debug_group.path(
64
+ `M${centroSx.x - braccioCrocino},${centroSx.y}`+
65
+ `L${centroSx.x + braccioCrocino},${centroSx.y}` +
66
+ `M${centroSx.x},${centroSx.y - braccioCrocino}`+
67
+ `L${centroSx.x},${centroSx.y + braccioCrocino}`
68
+ ).attr({'data-debug-info': 'Centro C', stroke: '#2C7D06'});
69
+
70
+ // circonferenza verde e raggio
71
+ debug_group
72
+ .circle(rSx * 2)
73
+ .move(centroSx.x - rSx, centroSx.y - rSx)
74
+ .attr({
75
+ 'data-debug-info': 'Circ. c',
76
+ stroke: '#3C8A00',
77
+ 'stroke-width': 2,
78
+ });
79
+
80
+ debug_group
81
+ .path(
82
+ `M${centroSx.x},${centroSx.y}`+
83
+ `L${polarToCartesianString(120, centroSx.x, centroSx.y, rSx)}`
84
+ )
85
+ .attr({
86
+ 'data-debug-info': 'Raggio circ. c',
87
+ stroke: '#3C8A00',
88
+ ...radiusAttrs
89
+ });
90
+
91
+
92
+
93
+ // circonferenza rossa e raggio
94
+ debug_group.path(
95
+ `M${centroDx.x - braccioCrocino},${centroDx.y}`+
96
+ `L${centroDx.x + braccioCrocino},${centroDx.y}` +
97
+ `M${centroDx.x},${centroDx.y - braccioCrocino}`+
98
+ `L${centroDx.x},${centroDx.y + braccioCrocino}`
99
+ ).attr({'data-debug-info': 'Centro B', stroke: '#c00'});
100
+
101
+
102
+ debug_group
103
+ .circle(rDx * 2)
104
+ .move(centroDx.x - rDx, centroDx.y - rDx)
105
+ .attr({
106
+ 'data-debug-info': 'Circ. b',
107
+ stroke: '#c00',
108
+ 'stroke-width': 2,
109
+ });
110
+
111
+ debug_group
112
+ .path(
113
+ `M${centroDx.x},${centroDx.y}`+
114
+ `L${polarToCartesianString(60, centroDx.x, centroDx.y, rDx)}`
115
+ )
116
+ .attr({
117
+ 'data-debug-info': 'Raggio circ. b',
118
+ stroke: '#c00',
119
+ ...radiusAttrs
120
+ });
121
+
122
+
123
+ return debug_group;
124
+ }