@internetstiftelsen/charts 0.8.0 → 0.9.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/LICENSE +21 -0
- package/README.md +97 -8
- package/{area.d.ts → dist/area.d.ts} +1 -2
- package/{area.js → dist/area.js} +2 -19
- package/{bar.d.ts → dist/bar.d.ts} +3 -5
- package/{bar.js → dist/bar.js} +8 -33
- package/{base-chart.d.ts → dist/base-chart.d.ts} +75 -14
- package/{base-chart.js → dist/base-chart.js} +429 -122
- package/dist/chart-interface.d.ts +19 -0
- package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
- package/dist/donut-chart.d.ts +51 -0
- package/dist/donut-chart.js +374 -0
- package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +19 -8
- package/{gauge-chart.js → dist/gauge-chart.js} +317 -106
- package/{grid.d.ts → dist/grid.d.ts} +1 -1
- package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
- package/{legend.d.ts → dist/legend.d.ts} +3 -1
- package/{legend.js → dist/legend.js} +32 -0
- package/{line.d.ts → dist/line.d.ts} +1 -1
- package/{line.js → dist/line.js} +3 -25
- package/{pie-chart.d.ts → dist/pie-chart.d.ts} +10 -21
- package/{pie-chart.js → dist/pie-chart.js} +51 -172
- package/dist/radial-chart-base.d.ts +25 -0
- package/dist/radial-chart-base.js +79 -0
- package/dist/scale-utils.d.ts +3 -0
- package/dist/scale-utils.js +14 -0
- package/{theme.d.ts → dist/theme.d.ts} +2 -0
- package/{theme.js → dist/theme.js} +24 -29
- package/{title.d.ts → dist/title.d.ts} +1 -1
- package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
- package/{tooltip.js → dist/tooltip.js} +239 -74
- package/{types.d.ts → dist/types.d.ts} +27 -10
- package/{utils.d.ts → dist/utils.d.ts} +7 -2
- package/{utils.js → dist/utils.js} +24 -5
- package/dist/word-cloud-chart.d.ts +32 -0
- package/dist/word-cloud-chart.js +201 -0
- package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
- package/{x-axis.js → dist/x-axis.js} +18 -14
- package/{xy-chart.d.ts → dist/xy-chart.d.ts} +14 -9
- package/{xy-chart.js → dist/xy-chart.js} +107 -130
- package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
- package/{y-axis.js → dist/y-axis.js} +4 -4
- package/package.json +39 -35
- package/chart-interface.d.ts +0 -13
- package/donut-chart.d.ts +0 -38
- package/donut-chart.js +0 -316
- /package/{chart-interface.js → dist/chart-interface.js} +0 -0
- /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
- /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
- /package/{export-image.js → dist/export-image.js} +0 -0
- /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
- /package/{export-pdf.js → dist/export-pdf.js} +0 -0
- /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
- /package/{export-tabular.js → dist/export-tabular.js} +0 -0
- /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
- /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
- /package/{grid.js → dist/grid.js} +0 -0
- /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
- /package/{grouped-data.js → dist/grouped-data.js} +0 -0
- /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
- /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
- /package/{layout-manager.js → dist/layout-manager.js} +0 -0
- /package/{title.js → dist/title.js} +0 -0
- /package/{types.js → dist/types.js} +0 -0
- /package/{validation.d.ts → dist/validation.d.ts} +0 -0
- /package/{validation.js → dist/validation.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { arc, select } from 'd3';
|
|
2
|
-
import { BaseChart } from './base-chart.js';
|
|
1
|
+
import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, select, } from 'd3';
|
|
2
|
+
import { BaseChart, } from './base-chart.js';
|
|
3
3
|
import { DEFAULT_COLOR_PALETTE } from './theme.js';
|
|
4
4
|
import { ChartValidator } from './validation.js';
|
|
5
5
|
const DEFAULT_START_ANGLE = -Math.PI * 0.75;
|
|
@@ -10,6 +10,9 @@ const DEFAULT_VALUE_KEY = 'value';
|
|
|
10
10
|
const DEFAULT_HALF_CIRCLE = false;
|
|
11
11
|
const DEFAULT_MIN_VALUE = 0;
|
|
12
12
|
const DEFAULT_MAX_VALUE = 100;
|
|
13
|
+
const DEFAULT_ANIMATE = false;
|
|
14
|
+
const DEFAULT_ANIMATION_DURATION_MS = 700;
|
|
15
|
+
const DEFAULT_ANIMATION_EASING_PRESET = 'ease-in-out';
|
|
13
16
|
const DEFAULT_INNER_RADIUS_RATIO = 0.68;
|
|
14
17
|
const DEFAULT_CORNER_RADIUS = 4;
|
|
15
18
|
const DEFAULT_TRACK_COLOR = '#e5e7eb';
|
|
@@ -45,6 +48,14 @@ const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
|
|
|
45
48
|
const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
|
|
46
49
|
const TOOLTIP_OFFSET_PX = 12;
|
|
47
50
|
const EDGE_MARGIN_PX = 10;
|
|
51
|
+
const GAUGE_ANIMATION_EASING_PRESETS = {
|
|
52
|
+
linear: easeLinear,
|
|
53
|
+
'ease-in': easeCubicIn,
|
|
54
|
+
'ease-out': easeCubicOut,
|
|
55
|
+
'ease-in-out': easeCubicInOut,
|
|
56
|
+
'bounce-out': easeBounceOut,
|
|
57
|
+
'elastic-out': easeElasticOut,
|
|
58
|
+
};
|
|
48
59
|
const DUMMY_ARC_DATUM = {
|
|
49
60
|
innerRadius: 0,
|
|
50
61
|
outerRadius: 0,
|
|
@@ -96,6 +107,12 @@ export class GaugeChart extends BaseChart {
|
|
|
96
107
|
writable: true,
|
|
97
108
|
value: void 0
|
|
98
109
|
});
|
|
110
|
+
Object.defineProperty(this, "animation", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: void 0
|
|
115
|
+
});
|
|
99
116
|
Object.defineProperty(this, "halfCircle", {
|
|
100
117
|
enumerable: true,
|
|
101
118
|
configurable: true,
|
|
@@ -216,6 +233,12 @@ export class GaugeChart extends BaseChart {
|
|
|
216
233
|
writable: true,
|
|
217
234
|
value: null
|
|
218
235
|
});
|
|
236
|
+
Object.defineProperty(this, "lastRenderedValue", {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true,
|
|
240
|
+
value: null
|
|
241
|
+
});
|
|
219
242
|
Object.defineProperty(this, "defaultFormat", {
|
|
220
243
|
enumerable: true,
|
|
221
244
|
configurable: true,
|
|
@@ -235,6 +258,7 @@ export class GaugeChart extends BaseChart {
|
|
|
235
258
|
this.targetValueKey = config.targetValueKey;
|
|
236
259
|
this.minValue = gauge.min ?? DEFAULT_MIN_VALUE;
|
|
237
260
|
this.maxValue = gauge.max ?? DEFAULT_MAX_VALUE;
|
|
261
|
+
this.animation = this.normalizeAnimationConfig(gauge.animate);
|
|
238
262
|
this.halfCircle = gauge.halfCircle ?? DEFAULT_HALF_CIRCLE;
|
|
239
263
|
this.startAngle =
|
|
240
264
|
gauge.startAngle ??
|
|
@@ -260,7 +284,7 @@ export class GaugeChart extends BaseChart {
|
|
|
260
284
|
this.valueLabelStyle = this.normalizeValueLabelStyle(gauge.valueLabelStyle);
|
|
261
285
|
this.validateGaugeConfig();
|
|
262
286
|
this.segments = this.prepareSegments();
|
|
263
|
-
this.
|
|
287
|
+
this.initializeDataState();
|
|
264
288
|
}
|
|
265
289
|
normalizeNeedleConfig(config) {
|
|
266
290
|
if (config === false) {
|
|
@@ -328,6 +352,147 @@ export class GaugeChart extends BaseChart {
|
|
|
328
352
|
formatter: config?.formatter ?? this.defaultFormat,
|
|
329
353
|
};
|
|
330
354
|
}
|
|
355
|
+
normalizeAnimationConfig(config) {
|
|
356
|
+
if (config === undefined) {
|
|
357
|
+
return {
|
|
358
|
+
show: DEFAULT_ANIMATE,
|
|
359
|
+
duration: DEFAULT_ANIMATION_DURATION_MS,
|
|
360
|
+
easing: GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (typeof config === 'boolean') {
|
|
364
|
+
return {
|
|
365
|
+
show: config,
|
|
366
|
+
duration: DEFAULT_ANIMATION_DURATION_MS,
|
|
367
|
+
easing: GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
show: config.show ?? true,
|
|
372
|
+
duration: config.duration ?? DEFAULT_ANIMATION_DURATION_MS,
|
|
373
|
+
easing: this.resolveAnimationEasing(config.easing),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
resolveAnimationEasing(easing) {
|
|
377
|
+
if (!easing) {
|
|
378
|
+
return GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
|
|
379
|
+
}
|
|
380
|
+
if (typeof easing === 'function') {
|
|
381
|
+
return easing;
|
|
382
|
+
}
|
|
383
|
+
if (easing in GAUGE_ANIMATION_EASING_PRESETS) {
|
|
384
|
+
return GAUGE_ANIMATION_EASING_PRESETS[easing];
|
|
385
|
+
}
|
|
386
|
+
if (easing.startsWith('linear(')) {
|
|
387
|
+
const parsedCssLinear = this.parseCssLinearEasing(easing);
|
|
388
|
+
if (parsedCssLinear) {
|
|
389
|
+
return parsedCssLinear;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
ChartValidator.warn(`GaugeChart: unsupported gauge.animate.easing '${easing}', falling back to '${DEFAULT_ANIMATION_EASING_PRESET}'`);
|
|
393
|
+
return GAUGE_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
|
|
394
|
+
}
|
|
395
|
+
parseCssLinearEasing(cssLinearEasing) {
|
|
396
|
+
const normalized = cssLinearEasing.trim();
|
|
397
|
+
if (!normalized.startsWith('linear(') || !normalized.endsWith(')')) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const body = normalized.slice('linear('.length, -1);
|
|
401
|
+
const tokens = body
|
|
402
|
+
.split(',')
|
|
403
|
+
.map((token) => token.trim())
|
|
404
|
+
.filter((token) => token.length > 0);
|
|
405
|
+
if (tokens.length < 2) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const rawStops = tokens.map((token) => {
|
|
409
|
+
const parts = token.split(/\s+/).filter(Boolean);
|
|
410
|
+
if (parts.length === 0 || parts.length > 2) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const value = Number(parts[0]);
|
|
414
|
+
if (!Number.isFinite(value)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
if (parts.length === 1) {
|
|
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
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
value,
|
|
430
|
+
position: percentageValue / 100,
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
if (rawStops.some((stop) => stop === null)) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const stops = rawStops;
|
|
437
|
+
const positions = stops.map((stop) => stop.position);
|
|
438
|
+
if (positions[0] === undefined) {
|
|
439
|
+
positions[0] = 0;
|
|
440
|
+
}
|
|
441
|
+
if (positions[positions.length - 1] === undefined) {
|
|
442
|
+
positions[positions.length - 1] = 1;
|
|
443
|
+
}
|
|
444
|
+
let previousDefinedIndex = 0;
|
|
445
|
+
for (let currentIndex = 1; currentIndex < positions.length; currentIndex += 1) {
|
|
446
|
+
const currentPosition = positions[currentIndex];
|
|
447
|
+
if (currentPosition === undefined) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const previousPosition = positions[previousDefinedIndex];
|
|
451
|
+
if (currentPosition < previousPosition) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const missingCount = currentIndex - previousDefinedIndex - 1;
|
|
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
|
+
}
|
|
463
|
+
previousDefinedIndex = currentIndex;
|
|
464
|
+
}
|
|
465
|
+
const easingPoints = stops.map((stop, index) => {
|
|
466
|
+
return {
|
|
467
|
+
value: stop.value,
|
|
468
|
+
position: positions[index],
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
return (progress) => {
|
|
472
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
473
|
+
if (clamped <= easingPoints[0].position) {
|
|
474
|
+
return easingPoints[0].value;
|
|
475
|
+
}
|
|
476
|
+
const lastPoint = easingPoints[easingPoints.length - 1];
|
|
477
|
+
if (clamped >= lastPoint.position) {
|
|
478
|
+
return lastPoint.value;
|
|
479
|
+
}
|
|
480
|
+
for (let index = 0; index < easingPoints.length - 1; index += 1) {
|
|
481
|
+
const left = easingPoints[index];
|
|
482
|
+
const right = easingPoints[index + 1];
|
|
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;
|
|
492
|
+
}
|
|
493
|
+
return lastPoint.value;
|
|
494
|
+
};
|
|
495
|
+
}
|
|
331
496
|
normalizeTickLabelStyle(config) {
|
|
332
497
|
return {
|
|
333
498
|
fontSize: config?.fontSize ?? DEFAULT_TICK_LABEL_FONT_SIZE,
|
|
@@ -392,6 +557,10 @@ export class GaugeChart extends BaseChart {
|
|
|
392
557
|
if (this.marker.width <= 0) {
|
|
393
558
|
throw new Error(`GaugeChart: gauge.marker.width must be > 0, received '${this.marker.width}'`);
|
|
394
559
|
}
|
|
560
|
+
if (!Number.isFinite(this.animation.duration) ||
|
|
561
|
+
this.animation.duration < 0) {
|
|
562
|
+
throw new Error(`GaugeChart: gauge.animate.duration must be >= 0, received '${this.animation.duration}'`);
|
|
563
|
+
}
|
|
395
564
|
this.validateSegments(this.configuredSegments);
|
|
396
565
|
}
|
|
397
566
|
validateSegments(segments) {
|
|
@@ -502,62 +671,22 @@ export class GaugeChart extends BaseChart {
|
|
|
502
671
|
};
|
|
503
672
|
});
|
|
504
673
|
}
|
|
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(() => {
|
|
513
|
-
if (!this.container) {
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
this.update(this.data);
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
else if (type === 'title') {
|
|
520
|
-
this.title = component;
|
|
521
|
-
}
|
|
522
|
-
return this;
|
|
523
|
-
}
|
|
524
674
|
getExportComponents() {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
components.push(this.tooltip);
|
|
531
|
-
}
|
|
532
|
-
if (this.legend?.isInlineMode()) {
|
|
533
|
-
components.push(this.legend);
|
|
534
|
-
}
|
|
535
|
-
return components;
|
|
675
|
+
return this.getBaseExportComponents({
|
|
676
|
+
title: true,
|
|
677
|
+
tooltip: true,
|
|
678
|
+
legend: this.legend?.isInlineMode(),
|
|
679
|
+
});
|
|
536
680
|
}
|
|
537
681
|
update(data) {
|
|
538
|
-
this.
|
|
539
|
-
this.refreshResolvedValues();
|
|
682
|
+
this.lastRenderedValue = this.value;
|
|
540
683
|
super.update(data);
|
|
541
684
|
}
|
|
542
|
-
getLayoutComponents() {
|
|
543
|
-
const components = [];
|
|
544
|
-
if (this.title) {
|
|
545
|
-
components.push(this.title);
|
|
546
|
-
}
|
|
547
|
-
if (this.legend) {
|
|
548
|
-
components.push(this.legend);
|
|
549
|
-
}
|
|
550
|
-
return components;
|
|
551
|
-
}
|
|
552
|
-
prepareLayout() {
|
|
553
|
-
const svgNode = this.svg?.node();
|
|
554
|
-
if (svgNode && this.legend?.isInlineMode()) {
|
|
555
|
-
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
685
|
createExportChart() {
|
|
559
686
|
return new GaugeChart({
|
|
560
687
|
data: this.data,
|
|
688
|
+
width: this.configuredWidth,
|
|
689
|
+
height: this.configuredHeight,
|
|
561
690
|
theme: this.theme,
|
|
562
691
|
responsive: this.responsiveConfig,
|
|
563
692
|
valueKey: this.valueKey,
|
|
@@ -567,6 +696,7 @@ export class GaugeChart extends BaseChart {
|
|
|
567
696
|
targetValue: this.configuredTargetValue,
|
|
568
697
|
min: this.minValue,
|
|
569
698
|
max: this.maxValue,
|
|
699
|
+
animate: false,
|
|
570
700
|
halfCircle: this.halfCircle,
|
|
571
701
|
startAngle: this.startAngle,
|
|
572
702
|
endAngle: this.endAngle,
|
|
@@ -590,17 +720,14 @@ export class GaugeChart extends BaseChart {
|
|
|
590
720
|
},
|
|
591
721
|
});
|
|
592
722
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const titlePosition = this.layoutManager.getComponentPosition(this.title);
|
|
600
|
-
this.title.render(this.svg, this.theme, this.width, titlePosition.x, titlePosition.y);
|
|
601
|
-
}
|
|
723
|
+
syncDerivedState() {
|
|
724
|
+
this.refreshResolvedValues();
|
|
725
|
+
}
|
|
726
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
727
|
+
svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
|
|
728
|
+
this.renderTitle(svg);
|
|
602
729
|
if (this.tooltip) {
|
|
603
|
-
this.tooltip.initialize(this.
|
|
730
|
+
this.tooltip.initialize(this.renderTheme);
|
|
604
731
|
}
|
|
605
732
|
const labelAllowance = this.ticks.show && this.ticks.showLabels
|
|
606
733
|
? this.ticks.size + this.ticks.labelOffset + 14
|
|
@@ -608,29 +735,30 @@ export class GaugeChart extends BaseChart {
|
|
|
608
735
|
? this.ticks.size + 10
|
|
609
736
|
: 8;
|
|
610
737
|
let outerRadius;
|
|
611
|
-
const centerX =
|
|
738
|
+
const centerX = plotArea.left + plotArea.width / 2;
|
|
612
739
|
let centerY;
|
|
613
740
|
if (this.halfCircle) {
|
|
614
741
|
const valueSpace = this.showValue
|
|
615
742
|
? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
|
|
616
743
|
: DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
|
|
617
|
-
const maxHorizontalRadius =
|
|
618
|
-
const maxVerticalRadius =
|
|
744
|
+
const maxHorizontalRadius = plotArea.width / 2 - labelAllowance - 8;
|
|
745
|
+
const maxVerticalRadius = plotArea.height - valueSpace - labelAllowance - 8;
|
|
619
746
|
outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
|
|
620
|
-
centerY =
|
|
747
|
+
centerY = plotArea.top + labelAllowance + outerRadius + 4;
|
|
621
748
|
}
|
|
622
749
|
else {
|
|
623
|
-
const maxRadius = Math.min(
|
|
750
|
+
const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
|
|
624
751
|
outerRadius = Math.max(24, maxRadius - labelAllowance);
|
|
625
|
-
centerY =
|
|
752
|
+
centerY = plotArea.top + plotArea.height / 2;
|
|
626
753
|
}
|
|
627
754
|
const innerRadius = this.thickness !== null
|
|
628
755
|
? Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1))
|
|
629
756
|
: Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
|
|
630
|
-
const gaugeGroup =
|
|
757
|
+
const gaugeGroup = plotGroup
|
|
631
758
|
.append('g')
|
|
632
759
|
.attr('class', 'gauge')
|
|
633
760
|
.attr('transform', `translate(${centerX}, ${centerY})`);
|
|
761
|
+
const animationStartValue = this.resolveAnimationStartValue();
|
|
634
762
|
this.renderTrack(gaugeGroup, innerRadius, outerRadius);
|
|
635
763
|
const visibleSegments = this.getVisibleSegments();
|
|
636
764
|
if (visibleSegments.length > 0) {
|
|
@@ -645,7 +773,7 @@ export class GaugeChart extends BaseChart {
|
|
|
645
773
|
const shouldRenderProgress = visibleSegments.length === 0;
|
|
646
774
|
if (shouldRenderProgress) {
|
|
647
775
|
const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
|
|
648
|
-
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer);
|
|
776
|
+
this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer, animationStartValue);
|
|
649
777
|
}
|
|
650
778
|
if (this.ticks.show &&
|
|
651
779
|
this.ticks.count > 0 &&
|
|
@@ -653,21 +781,30 @@ export class GaugeChart extends BaseChart {
|
|
|
653
781
|
this.renderTicks(gaugeGroup, outerRadius);
|
|
654
782
|
}
|
|
655
783
|
if (this.showValue) {
|
|
656
|
-
this.renderValueText(gaugeGroup, outerRadius);
|
|
784
|
+
this.renderValueText(gaugeGroup, outerRadius, animationStartValue);
|
|
657
785
|
}
|
|
658
786
|
if (this.targetValue !== null) {
|
|
659
787
|
this.renderTargetMarker(gaugeGroup, innerRadius, outerRadius);
|
|
660
788
|
}
|
|
661
789
|
if (this.needle.show) {
|
|
662
|
-
this.renderNeedle(gaugeGroup, innerRadius, outerRadius);
|
|
790
|
+
this.renderNeedle(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
663
791
|
}
|
|
664
792
|
else if (this.marker.show) {
|
|
665
|
-
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius);
|
|
793
|
+
this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, animationStartValue);
|
|
666
794
|
}
|
|
667
795
|
if (this.tooltip) {
|
|
668
796
|
this.attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor);
|
|
669
797
|
}
|
|
670
|
-
this.
|
|
798
|
+
this.renderInlineLegend(svg);
|
|
799
|
+
}
|
|
800
|
+
resolveAnimationStartValue() {
|
|
801
|
+
if (this.lastRenderedValue === null) {
|
|
802
|
+
return this.minValue;
|
|
803
|
+
}
|
|
804
|
+
return Math.min(this.maxValue, Math.max(this.minValue, this.lastRenderedValue));
|
|
805
|
+
}
|
|
806
|
+
shouldAnimateTransition(startValue) {
|
|
807
|
+
return this.animation.show && startValue !== this.value;
|
|
671
808
|
}
|
|
672
809
|
buildAriaLabel() {
|
|
673
810
|
const statusLabel = this.findSegmentStatusLabel();
|
|
@@ -689,11 +826,8 @@ export class GaugeChart extends BaseChart {
|
|
|
689
826
|
return null;
|
|
690
827
|
}
|
|
691
828
|
getVisibleSegments() {
|
|
692
|
-
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
return this.segments.filter((segment) => {
|
|
696
|
-
return this.legend.isSeriesVisible(segment.legendLabel);
|
|
829
|
+
return this.filterVisibleItems(this.segments, (segment) => {
|
|
830
|
+
return segment.legendLabel;
|
|
697
831
|
});
|
|
698
832
|
}
|
|
699
833
|
resolveProgressColor(segments) {
|
|
@@ -811,18 +945,44 @@ export class GaugeChart extends BaseChart {
|
|
|
811
945
|
.attr('stroke-opacity', 0.95)
|
|
812
946
|
.style('pointer-events', 'none');
|
|
813
947
|
}
|
|
814
|
-
renderProgress(gaugeGroup, progressColor, innerRadius, outerRadius) {
|
|
948
|
+
renderProgress(gaugeGroup, progressColor, innerRadius, outerRadius, startValue) {
|
|
815
949
|
const progressArc = arc()
|
|
816
950
|
.innerRadius(innerRadius)
|
|
817
951
|
.outerRadius(outerRadius)
|
|
818
952
|
.startAngle(this.startAngle)
|
|
819
|
-
.endAngle(
|
|
953
|
+
.endAngle((datum) => datum.endAngle)
|
|
820
954
|
.cornerRadius(this.cornerRadius);
|
|
821
|
-
|
|
955
|
+
const startAngle = this.valueToAngle(startValue);
|
|
956
|
+
const endAngle = this.valueToAngle(this.value);
|
|
957
|
+
const startPath = progressArc({
|
|
958
|
+
...DUMMY_ARC_DATUM,
|
|
959
|
+
endAngle: startAngle,
|
|
960
|
+
});
|
|
961
|
+
const endPath = progressArc({
|
|
962
|
+
...DUMMY_ARC_DATUM,
|
|
963
|
+
endAngle,
|
|
964
|
+
});
|
|
965
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
966
|
+
const progressPath = gaugeGroup
|
|
822
967
|
.append('path')
|
|
823
968
|
.attr('class', 'gauge-progress')
|
|
824
|
-
.attr('fill', progressColor)
|
|
825
|
-
|
|
969
|
+
.attr('fill', progressColor);
|
|
970
|
+
if (shouldAnimate) {
|
|
971
|
+
const angleInterpolator = interpolateNumber(startAngle, endAngle);
|
|
972
|
+
progressPath
|
|
973
|
+
.attr('d', startPath)
|
|
974
|
+
.transition()
|
|
975
|
+
.duration(this.animation.duration)
|
|
976
|
+
.ease(this.animation.easing)
|
|
977
|
+
.attrTween('d', () => {
|
|
978
|
+
return (progress) => progressArc({
|
|
979
|
+
...DUMMY_ARC_DATUM,
|
|
980
|
+
endAngle: angleInterpolator(progress),
|
|
981
|
+
}) ?? '';
|
|
982
|
+
});
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
progressPath.attr('d', endPath);
|
|
826
986
|
}
|
|
827
987
|
renderTicks(gaugeGroup, outerRadius) {
|
|
828
988
|
const tickGroup = gaugeGroup.append('g').attr('class', 'gauge-ticks');
|
|
@@ -889,21 +1049,43 @@ export class GaugeChart extends BaseChart {
|
|
|
889
1049
|
.attr('stroke-width', DEFAULT_TARGET_MARKER_STROKE_WIDTH)
|
|
890
1050
|
.attr('stroke-linecap', 'round');
|
|
891
1051
|
}
|
|
892
|
-
renderNeedle(gaugeGroup, innerRadius, outerRadius) {
|
|
1052
|
+
renderNeedle(gaugeGroup, innerRadius, outerRadius, startValue) {
|
|
893
1053
|
const needleAngle = this.valueToAngle(this.value);
|
|
1054
|
+
const startNeedleAngle = this.valueToAngle(startValue);
|
|
894
1055
|
const maxLength = Math.max(innerRadius + 2, outerRadius - 2);
|
|
895
1056
|
const length = maxLength * this.needle.lengthRatio;
|
|
896
1057
|
const needlePoint = this.pointAt(needleAngle, length);
|
|
897
|
-
|
|
1058
|
+
const startNeedlePoint = this.pointAt(startNeedleAngle, length);
|
|
1059
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1060
|
+
const initialNeedlePoint = shouldAnimate
|
|
1061
|
+
? startNeedlePoint
|
|
1062
|
+
: needlePoint;
|
|
1063
|
+
const needleLine = gaugeGroup
|
|
898
1064
|
.append('line')
|
|
899
1065
|
.attr('class', 'gauge-needle')
|
|
900
1066
|
.attr('x1', 0)
|
|
901
1067
|
.attr('y1', 0)
|
|
902
|
-
.attr('x2',
|
|
903
|
-
.attr('y2',
|
|
1068
|
+
.attr('x2', initialNeedlePoint.x)
|
|
1069
|
+
.attr('y2', initialNeedlePoint.y)
|
|
904
1070
|
.attr('stroke', this.needle.color)
|
|
905
1071
|
.attr('stroke-width', this.needle.width)
|
|
906
1072
|
.attr('stroke-linecap', 'round');
|
|
1073
|
+
if (shouldAnimate) {
|
|
1074
|
+
const angleInterpolator = interpolateNumber(startNeedleAngle, needleAngle);
|
|
1075
|
+
needleLine
|
|
1076
|
+
.transition()
|
|
1077
|
+
.duration(this.animation.duration)
|
|
1078
|
+
.ease(this.animation.easing)
|
|
1079
|
+
.tween('needle-rotation', () => {
|
|
1080
|
+
return (progress) => {
|
|
1081
|
+
const interpolatedAngle = angleInterpolator(progress);
|
|
1082
|
+
const interpolatedPoint = this.pointAt(interpolatedAngle, length);
|
|
1083
|
+
needleLine
|
|
1084
|
+
.attr('x2', interpolatedPoint.x)
|
|
1085
|
+
.attr('y2', interpolatedPoint.y);
|
|
1086
|
+
};
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
907
1089
|
gaugeGroup
|
|
908
1090
|
.append('circle')
|
|
909
1091
|
.attr('class', 'gauge-needle-cap')
|
|
@@ -912,28 +1094,55 @@ export class GaugeChart extends BaseChart {
|
|
|
912
1094
|
.attr('r', this.needle.capRadius)
|
|
913
1095
|
.attr('fill', this.needle.color);
|
|
914
1096
|
}
|
|
915
|
-
renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius) {
|
|
1097
|
+
renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, startValue) {
|
|
916
1098
|
const markerAngle = this.valueToAngle(this.value);
|
|
1099
|
+
const startMarkerAngle = this.valueToAngle(startValue);
|
|
917
1100
|
const markerInner = innerRadius + 1;
|
|
918
1101
|
const markerOuter = Math.max(markerInner + 1, outerRadius - 1);
|
|
919
1102
|
const innerPoint = this.pointAt(markerAngle, markerInner);
|
|
920
1103
|
const outerPoint = this.pointAt(markerAngle, markerOuter);
|
|
921
|
-
|
|
1104
|
+
const startInnerPoint = this.pointAt(startMarkerAngle, markerInner);
|
|
1105
|
+
const startOuterPoint = this.pointAt(startMarkerAngle, markerOuter);
|
|
1106
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1107
|
+
const initialInnerPoint = shouldAnimate ? startInnerPoint : innerPoint;
|
|
1108
|
+
const initialOuterPoint = shouldAnimate ? startOuterPoint : outerPoint;
|
|
1109
|
+
const markerLine = gaugeGroup
|
|
922
1110
|
.append('line')
|
|
923
1111
|
.attr('class', 'gauge-marker')
|
|
924
|
-
.attr('x1',
|
|
925
|
-
.attr('y1',
|
|
926
|
-
.attr('x2',
|
|
927
|
-
.attr('y2',
|
|
1112
|
+
.attr('x1', initialInnerPoint.x)
|
|
1113
|
+
.attr('y1', initialInnerPoint.y)
|
|
1114
|
+
.attr('x2', initialOuterPoint.x)
|
|
1115
|
+
.attr('y2', initialOuterPoint.y)
|
|
928
1116
|
.attr('stroke', this.marker.color)
|
|
929
1117
|
.attr('stroke-width', this.marker.width)
|
|
930
1118
|
.attr('stroke-linecap', 'round');
|
|
1119
|
+
if (shouldAnimate) {
|
|
1120
|
+
const angleInterpolator = interpolateNumber(startMarkerAngle, markerAngle);
|
|
1121
|
+
markerLine
|
|
1122
|
+
.transition()
|
|
1123
|
+
.duration(this.animation.duration)
|
|
1124
|
+
.ease(this.animation.easing)
|
|
1125
|
+
.tween('marker-sweep', () => {
|
|
1126
|
+
return (progress) => {
|
|
1127
|
+
const interpolatedAngle = angleInterpolator(progress);
|
|
1128
|
+
const interpolatedInner = this.pointAt(interpolatedAngle, markerInner);
|
|
1129
|
+
const interpolatedOuter = this.pointAt(interpolatedAngle, markerOuter);
|
|
1130
|
+
markerLine
|
|
1131
|
+
.attr('x1', interpolatedInner.x)
|
|
1132
|
+
.attr('y1', interpolatedInner.y)
|
|
1133
|
+
.attr('x2', interpolatedOuter.x)
|
|
1134
|
+
.attr('y2', interpolatedOuter.y);
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
931
1138
|
}
|
|
932
|
-
renderValueText(gaugeGroup, outerRadius) {
|
|
1139
|
+
renderValueText(gaugeGroup, outerRadius, startValue) {
|
|
933
1140
|
const mainValueY = this.halfCircle
|
|
934
1141
|
? DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y
|
|
935
1142
|
: Math.max(DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y, outerRadius * 0.58);
|
|
936
|
-
|
|
1143
|
+
const shouldAnimate = this.shouldAnimateTransition(startValue);
|
|
1144
|
+
const initialValue = shouldAnimate ? startValue : this.value;
|
|
1145
|
+
const valueText = gaugeGroup
|
|
937
1146
|
.append('text')
|
|
938
1147
|
.attr('class', 'gauge-value')
|
|
939
1148
|
.attr('x', 0)
|
|
@@ -943,7 +1152,19 @@ export class GaugeChart extends BaseChart {
|
|
|
943
1152
|
.attr('font-weight', this.valueLabelStyle.fontWeight)
|
|
944
1153
|
.attr('font-family', this.valueLabelStyle.fontFamily)
|
|
945
1154
|
.attr('fill', this.valueLabelStyle.color)
|
|
946
|
-
.text(this.valueFormatter(
|
|
1155
|
+
.text(this.valueFormatter(initialValue));
|
|
1156
|
+
if (shouldAnimate) {
|
|
1157
|
+
valueText
|
|
1158
|
+
.transition()
|
|
1159
|
+
.duration(this.animation.duration)
|
|
1160
|
+
.ease(this.animation.easing)
|
|
1161
|
+
.tween('text', () => {
|
|
1162
|
+
return (progress) => {
|
|
1163
|
+
const currentValue = startValue + (this.value - startValue) * progress;
|
|
1164
|
+
valueText.text(this.valueFormatter(currentValue));
|
|
1165
|
+
};
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
947
1168
|
}
|
|
948
1169
|
attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor) {
|
|
949
1170
|
const interactionArc = arc()
|
|
@@ -1039,16 +1260,6 @@ export class GaugeChart extends BaseChart {
|
|
|
1039
1260
|
EDGE_MARGIN_PX));
|
|
1040
1261
|
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
1041
1262
|
}
|
|
1042
|
-
renderLegend() {
|
|
1043
|
-
if (!this.legend ||
|
|
1044
|
-
!this.legend.isInlineMode() ||
|
|
1045
|
-
!this.svg ||
|
|
1046
|
-
this.segments.length === 0) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const legendPosition = this.layoutManager.getComponentPosition(this.legend);
|
|
1050
|
-
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPosition.x, legendPosition.y);
|
|
1051
|
-
}
|
|
1052
1263
|
getLegendSeries() {
|
|
1053
1264
|
return this.segments.map((segment) => {
|
|
1054
1265
|
return {
|
|
@@ -8,6 +8,6 @@ export declare class Grid implements ChartComponent<GridConfigBase> {
|
|
|
8
8
|
readonly exportHooks?: ExportHooks<GridConfigBase>;
|
|
9
9
|
constructor(config?: GridConfig);
|
|
10
10
|
getExportConfig(): GridConfigBase;
|
|
11
|
-
createExportComponent(override?: Partial<GridConfigBase>): ChartComponent
|
|
11
|
+
createExportComponent(override?: Partial<GridConfigBase>): ChartComponent<GridConfigBase>;
|
|
12
12
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme): void;
|
|
13
13
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { LayoutAwareComponentBase } from './chart-interface.js';
|
|
2
|
+
import type { ResolvedChartTheme } from './types.js';
|
|
3
3
|
export type PlotAreaBounds = {
|
|
4
4
|
left: number;
|
|
5
5
|
right: number;
|
|
@@ -20,16 +20,16 @@ export declare class LayoutManager {
|
|
|
20
20
|
private theme;
|
|
21
21
|
private plotBounds;
|
|
22
22
|
private componentPositions;
|
|
23
|
-
constructor(theme:
|
|
23
|
+
constructor(theme: ResolvedChartTheme);
|
|
24
24
|
/**
|
|
25
25
|
* Calculate layout based on registered components
|
|
26
26
|
* Returns the plot area bounds
|
|
27
27
|
*/
|
|
28
|
-
calculateLayout(components:
|
|
28
|
+
calculateLayout(components: LayoutAwareComponentBase[]): PlotAreaBounds;
|
|
29
29
|
/**
|
|
30
30
|
* Get the position for a specific component
|
|
31
31
|
*/
|
|
32
|
-
getComponentPosition(component:
|
|
32
|
+
getComponentPosition(component: LayoutAwareComponentBase): ComponentPosition;
|
|
33
33
|
/**
|
|
34
34
|
* Calculate positions for all components based on their space requirements
|
|
35
35
|
* Components are positioned in registration order, stacking outward from the plot area
|
|
@@ -20,7 +20,7 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
20
20
|
private estimatedLayoutSignature;
|
|
21
21
|
constructor(config?: LegendConfig);
|
|
22
22
|
getExportConfig(): LegendConfigBase;
|
|
23
|
-
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent
|
|
23
|
+
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent<LegendConfigBase>;
|
|
24
24
|
setToggleCallback(callback: () => void): void;
|
|
25
25
|
isInlineMode(): boolean;
|
|
26
26
|
isDisconnectedMode(): boolean;
|
|
@@ -40,6 +40,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
40
40
|
getRequiredSpace(): ComponentSpace;
|
|
41
41
|
getMeasuredHeight(): number;
|
|
42
42
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
|
|
43
|
+
private isLegendItemVisible;
|
|
44
|
+
private isToggleActivationKey;
|
|
43
45
|
private computeLayout;
|
|
44
46
|
private resolveLayoutSettings;
|
|
45
47
|
private buildLegendItems;
|