@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.9.0",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@opendata-ai/openchart-core": "7.
|
|
51
|
+
"@opendata-ai/openchart-core": "7.9.0",
|
|
52
52
|
"d3-array": "^3.2.0",
|
|
53
53
|
"d3-format": "^3.1.2",
|
|
54
54
|
"d3-interpolate": "^3.0.0",
|
|
@@ -224,6 +224,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
|
|
|
224
224
|
},
|
|
225
225
|
"fill": "#06b6d4",
|
|
226
226
|
"height": 57.45223880597016,
|
|
227
|
+
"key": "|A",
|
|
227
228
|
"label": {
|
|
228
229
|
"connector": undefined,
|
|
229
230
|
"style": {
|
|
@@ -258,6 +259,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
|
|
|
258
259
|
},
|
|
259
260
|
"fill": "#06b6d4",
|
|
260
261
|
"height": 57.45223880597016,
|
|
262
|
+
"key": "|B",
|
|
261
263
|
"label": {
|
|
262
264
|
"connector": undefined,
|
|
263
265
|
"style": {
|
|
@@ -292,6 +294,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
|
|
|
292
294
|
},
|
|
293
295
|
"fill": "#06b6d4",
|
|
294
296
|
"height": 57.45223880597016,
|
|
297
|
+
"key": "|C",
|
|
295
298
|
"label": {
|
|
296
299
|
"connector": undefined,
|
|
297
300
|
"style": {
|
|
@@ -1037,8 +1040,14 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
1037
1040
|
"y": 105.2,
|
|
1038
1041
|
},
|
|
1039
1042
|
],
|
|
1043
|
+
"interpolate": "monotone",
|
|
1044
|
+
"key": "US",
|
|
1040
1045
|
"opacity": undefined,
|
|
1041
1046
|
"path": "M47,334.25L651.761,105.2",
|
|
1047
|
+
"pointKeys": [
|
|
1048
|
+
"2020-01-01",
|
|
1049
|
+
"2021-01-01",
|
|
1050
|
+
],
|
|
1042
1051
|
"points": [
|
|
1043
1052
|
{
|
|
1044
1053
|
"x": 47,
|
|
@@ -1129,8 +1138,14 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
1129
1138
|
"y": 143.375,
|
|
1130
1139
|
},
|
|
1131
1140
|
],
|
|
1141
|
+
"interpolate": "monotone",
|
|
1142
|
+
"key": "UK",
|
|
1132
1143
|
"opacity": undefined,
|
|
1133
1144
|
"path": "M47,296.075L651.761,143.375",
|
|
1145
|
+
"pointKeys": [
|
|
1146
|
+
"2020-01-01",
|
|
1147
|
+
"2021-01-01",
|
|
1148
|
+
],
|
|
1134
1149
|
"points": [
|
|
1135
1150
|
{
|
|
1136
1151
|
"x": 47,
|
|
@@ -1221,8 +1236,14 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
1221
1236
|
"y": 242.62999999999997,
|
|
1222
1237
|
},
|
|
1223
1238
|
],
|
|
1239
|
+
"interpolate": "monotone",
|
|
1240
|
+
"key": "FR",
|
|
1224
1241
|
"opacity": undefined,
|
|
1225
1242
|
"path": "M47,349.52L651.761,242.63",
|
|
1243
|
+
"pointKeys": [
|
|
1244
|
+
"2020-01-01",
|
|
1245
|
+
"2021-01-01",
|
|
1246
|
+
],
|
|
1226
1247
|
"points": [
|
|
1227
1248
|
{
|
|
1228
1249
|
"x": 47,
|
|
@@ -1313,8 +1334,14 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
1313
1334
|
"y": 196.82,
|
|
1314
1335
|
},
|
|
1315
1336
|
],
|
|
1337
|
+
"interpolate": "monotone",
|
|
1338
|
+
"key": "DE",
|
|
1316
1339
|
"opacity": undefined,
|
|
1317
1340
|
"path": "M47,318.98L651.761,196.82",
|
|
1341
|
+
"pointKeys": [
|
|
1342
|
+
"2020-01-01",
|
|
1343
|
+
"2021-01-01",
|
|
1344
|
+
],
|
|
1318
1345
|
"points": [
|
|
1319
1346
|
{
|
|
1320
1347
|
"x": 47,
|
|
@@ -1804,6 +1831,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
1804
1831
|
"type": "linear",
|
|
1805
1832
|
},
|
|
1806
1833
|
"height": 101.27142857142857,
|
|
1834
|
+
"key": "|A",
|
|
1807
1835
|
"label": {
|
|
1808
1836
|
"connector": undefined,
|
|
1809
1837
|
"style": {
|
|
@@ -1849,6 +1877,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
1849
1877
|
"type": "linear",
|
|
1850
1878
|
},
|
|
1851
1879
|
"height": 185.6642857142857,
|
|
1880
|
+
"key": "|B",
|
|
1852
1881
|
"label": {
|
|
1853
1882
|
"connector": undefined,
|
|
1854
1883
|
"style": {
|
|
@@ -1894,6 +1923,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
1894
1923
|
"type": "linear",
|
|
1895
1924
|
},
|
|
1896
1925
|
"height": 236.29999999999998,
|
|
1926
|
+
"key": "|C",
|
|
1897
1927
|
"label": {
|
|
1898
1928
|
"connector": undefined,
|
|
1899
1929
|
"style": {
|
|
@@ -1939,6 +1969,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
1939
1969
|
"type": "linear",
|
|
1940
1970
|
},
|
|
1941
1971
|
"height": 151.90714285714287,
|
|
1972
|
+
"key": "|D",
|
|
1942
1973
|
"label": {
|
|
1943
1974
|
"connector": undefined,
|
|
1944
1975
|
"style": {
|
|
@@ -50,11 +50,11 @@ describe('compileChart with animation', () => {
|
|
|
50
50
|
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
51
51
|
|
|
52
52
|
expect(layout.animation).toBeDefined();
|
|
53
|
-
expect(layout.animation!.
|
|
54
|
-
expect(layout.animation!.duration).toBe(500);
|
|
55
|
-
expect(layout.animation!.ease).toBe('smooth');
|
|
56
|
-
expect(layout.animation!.staggerDelay).toBe(80);
|
|
57
|
-
expect(layout.animation!.staggerOrder).toBe('index');
|
|
53
|
+
expect(layout.animation!.enter).toBeDefined();
|
|
54
|
+
expect(layout.animation!.enter!.duration).toBe(500);
|
|
55
|
+
expect(layout.animation!.enter!.ease).toBe('smooth');
|
|
56
|
+
expect(layout.animation!.enter!.staggerDelay).toBe(80);
|
|
57
|
+
expect(layout.animation!.enter!.staggerOrder).toBe('index');
|
|
58
58
|
expect(layout.animation!.annotationDelay).toBe(200);
|
|
59
59
|
});
|
|
60
60
|
|
|
@@ -80,8 +80,8 @@ describe('compileChart with animation', () => {
|
|
|
80
80
|
const layout = compileChart(spec, { width: 600, height: 400 });
|
|
81
81
|
|
|
82
82
|
expect(layout.animation).toBeDefined();
|
|
83
|
-
expect(layout.animation!.duration).toBe(800);
|
|
84
|
-
expect(layout.animation!.ease).toBe('smooth');
|
|
83
|
+
expect(layout.animation!.enter!.duration).toBe(800);
|
|
84
|
+
expect(layout.animation!.enter!.ease).toBe('smooth');
|
|
85
85
|
expect(layout.animation!.annotationDelay).toBe(500);
|
|
86
86
|
});
|
|
87
87
|
|
|
@@ -152,7 +152,7 @@ describe('compileChart with animation', () => {
|
|
|
152
152
|
} as ChartSpec;
|
|
153
153
|
const layoutCompact = compileChart(specNoAnim, { width: 350, height: 400 });
|
|
154
154
|
expect(layoutCompact.animation).toBeDefined();
|
|
155
|
-
expect(layoutCompact.animation!.
|
|
155
|
+
expect(layoutCompact.animation!.enter).toBeDefined();
|
|
156
156
|
|
|
157
157
|
// At full width (> 700), no override applies, so no animation
|
|
158
158
|
const layoutFull = compileChart(specNoAnim, { width: 800, height: 400 });
|
|
@@ -943,7 +943,7 @@ describe('compileChart', () => {
|
|
|
943
943
|
};
|
|
944
944
|
const layout = compileChart(spec, { width: 320, height: 200 });
|
|
945
945
|
expect(layout.display).toBe('sparkline');
|
|
946
|
-
expect(layout.animation?.
|
|
946
|
+
expect(layout.animation?.enter).toBeDefined();
|
|
947
947
|
});
|
|
948
948
|
|
|
949
949
|
it('breakpoint chrome wins when top-level is sparkline', () => {
|
|
@@ -1012,8 +1012,8 @@ describe('compileChart', () => {
|
|
|
1012
1012
|
{ ...lineSpec, display: 'sparkline' as const, animation: true },
|
|
1013
1013
|
{ width: 400, height: 80 },
|
|
1014
1014
|
);
|
|
1015
|
-
expect(layout.animation?.
|
|
1016
|
-
expect(layout.animation?.duration).toBe(1100);
|
|
1015
|
+
expect(layout.animation?.enter).toBeDefined();
|
|
1016
|
+
expect(layout.animation?.enter?.duration).toBe(1100);
|
|
1017
1017
|
});
|
|
1018
1018
|
|
|
1019
1019
|
it('sparkline + animation: { enter: { duration: 500 } } respects user duration', () => {
|
|
@@ -1025,12 +1025,12 @@ describe('compileChart', () => {
|
|
|
1025
1025
|
},
|
|
1026
1026
|
{ width: 400, height: 80 },
|
|
1027
1027
|
);
|
|
1028
|
-
expect(layout.animation?.duration).toBe(500);
|
|
1028
|
+
expect(layout.animation?.enter?.duration).toBe(500);
|
|
1029
1029
|
});
|
|
1030
1030
|
|
|
1031
1031
|
it('full mode + animation: true uses default 500ms (sparkline bump does not leak)', () => {
|
|
1032
1032
|
const layout = compileChart({ ...lineSpec, animation: true }, { width: 600, height: 400 });
|
|
1033
|
-
expect(layout.animation?.duration).toBe(500);
|
|
1033
|
+
expect(layout.animation?.enter?.duration).toBe(500);
|
|
1034
1034
|
});
|
|
1035
1035
|
|
|
1036
1036
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for mark key stamping across chart types.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that compileChart() stamps stable `key` fields on marks
|
|
5
|
+
* for data-update transitions. Each chart type has its own keying
|
|
6
|
+
* strategy; these tests verify the end-to-end pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ChartSpec } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { compileChart } from '../compile';
|
|
12
|
+
|
|
13
|
+
const dims = { width: 600, height: 400 };
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Column chart keys
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
describe('column chart mark keys', () => {
|
|
20
|
+
it('stamps unique keys on simple column marks', () => {
|
|
21
|
+
const spec: ChartSpec = {
|
|
22
|
+
mark: 'bar',
|
|
23
|
+
data: [
|
|
24
|
+
{ category: 'A', value: 10 },
|
|
25
|
+
{ category: 'B', value: 20 },
|
|
26
|
+
{ category: 'C', value: 30 },
|
|
27
|
+
],
|
|
28
|
+
encoding: {
|
|
29
|
+
x: { field: 'category', type: 'nominal' },
|
|
30
|
+
y: { field: 'value', type: 'quantitative' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const layout = compileChart(spec, dims);
|
|
34
|
+
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
35
|
+
expect(rects.length).toBe(3);
|
|
36
|
+
|
|
37
|
+
for (const m of rects) {
|
|
38
|
+
expect((m as { key?: string }).key).toBeDefined();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const keys = rects.map((m) => (m as { key?: string }).key);
|
|
42
|
+
expect(new Set(keys).size).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('deduplicates keys for duplicate category values', () => {
|
|
46
|
+
const spec: ChartSpec = {
|
|
47
|
+
mark: 'bar',
|
|
48
|
+
data: [
|
|
49
|
+
{ category: 'A', value: 10 },
|
|
50
|
+
{ category: 'A', value: 20 },
|
|
51
|
+
{ category: 'B', value: 30 },
|
|
52
|
+
],
|
|
53
|
+
encoding: {
|
|
54
|
+
x: { field: 'category', type: 'nominal' },
|
|
55
|
+
y: { field: 'value', type: 'quantitative' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const layout = compileChart(spec, dims);
|
|
59
|
+
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
60
|
+
const keys = rects.map((m) => (m as { key?: string }).key);
|
|
61
|
+
// All keys must be unique even with duplicate categories
|
|
62
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('stamps keys on grouped columns', () => {
|
|
66
|
+
const spec: ChartSpec = {
|
|
67
|
+
mark: 'bar',
|
|
68
|
+
data: [
|
|
69
|
+
{ category: 'Q1', series: 'Revenue', value: 100 },
|
|
70
|
+
{ category: 'Q1', series: 'Cost', value: 60 },
|
|
71
|
+
{ category: 'Q2', series: 'Revenue', value: 150 },
|
|
72
|
+
{ category: 'Q2', series: 'Cost', value: 90 },
|
|
73
|
+
],
|
|
74
|
+
encoding: {
|
|
75
|
+
x: { field: 'category', type: 'nominal' },
|
|
76
|
+
y: { field: 'value', type: 'quantitative' },
|
|
77
|
+
color: { field: 'series', type: 'nominal' },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const layout = compileChart(spec, dims);
|
|
81
|
+
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
82
|
+
expect(rects.length).toBeGreaterThanOrEqual(4);
|
|
83
|
+
|
|
84
|
+
const keys = rects.map((m) => (m as { key?: string }).key);
|
|
85
|
+
for (const k of keys) {
|
|
86
|
+
expect(k).toBeDefined();
|
|
87
|
+
}
|
|
88
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Bar chart keys
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
describe('bar chart mark keys', () => {
|
|
97
|
+
it('stamps unique keys on horizontal bar marks', () => {
|
|
98
|
+
const spec: ChartSpec = {
|
|
99
|
+
mark: 'bar',
|
|
100
|
+
data: [
|
|
101
|
+
{ name: 'Alpha', value: 10 },
|
|
102
|
+
{ name: 'Beta', value: 30 },
|
|
103
|
+
{ name: 'Gamma', value: 20 },
|
|
104
|
+
],
|
|
105
|
+
encoding: {
|
|
106
|
+
x: { field: 'value', type: 'quantitative' },
|
|
107
|
+
y: { field: 'name', type: 'nominal' },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const layout = compileChart(spec, dims);
|
|
111
|
+
const rects = layout.marks.filter((m) => m.type === 'rect');
|
|
112
|
+
const keys = rects.map((m) => (m as { key?: string }).key);
|
|
113
|
+
for (const k of keys) {
|
|
114
|
+
expect(k).toBeDefined();
|
|
115
|
+
}
|
|
116
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Line chart keys
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe('line chart mark keys', () => {
|
|
125
|
+
it('stamps key on line marks for multi-series', () => {
|
|
126
|
+
const spec: ChartSpec = {
|
|
127
|
+
mark: 'line',
|
|
128
|
+
data: [
|
|
129
|
+
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
130
|
+
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
131
|
+
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
132
|
+
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
133
|
+
],
|
|
134
|
+
encoding: {
|
|
135
|
+
x: { field: 'date', type: 'temporal' },
|
|
136
|
+
y: { field: 'value', type: 'quantitative' },
|
|
137
|
+
color: { field: 'country', type: 'nominal' },
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const layout = compileChart(spec, dims);
|
|
141
|
+
const lines = layout.marks.filter((m) => m.type === 'line');
|
|
142
|
+
expect(lines.length).toBe(2);
|
|
143
|
+
|
|
144
|
+
const keys = lines.map((m) => (m as { key?: string }).key);
|
|
145
|
+
for (const k of keys) {
|
|
146
|
+
expect(k).toBeDefined();
|
|
147
|
+
}
|
|
148
|
+
expect(new Set(keys).size).toBe(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('stamps pointKeys on line marks with temporal x', () => {
|
|
152
|
+
const spec: ChartSpec = {
|
|
153
|
+
mark: 'line',
|
|
154
|
+
data: [
|
|
155
|
+
{ date: '2020-01-01', value: 10 },
|
|
156
|
+
{ date: '2021-01-01', value: 40 },
|
|
157
|
+
{ date: '2022-01-01', value: 25 },
|
|
158
|
+
],
|
|
159
|
+
encoding: {
|
|
160
|
+
x: { field: 'date', type: 'temporal' },
|
|
161
|
+
y: { field: 'value', type: 'quantitative' },
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const layout = compileChart(spec, dims);
|
|
165
|
+
const lines = layout.marks.filter((m) => m.type === 'line');
|
|
166
|
+
expect(lines.length).toBe(1);
|
|
167
|
+
|
|
168
|
+
const line = lines[0] as { pointKeys?: string[] };
|
|
169
|
+
expect(line.pointKeys).toBeDefined();
|
|
170
|
+
expect(line.pointKeys!.length).toBe(3);
|
|
171
|
+
|
|
172
|
+
// Each point key should be a unique, defined string
|
|
173
|
+
for (const pk of line.pointKeys!) {
|
|
174
|
+
expect(pk).toBeDefined();
|
|
175
|
+
expect(typeof pk).toBe('string');
|
|
176
|
+
}
|
|
177
|
+
expect(new Set(line.pointKeys!).size).toBe(3);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Scatter chart keys
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
describe('scatter chart mark keys', () => {
|
|
186
|
+
it('stamps keys on scatter point marks', () => {
|
|
187
|
+
const spec: ChartSpec = {
|
|
188
|
+
mark: 'point',
|
|
189
|
+
data: [
|
|
190
|
+
{ x: 10, y: 20, label: 'A' },
|
|
191
|
+
{ x: 30, y: 40, label: 'B' },
|
|
192
|
+
{ x: 50, y: 60, label: 'C' },
|
|
193
|
+
],
|
|
194
|
+
encoding: {
|
|
195
|
+
x: { field: 'x', type: 'quantitative' },
|
|
196
|
+
y: { field: 'y', type: 'quantitative' },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const layout = compileChart(spec, dims);
|
|
200
|
+
const points = layout.marks.filter((m) => m.type === 'point');
|
|
201
|
+
expect(points.length).toBe(3);
|
|
202
|
+
|
|
203
|
+
const keys = points.map((m) => (m as { key?: string }).key);
|
|
204
|
+
for (const k of keys) {
|
|
205
|
+
expect(k).toBeDefined();
|
|
206
|
+
}
|
|
207
|
+
expect(new Set(keys).size).toBe(3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('uses encoding.key field when specified', () => {
|
|
211
|
+
const spec: ChartSpec = {
|
|
212
|
+
mark: 'point',
|
|
213
|
+
data: [
|
|
214
|
+
{ x: 10, y: 20, id: 'item-1' },
|
|
215
|
+
{ x: 30, y: 40, id: 'item-2' },
|
|
216
|
+
{ x: 50, y: 60, id: 'item-3' },
|
|
217
|
+
],
|
|
218
|
+
encoding: {
|
|
219
|
+
x: { field: 'x', type: 'quantitative' },
|
|
220
|
+
y: { field: 'y', type: 'quantitative' },
|
|
221
|
+
key: { field: 'id' },
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const layout = compileChart(spec, dims);
|
|
225
|
+
const points = layout.marks.filter((m) => m.type === 'point');
|
|
226
|
+
const keys = points.map((m) => (m as { key?: string }).key);
|
|
227
|
+
|
|
228
|
+
expect(keys).toContain('item-1');
|
|
229
|
+
expect(keys).toContain('item-2');
|
|
230
|
+
expect(keys).toContain('item-3');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Pie chart keys
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
describe('pie chart mark keys', () => {
|
|
239
|
+
it('stamps keys on arc marks', () => {
|
|
240
|
+
const spec: ChartSpec = {
|
|
241
|
+
mark: 'arc',
|
|
242
|
+
data: [
|
|
243
|
+
{ category: 'Desktop', share: 60 },
|
|
244
|
+
{ category: 'Mobile', share: 30 },
|
|
245
|
+
{ category: 'Tablet', share: 10 },
|
|
246
|
+
],
|
|
247
|
+
encoding: {
|
|
248
|
+
y: { field: 'share', type: 'quantitative' },
|
|
249
|
+
color: { field: 'category', type: 'nominal' },
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const layout = compileChart(spec, dims);
|
|
253
|
+
const arcs = layout.marks.filter((m) => m.type === 'arc');
|
|
254
|
+
expect(arcs.length).toBe(3);
|
|
255
|
+
|
|
256
|
+
const keys = arcs.map((m) => (m as { key?: string }).key);
|
|
257
|
+
for (const k of keys) {
|
|
258
|
+
expect(k).toBeDefined();
|
|
259
|
+
}
|
|
260
|
+
expect(new Set(keys).size).toBe(3);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Area chart keys
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
describe('area chart mark keys', () => {
|
|
269
|
+
it('stamps key on area marks', () => {
|
|
270
|
+
const spec: ChartSpec = {
|
|
271
|
+
mark: 'area',
|
|
272
|
+
data: [
|
|
273
|
+
{ date: '2020-01-01', value: 10, series: 'A' },
|
|
274
|
+
{ date: '2021-01-01', value: 40, series: 'A' },
|
|
275
|
+
{ date: '2020-01-01', value: 15, series: 'B' },
|
|
276
|
+
{ date: '2021-01-01', value: 35, series: 'B' },
|
|
277
|
+
],
|
|
278
|
+
encoding: {
|
|
279
|
+
x: { field: 'date', type: 'temporal' },
|
|
280
|
+
y: { field: 'value', type: 'quantitative' },
|
|
281
|
+
color: { field: 'series', type: 'nominal' },
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
const layout = compileChart(spec, dims);
|
|
285
|
+
const areas = layout.marks.filter((m) => m.type === 'area');
|
|
286
|
+
expect(areas.length).toBe(2);
|
|
287
|
+
|
|
288
|
+
const keys = areas.map((m) => (m as { key?: string }).key);
|
|
289
|
+
for (const k of keys) {
|
|
290
|
+
expect(k).toBeDefined();
|
|
291
|
+
}
|
|
292
|
+
expect(new Set(keys).size).toBe(2);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -120,11 +120,7 @@ describe('assignAnimationIndices', () => {
|
|
|
120
120
|
{ type: 'rect', x: 0, y: 0, width: 10, height: 50, fill: '#000' } as RectMark,
|
|
121
121
|
];
|
|
122
122
|
const animation: ResolvedAnimation = {
|
|
123
|
-
|
|
124
|
-
duration: 500,
|
|
125
|
-
ease: 'smooth',
|
|
126
|
-
staggerDelay: 50,
|
|
127
|
-
staggerOrder: 'value',
|
|
123
|
+
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
128
124
|
annotationDelay: 0,
|
|
129
125
|
};
|
|
130
126
|
assignAnimationIndices(marks, animation);
|
|
@@ -165,11 +161,7 @@ describe('assignAnimationIndices', () => {
|
|
|
165
161
|
} as RectMark,
|
|
166
162
|
];
|
|
167
163
|
const animation: ResolvedAnimation = {
|
|
168
|
-
|
|
169
|
-
duration: 500,
|
|
170
|
-
ease: 'smooth',
|
|
171
|
-
staggerDelay: 50,
|
|
172
|
-
staggerOrder: 'value',
|
|
164
|
+
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
173
165
|
annotationDelay: 0,
|
|
174
166
|
};
|
|
175
167
|
assignAnimationIndices(marks, animation);
|
|
@@ -205,11 +197,7 @@ describe('assignAnimationIndices', () => {
|
|
|
205
197
|
} as RectMark,
|
|
206
198
|
];
|
|
207
199
|
const animation: ResolvedAnimation = {
|
|
208
|
-
|
|
209
|
-
duration: 500,
|
|
210
|
-
ease: 'smooth',
|
|
211
|
-
staggerDelay: 50,
|
|
212
|
-
staggerOrder: 'value',
|
|
200
|
+
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
213
201
|
annotationDelay: 0,
|
|
214
202
|
};
|
|
215
203
|
assignAnimationIndices(marks, animation);
|
|
@@ -226,16 +214,11 @@ describe('assignAnimationIndices', () => {
|
|
|
226
214
|
expect(marks[0].animationIndex).toBeUndefined();
|
|
227
215
|
});
|
|
228
216
|
|
|
229
|
-
it('is a no-op when animation
|
|
217
|
+
it('is a no-op when animation has no enter phase', () => {
|
|
230
218
|
const marks: Mark[] = [
|
|
231
219
|
{ type: 'rect', x: 0, y: 0, width: 10, height: 30, fill: '#000' } as RectMark,
|
|
232
220
|
];
|
|
233
221
|
const animation: ResolvedAnimation = {
|
|
234
|
-
enabled: false,
|
|
235
|
-
duration: 500,
|
|
236
|
-
ease: 'smooth',
|
|
237
|
-
staggerDelay: 50,
|
|
238
|
-
staggerOrder: 'value',
|
|
239
222
|
annotationDelay: 0,
|
|
240
223
|
};
|
|
241
224
|
assignAnimationIndices(marks, animation);
|
|
@@ -244,11 +227,7 @@ describe('assignAnimationIndices', () => {
|
|
|
244
227
|
|
|
245
228
|
it('handles empty marks array', () => {
|
|
246
229
|
const animation: ResolvedAnimation = {
|
|
247
|
-
|
|
248
|
-
duration: 500,
|
|
249
|
-
ease: 'smooth',
|
|
250
|
-
staggerDelay: 50,
|
|
251
|
-
staggerOrder: 'value',
|
|
230
|
+
enter: { duration: 500, ease: 'smooth', staggerDelay: 50, staggerOrder: 'value' },
|
|
252
231
|
annotationDelay: 0,
|
|
253
232
|
};
|
|
254
233
|
const marks: Mark[] = [];
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from '@opendata-ai/openchart-core';
|
|
20
20
|
import { isGradientDef, NARROW_VIEWPORT_MAX } from '@opendata-ai/openchart-core';
|
|
21
21
|
import type { ScaleBand, ScaleLinear } from 'd3-scale';
|
|
22
|
+
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
22
23
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
23
24
|
import type { ResolvedScales } from '../../layout/scales';
|
|
24
25
|
import { isConditionalValueDef, resolveConditionalValue } from '../../transforms/conditional';
|
|
@@ -194,9 +195,26 @@ export function computeBarMarks(
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
// Stamp identity keys for data-update transitions
|
|
199
|
+
const categoryField = yChannel.field;
|
|
200
|
+
stampBarKeys(marks, categoryField, colorField);
|
|
201
|
+
|
|
197
202
|
return applyMarkDefOverrides(marks, spec, bandwidth);
|
|
198
203
|
}
|
|
199
204
|
|
|
205
|
+
/** Stamp stable identity keys on bar marks for data-update transitions. */
|
|
206
|
+
function stampBarKeys(marks: RectMark[], categoryField: string, colorField?: string): void {
|
|
207
|
+
const rawKeys = marks.map((m) => {
|
|
208
|
+
const series = colorField ? String(m.data[colorField] ?? '') : '';
|
|
209
|
+
const cat = serializeKeyValue(m.data[categoryField]);
|
|
210
|
+
return `${series}|${cat}`;
|
|
211
|
+
});
|
|
212
|
+
const keys = dedupeKeys(rawKeys);
|
|
213
|
+
for (let i = 0; i < marks.length; i++) {
|
|
214
|
+
marks[i].key = keys[i];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
/** Compute stacked horizontal bars with support for zero/normalize/center modes. */
|
|
201
219
|
function computeStackedBars(
|
|
202
220
|
data: DataRow[],
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from '@opendata-ai/openchart-core';
|
|
23
23
|
import { isGradientDef } from '@opendata-ai/openchart-core';
|
|
24
24
|
import type { ScaleBand, ScaleLinear } from 'd3-scale';
|
|
25
|
+
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
25
26
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
26
27
|
import type { ResolvedScales } from '../../layout/scales';
|
|
27
28
|
import { isConditionalValueDef, resolveConditionalValue } from '../../transforms/conditional';
|
|
@@ -163,9 +164,26 @@ export function computeColumnMarks(
|
|
|
163
164
|
);
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
// Stamp identity keys for data-update transitions
|
|
168
|
+
const categoryField = xChannel.field;
|
|
169
|
+
stampColumnKeys(marks, categoryField, colorField);
|
|
170
|
+
|
|
166
171
|
return applyMarkDefOverrides(marks, spec, bandwidth);
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
/** Stamp stable identity keys on column marks for data-update transitions. */
|
|
175
|
+
function stampColumnKeys(marks: RectMark[], categoryField: string, colorField?: string): void {
|
|
176
|
+
const rawKeys = marks.map((m) => {
|
|
177
|
+
const series = colorField ? String(m.data[colorField] ?? '') : '';
|
|
178
|
+
const cat = serializeKeyValue(m.data[categoryField]);
|
|
179
|
+
return `${series}|${cat}`;
|
|
180
|
+
});
|
|
181
|
+
const keys = dedupeKeys(rawKeys);
|
|
182
|
+
for (let i = 0; i < marks.length; i++) {
|
|
183
|
+
marks[i].key = keys[i];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
169
187
|
/** Compute simple (non-grouped) vertical columns. */
|
|
170
188
|
function computeSimpleColumns(
|
|
171
189
|
data: DataRow[],
|