@opendata-ai/openchart-engine 1.2.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 +366 -0
- package/dist/index.js +4227 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/__test-fixtures__/specs.ts +124 -0
- package/src/__tests__/axes.test.ts +114 -0
- package/src/__tests__/compile-chart.test.ts +337 -0
- package/src/__tests__/dimensions.test.ts +151 -0
- package/src/__tests__/legend.test.ts +113 -0
- package/src/__tests__/scales.test.ts +109 -0
- package/src/annotations/__tests__/compute.test.ts +454 -0
- package/src/annotations/compute.ts +603 -0
- package/src/charts/__tests__/registry.test.ts +110 -0
- package/src/charts/bar/__tests__/compute.test.ts +294 -0
- package/src/charts/bar/__tests__/labels.test.ts +75 -0
- package/src/charts/bar/compute.ts +205 -0
- package/src/charts/bar/index.ts +33 -0
- package/src/charts/bar/labels.ts +132 -0
- package/src/charts/column/__tests__/compute.test.ts +277 -0
- package/src/charts/column/compute.ts +282 -0
- package/src/charts/column/index.ts +33 -0
- package/src/charts/column/labels.ts +108 -0
- package/src/charts/dot/__tests__/compute.test.ts +344 -0
- package/src/charts/dot/compute.ts +257 -0
- package/src/charts/dot/index.ts +46 -0
- package/src/charts/dot/labels.ts +97 -0
- package/src/charts/line/__tests__/compute.test.ts +437 -0
- package/src/charts/line/__tests__/labels.test.ts +93 -0
- package/src/charts/line/area.ts +288 -0
- package/src/charts/line/compute.ts +177 -0
- package/src/charts/line/index.ts +68 -0
- package/src/charts/line/labels.ts +144 -0
- package/src/charts/pie/__tests__/compute.test.ts +276 -0
- package/src/charts/pie/compute.ts +234 -0
- package/src/charts/pie/index.ts +49 -0
- package/src/charts/pie/labels.ts +142 -0
- package/src/charts/registry.ts +64 -0
- package/src/charts/scatter/__tests__/compute.test.ts +304 -0
- package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
- package/src/charts/scatter/compute.ts +124 -0
- package/src/charts/scatter/index.ts +41 -0
- package/src/charts/scatter/trendline.ts +100 -0
- package/src/charts/utils.ts +120 -0
- package/src/compile.ts +368 -0
- package/src/compiler/__tests__/compile.test.ts +87 -0
- package/src/compiler/__tests__/normalize.test.ts +210 -0
- package/src/compiler/__tests__/validate.test.ts +440 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/normalize.ts +269 -0
- package/src/compiler/types.ts +148 -0
- package/src/compiler/validate.ts +581 -0
- package/src/graphs/__tests__/community.test.ts +228 -0
- package/src/graphs/__tests__/compile-graph.test.ts +315 -0
- package/src/graphs/__tests__/encoding.test.ts +314 -0
- package/src/graphs/community.ts +92 -0
- package/src/graphs/compile-graph.ts +291 -0
- package/src/graphs/encoding.ts +302 -0
- package/src/graphs/types.ts +98 -0
- package/src/index.ts +74 -0
- package/src/layout/axes.ts +194 -0
- package/src/layout/dimensions.ts +199 -0
- package/src/layout/gridlines.ts +84 -0
- package/src/layout/scales.ts +426 -0
- package/src/legend/compute.ts +186 -0
- package/src/tables/__tests__/bar-column.test.ts +147 -0
- package/src/tables/__tests__/category-colors.test.ts +153 -0
- package/src/tables/__tests__/compile-table.test.ts +208 -0
- package/src/tables/__tests__/format-cells.test.ts +126 -0
- package/src/tables/__tests__/heatmap.test.ts +124 -0
- package/src/tables/__tests__/pagination.test.ts +78 -0
- package/src/tables/__tests__/search.test.ts +94 -0
- package/src/tables/__tests__/sort.test.ts +107 -0
- package/src/tables/__tests__/sparkline.test.ts +122 -0
- package/src/tables/bar-column.ts +94 -0
- package/src/tables/category-colors.ts +67 -0
- package/src/tables/compile-table.ts +420 -0
- package/src/tables/format-cells.ts +110 -0
- package/src/tables/heatmap.ts +121 -0
- package/src/tables/pagination.ts +46 -0
- package/src/tables/search.ts +66 -0
- package/src/tables/sort.ts +69 -0
- package/src/tables/sparkline.ts +113 -0
- package/src/tables/utils.ts +16 -0
- package/src/tooltips/__tests__/compute.test.ts +328 -0
- package/src/tooltips/compute.ts +231 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec normalization: fill in defaults and infer types.
|
|
3
|
+
*
|
|
4
|
+
* Takes a validated VizSpec and produces a NormalizedSpec where:
|
|
5
|
+
* - All optional fields have sensible defaults
|
|
6
|
+
* - Chrome strings are converted to ChromeText objects
|
|
7
|
+
* - Encoding types are inferred from data if not specified
|
|
8
|
+
* - Annotations have default styles
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Annotation,
|
|
13
|
+
ChartSpec,
|
|
14
|
+
Chrome,
|
|
15
|
+
ChromeText,
|
|
16
|
+
DataRow,
|
|
17
|
+
Encoding,
|
|
18
|
+
FieldType,
|
|
19
|
+
GraphSpec,
|
|
20
|
+
TableSpec,
|
|
21
|
+
VizSpec,
|
|
22
|
+
} from '@opendata-ai/openchart-core';
|
|
23
|
+
import { isChartSpec, isGraphSpec, isTableSpec } from '@opendata-ai/openchart-core';
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
NormalizedChartSpec,
|
|
27
|
+
NormalizedChrome,
|
|
28
|
+
NormalizedGraphSpec,
|
|
29
|
+
NormalizedSpec,
|
|
30
|
+
NormalizedTableSpec,
|
|
31
|
+
} from './types';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Chrome normalization
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Convert a string | ChromeText | undefined to ChromeText | undefined. */
|
|
38
|
+
function normalizeChromeField(value: string | ChromeText | undefined): ChromeText | undefined {
|
|
39
|
+
if (value === undefined) return undefined;
|
|
40
|
+
if (typeof value === 'string') return { text: value };
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Normalize all chrome fields from strings to ChromeText objects. */
|
|
45
|
+
function normalizeChrome(chrome: Chrome | undefined): NormalizedChrome {
|
|
46
|
+
if (!chrome) return {};
|
|
47
|
+
return {
|
|
48
|
+
title: normalizeChromeField(chrome.title),
|
|
49
|
+
subtitle: normalizeChromeField(chrome.subtitle),
|
|
50
|
+
source: normalizeChromeField(chrome.source),
|
|
51
|
+
byline: normalizeChromeField(chrome.byline),
|
|
52
|
+
footer: normalizeChromeField(chrome.footer),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Type inference
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Sample values from a data column and infer the field type. */
|
|
61
|
+
function inferFieldType(data: DataRow[], field: string): FieldType {
|
|
62
|
+
// Sample up to 10 rows
|
|
63
|
+
const sampleSize = Math.min(10, data.length);
|
|
64
|
+
let numericCount = 0;
|
|
65
|
+
let dateCount = 0;
|
|
66
|
+
let totalNonNull = 0;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
69
|
+
const value = data[i][field];
|
|
70
|
+
if (value == null) continue;
|
|
71
|
+
totalNonNull++;
|
|
72
|
+
|
|
73
|
+
// Check numeric
|
|
74
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
75
|
+
numericCount++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check date-like strings
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
// Try as number first
|
|
82
|
+
const num = Number(value);
|
|
83
|
+
if (!Number.isNaN(num) && Number.isFinite(num) && value.trim() !== '') {
|
|
84
|
+
numericCount++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Try as date
|
|
89
|
+
const date = new Date(value);
|
|
90
|
+
if (!Number.isNaN(date.getTime())) {
|
|
91
|
+
dateCount++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
97
|
+
dateCount++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (totalNonNull === 0) return 'nominal';
|
|
102
|
+
|
|
103
|
+
// If >80% of sampled values are dates, it's temporal
|
|
104
|
+
if (dateCount / totalNonNull > 0.8) return 'temporal';
|
|
105
|
+
// If >80% are numeric, it's quantitative
|
|
106
|
+
if (numericCount / totalNonNull > 0.8) return 'quantitative';
|
|
107
|
+
// Otherwise it's nominal
|
|
108
|
+
return 'nominal';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Infer types for encoding channels that don't have one specified. */
|
|
112
|
+
function inferEncodingTypes(encoding: Encoding, data: DataRow[], warnings: string[]): Encoding {
|
|
113
|
+
const result = { ...encoding };
|
|
114
|
+
|
|
115
|
+
for (const channel of ['x', 'y', 'color', 'size', 'detail'] as const) {
|
|
116
|
+
const spec = result[channel];
|
|
117
|
+
if (!spec) continue;
|
|
118
|
+
|
|
119
|
+
if (!spec.type) {
|
|
120
|
+
const inferred = inferFieldType(data, spec.field);
|
|
121
|
+
result[channel] = { ...spec, type: inferred };
|
|
122
|
+
warnings.push(
|
|
123
|
+
`Inferred encoding.${channel}.type as "${inferred}" from data values for field "${spec.field}"`,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
// Check for potential mismatches and warn
|
|
127
|
+
const actualType = inferFieldType(data, spec.field);
|
|
128
|
+
if (spec.type === 'nominal' && actualType === 'temporal') {
|
|
129
|
+
warnings.push(`Field "${spec.field}" looks temporal but was declared as nominal`);
|
|
130
|
+
}
|
|
131
|
+
if (spec.type === 'nominal' && actualType === 'quantitative') {
|
|
132
|
+
warnings.push(`Field "${spec.field}" looks quantitative but was declared as nominal`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Annotation normalization
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** Apply default styles to annotations that don't have them. */
|
|
145
|
+
function normalizeAnnotations(annotations: Annotation[] | undefined): Annotation[] {
|
|
146
|
+
if (!annotations || annotations.length === 0) return [];
|
|
147
|
+
|
|
148
|
+
return annotations.map((ann) => {
|
|
149
|
+
switch (ann.type) {
|
|
150
|
+
case 'text':
|
|
151
|
+
return {
|
|
152
|
+
...ann,
|
|
153
|
+
fontSize: ann.fontSize ?? 12,
|
|
154
|
+
fontWeight: ann.fontWeight ?? 400,
|
|
155
|
+
opacity: ann.opacity ?? 1,
|
|
156
|
+
};
|
|
157
|
+
case 'range':
|
|
158
|
+
return {
|
|
159
|
+
...ann,
|
|
160
|
+
opacity: ann.opacity ?? 0.1,
|
|
161
|
+
fill: ann.fill ?? '#000000',
|
|
162
|
+
};
|
|
163
|
+
case 'refline':
|
|
164
|
+
return {
|
|
165
|
+
...ann,
|
|
166
|
+
style: ann.style ?? 'dashed',
|
|
167
|
+
strokeWidth: ann.strokeWidth ?? 1,
|
|
168
|
+
stroke: ann.stroke ?? '#666666',
|
|
169
|
+
opacity: ann.opacity ?? 0.8,
|
|
170
|
+
};
|
|
171
|
+
default:
|
|
172
|
+
return ann;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Spec-level normalization
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function normalizeChartSpec(spec: ChartSpec, warnings: string[]): NormalizedChartSpec {
|
|
182
|
+
const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
type: spec.type,
|
|
186
|
+
data: spec.data,
|
|
187
|
+
encoding,
|
|
188
|
+
chrome: normalizeChrome(spec.chrome),
|
|
189
|
+
annotations: normalizeAnnotations(spec.annotations),
|
|
190
|
+
labels: {
|
|
191
|
+
density: spec.labels?.density ?? 'auto',
|
|
192
|
+
format: spec.labels?.format ?? '',
|
|
193
|
+
offsets: spec.labels?.offsets,
|
|
194
|
+
},
|
|
195
|
+
legend: spec.legend,
|
|
196
|
+
responsive: spec.responsive ?? true,
|
|
197
|
+
theme: spec.theme ?? {},
|
|
198
|
+
darkMode: spec.darkMode ?? 'off',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeTableSpec(spec: TableSpec, _warnings: string[]): NormalizedTableSpec {
|
|
203
|
+
return {
|
|
204
|
+
type: 'table',
|
|
205
|
+
data: spec.data,
|
|
206
|
+
columns: spec.columns,
|
|
207
|
+
rowKey: spec.rowKey,
|
|
208
|
+
chrome: normalizeChrome(spec.chrome),
|
|
209
|
+
theme: spec.theme ?? {},
|
|
210
|
+
darkMode: spec.darkMode ?? 'off',
|
|
211
|
+
search: spec.search ?? false,
|
|
212
|
+
pagination: spec.pagination ?? false,
|
|
213
|
+
stickyFirstColumn: spec.stickyFirstColumn ?? false,
|
|
214
|
+
compact: spec.compact ?? false,
|
|
215
|
+
responsive: spec.responsive ?? true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeGraphSpec(spec: GraphSpec, _warnings: string[]): NormalizedGraphSpec {
|
|
220
|
+
// Default layout with chargeStrength and linkDistance
|
|
221
|
+
const defaultLayout = {
|
|
222
|
+
type: 'force' as const,
|
|
223
|
+
chargeStrength: -300,
|
|
224
|
+
linkDistance: 30,
|
|
225
|
+
};
|
|
226
|
+
const layout = spec.layout
|
|
227
|
+
? {
|
|
228
|
+
...defaultLayout,
|
|
229
|
+
...spec.layout,
|
|
230
|
+
}
|
|
231
|
+
: defaultLayout;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
type: 'graph',
|
|
235
|
+
nodes: spec.nodes,
|
|
236
|
+
edges: spec.edges,
|
|
237
|
+
encoding: spec.encoding ?? {},
|
|
238
|
+
layout,
|
|
239
|
+
chrome: normalizeChrome(spec.chrome),
|
|
240
|
+
annotations: normalizeAnnotations(spec.annotations),
|
|
241
|
+
theme: spec.theme ?? {},
|
|
242
|
+
darkMode: spec.darkMode ?? 'off',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Public API
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Normalize a validated VizSpec, filling in all defaults.
|
|
252
|
+
*
|
|
253
|
+
* @param spec - A validated VizSpec (must pass validateSpec first).
|
|
254
|
+
* @param warnings - Mutable array to collect non-fatal warnings.
|
|
255
|
+
* @returns A NormalizedSpec with all optionals filled.
|
|
256
|
+
*/
|
|
257
|
+
export function normalizeSpec(spec: VizSpec, warnings: string[] = []): NormalizedSpec {
|
|
258
|
+
if (isChartSpec(spec)) {
|
|
259
|
+
return normalizeChartSpec(spec, warnings);
|
|
260
|
+
}
|
|
261
|
+
if (isTableSpec(spec)) {
|
|
262
|
+
return normalizeTableSpec(spec, warnings);
|
|
263
|
+
}
|
|
264
|
+
if (isGraphSpec(spec)) {
|
|
265
|
+
return normalizeGraphSpec(spec, warnings);
|
|
266
|
+
}
|
|
267
|
+
// Should never happen after validation
|
|
268
|
+
throw new Error(`Unknown spec type: ${(spec as Record<string, unknown>).type}`);
|
|
269
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal engine types for the compilation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* NormalizedSpec is the engine's internal representation where all optionals
|
|
5
|
+
* have been filled with defaults. It's intentionally NOT in the core package
|
|
6
|
+
* since it's an engine implementation detail, not a public contract.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AggregateOp,
|
|
11
|
+
Annotation,
|
|
12
|
+
AxisConfig,
|
|
13
|
+
ChartType,
|
|
14
|
+
ChromeText,
|
|
15
|
+
ColumnConfig,
|
|
16
|
+
DarkMode,
|
|
17
|
+
DataRow,
|
|
18
|
+
Encoding,
|
|
19
|
+
FieldType,
|
|
20
|
+
GraphEncoding,
|
|
21
|
+
GraphLayoutConfig,
|
|
22
|
+
GraphSpec,
|
|
23
|
+
LabelConfig,
|
|
24
|
+
LegendConfig,
|
|
25
|
+
ScaleConfig,
|
|
26
|
+
ThemeConfig,
|
|
27
|
+
} from '@opendata-ai/openchart-core';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// NormalizedChrome: all fields are ChromeText objects (not plain strings)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Chrome with all string values normalized to ChromeText objects. */
|
|
34
|
+
export interface NormalizedChrome {
|
|
35
|
+
title?: ChromeText;
|
|
36
|
+
subtitle?: ChromeText;
|
|
37
|
+
source?: ChromeText;
|
|
38
|
+
byline?: ChromeText;
|
|
39
|
+
footer?: ChromeText;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// NormalizedEncoding: all encoding channels have their type filled in
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** An encoding channel with a guaranteed type (inferred if not provided). */
|
|
47
|
+
export interface NormalizedEncodingChannel {
|
|
48
|
+
field: string;
|
|
49
|
+
type: FieldType;
|
|
50
|
+
aggregate?: AggregateOp;
|
|
51
|
+
axis?: AxisConfig;
|
|
52
|
+
scale?: ScaleConfig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// NormalizedSpec types
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** A ChartSpec with all optional fields filled with sensible defaults. */
|
|
60
|
+
export interface NormalizedChartSpec {
|
|
61
|
+
type: ChartType;
|
|
62
|
+
data: DataRow[];
|
|
63
|
+
encoding: Encoding;
|
|
64
|
+
chrome: NormalizedChrome;
|
|
65
|
+
annotations: Annotation[];
|
|
66
|
+
/** Normalized label configuration with defaults applied. density and format are always set; offsets stays optional. */
|
|
67
|
+
labels: Required<Pick<LabelConfig, 'density' | 'format'>> & Pick<LabelConfig, 'offsets'>;
|
|
68
|
+
/** Legend configuration (position override). */
|
|
69
|
+
legend?: LegendConfig;
|
|
70
|
+
responsive: boolean;
|
|
71
|
+
theme: ThemeConfig;
|
|
72
|
+
darkMode: DarkMode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A TableSpec with all optional fields filled with sensible defaults. */
|
|
76
|
+
export interface NormalizedTableSpec {
|
|
77
|
+
type: 'table';
|
|
78
|
+
data: DataRow[];
|
|
79
|
+
columns: ColumnConfig[];
|
|
80
|
+
rowKey?: string;
|
|
81
|
+
chrome: NormalizedChrome;
|
|
82
|
+
theme: ThemeConfig;
|
|
83
|
+
darkMode: DarkMode;
|
|
84
|
+
search: boolean;
|
|
85
|
+
pagination: boolean | { pageSize: number };
|
|
86
|
+
stickyFirstColumn: boolean;
|
|
87
|
+
compact: boolean;
|
|
88
|
+
responsive: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A GraphSpec with all optional fields filled with sensible defaults. */
|
|
92
|
+
export interface NormalizedGraphSpec {
|
|
93
|
+
type: 'graph';
|
|
94
|
+
nodes: GraphSpec['nodes'];
|
|
95
|
+
edges: GraphSpec['edges'];
|
|
96
|
+
encoding: GraphEncoding;
|
|
97
|
+
layout: GraphLayoutConfig;
|
|
98
|
+
chrome: NormalizedChrome;
|
|
99
|
+
annotations: Annotation[];
|
|
100
|
+
theme: ThemeConfig;
|
|
101
|
+
darkMode: DarkMode;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Discriminated union of all normalized spec types. */
|
|
105
|
+
export type NormalizedSpec = NormalizedChartSpec | NormalizedTableSpec | NormalizedGraphSpec;
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Validation types
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/** Machine-readable error code for programmatic handling. */
|
|
112
|
+
export type ValidationErrorCode =
|
|
113
|
+
| 'MISSING_FIELD'
|
|
114
|
+
| 'INVALID_TYPE'
|
|
115
|
+
| 'INVALID_VALUE'
|
|
116
|
+
| 'ENCODING_MISMATCH'
|
|
117
|
+
| 'DATA_FIELD_MISSING'
|
|
118
|
+
| 'EMPTY_DATA';
|
|
119
|
+
|
|
120
|
+
/** A single validation error with context. */
|
|
121
|
+
export interface ValidationError {
|
|
122
|
+
/** Error message describing what's wrong. */
|
|
123
|
+
message: string;
|
|
124
|
+
/** The path to the problematic value (e.g. "encoding.x.field"). */
|
|
125
|
+
path?: string;
|
|
126
|
+
/** Machine-readable error code for programmatic handling. */
|
|
127
|
+
code: ValidationErrorCode;
|
|
128
|
+
/** Actionable suggestion for fixing the error. */
|
|
129
|
+
suggestion: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Result of spec validation. */
|
|
133
|
+
export interface ValidationResult {
|
|
134
|
+
/** Whether the spec is valid. */
|
|
135
|
+
valid: boolean;
|
|
136
|
+
/** Validation errors (empty if valid). */
|
|
137
|
+
errors: ValidationError[];
|
|
138
|
+
/** The validated spec cast to VizSpec, or null if invalid. */
|
|
139
|
+
normalized: import('@opendata-ai/openchart-core').VizSpec | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Result of the compile pipeline (validate + normalize). */
|
|
143
|
+
export interface CompileResult {
|
|
144
|
+
/** The normalized spec with all defaults applied. */
|
|
145
|
+
spec: NormalizedSpec;
|
|
146
|
+
/** Non-fatal warnings (e.g. type mismatches that were auto-corrected). */
|
|
147
|
+
warnings: string[];
|
|
148
|
+
}
|