@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.
Files changed (46) hide show
  1. package/README.md +137 -3
  2. package/dist/area.d.ts +2 -0
  3. package/dist/area.js +39 -31
  4. package/dist/bar.d.ts +20 -1
  5. package/dist/bar.js +395 -519
  6. package/dist/base-chart.d.ts +21 -1
  7. package/dist/base-chart.js +166 -93
  8. package/dist/chart-group.d.ts +137 -0
  9. package/dist/chart-group.js +1155 -0
  10. package/dist/chart-interface.d.ts +1 -1
  11. package/dist/donut-center-content.d.ts +1 -0
  12. package/dist/donut-center-content.js +21 -38
  13. package/dist/donut-chart.js +30 -15
  14. package/dist/gauge-chart.d.ts +20 -0
  15. package/dist/gauge-chart.js +229 -133
  16. package/dist/legend-state.d.ts +19 -0
  17. package/dist/legend-state.js +81 -0
  18. package/dist/legend.d.ts +5 -2
  19. package/dist/legend.js +45 -38
  20. package/dist/line.js +3 -1
  21. package/dist/pie-chart.d.ts +3 -0
  22. package/dist/pie-chart.js +45 -19
  23. package/dist/scatter.d.ts +16 -0
  24. package/dist/scatter.js +165 -0
  25. package/dist/tooltip.d.ts +2 -1
  26. package/dist/tooltip.js +21 -25
  27. package/dist/types.d.ts +19 -1
  28. package/dist/utils.js +11 -19
  29. package/dist/validation.d.ts +4 -0
  30. package/dist/validation.js +19 -0
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-chart.d.ts +40 -1
  34. package/dist/xy-chart.js +488 -165
  35. package/dist/y-axis.d.ts +7 -2
  36. package/dist/y-axis.js +99 -10
  37. package/docs/chart-group.md +213 -0
  38. package/docs/components.md +321 -0
  39. package/docs/donut-chart.md +193 -0
  40. package/docs/gauge-chart.md +175 -0
  41. package/docs/getting-started.md +311 -0
  42. package/docs/pie-chart.md +123 -0
  43. package/docs/theming.md +162 -0
  44. package/docs/word-cloud-chart.md +98 -0
  45. package/docs/xy-chart.md +517 -0
  46. package/package.json +6 -4
@@ -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.configuredValue = gauge.value;
255
- this.configuredTargetValue = gauge.targetValue;
256
- this.configuredSegments = gauge.segments ?? [];
257
- this.valueKey = config.valueKey ?? DEFAULT_VALUE_KEY;
258
- this.targetValueKey = config.targetValueKey;
259
- this.minValue = gauge.min ?? DEFAULT_MIN_VALUE;
260
- this.maxValue = gauge.max ?? DEFAULT_MAX_VALUE;
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 = gauge.halfCircle ?? DEFAULT_HALF_CIRCLE;
263
- this.startAngle =
264
- gauge.startAngle ??
265
- (this.halfCircle ? DEFAULT_HALF_START_ANGLE : DEFAULT_START_ANGLE);
266
- this.endAngle =
267
- gauge.endAngle ??
268
- (this.halfCircle ? DEFAULT_HALF_END_ANGLE : DEFAULT_END_ANGLE);
269
- this.innerRadiusRatio = gauge.innerRadius ?? DEFAULT_INNER_RADIUS_RATIO;
270
- this.thickness = gauge.thickness ?? null;
271
- this.cornerRadius = gauge.cornerRadius ?? DEFAULT_CORNER_RADIUS;
272
- this.trackColor = gauge.trackColor ?? DEFAULT_TRACK_COLOR;
273
- this.progressColor =
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: config?.count ?? DEFAULT_TICK_COUNT,
347
- show: config?.show ?? DEFAULT_TICKS_SHOW,
348
- showLines: config?.showLines ?? DEFAULT_TICKS_SHOW_LINES,
349
- showLabels: config?.showLabels ?? DEFAULT_TICKS_SHOW_LABELS,
350
- size: config?.size ?? DEFAULT_TICK_SIZE,
351
- labelOffset: config?.labelOffset ?? DEFAULT_TICK_LABEL_OFFSET,
352
- formatter: config?.formatter ?? this.defaultFormat,
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
- 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
- }
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: percentageValue / 100,
451
+ value: stop.value,
452
+ position: positions[index],
431
453
  };
432
454
  });
433
- if (rawStops.some((stop) => stop === null)) {
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 stops = rawStops;
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
- 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
- }
502
+ this.fillMissingLinearEasingPositions(positions, previousDefinedIndex, currentIndex);
463
503
  previousDefinedIndex = currentIndex;
464
504
  }
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;
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
- 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;
532
+ const span = right.position - left.position;
533
+ if (span <= 0) {
534
+ return right.value;
492
535
  }
493
- return lastPoint.value;
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.legend?.isInlineMode(),
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 labelAllowance = this.ticks.show && this.ticks.showLabels
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
- if (this.segmentStyle === 'gradient') {
766
- this.renderGradientSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
767
- }
768
- else {
769
- this.renderSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
770
- }
856
+ this.renderVisibleSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
857
+ return;
771
858
  }
772
- const progressColor = this.resolveProgressColor(visibleSegments);
773
- const shouldRenderProgress = visibleSegments.length === 0;
774
- if (shouldRenderProgress) {
775
- const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
776
- this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer, animationStartValue);
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
- else if (this.marker.show) {
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 visibilityState;
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 triggerChange;
56
+ private bindStateController;
54
57
  }