@opendata-ai/openchart-core 8.0.0-rc.9 → 8.1.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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +403 -45
  3. package/dist/index.js +50 -18
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.css.map +1 -1
  7. package/package.json +1 -2
  8. package/schema/chart.schema.json +375 -69
  9. package/schema/table.schema.json +35 -1
  10. package/schema/vizspec.schema.json +992 -171
  11. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  12. package/src/accessibility/__tests__/aria.test.ts +0 -125
  13. package/src/accessibility/alt-text.ts +0 -155
  14. package/src/accessibility/aria.ts +0 -75
  15. package/src/accessibility/index.ts +0 -6
  16. package/src/colors/__tests__/colorblind.test.ts +0 -75
  17. package/src/colors/__tests__/contrast.test.ts +0 -119
  18. package/src/colors/__tests__/palettes.test.ts +0 -74
  19. package/src/colors/colorblind.ts +0 -131
  20. package/src/colors/contrast.ts +0 -143
  21. package/src/colors/index.ts +0 -36
  22. package/src/colors/palettes.ts +0 -206
  23. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  24. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  25. package/src/helpers/deep-merge.ts +0 -29
  26. package/src/helpers/spec-builders.ts +0 -479
  27. package/src/index.ts +0 -215
  28. package/src/labels/__tests__/collision.test.ts +0 -310
  29. package/src/labels/collision.ts +0 -249
  30. package/src/labels/index.ts +0 -13
  31. package/src/layout/__tests__/chrome.test.ts +0 -208
  32. package/src/layout/__tests__/text-measure.test.ts +0 -57
  33. package/src/layout/chrome.ts +0 -470
  34. package/src/layout/index.ts +0 -18
  35. package/src/layout/text-measure.ts +0 -404
  36. package/src/layout/text-wrap.ts +0 -93
  37. package/src/locale/__tests__/format.test.ts +0 -606
  38. package/src/locale/format.ts +0 -459
  39. package/src/locale/index.ts +0 -21
  40. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  41. package/src/responsive/__tests__/metrics.test.ts +0 -52
  42. package/src/responsive/breakpoints.ts +0 -177
  43. package/src/responsive/index.ts +0 -53
  44. package/src/responsive/metrics.ts +0 -299
  45. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  46. package/src/schema/__tests__/schema.test.ts +0 -179
  47. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  48. package/src/styles/animation.css +0 -213
  49. package/src/styles/base.css +0 -102
  50. package/src/styles/chrome.css +0 -174
  51. package/src/styles/dark.css +0 -44
  52. package/src/styles/graph.css +0 -135
  53. package/src/styles/index.css +0 -28
  54. package/src/styles/keyframes.css +0 -151
  55. package/src/styles/legend.css +0 -18
  56. package/src/styles/reduced-motion.css +0 -67
  57. package/src/styles/series-search.css +0 -153
  58. package/src/styles/sparkline.css +0 -16
  59. package/src/styles/table-animation.css +0 -93
  60. package/src/styles/table.css +0 -398
  61. package/src/styles/token-definitions.ts +0 -199
  62. package/src/styles/tokens.css +0 -156
  63. package/src/styles/tooltip.css +0 -126
  64. package/src/styles/you-draw-it.css +0 -126
  65. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  66. package/src/theme/__tests__/defaults.test.ts +0 -73
  67. package/src/theme/__tests__/resolve.test.ts +0 -277
  68. package/src/theme/dark-mode.ts +0 -258
  69. package/src/theme/defaults.ts +0 -110
  70. package/src/theme/index.ts +0 -8
  71. package/src/theme/presets.ts +0 -64
  72. package/src/theme/resolve.ts +0 -312
  73. package/src/types/__tests__/encoding.test.ts +0 -368
  74. package/src/types/__tests__/gradient.test.ts +0 -95
  75. package/src/types/__tests__/spec.test.ts +0 -499
  76. package/src/types/encoding.ts +0 -280
  77. package/src/types/events.ts +0 -236
  78. package/src/types/index.ts +0 -290
  79. package/src/types/layout.ts +0 -2024
  80. package/src/types/schema-roots.ts +0 -32
  81. package/src/types/spec.ts +0 -2925
  82. package/src/types/table.ts +0 -108
  83. 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
- }