@internetstiftelsen/charts 0.9.2 → 0.10.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.
- package/README.md +137 -3
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +20 -1
- package/dist/bar.js +395 -519
- package/dist/base-chart.d.ts +21 -1
- package/dist/base-chart.js +166 -93
- package/dist/chart-group.d.ts +137 -0
- package/dist/chart-group.js +1155 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +30 -15
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +229 -133
- package/dist/legend-state.d.ts +19 -0
- package/dist/legend-state.js +81 -0
- package/dist/legend.d.ts +5 -2
- package/dist/legend.js +45 -38
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +45 -19
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +165 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +21 -25
- package/dist/types.d.ts +19 -1
- package/dist/utils.js +11 -19
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +40 -1
- package/dist/xy-chart.js +488 -165
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/chart-group.md +213 -0
- package/docs/components.md +321 -0
- package/docs/donut-chart.md +193 -0
- package/docs/gauge-chart.md +175 -0
- package/docs/getting-started.md +311 -0
- package/docs/pie-chart.md +123 -0
- package/docs/theming.md +162 -0
- package/docs/word-cloud-chart.md +98 -0
- package/docs/xy-chart.md +517 -0
- package/package.json +6 -4
package/dist/gauge-chart.js
CHANGED
|
@@ -2,6 +2,9 @@ import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElas
|
|
|
2
2
|
import { BaseChart, } from './base-chart.js';
|
|
3
3
|
import { DEFAULT_COLOR_PALETTE } from './theme.js';
|
|
4
4
|
import { ChartValidator } from './validation.js';
|
|
5
|
+
function resolveDefault(value, fallback) {
|
|
6
|
+
return value === undefined ? fallback : value;
|
|
7
|
+
}
|
|
5
8
|
const DEFAULT_START_ANGLE = -Math.PI * 0.75;
|
|
6
9
|
const DEFAULT_END_ANGLE = Math.PI * 0.75;
|
|
7
10
|
const DEFAULT_HALF_START_ANGLE = -Math.PI / 2;
|
|
@@ -251,32 +254,27 @@ export class GaugeChart extends BaseChart {
|
|
|
251
254
|
}
|
|
252
255
|
});
|
|
253
256
|
const gauge = config.gauge ?? {};
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
256
|
-
this.
|
|
257
|
-
this.
|
|
258
|
-
this.
|
|
259
|
-
this.
|
|
260
|
-
this.
|
|
257
|
+
const resolvedGauge = this.resolveGaugeConstructorValues(config, gauge);
|
|
258
|
+
this.configuredValue = resolvedGauge.configuredValue;
|
|
259
|
+
this.configuredTargetValue = resolvedGauge.configuredTargetValue;
|
|
260
|
+
this.configuredSegments = resolvedGauge.configuredSegments;
|
|
261
|
+
this.valueKey = resolvedGauge.valueKey;
|
|
262
|
+
this.targetValueKey = resolvedGauge.targetValueKey;
|
|
263
|
+
this.minValue = resolvedGauge.minValue;
|
|
264
|
+
this.maxValue = resolvedGauge.maxValue;
|
|
261
265
|
this.animation = this.normalizeAnimationConfig(gauge.animate);
|
|
262
|
-
this.halfCircle =
|
|
263
|
-
this.startAngle =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
this.
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
273
|
-
this.
|
|
274
|
-
gauge.progressColor ??
|
|
275
|
-
this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX);
|
|
276
|
-
this.targetColor = gauge.targetColor ?? DEFAULT_TARGET_COLOR;
|
|
277
|
-
this.segmentStyle = gauge.segmentStyle ?? DEFAULT_SEGMENT_STYLE;
|
|
278
|
-
this.valueFormatter = gauge.valueFormatter ?? this.defaultFormat;
|
|
279
|
-
this.showValue = gauge.showValue ?? DEFAULT_SHOW_VALUE;
|
|
266
|
+
this.halfCircle = resolvedGauge.halfCircle;
|
|
267
|
+
this.startAngle = resolvedGauge.startAngle;
|
|
268
|
+
this.endAngle = resolvedGauge.endAngle;
|
|
269
|
+
this.innerRadiusRatio = resolvedGauge.innerRadiusRatio;
|
|
270
|
+
this.thickness = resolvedGauge.thickness;
|
|
271
|
+
this.cornerRadius = resolvedGauge.cornerRadius;
|
|
272
|
+
this.trackColor = resolvedGauge.trackColor;
|
|
273
|
+
this.progressColor = resolvedGauge.progressColor;
|
|
274
|
+
this.targetColor = resolvedGauge.targetColor;
|
|
275
|
+
this.segmentStyle = resolvedGauge.segmentStyle;
|
|
276
|
+
this.valueFormatter = resolvedGauge.valueFormatter;
|
|
277
|
+
this.showValue = resolvedGauge.showValue;
|
|
280
278
|
this.needle = this.normalizeNeedleConfig(gauge.needle);
|
|
281
279
|
this.marker = this.normalizeMarkerConfig(gauge.marker, !this.needle.show);
|
|
282
280
|
this.ticks = this.normalizeTickConfig(gauge.ticks);
|
|
@@ -286,6 +284,30 @@ export class GaugeChart extends BaseChart {
|
|
|
286
284
|
this.segments = this.prepareSegments();
|
|
287
285
|
this.initializeDataState();
|
|
288
286
|
}
|
|
287
|
+
resolveGaugeConstructorValues(config, gauge) {
|
|
288
|
+
const halfCircle = resolveDefault(gauge.halfCircle, DEFAULT_HALF_CIRCLE);
|
|
289
|
+
return {
|
|
290
|
+
configuredValue: gauge.value,
|
|
291
|
+
configuredTargetValue: gauge.targetValue,
|
|
292
|
+
configuredSegments: resolveDefault(gauge.segments, []),
|
|
293
|
+
valueKey: resolveDefault(config.valueKey, DEFAULT_VALUE_KEY),
|
|
294
|
+
targetValueKey: config.targetValueKey,
|
|
295
|
+
minValue: resolveDefault(gauge.min, DEFAULT_MIN_VALUE),
|
|
296
|
+
maxValue: resolveDefault(gauge.max, DEFAULT_MAX_VALUE),
|
|
297
|
+
halfCircle,
|
|
298
|
+
startAngle: resolveDefault(gauge.startAngle, halfCircle ? DEFAULT_HALF_START_ANGLE : DEFAULT_START_ANGLE),
|
|
299
|
+
endAngle: resolveDefault(gauge.endAngle, halfCircle ? DEFAULT_HALF_END_ANGLE : DEFAULT_END_ANGLE),
|
|
300
|
+
innerRadiusRatio: resolveDefault(gauge.innerRadius, DEFAULT_INNER_RADIUS_RATIO),
|
|
301
|
+
thickness: resolveDefault(gauge.thickness, null),
|
|
302
|
+
cornerRadius: resolveDefault(gauge.cornerRadius, DEFAULT_CORNER_RADIUS),
|
|
303
|
+
trackColor: resolveDefault(gauge.trackColor, DEFAULT_TRACK_COLOR),
|
|
304
|
+
progressColor: resolveDefault(gauge.progressColor, this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX)),
|
|
305
|
+
targetColor: resolveDefault(gauge.targetColor, DEFAULT_TARGET_COLOR),
|
|
306
|
+
segmentStyle: resolveDefault(gauge.segmentStyle, DEFAULT_SEGMENT_STYLE),
|
|
307
|
+
valueFormatter: resolveDefault(gauge.valueFormatter, this.defaultFormat),
|
|
308
|
+
showValue: resolveDefault(gauge.showValue, DEFAULT_SHOW_VALUE),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
289
311
|
normalizeNeedleConfig(config) {
|
|
290
312
|
if (config === false) {
|
|
291
313
|
return {
|
|
@@ -342,14 +364,24 @@ export class GaugeChart extends BaseChart {
|
|
|
342
364
|
return palette[index % palette.length];
|
|
343
365
|
}
|
|
344
366
|
normalizeTickConfig(config) {
|
|
367
|
+
const resolvedConfig = {
|
|
368
|
+
count: DEFAULT_TICK_COUNT,
|
|
369
|
+
show: DEFAULT_TICKS_SHOW,
|
|
370
|
+
showLines: DEFAULT_TICKS_SHOW_LINES,
|
|
371
|
+
showLabels: DEFAULT_TICKS_SHOW_LABELS,
|
|
372
|
+
size: DEFAULT_TICK_SIZE,
|
|
373
|
+
labelOffset: DEFAULT_TICK_LABEL_OFFSET,
|
|
374
|
+
formatter: this.defaultFormat,
|
|
375
|
+
...config,
|
|
376
|
+
};
|
|
345
377
|
return {
|
|
346
|
-
count:
|
|
347
|
-
show:
|
|
348
|
-
showLines:
|
|
349
|
-
showLabels:
|
|
350
|
-
size:
|
|
351
|
-
labelOffset:
|
|
352
|
-
formatter:
|
|
378
|
+
count: resolvedConfig.count,
|
|
379
|
+
show: resolvedConfig.show,
|
|
380
|
+
showLines: resolvedConfig.showLines,
|
|
381
|
+
showLabels: resolvedConfig.showLabels,
|
|
382
|
+
size: resolvedConfig.size,
|
|
383
|
+
labelOffset: resolvedConfig.labelOffset,
|
|
384
|
+
formatter: resolvedConfig.formatter,
|
|
353
385
|
};
|
|
354
386
|
}
|
|
355
387
|
normalizeAnimationConfig(config) {
|
|
@@ -405,35 +437,51 @@ export class GaugeChart extends BaseChart {
|
|
|
405
437
|
if (tokens.length < 2) {
|
|
406
438
|
return null;
|
|
407
439
|
}
|
|
408
|
-
const rawStops = tokens.map((token) =>
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return { value, position: undefined };
|
|
419
|
-
}
|
|
420
|
-
const percentageToken = parts[1];
|
|
421
|
-
if (!percentageToken.endsWith('%')) {
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
const percentageValue = Number(percentageToken.slice(0, -1));
|
|
425
|
-
if (!Number.isFinite(percentageValue)) {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
440
|
+
const rawStops = tokens.map((token) => this.parseLinearEasingStop(token));
|
|
441
|
+
if (rawStops.some((stop) => stop === null)) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const stops = rawStops;
|
|
445
|
+
const positions = this.resolveLinearEasingPositions(stops);
|
|
446
|
+
if (!positions) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const easingPoints = stops.map((stop, index) => {
|
|
428
450
|
return {
|
|
429
|
-
value,
|
|
430
|
-
position:
|
|
451
|
+
value: stop.value,
|
|
452
|
+
position: positions[index],
|
|
431
453
|
};
|
|
432
454
|
});
|
|
433
|
-
|
|
455
|
+
return (progress) => {
|
|
456
|
+
return this.interpolateLinearEasing(progress, easingPoints);
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
parseLinearEasingStop(token) {
|
|
460
|
+
const parts = token.split(/\s+/).filter(Boolean);
|
|
461
|
+
if (parts.length === 0 || parts.length > 2) {
|
|
434
462
|
return null;
|
|
435
463
|
}
|
|
436
|
-
const
|
|
464
|
+
const value = Number(parts[0]);
|
|
465
|
+
if (!Number.isFinite(value)) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
if (parts.length === 1) {
|
|
469
|
+
return { value, position: undefined };
|
|
470
|
+
}
|
|
471
|
+
const percentageToken = parts[1];
|
|
472
|
+
if (!percentageToken.endsWith('%')) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const percentageValue = Number(percentageToken.slice(0, -1));
|
|
476
|
+
if (!Number.isFinite(percentageValue)) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
value,
|
|
481
|
+
position: percentageValue / 100,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
resolveLinearEasingPositions(stops) {
|
|
437
485
|
const positions = stops.map((stop) => stop.position);
|
|
438
486
|
if (positions[0] === undefined) {
|
|
439
487
|
positions[0] = 0;
|
|
@@ -451,47 +499,44 @@ export class GaugeChart extends BaseChart {
|
|
|
451
499
|
if (currentPosition < previousPosition) {
|
|
452
500
|
return null;
|
|
453
501
|
}
|
|
454
|
-
|
|
455
|
-
if (missingCount > 0) {
|
|
456
|
-
for (let missingIndex = 1; missingIndex <= missingCount; missingIndex += 1) {
|
|
457
|
-
const ratio = missingIndex / (missingCount + 1);
|
|
458
|
-
positions[previousDefinedIndex + missingIndex] =
|
|
459
|
-
previousPosition +
|
|
460
|
-
(currentPosition - previousPosition) * ratio;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
502
|
+
this.fillMissingLinearEasingPositions(positions, previousDefinedIndex, currentIndex);
|
|
463
503
|
previousDefinedIndex = currentIndex;
|
|
464
504
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
505
|
+
return positions;
|
|
506
|
+
}
|
|
507
|
+
fillMissingLinearEasingPositions(positions, previousDefinedIndex, currentIndex) {
|
|
508
|
+
const previousPosition = positions[previousDefinedIndex];
|
|
509
|
+
const currentPosition = positions[currentIndex];
|
|
510
|
+
const missingCount = currentIndex - previousDefinedIndex - 1;
|
|
511
|
+
for (let missingIndex = 1; missingIndex <= missingCount; missingIndex += 1) {
|
|
512
|
+
const ratio = missingIndex / (missingCount + 1);
|
|
513
|
+
positions[previousDefinedIndex + missingIndex] =
|
|
514
|
+
previousPosition + (currentPosition - previousPosition) * ratio;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
interpolateLinearEasing(progress, easingPoints) {
|
|
518
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
519
|
+
if (clamped <= easingPoints[0].position) {
|
|
520
|
+
return easingPoints[0].value;
|
|
521
|
+
}
|
|
522
|
+
const lastPoint = easingPoints[easingPoints.length - 1];
|
|
523
|
+
if (clamped >= lastPoint.position) {
|
|
524
|
+
return lastPoint.value;
|
|
525
|
+
}
|
|
526
|
+
for (let index = 0; index < easingPoints.length - 1; index += 1) {
|
|
527
|
+
const left = easingPoints[index];
|
|
528
|
+
const right = easingPoints[index + 1];
|
|
529
|
+
if (clamped > right.position) {
|
|
530
|
+
continue;
|
|
479
531
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (clamped > right.position) {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
const span = right.position - left.position;
|
|
487
|
-
if (span <= 0) {
|
|
488
|
-
return right.value;
|
|
489
|
-
}
|
|
490
|
-
const localProgress = (clamped - left.position) / span;
|
|
491
|
-
return left.value + (right.value - left.value) * localProgress;
|
|
532
|
+
const span = right.position - left.position;
|
|
533
|
+
if (span <= 0) {
|
|
534
|
+
return right.value;
|
|
492
535
|
}
|
|
493
|
-
|
|
494
|
-
|
|
536
|
+
const localProgress = (clamped - left.position) / span;
|
|
537
|
+
return left.value + (right.value - left.value) * localProgress;
|
|
538
|
+
}
|
|
539
|
+
return lastPoint.value;
|
|
495
540
|
}
|
|
496
541
|
normalizeTickLabelStyle(config) {
|
|
497
542
|
return {
|
|
@@ -512,6 +557,14 @@ export class GaugeChart extends BaseChart {
|
|
|
512
557
|
};
|
|
513
558
|
}
|
|
514
559
|
validateGaugeConfig() {
|
|
560
|
+
this.validateGaugeRange();
|
|
561
|
+
this.validateGaugeGeometry();
|
|
562
|
+
this.validateTickSettings();
|
|
563
|
+
this.validateIndicatorSettings();
|
|
564
|
+
this.validateAnimationSettings();
|
|
565
|
+
this.validateSegments(this.configuredSegments);
|
|
566
|
+
}
|
|
567
|
+
validateGaugeRange() {
|
|
515
568
|
if (!Number.isFinite(this.minValue) ||
|
|
516
569
|
!Number.isFinite(this.maxValue)) {
|
|
517
570
|
throw new Error(`GaugeChart: gauge.min and gauge.max must be finite numbers, received min='${this.minValue}' and max='${this.maxValue}'`);
|
|
@@ -519,6 +572,8 @@ export class GaugeChart extends BaseChart {
|
|
|
519
572
|
if (this.minValue >= this.maxValue) {
|
|
520
573
|
throw new Error(`GaugeChart: gauge.min must be less than gauge.max, received min='${this.minValue}' and max='${this.maxValue}'`);
|
|
521
574
|
}
|
|
575
|
+
}
|
|
576
|
+
validateGaugeGeometry() {
|
|
522
577
|
if (!Number.isFinite(this.startAngle) ||
|
|
523
578
|
!Number.isFinite(this.endAngle)) {
|
|
524
579
|
throw new Error(`GaugeChart: gauge.startAngle and gauge.endAngle must be finite numbers, received startAngle='${this.startAngle}' and endAngle='${this.endAngle}'`);
|
|
@@ -536,6 +591,8 @@ export class GaugeChart extends BaseChart {
|
|
|
536
591
|
(!Number.isFinite(this.thickness) || this.thickness <= 0)) {
|
|
537
592
|
throw new Error(`GaugeChart: gauge.thickness must be > 0 when provided, received '${this.thickness}'`);
|
|
538
593
|
}
|
|
594
|
+
}
|
|
595
|
+
validateTickSettings() {
|
|
539
596
|
if (!Number.isInteger(this.ticks.count) || this.ticks.count < 0) {
|
|
540
597
|
throw new Error(`GaugeChart: gauge.ticks.count must be a non-negative integer, received '${this.ticks.count}'`);
|
|
541
598
|
}
|
|
@@ -545,6 +602,8 @@ export class GaugeChart extends BaseChart {
|
|
|
545
602
|
if (this.ticks.labelOffset < 0) {
|
|
546
603
|
throw new Error(`GaugeChart: gauge.ticks.labelOffset must be >= 0, received '${this.ticks.labelOffset}'`);
|
|
547
604
|
}
|
|
605
|
+
}
|
|
606
|
+
validateIndicatorSettings() {
|
|
548
607
|
if (this.needle.width <= 0) {
|
|
549
608
|
throw new Error(`GaugeChart: gauge.needle.width must be > 0, received '${this.needle.width}'`);
|
|
550
609
|
}
|
|
@@ -557,11 +616,12 @@ export class GaugeChart extends BaseChart {
|
|
|
557
616
|
if (this.marker.width <= 0) {
|
|
558
617
|
throw new Error(`GaugeChart: gauge.marker.width must be > 0, received '${this.marker.width}'`);
|
|
559
618
|
}
|
|
619
|
+
}
|
|
620
|
+
validateAnimationSettings() {
|
|
560
621
|
if (!Number.isFinite(this.animation.duration) ||
|
|
561
622
|
this.animation.duration < 0) {
|
|
562
623
|
throw new Error(`GaugeChart: gauge.animate.duration must be >= 0, received '${this.animation.duration}'`);
|
|
563
624
|
}
|
|
564
|
-
this.validateSegments(this.configuredSegments);
|
|
565
625
|
}
|
|
566
626
|
validateSegments(segments) {
|
|
567
627
|
if (segments.length === 0) {
|
|
@@ -675,7 +735,7 @@ export class GaugeChart extends BaseChart {
|
|
|
675
735
|
return this.getBaseExportComponents({
|
|
676
736
|
title: true,
|
|
677
737
|
tooltip: true,
|
|
678
|
-
legend: this.
|
|
738
|
+
legend: this.shouldIncludeLegendInExport(),
|
|
679
739
|
});
|
|
680
740
|
}
|
|
681
741
|
update(data) {
|
|
@@ -729,31 +789,7 @@ export class GaugeChart extends BaseChart {
|
|
|
729
789
|
if (this.tooltip) {
|
|
730
790
|
this.tooltip.initialize(this.renderTheme);
|
|
731
791
|
}
|
|
732
|
-
const
|
|
733
|
-
? this.ticks.size + this.ticks.labelOffset + 14
|
|
734
|
-
: this.ticks.show && this.ticks.showLines
|
|
735
|
-
? this.ticks.size + 10
|
|
736
|
-
: 8;
|
|
737
|
-
let outerRadius;
|
|
738
|
-
const centerX = plotArea.left + plotArea.width / 2;
|
|
739
|
-
let centerY;
|
|
740
|
-
if (this.halfCircle) {
|
|
741
|
-
const valueSpace = this.showValue
|
|
742
|
-
? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
|
|
743
|
-
: DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
|
|
744
|
-
const maxHorizontalRadius = plotArea.width / 2 - labelAllowance - 8;
|
|
745
|
-
const maxVerticalRadius = plotArea.height - valueSpace - labelAllowance - 8;
|
|
746
|
-
outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
|
|
747
|
-
centerY = plotArea.top + labelAllowance + outerRadius + 4;
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
|
|
751
|
-
outerRadius = Math.max(24, maxRadius - labelAllowance);
|
|
752
|
-
centerY = plotArea.top + plotArea.height / 2;
|
|
753
|
-
}
|
|
754
|
-
const innerRadius = this.thickness !== null
|
|
755
|
-
? Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1))
|
|
756
|
-
: Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
|
|
792
|
+
const { centerX, centerY, innerRadius, outerRadius } = this.resolveGaugeGeometry(plotArea);
|
|
757
793
|
const gaugeGroup = plotGroup
|
|
758
794
|
.append('g')
|
|
759
795
|
.attr('class', 'gauge')
|
|
@@ -761,20 +797,76 @@ export class GaugeChart extends BaseChart {
|
|
|
761
797
|
const animationStartValue = this.resolveAnimationStartValue();
|
|
762
798
|
this.renderTrack(gaugeGroup, innerRadius, outerRadius);
|
|
763
799
|
const visibleSegments = this.getVisibleSegments();
|
|
800
|
+
const progressColor = this.resolveProgressColor(visibleSegments);
|
|
801
|
+
this.renderSegmentsOrProgress(gaugeGroup, visibleSegments, innerRadius, outerRadius, progressColor, animationStartValue);
|
|
802
|
+
this.renderGaugeLabels(gaugeGroup, outerRadius, animationStartValue);
|
|
803
|
+
this.renderGaugeIndicators(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
804
|
+
this.attachTooltipIfEnabled(gaugeGroup, innerRadius, outerRadius, progressColor);
|
|
805
|
+
this.renderInlineLegend(svg);
|
|
806
|
+
}
|
|
807
|
+
resolveGaugeGeometry(plotArea) {
|
|
808
|
+
const labelAllowance = this.resolveLabelAllowance();
|
|
809
|
+
const centerX = plotArea.left + plotArea.width / 2;
|
|
810
|
+
const { centerY, outerRadius } = this.halfCircle
|
|
811
|
+
? this.resolveHalfCircleGeometry(plotArea, labelAllowance)
|
|
812
|
+
: this.resolveFullCircleGeometry(plotArea, labelAllowance);
|
|
813
|
+
return {
|
|
814
|
+
centerX,
|
|
815
|
+
centerY,
|
|
816
|
+
outerRadius,
|
|
817
|
+
innerRadius: this.resolveInnerRadius(outerRadius),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
resolveLabelAllowance() {
|
|
821
|
+
if (this.ticks.show && this.ticks.showLabels) {
|
|
822
|
+
return this.ticks.size + this.ticks.labelOffset + 14;
|
|
823
|
+
}
|
|
824
|
+
if (this.ticks.show && this.ticks.showLines) {
|
|
825
|
+
return this.ticks.size + 10;
|
|
826
|
+
}
|
|
827
|
+
return 8;
|
|
828
|
+
}
|
|
829
|
+
resolveHalfCircleGeometry(plotArea, labelAllowance) {
|
|
830
|
+
const valueSpace = this.showValue
|
|
831
|
+
? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
|
|
832
|
+
: DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
|
|
833
|
+
const maxHorizontalRadius = plotArea.width / 2 - labelAllowance - 8;
|
|
834
|
+
const maxVerticalRadius = plotArea.height - valueSpace - labelAllowance - 8;
|
|
835
|
+
const outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
|
|
836
|
+
return {
|
|
837
|
+
outerRadius,
|
|
838
|
+
centerY: plotArea.top + labelAllowance + outerRadius + 4,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
resolveFullCircleGeometry(plotArea, labelAllowance) {
|
|
842
|
+
const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
|
|
843
|
+
return {
|
|
844
|
+
outerRadius: Math.max(24, maxRadius - labelAllowance),
|
|
845
|
+
centerY: plotArea.top + plotArea.height / 2,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
resolveInnerRadius(outerRadius) {
|
|
849
|
+
if (this.thickness !== null) {
|
|
850
|
+
return Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1));
|
|
851
|
+
}
|
|
852
|
+
return Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
|
|
853
|
+
}
|
|
854
|
+
renderSegmentsOrProgress(gaugeGroup, visibleSegments, innerRadius, outerRadius, progressColor, animationStartValue) {
|
|
764
855
|
if (visibleSegments.length > 0) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
this.renderSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
770
|
-
}
|
|
856
|
+
this.renderVisibleSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
857
|
+
return;
|
|
771
858
|
}
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
859
|
+
const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
|
|
860
|
+
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer, animationStartValue);
|
|
861
|
+
}
|
|
862
|
+
renderVisibleSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius) {
|
|
863
|
+
if (this.segmentStyle === 'gradient') {
|
|
864
|
+
this.renderGradientSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
865
|
+
return;
|
|
777
866
|
}
|
|
867
|
+
this.renderSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
|
|
868
|
+
}
|
|
869
|
+
renderGaugeLabels(gaugeGroup, outerRadius, animationStartValue) {
|
|
778
870
|
if (this.ticks.show &&
|
|
779
871
|
this.ticks.count > 0 &&
|
|
780
872
|
(this.ticks.showLines || this.ticks.showLabels)) {
|
|
@@ -783,19 +875,23 @@ export class GaugeChart extends BaseChart {
|
|
|
783
875
|
if (this.showValue) {
|
|
784
876
|
this.renderValueText(gaugeGroup, outerRadius, animationStartValue);
|
|
785
877
|
}
|
|
878
|
+
}
|
|
879
|
+
renderGaugeIndicators(gaugeGroup, innerRadius, outerRadius, animationStartValue) {
|
|
786
880
|
if (this.targetValue !== null) {
|
|
787
881
|
this.renderTargetMarker(gaugeGroup, innerRadius, outerRadius);
|
|
788
882
|
}
|
|
789
883
|
if (this.needle.show) {
|
|
790
884
|
this.renderNeedle(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
885
|
+
return;
|
|
791
886
|
}
|
|
792
|
-
|
|
887
|
+
if (this.marker.show) {
|
|
793
888
|
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
794
889
|
}
|
|
890
|
+
}
|
|
891
|
+
attachTooltipIfEnabled(gaugeGroup, innerRadius, outerRadius, progressColor) {
|
|
795
892
|
if (this.tooltip) {
|
|
796
893
|
this.attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor);
|
|
797
894
|
}
|
|
798
|
-
this.renderInlineLegend(svg);
|
|
799
895
|
}
|
|
800
896
|
resolveAnimationStartValue() {
|
|
801
897
|
if (this.lastRenderedValue === null) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type LegendVisibilityMap = Record<string, boolean>;
|
|
2
|
+
type LegendStateMutationOptions = {
|
|
3
|
+
silent?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare class LegendStateController {
|
|
6
|
+
private visibilityState;
|
|
7
|
+
private readonly changeCallbacks;
|
|
8
|
+
clone(): LegendStateController;
|
|
9
|
+
hasSeries(dataKey: string): boolean;
|
|
10
|
+
isSeriesVisible(dataKey: string): boolean;
|
|
11
|
+
ensureSeries(dataKeys: Iterable<string>, options?: LegendStateMutationOptions): void;
|
|
12
|
+
setSeriesVisible(dataKey: string, visible: boolean, options?: LegendStateMutationOptions): void;
|
|
13
|
+
toggleSeries(dataKey: string, options?: LegendStateMutationOptions): void;
|
|
14
|
+
setVisibilityMap(visibility: LegendVisibilityMap, options?: LegendStateMutationOptions): void;
|
|
15
|
+
subscribe(callback: () => void): () => void;
|
|
16
|
+
toVisibilityMap(): LegendVisibilityMap;
|
|
17
|
+
private triggerChange;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class LegendStateController {
|
|
2
|
+
constructor() {
|
|
3
|
+
Object.defineProperty(this, "visibilityState", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: new Map()
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "changeCallbacks", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: new Set()
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
clone() {
|
|
17
|
+
const controller = new LegendStateController();
|
|
18
|
+
controller.visibilityState = new Map(this.visibilityState);
|
|
19
|
+
return controller;
|
|
20
|
+
}
|
|
21
|
+
hasSeries(dataKey) {
|
|
22
|
+
return this.visibilityState.has(dataKey);
|
|
23
|
+
}
|
|
24
|
+
isSeriesVisible(dataKey) {
|
|
25
|
+
return this.visibilityState.get(dataKey) ?? true;
|
|
26
|
+
}
|
|
27
|
+
ensureSeries(dataKeys, options) {
|
|
28
|
+
let changed = false;
|
|
29
|
+
for (const dataKey of dataKeys) {
|
|
30
|
+
if (this.visibilityState.has(dataKey)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
this.visibilityState.set(dataKey, true);
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
if (changed && !options?.silent) {
|
|
37
|
+
this.triggerChange();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setSeriesVisible(dataKey, visible, options) {
|
|
41
|
+
const currentValue = this.visibilityState.get(dataKey);
|
|
42
|
+
if (currentValue === visible) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.visibilityState.set(dataKey, visible);
|
|
46
|
+
if (!options?.silent) {
|
|
47
|
+
this.triggerChange();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
toggleSeries(dataKey, options) {
|
|
51
|
+
this.setSeriesVisible(dataKey, !this.isSeriesVisible(dataKey), options);
|
|
52
|
+
}
|
|
53
|
+
setVisibilityMap(visibility, options) {
|
|
54
|
+
let changed = false;
|
|
55
|
+
Object.entries(visibility).forEach(([dataKey, visible]) => {
|
|
56
|
+
const currentValue = this.visibilityState.get(dataKey);
|
|
57
|
+
if (currentValue === visible) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.visibilityState.set(dataKey, visible);
|
|
61
|
+
changed = true;
|
|
62
|
+
});
|
|
63
|
+
if (changed && !options?.silent) {
|
|
64
|
+
this.triggerChange();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
subscribe(callback) {
|
|
68
|
+
this.changeCallbacks.add(callback);
|
|
69
|
+
return () => {
|
|
70
|
+
this.changeCallbacks.delete(callback);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
toVisibilityMap() {
|
|
74
|
+
return Object.fromEntries(this.visibilityState.entries());
|
|
75
|
+
}
|
|
76
|
+
triggerChange() {
|
|
77
|
+
this.changeCallbacks.forEach((callback) => {
|
|
78
|
+
callback();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/legend.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { LegendConfig, ChartTheme, LegendSeries, ExportHooks, LegendConfigBase, LegendMode } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
+
import { LegendStateController } from './legend-state.js';
|
|
4
5
|
export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
5
6
|
readonly type: "legend";
|
|
6
7
|
mode: LegendMode;
|
|
@@ -13,7 +14,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
13
14
|
private readonly itemSpacingX?;
|
|
14
15
|
private readonly itemSpacingY?;
|
|
15
16
|
private readonly gapBetweenBoxAndText;
|
|
16
|
-
private
|
|
17
|
+
private stateController;
|
|
18
|
+
private stateControllerCleanup;
|
|
17
19
|
private onToggleCallback?;
|
|
18
20
|
private onChangeCallbacks;
|
|
19
21
|
private estimatedLayout;
|
|
@@ -22,6 +24,7 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
22
24
|
getExportConfig(): LegendConfigBase;
|
|
23
25
|
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent<LegendConfigBase>;
|
|
24
26
|
setToggleCallback(callback: () => void): void;
|
|
27
|
+
setStateController(controller: LegendStateController): void;
|
|
25
28
|
isInlineMode(): boolean;
|
|
26
29
|
isDisconnectedMode(): boolean;
|
|
27
30
|
isHiddenMode(): boolean;
|
|
@@ -50,5 +53,5 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
50
53
|
private positionRows;
|
|
51
54
|
private getLayoutSignature;
|
|
52
55
|
private getFallbackRowHeight;
|
|
53
|
-
private
|
|
56
|
+
private bindStateController;
|
|
54
57
|
}
|