@opendata-ai/openchart-engine 6.0.0 → 6.1.1

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 (65) hide show
  1. package/dist/index.d.ts +155 -19
  2. package/dist/index.js +1513 -164
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/__test-fixtures__/specs.ts +6 -3
  6. package/src/__tests__/axes.test.ts +168 -4
  7. package/src/__tests__/compile-chart.test.ts +23 -12
  8. package/src/__tests__/compile-layer.test.ts +386 -0
  9. package/src/__tests__/dimensions.test.ts +6 -3
  10. package/src/__tests__/legend.test.ts +6 -3
  11. package/src/__tests__/scales.test.ts +176 -2
  12. package/src/annotations/__tests__/compute.test.ts +8 -4
  13. package/src/charts/bar/__tests__/compute.test.ts +12 -6
  14. package/src/charts/bar/compute.ts +21 -5
  15. package/src/charts/column/__tests__/compute.test.ts +14 -7
  16. package/src/charts/column/compute.ts +21 -6
  17. package/src/charts/dot/__tests__/compute.test.ts +10 -5
  18. package/src/charts/dot/compute.ts +10 -4
  19. package/src/charts/line/__tests__/compute.test.ts +102 -11
  20. package/src/charts/line/__tests__/curves.test.ts +51 -0
  21. package/src/charts/line/__tests__/labels.test.ts +2 -1
  22. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  23. package/src/charts/line/area.ts +19 -8
  24. package/src/charts/line/compute.ts +64 -25
  25. package/src/charts/line/curves.ts +40 -0
  26. package/src/charts/pie/__tests__/compute.test.ts +10 -5
  27. package/src/charts/pie/compute.ts +2 -1
  28. package/src/charts/rule/index.ts +127 -0
  29. package/src/charts/scatter/__tests__/compute.test.ts +10 -5
  30. package/src/charts/scatter/compute.ts +15 -5
  31. package/src/charts/text/index.ts +92 -0
  32. package/src/charts/tick/index.ts +84 -0
  33. package/src/charts/utils.ts +1 -1
  34. package/src/compile.ts +175 -23
  35. package/src/compiler/__tests__/compile.test.ts +4 -4
  36. package/src/compiler/__tests__/normalize.test.ts +4 -4
  37. package/src/compiler/__tests__/validate.test.ts +25 -26
  38. package/src/compiler/index.ts +1 -1
  39. package/src/compiler/normalize.ts +77 -4
  40. package/src/compiler/types.ts +6 -2
  41. package/src/compiler/validate.ts +167 -35
  42. package/src/graphs/__tests__/compile-graph.test.ts +2 -2
  43. package/src/graphs/compile-graph.ts +2 -2
  44. package/src/index.ts +17 -1
  45. package/src/layout/axes.ts +122 -20
  46. package/src/layout/dimensions.ts +15 -9
  47. package/src/layout/scales.ts +320 -31
  48. package/src/legend/compute.ts +9 -6
  49. package/src/tables/__tests__/compile-table.test.ts +1 -1
  50. package/src/tooltips/__tests__/compute.test.ts +10 -5
  51. package/src/tooltips/compute.ts +32 -14
  52. package/src/transforms/__tests__/bin.test.ts +88 -0
  53. package/src/transforms/__tests__/calculate.test.ts +146 -0
  54. package/src/transforms/__tests__/conditional.test.ts +109 -0
  55. package/src/transforms/__tests__/filter.test.ts +59 -0
  56. package/src/transforms/__tests__/index.test.ts +93 -0
  57. package/src/transforms/__tests__/predicates.test.ts +176 -0
  58. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  59. package/src/transforms/bin.ts +87 -0
  60. package/src/transforms/calculate.ts +60 -0
  61. package/src/transforms/conditional.ts +46 -0
  62. package/src/transforms/filter.ts +17 -0
  63. package/src/transforms/index.ts +48 -0
  64. package/src/transforms/predicates.ts +90 -0
  65. package/src/transforms/timeunit.ts +88 -0
@@ -0,0 +1,386 @@
1
+ import type { ChartSpec, LayerSpec } from '@opendata-ai/openchart-core';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { compileLayer } from '../compile';
4
+ import { flattenLayers } from '../compiler/normalize';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Test data
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const sharedData = [
11
+ { month: 'Jan', sales: 100, target: 80, region: 'West' },
12
+ { month: 'Feb', sales: 150, target: 120, region: 'West' },
13
+ { month: 'Mar', sales: 130, target: 140, region: 'East' },
14
+ { month: 'Apr', sales: 200, target: 160, region: 'East' },
15
+ ];
16
+
17
+ const compileOpts = { width: 600, height: 400 };
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // flattenLayers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ describe('flattenLayers', () => {
24
+ it('flattens a simple two-layer spec into two ChartSpecs', () => {
25
+ const spec: LayerSpec = {
26
+ layer: [
27
+ {
28
+ mark: 'bar',
29
+ data: sharedData,
30
+ encoding: {
31
+ x: { field: 'sales', type: 'quantitative' },
32
+ y: { field: 'month', type: 'nominal' },
33
+ },
34
+ },
35
+ {
36
+ mark: 'line',
37
+ data: sharedData,
38
+ encoding: {
39
+ x: { field: 'month', type: 'nominal' },
40
+ y: { field: 'target', type: 'quantitative' },
41
+ },
42
+ },
43
+ ],
44
+ };
45
+
46
+ const leaves = flattenLayers(spec);
47
+ expect(leaves).toHaveLength(2);
48
+ expect(leaves[0].mark).toBe('bar');
49
+ expect(leaves[1].mark).toBe('line');
50
+ });
51
+
52
+ it('inherits shared data from parent when child has none', () => {
53
+ // When children have data: [], they keep it. But if data is undefined, parent is used.
54
+ const specNoChildData: LayerSpec = {
55
+ data: sharedData,
56
+ layer: [
57
+ {
58
+ mark: 'bar',
59
+ encoding: {
60
+ x: { field: 'sales', type: 'quantitative' },
61
+ y: { field: 'month', type: 'nominal' },
62
+ },
63
+ } as ChartSpec,
64
+ {
65
+ mark: 'line',
66
+ encoding: {
67
+ x: { field: 'month', type: 'nominal' },
68
+ y: { field: 'target', type: 'quantitative' },
69
+ },
70
+ } as ChartSpec,
71
+ ],
72
+ };
73
+
74
+ const leaves = flattenLayers(specNoChildData);
75
+ expect(leaves).toHaveLength(2);
76
+ expect(leaves[0].data).toEqual(sharedData);
77
+ expect(leaves[1].data).toEqual(sharedData);
78
+ });
79
+
80
+ it('inherits shared encoding from parent, child channels override', () => {
81
+ const spec: LayerSpec = {
82
+ data: sharedData,
83
+ encoding: {
84
+ x: { field: 'month', type: 'nominal' },
85
+ color: { field: 'region', type: 'nominal' },
86
+ },
87
+ layer: [
88
+ {
89
+ mark: 'bar',
90
+ encoding: {
91
+ y: { field: 'sales', type: 'quantitative' },
92
+ },
93
+ } as ChartSpec,
94
+ {
95
+ mark: 'line',
96
+ encoding: {
97
+ y: { field: 'target', type: 'quantitative' },
98
+ // Override x from parent
99
+ x: { field: 'month', type: 'ordinal' },
100
+ },
101
+ } as ChartSpec,
102
+ ],
103
+ };
104
+
105
+ const leaves = flattenLayers(spec);
106
+ expect(leaves).toHaveLength(2);
107
+
108
+ // First leaf: inherits x and color from parent, has own y
109
+ expect(leaves[0].encoding.x?.field).toBe('month');
110
+ expect(leaves[0].encoding.x?.type).toBe('nominal');
111
+ expect(leaves[0].encoding.y?.field).toBe('sales');
112
+ expect(leaves[0].encoding.color).toEqual({ field: 'region', type: 'nominal' });
113
+
114
+ // Second leaf: overrides x, inherits color from parent, has own y
115
+ expect(leaves[1].encoding.x?.type).toBe('ordinal');
116
+ expect(leaves[1].encoding.y?.field).toBe('target');
117
+ expect(leaves[1].encoding.color).toEqual({ field: 'region', type: 'nominal' });
118
+ });
119
+
120
+ it('chains parent transforms before child transforms', () => {
121
+ const spec: LayerSpec = {
122
+ data: sharedData,
123
+ transform: [{ filter: { field: 'region', equal: 'West' } }],
124
+ layer: [
125
+ {
126
+ mark: 'bar',
127
+ transform: [{ filter: { field: 'sales', gt: 100 } }],
128
+ encoding: {
129
+ x: { field: 'sales', type: 'quantitative' },
130
+ y: { field: 'month', type: 'nominal' },
131
+ },
132
+ } as ChartSpec,
133
+ ],
134
+ };
135
+
136
+ const leaves = flattenLayers(spec);
137
+ expect(leaves).toHaveLength(1);
138
+ expect(leaves[0].transform).toHaveLength(2);
139
+ // Parent transform first
140
+ expect(leaves[0].transform![0]).toEqual({ filter: { field: 'region', equal: 'West' } });
141
+ // Child transform second
142
+ expect(leaves[0].transform![1]).toEqual({ filter: { field: 'sales', gt: 100 } });
143
+ });
144
+
145
+ it('handles nested layers recursively', () => {
146
+ const spec: LayerSpec = {
147
+ data: sharedData,
148
+ layer: [
149
+ {
150
+ // Nested LayerSpec
151
+ layer: [
152
+ {
153
+ mark: 'bar',
154
+ encoding: {
155
+ x: { field: 'sales', type: 'quantitative' },
156
+ y: { field: 'month', type: 'nominal' },
157
+ },
158
+ } as ChartSpec,
159
+ {
160
+ mark: 'line',
161
+ encoding: {
162
+ x: { field: 'month', type: 'nominal' },
163
+ y: { field: 'target', type: 'quantitative' },
164
+ },
165
+ } as ChartSpec,
166
+ ],
167
+ } as LayerSpec,
168
+ {
169
+ mark: 'point',
170
+ encoding: {
171
+ x: { field: 'month', type: 'nominal' },
172
+ y: { field: 'sales', type: 'quantitative' },
173
+ },
174
+ } as ChartSpec,
175
+ ],
176
+ };
177
+
178
+ const leaves = flattenLayers(spec);
179
+ expect(leaves).toHaveLength(3);
180
+ expect(leaves[0].mark).toBe('bar');
181
+ expect(leaves[1].mark).toBe('line');
182
+ expect(leaves[2].mark).toBe('point');
183
+ // All should inherit data from the top-level parent
184
+ for (const leaf of leaves) {
185
+ expect(leaf.data).toEqual(sharedData);
186
+ }
187
+ });
188
+ });
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // compileLayer
192
+ // ---------------------------------------------------------------------------
193
+
194
+ describe('compileLayer', () => {
195
+ it('compiles a basic two-layer spec into a single ChartLayout', () => {
196
+ const spec: LayerSpec = {
197
+ layer: [
198
+ {
199
+ mark: 'bar' as const,
200
+ data: [
201
+ { name: 'A', value: 10 },
202
+ { name: 'B', value: 30 },
203
+ { name: 'C', value: 20 },
204
+ ],
205
+ encoding: {
206
+ x: { field: 'value', type: 'quantitative' as const },
207
+ y: { field: 'name', type: 'nominal' as const },
208
+ },
209
+ },
210
+ {
211
+ mark: 'bar' as const,
212
+ data: [
213
+ { name: 'A', value: 15 },
214
+ { name: 'B', value: 25 },
215
+ { name: 'C', value: 18 },
216
+ ],
217
+ encoding: {
218
+ x: { field: 'value', type: 'quantitative' as const },
219
+ y: { field: 'name', type: 'nominal' as const },
220
+ },
221
+ },
222
+ ],
223
+ };
224
+
225
+ const layout = compileLayer(spec, compileOpts);
226
+
227
+ // Should have a valid chart area
228
+ expect(layout.area.width).toBeGreaterThan(0);
229
+ expect(layout.area.height).toBeGreaterThan(0);
230
+
231
+ // Should have marks from both layers
232
+ expect(layout.marks.length).toBeGreaterThan(0);
233
+ });
234
+
235
+ it('produces combined marks from all layers', () => {
236
+ const spec: LayerSpec = {
237
+ layer: [
238
+ {
239
+ mark: 'bar' as const,
240
+ data: [
241
+ { name: 'A', value: 10 },
242
+ { name: 'B', value: 20 },
243
+ ],
244
+ encoding: {
245
+ x: { field: 'value', type: 'quantitative' as const },
246
+ y: { field: 'name', type: 'nominal' as const },
247
+ },
248
+ },
249
+ {
250
+ mark: 'bar' as const,
251
+ data: [
252
+ { name: 'A', value: 5 },
253
+ { name: 'B', value: 15 },
254
+ ],
255
+ encoding: {
256
+ x: { field: 'value', type: 'quantitative' as const },
257
+ y: { field: 'name', type: 'nominal' as const },
258
+ },
259
+ },
260
+ ],
261
+ };
262
+
263
+ const layout = compileLayer(spec, compileOpts);
264
+ // Two layers each with 2 data points should produce marks from both
265
+ expect(layout.marks.length).toBeGreaterThanOrEqual(4);
266
+ });
267
+
268
+ it('uses layer-level chrome over leaf chrome', () => {
269
+ const spec: LayerSpec = {
270
+ chrome: {
271
+ title: 'Layer Title',
272
+ subtitle: 'Combined view',
273
+ },
274
+ layer: [
275
+ {
276
+ mark: 'bar' as const,
277
+ data: [{ name: 'A', value: 10 }],
278
+ encoding: {
279
+ x: { field: 'value', type: 'quantitative' as const },
280
+ y: { field: 'name', type: 'nominal' as const },
281
+ },
282
+ chrome: {
283
+ title: 'Bar Title',
284
+ },
285
+ },
286
+ ],
287
+ };
288
+
289
+ const layout = compileLayer(spec, compileOpts);
290
+ // The layer-level chrome should be used
291
+ expect(layout.chrome.title?.text).toBe('Layer Title');
292
+ expect(layout.chrome.subtitle?.text).toBe('Combined view');
293
+ });
294
+
295
+ it('unions scale domains across layers for shared scales', () => {
296
+ // Layer 1 has values 0-50, layer 2 has values 0-100.
297
+ // The shared scale should encompass 0-100.
298
+ const spec: LayerSpec = {
299
+ layer: [
300
+ {
301
+ mark: 'bar' as const,
302
+ data: [{ name: 'A', value: 50 }],
303
+ encoding: {
304
+ x: { field: 'value', type: 'quantitative' as const },
305
+ y: { field: 'name', type: 'nominal' as const },
306
+ },
307
+ },
308
+ {
309
+ mark: 'bar' as const,
310
+ data: [{ name: 'A', value: 100 }],
311
+ encoding: {
312
+ x: { field: 'value', type: 'quantitative' as const },
313
+ y: { field: 'name', type: 'nominal' as const },
314
+ },
315
+ },
316
+ ],
317
+ };
318
+
319
+ const layout = compileLayer(spec, compileOpts);
320
+ // Layout should compile without error and have marks
321
+ expect(layout.marks.length).toBeGreaterThan(0);
322
+ expect(layout.area.width).toBeGreaterThan(0);
323
+ });
324
+
325
+ it('compiles a single-layer LayerSpec identically to a ChartSpec', () => {
326
+ const chartData = [
327
+ { name: 'A', value: 10 },
328
+ { name: 'B', value: 30 },
329
+ ];
330
+
331
+ const layerSpec: LayerSpec = {
332
+ layer: [
333
+ {
334
+ mark: 'bar' as const,
335
+ data: chartData,
336
+ encoding: {
337
+ x: { field: 'value', type: 'quantitative' as const },
338
+ y: { field: 'name', type: 'nominal' as const },
339
+ },
340
+ },
341
+ ],
342
+ };
343
+
344
+ const layout = compileLayer(layerSpec, compileOpts);
345
+ expect(layout.area.width).toBeGreaterThan(0);
346
+ expect(layout.marks.length).toBeGreaterThan(0);
347
+ });
348
+
349
+ it('deduplicates legend entries when layers share a color field', () => {
350
+ const spec: LayerSpec = {
351
+ layer: [
352
+ {
353
+ mark: 'bar' as const,
354
+ data: [
355
+ { name: 'A', value: 10, cat: 'X' },
356
+ { name: 'B', value: 20, cat: 'Y' },
357
+ ],
358
+ encoding: {
359
+ x: { field: 'value', type: 'quantitative' as const },
360
+ y: { field: 'name', type: 'nominal' as const },
361
+ color: { field: 'cat', type: 'nominal' as const },
362
+ },
363
+ },
364
+ {
365
+ mark: 'bar' as const,
366
+ data: [
367
+ { name: 'A', value: 5, cat: 'X' },
368
+ { name: 'B', value: 15, cat: 'Y' },
369
+ ],
370
+ encoding: {
371
+ x: { field: 'value', type: 'quantitative' as const },
372
+ y: { field: 'name', type: 'nominal' as const },
373
+ color: { field: 'cat', type: 'nominal' as const },
374
+ },
375
+ },
376
+ ],
377
+ };
378
+
379
+ const layout = compileLayer(spec, compileOpts);
380
+ // Should have exactly 2 legend entries (X, Y), not 4
381
+ const uniqueLabels = new Set(layout.legend.entries.map((e) => e.label));
382
+ expect(uniqueLabels.size).toBe(2);
383
+ expect(uniqueLabels.has('X')).toBe(true);
384
+ expect(uniqueLabels.has('Y')).toBe(true);
385
+ });
386
+ });
@@ -5,7 +5,8 @@ import type { NormalizedChartSpec } from '../compiler/types';
5
5
  import { computeDimensions } from '../layout/dimensions';
6
6
 
7
7
  const baseSpec: NormalizedChartSpec = {
8
- type: 'line',
8
+ markType: 'line',
9
+ markDef: { type: 'line' },
9
10
  data: [
10
11
  { date: '2020-01-01', value: 10 },
11
12
  { date: '2021-01-01', value: 20 },
@@ -152,7 +153,8 @@ describe('computeDimensions', () => {
152
153
  it('reserves extra bottom space for rotated x-axis labels', () => {
153
154
  const rotatedSpec: NormalizedChartSpec = {
154
155
  ...baseSpec,
155
- type: 'column',
156
+ markType: 'bar',
157
+ markDef: { type: 'bar', orient: 'vertical' },
156
158
  data: [
157
159
  { category: 'California', value: 10 },
158
160
  { category: 'New York', value: 20 },
@@ -165,7 +167,8 @@ describe('computeDimensions', () => {
165
167
  };
166
168
  const normalSpec: NormalizedChartSpec = {
167
169
  ...baseSpec,
168
- type: 'column',
170
+ markType: 'bar',
171
+ markDef: { type: 'bar', orient: 'vertical' },
169
172
  data: rotatedSpec.data,
170
173
  encoding: {
171
174
  x: { field: 'category', type: 'nominal' },
@@ -5,7 +5,8 @@ import type { NormalizedChartSpec } from '../compiler/types';
5
5
  import { computeLegend } from '../legend/compute';
6
6
 
7
7
  const specWithColor: NormalizedChartSpec = {
8
- type: 'line',
8
+ markType: 'line',
9
+ markDef: { type: 'line' },
9
10
  data: [
10
11
  { date: '2020', value: 10, country: 'US' },
11
12
  { date: '2021', value: 20, country: 'UK' },
@@ -116,7 +117,8 @@ describe('computeLegend', () => {
116
117
 
117
118
  const barSpec: NormalizedChartSpec = {
118
119
  ...specWithColor,
119
- type: 'bar',
120
+ markType: 'bar',
121
+ markDef: { type: 'bar' },
120
122
  encoding: {
121
123
  x: { field: 'value', type: 'quantitative' },
122
124
  y: { field: 'date', type: 'nominal' },
@@ -128,7 +130,8 @@ describe('computeLegend', () => {
128
130
 
129
131
  const scatterSpec: NormalizedChartSpec = {
130
132
  ...specWithColor,
131
- type: 'scatter',
133
+ markType: 'point',
134
+ markDef: { type: 'point' },
132
135
  encoding: {
133
136
  x: { field: 'value', type: 'quantitative' },
134
137
  y: { field: 'value', type: 'quantitative' },
@@ -3,7 +3,8 @@ import type { NormalizedChartSpec } from '../compiler/types';
3
3
  import { computeScales } from '../layout/scales';
4
4
 
5
5
  const lineSpec: NormalizedChartSpec = {
6
- type: 'line',
6
+ markType: 'line',
7
+ markDef: { type: 'line' },
7
8
  data: [
8
9
  { date: '2020-01-01', value: 10, country: 'US' },
9
10
  { date: '2021-01-01', value: 50, country: 'US' },
@@ -23,7 +24,8 @@ const lineSpec: NormalizedChartSpec = {
23
24
  };
24
25
 
25
26
  const barSpec: NormalizedChartSpec = {
26
- type: 'bar',
27
+ markType: 'bar',
28
+ markDef: { type: 'bar' },
27
29
  data: [
28
30
  { category: 'A', count: 10 },
29
31
  { category: 'B', count: 30 },
@@ -107,3 +109,175 @@ describe('computeScales', () => {
107
109
  expect(xDomain[1]).toBeGreaterThanOrEqual(30);
108
110
  });
109
111
  });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Expanded scale type tests
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /** Helper to make a spec with an explicit scale type on the y axis. */
118
+ function makeQuantSpec(scaleConfig: Record<string, unknown>): NormalizedChartSpec {
119
+ return {
120
+ markType: 'line',
121
+ markDef: { type: 'line' },
122
+ data: [
123
+ { x: 1, y: 1 },
124
+ { x: 2, y: 10 },
125
+ { x: 3, y: 100 },
126
+ { x: 4, y: 1000 },
127
+ ],
128
+ encoding: {
129
+ x: { field: 'x', type: 'quantitative' },
130
+ y: { field: 'y', type: 'quantitative', scale: scaleConfig },
131
+ },
132
+ chrome: {},
133
+ annotations: [],
134
+ responsive: true,
135
+ theme: {},
136
+ darkMode: 'off',
137
+ labels: { density: 'auto', format: '' },
138
+ } as NormalizedChartSpec;
139
+ }
140
+
141
+ describe('expanded scale types', () => {
142
+ it('creates a log scale with explicit type override', () => {
143
+ const spec = makeQuantSpec({ type: 'log' });
144
+ const scales = computeScales(spec, chartArea, spec.data);
145
+ expect(scales.y!.type).toBe('log');
146
+ // Log scale maps multiplicatively - position for 10 should be between 1 and 100
147
+ const pos1 = scales.y!.scale(1) as number;
148
+ const pos10 = scales.y!.scale(10) as number;
149
+ const pos100 = scales.y!.scale(100) as number;
150
+ expect(pos10).toBeLessThan(pos1); // Y is inverted
151
+ expect(pos10).toBeGreaterThan(pos100);
152
+ });
153
+
154
+ it('applies base config to log scale', () => {
155
+ const spec = makeQuantSpec({ type: 'log', base: 2 });
156
+ const scales = computeScales(spec, chartArea, spec.data);
157
+ expect(scales.y!.type).toBe('log');
158
+ });
159
+
160
+ it('creates a pow scale with exponent', () => {
161
+ const spec = makeQuantSpec({ type: 'pow', exponent: 2 });
162
+ const scales = computeScales(spec, chartArea, spec.data);
163
+ expect(scales.y!.type).toBe('pow');
164
+ // Pow scale should still map data to pixel range
165
+ const pos = scales.y!.scale(50) as number;
166
+ expect(pos).toBeGreaterThanOrEqual(chartArea.y);
167
+ expect(pos).toBeLessThanOrEqual(chartArea.y + chartArea.height);
168
+ });
169
+
170
+ it('creates a sqrt scale', () => {
171
+ const spec = makeQuantSpec({ type: 'sqrt' });
172
+ const scales = computeScales(spec, chartArea, spec.data);
173
+ expect(scales.y!.type).toBe('sqrt');
174
+ });
175
+
176
+ it('creates a symlog scale with constant', () => {
177
+ const spec = makeQuantSpec({ type: 'symlog', constant: 2 });
178
+ const scales = computeScales(spec, chartArea, spec.data);
179
+ expect(scales.y!.type).toBe('symlog');
180
+ });
181
+
182
+ it('creates a utc scale for explicit utc type', () => {
183
+ const spec: NormalizedChartSpec = {
184
+ markType: 'line',
185
+ markDef: { type: 'line' },
186
+ data: [
187
+ { date: '2020-01-01', value: 10 },
188
+ { date: '2022-01-01', value: 30 },
189
+ ],
190
+ encoding: {
191
+ x: { field: 'date', type: 'temporal', scale: { type: 'utc' } },
192
+ y: { field: 'value', type: 'quantitative' },
193
+ },
194
+ chrome: {},
195
+ annotations: [],
196
+ responsive: true,
197
+ theme: {},
198
+ darkMode: 'off',
199
+ labels: { density: 'auto', format: '' },
200
+ };
201
+ const scales = computeScales(spec, chartArea, spec.data);
202
+ expect(scales.x!.type).toBe('utc');
203
+ });
204
+
205
+ it('creates a quantile scale', () => {
206
+ const spec = makeQuantSpec({ type: 'quantile' });
207
+ const scales = computeScales(spec, chartArea, spec.data);
208
+ expect(scales.y!.type).toBe('quantile');
209
+ });
210
+
211
+ it('creates a quantize scale', () => {
212
+ const spec = makeQuantSpec({ type: 'quantize' });
213
+ const scales = computeScales(spec, chartArea, spec.data);
214
+ expect(scales.y!.type).toBe('quantize');
215
+ });
216
+
217
+ it('creates a threshold scale', () => {
218
+ const spec = makeQuantSpec({ type: 'threshold', domain: [10, 100] });
219
+ const scales = computeScales(spec, chartArea, spec.data);
220
+ expect(scales.y!.type).toBe('threshold');
221
+ });
222
+ });
223
+
224
+ describe('scale config properties', () => {
225
+ it('applies clamp to linear scale', () => {
226
+ const spec = makeQuantSpec({ type: 'linear', domain: [0, 100], clamp: true });
227
+ const scales = computeScales(spec, chartArea, spec.data);
228
+ // Values outside domain should be clamped to range edges
229
+ const posAbove = scales.y!.scale(200) as number;
230
+ const posAtMax = scales.y!.scale(100) as number;
231
+ expect(posAbove).toBe(posAtMax);
232
+ });
233
+
234
+ it('applies reverse to linear scale', () => {
235
+ const specNormal = makeQuantSpec({ type: 'linear' });
236
+ const specReversed = makeQuantSpec({ type: 'linear', reverse: true });
237
+ const normal = computeScales(specNormal, chartArea, specNormal.data);
238
+ const reversed = computeScales(specReversed, chartArea, specReversed.data);
239
+
240
+ // In a reversed scale, higher values should map to the opposite end
241
+ const normalRange = normal.y!.scale.range() as number[];
242
+ const reversedRange = reversed.y!.scale.range() as number[];
243
+ expect(normalRange[0]).toBe(reversedRange[1]);
244
+ expect(normalRange[1]).toBe(reversedRange[0]);
245
+ });
246
+
247
+ it('applies padding to band scale', () => {
248
+ const specDefault: NormalizedChartSpec = {
249
+ ...barSpec,
250
+ encoding: {
251
+ x: { field: 'count', type: 'quantitative' },
252
+ y: { field: 'category', type: 'nominal' },
253
+ },
254
+ };
255
+ const specPadded: NormalizedChartSpec = {
256
+ ...barSpec,
257
+ encoding: {
258
+ x: { field: 'count', type: 'quantitative' },
259
+ y: { field: 'category', type: 'nominal', scale: { padding: 0.1 } },
260
+ },
261
+ };
262
+
263
+ const defaultScales = computeScales(specDefault, chartArea, specDefault.data);
264
+ const paddedScales = computeScales(specPadded, chartArea, specPadded.data);
265
+
266
+ // Smaller padding = wider bands
267
+ const defaultBandwidth = defaultScales.y!.scale.bandwidth!() as number;
268
+ const paddedBandwidth = paddedScales.y!.scale.bandwidth!() as number;
269
+ expect(paddedBandwidth).toBeGreaterThan(defaultBandwidth);
270
+ });
271
+
272
+ it('backward compatible: existing specs still work', () => {
273
+ // lineSpec and barSpec from before should still produce valid scales
274
+ const lineScales = computeScales(lineSpec, chartArea, lineSpec.data);
275
+ expect(lineScales.x!.type).toBe('time');
276
+ expect(lineScales.y!.type).toBe('linear');
277
+ expect(lineScales.color!.type).toBe('ordinal');
278
+
279
+ const barScales = computeScales(barSpec, chartArea, barSpec.data);
280
+ expect(barScales.x!.type).toBe('linear');
281
+ expect(barScales.y!.type).toBe('band');
282
+ });
283
+ });
@@ -26,7 +26,8 @@ const compactStrategy: LayoutStrategy = {
26
26
 
27
27
  function makeSpec(annotations: Annotation[]): NormalizedChartSpec {
28
28
  return {
29
- type: 'line',
29
+ markType: 'line',
30
+ markDef: { type: 'line' },
30
31
  data: [
31
32
  { date: '2019-01-01', value: 10 },
32
33
  { date: '2020-01-01', value: 20 },
@@ -139,7 +140,8 @@ describe('computeAnnotations', () => {
139
140
 
140
141
  it('interpolates range position for values between ordinal data points', () => {
141
142
  const ordinalSpec: NormalizedChartSpec = {
142
- type: 'line',
143
+ markType: 'line',
144
+ markDef: { type: 'line' },
143
145
  data: [
144
146
  { year: '2005', value: 10 },
145
147
  { year: '2007', value: 20 },
@@ -182,7 +184,8 @@ describe('computeAnnotations', () => {
182
184
 
183
185
  it('clamps interpolation for values outside the ordinal domain range', () => {
184
186
  const ordinalSpec: NormalizedChartSpec = {
185
- type: 'line',
187
+ markType: 'line',
188
+ markDef: { type: 'line' },
186
189
  data: [
187
190
  { year: '2005', value: 10 },
188
191
  { year: '2007', value: 20 },
@@ -209,7 +212,8 @@ describe('computeAnnotations', () => {
209
212
 
210
213
  it('returns null for non-numeric ordinal domain values', () => {
211
214
  const catSpec: NormalizedChartSpec = {
212
- type: 'column',
215
+ markType: 'bar',
216
+ markDef: { type: 'bar', orient: 'vertical' },
213
217
  data: [
214
218
  { category: 'Jan', value: 10 },
215
219
  { category: 'Feb', value: 20 },