@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.
- package/README.md +65 -1
- package/dist/area.d.ts +11 -1
- package/dist/area.js +199 -55
- package/dist/bar.d.ts +26 -1
- package/dist/bar.js +425 -306
- package/dist/base-chart.d.ts +5 -0
- package/dist/base-chart.js +91 -67
- package/dist/chart-group.d.ts +16 -0
- package/dist/chart-group.js +201 -143
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +32 -32
- package/dist/gauge-chart.d.ts +23 -4
- package/dist/gauge-chart.js +235 -185
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/legend.js +10 -9
- package/dist/line.d.ts +9 -1
- package/dist/line.js +144 -24
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +49 -47
- 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 +92 -9
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +968 -159
- package/dist/types.d.ts +23 -1
- package/dist/utils.js +11 -19
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +35 -1
- package/dist/xy-chart.js +358 -153
- 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/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/components.md +50 -1
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +88 -7
- package/package.json +5 -4
|
@@ -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 {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
2
|
+
import { sanitizeForCSS } from '../utils.js';
|
|
3
|
+
import { cloneXYSeriesSnapshotCollection, createXYSeriesSnapshotId, } from './helpers.js';
|
|
4
|
+
export function captureLiveAnimationState(plotGroup, visibleSeries, lastSeriesSnapshot) {
|
|
5
|
+
const snapshotCollection = cloneXYSeriesSnapshotCollection(lastSeriesSnapshot);
|
|
6
|
+
const pathState = new Map();
|
|
7
|
+
if (!plotGroup) {
|
|
8
|
+
return {
|
|
9
|
+
snapshotCollection,
|
|
10
|
+
pathState,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
visibleSeries.forEach((series) => {
|
|
14
|
+
captureLiveSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot);
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
snapshotCollection,
|
|
18
|
+
pathState,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function captureLiveSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot) {
|
|
22
|
+
switch (series.type) {
|
|
23
|
+
case 'bar':
|
|
24
|
+
captureLiveBarSeriesState(series, plotGroup, snapshotCollection);
|
|
25
|
+
return;
|
|
26
|
+
case 'line':
|
|
27
|
+
captureLiveLineSeriesState(series, plotGroup, snapshotCollection, pathState);
|
|
28
|
+
return;
|
|
29
|
+
case 'scatter':
|
|
30
|
+
captureLiveScatterSeriesState(series, plotGroup, snapshotCollection);
|
|
31
|
+
return;
|
|
32
|
+
case 'area':
|
|
33
|
+
captureLiveAreaSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function captureLiveBarSeriesState(series, plotGroup, snapshotCollection) {
|
|
38
|
+
const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
|
|
39
|
+
const snapshot = captureLiveBarSnapshot(plotGroup, series.dataKey);
|
|
40
|
+
if (!snapshot) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
snapshotCollection.set(seriesId, new Map([
|
|
44
|
+
...(snapshotCollection.get(seriesId) ?? new Map()),
|
|
45
|
+
...snapshot,
|
|
46
|
+
]));
|
|
47
|
+
}
|
|
48
|
+
function captureLiveLineSeriesState(series, plotGroup, snapshotCollection, pathState) {
|
|
49
|
+
const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
|
|
50
|
+
const snapshot = captureLivePointSnapshot(plotGroup, `.circle-${sanitizeForCSS(series.dataKey)}`);
|
|
51
|
+
const linePathState = captureLivePathState(plotGroup, `.line-${sanitizeForCSS(series.dataKey)}`);
|
|
52
|
+
if (snapshot) {
|
|
53
|
+
snapshotCollection.set(seriesId, new Map([
|
|
54
|
+
...(snapshotCollection.get(seriesId) ?? new Map()),
|
|
55
|
+
...snapshot,
|
|
56
|
+
]));
|
|
57
|
+
}
|
|
58
|
+
if (linePathState.path !== null || linePathState.revealProgress !== null) {
|
|
59
|
+
pathState.set(seriesId, {
|
|
60
|
+
linePath: linePathState.path,
|
|
61
|
+
lineRevealProgress: linePathState.revealProgress,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function captureLiveScatterSeriesState(series, plotGroup, snapshotCollection) {
|
|
66
|
+
const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
|
|
67
|
+
const snapshot = captureLivePointSnapshot(plotGroup, `.scatter-point-${sanitizeForCSS(series.dataKey)}`);
|
|
68
|
+
if (!snapshot) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
snapshotCollection.set(seriesId, new Map([
|
|
72
|
+
...(snapshotCollection.get(seriesId) ?? new Map()),
|
|
73
|
+
...snapshot,
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
76
|
+
function captureLiveAreaSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot) {
|
|
77
|
+
const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
|
|
78
|
+
const snapshot = captureLiveAreaSnapshot(plotGroup, series.dataKey, lastSeriesSnapshot.get(seriesId));
|
|
79
|
+
const areaPathState = captureLivePathState(plotGroup, `.area-${sanitizeForCSS(series.dataKey)}`);
|
|
80
|
+
const areaLinePathState = captureLivePathState(plotGroup, `.area-line-${sanitizeForCSS(series.dataKey)}`);
|
|
81
|
+
if (snapshot) {
|
|
82
|
+
snapshotCollection.set(seriesId, new Map([
|
|
83
|
+
...(snapshotCollection.get(seriesId) ?? new Map()),
|
|
84
|
+
...snapshot,
|
|
85
|
+
]));
|
|
86
|
+
}
|
|
87
|
+
if (areaPathState.path !== null ||
|
|
88
|
+
areaLinePathState.path !== null ||
|
|
89
|
+
areaPathState.revealProgress !== null ||
|
|
90
|
+
areaLinePathState.revealProgress !== null) {
|
|
91
|
+
pathState.set(seriesId, {
|
|
92
|
+
areaPath: areaPathState.path,
|
|
93
|
+
areaLinePath: areaLinePathState.path,
|
|
94
|
+
areaRevealProgress: areaPathState.revealProgress,
|
|
95
|
+
areaLineRevealProgress: areaLinePathState.revealProgress,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function captureLiveBarSnapshot(plotGroup, dataKey) {
|
|
100
|
+
const bars = plotGroup.selectAll(`.bar-${sanitizeForCSS(dataKey)}`);
|
|
101
|
+
if (bars.empty()) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
bars.interrupt();
|
|
105
|
+
const snapshot = new Map();
|
|
106
|
+
bars.each((datum, index, nodes) => {
|
|
107
|
+
if (!datum.snapshotKey) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const node = nodes[index];
|
|
111
|
+
const x = getNumericAttribute(node, 'x');
|
|
112
|
+
const y = getNumericAttribute(node, 'y');
|
|
113
|
+
const width = getNumericAttribute(node, 'width');
|
|
114
|
+
const height = getNumericAttribute(node, 'height');
|
|
115
|
+
if (x === null || y === null || width === null || height === null) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
snapshot.set(datum.snapshotKey, {
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
width,
|
|
122
|
+
height,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
return snapshot.size > 0 ? snapshot : null;
|
|
126
|
+
}
|
|
127
|
+
function captureLivePointSnapshot(plotGroup, selector) {
|
|
128
|
+
const points = plotGroup.selectAll(selector);
|
|
129
|
+
if (points.empty()) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
points.interrupt();
|
|
133
|
+
const snapshot = new Map();
|
|
134
|
+
points.each((datum, index, nodes) => {
|
|
135
|
+
if (!datum.snapshotKey) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const node = nodes[index];
|
|
139
|
+
const x = getNumericAttribute(node, 'cx');
|
|
140
|
+
const y = getNumericAttribute(node, 'cy');
|
|
141
|
+
if (x === null || y === null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
snapshot.set(datum.snapshotKey, {
|
|
145
|
+
x,
|
|
146
|
+
y,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return snapshot.size > 0 ? snapshot : null;
|
|
150
|
+
}
|
|
151
|
+
function captureLiveAreaSnapshot(plotGroup, dataKey, previousSnapshot) {
|
|
152
|
+
const pointSnapshot = captureLivePointSnapshot(plotGroup, `.area-point-${sanitizeForCSS(dataKey)}`);
|
|
153
|
+
if (!pointSnapshot) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const snapshot = new Map();
|
|
157
|
+
pointSnapshot.forEach((point, snapshotKey) => {
|
|
158
|
+
snapshot.set(snapshotKey, {
|
|
159
|
+
x: point.x,
|
|
160
|
+
y0: previousSnapshot?.get(snapshotKey)?.y0 ?? point.y,
|
|
161
|
+
y1: point.y,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
return snapshot.size > 0 ? snapshot : null;
|
|
165
|
+
}
|
|
166
|
+
function captureLivePathState(plotGroup, selector) {
|
|
167
|
+
const path = plotGroup.select(selector);
|
|
168
|
+
if (path.empty()) {
|
|
169
|
+
return {
|
|
170
|
+
path: null,
|
|
171
|
+
revealProgress: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const node = path.node();
|
|
175
|
+
const revealProgress = node ? captureLeftToRightRevealProgress(node) : null;
|
|
176
|
+
path.interrupt();
|
|
177
|
+
interruptLeftToRightReveal(path.node());
|
|
178
|
+
return {
|
|
179
|
+
path: path.attr('d') || null,
|
|
180
|
+
revealProgress,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function getNumericAttribute(element, attribute) {
|
|
184
|
+
if (!element) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const value = Number(element.getAttribute(attribute));
|
|
188
|
+
return Number.isFinite(value) ? value : null;
|
|
189
|
+
}
|
|
190
|
+
function captureLeftToRightRevealProgress(target) {
|
|
191
|
+
const clipRect = getRevealClipRect(target);
|
|
192
|
+
if (!clipRect) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const targetBounds = target.getBBox();
|
|
196
|
+
const clipWidth = getNumericAttribute(clipRect, 'width');
|
|
197
|
+
const clipX = getNumericAttribute(clipRect, 'x');
|
|
198
|
+
if (clipWidth === null || clipX === null) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const padding = targetBounds.x - clipX;
|
|
202
|
+
const totalWidth = targetBounds.width + padding * 2;
|
|
203
|
+
if (totalWidth <= 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const progress = clipWidth / totalWidth;
|
|
207
|
+
if (!Number.isFinite(progress) || progress >= 1) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return Math.max(0, Math.min(1, progress));
|
|
211
|
+
}
|
|
212
|
+
function interruptLeftToRightReveal(target) {
|
|
213
|
+
const clipRect = target ? getRevealClipRect(target) : null;
|
|
214
|
+
if (!clipRect) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
select(clipRect).interrupt();
|
|
218
|
+
}
|
|
219
|
+
function getRevealClipRect(target) {
|
|
220
|
+
const svg = target.ownerSVGElement;
|
|
221
|
+
if (!svg) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const clipPathId = extractClipPathId(target.getAttribute('clip-path'));
|
|
225
|
+
if (!clipPathId) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const clipPath = target.ownerDocument?.getElementById(clipPathId);
|
|
229
|
+
if (!clipPath || !clipPath.classList.contains('xy-animation-reveal-clip')) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return clipPath.querySelector('rect');
|
|
233
|
+
}
|
|
234
|
+
function extractClipPathId(value) {
|
|
235
|
+
if (!value) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const match = value.match(/url\((?:['"])?#([^'")]+)(?:['"])?\)/);
|
|
239
|
+
return match?.[1] ?? null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { XYMotionDriverContract } from './driver.js';
|
|
2
|
+
import type { XYAreaAnimationContext, XYBarAnimationContext, XYMotionRenderResult, XYMotionSeries, XYMotionUpdateContext, XYPointAnimationContext } from './types.js';
|
|
3
|
+
export declare class NoopXYMotionDriver implements XYMotionDriverContract {
|
|
4
|
+
prepareForUpdate(_context: XYMotionUpdateContext): void;
|
|
5
|
+
getPointAnimationContext(_series: XYMotionSeries, _baselineY: number): XYPointAnimationContext | undefined;
|
|
6
|
+
getAreaAnimationContext(_series: XYMotionSeries): XYAreaAnimationContext | undefined;
|
|
7
|
+
getBarAnimationContext(_series: XYMotionSeries, _baselineValuePosition: number): XYBarAnimationContext | undefined;
|
|
8
|
+
completeRender(_result: XYMotionRenderResult): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class NoopXYMotionDriver {
|
|
2
|
+
prepareForUpdate(_context) { }
|
|
3
|
+
getPointAnimationContext(_series, _baselineY) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
getAreaAnimationContext(_series) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
getBarAnimationContext(_series, _baselineValuePosition) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
completeRender(_result) {
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Selection } from 'd3';
|
|
2
|
+
export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
|
|
3
|
+
export type XYAnimationConfig = {
|
|
4
|
+
show?: boolean;
|
|
5
|
+
duration?: number;
|
|
6
|
+
easing?: XYAnimationEasingPreset | `linear(${string})` | ((progress: number) => number);
|
|
7
|
+
};
|
|
8
|
+
export type XYRenderAnimationMode = 'none' | 'initial' | 'update';
|
|
9
|
+
export type XYPointSnapshot = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
export type XYAreaPointSnapshot = {
|
|
14
|
+
x: number;
|
|
15
|
+
y0: number;
|
|
16
|
+
y1: number;
|
|
17
|
+
};
|
|
18
|
+
export type XYBarSnapshot = {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
export type XYSeriesDatumSnapshot = XYPointSnapshot | XYAreaPointSnapshot | XYBarSnapshot;
|
|
25
|
+
export type XYSeriesSnapshot<TSnapshot = XYSeriesDatumSnapshot> = Map<string, TSnapshot>;
|
|
26
|
+
export type XYSeriesSnapshotCollection = Map<string, XYSeriesSnapshot<XYSeriesDatumSnapshot>>;
|
|
27
|
+
export type NormalizedXYAnimation = {
|
|
28
|
+
show: boolean;
|
|
29
|
+
duration: number;
|
|
30
|
+
easing: (progress: number) => number;
|
|
31
|
+
};
|
|
32
|
+
export type XYPointAnimationContext = {
|
|
33
|
+
mode: Exclude<XYRenderAnimationMode, 'none'>;
|
|
34
|
+
duration: number;
|
|
35
|
+
easing: (progress: number) => number;
|
|
36
|
+
baselineY: number;
|
|
37
|
+
previousSnapshot?: XYSeriesSnapshot<XYPointSnapshot>;
|
|
38
|
+
previousPath?: string | null;
|
|
39
|
+
previousRevealProgress?: number | null;
|
|
40
|
+
};
|
|
41
|
+
export type XYAreaAnimationContext = {
|
|
42
|
+
mode: Exclude<XYRenderAnimationMode, 'none'>;
|
|
43
|
+
duration: number;
|
|
44
|
+
easing: (progress: number) => number;
|
|
45
|
+
previousSnapshot?: XYSeriesSnapshot<XYAreaPointSnapshot>;
|
|
46
|
+
previousAreaPath?: string | null;
|
|
47
|
+
previousLinePath?: string | null;
|
|
48
|
+
previousAreaRevealProgress?: number | null;
|
|
49
|
+
previousLineRevealProgress?: number | null;
|
|
50
|
+
};
|
|
51
|
+
export type XYBarAnimationContext = {
|
|
52
|
+
mode: Exclude<XYRenderAnimationMode, 'none'>;
|
|
53
|
+
duration: number;
|
|
54
|
+
easing: (progress: number) => number;
|
|
55
|
+
baselineValuePosition: number;
|
|
56
|
+
previousSnapshot?: XYSeriesSnapshot<XYBarSnapshot>;
|
|
57
|
+
};
|
|
58
|
+
export type XYSeriesRenderResult<TSnapshot = XYSeriesDatumSnapshot> = {
|
|
59
|
+
snapshot: XYSeriesSnapshot<TSnapshot>;
|
|
60
|
+
transitions: Promise<void>[];
|
|
61
|
+
};
|
|
62
|
+
export type XYMotionSeries = {
|
|
63
|
+
type: 'line' | 'scatter' | 'bar' | 'area';
|
|
64
|
+
dataKey: string;
|
|
65
|
+
};
|
|
66
|
+
export type XYMotionRenderResult = {
|
|
67
|
+
snapshotCollection: XYSeriesSnapshotCollection;
|
|
68
|
+
transitions: Promise<void>[];
|
|
69
|
+
};
|
|
70
|
+
export type XYMotionUpdateContext = {
|
|
71
|
+
plotGroup: Selection<SVGGElement, undefined, null, undefined> | null;
|
|
72
|
+
visibleSeries: XYMotionSeries[];
|
|
73
|
+
};
|
|
74
|
+
export type XYLivePathState = {
|
|
75
|
+
linePath?: string | null;
|
|
76
|
+
areaPath?: string | null;
|
|
77
|
+
areaLinePath?: string | null;
|
|
78
|
+
lineRevealProgress?: number | null;
|
|
79
|
+
areaRevealProgress?: number | null;
|
|
80
|
+
areaLineRevealProgress?: number | null;
|
|
81
|
+
};
|
|
82
|
+
export type XYLivePathStateCollection = Map<string, XYLivePathState>;
|
|
83
|
+
export type XYSnapshotBoundDatum = {
|
|
84
|
+
snapshotKey?: string;
|
|
85
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|