@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -8
  3. package/{area.d.ts → dist/area.d.ts} +1 -2
  4. package/{area.js → dist/area.js} +2 -19
  5. package/{bar.d.ts → dist/bar.d.ts} +3 -5
  6. package/{bar.js → dist/bar.js} +8 -33
  7. package/{base-chart.d.ts → dist/base-chart.d.ts} +75 -14
  8. package/{base-chart.js → dist/base-chart.js} +429 -122
  9. package/dist/chart-interface.d.ts +19 -0
  10. package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
  11. package/dist/donut-chart.d.ts +51 -0
  12. package/dist/donut-chart.js +374 -0
  13. package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +19 -8
  14. package/{gauge-chart.js → dist/gauge-chart.js} +317 -106
  15. package/{grid.d.ts → dist/grid.d.ts} +1 -1
  16. package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
  17. package/{legend.d.ts → dist/legend.d.ts} +3 -1
  18. package/{legend.js → dist/legend.js} +32 -0
  19. package/{line.d.ts → dist/line.d.ts} +1 -1
  20. package/{line.js → dist/line.js} +3 -25
  21. package/{pie-chart.d.ts → dist/pie-chart.d.ts} +10 -21
  22. package/{pie-chart.js → dist/pie-chart.js} +51 -172
  23. package/dist/radial-chart-base.d.ts +25 -0
  24. package/dist/radial-chart-base.js +79 -0
  25. package/dist/scale-utils.d.ts +3 -0
  26. package/dist/scale-utils.js +14 -0
  27. package/{theme.d.ts → dist/theme.d.ts} +2 -0
  28. package/{theme.js → dist/theme.js} +24 -29
  29. package/{title.d.ts → dist/title.d.ts} +1 -1
  30. package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
  31. package/{tooltip.js → dist/tooltip.js} +239 -74
  32. package/{types.d.ts → dist/types.d.ts} +27 -10
  33. package/{utils.d.ts → dist/utils.d.ts} +7 -2
  34. package/{utils.js → dist/utils.js} +24 -5
  35. package/dist/word-cloud-chart.d.ts +32 -0
  36. package/dist/word-cloud-chart.js +201 -0
  37. package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
  38. package/{x-axis.js → dist/x-axis.js} +18 -14
  39. package/{xy-chart.d.ts → dist/xy-chart.d.ts} +14 -9
  40. package/{xy-chart.js → dist/xy-chart.js} +107 -130
  41. package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
  42. package/{y-axis.js → dist/y-axis.js} +4 -4
  43. package/package.json +39 -35
  44. package/chart-interface.d.ts +0 -13
  45. package/donut-chart.d.ts +0 -38
  46. package/donut-chart.js +0 -316
  47. /package/{chart-interface.js → dist/chart-interface.js} +0 -0
  48. /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
  49. /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
  50. /package/{export-image.js → dist/export-image.js} +0 -0
  51. /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
  52. /package/{export-pdf.js → dist/export-pdf.js} +0 -0
  53. /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
  54. /package/{export-tabular.js → dist/export-tabular.js} +0 -0
  55. /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
  56. /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
  57. /package/{grid.js → dist/grid.js} +0 -0
  58. /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
  59. /package/{grouped-data.js → dist/grouped-data.js} +0 -0
  60. /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
  61. /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
  62. /package/{layout-manager.js → dist/layout-manager.js} +0 -0
  63. /package/{title.js → dist/title.js} +0 -0
  64. /package/{types.js → dist/types.js} +0 -0
  65. /package/{validation.d.ts → dist/validation.d.ts} +0 -0
  66. /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.refreshResolvedValues();
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
- const components = [];
526
- if (this.title) {
527
- components.push(this.title);
528
- }
529
- if (this.tooltip) {
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.data = data;
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
- renderChart() {
594
- if (!this.plotArea || !this.svg || !this.plotGroup) {
595
- throw new Error('Plot area not calculated');
596
- }
597
- this.svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
598
- if (this.title) {
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.theme);
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 = this.plotArea.left + this.plotArea.width / 2;
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 = this.plotArea.width / 2 - labelAllowance - 8;
618
- const maxVerticalRadius = this.plotArea.height - valueSpace - labelAllowance - 8;
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 = this.plotArea.top + labelAllowance + outerRadius + 4;
747
+ centerY = plotArea.top + labelAllowance + outerRadius + 4;
621
748
  }
622
749
  else {
623
- const maxRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
750
+ const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
624
751
  outerRadius = Math.max(24, maxRadius - labelAllowance);
625
- centerY = this.plotArea.top + this.plotArea.height / 2;
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 = this.plotGroup
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.renderLegend();
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
- if (!this.legend) {
693
- return this.segments;
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(this.valueToAngle(this.value))
953
+ .endAngle((datum) => datum.endAngle)
820
954
  .cornerRadius(this.cornerRadius);
821
- gaugeGroup
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
- .attr('d', progressArc(DUMMY_ARC_DATUM));
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
- gaugeGroup
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', needlePoint.x)
903
- .attr('y2', needlePoint.y)
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
- gaugeGroup
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', innerPoint.x)
925
- .attr('y1', innerPoint.y)
926
- .attr('x2', outerPoint.x)
927
- .attr('y2', outerPoint.y)
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
- gaugeGroup
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(this.value));
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 { LayoutAwareComponent } from './chart-interface.js';
2
- import type { ChartTheme } from './types.js';
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: ChartTheme);
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: LayoutAwareComponent[]): PlotAreaBounds;
28
+ calculateLayout(components: LayoutAwareComponentBase[]): PlotAreaBounds;
29
29
  /**
30
30
  * Get the position for a specific component
31
31
  */
32
- getComponentPosition(component: LayoutAwareComponent): ComponentPosition;
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;