@opendata-ai/openchart-engine 7.7.0 → 7.9.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/dist/index.d.ts +63 -4
- package/dist/index.js +227 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +31 -0
- package/src/__tests__/compile-animation.test.ts +8 -8
- package/src/__tests__/compile-chart.test.ts +5 -5
- package/src/__tests__/mark-keys.test.ts +294 -0
- package/src/charts/__tests__/post-process.test.ts +5 -26
- package/src/charts/bar/compute.ts +18 -0
- package/src/charts/column/compute.ts +18 -0
- package/src/charts/dot/compute.ts +30 -13
- package/src/charts/line/area.ts +20 -0
- package/src/charts/line/compute.ts +35 -1
- package/src/charts/line/curves.ts +47 -2
- package/src/charts/pie/compute.ts +13 -0
- package/src/charts/post-process.ts +2 -2
- package/src/charts/rule/index.ts +9 -0
- package/src/charts/scatter/compute.ts +15 -0
- package/src/charts/text/index.ts +9 -0
- package/src/charts/tick/index.ts +9 -0
- package/src/compile.ts +1 -1
- package/src/compiler/__tests__/animation.test.ts +78 -22
- package/src/compiler/__tests__/keys.test.ts +89 -0
- package/src/compiler/animation.ts +70 -50
- package/src/compiler/keys.ts +54 -0
- package/src/compiler/normalize.ts +1 -1
- package/src/index.ts +19 -1
- package/src/tables/__tests__/compile-table.test.ts +5 -5
- package/src/tilemap/compile-tilemap.ts +10 -2
|
@@ -2,28 +2,53 @@
|
|
|
2
2
|
* Animation resolver: normalizes AnimationSpec into fully resolved config.
|
|
3
3
|
*
|
|
4
4
|
* Handles the shorthand forms:
|
|
5
|
-
* - true ->
|
|
6
|
-
* - { enter: { duration: 800 } } -> merge with defaults
|
|
5
|
+
* - true -> all three phases with defaults
|
|
6
|
+
* - { enter: { duration: 800 } } -> merge with defaults per phase
|
|
7
7
|
* - false/undefined -> undefined (no animation)
|
|
8
|
+
*
|
|
9
|
+
* Each phase resolves independently:
|
|
10
|
+
* - true/config-object -> resolved phase merged onto that phase's defaults
|
|
11
|
+
* - false -> phase absent
|
|
12
|
+
* - undefined -> phase present with defaults (animation: {} behaves like true)
|
|
13
|
+
*
|
|
14
|
+
* Returns undefined only when ALL phases resolve absent.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
import type {
|
|
11
18
|
AnimationConfig,
|
|
12
|
-
AnimationEase,
|
|
13
19
|
AnimationPhaseConfig,
|
|
14
20
|
AnimationSpec,
|
|
15
21
|
AnimationStagger,
|
|
16
22
|
ResolvedAnimation,
|
|
23
|
+
ResolvedAnimationPhase,
|
|
17
24
|
} from '@opendata-ai/openchart-core';
|
|
18
25
|
|
|
19
26
|
/** Default values for entrance animation. */
|
|
20
|
-
const ENTER_DEFAULTS = {
|
|
27
|
+
export const ENTER_DEFAULTS: ResolvedAnimationPhase = {
|
|
21
28
|
duration: 500,
|
|
22
|
-
ease: 'smooth'
|
|
29
|
+
ease: 'smooth',
|
|
23
30
|
staggerDelay: 80,
|
|
24
|
-
staggerOrder: 'index'
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
staggerOrder: 'index',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Default values for update (data transition) animation. */
|
|
35
|
+
export const UPDATE_DEFAULTS: ResolvedAnimationPhase = {
|
|
36
|
+
duration: 500,
|
|
37
|
+
ease: 'smooth',
|
|
38
|
+
staggerDelay: 0,
|
|
39
|
+
staggerOrder: 'index',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Default values for exit animation. */
|
|
43
|
+
export const EXIT_DEFAULTS: ResolvedAnimationPhase = {
|
|
44
|
+
duration: 300,
|
|
45
|
+
ease: 'smooth',
|
|
46
|
+
staggerDelay: 0,
|
|
47
|
+
staggerOrder: 'index',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Default annotation delay in ms after marks finish. */
|
|
51
|
+
const DEFAULT_ANNOTATION_DELAY = 200;
|
|
27
52
|
|
|
28
53
|
/** Maximum total stagger time in ms. Prevents 200-bar charts from taking 6s. */
|
|
29
54
|
const MAX_TOTAL_STAGGER_MS = 2000;
|
|
@@ -35,35 +60,31 @@ const MAX_TOTAL_STAGGER_MS = 2000;
|
|
|
35
60
|
export function resolveAnimation(spec: AnimationSpec | undefined): ResolvedAnimation | undefined {
|
|
36
61
|
if (spec === undefined || spec === false) return undefined;
|
|
37
62
|
|
|
38
|
-
// true ->
|
|
63
|
+
// true -> all three phases with defaults
|
|
39
64
|
if (spec === true) {
|
|
40
65
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
staggerOrder: ENTER_DEFAULTS.staggerOrder,
|
|
46
|
-
annotationDelay: ENTER_DEFAULTS.annotationDelay,
|
|
66
|
+
enter: { ...ENTER_DEFAULTS },
|
|
67
|
+
update: { ...UPDATE_DEFAULTS },
|
|
68
|
+
exit: { ...EXIT_DEFAULTS },
|
|
69
|
+
annotationDelay: DEFAULT_ANNOTATION_DELAY,
|
|
47
70
|
};
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
// AnimationConfig object
|
|
51
74
|
const config = spec as AnimationConfig;
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
76
|
+
const enter = resolvePhase(config.enter, ENTER_DEFAULTS);
|
|
77
|
+
const update = resolvePhase(config.update, UPDATE_DEFAULTS);
|
|
78
|
+
const exit = resolvePhase(config.exit, EXIT_DEFAULTS);
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
// Return undefined only when ALL phases resolve absent
|
|
81
|
+
if (!enter && !update && !exit) return undefined;
|
|
59
82
|
|
|
60
83
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
staggerOrder: enterConfig.staggerOrder,
|
|
66
|
-
annotationDelay: config.annotationDelay ?? ENTER_DEFAULTS.annotationDelay,
|
|
84
|
+
enter: enter ?? undefined,
|
|
85
|
+
update: update ?? undefined,
|
|
86
|
+
exit: exit ?? undefined,
|
|
87
|
+
annotationDelay: config.annotationDelay ?? DEFAULT_ANNOTATION_DELAY,
|
|
67
88
|
};
|
|
68
89
|
}
|
|
69
90
|
|
|
@@ -75,48 +96,47 @@ export function clampStaggerDelay(delay: number, elementCount: number): number {
|
|
|
75
96
|
return Math.min(delay, MAX_TOTAL_STAGGER_MS / elementCount);
|
|
76
97
|
}
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a single animation phase.
|
|
101
|
+
* - false -> null (phase absent)
|
|
102
|
+
* - undefined -> phase present with defaults
|
|
103
|
+
* - true -> phase present with defaults
|
|
104
|
+
* - config object -> merge with defaults
|
|
105
|
+
*/
|
|
106
|
+
function resolvePhase(
|
|
107
|
+
phase: AnimationPhaseConfig | boolean | undefined,
|
|
108
|
+
defaults: ResolvedAnimationPhase,
|
|
109
|
+
): ResolvedAnimationPhase | null {
|
|
110
|
+
if (phase === false) return null;
|
|
88
111
|
|
|
89
|
-
function resolvePhaseConfig(phase: AnimationPhaseConfig | boolean | undefined): ResolvedPhase {
|
|
90
112
|
if (phase === undefined || phase === true) {
|
|
91
|
-
return {
|
|
92
|
-
duration: ENTER_DEFAULTS.duration,
|
|
93
|
-
ease: ENTER_DEFAULTS.ease,
|
|
94
|
-
staggerDelay: ENTER_DEFAULTS.staggerDelay,
|
|
95
|
-
staggerOrder: ENTER_DEFAULTS.staggerOrder,
|
|
96
|
-
};
|
|
113
|
+
return { ...defaults };
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
const cfg = phase as AnimationPhaseConfig;
|
|
100
|
-
const stagger = resolveStagger(cfg.stagger);
|
|
117
|
+
const stagger = resolveStagger(cfg.stagger, defaults);
|
|
101
118
|
|
|
102
119
|
return {
|
|
103
|
-
duration: cfg.duration ??
|
|
104
|
-
ease: cfg.ease ??
|
|
120
|
+
duration: cfg.duration ?? defaults.duration,
|
|
121
|
+
ease: cfg.ease ?? defaults.ease,
|
|
105
122
|
staggerDelay: stagger.delay,
|
|
106
123
|
staggerOrder: stagger.order,
|
|
107
124
|
};
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
function resolveStagger(
|
|
127
|
+
function resolveStagger(
|
|
128
|
+
stagger: AnimationStagger | boolean | undefined,
|
|
129
|
+
defaults: ResolvedAnimationPhase,
|
|
130
|
+
): {
|
|
111
131
|
delay: number;
|
|
112
132
|
order: 'index' | 'value' | 'reverse';
|
|
113
133
|
} {
|
|
114
134
|
if (stagger === false) return { delay: 0, order: 'index' };
|
|
115
135
|
if (stagger === undefined || stagger === true) {
|
|
116
|
-
return { delay:
|
|
136
|
+
return { delay: defaults.staggerDelay, order: defaults.staggerOrder };
|
|
117
137
|
}
|
|
118
138
|
return {
|
|
119
|
-
delay: stagger.delay ??
|
|
120
|
-
order: stagger.order ??
|
|
139
|
+
delay: stagger.delay ?? defaults.staggerDelay,
|
|
140
|
+
order: stagger.order ?? defaults.staggerOrder,
|
|
121
141
|
};
|
|
122
142
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark key utilities for data-update transitions.
|
|
3
|
+
*
|
|
4
|
+
* Keys uniquely identify marks across data updates so the renderer can
|
|
5
|
+
* match old marks to new marks and animate the difference. Each chart
|
|
6
|
+
* type stamps keys during its compute phase; these helpers provide the
|
|
7
|
+
* shared serialization and deduplication logic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Sentinel string for null/undefined values. Uses a Unicode symbol
|
|
11
|
+
* that won't collide with real data values. */
|
|
12
|
+
const NULL_KEY = '∅';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serialize a datum value into a stable key fragment.
|
|
16
|
+
* - Date -> epoch ms string (avoids locale-dependent Date.toString)
|
|
17
|
+
* - number/string/boolean -> String(v)
|
|
18
|
+
* - null/undefined -> null sentinel
|
|
19
|
+
*/
|
|
20
|
+
export function serializeKeyValue(v: unknown): string {
|
|
21
|
+
if (v === null || v === undefined) return NULL_KEY;
|
|
22
|
+
if (v instanceof Date) return String(v.getTime());
|
|
23
|
+
return String(v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Append occurrence suffixes to duplicate keys, in order.
|
|
28
|
+
*
|
|
29
|
+
* Only keys that appear more than once get suffixed. Unique keys pass
|
|
30
|
+
* through unchanged so keys remain human-readable when possible.
|
|
31
|
+
* Uses SOH (\x01) as separator to avoid collisions with real data values.
|
|
32
|
+
*/
|
|
33
|
+
export function dedupeKeys(keys: string[]): string[] {
|
|
34
|
+
// Count occurrences
|
|
35
|
+
const counts = new Map<string, number>();
|
|
36
|
+
for (const k of keys) {
|
|
37
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Assign suffixes only to duplicates
|
|
41
|
+
const seen = new Map<string, number>();
|
|
42
|
+
const result: string[] = [];
|
|
43
|
+
for (const k of keys) {
|
|
44
|
+
if (counts.get(k)! > 1) {
|
|
45
|
+
const idx = seen.get(k) ?? 0;
|
|
46
|
+
result.push(`${k}\x01${idx}`);
|
|
47
|
+
seen.set(k, idx + 1);
|
|
48
|
+
} else {
|
|
49
|
+
result.push(k);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
@@ -132,7 +132,7 @@ function inferFieldType(data: DataRow[], field: string): FieldType {
|
|
|
132
132
|
function inferEncodingTypes(encoding: Encoding, data: DataRow[], warnings: string[]): Encoding {
|
|
133
133
|
const result = { ...encoding };
|
|
134
134
|
|
|
135
|
-
for (const channel of ['x', 'y', 'color', 'size', 'detail'] as const) {
|
|
135
|
+
for (const channel of ['x', 'y', 'color', 'size', 'detail', 'key'] as const) {
|
|
136
136
|
const spec = result[channel];
|
|
137
137
|
if (!spec) continue;
|
|
138
138
|
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,25 @@ export {
|
|
|
26
26
|
// Animation resolution
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
-
export {
|
|
29
|
+
export {
|
|
30
|
+
clampStaggerDelay,
|
|
31
|
+
ENTER_DEFAULTS,
|
|
32
|
+
EXIT_DEFAULTS,
|
|
33
|
+
resolveAnimation,
|
|
34
|
+
UPDATE_DEFAULTS,
|
|
35
|
+
} from './compiler/animation';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Mark keying for data-update transitions
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export { dedupeKeys, serializeKeyValue } from './compiler/keys';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Geometry helpers for path reconstruction
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export { buildAreaPath, buildLinePath } from './charts/line/curves';
|
|
30
48
|
|
|
31
49
|
// ---------------------------------------------------------------------------
|
|
32
50
|
// Graph compilation types
|
|
@@ -209,9 +209,9 @@ describe('compileTable', () => {
|
|
|
209
209
|
it('includes resolved animation when spec has animation: true', () => {
|
|
210
210
|
const layout = compileTable({ ...baseSpec, animation: true }, baseOptions);
|
|
211
211
|
expect(layout.animation).toBeDefined();
|
|
212
|
-
expect(layout.animation!.
|
|
213
|
-
expect(layout.animation!.duration).toBe(500);
|
|
214
|
-
expect(layout.animation!.staggerDelay).toBe(80);
|
|
212
|
+
expect(layout.animation!.enter).toBeDefined();
|
|
213
|
+
expect(layout.animation!.enter!.duration).toBe(500);
|
|
214
|
+
expect(layout.animation!.enter!.staggerDelay).toBe(80);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
217
|
it('includes resolved animation with custom config', () => {
|
|
@@ -220,8 +220,8 @@ describe('compileTable', () => {
|
|
|
220
220
|
baseOptions,
|
|
221
221
|
);
|
|
222
222
|
expect(layout.animation).toBeDefined();
|
|
223
|
-
expect(layout.animation!.duration).toBe(800);
|
|
224
|
-
expect(layout.animation!.ease).toBe('snappy');
|
|
223
|
+
expect(layout.animation!.enter!.duration).toBe(800);
|
|
224
|
+
expect(layout.animation!.enter!.ease).toBe('snappy');
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it('does not include animation when spec omits it', () => {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
computeChrome,
|
|
39
39
|
estimateTextWidth,
|
|
40
40
|
formatNumber,
|
|
41
|
+
pickLabelColor,
|
|
41
42
|
resolveTheme,
|
|
42
43
|
SEQUENTIAL_PALETTES,
|
|
43
44
|
} from '@opendata-ai/openchart-core';
|
|
@@ -226,6 +227,7 @@ function compileQuantitative(
|
|
|
226
227
|
formattedValue,
|
|
227
228
|
hasData,
|
|
228
229
|
theme,
|
|
230
|
+
isDarkMode: !!isDarkMode,
|
|
229
231
|
}),
|
|
230
232
|
);
|
|
231
233
|
}
|
|
@@ -397,6 +399,7 @@ function compileCategorical(
|
|
|
397
399
|
formattedValue,
|
|
398
400
|
hasData,
|
|
399
401
|
theme,
|
|
402
|
+
isDarkMode: !!isDarkMode,
|
|
400
403
|
category: hasData ? category : undefined,
|
|
401
404
|
}),
|
|
402
405
|
);
|
|
@@ -488,6 +491,7 @@ interface TileMarkOptions {
|
|
|
488
491
|
formattedValue: string;
|
|
489
492
|
hasData: boolean;
|
|
490
493
|
theme: ResolvedTheme;
|
|
494
|
+
isDarkMode: boolean;
|
|
491
495
|
category?: string | null;
|
|
492
496
|
}
|
|
493
497
|
|
|
@@ -505,16 +509,20 @@ function buildTileMark(opts: TileMarkOptions): TileMapTileMark {
|
|
|
505
509
|
formattedValue,
|
|
506
510
|
hasData,
|
|
507
511
|
theme,
|
|
512
|
+
isDarkMode,
|
|
508
513
|
category,
|
|
509
514
|
} = opts;
|
|
510
515
|
const tileCenterX = gridOffsetX + pos.x + tileSize / 2;
|
|
511
516
|
const tileTopY = gridOffsetY + pos.y;
|
|
512
517
|
|
|
518
|
+
const textColor = hasData ? pickLabelColor(fill, isDarkMode) : '#ffffff';
|
|
519
|
+
const isLightText = textColor === '#ffffff';
|
|
520
|
+
|
|
513
521
|
const labelStyle: TextStyle = {
|
|
514
522
|
fontFamily: theme.fonts.family,
|
|
515
523
|
fontSize: tileSize > 24 ? 10 : 7,
|
|
516
524
|
fontWeight: 700,
|
|
517
|
-
fill:
|
|
525
|
+
fill: textColor,
|
|
518
526
|
lineHeight: 1.2,
|
|
519
527
|
};
|
|
520
528
|
|
|
@@ -522,7 +530,7 @@ function buildTileMark(opts: TileMarkOptions): TileMapTileMark {
|
|
|
522
530
|
fontFamily: theme.fonts.family,
|
|
523
531
|
fontSize: tileSize > 24 ? 10 : 7,
|
|
524
532
|
fontWeight: 300,
|
|
525
|
-
fill: 'rgba(255,255,255,0.6)',
|
|
533
|
+
fill: isLightText ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.45)',
|
|
526
534
|
lineHeight: 1.2,
|
|
527
535
|
};
|
|
528
536
|
|