@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,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
|
+
}
|