@internetstiftelsen/charts 0.10.1 → 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 (45) hide show
  1. package/README.md +64 -0
  2. package/dist/area.d.ts +9 -1
  3. package/dist/area.js +174 -38
  4. package/dist/bar.d.ts +9 -1
  5. package/dist/bar.js +130 -47
  6. package/dist/base-chart.js +11 -1
  7. package/dist/donut-chart.js +3 -18
  8. package/dist/gauge-chart.d.ts +3 -4
  9. package/dist/gauge-chart.js +7 -53
  10. package/dist/lazy-mount.d.ts +13 -0
  11. package/dist/lazy-mount.js +90 -0
  12. package/dist/line.d.ts +9 -1
  13. package/dist/line.js +141 -23
  14. package/dist/pie-chart.js +5 -29
  15. package/dist/radial-chart-base.d.ts +4 -3
  16. package/dist/radial-chart-base.js +27 -12
  17. package/dist/scatter.d.ts +5 -1
  18. package/dist/scatter.js +89 -8
  19. package/dist/theme.js +17 -0
  20. package/dist/tooltip.d.ts +55 -3
  21. package/dist/tooltip.js +950 -137
  22. package/dist/types.d.ts +20 -0
  23. package/dist/xy-animation.d.ts +3 -0
  24. package/dist/xy-animation.js +2 -0
  25. package/dist/xy-chart.d.ts +11 -1
  26. package/dist/xy-chart.js +107 -10
  27. package/dist/xy-motion/config.d.ts +2 -0
  28. package/dist/xy-motion/config.js +177 -0
  29. package/dist/xy-motion/driver.d.ts +9 -0
  30. package/dist/xy-motion/driver.js +10 -0
  31. package/dist/xy-motion/helpers.d.ts +17 -0
  32. package/dist/xy-motion/helpers.js +105 -0
  33. package/dist/xy-motion/live-state.d.ts +8 -0
  34. package/dist/xy-motion/live-state.js +240 -0
  35. package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
  36. package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
  37. package/dist/xy-motion/types.d.ts +85 -0
  38. package/dist/xy-motion/types.js +1 -0
  39. package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
  40. package/dist/xy-motion/xy-motion-driver.js +130 -0
  41. package/docs/components.md +36 -0
  42. package/docs/getting-started.md +35 -0
  43. package/docs/theming.md +14 -0
  44. package/docs/xy-chart.md +67 -1
  45. package/package.json +1 -1
package/dist/types.d.ts CHANGED
@@ -69,6 +69,14 @@ export type ChartTheme = {
69
69
  itemSpacingX: number;
70
70
  itemSpacingY: number;
71
71
  };
72
+ tooltip: {
73
+ background: string;
74
+ border: string;
75
+ color: string;
76
+ fontFamily: string;
77
+ fontSize: number;
78
+ fontWeight: string;
79
+ };
72
80
  line: {
73
81
  strokeWidth: number;
74
82
  point: {
@@ -277,7 +285,19 @@ export type GridConfigBase = {
277
285
  export type GridConfig = GridConfigBase & {
278
286
  exportHooks?: ExportHooks<GridConfigBase>;
279
287
  };
288
+ export type TooltipMode = 'shared' | 'split';
289
+ export type TooltipPosition = 'side' | 'vertical';
290
+ export type TooltipBarAnchorPosition = 'top' | 'middle';
291
+ export type TooltipTransitionConfig = {
292
+ show?: boolean;
293
+ duration?: number;
294
+ easing?: string;
295
+ };
280
296
  export type TooltipConfigBase = {
297
+ mode?: TooltipMode;
298
+ position?: TooltipPosition;
299
+ barAnchorPosition?: TooltipBarAnchorPosition;
300
+ transition?: TooltipTransitionConfig;
281
301
  formatter?: SeriesValueFormatter;
282
302
  labelFormatter?: (label: string, data: DataItem) => string;
283
303
  customFormatter?: (data: DataItem, series: {
@@ -0,0 +1,3 @@
1
+ export type { NormalizedXYAnimation, XYAnimationConfig, XYAnimationEasingPreset, XYAreaAnimationContext, XYAreaPointSnapshot, XYBarAnimationContext, XYBarSnapshot, XYMotionRenderResult, XYMotionSeries, XYMotionUpdateContext, XYPointAnimationContext, XYPointSnapshot, XYRenderAnimationMode, XYSeriesDatumSnapshot, XYSeriesRenderResult, XYSeriesSnapshot, XYSeriesSnapshotCollection, } from './xy-motion/types.js';
2
+ export { normalizeXYAnimationConfig } from './xy-motion/config.js';
3
+ export { buildXYDatumSnapshotKeys, cloneXYSeriesSnapshotCollection, createLeftToRightRevealTransition, createTransitionCompletionPromise, createXYAnimationId, createXYDatumKey, createXYSeriesSnapshotId, getEnterStaggerTiming, } from './xy-motion/helpers.js';
@@ -0,0 +1,2 @@
1
+ export { normalizeXYAnimationConfig } from './xy-motion/config.js';
2
+ export { buildXYDatumSnapshotKeys, cloneXYSeriesSnapshotCollection, createLeftToRightRevealTransition, createTransitionCompletionPromise, createXYAnimationId, createXYDatumKey, createXYSeriesSnapshotId, getEnterStaggerTiming, } from './xy-motion/helpers.js';
@@ -1,10 +1,13 @@
1
1
  import { BaseChart, type BaseChartConfig, type BaseLayoutContext, type BaseRenderContext } from './base-chart.js';
2
2
  import type { ChartComponentBase } from './chart-interface.js';
3
- import { type AreaStackConfig, type AxisScaleConfig, type BarStackConfig, type LegendSeries, type Orientation, type ScaleType } from './types.js';
3
+ import { type AreaStackConfig, type AxisScaleConfig, type BarStackConfig, type ChartData, type LegendSeries, type Orientation, type ScaleType } from './types.js';
4
+ import type { XYAnimationConfig } from './xy-animation.js';
5
+ export type { XYAnimationConfig, XYAnimationEasingPreset, } from './xy-animation.js';
4
6
  export type XYChartConfig = BaseChartConfig & {
5
7
  orientation?: Orientation;
6
8
  barStack?: BarStackConfig;
7
9
  areaStack?: AreaStackConfig;
10
+ animate?: boolean | XYAnimationConfig;
8
11
  };
9
12
  export declare class XYChart extends BaseChart {
10
13
  private readonly series;
@@ -13,11 +16,13 @@ export declare class XYChart extends BaseChart {
13
16
  private barStackReverseSeries;
14
17
  private areaStackMode;
15
18
  private readonly orientation;
19
+ private readonly motionDriver;
16
20
  private scaleConfigOverride;
17
21
  constructor(config: XYChartConfig);
18
22
  addChild(component: ChartComponentBase): this;
19
23
  protected getExportComponents(): ChartComponentBase[];
20
24
  protected createExportChart(): BaseChart;
25
+ update(data: ChartData): void;
21
26
  protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
22
27
  protected prepareLayout(context: BaseLayoutContext): void;
23
28
  private getYAxisEstimateLabels;
@@ -76,6 +81,11 @@ export declare class XYChart extends BaseChart {
76
81
  private applyNiceDomain;
77
82
  private getAreaTooltipValue;
78
83
  private renderSeries;
84
+ private getBarBaselinePosition;
85
+ private getPointBaselineY;
86
+ private getNumericPointBaselineDomain;
87
+ private getPointBaselineValue;
88
+ private getPointBaselineFallback;
79
89
  private computeStackingData;
80
90
  private computeAreaStackingContexts;
81
91
  }
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);
@@ -187,7 +215,8 @@ export class XYChart extends BaseChart {
187
215
  if (this.grid && this.x && this.y) {
188
216
  this.grid.render(plotGroup, this.x, this.y, this.renderTheme, this.isHorizontalOrientation());
189
217
  }
190
- this.renderSeries(visibleSeries);
218
+ const motionResult = this.renderSeries(visibleSeries);
219
+ this.setReadyPromise(this.motionDriver.completeRender(motionResult));
191
220
  this.renderAxes(svg, plotArea);
192
221
  this.attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType);
193
222
  this.renderInlineLegend(svg);
@@ -785,7 +814,10 @@ export class XYChart extends BaseChart {
785
814
  }
786
815
  renderSeries(visibleSeries) {
787
816
  if (!this.plotGroup || !this.x || !this.y) {
788
- return;
817
+ return {
818
+ snapshotCollection: new Map(),
819
+ transitions: [],
820
+ };
789
821
  }
790
822
  const xKey = this.getXKey();
791
823
  const categoryScaleType = this.getCategoryScaleType();
@@ -800,6 +832,8 @@ export class XYChart extends BaseChart {
800
832
  : null;
801
833
  const { cumulativeDataBySeriesIndex, positiveCumulativeDataBySeriesIndex, negativeCumulativeDataBySeriesIndex, totalData, positiveTotalData, negativeTotalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
802
834
  const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, areaSeries);
835
+ const snapshotCollection = new Map();
836
+ const transitions = [];
803
837
  barSeries.forEach((series, barIndex) => {
804
838
  const nextLayerData = this.barStackMode === 'layer'
805
839
  ? rawValuesBySeriesIndex.get(barIndex + 1)
@@ -819,20 +853,83 @@ export class XYChart extends BaseChart {
819
853
  gap: this.barStackGap,
820
854
  nextLayerData,
821
855
  };
822
- 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);
823
859
  });
824
860
  areaSeries.forEach((series) => {
825
- 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);
826
864
  });
827
865
  lineSeries.forEach((series) => {
828
- 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);
829
869
  });
830
870
  scatterSeries.forEach((series) => {
831
- 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);
832
874
  });
833
875
  if (areaValueLabelLayer) {
834
876
  areaValueLabelLayer.raise();
835
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);
836
933
  }
837
934
  computeStackingData(data, xKey, barSeries) {
838
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;
@@ -0,0 +1,177 @@
1
+ import { ChartValidationError, ChartValidator } from '../validation.js';
2
+ import { easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, } from 'd3';
3
+ const DEFAULT_ANIMATE = false;
4
+ const DEFAULT_ANIMATION_DURATION_MS = 700;
5
+ const DEFAULT_ANIMATION_EASING_PRESET = 'ease-in-out';
6
+ const XY_ANIMATION_EASING_PRESETS = {
7
+ linear: easeLinear,
8
+ 'ease-in': easeCubicIn,
9
+ 'ease-out': easeCubicOut,
10
+ 'ease-in-out': easeCubicInOut,
11
+ 'bounce-out': easeBounceOut,
12
+ 'elastic-out': easeElasticOut,
13
+ };
14
+ export function normalizeXYAnimationConfig(config) {
15
+ if (config === undefined) {
16
+ return {
17
+ show: DEFAULT_ANIMATE,
18
+ duration: DEFAULT_ANIMATION_DURATION_MS,
19
+ easing: XY_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
20
+ };
21
+ }
22
+ if (typeof config === 'boolean') {
23
+ return {
24
+ show: config,
25
+ duration: DEFAULT_ANIMATION_DURATION_MS,
26
+ easing: XY_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET],
27
+ };
28
+ }
29
+ const normalized = {
30
+ show: config.show ?? true,
31
+ duration: config.duration ?? DEFAULT_ANIMATION_DURATION_MS,
32
+ easing: resolveXYAnimationEasing(config.easing),
33
+ };
34
+ if (!Number.isFinite(normalized.duration) || normalized.duration < 0) {
35
+ throw new ChartValidationError(`XYChart: animate.duration must be >= 0, received '${normalized.duration}'`);
36
+ }
37
+ return normalized;
38
+ }
39
+ function resolveXYAnimationEasing(easing) {
40
+ if (!easing) {
41
+ return XY_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
42
+ }
43
+ if (typeof easing === 'function') {
44
+ return easing;
45
+ }
46
+ if (easing in XY_ANIMATION_EASING_PRESETS) {
47
+ return XY_ANIMATION_EASING_PRESETS[easing];
48
+ }
49
+ if (easing.startsWith('linear(')) {
50
+ const parsedCssLinear = parseCssLinearEasing(easing);
51
+ if (parsedCssLinear) {
52
+ return parsedCssLinear;
53
+ }
54
+ }
55
+ ChartValidator.warn(`XYChart: unsupported animate.easing '${easing}', falling back to '${DEFAULT_ANIMATION_EASING_PRESET}'`);
56
+ return XY_ANIMATION_EASING_PRESETS[DEFAULT_ANIMATION_EASING_PRESET];
57
+ }
58
+ function parseCssLinearEasing(cssLinearEasing) {
59
+ const normalized = cssLinearEasing.trim();
60
+ if (!normalized.startsWith('linear(') || !normalized.endsWith(')')) {
61
+ return null;
62
+ }
63
+ const body = normalized.slice('linear('.length, -1);
64
+ const tokens = body
65
+ .split(',')
66
+ .map((token) => token.trim())
67
+ .filter((token) => token.length > 0);
68
+ if (tokens.length < 2) {
69
+ return null;
70
+ }
71
+ const rawStops = tokens.map((token) => parseLinearEasingStop(token));
72
+ if (rawStops.some((stop) => stop === null)) {
73
+ return null;
74
+ }
75
+ const stops = rawStops;
76
+ const firstPosition = stops[0].position ?? 0;
77
+ const lastPosition = stops[stops.length - 1].position ?? 1;
78
+ if (!hasValidLinearStopPositions(stops, firstPosition, lastPosition)) {
79
+ return null;
80
+ }
81
+ fillMissingLinearStopPositions(stops, firstPosition, lastPosition);
82
+ return (progress) => sampleLinearEasing(stops, progress);
83
+ }
84
+ function hasValidLinearStopPositions(stops, firstPosition, lastPosition) {
85
+ if (lastPosition <= firstPosition) {
86
+ return false;
87
+ }
88
+ let previousPosition = firstPosition;
89
+ for (let index = 1; index < stops.length; index += 1) {
90
+ const stopPosition = stops[index].position;
91
+ if (stopPosition === undefined) {
92
+ continue;
93
+ }
94
+ if (stopPosition < previousPosition) {
95
+ return false;
96
+ }
97
+ previousPosition = stopPosition;
98
+ }
99
+ return true;
100
+ }
101
+ function parseLinearEasingStop(token) {
102
+ const parts = token
103
+ .trim()
104
+ .split(/\s+/)
105
+ .map((part) => part.trim())
106
+ .filter((part) => part.length > 0);
107
+ if (parts.length === 0 || parts.length > 2) {
108
+ return null;
109
+ }
110
+ const value = Number(parts[0]);
111
+ if (!Number.isFinite(value)) {
112
+ return null;
113
+ }
114
+ if (parts.length === 1) {
115
+ return { value, position: undefined };
116
+ }
117
+ const positionText = parts[1];
118
+ if (!positionText.endsWith('%')) {
119
+ return null;
120
+ }
121
+ const percentValue = Number(positionText.slice(0, -1));
122
+ if (!Number.isFinite(percentValue)) {
123
+ return null;
124
+ }
125
+ return {
126
+ value,
127
+ position: percentValue / 100,
128
+ };
129
+ }
130
+ function fillMissingLinearStopPositions(stops, firstPosition, lastPosition) {
131
+ stops[0].position = firstPosition;
132
+ stops[stops.length - 1].position = lastPosition;
133
+ let index = 1;
134
+ while (index < stops.length - 1) {
135
+ if (stops[index].position !== undefined) {
136
+ index += 1;
137
+ continue;
138
+ }
139
+ const startIndex = index - 1;
140
+ let endIndex = index + 1;
141
+ while (endIndex < stops.length &&
142
+ stops[endIndex].position === undefined) {
143
+ endIndex += 1;
144
+ }
145
+ const start = stops[startIndex].position ?? firstPosition;
146
+ const end = stops[endIndex].position ?? lastPosition;
147
+ const gapSize = (end - start) / (endIndex - startIndex);
148
+ for (let fillIndex = index; fillIndex < endIndex; fillIndex += 1) {
149
+ stops[fillIndex].position =
150
+ start + gapSize * (fillIndex - startIndex);
151
+ }
152
+ index = endIndex;
153
+ }
154
+ }
155
+ function sampleLinearEasing(stops, progress) {
156
+ if (progress <= (stops[0].position ?? 0)) {
157
+ return stops[0].value;
158
+ }
159
+ if (progress >= (stops[stops.length - 1].position ?? 1)) {
160
+ return stops[stops.length - 1].value;
161
+ }
162
+ for (let index = 1; index < stops.length; index += 1) {
163
+ const previousStop = stops[index - 1];
164
+ const nextStop = stops[index];
165
+ const start = previousStop.position ?? 0;
166
+ const end = nextStop.position ?? 1;
167
+ if (progress > end) {
168
+ continue;
169
+ }
170
+ if (end === start) {
171
+ return nextStop.value;
172
+ }
173
+ const ratio = (progress - start) / (end - start);
174
+ return (previousStop.value + (nextStop.value - previousStop.value) * ratio);
175
+ }
176
+ return stops[stops.length - 1].value;
177
+ }
@@ -0,0 +1,9 @@
1
+ import type { XYAnimationConfig, XYAreaAnimationContext, XYBarAnimationContext, XYMotionRenderResult, XYMotionSeries, XYMotionUpdateContext, XYPointAnimationContext } from './types.js';
2
+ export interface XYMotionDriverContract {
3
+ prepareForUpdate(context: XYMotionUpdateContext): void;
4
+ getPointAnimationContext(series: XYMotionSeries, baselineY: number): XYPointAnimationContext | undefined;
5
+ getAreaAnimationContext(series: XYMotionSeries): XYAreaAnimationContext | undefined;
6
+ getBarAnimationContext(series: XYMotionSeries, baselineValuePosition: number): XYBarAnimationContext | undefined;
7
+ completeRender(result: XYMotionRenderResult): Promise<void>;
8
+ }
9
+ export declare function createXYMotionDriver(config: boolean | XYAnimationConfig | undefined): XYMotionDriverContract;
@@ -0,0 +1,10 @@
1
+ import { normalizeXYAnimationConfig } from './config.js';
2
+ import { NoopXYMotionDriver } from './noop-xy-motion-driver.js';
3
+ import { XYMotionDriver } from './xy-motion-driver.js';
4
+ export function createXYMotionDriver(config) {
5
+ const animation = normalizeXYAnimationConfig(config);
6
+ if (!animation.show) {
7
+ return new NoopXYMotionDriver();
8
+ }
9
+ return new XYMotionDriver(animation);
10
+ }
@@ -0,0 +1,17 @@
1
+ import type { DataItem } from '../types.js';
2
+ type TransitionLike = {
3
+ end: () => Promise<unknown>;
4
+ };
5
+ type XYEnterStaggerTiming = {
6
+ delay: number;
7
+ duration: number;
8
+ };
9
+ export declare function createXYSeriesSnapshotId(seriesType: string, dataKey: string): string;
10
+ export declare function createXYDatumKey(value: unknown): string;
11
+ export declare function buildXYDatumSnapshotKeys(data: DataItem[], xKey: string): string[];
12
+ export declare function cloneXYSeriesSnapshotCollection<TSnapshot>(collection: Map<string, Map<string, TSnapshot>>): Map<string, Map<string, TSnapshot>>;
13
+ export declare function createTransitionCompletionPromise(transition: TransitionLike): Promise<void>;
14
+ export declare function createXYAnimationId(prefix: string): string;
15
+ export declare function createLeftToRightRevealTransition(target: SVGGraphicsElement | null, duration: number, easing: (progress: number) => number, idPrefix: string, padding?: number, initialProgress?: number): Promise<void>[];
16
+ export declare function getEnterStaggerTiming(index: number, itemCount: number, totalDuration: number, maxDelayShare?: number): XYEnterStaggerTiming;
17
+ export {};
@@ -0,0 +1,105 @@
1
+ import { select } from 'd3';
2
+ const DEFAULT_ENTER_STAGGER_SHARE = 0.55;
3
+ export function createXYSeriesSnapshotId(seriesType, dataKey) {
4
+ return `${seriesType}:${dataKey}`;
5
+ }
6
+ export function createXYDatumKey(value) {
7
+ return createXYDatumKeyWithOccurrence(value, 0);
8
+ }
9
+ function createXYDatumKeyWithOccurrence(value, occurrenceIndex) {
10
+ if (value instanceof Date) {
11
+ const isoValue = value.toISOString();
12
+ return occurrenceIndex === 0
13
+ ? isoValue
14
+ : `${isoValue}::${occurrenceIndex}`;
15
+ }
16
+ const stringValue = String(value);
17
+ return occurrenceIndex === 0
18
+ ? stringValue
19
+ : `${stringValue}::${occurrenceIndex}`;
20
+ }
21
+ export function buildXYDatumSnapshotKeys(data, xKey) {
22
+ const occurrenceCounts = new Map();
23
+ return data.map((entry) => {
24
+ const baseKey = createXYDatumKey(entry[xKey]);
25
+ const occurrenceIndex = occurrenceCounts.get(baseKey) ?? 0;
26
+ occurrenceCounts.set(baseKey, occurrenceIndex + 1);
27
+ return createXYDatumKeyWithOccurrence(entry[xKey], occurrenceIndex);
28
+ });
29
+ }
30
+ export function cloneXYSeriesSnapshotCollection(collection) {
31
+ return new Map(Array.from(collection.entries(), ([seriesKey, snapshot]) => {
32
+ return [seriesKey, new Map(snapshot)];
33
+ }));
34
+ }
35
+ export function createTransitionCompletionPromise(transition) {
36
+ return transition.end().then(() => undefined, () => undefined);
37
+ }
38
+ let xyAnimationIdCounter = 0;
39
+ export function createXYAnimationId(prefix) {
40
+ xyAnimationIdCounter += 1;
41
+ return `${prefix}-${xyAnimationIdCounter}`;
42
+ }
43
+ export function createLeftToRightRevealTransition(target, duration, easing, idPrefix, padding = 0, initialProgress = 0) {
44
+ if (!target) {
45
+ return [];
46
+ }
47
+ const svg = target.ownerSVGElement;
48
+ if (!svg) {
49
+ return [];
50
+ }
51
+ const bbox = target.getBBox();
52
+ const clipWidth = Math.max(1, bbox.width + padding * 2);
53
+ const clipHeight = Math.max(1, bbox.height + padding * 2);
54
+ const clampedInitialProgress = Math.max(0, Math.min(1, initialProgress));
55
+ if (clampedInitialProgress >= 1) {
56
+ return [];
57
+ }
58
+ const clipId = createXYAnimationId(idPrefix);
59
+ const defs = select(svg)
60
+ .selectAll('defs.xy-animation-defs')
61
+ .data([undefined])
62
+ .join('defs')
63
+ .attr('class', 'xy-animation-defs');
64
+ const clipPath = defs
65
+ .append('clipPath')
66
+ .attr('id', clipId)
67
+ .attr('class', 'xy-animation-reveal-clip');
68
+ const clipRect = clipPath
69
+ .append('rect')
70
+ .attr('x', bbox.x - padding)
71
+ .attr('y', bbox.y - padding)
72
+ .attr('width', clipWidth * clampedInitialProgress)
73
+ .attr('height', clipHeight);
74
+ const targetSelection = select(target);
75
+ targetSelection.attr('clip-path', `url(#${clipId})`);
76
+ return [
77
+ clipRect
78
+ .transition()
79
+ .duration(duration)
80
+ .ease(easing)
81
+ .attr('width', clipWidth)
82
+ .end()
83
+ .then(() => {
84
+ targetSelection.attr('clip-path', null);
85
+ clipPath.remove();
86
+ }, () => {
87
+ targetSelection.attr('clip-path', null);
88
+ clipPath.remove();
89
+ }),
90
+ ];
91
+ }
92
+ export function getEnterStaggerTiming(index, itemCount, totalDuration, maxDelayShare = DEFAULT_ENTER_STAGGER_SHARE) {
93
+ const delayRange = totalDuration * maxDelayShare;
94
+ const duration = Math.max(totalDuration - delayRange, 1);
95
+ if (itemCount <= 1) {
96
+ return {
97
+ delay: 0,
98
+ duration,
99
+ };
100
+ }
101
+ return {
102
+ delay: (delayRange * index) / (itemCount - 1),
103
+ duration,
104
+ };
105
+ }
@@ -0,0 +1,8 @@
1
+ import { type Selection } from 'd3';
2
+ import { type XYLivePathStateCollection, type XYMotionSeries, type XYSeriesSnapshotCollection } from './types.js';
3
+ type PlotGroup = Selection<SVGGElement, undefined, null, undefined>;
4
+ export declare function captureLiveAnimationState(plotGroup: PlotGroup | null, visibleSeries: XYMotionSeries[], lastSeriesSnapshot: XYSeriesSnapshotCollection): {
5
+ snapshotCollection: XYSeriesSnapshotCollection;
6
+ pathState: XYLivePathStateCollection;
7
+ };
8
+ export {};