@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.0.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 +149 -12
- package/dist/index.js +804 -155
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,629 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { validateSpec } from '../validate';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Test data
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const validLineData = [
|
|
9
|
-
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
10
|
-
{ date: '2021-01-01', value: 20, country: 'US' },
|
|
11
|
-
{ date: '2022-01-01', value: 30, country: 'UK' },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const validLineSpec = {
|
|
15
|
-
mark: 'line',
|
|
16
|
-
data: validLineData,
|
|
17
|
-
encoding: {
|
|
18
|
-
x: { field: 'date', type: 'temporal' },
|
|
19
|
-
y: { field: 'value', type: 'quantitative' },
|
|
20
|
-
color: { field: 'country', type: 'nominal' },
|
|
21
|
-
},
|
|
22
|
-
chrome: { title: 'GDP Growth' },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const validBarSpec = {
|
|
26
|
-
mark: 'bar',
|
|
27
|
-
data: [
|
|
28
|
-
{ category: 'A', count: 10 },
|
|
29
|
-
{ category: 'B', count: 20 },
|
|
30
|
-
],
|
|
31
|
-
encoding: {
|
|
32
|
-
x: { field: 'count', type: 'quantitative' },
|
|
33
|
-
y: { field: 'category', type: 'nominal' },
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const validArcSpec = {
|
|
38
|
-
mark: 'arc',
|
|
39
|
-
data: [
|
|
40
|
-
{ label: 'Apples', amount: 30 },
|
|
41
|
-
{ label: 'Oranges', amount: 50 },
|
|
42
|
-
{ label: 'Bananas', amount: 20 },
|
|
43
|
-
],
|
|
44
|
-
encoding: {
|
|
45
|
-
y: { field: 'amount', type: 'quantitative' },
|
|
46
|
-
color: { field: 'label', type: 'nominal' },
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Tests
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
describe('validateSpec', () => {
|
|
55
|
-
describe('basic shape checks', () => {
|
|
56
|
-
it('rejects null with INVALID_TYPE code', () => {
|
|
57
|
-
const result = validateSpec(null);
|
|
58
|
-
expect(result.valid).toBe(false);
|
|
59
|
-
expect(result.errors[0].message).toContain('non-null object');
|
|
60
|
-
expect(result.errors[0].code).toBe('INVALID_TYPE');
|
|
61
|
-
expect(result.errors[0].suggestion).toBeDefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('rejects arrays', () => {
|
|
65
|
-
const result = validateSpec([1, 2, 3]);
|
|
66
|
-
expect(result.valid).toBe(false);
|
|
67
|
-
expect(result.errors[0].code).toBe('INVALID_TYPE');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('rejects strings', () => {
|
|
71
|
-
const result = validateSpec('hello');
|
|
72
|
-
expect(result.valid).toBe(false);
|
|
73
|
-
expect(result.errors[0].code).toBe('INVALID_TYPE');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('rejects objects without mark or type with MISSING_FIELD code', () => {
|
|
77
|
-
const result = validateSpec({ data: [] });
|
|
78
|
-
expect(result.valid).toBe(false);
|
|
79
|
-
expect(result.errors[0].code).toBe('MISSING_FIELD');
|
|
80
|
-
expect(result.errors[0].suggestion).toContain('bar');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('rejects invalid mark values with INVALID_VALUE code', () => {
|
|
84
|
-
const result = validateSpec({ mark: 'waterfall' });
|
|
85
|
-
expect(result.valid).toBe(false);
|
|
86
|
-
expect(result.errors[0].message).toContain('"waterfall" is not a valid mark type');
|
|
87
|
-
expect(result.errors[0].code).toBe('INVALID_VALUE');
|
|
88
|
-
expect(result.errors[0].suggestion).toContain('line');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('chart specs', () => {
|
|
93
|
-
it('accepts a valid line spec', () => {
|
|
94
|
-
const result = validateSpec(validLineSpec);
|
|
95
|
-
expect(result.valid).toBe(true);
|
|
96
|
-
expect(result.errors).toHaveLength(0);
|
|
97
|
-
expect(result.normalized).not.toBeNull();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('accepts a valid bar spec', () => {
|
|
101
|
-
const result = validateSpec(validBarSpec);
|
|
102
|
-
expect(result.valid).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('accepts a valid arc spec', () => {
|
|
106
|
-
const result = validateSpec(validArcSpec);
|
|
107
|
-
expect(result.valid).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('rejects empty data with EMPTY_DATA code and suggestion', () => {
|
|
111
|
-
const result = validateSpec({ ...validLineSpec, data: [] });
|
|
112
|
-
expect(result.valid).toBe(false);
|
|
113
|
-
expect(result.errors[0].message).toContain('non-empty array');
|
|
114
|
-
expect(result.errors[0].code).toBe('EMPTY_DATA');
|
|
115
|
-
expect(result.errors[0].suggestion).toContain('Add at least one data row');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('rejects non-array data with INVALID_TYPE code', () => {
|
|
119
|
-
const result = validateSpec({ ...validLineSpec, data: 'not an array' });
|
|
120
|
-
expect(result.valid).toBe(false);
|
|
121
|
-
expect(result.errors[0].message).toContain('must be an array');
|
|
122
|
-
expect(result.errors[0].code).toBe('INVALID_TYPE');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('rejects missing encoding with MISSING_FIELD code and channel suggestion', () => {
|
|
126
|
-
const result = validateSpec({
|
|
127
|
-
mark: 'line',
|
|
128
|
-
data: validLineData,
|
|
129
|
-
});
|
|
130
|
-
expect(result.valid).toBe(false);
|
|
131
|
-
expect(result.errors[0].message).toContain('requires an "encoding" object');
|
|
132
|
-
expect(result.errors[0].code).toBe('MISSING_FIELD');
|
|
133
|
-
// Should suggest the required channels for this chart type
|
|
134
|
-
expect(result.errors[0].suggestion).toContain('encoding');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('rejects missing required channel with MISSING_FIELD code', () => {
|
|
138
|
-
const result = validateSpec({
|
|
139
|
-
mark: 'line',
|
|
140
|
-
data: validLineData,
|
|
141
|
-
encoding: {
|
|
142
|
-
x: { field: 'date', type: 'temporal' },
|
|
143
|
-
// Missing y, which is required for line charts
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
expect(result.valid).toBe(false);
|
|
147
|
-
const yError = result.errors.find((e) => e.message.includes('encoding.y'));
|
|
148
|
-
expect(yError).toBeDefined();
|
|
149
|
-
expect(yError!.code).toBe('MISSING_FIELD');
|
|
150
|
-
expect(yError!.suggestion).toBeDefined();
|
|
151
|
-
// Suggestion should mention available data columns
|
|
152
|
-
expect(yError!.suggestion).toContain('date');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('rejects field referencing non-existent column with DATA_FIELD_MISSING code', () => {
|
|
156
|
-
const result = validateSpec({
|
|
157
|
-
mark: 'line',
|
|
158
|
-
data: validLineData,
|
|
159
|
-
encoding: {
|
|
160
|
-
x: { field: 'nonexistent', type: 'temporal' },
|
|
161
|
-
y: { field: 'value', type: 'quantitative' },
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
expect(result.valid).toBe(false);
|
|
165
|
-
const fieldError = result.errors.find((e) =>
|
|
166
|
-
e.message.includes('"nonexistent" does not exist'),
|
|
167
|
-
);
|
|
168
|
-
expect(fieldError).toBeDefined();
|
|
169
|
-
expect(fieldError!.code).toBe('DATA_FIELD_MISSING');
|
|
170
|
-
expect(fieldError!.suggestion).toContain('date');
|
|
171
|
-
expect(fieldError!.suggestion).toContain('value');
|
|
172
|
-
expect(fieldError!.suggestion).toContain('country');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('rejects invalid field type with INVALID_VALUE code', () => {
|
|
176
|
-
const result = validateSpec({
|
|
177
|
-
mark: 'line',
|
|
178
|
-
data: validLineData,
|
|
179
|
-
encoding: {
|
|
180
|
-
x: { field: 'date', type: 'bogus' },
|
|
181
|
-
y: { field: 'value', type: 'quantitative' },
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
expect(result.valid).toBe(false);
|
|
185
|
-
const typeError = result.errors.find((e) => e.message.includes('"bogus" is not valid'));
|
|
186
|
-
expect(typeError).toBeDefined();
|
|
187
|
-
expect(typeError!.code).toBe('INVALID_VALUE');
|
|
188
|
-
expect(typeError!.suggestion).toContain('quantitative');
|
|
189
|
-
expect(typeError!.suggestion).toContain('temporal');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('rejects disallowed type for channel with ENCODING_MISMATCH code', () => {
|
|
193
|
-
const result = validateSpec({
|
|
194
|
-
mark: 'line',
|
|
195
|
-
data: validLineData,
|
|
196
|
-
encoding: {
|
|
197
|
-
x: { field: 'date', type: 'quantitative' },
|
|
198
|
-
y: { field: 'value', type: 'quantitative' },
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
expect(result.valid).toBe(false);
|
|
202
|
-
const mismatchError = result.errors.find((e) => e.message.includes('does not accept type'));
|
|
203
|
-
expect(mismatchError).toBeDefined();
|
|
204
|
-
expect(mismatchError!.code).toBe('ENCODING_MISMATCH');
|
|
205
|
-
expect(mismatchError!.suggestion).toContain('Change');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('catches temporal field with non-date values with ENCODING_MISMATCH', () => {
|
|
209
|
-
const result = validateSpec({
|
|
210
|
-
mark: 'line',
|
|
211
|
-
data: [
|
|
212
|
-
{ x: 'not-a-date', y: 10 },
|
|
213
|
-
{ x: 'also-not-a-date', y: 20 },
|
|
214
|
-
],
|
|
215
|
-
encoding: {
|
|
216
|
-
x: { field: 'x', type: 'temporal' },
|
|
217
|
-
y: { field: 'y', type: 'quantitative' },
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
expect(result.valid).toBe(false);
|
|
221
|
-
const dateError = result.errors.find((e) => e.message.includes('non-date values'));
|
|
222
|
-
expect(dateError).toBeDefined();
|
|
223
|
-
expect(dateError!.code).toBe('ENCODING_MISMATCH');
|
|
224
|
-
expect(dateError!.suggestion).toContain('nominal');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('catches quantitative field with non-numeric values with ENCODING_MISMATCH', () => {
|
|
228
|
-
const result = validateSpec({
|
|
229
|
-
mark: 'point',
|
|
230
|
-
data: [
|
|
231
|
-
{ x: 'hello', y: 10 },
|
|
232
|
-
{ x: 'world', y: 20 },
|
|
233
|
-
],
|
|
234
|
-
encoding: {
|
|
235
|
-
x: { field: 'x', type: 'quantitative' },
|
|
236
|
-
y: { field: 'y', type: 'quantitative' },
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
expect(result.valid).toBe(false);
|
|
240
|
-
const numError = result.errors.find((e) => e.message.includes('non-numeric values'));
|
|
241
|
-
expect(numError).toBeDefined();
|
|
242
|
-
expect(numError!.code).toBe('ENCODING_MISMATCH');
|
|
243
|
-
expect(numError!.suggestion).toContain('nominal');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('rejects invalid darkMode with INVALID_VALUE code', () => {
|
|
247
|
-
const result = validateSpec({
|
|
248
|
-
...validLineSpec,
|
|
249
|
-
darkMode: 'maybe',
|
|
250
|
-
});
|
|
251
|
-
expect(result.valid).toBe(false);
|
|
252
|
-
const dmError = result.errors.find((e) => e.message.includes('darkMode'));
|
|
253
|
-
expect(dmError).toBeDefined();
|
|
254
|
-
expect(dmError!.code).toBe('INVALID_VALUE');
|
|
255
|
-
expect(dmError!.suggestion).toContain('auto');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('rejects missing encoding channel field with MISSING_FIELD code', () => {
|
|
259
|
-
const result = validateSpec({
|
|
260
|
-
mark: 'bar',
|
|
261
|
-
data: [{ a: 1, b: 2 }],
|
|
262
|
-
encoding: {
|
|
263
|
-
x: { type: 'quantitative' },
|
|
264
|
-
y: { field: 'a', type: 'nominal' },
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
expect(result.valid).toBe(false);
|
|
268
|
-
const fieldError = result.errors.find((e) =>
|
|
269
|
-
e.message.includes('must have a "field" string'),
|
|
270
|
-
);
|
|
271
|
-
expect(fieldError).toBeDefined();
|
|
272
|
-
expect(fieldError!.code).toBe('MISSING_FIELD');
|
|
273
|
-
expect(fieldError!.suggestion).toContain('a');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('accepts tooltip as an array of valid encoding channels', () => {
|
|
277
|
-
const result = validateSpec({
|
|
278
|
-
mark: 'bar',
|
|
279
|
-
data: [{ a: 1, b: 2, c: 3 }],
|
|
280
|
-
encoding: {
|
|
281
|
-
x: { field: 'a', type: 'quantitative' },
|
|
282
|
-
y: { field: 'b', type: 'nominal' },
|
|
283
|
-
tooltip: [
|
|
284
|
-
{ field: 'a', type: 'quantitative' },
|
|
285
|
-
{ field: 'c', type: 'quantitative' },
|
|
286
|
-
],
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
expect(result.valid).toBe(true);
|
|
290
|
-
expect(result.errors).toHaveLength(0);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('rejects tooltip array element with missing field', () => {
|
|
294
|
-
const result = validateSpec({
|
|
295
|
-
mark: 'bar',
|
|
296
|
-
data: [{ a: 1, b: 2 }],
|
|
297
|
-
encoding: {
|
|
298
|
-
x: { field: 'a', type: 'quantitative' },
|
|
299
|
-
y: { field: 'b', type: 'nominal' },
|
|
300
|
-
tooltip: [{ field: 'a', type: 'quantitative' }, { type: 'quantitative' }],
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
expect(result.valid).toBe(false);
|
|
304
|
-
const err = result.errors.find((e) => e.message.includes('tooltip[1]'));
|
|
305
|
-
expect(err).toBeDefined();
|
|
306
|
-
expect(err!.code).toBe('MISSING_FIELD');
|
|
307
|
-
expect(err!.path).toBe('encoding.tooltip[1].field');
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe('range marks', () => {
|
|
312
|
-
const rangeData = [
|
|
313
|
-
{ country: 'Japan', y2000: 81.1, y2024: 87.9 },
|
|
314
|
-
{ country: 'USA', y2000: 79.5, y2024: 81.3 },
|
|
315
|
-
];
|
|
316
|
-
|
|
317
|
-
it('accepts a valid horizontal range spec (x + x2, nominal y)', () => {
|
|
318
|
-
const result = validateSpec({
|
|
319
|
-
mark: 'range',
|
|
320
|
-
data: rangeData,
|
|
321
|
-
encoding: {
|
|
322
|
-
y: { field: 'country', type: 'nominal' },
|
|
323
|
-
x: { field: 'y2000', type: 'quantitative' },
|
|
324
|
-
x2: { field: 'y2024' },
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
expect(result.valid).toBe(true);
|
|
328
|
-
expect(result.errors).toHaveLength(0);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('accepts a valid vertical range spec (y + y2, nominal x)', () => {
|
|
332
|
-
const result = validateSpec({
|
|
333
|
-
mark: 'range',
|
|
334
|
-
data: [{ month: 'Jan', lo: 27, hi: 39 }],
|
|
335
|
-
encoding: {
|
|
336
|
-
x: { field: 'month', type: 'nominal' },
|
|
337
|
-
y: { field: 'lo', type: 'quantitative' },
|
|
338
|
-
y2: { field: 'hi' },
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
expect(result.valid).toBe(true);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('rejects a horizontal range spec without x2, naming encoding.x2', () => {
|
|
345
|
-
const result = validateSpec({
|
|
346
|
-
mark: 'range',
|
|
347
|
-
data: rangeData,
|
|
348
|
-
encoding: {
|
|
349
|
-
y: { field: 'country', type: 'nominal' },
|
|
350
|
-
x: { field: 'y2000', type: 'quantitative' },
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
expect(result.valid).toBe(false);
|
|
354
|
-
const err = result.errors.find((e) => e.path === 'encoding.x2');
|
|
355
|
-
expect(err).toBeDefined();
|
|
356
|
-
expect(err!.code).toBe('MISSING_FIELD');
|
|
357
|
-
expect(err!.message).toContain('encoding.x2');
|
|
358
|
-
expect(err!.suggestion).toContain('x2: { field:');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('rejects a vertical range spec without y2, naming encoding.y2', () => {
|
|
362
|
-
const result = validateSpec({
|
|
363
|
-
mark: 'range',
|
|
364
|
-
data: [{ month: 'Jan', lo: 27, hi: 39 }],
|
|
365
|
-
encoding: {
|
|
366
|
-
x: { field: 'month', type: 'nominal' },
|
|
367
|
-
y: { field: 'lo', type: 'quantitative' },
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
expect(result.valid).toBe(false);
|
|
371
|
-
const err = result.errors.find((e) => e.path === 'encoding.y2');
|
|
372
|
-
expect(err).toBeDefined();
|
|
373
|
-
expect(err!.code).toBe('MISSING_FIELD');
|
|
374
|
-
expect(err!.message).toContain('encoding.y2');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('rejects y2 on a horizontal range spec', () => {
|
|
378
|
-
const result = validateSpec({
|
|
379
|
-
mark: 'range',
|
|
380
|
-
data: rangeData,
|
|
381
|
-
encoding: {
|
|
382
|
-
y: { field: 'country', type: 'nominal' },
|
|
383
|
-
x: { field: 'y2000', type: 'quantitative' },
|
|
384
|
-
x2: { field: 'y2024' },
|
|
385
|
-
y2: { field: 'y2024' },
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
|
-
expect(result.valid).toBe(false);
|
|
389
|
-
const err = result.errors.find((e) => e.path === 'encoding.y2');
|
|
390
|
-
expect(err).toBeDefined();
|
|
391
|
-
expect(err!.code).toBe('INVALID_VALUE');
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it('rejects a range spec without any categorical axis', () => {
|
|
395
|
-
const result = validateSpec({
|
|
396
|
-
mark: 'range',
|
|
397
|
-
data: rangeData,
|
|
398
|
-
encoding: {
|
|
399
|
-
y: { field: 'y2000', type: 'quantitative' },
|
|
400
|
-
x: { field: 'y2024', type: 'quantitative' },
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
expect(result.valid).toBe(false);
|
|
404
|
-
const err = result.errors.find((e) => e.code === 'ENCODING_MISMATCH');
|
|
405
|
-
expect(err).toBeDefined();
|
|
406
|
-
expect(err!.message).toContain('category axis');
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('rejects an unknown mark.style value', () => {
|
|
410
|
-
const result = validateSpec({
|
|
411
|
-
mark: { type: 'range', style: 'ribbon' },
|
|
412
|
-
data: rangeData,
|
|
413
|
-
encoding: {
|
|
414
|
-
y: { field: 'country', type: 'nominal' },
|
|
415
|
-
x: { field: 'y2000', type: 'quantitative' },
|
|
416
|
-
x2: { field: 'y2024' },
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
expect(result.valid).toBe(false);
|
|
420
|
-
const err = result.errors.find((e) => e.path === 'mark.style');
|
|
421
|
-
expect(err).toBeDefined();
|
|
422
|
-
expect(err!.code).toBe('INVALID_VALUE');
|
|
423
|
-
expect(err!.message).toContain('dumbbell, arrow, bar');
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
describe('table specs', () => {
|
|
428
|
-
it('accepts a valid table spec', () => {
|
|
429
|
-
const result = validateSpec({
|
|
430
|
-
type: 'table',
|
|
431
|
-
data: [{ name: 'Alice', age: 30 }],
|
|
432
|
-
columns: [{ key: 'name' }, { key: 'age' }],
|
|
433
|
-
});
|
|
434
|
-
expect(result.valid).toBe(true);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('rejects table without data', () => {
|
|
438
|
-
const result = validateSpec({
|
|
439
|
-
type: 'table',
|
|
440
|
-
columns: [{ key: 'name' }],
|
|
441
|
-
});
|
|
442
|
-
expect(result.valid).toBe(false);
|
|
443
|
-
expect(result.errors[0].code).toBe('INVALID_TYPE');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('rejects table with empty data with EMPTY_DATA code', () => {
|
|
447
|
-
const result = validateSpec({
|
|
448
|
-
type: 'table',
|
|
449
|
-
data: [],
|
|
450
|
-
columns: [{ key: 'name' }],
|
|
451
|
-
});
|
|
452
|
-
expect(result.valid).toBe(false);
|
|
453
|
-
expect(result.errors[0].code).toBe('EMPTY_DATA');
|
|
454
|
-
expect(result.errors[0].suggestion).toBeDefined();
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('rejects table without columns with MISSING_FIELD code', () => {
|
|
458
|
-
const result = validateSpec({
|
|
459
|
-
type: 'table',
|
|
460
|
-
data: [{ name: 'Alice' }],
|
|
461
|
-
});
|
|
462
|
-
expect(result.valid).toBe(false);
|
|
463
|
-
expect(result.errors.some((e) => e.message.includes('"columns" array'))).toBe(true);
|
|
464
|
-
expect(result.errors[0].code).toBe('MISSING_FIELD');
|
|
465
|
-
expect(result.errors[0].suggestion).toContain('columns');
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('rejects table column referencing non-existent field with DATA_FIELD_MISSING', () => {
|
|
469
|
-
const result = validateSpec({
|
|
470
|
-
type: 'table',
|
|
471
|
-
data: [{ name: 'Alice', age: 30 }],
|
|
472
|
-
columns: [{ key: 'nonexistent' }],
|
|
473
|
-
});
|
|
474
|
-
expect(result.valid).toBe(false);
|
|
475
|
-
const colError = result.errors.find((e) =>
|
|
476
|
-
e.message.includes('"nonexistent" does not exist'),
|
|
477
|
-
);
|
|
478
|
-
expect(colError).toBeDefined();
|
|
479
|
-
expect(colError!.code).toBe('DATA_FIELD_MISSING');
|
|
480
|
-
expect(colError!.suggestion).toContain('name');
|
|
481
|
-
expect(colError!.suggestion).toContain('age');
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
describe('graph specs', () => {
|
|
486
|
-
it('accepts a valid graph spec', () => {
|
|
487
|
-
const result = validateSpec({
|
|
488
|
-
type: 'graph',
|
|
489
|
-
nodes: [{ id: 'a' }, { id: 'b' }],
|
|
490
|
-
edges: [{ source: 'a', target: 'b' }],
|
|
491
|
-
});
|
|
492
|
-
expect(result.valid).toBe(true);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('rejects graph without nodes with MISSING_FIELD code', () => {
|
|
496
|
-
const result = validateSpec({
|
|
497
|
-
type: 'graph',
|
|
498
|
-
edges: [],
|
|
499
|
-
});
|
|
500
|
-
expect(result.valid).toBe(false);
|
|
501
|
-
const nodeError = result.errors.find((e) => e.message.includes('"nodes"'));
|
|
502
|
-
expect(nodeError).toBeDefined();
|
|
503
|
-
expect(nodeError!.code).toBe('MISSING_FIELD');
|
|
504
|
-
expect(nodeError!.suggestion).toContain('id');
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it('rejects graph with empty nodes with EMPTY_DATA code', () => {
|
|
508
|
-
const result = validateSpec({
|
|
509
|
-
type: 'graph',
|
|
510
|
-
nodes: [],
|
|
511
|
-
edges: [],
|
|
512
|
-
});
|
|
513
|
-
expect(result.valid).toBe(false);
|
|
514
|
-
const emptyError = result.errors.find((e) => e.message.includes('non-empty'));
|
|
515
|
-
expect(emptyError).toBeDefined();
|
|
516
|
-
expect(emptyError!.code).toBe('EMPTY_DATA');
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it('rejects graph without edges with MISSING_FIELD code', () => {
|
|
520
|
-
const result = validateSpec({
|
|
521
|
-
type: 'graph',
|
|
522
|
-
nodes: [{ id: 'a' }],
|
|
523
|
-
});
|
|
524
|
-
expect(result.valid).toBe(false);
|
|
525
|
-
const edgeError = result.errors.find((e) => e.message.includes('"edges"'));
|
|
526
|
-
expect(edgeError).toBeDefined();
|
|
527
|
-
expect(edgeError!.code).toBe('MISSING_FIELD');
|
|
528
|
-
expect(edgeError!.suggestion).toContain('edges');
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
describe('error codes and suggestions', () => {
|
|
533
|
-
it('every error has a code', () => {
|
|
534
|
-
// Test several invalid specs and verify all errors have codes
|
|
535
|
-
const results = [
|
|
536
|
-
validateSpec(null),
|
|
537
|
-
validateSpec({ data: [] }),
|
|
538
|
-
validateSpec({ mark: 'waterfall' }),
|
|
539
|
-
validateSpec({ mark: 'line', data: [] }),
|
|
540
|
-
validateSpec({
|
|
541
|
-
mark: 'line',
|
|
542
|
-
data: [{ x: 1 }],
|
|
543
|
-
encoding: { x: { field: 'missing', type: 'quantitative' } },
|
|
544
|
-
}),
|
|
545
|
-
];
|
|
546
|
-
|
|
547
|
-
for (const result of results) {
|
|
548
|
-
for (const error of result.errors) {
|
|
549
|
-
expect(error.code).toBeDefined();
|
|
550
|
-
expect(typeof error.code).toBe('string');
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('every error has a suggestion', () => {
|
|
556
|
-
const results = [
|
|
557
|
-
validateSpec(null),
|
|
558
|
-
validateSpec({ data: [] }),
|
|
559
|
-
validateSpec({ mark: 'waterfall' }),
|
|
560
|
-
validateSpec({ mark: 'line', data: [] }),
|
|
561
|
-
];
|
|
562
|
-
|
|
563
|
-
for (const result of results) {
|
|
564
|
-
for (const error of result.errors) {
|
|
565
|
-
expect(error.suggestion).toBeDefined();
|
|
566
|
-
expect(typeof error.suggestion).toBe('string');
|
|
567
|
-
expect(error.suggestion!.length).toBeGreaterThan(0);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('DATA_FIELD_MISSING suggestion lists available fields', () => {
|
|
573
|
-
const result = validateSpec({
|
|
574
|
-
mark: 'bar',
|
|
575
|
-
data: [{ alpha: 1, beta: 2, gamma: 3 }],
|
|
576
|
-
encoding: {
|
|
577
|
-
x: { field: 'nonexistent', type: 'quantitative' },
|
|
578
|
-
y: { field: 'alpha', type: 'nominal' },
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
const fieldError = result.errors.find((e) => e.code === 'DATA_FIELD_MISSING');
|
|
583
|
-
expect(fieldError).toBeDefined();
|
|
584
|
-
expect(fieldError!.suggestion).toContain('alpha');
|
|
585
|
-
expect(fieldError!.suggestion).toContain('beta');
|
|
586
|
-
expect(fieldError!.suggestion).toContain('gamma');
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
describe('transform-created fields', () => {
|
|
591
|
-
it('accepts encoding fields produced by a chained bin + aggregate (histogram)', () => {
|
|
592
|
-
const result = validateSpec({
|
|
593
|
-
mark: 'bar',
|
|
594
|
-
data: [{ hours: 3.1 }, { hours: 3.4 }, { hours: 4.0 }],
|
|
595
|
-
transform: [
|
|
596
|
-
{ bin: { step: 0.5 }, field: 'hours', as: 'binStart' },
|
|
597
|
-
{ aggregate: [{ op: 'count', field: 'hours', as: 'finishers' }], groupby: ['binStart'] },
|
|
598
|
-
],
|
|
599
|
-
encoding: {
|
|
600
|
-
x: { field: 'binStart', type: 'ordinal' },
|
|
601
|
-
y: { field: 'finishers', type: 'quantitative' },
|
|
602
|
-
},
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
expect(result.valid).toBe(true);
|
|
606
|
-
expect(result.errors.find((e) => e.code === 'DATA_FIELD_MISSING')).toBeUndefined();
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('accepts an encoding field produced by a window transform', () => {
|
|
610
|
-
const result = validateSpec({
|
|
611
|
-
mark: 'bar',
|
|
612
|
-
data: [
|
|
613
|
-
{ day: '1', value: 10 },
|
|
614
|
-
{ day: '2', value: 20 },
|
|
615
|
-
],
|
|
616
|
-
transform: [
|
|
617
|
-
{ window: [{ op: 'cumsum', field: 'value', as: 'running' }], sort: [{ field: 'day' }] },
|
|
618
|
-
],
|
|
619
|
-
encoding: {
|
|
620
|
-
x: { field: 'day', type: 'ordinal' },
|
|
621
|
-
y: { field: 'running', type: 'quantitative' },
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
expect(result.valid).toBe(true);
|
|
626
|
-
expect(result.errors.find((e) => e.code === 'DATA_FIELD_MISSING')).toBeUndefined();
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VL-idiom guessability suite.
|
|
3
|
-
*
|
|
4
|
-
* Each fixture in fixtures/vl-parity pairs a Vega-Lite-style spec ("vl") with
|
|
5
|
-
* the canonical openchart spec ("canonical") for one accepted idiom, and
|
|
6
|
-
* asserts they compile to deep-equal ChartLayouts. Layered fixtures
|
|
7
|
-
* (layered: true) run through compileLayer to prove the same sugar works on
|
|
8
|
-
* LayerSpec children.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { dirname, join } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import type { CompileOptions, LayerSpec } from '@opendata-ai/openchart-core';
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
-
import { compileChart, compileLayer } from '../../compile';
|
|
17
|
-
|
|
18
|
-
interface ParityFixture {
|
|
19
|
-
file: string;
|
|
20
|
-
layered?: boolean;
|
|
21
|
-
options: CompileOptions;
|
|
22
|
-
/** Per-side option overrides (e.g. the width/height fixture compiles the VL side in a larger container). */
|
|
23
|
-
vlOptions?: CompileOptions;
|
|
24
|
-
canonicalOptions?: CompileOptions;
|
|
25
|
-
vl: Record<string, unknown>;
|
|
26
|
-
canonical: Record<string, unknown>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'vl-parity');
|
|
30
|
-
const fixtures: ParityFixture[] = readdirSync(fixturesDir)
|
|
31
|
-
.filter((file) => file.endsWith('.json'))
|
|
32
|
-
.sort()
|
|
33
|
-
.map((file) => ({
|
|
34
|
-
file,
|
|
35
|
-
...(JSON.parse(readFileSync(join(fixturesDir, file), 'utf8')) as Omit<ParityFixture, 'file'>),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
warnSpy.mockRestore();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('VL-idiom parity fixtures', () => {
|
|
49
|
-
it('loads at least one flat and one layered fixture', () => {
|
|
50
|
-
expect(fixtures.some((f) => !f.layered)).toBe(true);
|
|
51
|
-
expect(fixtures.some((f) => f.layered)).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
for (const fixture of fixtures) {
|
|
55
|
-
it(`${fixture.file}: VL form compiles to the same layout as the canonical form`, () => {
|
|
56
|
-
const vlLayout = fixture.layered
|
|
57
|
-
? compileLayer(fixture.vl as unknown as LayerSpec, fixture.vlOptions ?? fixture.options)
|
|
58
|
-
: compileChart(fixture.vl, fixture.vlOptions ?? fixture.options);
|
|
59
|
-
const canonicalLayout = fixture.layered
|
|
60
|
-
? compileLayer(
|
|
61
|
-
fixture.canonical as unknown as LayerSpec,
|
|
62
|
-
fixture.canonicalOptions ?? fixture.options,
|
|
63
|
-
)
|
|
64
|
-
: compileChart(fixture.canonical, fixture.canonicalOptions ?? fixture.options);
|
|
65
|
-
expect(vlLayout).toEqual(canonicalLayout);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
});
|