@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
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genera un indicatore grafico di tipo rating/tachimetro.
|
|
3
|
+
* Mostra un'asticella mobile su una scala suddivisa in porzioni colorate.
|
|
4
|
+
* Supporta la visualizzazione di etichette principali e fino a due mini-display secondari.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} options - Oggetto contenente i parametri di configurazione.
|
|
7
|
+
* @returns {Promise<string|undefined>} Restituisce la stringa SVG se container è null, altrimenti la renderizza nel DOM.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import { parseContainer, polarToCartesianString, polarToCartesian } from '../chart-utils.js';
|
|
12
|
+
import { createTextEl } from '../create-text-element.js';
|
|
13
|
+
import { rad30, getDisplayBackgroundPath, calcolaPuntoInterno, normalizeAngle, isCircleDx } from './ratingDisplayUtils.js';
|
|
14
|
+
|
|
15
|
+
export async function ratingDisplay({
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {boolean} [debug=false] - Modalità debug: se attiva mostra i riferimenti grafici (centri di rotazione, angoli) e i metadati data-debug-info.
|
|
19
|
+
*/
|
|
20
|
+
debug = false,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {HTMLElement|string|null} [container=null] - Elemento DOM o selettore CSS dove iniettare il grafico. Se null, la funzione restituisce l'SVG come stringa.
|
|
24
|
+
*/
|
|
25
|
+
container = null,
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {number|null} [width=null] - Larghezza del documento SVG. Obbligatoria se eseguito in ambiente Node.
|
|
29
|
+
*/
|
|
30
|
+
width = null,
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {number|function} [centerGap=r=>r/8] - Distanza tra il centro della circonferenza esterna e ciascuna delle circonferenze interne (*dA* e *dB* nella documentzione).
|
|
34
|
+
* Numero o funzione del raggio dato.
|
|
35
|
+
*/
|
|
36
|
+
centerGap = r => r/9,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {number|function} [scaleEndThickness=r=>r/8] - Spessore finale della scala (indicato con *s* nella documentazione).
|
|
40
|
+
* Numero o funzione del raggio dato.
|
|
41
|
+
*/
|
|
42
|
+
scaleEndThickness = r => r/5,
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {number} [portions=5] - Numero di porzioni (sezioni di colore) in cui è diviso il tachimetro.
|
|
46
|
+
*/
|
|
47
|
+
portions = 5,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {number} [portionGap=10] - Distanza tra una porzione e la successiva.
|
|
51
|
+
* Corrisponde all'angolo in gradi tra le due semirette passanti per il centro della circonferenza esterna e i segmenti laterali delle porzioni.
|
|
52
|
+
* Indicato con *gamma* nella documentazione
|
|
53
|
+
*/
|
|
54
|
+
portionGap = 2,
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} [svgAttrs={}] - Attributi aggiuntivi (es. class, id) da applicare al tag SVG radice.
|
|
58
|
+
*/
|
|
59
|
+
svgAttrs = {},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object} [displayBackgroundAttrs] - Attributi grafici (fill, stroke) per l'arco di sfondo esterno.
|
|
63
|
+
*/
|
|
64
|
+
displayBackgroundAttrs = {
|
|
65
|
+
fill: 'none',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {number} [displayPadding=10] - Spazio tra il bordo del background e gli elementi interni del display.
|
|
70
|
+
*/
|
|
71
|
+
displayPadding = 10,
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {number} [scaleStartValue=0] - Valore minimo della scala numerica da cui parte il calcolo della posizione.
|
|
75
|
+
*/
|
|
76
|
+
scaleStartValue = 0,
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {boolean} [rodPositionForceCenter=false] - Se true, l'asticella punta sempre al centro della fascia; se false, segue la proporzione del valore esatto.
|
|
80
|
+
*/
|
|
81
|
+
rodPositionForceCenter = false,
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {number|null} [value=null] - Valore numerico da rappresentare. Determina il colore e la posizione dell'asticella.
|
|
86
|
+
*/
|
|
87
|
+
value = null,
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string[]|null} [scaleColors=null] - Array di colori per le porzioni della scala. Se null, usa i colori di default della chartInstance.
|
|
91
|
+
*/
|
|
92
|
+
scaleColors = null,
|
|
93
|
+
|
|
94
|
+
/** * @param {Object|null} [scaleAttrs=null] - Attributi SVG aggiuntivi applicati ai percorsi (path) delle porzioni della scala.
|
|
95
|
+
*/
|
|
96
|
+
scaleAttrs = null,
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string|null} [scaleTicksColor=null] - Colore dei segni di separazione tra le porzioni. Se null, usa lo stesso colore della porzione adiacente.
|
|
100
|
+
*/
|
|
101
|
+
scaleTicksColor = null,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {Object} [rodAttrs] - Attributi per l'asticella indicatrice e il suo anello centrale.
|
|
105
|
+
*/
|
|
106
|
+
rodAttrs = {
|
|
107
|
+
fill: '#000'
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {Object[]|null} [displayLabel=null] - Testo centrale sotto l'asticella. Array di max 2 oggetti: {label, font, fill, fontFilePath}.
|
|
112
|
+
*/
|
|
113
|
+
displayLabel = null,
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} [displayLabelTopMargin=4] - Distanza verticale tra l'anello dell'asticella e la prima riga dell'etichetta.
|
|
117
|
+
*/
|
|
118
|
+
displayLabelTopMargin = 4,
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {number} [displayLabelRowGap=2] - Spazio verticale tra la prima e la seconda riga dell'etichetta principale.
|
|
122
|
+
*/
|
|
123
|
+
displayLabelRowGap = 2,
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Object[]|null} [miniDisplay=null] - Configurazione mini display secondari (max 2). Srray di uno o due oggetti
|
|
127
|
+
* @param {string} miniDisplay[].position - Posizione: 'sx' | 'dx' (alias: 'left' | 'right').
|
|
128
|
+
* @param {number} miniDisplay[].value - Valore numerico da rappresentare.
|
|
129
|
+
* @param {string} miniDisplay[].type - Tipo di visualizzazione: 'gauge' (tachimetro) o 'value' (testo di `value`).
|
|
130
|
+
* @param {Object} [miniDisplay[].typeValueFont] - oggetto analogo a quelli definito in displayLabel per la rappresentazione di `type: value`. Se typeValueFont.label non è presente, viene utilizzato `value` (arrotondato all'intero)
|
|
131
|
+
* @param {Object[]} [miniDisplay[].mdLabel] - Array di etichette per il mini display (stessa struttura di displayLabel).
|
|
132
|
+
* @param {Object} [miniDisplay[].mdArcAttrs] - oggetto simile a displayBackgroundAttrs, è relativo all'arco che avvolge il mini display
|
|
133
|
+
* @param {Object} [miniDisplay[].mdRodAttrs] - Attributi per l'asticella del mini display.
|
|
134
|
+
*/
|
|
135
|
+
miniDisplay = null,
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {number} [miniDisplayTopMargin=0] - Distanza verticale tra la base del display principale e l'inizio dei mini display.
|
|
139
|
+
*/
|
|
140
|
+
miniDisplayTopMargin = 0,
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {number} [miniDisplayLabelTopMargin=0] - Distanza verticale tra la base dell'arco del mini display e le sue etichette.
|
|
144
|
+
*/
|
|
145
|
+
miniDisplayLabelTopMargin = 0,
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {number} [miniDisplayLabelRowGap=2] - Distanza tra le righe di testo (mdLabel) del mini display.
|
|
149
|
+
*/
|
|
150
|
+
miniDisplayLabelRowGap = 2,
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {boolean} [animation=false] - Se true, abilita l'animazione dell'asticella (richiede implementazione specifica).
|
|
154
|
+
*/
|
|
155
|
+
animation = false,
|
|
156
|
+
|
|
157
|
+
// animation_ms = 100, // number
|
|
158
|
+
// animation_fps = 60, // number
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
}) {
|
|
162
|
+
|
|
163
|
+
const chartInstance = this;
|
|
164
|
+
|
|
165
|
+
// TODO la rotazione dell'asticella deve tenere conto degli step, non può basarsi unicamente sul calcolo dell'angolo
|
|
166
|
+
// in modo particale se i valori degli step non sono uguali
|
|
167
|
+
// TODO opzione per prozioni disuguali basate sul loro valore? (es. 3 porzioni con rapporto 1 1 2, in cui l'ultima corrisponde ad un valore delle altre)
|
|
168
|
+
// TODO valutare l'utilizzo di un ovale ancihé un cerchio per la figura base
|
|
169
|
+
try {
|
|
170
|
+
|
|
171
|
+
scaleColors??= chartInstance.defaults.colors;
|
|
172
|
+
|
|
173
|
+
// colore e indice della porzione corrispondenti al valore
|
|
174
|
+
const getColorFromValue = value => {
|
|
175
|
+
const portionIndex = Math.max(
|
|
176
|
+
0,
|
|
177
|
+
Math.min( portions - 1, Math.floor(value) - scaleStartValue)
|
|
178
|
+
);
|
|
179
|
+
return [portionIndex, scaleColors[portionIndex]];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
// container element e rilevamento larghezza se necessario
|
|
184
|
+
// l'altezza viene calcolata alla fine
|
|
185
|
+
[container, width] = parseContainer({ container: container, width: width });
|
|
186
|
+
|
|
187
|
+
// =>> display principale
|
|
188
|
+
// raggio circonferenza esterna
|
|
189
|
+
const mainRadius = (width/2) - parseFloat(displayBackgroundAttrs?.['stroke-width']??0) - displayPadding??0,
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
altezza dell'area del display
|
|
193
|
+
è pari al raggio + la distanza corrispondente ad uno spostamento di 30°
|
|
194
|
+
(l'arco di cerchio dell'area display è di 240°),
|
|
195
|
+
distanza che corrisponde al centro di rotazione dell'asticella.
|
|
196
|
+
La base dell'area del display principale è quindi la corda posizionata agli estremi dell'arco.
|
|
197
|
+
In questa fase corrisponde all'altezza dell'SVG.
|
|
198
|
+
Viene ricalcolato dopo l'eventuale aggiunta dei miniDisplay
|
|
199
|
+
*/
|
|
200
|
+
height = mainRadius + (mainRadius * Math.sin(rad30)) +
|
|
201
|
+
// spessore traccia arco esterno, se presente (solo per la parte superiore)
|
|
202
|
+
parseFloat(displayBackgroundAttrs?.['stroke-width']??0) +
|
|
203
|
+
// padding
|
|
204
|
+
displayPadding??0 * 2,
|
|
205
|
+
/*
|
|
206
|
+
centro circonferenza principale = centro di rotazione dell'asticella
|
|
207
|
+
*/
|
|
208
|
+
mainDisplayCenter = {x: width/2, y: width/2}
|
|
209
|
+
;
|
|
210
|
+
|
|
211
|
+
centerGap = typeof centerGap === 'number'? centerGap : centerGap(mainRadius);
|
|
212
|
+
scaleEndThickness = typeof scaleEndThickness === 'number'? scaleEndThickness : scaleEndThickness(mainRadius);
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// =>> verifiche e impostazioni di base
|
|
216
|
+
if(value == null || typeof value !== 'number') {
|
|
217
|
+
throw '`value` è obbligatorio e deve essere un numero';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// valore massimo rappresentabile
|
|
221
|
+
value = Math.min(value, portions);
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// il centro `C` deve rimanere all'interno della circonferenza `a` per permettere la tangenza nel punto `Tc`:
|
|
225
|
+
// dC < rA
|
|
226
|
+
if(centerGap >= mainRadius) {
|
|
227
|
+
throw 'Il valore `centerGap` deve essere minore della lunghezza del raggio principale `mainRadius`';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
// lo spessore finale `s` della scala non deve portare il raggio `rB` a valori negativi o incoerenti:
|
|
232
|
+
// dB + s < rA
|
|
233
|
+
if(centerGap + scaleEndThickness >= mainRadius) {
|
|
234
|
+
throw 'La somma dei valori `centerGap` e `scaleEndThickness` deve essere inferiore alla lunghezza del raggio principale `mainRadius`';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
la scala parte da 1 e arriva a `portions` (viene tolta una piccola
|
|
240
|
+
porzione finale per maggiore precisione)
|
|
241
|
+
*/
|
|
242
|
+
const parsedDisplayValue = rodPositionForceCenter? Math.floor(value) + .499 : value;
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
// =>> animazione e posizione iniziale dell'asticella (rod)
|
|
246
|
+
let rotazioneAsticella = 0;
|
|
247
|
+
|
|
248
|
+
if(animation) {
|
|
249
|
+
|
|
250
|
+
// TODO animazione
|
|
251
|
+
|
|
252
|
+
// // NB la mutazione non è rilevata se la card è già visibile nel viewport
|
|
253
|
+
// // per questo è presente l'if iniziale
|
|
254
|
+
// if(inViewport) {
|
|
255
|
+
// display_animation();
|
|
256
|
+
// }
|
|
257
|
+
|
|
258
|
+
} else {
|
|
259
|
+
rotazioneAsticella = Math.max(0, ((parsedDisplayValue - scaleStartValue) * 240) / (portions - .001));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// =>> definizione SVG (viewbox e size vengono impostati alla fine, perché vengono definiti durante l'elaborazione)
|
|
265
|
+
const svgCanvas = chartInstance.createSvgCanvas(container);
|
|
266
|
+
svgCanvas
|
|
267
|
+
.fill('none')
|
|
268
|
+
.attr({...svgAttrs})
|
|
269
|
+
;
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
// =>> gruppo Display Principale
|
|
273
|
+
const mainDisplayGroup = svgCanvas.group()
|
|
274
|
+
.attr({
|
|
275
|
+
'data-debug-info': debug? 'Gruppo Display Principale' : null,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
// elemento che funge da background esterno del display
|
|
280
|
+
// NB: è aperto nella parte inferiore, quindi in basso l'eventuale bordo non viene applicato
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// =>> area background
|
|
284
|
+
// solo se esistono proprietà in `displayBackgroundAttrs`
|
|
285
|
+
|
|
286
|
+
if(Object.keys(displayBackgroundAttrs??{}).length) {
|
|
287
|
+
|
|
288
|
+
if(displayBackgroundAttrs.stroke && !displayBackgroundAttrs['stroke-width']) {
|
|
289
|
+
displayBackgroundAttrs['stroke-width'] = 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
mainDisplayGroup.path(
|
|
293
|
+
getDisplayBackgroundPath(width/2 - parseFloat(displayBackgroundAttrs?.['stroke-width']??0)/2, mainDisplayCenter.x, mainDisplayCenter.y)
|
|
294
|
+
)
|
|
295
|
+
.attr({
|
|
296
|
+
'data-debug-info': debug? 'area display principale' : null,
|
|
297
|
+
...(displayBackgroundAttrs??{})
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
// =>> scala
|
|
303
|
+
const scaleGroup = mainDisplayGroup.group().attr({
|
|
304
|
+
'data-debug-info': debug? 'scales' : null,
|
|
305
|
+
stroke: 'none',
|
|
306
|
+
...(scaleAttrs?? {}),
|
|
307
|
+
}),
|
|
308
|
+
ticksGroup = mainDisplayGroup.group().attr({
|
|
309
|
+
'data-debug-info': debug? 'ticks' : null,
|
|
310
|
+
fill: 'none',
|
|
311
|
+
'stroke-width': .5,
|
|
312
|
+
'stroke-linecap': 'butt',
|
|
313
|
+
}),
|
|
314
|
+
angoloInizio = 210,
|
|
315
|
+
ampiezzaTotale = 240,
|
|
316
|
+
ampiezzaPorzione = ampiezzaTotale / portions,
|
|
317
|
+
|
|
318
|
+
// parametri condivisi dalla funzione di calcolo dei punti interni
|
|
319
|
+
// i
|
|
320
|
+
configCalcoloPuntiInterni = {
|
|
321
|
+
// raggi della circonferenze di sinistra e destra (nella documentazione *c*, in verde, e *b*, in rosso)
|
|
322
|
+
rSx: mainRadius - centerGap, // rB
|
|
323
|
+
rDx: mainRadius - centerGap - scaleEndThickness, // rC
|
|
324
|
+
|
|
325
|
+
// centri delle circonferenze, non necesarie alla funzione calcolaPuntoInterno ma incluse in questo oggetto per altri utilizzi
|
|
326
|
+
centroSx: polarToCartesian(210, mainDisplayCenter.x, mainDisplayCenter.y, centerGap), // C nella documentazione
|
|
327
|
+
centroDx: polarToCartesian(30, mainDisplayCenter.x, mainDisplayCenter.y, centerGap), // B
|
|
328
|
+
|
|
329
|
+
mainCenter: mainDisplayCenter,
|
|
330
|
+
centerGap: centerGap,
|
|
331
|
+
|
|
332
|
+
// scaleEndThickness: scaleEndThickness
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// =>> porzioni e ticks
|
|
336
|
+
for(let p = 0; p < portions; p++) {
|
|
337
|
+
|
|
338
|
+
// if(p < portions - 1) continue;
|
|
339
|
+
|
|
340
|
+
let startDeg = angoloInizio - (p * ampiezzaPorzione),
|
|
341
|
+
endDeg = startDeg - ampiezzaPorzione;
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
// aggiunta gap
|
|
345
|
+
if(p > 0) {
|
|
346
|
+
startDeg -= portionGap/2;
|
|
347
|
+
}
|
|
348
|
+
if (p === portions - 1) {
|
|
349
|
+
endDeg = angoloInizio - ampiezzaTotale; // -30
|
|
350
|
+
} else {
|
|
351
|
+
endDeg += portionGap/2;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
startDeg = normalizeAngle(startDeg);
|
|
355
|
+
endDeg = normalizeAngle(endDeg);
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
// seguendo la documentazione, i 4 punti da indiduare per ogni porzione sono
|
|
359
|
+
// punti esterni esternoSX (D nella documentazione) e esternoDX (E)
|
|
360
|
+
// punti interni: internoSX (G) e internoDX (F)
|
|
361
|
+
// nella porzione iniziale esternoSX e internoSX coincidono
|
|
362
|
+
|
|
363
|
+
// punti esterni: corrispondono alle coordinate polari per l'angolo dato e il raggio mainRadius
|
|
364
|
+
const esternoSX = polarToCartesianString(startDeg, mainDisplayCenter.x, mainDisplayCenter.y, mainRadius),
|
|
365
|
+
esternoDX = polarToCartesianString(endDeg, mainDisplayCenter.x, mainDisplayCenter.y, mainRadius);
|
|
366
|
+
|
|
367
|
+
// punti interni
|
|
368
|
+
// sono ricavati da una funzione che calcola la distanza rho dal centro della circ. esterna (rA),
|
|
369
|
+
// basandosi su:
|
|
370
|
+
// l'angolo della semiretta che contiene il punto
|
|
371
|
+
// la distanza centerGap tra il centro della circonferenza principale e quella interna in uso (d nella documentazione)
|
|
372
|
+
// e lo spessore finale della scala (scaleEndThickness = s nella documentazione)
|
|
373
|
+
|
|
374
|
+
// tutti i parametri, escluso l'angolo, sono uguali per tutti i punti e sono definiti
|
|
375
|
+
// nell'oggetto configPuntiInterni, definito in precedenza
|
|
376
|
+
|
|
377
|
+
const internoSX = calcolaPuntoInterno({angoloDeg: startDeg, ...configCalcoloPuntiInterni}), // sx
|
|
378
|
+
internoDX = calcolaPuntoInterno({angoloDeg: endDeg, ...configCalcoloPuntiInterni}); // dx
|
|
379
|
+
|
|
380
|
+
// se la porzione include l'angolo di 30° è necessario cambiare circonferenza
|
|
381
|
+
let cambioCirconferenza = '';
|
|
382
|
+
if(!isCircleDx(startDeg) && isCircleDx(endDeg)) {
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
// coordinate punto sulla circonferenza interna rossa con angolo di 30 gradi
|
|
386
|
+
const punto30 = polarToCartesianString(
|
|
387
|
+
30,
|
|
388
|
+
configCalcoloPuntiInterni.centroDx.x,
|
|
389
|
+
configCalcoloPuntiInterni.centroDx.y,
|
|
390
|
+
configCalcoloPuntiInterni.rDx
|
|
391
|
+
);
|
|
392
|
+
cambioCirconferenza =
|
|
393
|
+
`A${configCalcoloPuntiInterni.centroDx.x},${configCalcoloPuntiInterni.centroDx.y} 0 0 0 ${punto30}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const path = `M${esternoSX}` + // punto esterno sx
|
|
397
|
+
// rx ry x-axis-angle large-arc-flag sweep-flag end-x end-y
|
|
398
|
+
`A${mainDisplayCenter.x},${mainDisplayCenter.y} 0 0 1 ${esternoDX}` + // punto esterno dx
|
|
399
|
+
`L${internoDX}` + // punto interno dx
|
|
400
|
+
cambioCirconferenza +
|
|
401
|
+
`A${mainDisplayCenter.x},${mainDisplayCenter.y} 0 0 0 ${internoSX}` + // punto interno sx
|
|
402
|
+
'z'
|
|
403
|
+
|
|
404
|
+
;
|
|
405
|
+
|
|
406
|
+
console.log(p, path);
|
|
407
|
+
|
|
408
|
+
scaleGroup.path(path).attr({
|
|
409
|
+
fill: scaleColors?.[p]?? '#000',
|
|
410
|
+
...{fill: 'none', stroke: 'blue', 'stroke-width': 3},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
// ticksGroup.path(p)
|
|
415
|
+
// .attr({
|
|
416
|
+
// stroke: scaleTicksColor? scaleTicksColor : scaleColors?.[idx + 1]?? '#000',
|
|
417
|
+
// });
|
|
418
|
+
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
// =>> anello e asticella
|
|
423
|
+
const rodGroup = mainDisplayGroup.group()
|
|
424
|
+
.attr({
|
|
425
|
+
'data-debug-info': debug? 'asticella' : null
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// anello
|
|
429
|
+
// https://www.smashingmagazine.com/2019/03/svg-circle-decomposition-paths/
|
|
430
|
+
const ringPath = ({cx, cy, outerRadius, innerRadius}) => {
|
|
431
|
+
|
|
432
|
+
return `M${cx - outerRadius},${cy}`+
|
|
433
|
+
// rx ry x-axis-angle large-arc-flag sweep-flag end-x end-y
|
|
434
|
+
`A${outerRadius},${outerRadius} 0 1 0 ${cx+outerRadius},${cy}` +
|
|
435
|
+
`A${outerRadius},${outerRadius} 0 1 0 ${cx-outerRadius},${cy}` +
|
|
436
|
+
|
|
437
|
+
`M${cx - innerRadius},${cy}` +
|
|
438
|
+
`A${innerRadius},${innerRadius} 0 1 0 ${cx+innerRadius},${cy}` +
|
|
439
|
+
`A${innerRadius},${innerRadius} 0 1 0 ${cx-innerRadius},${cy}`;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// parametri condivisi tra asticella e anello
|
|
443
|
+
const ringOuterRadius = mainRadius / 9,
|
|
444
|
+
ringInnerRadius = mainRadius / 14
|
|
445
|
+
;
|
|
446
|
+
|
|
447
|
+
rodGroup.path(ringPath({
|
|
448
|
+
cx: mainDisplayCenter.x,
|
|
449
|
+
cy: mainDisplayCenter.y,
|
|
450
|
+
outerRadius: ringOuterRadius,
|
|
451
|
+
innerRadius: ringInnerRadius
|
|
452
|
+
}))
|
|
453
|
+
.attr({
|
|
454
|
+
'fill-rule': 'evenodd',
|
|
455
|
+
...(rodAttrs?? {})
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// bbox del solo anello di base dell'asticella
|
|
459
|
+
// usato per il posizionamento delle etichette
|
|
460
|
+
const rodGroupBaseBbox = rodGroup.bbox();
|
|
461
|
+
|
|
462
|
+
// asticella
|
|
463
|
+
// rodGroup.polygon('59.1813 139.0943 59.1813 139.0943 98.8572 108.491 105.5448 120.0899 59.1813 139.0943')
|
|
464
|
+
const ampiezzaAsticella = 90 // degrees
|
|
465
|
+
,lunghezzaAsticella = mainRadius * .55 // TODO considerare spessore finale scala
|
|
466
|
+
// distanza dal centro della circonferenza alla base dell'asticella, deve essere interno al'anello
|
|
467
|
+
,distanzaCentroBaseAsticella = ringInnerRadius + (ringOuterRadius - ringInnerRadius)/2
|
|
468
|
+
;
|
|
469
|
+
|
|
470
|
+
rodGroup.path(
|
|
471
|
+
`M${polarToCartesianString(
|
|
472
|
+
210,
|
|
473
|
+
mainDisplayCenter.x, mainDisplayCenter.y,
|
|
474
|
+
lunghezzaAsticella
|
|
475
|
+
)}`+
|
|
476
|
+
`L${polarToCartesianString(
|
|
477
|
+
210 - (ampiezzaAsticella/2),
|
|
478
|
+
mainDisplayCenter.x, mainDisplayCenter.y,
|
|
479
|
+
distanzaCentroBaseAsticella
|
|
480
|
+
)}`+
|
|
481
|
+
|
|
482
|
+
// rx ry angle large-arc-flag sweep-flag x y
|
|
483
|
+
`A${distanzaCentroBaseAsticella},${distanzaCentroBaseAsticella} ${ampiezzaAsticella} 0 0 `+
|
|
484
|
+
`${polarToCartesianString(
|
|
485
|
+
210 + (ampiezzaAsticella/2),
|
|
486
|
+
mainDisplayCenter.x, mainDisplayCenter.y,
|
|
487
|
+
distanzaCentroBaseAsticella
|
|
488
|
+
)}`+
|
|
489
|
+
'Z'
|
|
490
|
+
)
|
|
491
|
+
.transform({
|
|
492
|
+
origin: {
|
|
493
|
+
x: mainDisplayCenter.x,
|
|
494
|
+
y: mainDisplayCenter.y,
|
|
495
|
+
},
|
|
496
|
+
rotate: rotazioneAsticella
|
|
497
|
+
})
|
|
498
|
+
.attr({
|
|
499
|
+
'data-debug-info': debug? `Value: ${parsedDisplayValue}, {Angolo: ${rotazioneAsticella}, Start value: ${scaleStartValue}` : null,
|
|
500
|
+
...(rodAttrs?? {})
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
/*
|
|
504
|
+
// =>> function setLabel
|
|
505
|
+
const setLabel = async ({
|
|
506
|
+
labelObj,
|
|
507
|
+
baseX,
|
|
508
|
+
baseY,
|
|
509
|
+
dy,
|
|
510
|
+
rowGap = 0, // ulteriore correzione tra le distanze delle due righe
|
|
511
|
+
parentEl = svgCanvas
|
|
512
|
+
}) => {
|
|
513
|
+
if(labelObj?.label) {
|
|
514
|
+
|
|
515
|
+
const [label_bbox, labelEl] = await createTextEl (chartInstance, {
|
|
516
|
+
text: labelObj.label,
|
|
517
|
+
textFill: labelObj.fill??= '#000',
|
|
518
|
+
fontAttr: labelObj.font??{},
|
|
519
|
+
fontFilePath: labelObj.fontFilePath,
|
|
520
|
+
parentElement: parentEl
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// posizionamento Y
|
|
524
|
+
// dy è lo spostamento aggiuntivo pari all'altezza dell'eventuale riga precedente
|
|
525
|
+
const labelY = baseY + (dy? dy + rowGap : 0);
|
|
526
|
+
|
|
527
|
+
// posizionamento, al centro sotto l'asticella
|
|
528
|
+
labelEl.move(
|
|
529
|
+
baseX - label_bbox.width / 2,
|
|
530
|
+
labelY
|
|
531
|
+
)
|
|
532
|
+
.attr({
|
|
533
|
+
'data-debug-info': debug? `label “${labelObj.label}”` : null
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
return label_bbox.height;
|
|
537
|
+
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// =>> displayLabel
|
|
542
|
+
if(displayLabelTopMargin != null && typeof displayLabelTopMargin !== 'number') {
|
|
543
|
+
throw '`displayLabelTopMargin` deve essere null o un numero';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
displayLabel??=[];
|
|
547
|
+
if(displayLabel.length > 2) {
|
|
548
|
+
throw 'Non è possibile definire più di 2 elementi `displayLabel`';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let prevLabelHeight = 0;
|
|
552
|
+
for await (const labelObj of displayLabel) {
|
|
553
|
+
|
|
554
|
+
prevLabelHeight = await setLabel({
|
|
555
|
+
labelObj: labelObj,
|
|
556
|
+
baseX: mainDisplayCenter.x,
|
|
557
|
+
baseY: rodGroupBaseBbox.y2 + (displayLabelTopMargin??0),
|
|
558
|
+
dy: prevLabelHeight,
|
|
559
|
+
rowGap: displayLabelRowGap,
|
|
560
|
+
parentEl: mainDisplayGroup
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// =>> ricalcolo altezza gruppo principale
|
|
565
|
+
const mainDisplayGroupBbox = mainDisplayGroup.bbox();
|
|
566
|
+
height= mainDisplayGroupBbox.y + mainDisplayGroupBbox.y2;
|
|
567
|
+
|
|
568
|
+
// SECTION miniDisplay
|
|
569
|
+
if(miniDisplayTopMargin != null && typeof miniDisplayTopMargin !== 'number') {
|
|
570
|
+
throw '`miniDisplayTopMargin` deve essere null o un numero';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
miniDisplay??=[];
|
|
574
|
+
if(miniDisplay.length > 2) {
|
|
575
|
+
throw 'Non è possibile impostare più di 2 `miniDisplay`';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
let miniDisplayIdx = 1, miniDisplayMaxHeight = 0, mdArcAttrsHeight = null;
|
|
580
|
+
for await (const md of miniDisplay) {
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
const errorPrefix = `miniDisplay #${miniDisplayIdx}`;
|
|
584
|
+
|
|
585
|
+
// =>> controlli
|
|
586
|
+
['position', 'type', 'value'].forEach(item => {
|
|
587
|
+
if(md[item] == null) {
|
|
588
|
+
throw `${errorPrefix}: '${item}' è obbligatorio`;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if(['sx', 'dx', 'left', 'right'].indexOf(md.position) === -1) {
|
|
593
|
+
throw `${errorPrefix}: 'position' deve essere uno tra 'sx', 'dx', 'left' o 'right'`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if(typeof md.value !== 'number') {
|
|
597
|
+
throw `${errorPrefix}: 'value' deve essere un numero`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if(['gauge', 'value'].indexOf(md.type) === -1) {
|
|
601
|
+
throw `${errorPrefix}: 'type' deve essere uno tra 'gauge' o 'value'`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// default
|
|
605
|
+
md.mdArcAttrs = {
|
|
606
|
+
// defaults
|
|
607
|
+
...{
|
|
608
|
+
fill: 'none',
|
|
609
|
+
'stroke-linecap': 'round',
|
|
610
|
+
'stroke-width': 0
|
|
611
|
+
},
|
|
612
|
+
...(md.mdArcAttrs??{})
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
// =>> dimensioni
|
|
617
|
+
const
|
|
618
|
+
// raggio circonferenza che include l'arco
|
|
619
|
+
mdRadius = 26,
|
|
620
|
+
|
|
621
|
+
mdWidrh = (mdRadius * 2) + (md.mdArcAttrs?.['stroke-width']??0), // impegna sia la parte destra che la sinistra
|
|
622
|
+
|
|
623
|
+
// margine orizzontale dal bordo esterno dell'SVG
|
|
624
|
+
xMargin = 20,
|
|
625
|
+
|
|
626
|
+
// coordinate dell'origine del miniDisplay
|
|
627
|
+
mdOrigin = {
|
|
628
|
+
y: height+ (miniDisplayTopMargin?? 0),
|
|
629
|
+
x: (md.position === 'sx' || md.position === 'left')
|
|
630
|
+
? xMargin
|
|
631
|
+
: width - xMargin - mdWidrh
|
|
632
|
+
}
|
|
633
|
+
;
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
// =>> mdGroup
|
|
637
|
+
const mdGroup = svgCanvas.group()
|
|
638
|
+
.attr({
|
|
639
|
+
'data-debug-info': debug? `miniDisplay #${miniDisplayIdx}` : null,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// =>> definizione arco
|
|
643
|
+
const
|
|
644
|
+
// coordinate del centro della circonferenza che include l'arco
|
|
645
|
+
mdArcAttrsCx= mdOrigin.x + mdRadius, // + md.mdArcAttrs.['stroke-width'] / 2,
|
|
646
|
+
mdArcAttrsCy= mdOrigin.y + mdRadius // + md.mdArcAttrs.['stroke-width'] / 2,
|
|
647
|
+
;
|
|
648
|
+
|
|
649
|
+
// minidisplay arc
|
|
650
|
+
mdGroup.path(getDisplayBackgroundPath(mdRadius, mdArcAttrsCx, mdArcAttrsCy))
|
|
651
|
+
.attr({
|
|
652
|
+
'data-debug-info': debug? `miniDisplay #${miniDisplayIdx}: arco` : null,
|
|
653
|
+
...md.mdArcAttrs,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// altezza arco, per il posizionamento delle etichette
|
|
657
|
+
// viene misurato solo il primo arco, in modo che le etichette siano comq allineate
|
|
658
|
+
// se necessario, utilizzare il parametro miniDisplayLabelTopMargin per distanziare il testo dalla base dell'arco
|
|
659
|
+
if(mdArcAttrsHeight == null) {
|
|
660
|
+
mdArcAttrsHeight = mdGroup.bbox().height;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
// =>> modalità "value"
|
|
665
|
+
if(md.type === 'value') {
|
|
666
|
+
|
|
667
|
+
md.typeValueFont ??= {};
|
|
668
|
+
md.typeValueFont.label ??= String(Math.floor(md.value));
|
|
669
|
+
|
|
670
|
+
const [value_bbox, valueEl] = await createTextEl (chartInstance, {
|
|
671
|
+
text: md.typeValueFont.label,
|
|
672
|
+
textFill: md.typeValueFont.fill??= '#000',
|
|
673
|
+
fontAttr: md.typeValueFont.font??{},
|
|
674
|
+
fontFilePath: md.typeValueFont.fontFilePath,
|
|
675
|
+
parentElement: mdGroup
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
valueEl.move(
|
|
679
|
+
mdArcAttrsCx - value_bbox.w/2,
|
|
680
|
+
mdArcAttrsCy - value_bbox.h/2 - 2, // 2: spostamento basline verso l'alto
|
|
681
|
+
)
|
|
682
|
+
.attr({
|
|
683
|
+
'data-debug-info': debug? `miniDisplay #${miniDisplayIdx}: value` : null,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// =>> modalità "gauge"
|
|
687
|
+
} else {
|
|
688
|
+
|
|
689
|
+
// segmento indicatore colore
|
|
690
|
+
const segmentStrokeWidth = 6
|
|
691
|
+
// spazio segmento arco
|
|
692
|
+
,distanzaSegmentoArco = 2
|
|
693
|
+
// correzione raggio per dare spazio allo spessore della traccia
|
|
694
|
+
,segmentRadius = mdRadius - segmentStrokeWidth/2 - distanzaSegmentoArco
|
|
695
|
+
// ampiezza angolo che sottende il segmento circolare
|
|
696
|
+
// 30 è l'angolo del punto iniziale
|
|
697
|
+
,segmentAngle = toRadians(240/portions - 30)
|
|
698
|
+
|
|
699
|
+
,startPointX = mdArcAttrsCx - (segmentRadius * Math.cos(rad30))
|
|
700
|
+
,startPointY = mdArcAttrsCy + (segmentRadius * Math.sin(rad30))
|
|
701
|
+
,endPointX = mdArcAttrsCx - (segmentRadius * Math.cos(segmentAngle))
|
|
702
|
+
,endPointY = mdArcAttrsCy - (segmentRadius * Math.sin(segmentAngle))
|
|
703
|
+
|
|
704
|
+
,segmentPath = `M ${startPointX},${startPointY} ` +
|
|
705
|
+
// rx ry x-axis-angle large-arc-flag sweep-flag end-x end-y
|
|
706
|
+
`A ${segmentRadius} ${segmentRadius} 0 0 1 ${endPointX} ${endPointY}`
|
|
707
|
+
|
|
708
|
+
,segmentGroup = mdGroup.group()
|
|
709
|
+
;
|
|
710
|
+
|
|
711
|
+
// =>> segmento mini display gauge
|
|
712
|
+
segmentGroup.path(segmentPath)
|
|
713
|
+
.attr({
|
|
714
|
+
stroke: getColorFromValue(md.value)[1],
|
|
715
|
+
'stroke-width': segmentStrokeWidth,
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// =>> anello mini asticella
|
|
719
|
+
const ringOuterRadius = 12;
|
|
720
|
+
mdGroup.path( ringPath({
|
|
721
|
+
cx: mdArcAttrsCx,
|
|
722
|
+
cy: mdArcAttrsCy,
|
|
723
|
+
outerRadius: ringOuterRadius,
|
|
724
|
+
innerRadius: 6
|
|
725
|
+
}))
|
|
726
|
+
.attr({
|
|
727
|
+
...(rodAttrs?? {}),
|
|
728
|
+
'fill-rule': 'evenodd'
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// =>> mini asticella
|
|
732
|
+
// viene prima creata allineata con l'asse X del mini display e poi ruotata al centro della porzione d'inizio
|
|
733
|
+
// in seguito, la rotazione applicata al gruppo la porta nella posizione definitiva
|
|
734
|
+
segmentGroup.polygon(
|
|
735
|
+
`${mdArcAttrsCx - ringOuterRadius/2 + 2},${mdArcAttrsCy - 3} `+
|
|
736
|
+
`${mdArcAttrsCx - segmentRadius + 6},${mdArcAttrsCy} `+
|
|
737
|
+
`${mdArcAttrsCx - ringOuterRadius/2 + 2},${mdArcAttrsCy + 3}`
|
|
738
|
+
)
|
|
739
|
+
.attr(rodAttrs?? {})
|
|
740
|
+
.transform({
|
|
741
|
+
origin: {
|
|
742
|
+
x: mdArcAttrsCx,
|
|
743
|
+
y: mdArcAttrsCy,
|
|
744
|
+
},
|
|
745
|
+
rotate: ((240/portions) / 2) - 30
|
|
746
|
+
})
|
|
747
|
+
;
|
|
748
|
+
|
|
749
|
+
segmentGroup.transform({
|
|
750
|
+
origin: {
|
|
751
|
+
x: mdArcAttrsCx,
|
|
752
|
+
y: mdArcAttrsCy,
|
|
753
|
+
},
|
|
754
|
+
rotate: (240/portions) * getColorIndexForValueXXXXX(md.value) - scaleStartValue
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// =>> label mini display
|
|
759
|
+
|
|
760
|
+
if(miniDisplayLabelTopMargin != null && typeof miniDisplayLabelTopMargin !== 'number') {
|
|
761
|
+
throw `${errorPrefix}: 'miniDisplayLabelTopMargin' deve essere null o un numero`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
md.mdLabel??=[];
|
|
765
|
+
if(md.mdLabel.length > 2) {
|
|
766
|
+
throw `${errorPrefix}: non è possibile definire più di 2 elementi 'mdLabel'`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
let prevLabelHeight = 0;
|
|
771
|
+
for await (const labelObj of md.mdLabel) {
|
|
772
|
+
|
|
773
|
+
prevLabelHeight = await setLabel({
|
|
774
|
+
labelObj: labelObj,
|
|
775
|
+
baseX: mdArcAttrsCx,
|
|
776
|
+
baseY: mdOrigin.y + mdArcAttrsHeight + (miniDisplayLabelTopMargin??0),
|
|
777
|
+
dy: prevLabelHeight,
|
|
778
|
+
rowGap: miniDisplayLabelRowGap,
|
|
779
|
+
parentEl: mdGroup
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
// =>> indicatori per debug
|
|
786
|
+
if(debug) {
|
|
787
|
+
// centro mini display
|
|
788
|
+
const gap = 10;
|
|
789
|
+
const debug_group = mdGroup.group()
|
|
790
|
+
.attr({
|
|
791
|
+
'data-debug-info': 'centro mini display',
|
|
792
|
+
stroke: '#05923e',
|
|
793
|
+
fill: 'none',
|
|
794
|
+
'stroke-width': .5,
|
|
795
|
+
class: 'debug-info'
|
|
796
|
+
});
|
|
797
|
+
debug_group.line({x1: mdArcAttrsCx - gap, y1: mdArcAttrsCy, x2: mdArcAttrsCx + gap, y2: mdArcAttrsCy});
|
|
798
|
+
debug_group.line({x1: mdArcAttrsCx, y1: mdArcAttrsCy - gap, x2: mdArcAttrsCx, y2: mdArcAttrsCy + gap});
|
|
799
|
+
|
|
800
|
+
// origine mini display
|
|
801
|
+
// debug_group.line({x1: mdOrigin.x - gap, y1: mdOrigin.y, x2: mdOrigin.x + gap, y2: mdOrigin.y});
|
|
802
|
+
// debug_group.line({x1: mdOrigin.x, y1: mdOrigin.y - gap, x2: mdOrigin.x, y2: mdOrigin.y + gap});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// =>> ricalcolo miniDisplayMaxHeight
|
|
806
|
+
miniDisplayMaxHeight = Math.max(
|
|
807
|
+
miniDisplayMaxHeight,
|
|
808
|
+
mdGroup.bbox().h + (miniDisplayTopMargin?? 0) + md.mdArcAttrs['stroke-width'] // strokeWidth: spessore traccia nel caso non ci sia testo
|
|
809
|
+
);
|
|
810
|
+
miniDisplayIdx++;
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// !SECTION end miniDisplay
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
// =>> aggiornamento dimensioni SVG
|
|
818
|
+
height+= miniDisplayMaxHeight;
|
|
819
|
+
// width = mainDisplayGroupBbox.w;
|
|
820
|
+
*/
|
|
821
|
+
|
|
822
|
+
svgCanvas
|
|
823
|
+
// .size(width, height)
|
|
824
|
+
.viewbox(0, 0, width, height);
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
// indicatori per debug
|
|
828
|
+
if(debug) {
|
|
829
|
+
import('./debug-tools.js').then(m => m.run({
|
|
830
|
+
mainDisplayCenter: mainDisplayCenter,
|
|
831
|
+
width: width,
|
|
832
|
+
height: height,
|
|
833
|
+
mainRadius: mainRadius,
|
|
834
|
+
// centerGap: centerGap,
|
|
835
|
+
// scaleEndThickness: scaleEndThickness
|
|
836
|
+
|
|
837
|
+
...configCalcoloPuntiInterni
|
|
838
|
+
}).addTo(svgCanvas));
|
|
839
|
+
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if(!container) {
|
|
843
|
+
return svgCanvas.svg();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
} catch(e) {
|
|
847
|
+
console.error( 'MinimoCharts → ratingDisplay:', e ); // eslint-disable-line
|
|
848
|
+
}
|
|
849
|
+
}
|