@internetstiftelsen/charts 0.5.1 → 0.6.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/README.md +4 -2
- package/area.d.ts +26 -0
- package/area.js +331 -0
- package/base-chart.d.ts +10 -3
- package/base-chart.js +33 -19
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +1 -1
- package/donut-center-content.js +7 -6
- package/donut-chart.d.ts +1 -0
- package/donut-chart.js +8 -1
- package/export-tabular.d.ts +4 -4
- package/export-tabular.js +8 -0
- package/export-xlsx.d.ts +2 -2
- package/gauge-chart.d.ts +138 -0
- package/gauge-chart.js +1041 -0
- package/grouped-data.d.ts +19 -0
- package/grouped-data.js +122 -0
- package/grouped-tabular.d.ts +26 -0
- package/grouped-tabular.js +149 -0
- package/package.json +1 -1
- package/pie-chart.d.ts +80 -0
- package/pie-chart.js +665 -0
- package/theme.d.ts +1 -0
- package/theme.js +25 -16
- package/tooltip.d.ts +3 -2
- package/tooltip.js +40 -39
- package/types.d.ts +46 -0
- package/validation.d.ts +4 -0
- package/validation.js +25 -0
- package/x-axis.d.ts +10 -2
- package/x-axis.js +205 -15
- package/xy-chart.d.ts +10 -1
- package/xy-chart.js +307 -90
package/gauge-chart.js
ADDED
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
import { arc, select } from 'd3';
|
|
2
|
+
import { BaseChart } from './base-chart.js';
|
|
3
|
+
import { DEFAULT_COLOR_PALETTE } from './theme.js';
|
|
4
|
+
import { ChartValidator } from './validation.js';
|
|
5
|
+
const DEFAULT_START_ANGLE = -Math.PI * 0.75;
|
|
6
|
+
const DEFAULT_END_ANGLE = Math.PI * 0.75;
|
|
7
|
+
const DEFAULT_HALF_START_ANGLE = -Math.PI / 2;
|
|
8
|
+
const DEFAULT_HALF_END_ANGLE = Math.PI / 2;
|
|
9
|
+
const DEFAULT_VALUE_KEY = 'value';
|
|
10
|
+
const DEFAULT_HALF_CIRCLE = false;
|
|
11
|
+
const DEFAULT_MIN_VALUE = 0;
|
|
12
|
+
const DEFAULT_MAX_VALUE = 100;
|
|
13
|
+
const DEFAULT_INNER_RADIUS_RATIO = 0.68;
|
|
14
|
+
const DEFAULT_CORNER_RADIUS = 4;
|
|
15
|
+
const DEFAULT_TRACK_COLOR = '#e5e7eb';
|
|
16
|
+
const DEFAULT_TARGET_COLOR = '#111827';
|
|
17
|
+
const DEFAULT_SEGMENT_STYLE = 'solid';
|
|
18
|
+
const DEFAULT_SHOW_VALUE = true;
|
|
19
|
+
const DEFAULT_NEEDLE_SHOW = true;
|
|
20
|
+
const DEFAULT_THEME_PALETTE_INDEX = 0;
|
|
21
|
+
const DEFAULT_NEEDLE_WIDTH = 3;
|
|
22
|
+
const DEFAULT_NEEDLE_LENGTH_RATIO = 0.92;
|
|
23
|
+
const DEFAULT_NEEDLE_CAP_RADIUS = 6;
|
|
24
|
+
const DEFAULT_MARKER_WIDTH = 3;
|
|
25
|
+
const DEFAULT_TICK_COUNT = 5;
|
|
26
|
+
const DEFAULT_TICKS_SHOW = true;
|
|
27
|
+
const DEFAULT_TICKS_SHOW_LINES = true;
|
|
28
|
+
const DEFAULT_TICKS_SHOW_LABELS = true;
|
|
29
|
+
const DEFAULT_TICK_SIZE = 8;
|
|
30
|
+
const DEFAULT_TICK_LABEL_OFFSET = 12;
|
|
31
|
+
const DEFAULT_TICK_LABEL_FONT_SIZE = 11;
|
|
32
|
+
const DEFAULT_TICK_LABEL_FONT_WEIGHT = 'normal';
|
|
33
|
+
const DEFAULT_TICK_LABEL_COLOR = '#4b5563';
|
|
34
|
+
const DEFAULT_VALUE_LABEL_FONT_SIZE = 28;
|
|
35
|
+
const DEFAULT_VALUE_LABEL_FONT_WEIGHT = '700';
|
|
36
|
+
const DEFAULT_VALUE_LABEL_COLOR = '#111827';
|
|
37
|
+
const DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL = 46;
|
|
38
|
+
const DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL = 12;
|
|
39
|
+
const DEFAULT_TICK_LINE_COLOR = '#6b7280';
|
|
40
|
+
const DEFAULT_TICK_LINE_STROKE_WIDTH = 1.5;
|
|
41
|
+
const DEFAULT_TARGET_MARKER_STROKE_WIDTH = 2.5;
|
|
42
|
+
const DEFAULT_PROGRESS_RADIUS_INSET = 2;
|
|
43
|
+
const MIN_PROGRESS_BAND_THICKNESS = 1;
|
|
44
|
+
const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
|
|
45
|
+
const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
|
|
46
|
+
const TOOLTIP_OFFSET_PX = 12;
|
|
47
|
+
const EDGE_MARGIN_PX = 10;
|
|
48
|
+
const DUMMY_ARC_DATUM = {
|
|
49
|
+
innerRadius: 0,
|
|
50
|
+
outerRadius: 0,
|
|
51
|
+
startAngle: 0,
|
|
52
|
+
endAngle: 0,
|
|
53
|
+
};
|
|
54
|
+
export class GaugeChart extends BaseChart {
|
|
55
|
+
constructor(config) {
|
|
56
|
+
super(config);
|
|
57
|
+
Object.defineProperty(this, "configuredValue", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
value: void 0
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(this, "configuredTargetValue", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
value: void 0
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(this, "configuredSegments", {
|
|
70
|
+
enumerable: true,
|
|
71
|
+
configurable: true,
|
|
72
|
+
writable: true,
|
|
73
|
+
value: void 0
|
|
74
|
+
});
|
|
75
|
+
Object.defineProperty(this, "valueKey", {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
value: void 0
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(this, "targetValueKey", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
value: void 0
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(this, "minValue", {
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true,
|
|
91
|
+
value: void 0
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(this, "maxValue", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
configurable: true,
|
|
96
|
+
writable: true,
|
|
97
|
+
value: void 0
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "halfCircle", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: void 0
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(this, "startAngle", {
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
writable: true,
|
|
109
|
+
value: void 0
|
|
110
|
+
});
|
|
111
|
+
Object.defineProperty(this, "endAngle", {
|
|
112
|
+
enumerable: true,
|
|
113
|
+
configurable: true,
|
|
114
|
+
writable: true,
|
|
115
|
+
value: void 0
|
|
116
|
+
});
|
|
117
|
+
Object.defineProperty(this, "innerRadiusRatio", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true,
|
|
121
|
+
value: void 0
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(this, "thickness", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
configurable: true,
|
|
126
|
+
writable: true,
|
|
127
|
+
value: void 0
|
|
128
|
+
});
|
|
129
|
+
Object.defineProperty(this, "cornerRadius", {
|
|
130
|
+
enumerable: true,
|
|
131
|
+
configurable: true,
|
|
132
|
+
writable: true,
|
|
133
|
+
value: void 0
|
|
134
|
+
});
|
|
135
|
+
Object.defineProperty(this, "trackColor", {
|
|
136
|
+
enumerable: true,
|
|
137
|
+
configurable: true,
|
|
138
|
+
writable: true,
|
|
139
|
+
value: void 0
|
|
140
|
+
});
|
|
141
|
+
Object.defineProperty(this, "progressColor", {
|
|
142
|
+
enumerable: true,
|
|
143
|
+
configurable: true,
|
|
144
|
+
writable: true,
|
|
145
|
+
value: void 0
|
|
146
|
+
});
|
|
147
|
+
Object.defineProperty(this, "targetColor", {
|
|
148
|
+
enumerable: true,
|
|
149
|
+
configurable: true,
|
|
150
|
+
writable: true,
|
|
151
|
+
value: void 0
|
|
152
|
+
});
|
|
153
|
+
Object.defineProperty(this, "segmentStyle", {
|
|
154
|
+
enumerable: true,
|
|
155
|
+
configurable: true,
|
|
156
|
+
writable: true,
|
|
157
|
+
value: void 0
|
|
158
|
+
});
|
|
159
|
+
Object.defineProperty(this, "valueFormatter", {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
configurable: true,
|
|
162
|
+
writable: true,
|
|
163
|
+
value: void 0
|
|
164
|
+
});
|
|
165
|
+
Object.defineProperty(this, "showValue", {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
writable: true,
|
|
169
|
+
value: void 0
|
|
170
|
+
});
|
|
171
|
+
Object.defineProperty(this, "needle", {
|
|
172
|
+
enumerable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
writable: true,
|
|
175
|
+
value: void 0
|
|
176
|
+
});
|
|
177
|
+
Object.defineProperty(this, "marker", {
|
|
178
|
+
enumerable: true,
|
|
179
|
+
configurable: true,
|
|
180
|
+
writable: true,
|
|
181
|
+
value: void 0
|
|
182
|
+
});
|
|
183
|
+
Object.defineProperty(this, "ticks", {
|
|
184
|
+
enumerable: true,
|
|
185
|
+
configurable: true,
|
|
186
|
+
writable: true,
|
|
187
|
+
value: void 0
|
|
188
|
+
});
|
|
189
|
+
Object.defineProperty(this, "tickLabelStyle", {
|
|
190
|
+
enumerable: true,
|
|
191
|
+
configurable: true,
|
|
192
|
+
writable: true,
|
|
193
|
+
value: void 0
|
|
194
|
+
});
|
|
195
|
+
Object.defineProperty(this, "valueLabelStyle", {
|
|
196
|
+
enumerable: true,
|
|
197
|
+
configurable: true,
|
|
198
|
+
writable: true,
|
|
199
|
+
value: void 0
|
|
200
|
+
});
|
|
201
|
+
Object.defineProperty(this, "segments", {
|
|
202
|
+
enumerable: true,
|
|
203
|
+
configurable: true,
|
|
204
|
+
writable: true,
|
|
205
|
+
value: []
|
|
206
|
+
});
|
|
207
|
+
Object.defineProperty(this, "value", {
|
|
208
|
+
enumerable: true,
|
|
209
|
+
configurable: true,
|
|
210
|
+
writable: true,
|
|
211
|
+
value: 0
|
|
212
|
+
});
|
|
213
|
+
Object.defineProperty(this, "targetValue", {
|
|
214
|
+
enumerable: true,
|
|
215
|
+
configurable: true,
|
|
216
|
+
writable: true,
|
|
217
|
+
value: null
|
|
218
|
+
});
|
|
219
|
+
Object.defineProperty(this, "defaultFormat", {
|
|
220
|
+
enumerable: true,
|
|
221
|
+
configurable: true,
|
|
222
|
+
writable: true,
|
|
223
|
+
value: (value) => {
|
|
224
|
+
if (Number.isInteger(value)) {
|
|
225
|
+
return String(value);
|
|
226
|
+
}
|
|
227
|
+
return value.toFixed(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
const gauge = config.gauge ?? {};
|
|
231
|
+
this.configuredValue = gauge.value;
|
|
232
|
+
this.configuredTargetValue = gauge.targetValue;
|
|
233
|
+
this.configuredSegments = gauge.segments ?? [];
|
|
234
|
+
this.valueKey = config.valueKey ?? DEFAULT_VALUE_KEY;
|
|
235
|
+
this.targetValueKey = config.targetValueKey;
|
|
236
|
+
this.minValue = gauge.min ?? DEFAULT_MIN_VALUE;
|
|
237
|
+
this.maxValue = gauge.max ?? DEFAULT_MAX_VALUE;
|
|
238
|
+
this.halfCircle = gauge.halfCircle ?? DEFAULT_HALF_CIRCLE;
|
|
239
|
+
this.startAngle =
|
|
240
|
+
gauge.startAngle ??
|
|
241
|
+
(this.halfCircle ? DEFAULT_HALF_START_ANGLE : DEFAULT_START_ANGLE);
|
|
242
|
+
this.endAngle =
|
|
243
|
+
gauge.endAngle ??
|
|
244
|
+
(this.halfCircle ? DEFAULT_HALF_END_ANGLE : DEFAULT_END_ANGLE);
|
|
245
|
+
this.innerRadiusRatio = gauge.innerRadius ?? DEFAULT_INNER_RADIUS_RATIO;
|
|
246
|
+
this.thickness = gauge.thickness ?? null;
|
|
247
|
+
this.cornerRadius = gauge.cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
248
|
+
this.trackColor = gauge.trackColor ?? DEFAULT_TRACK_COLOR;
|
|
249
|
+
this.progressColor =
|
|
250
|
+
gauge.progressColor ??
|
|
251
|
+
this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX);
|
|
252
|
+
this.targetColor = gauge.targetColor ?? DEFAULT_TARGET_COLOR;
|
|
253
|
+
this.segmentStyle = gauge.segmentStyle ?? DEFAULT_SEGMENT_STYLE;
|
|
254
|
+
this.valueFormatter = gauge.valueFormatter ?? this.defaultFormat;
|
|
255
|
+
this.showValue = gauge.showValue ?? DEFAULT_SHOW_VALUE;
|
|
256
|
+
this.needle = this.normalizeNeedleConfig(gauge.needle);
|
|
257
|
+
this.marker = this.normalizeMarkerConfig(gauge.marker, !this.needle.show);
|
|
258
|
+
this.ticks = this.normalizeTickConfig(gauge.ticks);
|
|
259
|
+
this.tickLabelStyle = this.normalizeTickLabelStyle(gauge.ticks?.labelStyle);
|
|
260
|
+
this.valueLabelStyle = this.normalizeValueLabelStyle(gauge.valueLabelStyle);
|
|
261
|
+
this.validateGaugeConfig();
|
|
262
|
+
this.segments = this.prepareSegments();
|
|
263
|
+
this.refreshResolvedValues();
|
|
264
|
+
}
|
|
265
|
+
normalizeNeedleConfig(config) {
|
|
266
|
+
if (config === false) {
|
|
267
|
+
return {
|
|
268
|
+
show: false,
|
|
269
|
+
color: DEFAULT_TARGET_COLOR,
|
|
270
|
+
width: DEFAULT_NEEDLE_WIDTH,
|
|
271
|
+
lengthRatio: DEFAULT_NEEDLE_LENGTH_RATIO,
|
|
272
|
+
capRadius: DEFAULT_NEEDLE_CAP_RADIUS,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (config === true || config === undefined) {
|
|
276
|
+
return {
|
|
277
|
+
show: true,
|
|
278
|
+
color: DEFAULT_TARGET_COLOR,
|
|
279
|
+
width: DEFAULT_NEEDLE_WIDTH,
|
|
280
|
+
lengthRatio: DEFAULT_NEEDLE_LENGTH_RATIO,
|
|
281
|
+
capRadius: DEFAULT_NEEDLE_CAP_RADIUS,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
show: config.show ?? DEFAULT_NEEDLE_SHOW,
|
|
286
|
+
color: config.color ?? DEFAULT_TARGET_COLOR,
|
|
287
|
+
width: config.width ?? DEFAULT_NEEDLE_WIDTH,
|
|
288
|
+
lengthRatio: config.lengthRatio ?? DEFAULT_NEEDLE_LENGTH_RATIO,
|
|
289
|
+
capRadius: config.capRadius ?? DEFAULT_NEEDLE_CAP_RADIUS,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
normalizeMarkerConfig(config, defaultShow) {
|
|
293
|
+
if (config === false) {
|
|
294
|
+
return {
|
|
295
|
+
show: false,
|
|
296
|
+
color: this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX),
|
|
297
|
+
width: DEFAULT_MARKER_WIDTH,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (config === true || config === undefined) {
|
|
301
|
+
return {
|
|
302
|
+
show: defaultShow,
|
|
303
|
+
color: this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX),
|
|
304
|
+
width: DEFAULT_MARKER_WIDTH,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
show: config.show ?? defaultShow,
|
|
309
|
+
color: config.color ?? this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX),
|
|
310
|
+
width: config.width ?? DEFAULT_MARKER_WIDTH,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
getThemePaletteColor(index) {
|
|
314
|
+
const palette = this.theme.colorPalette.length > 0
|
|
315
|
+
? this.theme.colorPalette
|
|
316
|
+
: DEFAULT_COLOR_PALETTE;
|
|
317
|
+
return palette[index % palette.length];
|
|
318
|
+
}
|
|
319
|
+
normalizeTickConfig(config) {
|
|
320
|
+
return {
|
|
321
|
+
count: config?.count ?? DEFAULT_TICK_COUNT,
|
|
322
|
+
show: config?.show ?? DEFAULT_TICKS_SHOW,
|
|
323
|
+
showLines: config?.showLines ?? DEFAULT_TICKS_SHOW_LINES,
|
|
324
|
+
showLabels: config?.showLabels ?? DEFAULT_TICKS_SHOW_LABELS,
|
|
325
|
+
size: config?.size ?? DEFAULT_TICK_SIZE,
|
|
326
|
+
labelOffset: config?.labelOffset ?? DEFAULT_TICK_LABEL_OFFSET,
|
|
327
|
+
formatter: config?.formatter ?? this.defaultFormat,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
normalizeTickLabelStyle(config) {
|
|
331
|
+
return {
|
|
332
|
+
fontSize: config?.fontSize ?? DEFAULT_TICK_LABEL_FONT_SIZE,
|
|
333
|
+
fontFamily: config?.fontFamily ?? this.theme.axis.fontFamily,
|
|
334
|
+
fontWeight: config?.fontWeight ??
|
|
335
|
+
this.theme.axis.fontWeight ??
|
|
336
|
+
DEFAULT_TICK_LABEL_FONT_WEIGHT,
|
|
337
|
+
color: config?.color ?? DEFAULT_TICK_LABEL_COLOR,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
normalizeValueLabelStyle(config) {
|
|
341
|
+
return {
|
|
342
|
+
fontSize: config?.fontSize ?? DEFAULT_VALUE_LABEL_FONT_SIZE,
|
|
343
|
+
fontFamily: config?.fontFamily ?? this.theme.axis.fontFamily,
|
|
344
|
+
fontWeight: config?.fontWeight ?? DEFAULT_VALUE_LABEL_FONT_WEIGHT,
|
|
345
|
+
color: config?.color ?? DEFAULT_VALUE_LABEL_COLOR,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
validateGaugeConfig() {
|
|
349
|
+
if (!Number.isFinite(this.minValue) ||
|
|
350
|
+
!Number.isFinite(this.maxValue)) {
|
|
351
|
+
throw new Error(`GaugeChart: gauge.min and gauge.max must be finite numbers, received min='${this.minValue}' and max='${this.maxValue}'`);
|
|
352
|
+
}
|
|
353
|
+
if (this.minValue >= this.maxValue) {
|
|
354
|
+
throw new Error(`GaugeChart: gauge.min must be less than gauge.max, received min='${this.minValue}' and max='${this.maxValue}'`);
|
|
355
|
+
}
|
|
356
|
+
if (!Number.isFinite(this.startAngle) ||
|
|
357
|
+
!Number.isFinite(this.endAngle)) {
|
|
358
|
+
throw new Error(`GaugeChart: gauge.startAngle and gauge.endAngle must be finite numbers, received startAngle='${this.startAngle}' and endAngle='${this.endAngle}'`);
|
|
359
|
+
}
|
|
360
|
+
if (this.endAngle <= this.startAngle) {
|
|
361
|
+
throw new Error(`GaugeChart: gauge.endAngle must be greater than gauge.startAngle, received startAngle='${this.startAngle}' and endAngle='${this.endAngle}'`);
|
|
362
|
+
}
|
|
363
|
+
if (this.innerRadiusRatio < 0 || this.innerRadiusRatio >= 1) {
|
|
364
|
+
throw new Error(`GaugeChart: gauge.innerRadius must be between 0 and <1, received '${this.innerRadiusRatio}'`);
|
|
365
|
+
}
|
|
366
|
+
if (this.cornerRadius < 0) {
|
|
367
|
+
throw new Error(`GaugeChart: gauge.cornerRadius must be >= 0, received '${this.cornerRadius}'`);
|
|
368
|
+
}
|
|
369
|
+
if (this.thickness !== null &&
|
|
370
|
+
(!Number.isFinite(this.thickness) || this.thickness <= 0)) {
|
|
371
|
+
throw new Error(`GaugeChart: gauge.thickness must be > 0 when provided, received '${this.thickness}'`);
|
|
372
|
+
}
|
|
373
|
+
if (!Number.isInteger(this.ticks.count) || this.ticks.count < 0) {
|
|
374
|
+
throw new Error(`GaugeChart: gauge.ticks.count must be a non-negative integer, received '${this.ticks.count}'`);
|
|
375
|
+
}
|
|
376
|
+
if (this.ticks.size < 0) {
|
|
377
|
+
throw new Error(`GaugeChart: gauge.ticks.size must be >= 0, received '${this.ticks.size}'`);
|
|
378
|
+
}
|
|
379
|
+
if (this.ticks.labelOffset < 0) {
|
|
380
|
+
throw new Error(`GaugeChart: gauge.ticks.labelOffset must be >= 0, received '${this.ticks.labelOffset}'`);
|
|
381
|
+
}
|
|
382
|
+
if (this.needle.width <= 0) {
|
|
383
|
+
throw new Error(`GaugeChart: gauge.needle.width must be > 0, received '${this.needle.width}'`);
|
|
384
|
+
}
|
|
385
|
+
if (this.needle.lengthRatio <= 0 || this.needle.lengthRatio > 1.2) {
|
|
386
|
+
throw new Error(`GaugeChart: gauge.needle.lengthRatio must be > 0 and <= 1.2, received '${this.needle.lengthRatio}'`);
|
|
387
|
+
}
|
|
388
|
+
if (this.needle.capRadius < 0) {
|
|
389
|
+
throw new Error(`GaugeChart: gauge.needle.capRadius must be >= 0, received '${this.needle.capRadius}'`);
|
|
390
|
+
}
|
|
391
|
+
if (this.marker.width <= 0) {
|
|
392
|
+
throw new Error(`GaugeChart: gauge.marker.width must be > 0, received '${this.marker.width}'`);
|
|
393
|
+
}
|
|
394
|
+
this.validateSegments(this.configuredSegments);
|
|
395
|
+
}
|
|
396
|
+
validateSegments(segments) {
|
|
397
|
+
if (segments.length === 0) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
segments.forEach((segment, index) => {
|
|
401
|
+
if (!Number.isFinite(segment.from) ||
|
|
402
|
+
!Number.isFinite(segment.to)) {
|
|
403
|
+
throw new Error(`GaugeChart: gauge.segments[${index}] must use finite from/to values, received from='${segment.from}' and to='${segment.to}'`);
|
|
404
|
+
}
|
|
405
|
+
if (segment.from >= segment.to) {
|
|
406
|
+
throw new Error(`GaugeChart: gauge.segments[${index}] must have from < to, received from='${segment.from}' and to='${segment.to}'`);
|
|
407
|
+
}
|
|
408
|
+
if (segment.from < this.minValue || segment.to > this.maxValue) {
|
|
409
|
+
throw new Error(`GaugeChart: gauge.segments[${index}] must be within [${this.minValue}, ${this.maxValue}], received from='${segment.from}' and to='${segment.to}'`);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const sortedSegments = [...segments].sort((left, right) => {
|
|
413
|
+
return left.from - right.from;
|
|
414
|
+
});
|
|
415
|
+
for (let index = 1; index < sortedSegments.length; index += 1) {
|
|
416
|
+
const previous = sortedSegments[index - 1];
|
|
417
|
+
const current = sortedSegments[index];
|
|
418
|
+
if (current.from < previous.to) {
|
|
419
|
+
throw new Error(`GaugeChart: gauge.segments overlap between [${previous.from}, ${previous.to}] and [${current.from}, ${current.to}]`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
refreshResolvedValues() {
|
|
424
|
+
this.value = this.resolveValue(this.configuredValue);
|
|
425
|
+
this.targetValue = this.resolveTargetValue(this.configuredTargetValue);
|
|
426
|
+
}
|
|
427
|
+
resolveValue(configuredValue) {
|
|
428
|
+
if (configuredValue !== undefined) {
|
|
429
|
+
const parsed = this.parseFiniteNumber(configuredValue, 'gauge.value');
|
|
430
|
+
return this.clampToDomain(parsed, 'value');
|
|
431
|
+
}
|
|
432
|
+
ChartValidator.validateDataKey(this.data, this.valueKey, 'GaugeChart');
|
|
433
|
+
this.warnIfMultipleRows('value');
|
|
434
|
+
const rawValue = this.data[0][this.valueKey];
|
|
435
|
+
const parsed = this.parseFiniteNumber(rawValue, `data['${this.valueKey}']`);
|
|
436
|
+
return this.clampToDomain(parsed, 'value');
|
|
437
|
+
}
|
|
438
|
+
resolveTargetValue(configuredTargetValue) {
|
|
439
|
+
if (configuredTargetValue !== undefined) {
|
|
440
|
+
const parsed = this.parseFiniteNumber(configuredTargetValue, 'gauge.targetValue');
|
|
441
|
+
return this.clampToDomain(parsed, 'targetValue');
|
|
442
|
+
}
|
|
443
|
+
if (!this.targetValueKey) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
ChartValidator.validateDataKey(this.data, this.targetValueKey, 'GaugeChart');
|
|
447
|
+
this.warnIfMultipleRows('targetValue');
|
|
448
|
+
const rawTargetValue = this.data[0][this.targetValueKey];
|
|
449
|
+
if (rawTargetValue === undefined ||
|
|
450
|
+
rawTargetValue === null ||
|
|
451
|
+
rawTargetValue === '') {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const parsed = this.parseFiniteNumber(rawTargetValue, `data['${this.targetValueKey}']`);
|
|
455
|
+
return this.clampToDomain(parsed, 'targetValue');
|
|
456
|
+
}
|
|
457
|
+
warnIfMultipleRows(field) {
|
|
458
|
+
if (this.data.length <= 1) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
ChartValidator.warn(`GaugeChart: received ${this.data.length} rows; using the first row for ${field}`);
|
|
462
|
+
}
|
|
463
|
+
parseFiniteNumber(rawValue, fieldLabel) {
|
|
464
|
+
const parsed = typeof rawValue === 'number'
|
|
465
|
+
? rawValue
|
|
466
|
+
: typeof rawValue === 'string'
|
|
467
|
+
? parseFloat(rawValue)
|
|
468
|
+
: NaN;
|
|
469
|
+
if (!Number.isFinite(parsed)) {
|
|
470
|
+
throw new Error(`GaugeChart: ${fieldLabel} must be a finite number, received '${rawValue}'`);
|
|
471
|
+
}
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
clampToDomain(value, label) {
|
|
475
|
+
const clamped = Math.min(this.maxValue, Math.max(this.minValue, value));
|
|
476
|
+
if (clamped !== value) {
|
|
477
|
+
ChartValidator.warn(`GaugeChart: clamped ${label} from '${value}' to '${clamped}' within [${this.minValue}, ${this.maxValue}]`);
|
|
478
|
+
}
|
|
479
|
+
return clamped;
|
|
480
|
+
}
|
|
481
|
+
prepareSegments() {
|
|
482
|
+
if (this.configuredSegments.length === 0) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
const sortedSegments = [...this.configuredSegments].sort((left, right) => {
|
|
486
|
+
return left.from - right.from;
|
|
487
|
+
});
|
|
488
|
+
const labelCount = new Map();
|
|
489
|
+
return sortedSegments.map((segment, index) => {
|
|
490
|
+
const baseLabel = segment.label?.trim()
|
|
491
|
+
? segment.label.trim()
|
|
492
|
+
: `${this.defaultFormat(segment.from)}-${this.defaultFormat(segment.to)}`;
|
|
493
|
+
const existingCount = labelCount.get(baseLabel) ?? 0;
|
|
494
|
+
labelCount.set(baseLabel, existingCount + 1);
|
|
495
|
+
return {
|
|
496
|
+
...segment,
|
|
497
|
+
color: segment.color ??
|
|
498
|
+
this.getThemePaletteColor(index),
|
|
499
|
+
legendLabel: existingCount === 0
|
|
500
|
+
? baseLabel
|
|
501
|
+
: `${baseLabel} (${existingCount + 1})`,
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
addChild(component) {
|
|
506
|
+
const type = component.type;
|
|
507
|
+
if (type === 'tooltip') {
|
|
508
|
+
this.tooltip = component;
|
|
509
|
+
}
|
|
510
|
+
else if (type === 'legend') {
|
|
511
|
+
this.legend = component;
|
|
512
|
+
this.legend.setToggleCallback(() => this.update(this.data));
|
|
513
|
+
}
|
|
514
|
+
else if (type === 'title') {
|
|
515
|
+
this.title = component;
|
|
516
|
+
}
|
|
517
|
+
return this;
|
|
518
|
+
}
|
|
519
|
+
getExportComponents() {
|
|
520
|
+
const components = [];
|
|
521
|
+
if (this.title) {
|
|
522
|
+
components.push(this.title);
|
|
523
|
+
}
|
|
524
|
+
if (this.tooltip) {
|
|
525
|
+
components.push(this.tooltip);
|
|
526
|
+
}
|
|
527
|
+
if (this.legend) {
|
|
528
|
+
components.push(this.legend);
|
|
529
|
+
}
|
|
530
|
+
return components;
|
|
531
|
+
}
|
|
532
|
+
update(data) {
|
|
533
|
+
this.data = data;
|
|
534
|
+
this.refreshResolvedValues();
|
|
535
|
+
super.update(data);
|
|
536
|
+
}
|
|
537
|
+
getLayoutComponents() {
|
|
538
|
+
const components = [];
|
|
539
|
+
if (this.title) {
|
|
540
|
+
components.push(this.title);
|
|
541
|
+
}
|
|
542
|
+
if (this.legend) {
|
|
543
|
+
components.push(this.legend);
|
|
544
|
+
}
|
|
545
|
+
return components;
|
|
546
|
+
}
|
|
547
|
+
createExportChart() {
|
|
548
|
+
return new GaugeChart({
|
|
549
|
+
data: this.data,
|
|
550
|
+
theme: this.theme,
|
|
551
|
+
valueKey: this.valueKey,
|
|
552
|
+
targetValueKey: this.targetValueKey,
|
|
553
|
+
gauge: {
|
|
554
|
+
value: this.configuredValue,
|
|
555
|
+
targetValue: this.configuredTargetValue,
|
|
556
|
+
min: this.minValue,
|
|
557
|
+
max: this.maxValue,
|
|
558
|
+
halfCircle: this.halfCircle,
|
|
559
|
+
startAngle: this.startAngle,
|
|
560
|
+
endAngle: this.endAngle,
|
|
561
|
+
innerRadius: this.innerRadiusRatio,
|
|
562
|
+
thickness: this.thickness ?? undefined,
|
|
563
|
+
cornerRadius: this.cornerRadius,
|
|
564
|
+
trackColor: this.trackColor,
|
|
565
|
+
progressColor: this.progressColor,
|
|
566
|
+
targetColor: this.targetColor,
|
|
567
|
+
segmentStyle: this.segmentStyle,
|
|
568
|
+
segments: this.configuredSegments,
|
|
569
|
+
needle: this.needle,
|
|
570
|
+
marker: this.marker,
|
|
571
|
+
showValue: this.showValue,
|
|
572
|
+
valueFormatter: this.valueFormatter,
|
|
573
|
+
valueLabelStyle: this.valueLabelStyle,
|
|
574
|
+
ticks: {
|
|
575
|
+
...this.ticks,
|
|
576
|
+
labelStyle: this.tickLabelStyle,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
renderChart() {
|
|
582
|
+
if (!this.plotArea || !this.svg || !this.plotGroup) {
|
|
583
|
+
throw new Error('Plot area not calculated');
|
|
584
|
+
}
|
|
585
|
+
this.svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
|
|
586
|
+
if (this.title) {
|
|
587
|
+
const titlePosition = this.layoutManager.getComponentPosition(this.title);
|
|
588
|
+
this.title.render(this.svg, this.theme, this.width, titlePosition.x, titlePosition.y);
|
|
589
|
+
}
|
|
590
|
+
if (this.tooltip) {
|
|
591
|
+
this.tooltip.initialize(this.theme);
|
|
592
|
+
}
|
|
593
|
+
const labelAllowance = this.ticks.show && this.ticks.showLabels
|
|
594
|
+
? this.ticks.size + this.ticks.labelOffset + 14
|
|
595
|
+
: this.ticks.show && this.ticks.showLines
|
|
596
|
+
? this.ticks.size + 10
|
|
597
|
+
: 8;
|
|
598
|
+
let outerRadius;
|
|
599
|
+
const centerX = this.plotArea.left + this.plotArea.width / 2;
|
|
600
|
+
let centerY;
|
|
601
|
+
if (this.halfCircle) {
|
|
602
|
+
const valueSpace = this.showValue
|
|
603
|
+
? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
|
|
604
|
+
: DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
|
|
605
|
+
const maxHorizontalRadius = this.plotArea.width / 2 - labelAllowance - 8;
|
|
606
|
+
const maxVerticalRadius = this.plotArea.height - valueSpace - labelAllowance - 8;
|
|
607
|
+
outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
|
|
608
|
+
centerY = this.plotArea.top + labelAllowance + outerRadius + 4;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const maxRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
|
|
612
|
+
outerRadius = Math.max(24, maxRadius - labelAllowance);
|
|
613
|
+
centerY = this.plotArea.top + this.plotArea.height / 2;
|
|
614
|
+
}
|
|
615
|
+
const innerRadius = this.thickness !== null
|
|
616
|
+
? Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1))
|
|
617
|
+
: Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
|
|
618
|
+
const gaugeGroup = this.plotGroup
|
|
619
|
+
.append('g')
|
|
620
|
+
.attr('class', 'gauge')
|
|
621
|
+
.attr('transform', `translate(${centerX}, ${centerY})`);
|
|
622
|
+
this.renderTrack(gaugeGroup, innerRadius, outerRadius);
|
|
623
|
+
const visibleSegments = this.getVisibleSegments();
|
|
624
|
+
if (visibleSegments.length > 0) {
|
|
625
|
+
if (this.segmentStyle === 'gradient') {
|
|
626
|
+
this.renderGradientSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
this.renderSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const progressColor = this.resolveProgressColor(visibleSegments);
|
|
633
|
+
const shouldRenderProgress = visibleSegments.length === 0;
|
|
634
|
+
if (shouldRenderProgress) {
|
|
635
|
+
const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
|
|
636
|
+
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer);
|
|
637
|
+
}
|
|
638
|
+
if (this.ticks.show &&
|
|
639
|
+
this.ticks.count > 0 &&
|
|
640
|
+
(this.ticks.showLines || this.ticks.showLabels)) {
|
|
641
|
+
this.renderTicks(gaugeGroup, outerRadius);
|
|
642
|
+
}
|
|
643
|
+
if (this.showValue) {
|
|
644
|
+
this.renderValueText(gaugeGroup, outerRadius);
|
|
645
|
+
}
|
|
646
|
+
if (this.targetValue !== null) {
|
|
647
|
+
this.renderTargetMarker(gaugeGroup, innerRadius, outerRadius);
|
|
648
|
+
}
|
|
649
|
+
if (this.needle.show) {
|
|
650
|
+
this.renderNeedle(gaugeGroup, innerRadius, outerRadius);
|
|
651
|
+
}
|
|
652
|
+
else if (this.marker.show) {
|
|
653
|
+
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius);
|
|
654
|
+
}
|
|
655
|
+
if (this.tooltip) {
|
|
656
|
+
this.attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor);
|
|
657
|
+
}
|
|
658
|
+
this.renderLegend();
|
|
659
|
+
}
|
|
660
|
+
buildAriaLabel() {
|
|
661
|
+
const statusLabel = this.findSegmentStatusLabel();
|
|
662
|
+
const targetText = this.targetValue === null
|
|
663
|
+
? ''
|
|
664
|
+
: ` Target ${this.valueFormatter(this.targetValue)}.`;
|
|
665
|
+
const statusText = statusLabel ? ` Status: ${statusLabel}.` : '';
|
|
666
|
+
return `Gauge chart. Value ${this.valueFormatter(this.value)} on scale ${this.valueFormatter(this.minValue)} to ${this.valueFormatter(this.maxValue)}.${targetText}${statusText}`;
|
|
667
|
+
}
|
|
668
|
+
findSegmentStatusLabel() {
|
|
669
|
+
if (this.segments.length === 0) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
for (const segment of this.segments) {
|
|
673
|
+
if (this.value >= segment.from && this.value <= segment.to) {
|
|
674
|
+
return segment.label ?? segment.legendLabel;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
getVisibleSegments() {
|
|
680
|
+
if (!this.legend) {
|
|
681
|
+
return this.segments;
|
|
682
|
+
}
|
|
683
|
+
return this.segments.filter((segment) => {
|
|
684
|
+
return this.legend.isSeriesVisible(segment.legendLabel);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
resolveProgressColor(segments) {
|
|
688
|
+
for (const segment of segments) {
|
|
689
|
+
if (this.value >= segment.from && this.value <= segment.to) {
|
|
690
|
+
return segment.color;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return this.progressColor;
|
|
694
|
+
}
|
|
695
|
+
valueToAngle(value) {
|
|
696
|
+
const ratio = (value - this.minValue) / (this.maxValue - this.minValue);
|
|
697
|
+
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
|
698
|
+
return (this.startAngle + clampedRatio * (this.endAngle - this.startAngle));
|
|
699
|
+
}
|
|
700
|
+
getProgressRadii(innerRadius, outerRadius) {
|
|
701
|
+
const bandThickness = Math.max(0, outerRadius - innerRadius);
|
|
702
|
+
const maxInset = Math.max(0, (bandThickness - MIN_PROGRESS_BAND_THICKNESS) / 2);
|
|
703
|
+
const inset = Math.min(DEFAULT_PROGRESS_RADIUS_INSET, maxInset);
|
|
704
|
+
return {
|
|
705
|
+
inner: innerRadius + inset,
|
|
706
|
+
outer: outerRadius - inset,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
pointAt(angle, radius) {
|
|
710
|
+
return {
|
|
711
|
+
x: Math.sin(angle) * radius,
|
|
712
|
+
y: -Math.cos(angle) * radius,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
renderTrack(gaugeGroup, innerRadius, outerRadius) {
|
|
716
|
+
const trackArc = arc()
|
|
717
|
+
.innerRadius(innerRadius)
|
|
718
|
+
.outerRadius(outerRadius)
|
|
719
|
+
.startAngle(this.startAngle)
|
|
720
|
+
.endAngle(this.endAngle)
|
|
721
|
+
.cornerRadius(this.cornerRadius);
|
|
722
|
+
gaugeGroup
|
|
723
|
+
.append('path')
|
|
724
|
+
.attr('class', 'gauge-track')
|
|
725
|
+
.attr('fill', this.trackColor)
|
|
726
|
+
.attr('d', trackArc(DUMMY_ARC_DATUM));
|
|
727
|
+
}
|
|
728
|
+
renderSegments(gaugeGroup, segments, innerRadius, outerRadius) {
|
|
729
|
+
const segmentArc = arc()
|
|
730
|
+
.innerRadius(innerRadius)
|
|
731
|
+
.outerRadius(outerRadius)
|
|
732
|
+
.startAngle((segment) => this.valueToAngle(segment.from))
|
|
733
|
+
.endAngle((segment) => this.valueToAngle(segment.to))
|
|
734
|
+
.cornerRadius(this.cornerRadius);
|
|
735
|
+
gaugeGroup
|
|
736
|
+
.append('g')
|
|
737
|
+
.attr('class', 'gauge-segments')
|
|
738
|
+
.selectAll('.gauge-segment')
|
|
739
|
+
.data(segments)
|
|
740
|
+
.join('path')
|
|
741
|
+
.attr('class', 'gauge-segment')
|
|
742
|
+
.attr('fill', (segment) => segment.color)
|
|
743
|
+
.attr('opacity', 0.92)
|
|
744
|
+
.attr('d', (segment) => segmentArc(segment));
|
|
745
|
+
}
|
|
746
|
+
renderGradientSegments(gaugeGroup, segments, innerRadius, outerRadius) {
|
|
747
|
+
if (!this.svg || segments.length === 0) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const defs = this.svg.select('defs').empty()
|
|
751
|
+
? this.svg.append('defs')
|
|
752
|
+
: this.svg.select('defs');
|
|
753
|
+
const gradientId = `gauge-gradient-${Math.random()
|
|
754
|
+
.toString(36)
|
|
755
|
+
.slice(2, 10)}`;
|
|
756
|
+
const midRadius = (innerRadius + outerRadius) / 2;
|
|
757
|
+
const startPoint = this.pointAt(this.startAngle, midRadius);
|
|
758
|
+
const endPoint = this.pointAt(this.endAngle, midRadius);
|
|
759
|
+
const strokeWidth = Math.max(1, outerRadius - innerRadius);
|
|
760
|
+
const sortedSegments = [...segments].sort((left, right) => {
|
|
761
|
+
return left.from - right.from;
|
|
762
|
+
});
|
|
763
|
+
const gradient = defs
|
|
764
|
+
.append('linearGradient')
|
|
765
|
+
.attr('id', gradientId)
|
|
766
|
+
.attr('gradientUnits', 'userSpaceOnUse')
|
|
767
|
+
.attr('x1', startPoint.x)
|
|
768
|
+
.attr('y1', startPoint.y)
|
|
769
|
+
.attr('x2', endPoint.x)
|
|
770
|
+
.attr('y2', endPoint.y);
|
|
771
|
+
if (sortedSegments.length > 0) {
|
|
772
|
+
const firstSegment = sortedSegments[0];
|
|
773
|
+
gradient
|
|
774
|
+
.append('stop')
|
|
775
|
+
.attr('offset', '0%')
|
|
776
|
+
.attr('stop-color', firstSegment.color);
|
|
777
|
+
sortedSegments.forEach((segment) => {
|
|
778
|
+
const toOffset = ((segment.to - this.minValue) /
|
|
779
|
+
(this.maxValue - this.minValue)) *
|
|
780
|
+
100;
|
|
781
|
+
gradient
|
|
782
|
+
.append('stop')
|
|
783
|
+
.attr('offset', `${Math.min(100, Math.max(0, toOffset)).toFixed(3)}%`)
|
|
784
|
+
.attr('stop-color', segment.color);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
const sweep = this.endAngle - this.startAngle;
|
|
788
|
+
const largeArcFlag = Math.abs(sweep) > Math.PI ? 1 : 0;
|
|
789
|
+
const sweepFlag = sweep >= 0 ? 1 : 0;
|
|
790
|
+
const gradientPath = `M${startPoint.x},${startPoint.y} A${midRadius},${midRadius} 0 ${largeArcFlag},${sweepFlag} ${endPoint.x},${endPoint.y}`;
|
|
791
|
+
gaugeGroup
|
|
792
|
+
.append('path')
|
|
793
|
+
.attr('class', 'gauge-gradient-ring')
|
|
794
|
+
.attr('d', gradientPath)
|
|
795
|
+
.attr('fill', 'none')
|
|
796
|
+
.attr('stroke', `url(#${gradientId})`)
|
|
797
|
+
.attr('stroke-width', strokeWidth)
|
|
798
|
+
.attr('stroke-linecap', 'butt')
|
|
799
|
+
.attr('stroke-opacity', 0.95)
|
|
800
|
+
.style('pointer-events', 'none');
|
|
801
|
+
}
|
|
802
|
+
renderProgress(gaugeGroup, progressColor, innerRadius, outerRadius) {
|
|
803
|
+
const progressArc = arc()
|
|
804
|
+
.innerRadius(innerRadius)
|
|
805
|
+
.outerRadius(outerRadius)
|
|
806
|
+
.startAngle(this.startAngle)
|
|
807
|
+
.endAngle(this.valueToAngle(this.value))
|
|
808
|
+
.cornerRadius(this.cornerRadius);
|
|
809
|
+
gaugeGroup
|
|
810
|
+
.append('path')
|
|
811
|
+
.attr('class', 'gauge-progress')
|
|
812
|
+
.attr('fill', progressColor)
|
|
813
|
+
.attr('d', progressArc(DUMMY_ARC_DATUM));
|
|
814
|
+
}
|
|
815
|
+
renderTicks(gaugeGroup, outerRadius) {
|
|
816
|
+
const tickGroup = gaugeGroup.append('g').attr('class', 'gauge-ticks');
|
|
817
|
+
for (let tickIndex = 0; tickIndex <= this.ticks.count; tickIndex += 1) {
|
|
818
|
+
const ratio = tickIndex / this.ticks.count;
|
|
819
|
+
const value = this.minValue + ratio * (this.maxValue - this.minValue);
|
|
820
|
+
const angle = this.valueToAngle(value);
|
|
821
|
+
const innerTickRadius = outerRadius + 2;
|
|
822
|
+
const outerTickRadius = innerTickRadius + this.ticks.size;
|
|
823
|
+
const innerPoint = this.pointAt(angle, innerTickRadius);
|
|
824
|
+
const outerPoint = this.pointAt(angle, outerTickRadius);
|
|
825
|
+
if (this.ticks.showLines) {
|
|
826
|
+
tickGroup
|
|
827
|
+
.append('line')
|
|
828
|
+
.attr('class', 'gauge-tick')
|
|
829
|
+
.attr('x1', innerPoint.x)
|
|
830
|
+
.attr('y1', innerPoint.y)
|
|
831
|
+
.attr('x2', outerPoint.x)
|
|
832
|
+
.attr('y2', outerPoint.y)
|
|
833
|
+
.attr('stroke', DEFAULT_TICK_LINE_COLOR)
|
|
834
|
+
.attr('stroke-width', DEFAULT_TICK_LINE_STROKE_WIDTH);
|
|
835
|
+
}
|
|
836
|
+
if (!this.ticks.showLabels) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const labelRadius = outerTickRadius + this.ticks.labelOffset;
|
|
840
|
+
const labelPoint = this.pointAt(angle, labelRadius);
|
|
841
|
+
const alignment = Math.abs(labelPoint.x) < 10
|
|
842
|
+
? 'middle'
|
|
843
|
+
: labelPoint.x > 0
|
|
844
|
+
? 'start'
|
|
845
|
+
: 'end';
|
|
846
|
+
tickGroup
|
|
847
|
+
.append('text')
|
|
848
|
+
.attr('class', 'gauge-tick-label')
|
|
849
|
+
.attr('x', labelPoint.x)
|
|
850
|
+
.attr('y', labelPoint.y)
|
|
851
|
+
.attr('text-anchor', alignment)
|
|
852
|
+
.attr('dominant-baseline', 'middle')
|
|
853
|
+
.attr('font-size', this.tickLabelStyle.fontSize)
|
|
854
|
+
.attr('font-family', this.tickLabelStyle.fontFamily)
|
|
855
|
+
.attr('font-weight', this.tickLabelStyle.fontWeight)
|
|
856
|
+
.attr('fill', this.tickLabelStyle.color)
|
|
857
|
+
.text(this.ticks.formatter(value));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
renderTargetMarker(gaugeGroup, innerRadius, outerRadius) {
|
|
861
|
+
if (this.targetValue === null) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const targetAngle = this.valueToAngle(this.targetValue);
|
|
865
|
+
const markerInner = innerRadius + 1;
|
|
866
|
+
const markerOuter = Math.max(markerInner + 1, outerRadius - 1);
|
|
867
|
+
const innerPoint = this.pointAt(targetAngle, markerInner);
|
|
868
|
+
const outerPoint = this.pointAt(targetAngle, markerOuter);
|
|
869
|
+
gaugeGroup
|
|
870
|
+
.append('line')
|
|
871
|
+
.attr('class', 'gauge-target-marker')
|
|
872
|
+
.attr('x1', innerPoint.x)
|
|
873
|
+
.attr('y1', innerPoint.y)
|
|
874
|
+
.attr('x2', outerPoint.x)
|
|
875
|
+
.attr('y2', outerPoint.y)
|
|
876
|
+
.attr('stroke', this.targetColor)
|
|
877
|
+
.attr('stroke-width', DEFAULT_TARGET_MARKER_STROKE_WIDTH)
|
|
878
|
+
.attr('stroke-linecap', 'round');
|
|
879
|
+
}
|
|
880
|
+
renderNeedle(gaugeGroup, innerRadius, outerRadius) {
|
|
881
|
+
const needleAngle = this.valueToAngle(this.value);
|
|
882
|
+
const maxLength = Math.max(innerRadius + 2, outerRadius - 2);
|
|
883
|
+
const length = maxLength * this.needle.lengthRatio;
|
|
884
|
+
const needlePoint = this.pointAt(needleAngle, length);
|
|
885
|
+
gaugeGroup
|
|
886
|
+
.append('line')
|
|
887
|
+
.attr('class', 'gauge-needle')
|
|
888
|
+
.attr('x1', 0)
|
|
889
|
+
.attr('y1', 0)
|
|
890
|
+
.attr('x2', needlePoint.x)
|
|
891
|
+
.attr('y2', needlePoint.y)
|
|
892
|
+
.attr('stroke', this.needle.color)
|
|
893
|
+
.attr('stroke-width', this.needle.width)
|
|
894
|
+
.attr('stroke-linecap', 'round');
|
|
895
|
+
gaugeGroup
|
|
896
|
+
.append('circle')
|
|
897
|
+
.attr('class', 'gauge-needle-cap')
|
|
898
|
+
.attr('cx', 0)
|
|
899
|
+
.attr('cy', 0)
|
|
900
|
+
.attr('r', this.needle.capRadius)
|
|
901
|
+
.attr('fill', this.needle.color);
|
|
902
|
+
}
|
|
903
|
+
renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius) {
|
|
904
|
+
const markerAngle = this.valueToAngle(this.value);
|
|
905
|
+
const markerInner = innerRadius + 1;
|
|
906
|
+
const markerOuter = Math.max(markerInner + 1, outerRadius - 1);
|
|
907
|
+
const innerPoint = this.pointAt(markerAngle, markerInner);
|
|
908
|
+
const outerPoint = this.pointAt(markerAngle, markerOuter);
|
|
909
|
+
gaugeGroup
|
|
910
|
+
.append('line')
|
|
911
|
+
.attr('class', 'gauge-marker')
|
|
912
|
+
.attr('x1', innerPoint.x)
|
|
913
|
+
.attr('y1', innerPoint.y)
|
|
914
|
+
.attr('x2', outerPoint.x)
|
|
915
|
+
.attr('y2', outerPoint.y)
|
|
916
|
+
.attr('stroke', this.marker.color)
|
|
917
|
+
.attr('stroke-width', this.marker.width)
|
|
918
|
+
.attr('stroke-linecap', 'round');
|
|
919
|
+
}
|
|
920
|
+
renderValueText(gaugeGroup, outerRadius) {
|
|
921
|
+
const mainValueY = this.halfCircle
|
|
922
|
+
? DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y
|
|
923
|
+
: Math.max(DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y, outerRadius * 0.58);
|
|
924
|
+
gaugeGroup
|
|
925
|
+
.append('text')
|
|
926
|
+
.attr('class', 'gauge-value')
|
|
927
|
+
.attr('x', 0)
|
|
928
|
+
.attr('y', mainValueY)
|
|
929
|
+
.attr('text-anchor', 'middle')
|
|
930
|
+
.attr('font-size', this.valueLabelStyle.fontSize)
|
|
931
|
+
.attr('font-weight', this.valueLabelStyle.fontWeight)
|
|
932
|
+
.attr('font-family', this.valueLabelStyle.fontFamily)
|
|
933
|
+
.attr('fill', this.valueLabelStyle.color)
|
|
934
|
+
.text(this.valueFormatter(this.value));
|
|
935
|
+
}
|
|
936
|
+
attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor) {
|
|
937
|
+
const interactionArc = arc()
|
|
938
|
+
.innerRadius(Math.max(0, innerRadius - 20))
|
|
939
|
+
.outerRadius(outerRadius + 20)
|
|
940
|
+
.startAngle(this.startAngle)
|
|
941
|
+
.endAngle(this.endAngle);
|
|
942
|
+
gaugeGroup
|
|
943
|
+
.append('path')
|
|
944
|
+
.attr('class', 'gauge-interaction-layer')
|
|
945
|
+
.attr('fill', 'transparent')
|
|
946
|
+
.attr('d', interactionArc(DUMMY_ARC_DATUM))
|
|
947
|
+
.style('pointer-events', 'all')
|
|
948
|
+
.style('cursor', 'pointer')
|
|
949
|
+
.on('mouseenter', (event) => {
|
|
950
|
+
const tooltipDiv = this.resolveTooltipDiv();
|
|
951
|
+
if (!tooltipDiv) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
tooltipDiv
|
|
955
|
+
.style('visibility', 'visible')
|
|
956
|
+
.html(this.buildTooltipContent(progressColor));
|
|
957
|
+
this.positionTooltip(event, tooltipDiv);
|
|
958
|
+
})
|
|
959
|
+
.on('mousemove', (event) => {
|
|
960
|
+
const tooltipDiv = this.resolveTooltipDiv();
|
|
961
|
+
if (!tooltipDiv) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
tooltipDiv
|
|
965
|
+
.style('visibility', 'visible')
|
|
966
|
+
.html(this.buildTooltipContent(progressColor));
|
|
967
|
+
this.positionTooltip(event, tooltipDiv);
|
|
968
|
+
})
|
|
969
|
+
.on('mouseleave', () => {
|
|
970
|
+
const tooltipDiv = this.resolveTooltipDiv();
|
|
971
|
+
if (!tooltipDiv) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
tooltipDiv.style('visibility', 'hidden');
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
resolveTooltipDiv() {
|
|
978
|
+
if (!this.tooltip) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
return select(`#${this.tooltip.id}`);
|
|
982
|
+
}
|
|
983
|
+
buildTooltipContent(progressColor) {
|
|
984
|
+
const payload = {
|
|
985
|
+
value: this.value,
|
|
986
|
+
targetValue: this.targetValue,
|
|
987
|
+
min: this.minValue,
|
|
988
|
+
max: this.maxValue,
|
|
989
|
+
};
|
|
990
|
+
if (this.tooltip?.customFormatter) {
|
|
991
|
+
const series = [
|
|
992
|
+
{ dataKey: 'value', fill: progressColor },
|
|
993
|
+
{ dataKey: 'targetValue', stroke: this.targetColor },
|
|
994
|
+
];
|
|
995
|
+
return this.tooltip.customFormatter(payload, series);
|
|
996
|
+
}
|
|
997
|
+
if (this.tooltip?.formatter) {
|
|
998
|
+
let content = '<strong>Gauge</strong><br/>';
|
|
999
|
+
content += this.tooltip.formatter('value', this.value, payload);
|
|
1000
|
+
if (this.targetValue !== null) {
|
|
1001
|
+
content += '<br/>';
|
|
1002
|
+
content += this.tooltip.formatter('targetValue', this.targetValue, payload);
|
|
1003
|
+
}
|
|
1004
|
+
return content;
|
|
1005
|
+
}
|
|
1006
|
+
let content = `<strong>Value</strong>: ${this.valueFormatter(this.value)}`;
|
|
1007
|
+
if (this.targetValue !== null) {
|
|
1008
|
+
content += `<br/><strong>Target</strong>: ${this.valueFormatter(this.targetValue)}`;
|
|
1009
|
+
}
|
|
1010
|
+
return content;
|
|
1011
|
+
}
|
|
1012
|
+
positionTooltip(event, tooltipDiv) {
|
|
1013
|
+
const node = tooltipDiv.node();
|
|
1014
|
+
if (!node) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const rect = node.getBoundingClientRect();
|
|
1018
|
+
let x = event.pageX + TOOLTIP_OFFSET_PX;
|
|
1019
|
+
let y = event.pageY - rect.height / 2;
|
|
1020
|
+
if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
1021
|
+
x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
|
|
1022
|
+
}
|
|
1023
|
+
x = Math.max(EDGE_MARGIN_PX, x);
|
|
1024
|
+
y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
|
|
1025
|
+
window.scrollY -
|
|
1026
|
+
rect.height -
|
|
1027
|
+
EDGE_MARGIN_PX));
|
|
1028
|
+
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
1029
|
+
}
|
|
1030
|
+
renderLegend() {
|
|
1031
|
+
if (!this.legend || !this.svg || this.segments.length === 0) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const legendPosition = this.layoutManager.getComponentPosition(this.legend);
|
|
1035
|
+
const legendSeries = this.segments.map((segment) => ({
|
|
1036
|
+
dataKey: segment.legendLabel,
|
|
1037
|
+
fill: segment.color,
|
|
1038
|
+
}));
|
|
1039
|
+
this.legend.render(this.svg, legendSeries, this.theme, this.width, legendPosition.x, legendPosition.y);
|
|
1040
|
+
}
|
|
1041
|
+
}
|