@opendata-ai/openchart-core 8.0.0-rc.8 → 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.
Files changed (82) hide show
  1. package/dist/index.d.ts +461 -56
  2. package/dist/index.js +62 -24
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/dist/styles.css.map +1 -1
  6. package/package.json +1 -2
  7. package/schema/chart.schema.json +373 -67
  8. package/schema/table.schema.json +35 -1
  9. package/schema/vizspec.schema.json +950 -152
  10. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  11. package/src/accessibility/__tests__/aria.test.ts +0 -125
  12. package/src/accessibility/alt-text.ts +0 -155
  13. package/src/accessibility/aria.ts +0 -75
  14. package/src/accessibility/index.ts +0 -6
  15. package/src/colors/__tests__/colorblind.test.ts +0 -75
  16. package/src/colors/__tests__/contrast.test.ts +0 -119
  17. package/src/colors/__tests__/palettes.test.ts +0 -74
  18. package/src/colors/colorblind.ts +0 -131
  19. package/src/colors/contrast.ts +0 -143
  20. package/src/colors/index.ts +0 -36
  21. package/src/colors/palettes.ts +0 -206
  22. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  23. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  24. package/src/helpers/deep-merge.ts +0 -29
  25. package/src/helpers/spec-builders.ts +0 -479
  26. package/src/index.ts +0 -215
  27. package/src/labels/__tests__/collision.test.ts +0 -310
  28. package/src/labels/collision.ts +0 -249
  29. package/src/labels/index.ts +0 -13
  30. package/src/layout/__tests__/chrome.test.ts +0 -208
  31. package/src/layout/__tests__/text-measure.test.ts +0 -57
  32. package/src/layout/chrome.ts +0 -470
  33. package/src/layout/index.ts +0 -18
  34. package/src/layout/text-measure.ts +0 -404
  35. package/src/layout/text-wrap.ts +0 -93
  36. package/src/locale/__tests__/format.test.ts +0 -606
  37. package/src/locale/format.ts +0 -459
  38. package/src/locale/index.ts +0 -21
  39. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  40. package/src/responsive/__tests__/metrics.test.ts +0 -52
  41. package/src/responsive/breakpoints.ts +0 -172
  42. package/src/responsive/index.ts +0 -53
  43. package/src/responsive/metrics.ts +0 -299
  44. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  45. package/src/schema/__tests__/schema.test.ts +0 -179
  46. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  47. package/src/styles/animation.css +0 -208
  48. package/src/styles/base.css +0 -102
  49. package/src/styles/chrome.css +0 -174
  50. package/src/styles/dark.css +0 -44
  51. package/src/styles/graph.css +0 -135
  52. package/src/styles/index.css +0 -28
  53. package/src/styles/keyframes.css +0 -151
  54. package/src/styles/legend.css +0 -18
  55. package/src/styles/reduced-motion.css +0 -67
  56. package/src/styles/series-search.css +0 -153
  57. package/src/styles/sparkline.css +0 -16
  58. package/src/styles/table-animation.css +0 -93
  59. package/src/styles/table.css +0 -398
  60. package/src/styles/token-definitions.ts +0 -199
  61. package/src/styles/tokens.css +0 -156
  62. package/src/styles/tooltip.css +0 -126
  63. package/src/styles/you-draw-it.css +0 -126
  64. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  65. package/src/theme/__tests__/defaults.test.ts +0 -73
  66. package/src/theme/__tests__/resolve.test.ts +0 -277
  67. package/src/theme/dark-mode.ts +0 -258
  68. package/src/theme/defaults.ts +0 -110
  69. package/src/theme/index.ts +0 -8
  70. package/src/theme/presets.ts +0 -64
  71. package/src/theme/resolve.ts +0 -312
  72. package/src/types/__tests__/encoding.test.ts +0 -368
  73. package/src/types/__tests__/gradient.test.ts +0 -95
  74. package/src/types/__tests__/spec.test.ts +0 -499
  75. package/src/types/encoding.ts +0 -280
  76. package/src/types/events.ts +0 -154
  77. package/src/types/index.ts +0 -289
  78. package/src/types/layout.ts +0 -2001
  79. package/src/types/schema-roots.ts +0 -32
  80. package/src/types/spec.ts +0 -2906
  81. package/src/types/table.ts +0 -108
  82. package/src/types/theme.ts +0 -209
@@ -1,338 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { EncodingChannel } from '../../types/spec';
3
- import {
4
- barChart,
5
- columnChart,
6
- dataTable,
7
- inferFieldType,
8
- lineChart,
9
- pieChart,
10
- scatterChart,
11
- } from '../spec-builders';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Test data
15
- // ---------------------------------------------------------------------------
16
-
17
- const timeSeriesData = [
18
- { date: '2020-01-01', value: 10, country: 'US' },
19
- { date: '2021-01-01', value: 40, country: 'US' },
20
- { date: '2020-01-01', value: 15, country: 'UK' },
21
- { date: '2021-01-01', value: 35, country: 'UK' },
22
- ];
23
-
24
- const categoricalData = [
25
- { name: 'Apples', count: 50, price: 1.2 },
26
- { name: 'Bananas', count: 30, price: 0.8 },
27
- { name: 'Oranges', count: 45, price: 1.5 },
28
- ];
29
-
30
- const numericData = [
31
- { x: 1, y: 2, size: 10, group: 'A' },
32
- { x: 3, y: 4, size: 20, group: 'B' },
33
- { x: 5, y: 1, size: 15, group: 'A' },
34
- ];
35
-
36
- const mixedData = [
37
- { id: 1, name: 'Alice', score: 95, joined: '2020-03-15' },
38
- { id: 2, name: 'Bob', score: 87, joined: '2021-07-22' },
39
- { id: 3, name: 'Carol', score: 92, joined: '2019-11-01' },
40
- ];
41
-
42
- // ---------------------------------------------------------------------------
43
- // inferFieldType
44
- // ---------------------------------------------------------------------------
45
-
46
- describe('inferFieldType', () => {
47
- it('infers quantitative for number fields', () => {
48
- expect(inferFieldType(categoricalData, 'count')).toBe('quantitative');
49
- expect(inferFieldType(categoricalData, 'price')).toBe('quantitative');
50
- });
51
-
52
- it('infers temporal for ISO date strings', () => {
53
- expect(inferFieldType(timeSeriesData, 'date')).toBe('temporal');
54
- });
55
-
56
- it('infers temporal for partial ISO dates (YYYY-MM format)', () => {
57
- const data = [{ period: '2020-01' }, { period: '2020-02' }];
58
- expect(inferFieldType(data, 'period')).toBe('temporal');
59
- });
60
-
61
- it('infers temporal for year-only strings (YYYY format)', () => {
62
- const data = [{ year: '2020' }, { year: '2021' }];
63
- expect(inferFieldType(data, 'year')).toBe('temporal');
64
- });
65
-
66
- it('infers nominal for plain string fields', () => {
67
- expect(inferFieldType(categoricalData, 'name')).toBe('nominal');
68
- expect(inferFieldType(timeSeriesData, 'country')).toBe('nominal');
69
- });
70
-
71
- it('infers nominal for mixed types', () => {
72
- const data = [{ value: 10 }, { value: 'text' }];
73
- expect(inferFieldType(data, 'value')).toBe('nominal');
74
- });
75
-
76
- it('handles null/undefined values gracefully', () => {
77
- const data = [{ value: null }, { value: undefined }, { value: 42 }];
78
- expect(inferFieldType(data, 'value')).toBe('quantitative');
79
- });
80
-
81
- it('handles Date objects as temporal', () => {
82
- const data = [{ date: new Date('2020-01-01') }, { date: new Date('2021-01-01') }];
83
- expect(inferFieldType(data, 'date')).toBe('temporal');
84
- });
85
-
86
- it('samples at most 20 values', () => {
87
- // Create 100 items, all numbers. Should still work without issue.
88
- const data = Array.from({ length: 100 }, (_, i) => ({ v: i }));
89
- expect(inferFieldType(data, 'v')).toBe('quantitative');
90
- });
91
-
92
- it('returns nominal for empty data', () => {
93
- expect(inferFieldType([], 'anything')).toBe('nominal');
94
- });
95
- });
96
-
97
- // ---------------------------------------------------------------------------
98
- // lineChart
99
- // ---------------------------------------------------------------------------
100
-
101
- describe('lineChart', () => {
102
- it('creates a line chart spec with string field names', () => {
103
- const spec = lineChart(timeSeriesData, 'date', 'value');
104
-
105
- expect(spec.mark).toBe('line');
106
- expect(spec.data).toBe(timeSeriesData);
107
- expect(spec.encoding.x).toEqual({ field: 'date', type: 'temporal' });
108
- expect(spec.encoding.y).toEqual({ field: 'value', type: 'quantitative' });
109
- });
110
-
111
- it('accepts full EncodingChannel objects', () => {
112
- const xChannel: EncodingChannel = {
113
- field: 'date',
114
- type: 'temporal',
115
- axis: { title: 'Year' },
116
- };
117
- const yChannel: EncodingChannel = {
118
- field: 'value',
119
- type: 'quantitative',
120
- scale: { zero: true },
121
- };
122
-
123
- const spec = lineChart(timeSeriesData, xChannel, yChannel);
124
-
125
- expect(spec.encoding.x).toEqual(xChannel);
126
- expect(spec.encoding.y).toEqual(yChannel);
127
- });
128
-
129
- it('includes color encoding from options', () => {
130
- const spec = lineChart(timeSeriesData, 'date', 'value', {
131
- color: 'country',
132
- });
133
-
134
- expect(spec.encoding.color).toEqual({ field: 'country', type: 'nominal' });
135
- });
136
-
137
- it('passes through chrome and annotations', () => {
138
- const spec = lineChart(timeSeriesData, 'date', 'value', {
139
- chrome: { title: 'GDP Growth' },
140
- annotations: [{ type: 'refline', y: 0, label: 'Zero' }],
141
- });
142
-
143
- expect(spec.chrome).toEqual({ title: 'GDP Growth' });
144
- expect(spec.annotations).toHaveLength(1);
145
- });
146
-
147
- it('passes through theme and darkMode', () => {
148
- const spec = lineChart(timeSeriesData, 'date', 'value', {
149
- theme: { borderRadius: 8 },
150
- darkMode: 'auto',
151
- });
152
-
153
- expect(spec.theme).toEqual({ borderRadius: 8 });
154
- expect(spec.darkMode).toBe('auto');
155
- });
156
- });
157
-
158
- // ---------------------------------------------------------------------------
159
- // barChart
160
- // ---------------------------------------------------------------------------
161
-
162
- describe('barChart', () => {
163
- it('maps category to y-axis and value to x-axis', () => {
164
- const spec = barChart(categoricalData, 'name', 'count');
165
-
166
- expect(spec.mark).toBe('bar');
167
- // Bar chart convention: category on y, value on x
168
- expect(spec.encoding.y).toEqual({ field: 'name', type: 'nominal' });
169
- expect(spec.encoding.x).toEqual({ field: 'count', type: 'quantitative' });
170
- });
171
-
172
- it('accepts full channel objects', () => {
173
- const catChannel: EncodingChannel = {
174
- field: 'name',
175
- type: 'ordinal',
176
- axis: { title: 'Fruit' },
177
- };
178
-
179
- const spec = barChart(categoricalData, catChannel, 'count');
180
-
181
- expect(spec.encoding.y).toEqual(catChannel);
182
- });
183
- });
184
-
185
- // ---------------------------------------------------------------------------
186
- // columnChart
187
- // ---------------------------------------------------------------------------
188
-
189
- describe('columnChart', () => {
190
- it('creates a column chart spec with x and y', () => {
191
- const spec = columnChart(categoricalData, 'name', 'count');
192
-
193
- // columnChart now produces mark: 'bar' (vertical orientation inferred from encoding)
194
- expect(typeof spec.mark).toBe('object');
195
- expect((spec.mark as Record<string, unknown>).type).toBe('bar');
196
- expect(spec.encoding.x).toEqual({ field: 'name', type: 'nominal' });
197
- expect(spec.encoding.y).toEqual({ field: 'count', type: 'quantitative' });
198
- });
199
- });
200
-
201
- // ---------------------------------------------------------------------------
202
- // pieChart
203
- // ---------------------------------------------------------------------------
204
-
205
- describe('pieChart', () => {
206
- it('maps category to color channel and value to theta', () => {
207
- const spec = pieChart(categoricalData, 'name', 'count');
208
-
209
- expect(spec.mark).toBe('arc');
210
- // Arc (pie) convention: value on theta (canonical), category on color, no x
211
- expect(spec.encoding.theta).toEqual({ field: 'count', type: 'quantitative' });
212
- expect(spec.encoding.color).toEqual({ field: 'name', type: 'nominal' });
213
- expect(spec.encoding.x).toBeUndefined();
214
- });
215
-
216
- it('includes size encoding when specified', () => {
217
- const spec = pieChart(numericData, 'group', 'y', { size: 'size' });
218
-
219
- expect(spec.encoding.size).toEqual({ field: 'size', type: 'quantitative' });
220
- });
221
- });
222
-
223
- // ---------------------------------------------------------------------------
224
- // scatterChart
225
- // ---------------------------------------------------------------------------
226
-
227
- describe('scatterChart', () => {
228
- it('creates a scatter chart with both axes quantitative', () => {
229
- const spec = scatterChart(numericData, 'x', 'y');
230
-
231
- expect(spec.mark).toBe('point');
232
- expect(spec.encoding.x).toEqual({ field: 'x', type: 'quantitative' });
233
- expect(spec.encoding.y).toEqual({ field: 'y', type: 'quantitative' });
234
- });
235
-
236
- it('supports color and size encoding', () => {
237
- const spec = scatterChart(numericData, 'x', 'y', {
238
- color: 'group',
239
- size: 'size',
240
- });
241
-
242
- expect(spec.encoding.color).toEqual({ field: 'group', type: 'nominal' });
243
- expect(spec.encoding.size).toEqual({ field: 'size', type: 'quantitative' });
244
- });
245
- });
246
-
247
- // ---------------------------------------------------------------------------
248
- // dataTable
249
- // ---------------------------------------------------------------------------
250
-
251
- describe('dataTable', () => {
252
- it('auto-generates columns from data keys', () => {
253
- const spec = dataTable(categoricalData);
254
-
255
- expect(spec.type).toBe('table');
256
- expect(spec.data).toBe(categoricalData);
257
- expect(spec.columns).toHaveLength(3);
258
- expect(spec.columns[0]).toEqual({ key: 'name', label: 'name', align: 'left' });
259
- expect(spec.columns[1]).toEqual({ key: 'count', label: 'count', align: 'right' });
260
- expect(spec.columns[2]).toEqual({ key: 'price', label: 'price', align: 'right' });
261
- });
262
-
263
- it('uses provided columns instead of auto-generating', () => {
264
- const columns = [
265
- { key: 'name', label: 'Fruit Name' },
266
- { key: 'count', label: 'Quantity', format: ',.0f' },
267
- ];
268
- const spec = dataTable(categoricalData, { columns });
269
-
270
- expect(spec.columns).toBe(columns);
271
- expect(spec.columns).toHaveLength(2);
272
- });
273
-
274
- it('right-aligns numeric columns in auto-generated config', () => {
275
- const spec = dataTable(mixedData);
276
-
277
- const idCol = spec.columns.find((c) => c.key === 'id');
278
- const scoreCol = spec.columns.find((c) => c.key === 'score');
279
- const nameCol = spec.columns.find((c) => c.key === 'name');
280
-
281
- expect(idCol?.align).toBe('right');
282
- expect(scoreCol?.align).toBe('right');
283
- expect(nameCol?.align).toBe('left');
284
- });
285
-
286
- it('passes through all table options', () => {
287
- const spec = dataTable(categoricalData, {
288
- rowKey: 'name',
289
- chrome: { title: 'Fruit Data' },
290
- search: true,
291
- pagination: { pageSize: 10 },
292
- stickyFirstColumn: true,
293
- compact: true,
294
- darkMode: 'auto',
295
- });
296
-
297
- expect(spec.rowKey).toBe('name');
298
- expect(spec.chrome).toEqual({ title: 'Fruit Data' });
299
- expect(spec.search).toBe(true);
300
- expect(spec.pagination).toEqual({ pageSize: 10 });
301
- expect(spec.stickyFirstColumn).toBe(true);
302
- expect(spec.compact).toBe(true);
303
- expect(spec.darkMode).toBe('auto');
304
- });
305
-
306
- it('returns empty columns for empty data', () => {
307
- const spec = dataTable([]);
308
-
309
- expect(spec.columns).toEqual([]);
310
- });
311
- });
312
-
313
- // ---------------------------------------------------------------------------
314
- // Mixed field ref (string vs object) across builders
315
- // ---------------------------------------------------------------------------
316
-
317
- describe('mixed FieldRef usage', () => {
318
- it('allows mixing string and object field refs', () => {
319
- const yChannel: EncodingChannel = {
320
- field: 'value',
321
- type: 'quantitative',
322
- aggregate: 'mean',
323
- axis: { title: 'Average Value', format: ',.1f' },
324
- };
325
-
326
- const spec = lineChart(timeSeriesData, 'date', yChannel, {
327
- color: { field: 'country', type: 'nominal' },
328
- });
329
-
330
- // x was a string, so type was inferred
331
- expect(spec.encoding.x?.type).toBe('temporal');
332
- // y was a full object, so it's passed through
333
- expect(spec.encoding.y?.aggregate).toBe('mean');
334
- expect(spec.encoding.y?.axis?.title).toBe('Average Value');
335
- // color was a full object
336
- expect(spec.encoding.color?.type).toBe('nominal');
337
- });
338
- });
@@ -1,29 +0,0 @@
1
- /**
2
- * Deep merge for applying partial patches onto a spec-shaped object.
3
- *
4
- * Used by the scrollytelling story layer (`vanilla/story`) to apply
5
- * cumulative deep-partial spec patches onto a base spec. Arrays REPLACE
6
- * rather than merge element-by-element: predictable behavior for things
7
- * like `annotations` or `data`, where index-wise merging would silently
8
- * produce nonsense. Plain objects merge key-by-key, recursively.
9
- */
10
- // biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing
11
- export function deepMergeSpec(target: any, source: any): any {
12
- if (source === undefined) return target;
13
- if (source === null) return source;
14
- if (
15
- typeof source !== 'object' ||
16
- Array.isArray(source) ||
17
- typeof target !== 'object' ||
18
- target === null ||
19
- Array.isArray(target)
20
- ) {
21
- return source;
22
- }
23
-
24
- const result = { ...target };
25
- for (const key of Object.keys(source)) {
26
- result[key] = deepMergeSpec(target[key], source[key]);
27
- }
28
- return result;
29
- }