@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,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Levenshtein "did you mean" repair hints for misspelled field references.
|
|
3
|
-
*
|
|
4
|
-
* When a spec references a field that is not a column in the provided data,
|
|
5
|
-
* validation suggests the nearest actual column (by edit distance) so an LLM
|
|
6
|
-
* or author can self-correct a typo in one step. Short/unrelated names must
|
|
7
|
-
* NOT produce a spurious suggestion.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, expect, it } from 'vitest';
|
|
11
|
-
import { validateSpec } from '../validate';
|
|
12
|
-
|
|
13
|
-
const data = [
|
|
14
|
-
{ region: 'West', revenue: 100, quarter: '2024-Q1' },
|
|
15
|
-
{ region: 'East', revenue: 200, quarter: '2024-Q2' },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
/** Pull the suggestion for the field-missing error on the given path. */
|
|
19
|
-
function suggestionFor(result: ReturnType<typeof validateSpec>, path: string): string {
|
|
20
|
-
const error = result.errors.find((e) => e.path === path);
|
|
21
|
-
return error?.suggestion ?? '';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('did-you-mean field suggestions', () => {
|
|
25
|
-
it('suggests the nearest column for a one-edit typo in an encoding field', () => {
|
|
26
|
-
const result = validateSpec({
|
|
27
|
-
mark: 'bar',
|
|
28
|
-
data,
|
|
29
|
-
encoding: {
|
|
30
|
-
x: { field: 'region', type: 'nominal' },
|
|
31
|
-
y: { field: 'reveune', type: 'quantitative' }, // typo of "revenue"
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
expect(result.valid).toBe(false);
|
|
35
|
-
const suggestion = suggestionFor(result, 'encoding.y.field');
|
|
36
|
-
expect(suggestion).toContain('Did you mean "revenue"?');
|
|
37
|
-
// The full available-column list is still present alongside the hint.
|
|
38
|
-
expect(suggestion).toContain('revenue');
|
|
39
|
-
expect(suggestion).toContain('region');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('does not invent a suggestion for a field with no close match', () => {
|
|
43
|
-
const result = validateSpec({
|
|
44
|
-
mark: 'bar',
|
|
45
|
-
data,
|
|
46
|
-
encoding: {
|
|
47
|
-
x: { field: 'region', type: 'nominal' },
|
|
48
|
-
y: { field: 'xyzqwerty', type: 'quantitative' },
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
expect(result.valid).toBe(false);
|
|
52
|
-
const suggestion = suggestionFor(result, 'encoding.y.field');
|
|
53
|
-
expect(suggestion).not.toContain('Did you mean');
|
|
54
|
-
// Still lists the real columns so the author can pick the right one.
|
|
55
|
-
expect(suggestion).toContain('revenue');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('suggests the nearest column for a misspelled table column key', () => {
|
|
59
|
-
const result = validateSpec({
|
|
60
|
-
type: 'table',
|
|
61
|
-
data,
|
|
62
|
-
columns: [{ key: 'quater' }], // typo of "quarter"
|
|
63
|
-
});
|
|
64
|
-
expect(result.valid).toBe(false);
|
|
65
|
-
const suggestion = suggestionFor(result, 'columns[0].key');
|
|
66
|
-
expect(suggestion).toContain('Did you mean "quarter"?');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('suggests the nearest column for a misspelled sankey encoding field', () => {
|
|
70
|
-
const result = validateSpec({
|
|
71
|
-
type: 'sankey',
|
|
72
|
-
data: [{ source: 'A', target: 'B', value: 10 }],
|
|
73
|
-
encoding: {
|
|
74
|
-
source: { field: 'sorce', type: 'nominal' }, // typo of "source"
|
|
75
|
-
target: { field: 'target', type: 'nominal' },
|
|
76
|
-
value: { field: 'value', type: 'quantitative' },
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
expect(result.valid).toBe(false);
|
|
80
|
-
const suggestion = suggestionFor(result, 'encoding.source.field');
|
|
81
|
-
expect(suggestion).toContain('Did you mean "source"?');
|
|
82
|
-
expect(suggestion).toContain('target');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('suggests the nearest column for a misspelled tilemap encoding field', () => {
|
|
86
|
-
const result = validateSpec({
|
|
87
|
-
type: 'tilemap',
|
|
88
|
-
data: [{ state: 'CA', value: 12000 }],
|
|
89
|
-
encoding: {
|
|
90
|
-
state: { field: 'state', type: 'nominal' },
|
|
91
|
-
value: { field: 'vlaue', type: 'quantitative' }, // typo of "value"
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
expect(result.valid).toBe(false);
|
|
95
|
-
const suggestion = suggestionFor(result, 'encoding.value.field');
|
|
96
|
-
expect(suggestion).toContain('Did you mean "value"?');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('suggests the nearest column for a misspelled barlist encoding field', () => {
|
|
100
|
-
const result = validateSpec({
|
|
101
|
-
type: 'barlist',
|
|
102
|
-
data: [{ label: 'A', value: 42 }],
|
|
103
|
-
encoding: {
|
|
104
|
-
label: { field: 'lable', type: 'nominal' }, // typo of "label"
|
|
105
|
-
value: { field: 'value', type: 'quantitative' },
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
expect(result.valid).toBe(false);
|
|
109
|
-
const suggestion = suggestionFor(result, 'encoding.label.field');
|
|
110
|
-
expect(suggestion).toContain('Did you mean "label"?');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('suggests the nearest column for a misspelled optional barlist channel field', () => {
|
|
114
|
-
const result = validateSpec({
|
|
115
|
-
type: 'barlist',
|
|
116
|
-
data: [{ label: 'A', value: 42, group: 'x' }],
|
|
117
|
-
encoding: {
|
|
118
|
-
label: { field: 'label', type: 'nominal' },
|
|
119
|
-
value: { field: 'value', type: 'quantitative' },
|
|
120
|
-
color: { field: 'gruop', type: 'nominal' }, // typo of "group"
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
expect(result.valid).toBe(false);
|
|
124
|
-
const suggestion = suggestionFor(result, 'encoding.color.field');
|
|
125
|
-
expect(suggestion).toContain('Did you mean "group"?');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('suggests the nearest node field for a misspelled graph encoding field', () => {
|
|
129
|
-
const result = validateSpec({
|
|
130
|
-
type: 'graph',
|
|
131
|
-
nodes: [{ id: 'a', category: 'x' }],
|
|
132
|
-
edges: [],
|
|
133
|
-
encoding: {
|
|
134
|
-
nodeColor: { field: 'catgory', type: 'nominal' }, // typo of "category"
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
expect(result.valid).toBe(false);
|
|
138
|
-
const suggestion = suggestionFor(result, 'encoding.nodeColor.field');
|
|
139
|
-
expect(suggestion).toContain('Did you mean "category"?');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('suggests the nearest node id for a misspelled graph edge endpoint', () => {
|
|
143
|
-
const result = validateSpec({
|
|
144
|
-
type: 'graph',
|
|
145
|
-
nodes: [{ id: 'alpha' }, { id: 'beta' }],
|
|
146
|
-
edges: [{ source: 'alpha', target: 'betta' }], // typo of "beta"
|
|
147
|
-
});
|
|
148
|
-
expect(result.valid).toBe(false);
|
|
149
|
-
const suggestion = suggestionFor(result, 'edges[0].target');
|
|
150
|
-
expect(suggestion).toContain('Did you mean "beta"?');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('does not match on a short unrelated field where the edit distance exceeds the threshold', () => {
|
|
154
|
-
const shortData = [
|
|
155
|
-
{ a: 1, b: 2 },
|
|
156
|
-
{ a: 3, b: 4 },
|
|
157
|
-
];
|
|
158
|
-
const result = validateSpec({
|
|
159
|
-
mark: 'bar',
|
|
160
|
-
data: shortData,
|
|
161
|
-
encoding: {
|
|
162
|
-
x: { field: 'zzz', type: 'nominal' },
|
|
163
|
-
y: { field: 'a', type: 'quantitative' },
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
expect(result.valid).toBe(false);
|
|
167
|
-
const suggestion = suggestionFor(result, 'encoding.x.field');
|
|
168
|
-
expect(suggestion).not.toContain('Did you mean');
|
|
169
|
-
});
|
|
170
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { validateSpec } from '../validate';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Test data
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const data = [
|
|
9
|
-
{ date: '2020', country: 'Germany', value: 100 },
|
|
10
|
-
{ date: '2020', country: 'France', value: 90 },
|
|
11
|
-
{ date: '2020', country: 'Italy', value: 80 },
|
|
12
|
-
{ date: '2021', country: 'Germany', value: 110 },
|
|
13
|
-
{ date: '2021', country: 'France', value: 95 },
|
|
14
|
-
{ date: '2021', country: 'Italy', value: 85 },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Tests
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
describe('validateSpec — highlight channel', () => {
|
|
22
|
-
it('accepts highlight on a nominal color channel with valid values', () => {
|
|
23
|
-
const result = validateSpec({
|
|
24
|
-
mark: 'line',
|
|
25
|
-
data,
|
|
26
|
-
encoding: {
|
|
27
|
-
x: { field: 'date', type: 'temporal' },
|
|
28
|
-
y: { field: 'value', type: 'quantitative' },
|
|
29
|
-
color: { field: 'country', type: 'nominal', highlight: ['Germany'] },
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
expect(result.valid).toBe(true);
|
|
33
|
-
expect(result.errors).toHaveLength(0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('accepts highlight as a single string (not array)', () => {
|
|
37
|
-
const result = validateSpec({
|
|
38
|
-
mark: 'line',
|
|
39
|
-
data,
|
|
40
|
-
encoding: {
|
|
41
|
-
x: { field: 'date', type: 'temporal' },
|
|
42
|
-
y: { field: 'value', type: 'quantitative' },
|
|
43
|
-
color: { field: 'country', type: 'nominal', highlight: 'Germany' },
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
expect(result.valid).toBe(true);
|
|
47
|
-
expect(result.errors).toHaveLength(0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('accepts highlight with multiple valid values', () => {
|
|
51
|
-
const result = validateSpec({
|
|
52
|
-
mark: 'line',
|
|
53
|
-
data,
|
|
54
|
-
encoding: {
|
|
55
|
-
x: { field: 'date', type: 'temporal' },
|
|
56
|
-
y: { field: 'value', type: 'quantitative' },
|
|
57
|
-
color: { field: 'country', type: 'nominal', highlight: ['Germany', 'France'] },
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
expect(result.valid).toBe(true);
|
|
61
|
-
expect(result.errors).toHaveLength(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('rejects highlight on a quantitative color channel', () => {
|
|
65
|
-
const result = validateSpec({
|
|
66
|
-
mark: 'point',
|
|
67
|
-
data: [
|
|
68
|
-
{ x: 1, y: 2, temp: 30 },
|
|
69
|
-
{ x: 3, y: 4, temp: 50 },
|
|
70
|
-
],
|
|
71
|
-
encoding: {
|
|
72
|
-
x: { field: 'x', type: 'quantitative' },
|
|
73
|
-
y: { field: 'y', type: 'quantitative' },
|
|
74
|
-
color: { field: 'temp', type: 'quantitative', highlight: ['30'] },
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
expect(result.valid).toBe(false);
|
|
78
|
-
const highlightError = result.errors.find((e) => e.message.toLowerCase().includes('highlight'));
|
|
79
|
-
expect(highlightError).toBeDefined();
|
|
80
|
-
// Implementation may use ENCODING_MISMATCH or INVALID_VALUE
|
|
81
|
-
expect(['ENCODING_MISMATCH', 'INVALID_VALUE']).toContain(highlightError!.code);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('does not reject highlight with unknown values at the validation level', () => {
|
|
85
|
-
// Unknown highlight values are a warning (surfaced during normalization),
|
|
86
|
-
// not a validation error. The spec should still pass validation.
|
|
87
|
-
const result = validateSpec({
|
|
88
|
-
mark: 'line',
|
|
89
|
-
data,
|
|
90
|
-
encoding: {
|
|
91
|
-
x: { field: 'date', type: 'temporal' },
|
|
92
|
-
y: { field: 'value', type: 'quantitative' },
|
|
93
|
-
color: { field: 'country', type: 'nominal', highlight: ['Spain'] },
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
expect(result.valid).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('does not produce highlight errors when no highlight is specified', () => {
|
|
100
|
-
const result = validateSpec({
|
|
101
|
-
mark: 'line',
|
|
102
|
-
data,
|
|
103
|
-
encoding: {
|
|
104
|
-
x: { field: 'date', type: 'temporal' },
|
|
105
|
-
y: { field: 'value', type: 'quantitative' },
|
|
106
|
-
color: { field: 'country', type: 'nominal' },
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
expect(result.valid).toBe(true);
|
|
110
|
-
expect(result.errors).toHaveLength(0);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Near-miss validation messages for VL idioms that are unsupported by
|
|
3
|
-
* decision. Each rejection must point the author at the openchart
|
|
4
|
-
* equivalent instead of failing with a generic type error.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from 'vitest';
|
|
8
|
-
import { validateSpec } from '../validate';
|
|
9
|
-
|
|
10
|
-
const encoding = {
|
|
11
|
-
x: { field: 'cat', type: 'nominal' },
|
|
12
|
-
y: { field: 'value', type: 'quantitative' },
|
|
13
|
-
};
|
|
14
|
-
const data = [
|
|
15
|
-
{ cat: 'A', value: 10 },
|
|
16
|
-
{ cat: 'B', value: 20 },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
describe('near-miss validation messages', () => {
|
|
20
|
-
it('rejects data.url with a pointer to inline data', () => {
|
|
21
|
-
const result = validateSpec({ mark: 'bar', data: { url: '/data.json' }, encoding });
|
|
22
|
-
expect(result.valid).toBe(false);
|
|
23
|
-
const error = result.errors.find((e) => e.path === 'data.url');
|
|
24
|
-
expect(error?.message).toContain('does not fetch remote data');
|
|
25
|
-
expect(error?.suggestion).toContain('inline');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('rejects the string form of calculate with the structured equivalent', () => {
|
|
29
|
-
const result = validateSpec({
|
|
30
|
-
mark: 'bar',
|
|
31
|
-
data,
|
|
32
|
-
transform: [{ calculate: 'datum.a / datum.b', as: 'ratio' }],
|
|
33
|
-
encoding,
|
|
34
|
-
});
|
|
35
|
-
expect(result.valid).toBe(false);
|
|
36
|
-
const error = result.errors.find((e) => e.path === 'transform[0].calculate');
|
|
37
|
-
expect(error?.message).toContain('structured expression object');
|
|
38
|
-
expect(error?.suggestion).toContain('calculate: { op:');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('rejects hconcat/vconcat with the side-by-side containers suggestion', () => {
|
|
42
|
-
for (const key of ['hconcat', 'vconcat']) {
|
|
43
|
-
const result = validateSpec({ [key]: [] });
|
|
44
|
-
expect(result.valid).toBe(false);
|
|
45
|
-
const error = result.errors.find((e) => e.path === key);
|
|
46
|
-
expect(error?.message).toContain(`"${key}" composition is not supported`);
|
|
47
|
-
expect(error?.suggestion).toContain('facet');
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('rejects the top-level facet operator with the facet channel suggestion', () => {
|
|
52
|
-
const result = validateSpec({
|
|
53
|
-
facet: { field: 'cat', type: 'nominal' },
|
|
54
|
-
spec: { mark: 'bar', encoding },
|
|
55
|
-
});
|
|
56
|
-
expect(result.valid).toBe(false);
|
|
57
|
-
const error = result.errors.find((e) => e.path === 'facet');
|
|
58
|
-
expect(error?.message).toContain('"facet" operator is not supported');
|
|
59
|
-
expect(error?.suggestion).toContain('encoding.facet');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('rejects unknown scheme names with the supported list', () => {
|
|
63
|
-
const result = validateSpec({
|
|
64
|
-
mark: 'bar',
|
|
65
|
-
data,
|
|
66
|
-
encoding: {
|
|
67
|
-
...encoding,
|
|
68
|
-
color: { field: 'value', type: 'quantitative', scale: { scheme: 'viridis' } },
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
expect(result.valid).toBe(false);
|
|
72
|
-
const error = result.errors.find((e) => e.path === 'encoding.color.scale.scheme');
|
|
73
|
-
expect(error?.message).toContain('"viridis" is not a supported scheme name');
|
|
74
|
-
expect(error?.suggestion).toContain('blues');
|
|
75
|
-
expect(error?.suggestion).toContain('scale.range');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('accepts known scheme names (resolved by sugar before validation on the compile path)', () => {
|
|
79
|
-
const result = validateSpec({
|
|
80
|
-
mark: 'bar',
|
|
81
|
-
data,
|
|
82
|
-
encoding: {
|
|
83
|
-
...encoding,
|
|
84
|
-
color: { field: 'value', type: 'quantitative', scale: { scheme: 'blues' } },
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
expect(result.errors.filter((e) => e.path === 'encoding.color.scale.scheme')).toHaveLength(0);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('scale.scheme on non-chart specs', () => {
|
|
92
|
-
it('rejects any scheme on a sankey color channel as dead config', () => {
|
|
93
|
-
// Sankey never reads scale.scheme (node colors cycle theme.colors.categorical),
|
|
94
|
-
// so even a known name like "blues" must not be silently accepted.
|
|
95
|
-
for (const scheme of ['viridis', 'blues']) {
|
|
96
|
-
const result = validateSpec({
|
|
97
|
-
type: 'sankey',
|
|
98
|
-
data: [{ source: 'A', target: 'B', value: 10 }],
|
|
99
|
-
encoding: {
|
|
100
|
-
source: { field: 'source', type: 'nominal' },
|
|
101
|
-
target: { field: 'target', type: 'nominal' },
|
|
102
|
-
value: { field: 'value', type: 'quantitative' },
|
|
103
|
-
color: { field: 'source', type: 'nominal', scale: { scheme } },
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
expect(result.valid).toBe(false);
|
|
107
|
-
const error = result.errors.find((e) => e.path === 'encoding.color.scale.scheme');
|
|
108
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
109
|
-
expect(error?.message).toContain('has no effect');
|
|
110
|
-
expect(error?.suggestion).toContain('theme.colors.categorical');
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('rejects any scheme on a tilemap value channel, pointing at the palette prop', () => {
|
|
115
|
-
const result = validateSpec({
|
|
116
|
-
type: 'tilemap',
|
|
117
|
-
data: [{ state: 'CA', value: 12000 }],
|
|
118
|
-
encoding: {
|
|
119
|
-
state: { field: 'state', type: 'nominal' },
|
|
120
|
-
value: { field: 'value', type: 'quantitative', scale: { scheme: 'viridis' } },
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
expect(result.valid).toBe(false);
|
|
124
|
-
const error = result.errors.find((e) => e.path === 'encoding.value.scale.scheme');
|
|
125
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
126
|
-
expect(error?.message).toContain('has no effect');
|
|
127
|
-
expect(error?.suggestion).toContain('palette: "green"');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('rejects any scheme on a barlist color channel as dead config', () => {
|
|
131
|
-
const result = validateSpec({
|
|
132
|
-
type: 'barlist',
|
|
133
|
-
data: [{ label: 'A', value: 42, group: 'x' }],
|
|
134
|
-
encoding: {
|
|
135
|
-
label: { field: 'label', type: 'nominal' },
|
|
136
|
-
value: { field: 'value', type: 'quantitative' },
|
|
137
|
-
color: { field: 'group', type: 'nominal', scale: { scheme: 'viridis' } },
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
expect(result.valid).toBe(false);
|
|
141
|
-
const error = result.errors.find((e) => e.path === 'encoding.color.scale.scheme');
|
|
142
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
143
|
-
expect(error?.message).toContain('has no effect');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('rejects any scheme on a graph nodeColor channel as dead config', () => {
|
|
147
|
-
// Graph never reads scale.scheme (encoding.ts consumes only
|
|
148
|
-
// scale.range/scale.domain), so even a known name must not pass.
|
|
149
|
-
for (const scheme of ['viridis', 'blues']) {
|
|
150
|
-
const result = validateSpec({
|
|
151
|
-
type: 'graph',
|
|
152
|
-
nodes: [{ id: 'a', category: 'x' }],
|
|
153
|
-
edges: [],
|
|
154
|
-
encoding: {
|
|
155
|
-
nodeColor: { field: 'category', type: 'nominal', scale: { scheme } },
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
expect(result.valid).toBe(false);
|
|
159
|
-
const error = result.errors.find((e) => e.path === 'encoding.nodeColor.scale.scheme');
|
|
160
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
161
|
-
expect(error?.message).toContain('has no effect');
|
|
162
|
-
expect(error?.suggestion).toContain('scale.range');
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('scale.scheme on map specs', () => {
|
|
168
|
-
// Minimal valid TopoJSON topology with one feature
|
|
169
|
-
const topo = {
|
|
170
|
-
type: 'Topology',
|
|
171
|
-
objects: {
|
|
172
|
-
things: {
|
|
173
|
-
type: 'GeometryCollection',
|
|
174
|
-
geometries: [{ type: 'Polygon', arcs: [[0]], id: '01' }],
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
arcs: [
|
|
178
|
-
[
|
|
179
|
-
[0, 0],
|
|
180
|
-
[0, 1],
|
|
181
|
-
[1, 0],
|
|
182
|
-
[-1, -1],
|
|
183
|
-
],
|
|
184
|
-
],
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
it('rejects a scheme the map palette set does not support', () => {
|
|
188
|
-
const result = validateSpec({
|
|
189
|
-
type: 'map',
|
|
190
|
-
geo: { features: topo },
|
|
191
|
-
data: [{ id: '01', value: 5 }],
|
|
192
|
-
encoding: {
|
|
193
|
-
key: { field: 'id' },
|
|
194
|
-
color: { field: 'value', type: 'quantitative', scale: { scheme: 'viridis' } },
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
expect(result.valid).toBe(false);
|
|
198
|
-
const error = result.errors.find((e) => e.path === 'encoding.color.scale.scheme');
|
|
199
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
200
|
-
expect(error?.message).toContain('"viridis" is not a supported map palette');
|
|
201
|
-
expect(error?.suggestion).toContain('green');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('accepts a supported sequential palette on a map color channel', () => {
|
|
205
|
-
const result = validateSpec({
|
|
206
|
-
type: 'map',
|
|
207
|
-
geo: { features: topo },
|
|
208
|
-
data: [{ id: '01', value: 5 }],
|
|
209
|
-
encoding: {
|
|
210
|
-
key: { field: 'id' },
|
|
211
|
-
color: { field: 'value', type: 'quantitative', scale: { scheme: 'green' } },
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
expect(result.errors.filter((e) => e.path === 'encoding.color.scale.scheme')).toHaveLength(0);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('rejects any scheme on a categorical map color channel as dead config', () => {
|
|
218
|
-
const result = validateSpec({
|
|
219
|
-
type: 'map',
|
|
220
|
-
geo: { features: topo },
|
|
221
|
-
data: [{ id: '01', region: 'south' }],
|
|
222
|
-
encoding: {
|
|
223
|
-
key: { field: 'id' },
|
|
224
|
-
color: { field: 'region', type: 'nominal', scale: { scheme: 'green' } },
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
expect(result.valid).toBe(false);
|
|
228
|
-
const error = result.errors.find((e) => e.path === 'encoding.color.scale.scheme');
|
|
229
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
230
|
-
expect(error?.message).toContain('has no effect');
|
|
231
|
-
expect(error?.suggestion).toContain('scale.range');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('rejects an unsupported scheme on the points layer color channel', () => {
|
|
235
|
-
const result = validateSpec({
|
|
236
|
-
type: 'map',
|
|
237
|
-
geo: { features: topo },
|
|
238
|
-
data: [],
|
|
239
|
-
encoding: { key: { field: 'id' } },
|
|
240
|
-
points: {
|
|
241
|
-
data: [{ lat: 30, lon: -97, score: 1 }],
|
|
242
|
-
latitude: { field: 'lat' },
|
|
243
|
-
longitude: { field: 'lon' },
|
|
244
|
-
color: { field: 'score', type: 'quantitative', scale: { scheme: 'magma' } },
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
const error = result.errors.find((e) => e.path === 'points.color.scale.scheme');
|
|
248
|
-
expect(error?.code).toBe('INVALID_VALUE');
|
|
249
|
-
expect(error?.message).toContain('"magma" is not a supported map palette');
|
|
250
|
-
});
|
|
251
|
-
});
|