@internetstiftelsen/charts 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +65 -1
  2. package/dist/area.d.ts +11 -1
  3. package/dist/area.js +199 -55
  4. package/dist/bar.d.ts +26 -1
  5. package/dist/bar.js +425 -306
  6. package/dist/base-chart.d.ts +5 -0
  7. package/dist/base-chart.js +91 -67
  8. package/dist/chart-group.d.ts +16 -0
  9. package/dist/chart-group.js +201 -143
  10. package/dist/donut-center-content.d.ts +1 -0
  11. package/dist/donut-center-content.js +21 -38
  12. package/dist/donut-chart.js +32 -32
  13. package/dist/gauge-chart.d.ts +23 -4
  14. package/dist/gauge-chart.js +235 -185
  15. package/dist/lazy-mount.d.ts +13 -0
  16. package/dist/lazy-mount.js +90 -0
  17. package/dist/legend.js +10 -9
  18. package/dist/line.d.ts +9 -1
  19. package/dist/line.js +144 -24
  20. package/dist/pie-chart.d.ts +3 -0
  21. package/dist/pie-chart.js +49 -47
  22. package/dist/radial-chart-base.d.ts +4 -3
  23. package/dist/radial-chart-base.js +27 -12
  24. package/dist/scatter.d.ts +5 -1
  25. package/dist/scatter.js +92 -9
  26. package/dist/theme.js +17 -0
  27. package/dist/tooltip.d.ts +55 -3
  28. package/dist/tooltip.js +968 -159
  29. package/dist/types.d.ts +23 -1
  30. package/dist/utils.js +11 -19
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-animation.d.ts +3 -0
  34. package/dist/xy-animation.js +2 -0
  35. package/dist/xy-chart.d.ts +35 -1
  36. package/dist/xy-chart.js +358 -153
  37. package/dist/xy-motion/config.d.ts +2 -0
  38. package/dist/xy-motion/config.js +177 -0
  39. package/dist/xy-motion/driver.d.ts +9 -0
  40. package/dist/xy-motion/driver.js +10 -0
  41. package/dist/xy-motion/helpers.d.ts +17 -0
  42. package/dist/xy-motion/helpers.js +105 -0
  43. package/dist/xy-motion/live-state.d.ts +8 -0
  44. package/dist/xy-motion/live-state.js +240 -0
  45. package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
  46. package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
  47. package/dist/xy-motion/types.d.ts +85 -0
  48. package/dist/xy-motion/types.js +1 -0
  49. package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
  50. package/dist/xy-motion/xy-motion-driver.js +130 -0
  51. package/dist/y-axis.d.ts +7 -2
  52. package/dist/y-axis.js +99 -10
  53. package/docs/components.md +50 -1
  54. package/docs/getting-started.md +35 -0
  55. package/docs/theming.md +14 -0
  56. package/docs/xy-chart.md +88 -7
  57. package/package.json +5 -4
@@ -1,7 +1,10 @@
1
- import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, select, } from 'd3';
2
- import { BaseChart, } from './base-chart.js';
1
+ import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, } from 'd3';
3
2
  import { DEFAULT_COLOR_PALETTE } from './theme.js';
4
3
  import { ChartValidator } from './validation.js';
4
+ import { RadialChartBase } from './radial-chart-base.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;
@@ -46,8 +49,6 @@ const DEFAULT_PROGRESS_RADIUS_INSET = 2;
46
49
  const MIN_PROGRESS_BAND_THICKNESS = 1;
47
50
  const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
48
51
  const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
49
- const TOOLTIP_OFFSET_PX = 12;
50
- const EDGE_MARGIN_PX = 10;
51
52
  const GAUGE_ANIMATION_EASING_PRESETS = {
52
53
  linear: easeLinear,
53
54
  'ease-in': easeCubicIn,
@@ -62,7 +63,7 @@ const DUMMY_ARC_DATUM = {
62
63
  startAngle: 0,
63
64
  endAngle: 0,
64
65
  };
65
- export class GaugeChart extends BaseChart {
66
+ export class GaugeChart extends RadialChartBase {
66
67
  constructor(config) {
67
68
  super(config);
68
69
  Object.defineProperty(this, "configuredValue", {
@@ -251,32 +252,27 @@ export class GaugeChart extends BaseChart {
251
252
  }
252
253
  });
253
254
  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;
255
+ const resolvedGauge = this.resolveGaugeConstructorValues(config, gauge);
256
+ this.configuredValue = resolvedGauge.configuredValue;
257
+ this.configuredTargetValue = resolvedGauge.configuredTargetValue;
258
+ this.configuredSegments = resolvedGauge.configuredSegments;
259
+ this.valueKey = resolvedGauge.valueKey;
260
+ this.targetValueKey = resolvedGauge.targetValueKey;
261
+ this.minValue = resolvedGauge.minValue;
262
+ this.maxValue = resolvedGauge.maxValue;
261
263
  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;
264
+ this.halfCircle = resolvedGauge.halfCircle;
265
+ this.startAngle = resolvedGauge.startAngle;
266
+ this.endAngle = resolvedGauge.endAngle;
267
+ this.innerRadiusRatio = resolvedGauge.innerRadiusRatio;
268
+ this.thickness = resolvedGauge.thickness;
269
+ this.cornerRadius = resolvedGauge.cornerRadius;
270
+ this.trackColor = resolvedGauge.trackColor;
271
+ this.progressColor = resolvedGauge.progressColor;
272
+ this.targetColor = resolvedGauge.targetColor;
273
+ this.segmentStyle = resolvedGauge.segmentStyle;
274
+ this.valueFormatter = resolvedGauge.valueFormatter;
275
+ this.showValue = resolvedGauge.showValue;
280
276
  this.needle = this.normalizeNeedleConfig(gauge.needle);
281
277
  this.marker = this.normalizeMarkerConfig(gauge.marker, !this.needle.show);
282
278
  this.ticks = this.normalizeTickConfig(gauge.ticks);
@@ -286,6 +282,30 @@ export class GaugeChart extends BaseChart {
286
282
  this.segments = this.prepareSegments();
287
283
  this.initializeDataState();
288
284
  }
285
+ resolveGaugeConstructorValues(config, gauge) {
286
+ const halfCircle = resolveDefault(gauge.halfCircle, DEFAULT_HALF_CIRCLE);
287
+ return {
288
+ configuredValue: gauge.value,
289
+ configuredTargetValue: gauge.targetValue,
290
+ configuredSegments: resolveDefault(gauge.segments, []),
291
+ valueKey: resolveDefault(config.valueKey, DEFAULT_VALUE_KEY),
292
+ targetValueKey: config.targetValueKey,
293
+ minValue: resolveDefault(gauge.min, DEFAULT_MIN_VALUE),
294
+ maxValue: resolveDefault(gauge.max, DEFAULT_MAX_VALUE),
295
+ halfCircle,
296
+ startAngle: resolveDefault(gauge.startAngle, halfCircle ? DEFAULT_HALF_START_ANGLE : DEFAULT_START_ANGLE),
297
+ endAngle: resolveDefault(gauge.endAngle, halfCircle ? DEFAULT_HALF_END_ANGLE : DEFAULT_END_ANGLE),
298
+ innerRadiusRatio: resolveDefault(gauge.innerRadius, DEFAULT_INNER_RADIUS_RATIO),
299
+ thickness: resolveDefault(gauge.thickness, null),
300
+ cornerRadius: resolveDefault(gauge.cornerRadius, DEFAULT_CORNER_RADIUS),
301
+ trackColor: resolveDefault(gauge.trackColor, DEFAULT_TRACK_COLOR),
302
+ progressColor: resolveDefault(gauge.progressColor, this.getThemePaletteColor(DEFAULT_THEME_PALETTE_INDEX)),
303
+ targetColor: resolveDefault(gauge.targetColor, DEFAULT_TARGET_COLOR),
304
+ segmentStyle: resolveDefault(gauge.segmentStyle, DEFAULT_SEGMENT_STYLE),
305
+ valueFormatter: resolveDefault(gauge.valueFormatter, this.defaultFormat),
306
+ showValue: resolveDefault(gauge.showValue, DEFAULT_SHOW_VALUE),
307
+ };
308
+ }
289
309
  normalizeNeedleConfig(config) {
290
310
  if (config === false) {
291
311
  return {
@@ -342,14 +362,24 @@ export class GaugeChart extends BaseChart {
342
362
  return palette[index % palette.length];
343
363
  }
344
364
  normalizeTickConfig(config) {
365
+ const resolvedConfig = {
366
+ count: DEFAULT_TICK_COUNT,
367
+ show: DEFAULT_TICKS_SHOW,
368
+ showLines: DEFAULT_TICKS_SHOW_LINES,
369
+ showLabels: DEFAULT_TICKS_SHOW_LABELS,
370
+ size: DEFAULT_TICK_SIZE,
371
+ labelOffset: DEFAULT_TICK_LABEL_OFFSET,
372
+ formatter: this.defaultFormat,
373
+ ...config,
374
+ };
345
375
  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,
376
+ count: resolvedConfig.count,
377
+ show: resolvedConfig.show,
378
+ showLines: resolvedConfig.showLines,
379
+ showLabels: resolvedConfig.showLabels,
380
+ size: resolvedConfig.size,
381
+ labelOffset: resolvedConfig.labelOffset,
382
+ formatter: resolvedConfig.formatter,
353
383
  };
354
384
  }
355
385
  normalizeAnimationConfig(config) {
@@ -405,35 +435,51 @@ export class GaugeChart extends BaseChart {
405
435
  if (tokens.length < 2) {
406
436
  return null;
407
437
  }
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
- }
438
+ const rawStops = tokens.map((token) => this.parseLinearEasingStop(token));
439
+ if (rawStops.some((stop) => stop === null)) {
440
+ return null;
441
+ }
442
+ const stops = rawStops;
443
+ const positions = this.resolveLinearEasingPositions(stops);
444
+ if (!positions) {
445
+ return null;
446
+ }
447
+ const easingPoints = stops.map((stop, index) => {
428
448
  return {
429
- value,
430
- position: percentageValue / 100,
449
+ value: stop.value,
450
+ position: positions[index],
431
451
  };
432
452
  });
433
- if (rawStops.some((stop) => stop === null)) {
453
+ return (progress) => {
454
+ return this.interpolateLinearEasing(progress, easingPoints);
455
+ };
456
+ }
457
+ parseLinearEasingStop(token) {
458
+ const parts = token.split(/\s+/).filter(Boolean);
459
+ if (parts.length === 0 || parts.length > 2) {
434
460
  return null;
435
461
  }
436
- const stops = rawStops;
462
+ const value = Number(parts[0]);
463
+ if (!Number.isFinite(value)) {
464
+ return null;
465
+ }
466
+ if (parts.length === 1) {
467
+ return { value, position: undefined };
468
+ }
469
+ const percentageToken = parts[1];
470
+ if (!percentageToken.endsWith('%')) {
471
+ return null;
472
+ }
473
+ const percentageValue = Number(percentageToken.slice(0, -1));
474
+ if (!Number.isFinite(percentageValue)) {
475
+ return null;
476
+ }
477
+ return {
478
+ value,
479
+ position: percentageValue / 100,
480
+ };
481
+ }
482
+ resolveLinearEasingPositions(stops) {
437
483
  const positions = stops.map((stop) => stop.position);
438
484
  if (positions[0] === undefined) {
439
485
  positions[0] = 0;
@@ -451,47 +497,44 @@ export class GaugeChart extends BaseChart {
451
497
  if (currentPosition < previousPosition) {
452
498
  return null;
453
499
  }
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
- }
500
+ this.fillMissingLinearEasingPositions(positions, previousDefinedIndex, currentIndex);
463
501
  previousDefinedIndex = currentIndex;
464
502
  }
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;
503
+ return positions;
504
+ }
505
+ fillMissingLinearEasingPositions(positions, previousDefinedIndex, currentIndex) {
506
+ const previousPosition = positions[previousDefinedIndex];
507
+ const currentPosition = positions[currentIndex];
508
+ const missingCount = currentIndex - previousDefinedIndex - 1;
509
+ for (let missingIndex = 1; missingIndex <= missingCount; missingIndex += 1) {
510
+ const ratio = missingIndex / (missingCount + 1);
511
+ positions[previousDefinedIndex + missingIndex] =
512
+ previousPosition + (currentPosition - previousPosition) * ratio;
513
+ }
514
+ }
515
+ interpolateLinearEasing(progress, easingPoints) {
516
+ const clamped = Math.max(0, Math.min(1, progress));
517
+ if (clamped <= easingPoints[0].position) {
518
+ return easingPoints[0].value;
519
+ }
520
+ const lastPoint = easingPoints[easingPoints.length - 1];
521
+ if (clamped >= lastPoint.position) {
522
+ return lastPoint.value;
523
+ }
524
+ for (let index = 0; index < easingPoints.length - 1; index += 1) {
525
+ const left = easingPoints[index];
526
+ const right = easingPoints[index + 1];
527
+ if (clamped > right.position) {
528
+ continue;
479
529
  }
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;
530
+ const span = right.position - left.position;
531
+ if (span <= 0) {
532
+ return right.value;
492
533
  }
493
- return lastPoint.value;
494
- };
534
+ const localProgress = (clamped - left.position) / span;
535
+ return left.value + (right.value - left.value) * localProgress;
536
+ }
537
+ return lastPoint.value;
495
538
  }
496
539
  normalizeTickLabelStyle(config) {
497
540
  return {
@@ -512,6 +555,14 @@ export class GaugeChart extends BaseChart {
512
555
  };
513
556
  }
514
557
  validateGaugeConfig() {
558
+ this.validateGaugeRange();
559
+ this.validateGaugeGeometry();
560
+ this.validateTickSettings();
561
+ this.validateIndicatorSettings();
562
+ this.validateAnimationSettings();
563
+ this.validateSegments(this.configuredSegments);
564
+ }
565
+ validateGaugeRange() {
515
566
  if (!Number.isFinite(this.minValue) ||
516
567
  !Number.isFinite(this.maxValue)) {
517
568
  throw new Error(`GaugeChart: gauge.min and gauge.max must be finite numbers, received min='${this.minValue}' and max='${this.maxValue}'`);
@@ -519,6 +570,8 @@ export class GaugeChart extends BaseChart {
519
570
  if (this.minValue >= this.maxValue) {
520
571
  throw new Error(`GaugeChart: gauge.min must be less than gauge.max, received min='${this.minValue}' and max='${this.maxValue}'`);
521
572
  }
573
+ }
574
+ validateGaugeGeometry() {
522
575
  if (!Number.isFinite(this.startAngle) ||
523
576
  !Number.isFinite(this.endAngle)) {
524
577
  throw new Error(`GaugeChart: gauge.startAngle and gauge.endAngle must be finite numbers, received startAngle='${this.startAngle}' and endAngle='${this.endAngle}'`);
@@ -536,6 +589,8 @@ export class GaugeChart extends BaseChart {
536
589
  (!Number.isFinite(this.thickness) || this.thickness <= 0)) {
537
590
  throw new Error(`GaugeChart: gauge.thickness must be > 0 when provided, received '${this.thickness}'`);
538
591
  }
592
+ }
593
+ validateTickSettings() {
539
594
  if (!Number.isInteger(this.ticks.count) || this.ticks.count < 0) {
540
595
  throw new Error(`GaugeChart: gauge.ticks.count must be a non-negative integer, received '${this.ticks.count}'`);
541
596
  }
@@ -545,6 +600,8 @@ export class GaugeChart extends BaseChart {
545
600
  if (this.ticks.labelOffset < 0) {
546
601
  throw new Error(`GaugeChart: gauge.ticks.labelOffset must be >= 0, received '${this.ticks.labelOffset}'`);
547
602
  }
603
+ }
604
+ validateIndicatorSettings() {
548
605
  if (this.needle.width <= 0) {
549
606
  throw new Error(`GaugeChart: gauge.needle.width must be > 0, received '${this.needle.width}'`);
550
607
  }
@@ -557,11 +614,12 @@ export class GaugeChart extends BaseChart {
557
614
  if (this.marker.width <= 0) {
558
615
  throw new Error(`GaugeChart: gauge.marker.width must be > 0, received '${this.marker.width}'`);
559
616
  }
617
+ }
618
+ validateAnimationSettings() {
560
619
  if (!Number.isFinite(this.animation.duration) ||
561
620
  this.animation.duration < 0) {
562
621
  throw new Error(`GaugeChart: gauge.animate.duration must be >= 0, received '${this.animation.duration}'`);
563
622
  }
564
- this.validateSegments(this.configuredSegments);
565
623
  }
566
624
  validateSegments(segments) {
567
625
  if (segments.length === 0) {
@@ -726,34 +784,8 @@ export class GaugeChart extends BaseChart {
726
784
  renderChart({ svg, plotGroup, plotArea, }) {
727
785
  svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
728
786
  this.renderTitle(svg);
729
- if (this.tooltip) {
730
- this.tooltip.initialize(this.renderTheme);
731
- }
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));
787
+ this.initializeTooltip();
788
+ const { centerX, centerY, innerRadius, outerRadius } = this.resolveGaugeGeometry(plotArea);
757
789
  const gaugeGroup = plotGroup
758
790
  .append('g')
759
791
  .attr('class', 'gauge')
@@ -761,20 +793,76 @@ export class GaugeChart extends BaseChart {
761
793
  const animationStartValue = this.resolveAnimationStartValue();
762
794
  this.renderTrack(gaugeGroup, innerRadius, outerRadius);
763
795
  const visibleSegments = this.getVisibleSegments();
796
+ const progressColor = this.resolveProgressColor(visibleSegments);
797
+ this.renderSegmentsOrProgress(gaugeGroup, visibleSegments, innerRadius, outerRadius, progressColor, animationStartValue);
798
+ this.renderGaugeLabels(gaugeGroup, outerRadius, animationStartValue);
799
+ this.renderGaugeIndicators(gaugeGroup, innerRadius, outerRadius, animationStartValue);
800
+ this.attachTooltipIfEnabled(gaugeGroup, innerRadius, outerRadius, progressColor);
801
+ this.renderInlineLegend(svg);
802
+ }
803
+ resolveGaugeGeometry(plotArea) {
804
+ const labelAllowance = this.resolveLabelAllowance();
805
+ const centerX = plotArea.left + plotArea.width / 2;
806
+ const { centerY, outerRadius } = this.halfCircle
807
+ ? this.resolveHalfCircleGeometry(plotArea, labelAllowance)
808
+ : this.resolveFullCircleGeometry(plotArea, labelAllowance);
809
+ return {
810
+ centerX,
811
+ centerY,
812
+ outerRadius,
813
+ innerRadius: this.resolveInnerRadius(outerRadius),
814
+ };
815
+ }
816
+ resolveLabelAllowance() {
817
+ if (this.ticks.show && this.ticks.showLabels) {
818
+ return this.ticks.size + this.ticks.labelOffset + 14;
819
+ }
820
+ if (this.ticks.show && this.ticks.showLines) {
821
+ return this.ticks.size + 10;
822
+ }
823
+ return 8;
824
+ }
825
+ resolveHalfCircleGeometry(plotArea, labelAllowance) {
826
+ const valueSpace = this.showValue
827
+ ? DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITH_LABEL
828
+ : DEFAULT_HALF_CIRCLE_VALUE_SPACE_WITHOUT_LABEL;
829
+ const maxHorizontalRadius = plotArea.width / 2 - labelAllowance - 8;
830
+ const maxVerticalRadius = plotArea.height - valueSpace - labelAllowance - 8;
831
+ const outerRadius = Math.max(24, Math.min(maxHorizontalRadius, maxVerticalRadius));
832
+ return {
833
+ outerRadius,
834
+ centerY: plotArea.top + labelAllowance + outerRadius + 4,
835
+ };
836
+ }
837
+ resolveFullCircleGeometry(plotArea, labelAllowance) {
838
+ const maxRadius = Math.min(plotArea.width, plotArea.height) / 2;
839
+ return {
840
+ outerRadius: Math.max(24, maxRadius - labelAllowance),
841
+ centerY: plotArea.top + plotArea.height / 2,
842
+ };
843
+ }
844
+ resolveInnerRadius(outerRadius) {
845
+ if (this.thickness !== null) {
846
+ return Math.max(0, outerRadius - Math.min(this.thickness, outerRadius - 1));
847
+ }
848
+ return Math.max(8, Math.min(outerRadius - 4, outerRadius * this.innerRadiusRatio));
849
+ }
850
+ renderSegmentsOrProgress(gaugeGroup, visibleSegments, innerRadius, outerRadius, progressColor, animationStartValue) {
764
851
  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
- }
852
+ this.renderVisibleSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
853
+ return;
771
854
  }
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);
855
+ const progressRadii = this.getProgressRadii(innerRadius, outerRadius);
856
+ this.renderProgress(gaugeGroup, progressColor, progressRadii.inner, progressRadii.outer, animationStartValue);
857
+ }
858
+ renderVisibleSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius) {
859
+ if (this.segmentStyle === 'gradient') {
860
+ this.renderGradientSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
861
+ return;
777
862
  }
863
+ this.renderSegments(gaugeGroup, visibleSegments, innerRadius, outerRadius);
864
+ }
865
+ renderGaugeLabels(gaugeGroup, outerRadius, animationStartValue) {
778
866
  if (this.ticks.show &&
779
867
  this.ticks.count > 0 &&
780
868
  (this.ticks.showLines || this.ticks.showLabels)) {
@@ -783,19 +871,23 @@ export class GaugeChart extends BaseChart {
783
871
  if (this.showValue) {
784
872
  this.renderValueText(gaugeGroup, outerRadius, animationStartValue);
785
873
  }
874
+ }
875
+ renderGaugeIndicators(gaugeGroup, innerRadius, outerRadius, animationStartValue) {
786
876
  if (this.targetValue !== null) {
787
877
  this.renderTargetMarker(gaugeGroup, innerRadius, outerRadius);
788
878
  }
789
879
  if (this.needle.show) {
790
880
  this.renderNeedle(gaugeGroup, innerRadius, outerRadius, animationStartValue);
881
+ return;
791
882
  }
792
- else if (this.marker.show) {
883
+ if (this.marker.show) {
793
884
  this.renderCurrentValueMarker(gaugeGroup, innerRadius, outerRadius, animationStartValue);
794
885
  }
886
+ }
887
+ attachTooltipIfEnabled(gaugeGroup, innerRadius, outerRadius, progressColor) {
795
888
  if (this.tooltip) {
796
889
  this.attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor);
797
890
  }
798
- this.renderInlineLegend(svg);
799
891
  }
800
892
  resolveAnimationStartValue() {
801
893
  if (this.lastRenderedValue === null) {
@@ -1180,39 +1272,15 @@ export class GaugeChart extends BaseChart {
1180
1272
  .style('pointer-events', 'all')
1181
1273
  .style('cursor', 'pointer')
1182
1274
  .on('mouseenter', (event) => {
1183
- const tooltipDiv = this.resolveTooltipDiv();
1184
- if (!tooltipDiv) {
1185
- return;
1186
- }
1187
- tooltipDiv
1188
- .style('visibility', 'visible')
1189
- .html(this.buildTooltipContent(progressColor));
1190
- this.positionTooltip(event, tooltipDiv);
1275
+ this.showTooltipFromPointer(event, this.buildTooltipContent(progressColor));
1191
1276
  })
1192
1277
  .on('mousemove', (event) => {
1193
- const tooltipDiv = this.resolveTooltipDiv();
1194
- if (!tooltipDiv) {
1195
- return;
1196
- }
1197
- tooltipDiv
1198
- .style('visibility', 'visible')
1199
- .html(this.buildTooltipContent(progressColor));
1200
- this.positionTooltip(event, tooltipDiv);
1278
+ this.showTooltipFromPointer(event, this.buildTooltipContent(progressColor));
1201
1279
  })
1202
1280
  .on('mouseleave', () => {
1203
- const tooltipDiv = this.resolveTooltipDiv();
1204
- if (!tooltipDiv) {
1205
- return;
1206
- }
1207
- tooltipDiv.style('visibility', 'hidden');
1281
+ this.hideTooltip();
1208
1282
  });
1209
1283
  }
1210
- resolveTooltipDiv() {
1211
- if (!this.tooltip) {
1212
- return null;
1213
- }
1214
- return select(`#${this.tooltip.id}`);
1215
- }
1216
1284
  buildTooltipContent(progressColor) {
1217
1285
  const payload = {
1218
1286
  value: this.value,
@@ -1242,24 +1310,6 @@ export class GaugeChart extends BaseChart {
1242
1310
  }
1243
1311
  return content;
1244
1312
  }
1245
- positionTooltip(event, tooltipDiv) {
1246
- const node = tooltipDiv.node();
1247
- if (!node) {
1248
- return;
1249
- }
1250
- const rect = node.getBoundingClientRect();
1251
- let x = event.pageX + TOOLTIP_OFFSET_PX;
1252
- let y = event.pageY - rect.height / 2;
1253
- if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
1254
- x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
1255
- }
1256
- x = Math.max(EDGE_MARGIN_PX, x);
1257
- y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
1258
- window.scrollY -
1259
- rect.height -
1260
- EDGE_MARGIN_PX));
1261
- tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
1262
- }
1263
1313
  getLegendSeries() {
1264
1314
  return this.segments.map((segment) => {
1265
1315
  return {
@@ -0,0 +1,13 @@
1
+ export type LazyMountableChart = {
2
+ destroy: () => void;
3
+ };
4
+ export type LazyChartFactory<TChart extends LazyMountableChart> = (container: HTMLElement) => TChart | Promise<TChart>;
5
+ export type LazyChartMountOptions = Pick<IntersectionObserverInit, 'root' | 'rootMargin' | 'threshold'> & {
6
+ onError?: (error: unknown) => void;
7
+ };
8
+ export type LazyChartMountHandle<TChart extends LazyMountableChart> = {
9
+ load: () => Promise<TChart | null>;
10
+ destroy: () => void;
11
+ getChart: () => TChart | null;
12
+ };
13
+ export declare function mountChartWhenVisible<TChart extends LazyMountableChart>(target: string | HTMLElement, factory: LazyChartFactory<TChart>, options?: LazyChartMountOptions): LazyChartMountHandle<TChart>;