@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,311 @@
1
+ // mostra un triangolo rettangolo disposto orizzontalmente che mostra
2
+ // la porzione di raggiungimento di un obiettivo
3
+
4
+ import { parseContainer, getElementFromContainer } from './helpers/chart-utils.js';
5
+ import { createTextEl } from './helpers/create-text-element.js';
6
+
7
+ export async function goal({
8
+ /** mostra info utili per il debug */
9
+ debug = false,
10
+
11
+ /** container (selettore o elemento DOM), se null viene restituito il codice SVG */
12
+ container = null,
13
+
14
+ /** attributi facoltativi per l'svg (classe, ecc) */
15
+ svgAttrs = {},
16
+
17
+ /**
18
+ larghezza e altezza del grafico (px),
19
+ Se il container è presente e il parametro `width` è `null`,
20
+ viene utilizzate la larghezza del container.
21
+
22
+ Lo spazio dell'eventuale etichetta orizzontale viene sottratto alla larghezza totale
23
+
24
+ L'altezza finale dell'elemento SVG corrisponde a `height`.
25
+ Se `height` è null, viene ricavato dalla somma di `chartHeight` e dallo spazio occupato dal cursore e dall'eventuale etichetta
26
+ Se `chartHeight` è null, è ricavata dalla differenza tra height e spazio cursore, ecc...
27
+ almeno uno dei due valori deve essere presente
28
+
29
+ */
30
+ width = null, // null || <value>
31
+ height = null, // <value> || null
32
+ chartHeight = 80, // <value> || null // altezza del solo grafico, senza etichette
33
+
34
+ /** eventuale etichetta da collocare sul lato destro */
35
+ rightLabel = null,
36
+
37
+ /** attributi font etichetta */
38
+ rightLabelFontAttrs = { size: 16 },
39
+
40
+ /** per conversione in tracciati (solo node) */
41
+ rightLabelFontFilePath = null,
42
+
43
+ /** rotazione in senso orario (true) o antiorario (false) */
44
+ rightLabelRotateCounterClockWise = true,
45
+
46
+ /** obiettivo */
47
+ goalValue,
48
+
49
+ /** valore corrente */
50
+ currentValue = 0,
51
+
52
+ /** se true l'area mancante all'obiettivo viene mostrata secondi i parametri in missingGoalAttrs */
53
+ showMissingGoalArea = true,
54
+
55
+ /** attributi per aree triangolo obiettivo raggiunto e non */
56
+
57
+ /** obiettivo raggiunto */
58
+ goalAreaAttrs = {
59
+ fill: '#ddd',
60
+ stroke: '#999',
61
+ 'stroke-width': 1
62
+ },
63
+
64
+ /** porzione dell'obiettivo non raggiunta, parametri utilizzati solo se `showMissingGoalArea === true` */
65
+ missingGoalAttrs = {
66
+ fill: '#f17575',
67
+ stroke: '#c00',
68
+ 'stroke-width': 1
69
+ },
70
+
71
+ /** mostra o meno il valore corrispondente al cursore */
72
+ showCurrentValue = false,
73
+
74
+ /** attributi font valore current */
75
+ currentValueFontAttrs = { size: 16 },
76
+ currentValueFontFilePath = null,
77
+
78
+ /** lato del quadrato che circoscrive il triangolo del cursore */
79
+ cursorSize = 12,
80
+
81
+ /** attributi per il cursore indicatore del valore corrente */
82
+ cursorAttrs = {
83
+ fill: '#000'
84
+ },
85
+
86
+ /** funzione per la formattazione valore locale */
87
+ currentValueFormatter = value => (value/100).toLocaleString('it-IT', {
88
+ minimumFractionDigits: 0,
89
+ maximumFractionDigits: 2,
90
+ style: 'percent'
91
+ })
92
+
93
+ }) {
94
+
95
+ const chartInstance = this;
96
+
97
+
98
+ try {
99
+
100
+ // i valori dell'obiettivo sono obbligatori
101
+ if(goalValue == null || currentValue == null) {
102
+ throw `goalValue e/o currentValue mancanti: goalValue: ${goalValue}, currentValue: ${currentValue})`;
103
+ }
104
+
105
+ container = getElementFromContainer(container);
106
+
107
+ const svgCanvas = chartInstance.createSvgCanvas(container, debug);
108
+
109
+ // calcolo altezza cursore ed eventuale etichetta valore corrente
110
+
111
+ // calcolo dimensioni cursore + etichetta valore (se presente)
112
+ // il cursore si vede sempre
113
+ const cursorGap = 2; // spazio verticale tra cursore e altri elementi
114
+ let cursorAndLabelHeight = cursorGap + cursorSize;
115
+
116
+ // calcolo dimensioni etichetta e aggiunta a svgCanvas
117
+ let currentValueLabelEl = null, currentValueLabelBbox;
118
+
119
+ if(showCurrentValue) {
120
+ currentValueLabelEl = await createTextEl(chartInstance, {
121
+ text: currentValueFormatter(currentValue),
122
+ textFill: currentValueFontAttrs.fill?? '#000',
123
+ fontAttr: currentValueFontAttrs,
124
+ fontFilePath: currentValueFontFilePath,
125
+ parentElement: svgCanvas
126
+ });
127
+ currentValueLabelBbox = currentValueLabelEl.bbox();
128
+ cursorAndLabelHeight += cursorGap + currentValueLabelBbox.height;
129
+ }
130
+
131
+ // ricalcolo altezza e dimensioni definitive
132
+ /*
133
+ `height` e `width` si riferiscono alle dimensioni complessiva dell'svg (compresi cursori ed etichette)
134
+ `chartHeight` e `chartWidth` a quelle del solo grafico (il triangolo)
135
+
136
+ se `height` è definita, `chartHeight` (l'altezza del grafico vero e proprio)
137
+ è ricavata sottraendo `cursorAndLabelHeight` (il valore iniziale di `chartHeight`, se impostato, viene ignorato)
138
+
139
+ Se `height` è null, il suo valore è dato dalla somma di `chartHeight` e `cursorAndLabelHeight`
140
+
141
+ Se entrambi sono nulli viene inviato un errore
142
+
143
+ Entrambi vengono ricalcolati in presenza dell'etichetta destra
144
+
145
+ Se `height` non è null, l'altezza svg non viene mai modificata per adattarsi alle dimensioni delle scritte
146
+ se eccedenti lo spazio disponibile
147
+ */
148
+
149
+ let heightWasDefined = false; // indica se l'altezza svg è predefinita o meno
150
+
151
+ if(height != null) {
152
+ chartHeight = height - cursorAndLabelHeight;
153
+ heightWasDefined = true;
154
+ } else {
155
+ height = chartHeight != null? chartHeight + cursorAndLabelHeight: null;
156
+ }
157
+
158
+ // calcolo width
159
+ [, width] = parseContainer({ container: container, width: width, height: height });
160
+
161
+
162
+ if(!width || !height) {
163
+ throw `width e/o height mancanti: width: ${width}, height: ${height} (chartHeight: ${chartHeight})`;
164
+ }
165
+
166
+ // larghezza iniziale grafico
167
+ // al netto del padding orizzontale pari a 1/2 cursore, in modo che sia visibile interamente
168
+ // quando posizionato agli estremi (anche in presenta di etichetta laterale, nel caso quest'ultima sia più alta del grafico)
169
+ let chartWidth = width - cursorSize;
170
+
171
+
172
+ // calcolo etichetta destra
173
+ // le sue dimensioni (relative all'elemento ruotato) sono considerate nella definizione dell'altezza del'SVG
174
+ // e delle dimesnioni del grafico
175
+ // calcolo coordinate y del grafico
176
+
177
+ let rightLabelGroup = null
178
+ ,rightLabel_bbox
179
+ ,chartYtop = 0 // coordinata y superiore del grafico
180
+ ,chartYbottom = height - cursorAndLabelHeight // coordinata y inferiore del grafico
181
+ ;
182
+
183
+ if(rightLabel != null) {
184
+
185
+ rightLabelGroup = svgCanvas.group() // necessario per la rotazione
186
+ .attr({ 'data-debug-info': debug? 'Gruppo rightLabel' : null });
187
+
188
+ const rightLabelEl = await createTextEl(chartInstance, {
189
+ text: rightLabel,
190
+ textFill: rightLabelFontAttrs.fill?? '#000',
191
+ fontAttr: rightLabelFontAttrs,
192
+ fontFilePath: rightLabelFontFilePath,
193
+ parentElement: rightLabelGroup
194
+ });
195
+
196
+ rightLabelEl.rotate(rightLabelRotateCounterClockWise? 90 : -90);
197
+ rightLabel_bbox = rightLabelGroup.bbox();
198
+ chartWidth -= rightLabel_bbox.width;
199
+
200
+ // altezza svg ricalcolata nel caso la scritta sia più alta del grafico
201
+ // (solo nel caso in cui l'altezza non sia stata predefinita nei parametri)
202
+ if(!heightWasDefined) {
203
+ height = Math.max(height, rightLabel_bbox.height);
204
+ }
205
+
206
+ // spazio sotto il grafico ricalcolato nel caso la parte della scritta eccedente
207
+ // il grafico sia maggiore dello spazio cursore
208
+ cursorAndLabelHeight = Math.max(cursorAndLabelHeight, (rightLabel_bbox.height - chartHeight) / 2);
209
+
210
+ // ricalcolo `chartYtop` nel caso la scritta sia più alta del grafico
211
+ chartYtop = Math.max(chartYtop, (rightLabel_bbox.height - chartHeight) / 2);
212
+
213
+ // chartYtop è sempre sopra (con etichette molto lunghe i valori si invertono)
214
+ if(chartYtop > chartYbottom) {
215
+ const tmp = chartYtop;
216
+ chartYtop = chartYbottom;
217
+ chartYbottom = tmp;
218
+ }
219
+ }
220
+
221
+ svgCanvas.viewbox(0, 0, width, height)
222
+ // .size(width, height)
223
+ .attr(svgAttrs);
224
+
225
+
226
+ // creazione grafico
227
+ svgCanvas.path(
228
+ `M0,${chartYbottom}` + // vertice a sinistra
229
+ `L${chartWidth},${chartYtop}` + // vertice in alto a destra
230
+ `L${chartWidth},${chartYbottom}` + // vertice in basso a destra
231
+ 'z'
232
+ )
233
+ .x(cursorSize / 2)
234
+ .attr({
235
+ 'data-debug-info': debug? 'Grafico' : null,
236
+ ...goalAreaAttrs
237
+ });
238
+
239
+ // posizionamento etichetta destra
240
+ if(rightLabelGroup) {
241
+ rightLabelGroup.move(
242
+ chartWidth + cursorSize,
243
+ (chartHeight - rightLabel_bbox.height) / 2
244
+ );
245
+ }
246
+
247
+
248
+ // posizione current value (non tiene conto delle dimensioni del cursore)
249
+ // non include il padding orizzontale (spazio cursore)
250
+ // `currentValue` è impostato in modo che non superi `goalValue`
251
+ const currentValuePosition = ((Math.min(goalValue, currentValue) * chartWidth) / goalValue);
252
+
253
+ // area obiettivo mancante
254
+ if(showMissingGoalArea && currentValue < goalValue) {
255
+ // angolo opposto al lato destro del triangolo (in radianti)
256
+ const angleRad = Math.atan(chartHeight / chartWidth);
257
+
258
+ // coordinata Y superiore del lato sinistro dell'area target non raggiunto
259
+ // (segmento AD nello schema)
260
+ const currentValuePositionTop =
261
+ chartYbottom - (
262
+ (currentValuePosition) * // larghezza porzione obiettivo raggiunto
263
+ Math.tan(angleRad)
264
+ );
265
+
266
+ svgCanvas.path(
267
+ `M${currentValuePosition},${currentValuePositionTop}` + // vertice superiore sx
268
+ `L${chartWidth},${chartYtop}` + // vertice in alto a destra
269
+ `L${chartWidth},${chartYbottom}` + // vertice in basso a destra
270
+ `L${currentValuePosition},${chartYbottom}` + // vertice inferiore sx
271
+ 'z'
272
+ )
273
+ .dx(cursorSize/2)
274
+ .attr({
275
+ 'data-debug-info': debug? 'Area obiettivo mancante' : null,
276
+ ...(missingGoalAttrs?? {})
277
+ });
278
+ }
279
+
280
+ // creazione e posizionamento cursore
281
+ svgCanvas.path(`M${cursorSize/2},0 L${cursorSize},${cursorSize} L0,${cursorSize} Z`)
282
+ .attr({
283
+ 'data-debug-info': debug? 'Cursore' : null,
284
+ ...(cursorAttrs?? {})
285
+ })
286
+ .move(
287
+ currentValuePosition,
288
+ chartYbottom + cursorGap
289
+ );
290
+
291
+
292
+ // posizionamento etichetta valore
293
+ if(currentValueLabelEl) {
294
+ const labelX = currentValuePosition - (currentValueLabelBbox.width / 2) + cursorSize / 2;
295
+ currentValueLabelEl.move(
296
+ labelX + currentValueLabelBbox.width > width
297
+ ? width - currentValueLabelBbox.width // blocca limite dx
298
+ : Math.max(0, labelX), // blocca limite sx
299
+ chartYbottom + (cursorGap * 2) + cursorSize
300
+ );
301
+ }
302
+
303
+ if(!container) {
304
+ return svgCanvas.svg();
305
+ }
306
+
307
+
308
+ } catch(e) {
309
+ console.error( 'MinimoCharts / goal →', e ); // eslint-disable-line
310
+ }
311
+ }
@@ -0,0 +1,254 @@
1
+ // costruisce un sistema di assi cartesiani
2
+ // restituisce due gruppi delle assi X e Y da posizionare successivamente
3
+
4
+ import { G } from '@svgdotjs/svg.js';
5
+ import { createTextEl } from './create-text-element.js';
6
+
7
+ export async function cartesianAxis(chartInstance, {
8
+ debug = false,
9
+ chart_area = {},
10
+ padding = 0,
11
+ width,
12
+ height,
13
+ axisAttrs = {},
14
+ ticksAttrs = {},
15
+ labelYAttrs = {},
16
+ labelYFontFilePath = null,
17
+ labelXAttrs = {},
18
+ labelXFontFilePath = null,
19
+ stepY_count,
20
+ stepY_size,
21
+ stepY_value,
22
+ min_value,
23
+ stepX_size,
24
+ stepX_count,
25
+ ticksLength,
26
+ showXgrid = true, // griglia verticale
27
+ showYgrid = true, // griglia orizzontale
28
+ gridLinesAttrs = {},
29
+
30
+ yAxisTitle = null,
31
+ yAxisTitleAttrs = {},
32
+ yAxisTitleFontFilePath = null,
33
+
34
+ xAxisTitle = null,
35
+ xAxisTitleAttrs = {},
36
+ xAxisTitleFontFilePath = null,
37
+
38
+ xLabels, // array con le etichette dell'asseX
39
+ // se true le etichette sono posizionate in corrispondenza dei ticks
40
+ // altrimenti sono centrate nello spazio corispondente a stepX_size
41
+ xLabelsOnTicks = true,
42
+ labelYFormatter = null // funzione
43
+ }) {
44
+
45
+ try {
46
+
47
+ const cartesianAxisGroup = new G();
48
+ cartesianAxisGroup.attr({ 'data-debug-info': debug? 'Gruppo Base Assi Cartesiani ed elementi correlati' : null });
49
+
50
+ // =>> ASSE Y
51
+ // *****************************************
52
+ const yAxisGroup = cartesianAxisGroup.group();
53
+ yAxisGroup
54
+ .attr({ 'data-debug-info': debug? 'Gruppo Asse Y' : null })
55
+ .line(
56
+ chart_area.left, chart_area.top,
57
+ chart_area.left, chart_area.bottom
58
+ )
59
+ .attr({
60
+ 'data-debug-info': debug? 'Asse Y' : null,
61
+ ...axisAttrs
62
+ });
63
+
64
+
65
+ // =>> ticks ed etichette asse Y (dal basso verso l'alto)
66
+ const ticksYGroup = yAxisGroup.group().attr({
67
+ 'data-debug-info': debug? 'Gruppo ticks Asse Y' : null,
68
+ });
69
+ const labelsYGroup = yAxisGroup.group().attr({
70
+ 'data-debug-info': debug? 'Gruppo etichette Asse Y' : null,
71
+ });
72
+
73
+ // gruppo per griglia asseY
74
+ let yGridGroup = null;
75
+ if(showYgrid) {
76
+ yGridGroup = cartesianAxisGroup.group()
77
+ .attr({ 'data-debug-info': debug? 'Gruppo Griglia Asse Y' : null });
78
+ }
79
+
80
+ for await (const stepIdx of Array.from(Array(stepY_count + 1).keys())) {
81
+
82
+ const y = chart_area.bottom - (stepY_size * stepIdx)
83
+ ,label = labelYFormatter(min_value + stepY_value * stepIdx);
84
+
85
+ // =>> ticks Y
86
+ ticksYGroup
87
+ .line(
88
+ chart_area.left - ticksLength, y,
89
+ chart_area.left, y
90
+ )
91
+ .attr(ticksAttrs);
92
+
93
+ // =>> etichette Y
94
+ const labelEl = await createTextEl(chartInstance, {
95
+ text: label,
96
+ textFill: labelYAttrs.fill?? '#000',
97
+ fontAttr: labelYAttrs,
98
+ fontFilePath: labelYFontFilePath,
99
+ parentElement: labelsYGroup
100
+ }),
101
+ label_bbox = labelEl.bbox();
102
+
103
+ labelEl.move(
104
+ chart_area.left - ticksLength - 2 - label_bbox.width, // 2 è un distanziatore tra l'etichetta e la linea
105
+ y - label_bbox.height / 2
106
+ );
107
+
108
+ // =>> griglia orizzontale (ortogonale all'asse Y)
109
+ if(showYgrid && stepIdx > 0) { // la prima linea coincide con l'asse X
110
+ yGridGroup.line(
111
+ chart_area.left, y,
112
+ chart_area.right, y
113
+ ).attr(gridLinesAttrs);
114
+ }
115
+
116
+ } // end for await stepIdx
117
+
118
+ // =>> titolo asse Y
119
+ if(yAxisTitle) {
120
+ const titleGroup = yAxisGroup.group(); // per la rotazione
121
+ titleGroup.attr({
122
+ 'data-debug-info': debug? 'Titolo asse Y' : null,
123
+ });
124
+
125
+ const titleEl = await createTextEl(chartInstance, {
126
+ text: yAxisTitle,
127
+ textFill: yAxisTitleAttrs.fill?? '#000',
128
+ fontAttr: yAxisTitleAttrs,
129
+ fontFilePath: yAxisTitleFontFilePath,
130
+ parentElement: titleGroup
131
+ });
132
+
133
+ titleEl.rotate(-90);
134
+ const title_bbox = titleGroup.bbox();
135
+
136
+ titleGroup.move(
137
+ padding,
138
+ (chart_area.bottom - title_bbox.height) / 2
139
+ );
140
+ }
141
+
142
+
143
+
144
+ // =>> ASSE X
145
+ // *****************************************
146
+ const xAxisGroup = cartesianAxisGroup.group();
147
+ xAxisGroup
148
+ .attr({ 'data-debug-info': debug? 'Gruppo Asse X' : null })
149
+ .line(
150
+ chart_area.left, chart_area.bottom,
151
+ chart_area.right, chart_area.bottom
152
+ )
153
+ .attr({
154
+ 'data-debug-info': debug? 'Asse X' : null,
155
+ ...axisAttrs
156
+ });
157
+
158
+ // =>> ticks, etichette ecc asse X (da sinistra a destra)
159
+ const ticksXGroup = xAxisGroup.group().attr({
160
+ 'data-debug-info': debug? 'Gruppo ticks Asse X' : null,
161
+ });
162
+ const labelsXGroup = xAxisGroup.group().attr({
163
+ 'data-debug-info': debug? 'Gruppo etichette Asse X' : null,
164
+ });
165
+
166
+
167
+ // coordinata y per il posizionamento delle etichette
168
+ const labelY = chart_area.bottom + ticksLength + 2;
169
+
170
+ // gruppo per griglia asseX (verticale, ortogonale all'asse X)
171
+ // il gruppo viene sempre creato, perché anche se non si stampa la griglia viene
172
+ // cmq aggiunta l'ultima linea a dx
173
+ const xGridGroup = cartesianAxisGroup.group()
174
+ .attr({ 'data-debug-info': debug? 'Gruppo Griglia Asse X' : null });
175
+
176
+
177
+ for (let i = 0; i <= stepX_count - 1; i++) {
178
+
179
+ const thisLabel = (Array.isArray(xLabels) ? xLabels[i] : undefined) ?? '',
180
+ labelIdx = i,
181
+ isLastStep = labelIdx === stepX_count - 1,
182
+ x = chart_area.left + (stepX_size * labelIdx);
183
+
184
+ // =>> ticks X
185
+ ticksXGroup
186
+ .line(
187
+ x, chart_area.bottom + ticksLength,
188
+ x, chart_area.bottom
189
+ )
190
+ .attr(ticksAttrs);
191
+
192
+ // =>> etichette X
193
+ const labelEl = await createTextEl (chartInstance, {
194
+ text: thisLabel,
195
+ textFill: labelXAttrs.fill?? '#000',
196
+ fontAttr: labelXAttrs,
197
+ fontFilePath: labelXFontFilePath,
198
+ parentElement: labelsXGroup
199
+ }),
200
+ label_bbox = labelEl.bbox();
201
+
202
+ // posizionamento
203
+ labelEl.move(
204
+ x - label_bbox.width / 2 + (xLabelsOnTicks? 0 : stepX_size / 2),
205
+ labelY
206
+ );
207
+
208
+ // eventuale correzione ultima etichetta
209
+ if(i === stepX_count - 1) {
210
+ const labelX2 = labelEl.bbox().x2;
211
+
212
+ if(labelX2 > width - padding) {
213
+ labelEl.dx(-(labelX2 - width + padding/2));
214
+ }
215
+ }
216
+
217
+
218
+ // =>> griglia verticale
219
+ // la prima linea coincide con l'asse Y
220
+ // l'ultima linea a dx viene stampata sempre
221
+ if((showXgrid && labelIdx > 0) || isLastStep ) {
222
+ xGridGroup.line(
223
+ x, chart_area.bottom,
224
+ x, chart_area.top
225
+ ).attr(gridLinesAttrs);
226
+ }
227
+
228
+ } // end for await thisLabel
229
+
230
+
231
+ // =>> titolo asse X
232
+ if(xAxisTitle) {
233
+ const titleEl = await createTextEl (chartInstance, {
234
+ text: xAxisTitle,
235
+ textFill: xAxisTitleAttrs.fill?? '#000',
236
+ fontAttr: xAxisTitleAttrs,
237
+ fontFilePath: xAxisTitleFontFilePath,
238
+ parentElement: xAxisGroup
239
+ }),
240
+ title_bbox = titleEl.bbox();
241
+
242
+ titleEl.move(
243
+ chart_area.left + (chart_area.width - title_bbox.width) / 2,
244
+ height - padding - title_bbox.height
245
+ );
246
+
247
+ }
248
+
249
+ return cartesianAxisGroup;
250
+
251
+ } catch(e) {
252
+ console.error( 'MinimoCharts → cartesianAxis', e ); // eslint-disable-line
253
+ }
254
+ }