@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.
- package/README.md +64 -0
- package/dist/area.d.ts +9 -1
- package/dist/area.js +174 -38
- package/dist/bar.d.ts +9 -1
- package/dist/bar.js +130 -47
- package/dist/base-chart.js +11 -1
- package/dist/donut-chart.js +3 -18
- package/dist/gauge-chart.d.ts +3 -4
- package/dist/gauge-chart.js +7 -53
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/line.d.ts +9 -1
- package/dist/line.js +141 -23
- package/dist/pie-chart.js +5 -29
- package/dist/radial-chart-base.d.ts +4 -3
- package/dist/radial-chart-base.js +27 -12
- package/dist/scatter.d.ts +5 -1
- package/dist/scatter.js +89 -8
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +950 -137
- package/dist/types.d.ts +20 -0
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +11 -1
- package/dist/xy-chart.js +107 -10
- package/dist/xy-motion/config.d.ts +2 -0
- package/dist/xy-motion/config.js +177 -0
- package/dist/xy-motion/driver.d.ts +9 -0
- package/dist/xy-motion/driver.js +10 -0
- package/dist/xy-motion/helpers.d.ts +17 -0
- package/dist/xy-motion/helpers.js +105 -0
- package/dist/xy-motion/live-state.d.ts +8 -0
- package/dist/xy-motion/live-state.js +240 -0
- package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
- package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
- package/dist/xy-motion/types.d.ts +85 -0
- package/dist/xy-motion/types.js +1 -0
- package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
- package/dist/xy-motion/xy-motion-driver.js +130 -0
- package/docs/components.md +36 -0
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +67 -1
- 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';
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -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 =
|
|
61
|
-
this.barStackGap =
|
|
62
|
-
this.barStackReverseSeries =
|
|
63
|
-
this.areaStackMode = config
|
|
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,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 {};
|