@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
package/dist/xy-chart.js CHANGED
@@ -4,7 +4,19 @@ import { ChartValidationError, ChartValidator } from './validation.js';
4
4
  import { GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
5
5
  import { resolveScaleValue } from './scale-utils.js';
6
6
  import { mergeDeep } from './utils.js';
7
+ import { createXYMotionDriver, } from './xy-motion/driver.js';
8
+ import { createXYSeriesSnapshotId } from './xy-motion/helpers.js';
7
9
  const DEFAULT_SERIES_COLOR = '#8884d8';
10
+ function resolveBarStackSettings(config) {
11
+ return {
12
+ mode: config.barStack?.mode ?? 'normal',
13
+ gap: config.barStack?.gap ?? 0.1,
14
+ reverseSeries: config.barStack?.reverseSeries ?? false,
15
+ };
16
+ }
17
+ function resolveAreaStackMode(config) {
18
+ return config.areaStack?.mode ?? 'none';
19
+ }
8
20
  function isXYSeries(component) {
9
21
  return (component.type === 'line' ||
10
22
  component.type === 'scatter' ||
@@ -50,17 +62,25 @@ export class XYChart extends BaseChart {
50
62
  writable: true,
51
63
  value: void 0
52
64
  });
65
+ Object.defineProperty(this, "motionDriver", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: void 0
70
+ });
53
71
  Object.defineProperty(this, "scaleConfigOverride", {
54
72
  enumerable: true,
55
73
  configurable: true,
56
74
  writable: true,
57
75
  value: null
58
76
  });
77
+ const barStack = resolveBarStackSettings(config);
59
78
  this.orientation = config.orientation ?? 'vertical';
60
- this.barStackMode = config.barStack?.mode ?? 'normal';
61
- this.barStackGap = config.barStack?.gap ?? 0.1;
62
- this.barStackReverseSeries = config.barStack?.reverseSeries ?? false;
63
- this.areaStackMode = config.areaStack?.mode ?? 'none';
79
+ this.barStackMode = barStack.mode;
80
+ this.barStackGap = barStack.gap;
81
+ this.barStackReverseSeries = barStack.reverseSeries;
82
+ this.areaStackMode = resolveAreaStackMode(config);
83
+ this.motionDriver = createXYMotionDriver(config.animate);
64
84
  }
65
85
  addChild(component) {
66
86
  if (isXYSeries(component)) {
@@ -101,7 +121,15 @@ export class XYChart extends BaseChart {
101
121
  areaStack: {
102
122
  mode: this.areaStackMode,
103
123
  },
124
+ animate: false,
125
+ });
126
+ }
127
+ update(data) {
128
+ this.motionDriver.prepareForUpdate({
129
+ plotGroup: this.plotGroup,
130
+ visibleSeries: this.getVisibleSeries(),
104
131
  });
132
+ super.update(data);
105
133
  }
106
134
  applyComponentOverrides(overrides) {
107
135
  const restoreBase = super.applyComponentOverrides(overrides);
@@ -114,6 +142,7 @@ export class XYChart extends BaseChart {
114
142
  prepareLayout(context) {
115
143
  super.prepareLayout(context);
116
144
  this.xAxis?.clearEstimatedSpace?.();
145
+ this.yAxis?.clearEstimatedSpace?.();
117
146
  if (this.xAxis) {
118
147
  const xKey = this.getXKey();
119
148
  const labelKey = this.xAxis.labelKey;
@@ -125,24 +154,59 @@ export class XYChart extends BaseChart {
125
154
  });
126
155
  this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode);
127
156
  }
157
+ if (this.yAxis) {
158
+ this.yAxis.estimateLayoutSpace?.(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode);
159
+ }
128
160
  }
129
- renderChart({ svg, plotGroup, plotArea, }) {
130
- this.validateSeriesOrientation();
131
- this.series.forEach((series) => {
132
- const typeName = this.getSeriesTypeName(series);
133
- ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
134
- ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
135
- });
136
- const valueScaleType = this.resolvedScaleConfig.y?.type ?? 'linear';
137
- if (valueScaleType === 'log') {
138
- this.series.forEach((series) => {
139
- const typeName = this.getSeriesTypeName(series);
140
- ChartValidator.validatePositiveData(this.data, series.dataKey, typeName);
161
+ getYAxisEstimateLabels() {
162
+ if (!this.yAxis) {
163
+ return [];
164
+ }
165
+ const yAxisConfig = this.yAxis.getExportConfig();
166
+ const { y: yConfig } = this.getResolvedAxisConfigs();
167
+ const tickFormat = yAxisConfig.tickFormat;
168
+ if (yConfig.type === 'band') {
169
+ const tickValues = this.resolveScaleDomain(yConfig, this.isHorizontalOrientation() ? this.getXKey() : null);
170
+ return tickValues.map((value) => {
171
+ if (typeof tickFormat === 'function') {
172
+ return tickFormat(value);
173
+ }
174
+ return String(value);
141
175
  });
142
176
  }
143
- if (this.xAxis?.dataKey) {
144
- ChartValidator.validateDataKey(this.data, this.xAxis.dataKey, 'XAxis');
177
+ const scale = this.createContinuousScaleForLayoutEstimate(yConfig);
178
+ const tickValues = scale.ticks(5);
179
+ if (typeof tickFormat === 'function') {
180
+ return tickValues.map((value) => tickFormat(value));
181
+ }
182
+ const tickFormatter = scale.tickFormat(5, typeof tickFormat === 'string' ? tickFormat : undefined);
183
+ return tickValues.map((value) => tickFormatter(value));
184
+ }
185
+ createContinuousScaleForLayoutEstimate(config) {
186
+ const domain = this.resolveScaleDomain(config, null);
187
+ switch (config.type) {
188
+ case 'linear': {
189
+ const scale = scaleLinear()
190
+ .domain(domain)
191
+ .rangeRound([100, 0]);
192
+ return config.nice === false ? scale : scale.nice();
193
+ }
194
+ case 'time': {
195
+ const scale = scaleTime()
196
+ .domain(domain)
197
+ .range([100, 0]);
198
+ return config.nice === false ? scale : scale.nice();
199
+ }
200
+ case 'log': {
201
+ const scale = scaleLog()
202
+ .domain(domain)
203
+ .rangeRound([100, 0]);
204
+ return config.nice === false ? scale : scale.nice();
205
+ }
145
206
  }
207
+ }
208
+ renderChart({ svg, plotGroup, plotArea, }) {
209
+ this.validateRenderState();
146
210
  const xKey = this.getXKey();
147
211
  const categoryScaleType = this.getCategoryScaleType();
148
212
  const visibleSeries = this.getVisibleSeries();
@@ -151,21 +215,10 @@ export class XYChart extends BaseChart {
151
215
  if (this.grid && this.x && this.y) {
152
216
  this.grid.render(plotGroup, this.x, this.y, this.renderTheme, this.isHorizontalOrientation());
153
217
  }
154
- this.renderSeries(visibleSeries);
155
- if (this.x && this.y) {
156
- if (this.xAxis) {
157
- this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
158
- }
159
- if (this.yAxis) {
160
- this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
161
- }
162
- }
163
- if (this.tooltip && this.x && this.y) {
164
- const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
165
- const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
166
- this.tooltip.initialize(this.renderTheme);
167
- this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
168
- }
218
+ const motionResult = this.renderSeries(visibleSeries);
219
+ this.setReadyPromise(this.motionDriver.completeRender(motionResult));
220
+ this.renderAxes(svg, plotArea);
221
+ this.attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType);
169
222
  this.renderInlineLegend(svg);
170
223
  }
171
224
  getXKey() {
@@ -487,6 +540,84 @@ export class XYChart extends BaseChart {
487
540
  const scaleType = config.type;
488
541
  const [rangeStart, rangeEnd] = this.getScaleRange(axis, scaleType, config.reverse ?? false);
489
542
  const domain = this.resolveScaleDomain(config, dataKey);
543
+ this.validateExplicitBarValueDomain(config, dataKey, scaleType, domain);
544
+ this.validateScaleDomain(scaleType, domain);
545
+ return this.buildScale(scaleType, domain, [rangeStart, rangeEnd], config);
546
+ }
547
+ resolveScaleDomain(config, dataKey) {
548
+ const buckets = this.getSeriesBuckets();
549
+ const stackedAreaGroups = this.getStackedAreaGroups(buckets.areaSeries);
550
+ if (this.isBarValueScale(config.type, dataKey, buckets.barSeries) &&
551
+ config.type === 'log') {
552
+ throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
553
+ }
554
+ const domain = this.resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups);
555
+ return this.applyNiceDomain(config, domain);
556
+ }
557
+ getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
558
+ const rawValue = dataPoint[series.dataKey];
559
+ if (rawValue === null || rawValue === undefined) {
560
+ return NaN;
561
+ }
562
+ const parsedValue = this.parseValue(rawValue);
563
+ if (!Number.isFinite(parsedValue)) {
564
+ return NaN;
565
+ }
566
+ if (series.type === 'bar') {
567
+ return series.getRenderedValue(parsedValue, this.orientation);
568
+ }
569
+ if (series.type !== 'area') {
570
+ return parsedValue;
571
+ }
572
+ return this.getAreaTooltipValue(dataPoint, xKey, parsedValue, areaStackingContextBySeries.get(series));
573
+ }
574
+ validateRenderState() {
575
+ this.validateSeriesOrientation();
576
+ this.validateSeriesData();
577
+ this.validateValueScaleRequirements();
578
+ this.validateXAxisDataKey();
579
+ }
580
+ validateSeriesData() {
581
+ this.series.forEach((series) => {
582
+ const typeName = this.getSeriesTypeName(series);
583
+ ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
584
+ ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
585
+ });
586
+ }
587
+ validateValueScaleRequirements() {
588
+ if ((this.resolvedScaleConfig.y?.type ?? 'linear') !== 'log') {
589
+ return;
590
+ }
591
+ this.series.forEach((series) => {
592
+ ChartValidator.validatePositiveData(this.data, series.dataKey, this.getSeriesTypeName(series));
593
+ });
594
+ }
595
+ validateXAxisDataKey() {
596
+ if (this.xAxis?.dataKey) {
597
+ ChartValidator.validateDataKey(this.data, this.xAxis.dataKey, 'XAxis');
598
+ }
599
+ }
600
+ renderAxes(svg, plotArea) {
601
+ if (!this.x || !this.y) {
602
+ return;
603
+ }
604
+ if (this.xAxis) {
605
+ this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
606
+ }
607
+ if (this.yAxis) {
608
+ this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
609
+ }
610
+ }
611
+ attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType) {
612
+ if (!this.tooltip || !this.x || !this.y) {
613
+ return;
614
+ }
615
+ const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
616
+ const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
617
+ this.tooltip.initialize(this.renderTheme);
618
+ this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
619
+ }
620
+ validateExplicitBarValueDomain(config, dataKey, scaleType, domain) {
490
621
  if (dataKey === null &&
491
622
  this.series.some((series) => series.type === 'bar') &&
492
623
  (scaleType === 'linear' || scaleType === 'log') &&
@@ -495,133 +626,154 @@ export class XYChart extends BaseChart {
495
626
  config.max !== undefined)) {
496
627
  ChartValidator.validateBarDomainIncludesZero(domain);
497
628
  }
629
+ }
630
+ validateScaleDomain(scaleType, domain) {
498
631
  if (scaleType === 'log') {
499
632
  ChartValidator.validateScaleConfig(scaleType, domain);
500
633
  }
634
+ }
635
+ buildScale(scaleType, domain, range, config) {
501
636
  switch (scaleType) {
502
- case 'band': {
503
- const scale = scaleBand()
637
+ case 'band':
638
+ return scaleBand()
504
639
  .domain(domain)
505
- .rangeRound([rangeStart, rangeEnd]);
506
- if (config.padding !== undefined) {
507
- scale.paddingInner(config.padding);
508
- }
509
- else {
510
- scale.paddingInner(0.1);
511
- }
512
- return scale;
513
- }
640
+ .rangeRound(range)
641
+ .paddingInner(config.padding ?? 0.1);
514
642
  case 'linear': {
515
643
  const scale = scaleLinear()
516
644
  .domain(domain)
517
- .rangeRound([rangeStart, rangeEnd]);
645
+ .rangeRound(range);
518
646
  return config.nice === false ? scale : scale.nice();
519
647
  }
520
648
  case 'time': {
521
649
  const scale = scaleTime()
522
650
  .domain(domain)
523
- .range([rangeStart, rangeEnd]);
651
+ .range(range);
524
652
  return config.nice === false ? scale : scale.nice();
525
653
  }
526
654
  case 'log': {
527
655
  const scale = scaleLog()
528
656
  .domain(domain)
529
- .rangeRound([rangeStart, rangeEnd]);
657
+ .rangeRound(range);
530
658
  return config.nice === false ? scale : scale.nice();
531
659
  }
532
660
  default:
533
661
  throw new Error(`Unsupported scale type: ${scaleType}`);
534
662
  }
535
663
  }
536
- resolveScaleDomain(config, dataKey) {
537
- const barSeries = this.series.filter((s) => s.type === 'bar');
538
- const lineSeries = this.series.filter((s) => s.type === 'line');
539
- const scatterSeries = this.series.filter((s) => s.type === 'scatter');
540
- const areaSeries = this.series.filter((s) => s.type === 'area');
541
- const stackedAreaGroups = this.getStackedAreaGroups(areaSeries);
542
- const isBarValueScale = dataKey === null &&
664
+ getSeriesBuckets() {
665
+ return {
666
+ barSeries: this.series.filter((s) => s.type === 'bar'),
667
+ lineSeries: this.series.filter((s) => s.type === 'line'),
668
+ scatterSeries: this.series.filter((s) => s.type === 'scatter'),
669
+ areaSeries: this.series.filter((s) => s.type === 'area'),
670
+ };
671
+ }
672
+ isBarValueScale(scaleType, dataKey, barSeries) {
673
+ return (dataKey === null &&
543
674
  barSeries.length > 0 &&
544
- (config.type === 'linear' || config.type === 'log');
545
- let domain;
546
- if (isBarValueScale && config.type === 'log') {
547
- throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
548
- }
675
+ (scaleType === 'linear' || scaleType === 'log'));
676
+ }
677
+ resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups) {
549
678
  if (config.domain) {
550
- domain = config.domain;
551
- }
552
- else if (config.type === 'band' && dataKey) {
553
- domain = this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
554
- }
555
- else if (config.type === 'time' && dataKey) {
556
- domain = [
557
- min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
558
- max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
559
- ];
560
- }
561
- else if ((config.type === 'linear' || config.type === 'log') &&
562
- dataKey) {
563
- const values = this.data
564
- .map((d) => this.parseValue(d[dataKey]))
565
- .filter((value) => Number.isFinite(value));
566
- const minVal = config.min ?? min(values) ?? 0;
567
- const maxVal = config.max ?? max(values) ?? 100;
568
- domain = [minVal, maxVal];
679
+ return config.domain;
569
680
  }
570
- else {
571
- const hasPercentBars = this.barStackMode === 'percent' && barSeries.length > 0;
572
- const hasPercentAreas = this.areaStackMode === 'percent' && stackedAreaGroups.size > 0;
573
- if (hasPercentBars || hasPercentAreas) {
574
- let minDomain = 0;
575
- let maxDomain = 100;
576
- if (hasPercentBars) {
577
- [minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
578
- }
579
- if (hasPercentAreas) {
580
- minDomain = Math.min(minDomain, 0);
581
- maxDomain = Math.max(maxDomain, 100);
582
- }
583
- domain = [minDomain, maxDomain];
584
- }
585
- else {
586
- const values = this.collectSeriesValues([
587
- ...lineSeries,
588
- ...scatterSeries,
589
- ...areaSeries,
590
- ]);
591
- const stackedValues = [];
592
- const minCandidates = [...values];
593
- const maxCandidates = [...values];
594
- if (barSeries.length > 0) {
595
- const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), barSeries);
596
- minCandidates.push(barMin);
597
- maxCandidates.push(barMax);
598
- }
599
- const stackedAreaKeys = new Set();
600
- stackedAreaGroups.forEach((stackSeries) => {
601
- stackSeries.forEach((series) => {
602
- stackedAreaKeys.add(series.dataKey);
603
- });
604
- this.data.forEach((dataPoint) => {
605
- const total = stackSeries.reduce((sum, series) => {
606
- const value = this.parseValue(dataPoint[series.dataKey]);
607
- return Number.isFinite(value) ? sum + value : sum;
608
- }, 0);
609
- stackedValues.push(total);
610
- });
611
- minCandidates.push(0);
612
- });
613
- areaSeries
614
- .filter((series) => !stackedAreaKeys.has(series.dataKey))
615
- .forEach((series) => {
616
- minCandidates.push(series.baseline);
617
- });
618
- const minVal = config.min ?? min(minCandidates) ?? 0;
619
- const maxVal = config.max ??
620
- max([...maxCandidates, ...stackedValues]) ??
621
- 100;
622
- domain = [minVal, maxVal];
623
- }
681
+ if (config.type === 'band' && dataKey) {
682
+ return this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
624
683
  }
684
+ if (config.type === 'time' && dataKey) {
685
+ return this.resolveTimeDomain(dataKey);
686
+ }
687
+ if ((config.type === 'linear' || config.type === 'log') && dataKey) {
688
+ return this.resolveNumericDataKeyDomain(config, dataKey);
689
+ }
690
+ return this.resolveSeriesValueDomain(config, buckets, stackedAreaGroups);
691
+ }
692
+ resolveTimeDomain(dataKey) {
693
+ return [
694
+ min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
695
+ max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
696
+ ];
697
+ }
698
+ resolveNumericDataKeyDomain(config, dataKey) {
699
+ const values = this.data
700
+ .map((d) => this.parseValue(d[dataKey]))
701
+ .filter((value) => Number.isFinite(value));
702
+ return [
703
+ config.min ?? min(values) ?? 0,
704
+ config.max ?? max(values) ?? 100,
705
+ ];
706
+ }
707
+ resolveSeriesValueDomain(config, buckets, stackedAreaGroups) {
708
+ if (this.hasPercentValueDomain(buckets.barSeries, stackedAreaGroups)) {
709
+ return this.resolvePercentValueDomain(buckets.barSeries, stackedAreaGroups);
710
+ }
711
+ return this.resolveStandardValueDomain(config, buckets, stackedAreaGroups);
712
+ }
713
+ hasPercentValueDomain(barSeries, stackedAreaGroups) {
714
+ return ((this.barStackMode === 'percent' && barSeries.length > 0) ||
715
+ (this.areaStackMode === 'percent' && stackedAreaGroups.size > 0));
716
+ }
717
+ resolvePercentValueDomain(barSeries, stackedAreaGroups) {
718
+ let minDomain = 0;
719
+ let maxDomain = 100;
720
+ if (this.barStackMode === 'percent' && barSeries.length > 0) {
721
+ [minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
722
+ }
723
+ if (this.areaStackMode === 'percent' && stackedAreaGroups.size > 0) {
724
+ minDomain = Math.min(minDomain, 0);
725
+ maxDomain = Math.max(maxDomain, 100);
726
+ }
727
+ return [minDomain, maxDomain];
728
+ }
729
+ resolveStandardValueDomain(config, buckets, stackedAreaGroups) {
730
+ const values = this.collectSeriesValues([
731
+ ...buckets.lineSeries,
732
+ ...buckets.scatterSeries,
733
+ ...buckets.areaSeries,
734
+ ]);
735
+ const minCandidates = [...values];
736
+ const maxCandidates = [...values];
737
+ const stackedValues = this.collectStackedAreaTotals(stackedAreaGroups);
738
+ if (buckets.barSeries.length > 0) {
739
+ const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), buckets.barSeries);
740
+ minCandidates.push(barMin);
741
+ maxCandidates.push(barMax);
742
+ }
743
+ this.addAreaBaselines(minCandidates, buckets.areaSeries, stackedAreaGroups);
744
+ return [
745
+ config.min ?? min(minCandidates) ?? 0,
746
+ config.max ?? max([...maxCandidates, ...stackedValues]) ?? 100,
747
+ ];
748
+ }
749
+ collectStackedAreaTotals(stackedAreaGroups) {
750
+ const stackedValues = [];
751
+ stackedAreaGroups.forEach((stackSeries) => {
752
+ this.data.forEach((dataPoint) => {
753
+ const total = stackSeries.reduce((sum, series) => {
754
+ const value = this.parseValue(dataPoint[series.dataKey]);
755
+ return Number.isFinite(value) ? sum + value : sum;
756
+ }, 0);
757
+ stackedValues.push(total);
758
+ });
759
+ });
760
+ return stackedValues;
761
+ }
762
+ addAreaBaselines(minCandidates, areaSeries, stackedAreaGroups) {
763
+ const stackedAreaKeys = new Set();
764
+ stackedAreaGroups.forEach((stackSeries) => {
765
+ stackSeries.forEach((series) => {
766
+ stackedAreaKeys.add(series.dataKey);
767
+ });
768
+ minCandidates.push(0);
769
+ });
770
+ areaSeries
771
+ .filter((series) => !stackedAreaKeys.has(series.dataKey))
772
+ .forEach((series) => {
773
+ minCandidates.push(series.baseline);
774
+ });
775
+ }
776
+ applyNiceDomain(config, domain) {
625
777
  if (config.nice === false) {
626
778
  return domain;
627
779
  }
@@ -645,22 +797,7 @@ export class XYChart extends BaseChart {
645
797
  }
646
798
  return domain;
647
799
  }
648
- getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
649
- const rawValue = dataPoint[series.dataKey];
650
- if (rawValue === null || rawValue === undefined) {
651
- return NaN;
652
- }
653
- const parsedValue = this.parseValue(rawValue);
654
- if (!Number.isFinite(parsedValue)) {
655
- return NaN;
656
- }
657
- if (series.type !== 'area') {
658
- if (series.type === 'bar') {
659
- return series.getRenderedValue(parsedValue, this.orientation);
660
- }
661
- return parsedValue;
662
- }
663
- const stackingContext = areaStackingContextBySeries.get(series);
800
+ getAreaTooltipValue(dataPoint, xKey, parsedValue, stackingContext) {
664
801
  if (!stackingContext || stackingContext.mode === 'none') {
665
802
  return parsedValue;
666
803
  }
@@ -677,7 +814,10 @@ export class XYChart extends BaseChart {
677
814
  }
678
815
  renderSeries(visibleSeries) {
679
816
  if (!this.plotGroup || !this.x || !this.y) {
680
- return;
817
+ return {
818
+ snapshotCollection: new Map(),
819
+ transitions: [],
820
+ };
681
821
  }
682
822
  const xKey = this.getXKey();
683
823
  const categoryScaleType = this.getCategoryScaleType();
@@ -692,6 +832,8 @@ export class XYChart extends BaseChart {
692
832
  : null;
693
833
  const { cumulativeDataBySeriesIndex, positiveCumulativeDataBySeriesIndex, negativeCumulativeDataBySeriesIndex, totalData, positiveTotalData, negativeTotalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
694
834
  const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, areaSeries);
835
+ const snapshotCollection = new Map();
836
+ const transitions = [];
695
837
  barSeries.forEach((series, barIndex) => {
696
838
  const nextLayerData = this.barStackMode === 'layer'
697
839
  ? rawValuesBySeriesIndex.get(barIndex + 1)
@@ -711,20 +853,83 @@ export class XYChart extends BaseChart {
711
853
  gap: this.barStackGap,
712
854
  nextLayerData,
713
855
  };
714
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, stackingContext, this.orientation);
856
+ const result = series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, stackingContext, this.orientation, this.motionDriver.getBarAnimationContext(series, this.getBarBaselinePosition()));
857
+ snapshotCollection.set(createXYSeriesSnapshotId(series.type, series.dataKey), result.snapshot);
858
+ transitions.push(...result.transitions);
715
859
  });
716
860
  areaSeries.forEach((series) => {
717
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, areaStackingContextBySeries.get(series), areaValueLabelLayer ?? undefined);
861
+ const result = series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, areaStackingContextBySeries.get(series), areaValueLabelLayer ?? undefined, this.motionDriver.getAreaAnimationContext(series));
862
+ snapshotCollection.set(createXYSeriesSnapshotId(series.type, series.dataKey), result.snapshot);
863
+ transitions.push(...result.transitions);
718
864
  });
719
865
  lineSeries.forEach((series) => {
720
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
866
+ const result = series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, this.motionDriver.getPointAnimationContext(series, this.getPointBaselineY()));
867
+ snapshotCollection.set(createXYSeriesSnapshotId(series.type, series.dataKey), result.snapshot);
868
+ transitions.push(...result.transitions);
721
869
  });
722
870
  scatterSeries.forEach((series) => {
723
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
871
+ const result = series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, this.motionDriver.getPointAnimationContext(series, this.getPointBaselineY()));
872
+ snapshotCollection.set(createXYSeriesSnapshotId(series.type, series.dataKey), result.snapshot);
873
+ transitions.push(...result.transitions);
724
874
  });
725
875
  if (areaValueLabelLayer) {
726
876
  areaValueLabelLayer.raise();
727
877
  }
878
+ return {
879
+ snapshotCollection,
880
+ transitions,
881
+ };
882
+ }
883
+ getBarBaselinePosition() {
884
+ const valueScale = this.isHorizontalOrientation() ? this.x : this.y;
885
+ if (!valueScale) {
886
+ return 0;
887
+ }
888
+ return valueScale(0) ?? 0;
889
+ }
890
+ getPointBaselineY() {
891
+ if (!this.y) {
892
+ return 0;
893
+ }
894
+ const numericDomain = this.getNumericPointBaselineDomain();
895
+ if (!numericDomain) {
896
+ return this.getPointBaselineFallback();
897
+ }
898
+ const baselineValue = this.getPointBaselineValue(numericDomain);
899
+ return (this.y(baselineValue) ?? this.getPointBaselineFallback());
900
+ }
901
+ getNumericPointBaselineDomain() {
902
+ if (!this.y) {
903
+ return null;
904
+ }
905
+ const domain = this.y.domain?.();
906
+ if (!Array.isArray(domain) ||
907
+ domain.length < 2 ||
908
+ typeof domain[0] !== 'number' ||
909
+ typeof domain[1] !== 'number') {
910
+ return null;
911
+ }
912
+ return [domain[0], domain[1]];
913
+ }
914
+ getPointBaselineValue(domain) {
915
+ const minValue = Math.min(...domain);
916
+ const maxValue = Math.max(...domain);
917
+ if (minValue <= 0 && maxValue >= 0) {
918
+ return 0;
919
+ }
920
+ return Math.abs(domain[0]) <= Math.abs(domain[1])
921
+ ? domain[0]
922
+ : domain[1];
923
+ }
924
+ getPointBaselineFallback() {
925
+ if (!this.y) {
926
+ return 0;
927
+ }
928
+ const range = this.y.range?.();
929
+ if (!Array.isArray(range) || range.length === 0) {
930
+ return 0;
931
+ }
932
+ return Math.max(...range);
728
933
  }
729
934
  computeStackingData(data, xKey, barSeries) {
730
935
  const cumulativeDataBySeriesIndex = new Map();
@@ -0,0 +1,2 @@
1
+ import type { NormalizedXYAnimation, XYAnimationConfig } from './types.js';
2
+ export declare function normalizeXYAnimationConfig(config: boolean | XYAnimationConfig | undefined): NormalizedXYAnimation;