@opendata-ai/openchart-core 3.0.0 → 6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-core",
3
- "version": "3.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -3,7 +3,7 @@ import type { ChartSpec } from '../../types/spec';
3
3
  import { generateAltText, generateDataTable } from '../alt-text';
4
4
 
5
5
  const lineSpec: ChartSpec = {
6
- type: 'line',
6
+ mark: 'line',
7
7
  data: [
8
8
  { date: '2020-01-01', value: 10, country: 'US' },
9
9
  { date: '2021-01-01', value: 20, country: 'US' },
@@ -19,7 +19,7 @@ const lineSpec: ChartSpec = {
19
19
  };
20
20
 
21
21
  const barSpec: ChartSpec = {
22
- type: 'bar',
22
+ mark: 'bar',
23
23
  data: [
24
24
  { category: 'A', value: 10 },
25
25
  { category: 'B', value: 20 },
@@ -86,7 +86,7 @@ describe('generateDataTable', () => {
86
86
 
87
87
  it('only includes encoded fields', () => {
88
88
  const spec: ChartSpec = {
89
- type: 'bar',
89
+ mark: 'bar',
90
90
  data: [{ category: 'A', value: 10, extra: 'ignored' }],
91
91
  encoding: {
92
92
  x: { field: 'category', type: 'nominal' },
@@ -100,7 +100,7 @@ describe('generateDataTable', () => {
100
100
 
101
101
  it('returns empty for spec with no encoding fields', () => {
102
102
  const spec: ChartSpec = {
103
- type: 'pie',
103
+ mark: 'arc',
104
104
  data: [{ value: 10 }],
105
105
  encoding: {},
106
106
  };
@@ -6,23 +6,12 @@
6
6
  */
7
7
 
8
8
  import type { ChartSpec, DataRow } from '../types/spec';
9
+ import { MARK_DISPLAY_NAMES, resolveMarkDef, resolveMarkType } from '../types/spec';
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // Alt text generation
12
13
  // ---------------------------------------------------------------------------
13
14
 
14
- /** Friendly display names for chart types. */
15
- const CHART_TYPE_NAMES: Record<string, string> = {
16
- line: 'Line chart',
17
- area: 'Area chart',
18
- bar: 'Bar chart',
19
- column: 'Column chart',
20
- pie: 'Pie chart',
21
- donut: 'Donut chart',
22
- dot: 'Dot plot',
23
- scatter: 'Scatter plot',
24
- };
25
-
26
15
  /**
27
16
  * Generate alt text describing a chart's content.
28
17
  *
@@ -33,7 +22,15 @@ const CHART_TYPE_NAMES: Record<string, string> = {
33
22
  * @param data - The data array.
34
23
  */
35
24
  export function generateAltText(spec: ChartSpec, data: DataRow[]): string {
36
- const chartName = CHART_TYPE_NAMES[spec.type] ?? `${spec.type} chart`;
25
+ const markType = resolveMarkType(spec.mark);
26
+ const markDef = resolveMarkDef(spec.mark);
27
+ let chartName = MARK_DISPLAY_NAMES[markType] ?? `${markType} chart`;
28
+
29
+ // Special case: donut detection
30
+ if (markType === 'arc' && markDef.innerRadius && markDef.innerRadius > 0) {
31
+ chartName = 'Donut chart';
32
+ }
33
+
37
34
  const parts: string[] = [chartName];
38
35
 
39
36
  // Add title context if present
@@ -68,7 +65,7 @@ export function generateAltText(spec: ChartSpec, data: DataRow[]): string {
68
65
  }
69
66
 
70
67
  // Describe series if color encoding is present
71
- if (spec.encoding.color && data.length > 0) {
68
+ if (spec.encoding.color && 'field' in spec.encoding.color && data.length > 0) {
72
69
  const colorField = spec.encoding.color.field;
73
70
  const uniqueSeries = [...new Set(data.map((d) => String(d[colorField])).filter(Boolean))];
74
71
  if (uniqueSeries.length > 0) {
@@ -102,8 +99,8 @@ export function generateDataTable(spec: ChartSpec, data: DataRow[]): unknown[][]
102
99
 
103
100
  if (encoding.x) fields.push(encoding.x.field);
104
101
  if (encoding.y) fields.push(encoding.y.field);
105
- if (encoding.color) fields.push(encoding.color.field);
106
- if (encoding.size) fields.push(encoding.size.field);
102
+ if (encoding.color && 'field' in encoding.color) fields.push(encoding.color.field);
103
+ if (encoding.size && 'field' in encoding.size) fields.push(encoding.size.field);
107
104
 
108
105
  // Deduplicate
109
106
  const uniqueFields = [...new Set(fields)];
@@ -102,7 +102,7 @@ describe('lineChart', () => {
102
102
  it('creates a line chart spec with string field names', () => {
103
103
  const spec = lineChart(timeSeriesData, 'date', 'value');
104
104
 
105
- expect(spec.type).toBe('line');
105
+ expect(spec.mark).toBe('line');
106
106
  expect(spec.data).toBe(timeSeriesData);
107
107
  expect(spec.encoding.x).toEqual({ field: 'date', type: 'temporal' });
108
108
  expect(spec.encoding.y).toEqual({ field: 'value', type: 'quantitative' });
@@ -163,7 +163,7 @@ describe('barChart', () => {
163
163
  it('maps category to y-axis and value to x-axis', () => {
164
164
  const spec = barChart(categoricalData, 'name', 'count');
165
165
 
166
- expect(spec.type).toBe('bar');
166
+ expect(spec.mark).toBe('bar');
167
167
  // Bar chart convention: category on y, value on x
168
168
  expect(spec.encoding.y).toEqual({ field: 'name', type: 'nominal' });
169
169
  expect(spec.encoding.x).toEqual({ field: 'count', type: 'quantitative' });
@@ -190,7 +190,9 @@ describe('columnChart', () => {
190
190
  it('creates a column chart spec with x and y', () => {
191
191
  const spec = columnChart(categoricalData, 'name', 'count');
192
192
 
193
- expect(spec.type).toBe('column');
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');
194
196
  expect(spec.encoding.x).toEqual({ field: 'name', type: 'nominal' });
195
197
  expect(spec.encoding.y).toEqual({ field: 'count', type: 'quantitative' });
196
198
  });
@@ -204,8 +206,8 @@ describe('pieChart', () => {
204
206
  it('maps category to color channel and value to y', () => {
205
207
  const spec = pieChart(categoricalData, 'name', 'count');
206
208
 
207
- expect(spec.type).toBe('pie');
208
- // Pie chart convention: value on y, category on color, no x
209
+ expect(spec.mark).toBe('arc');
210
+ // Arc (pie) convention: value on y, category on color, no x
209
211
  expect(spec.encoding.y).toEqual({ field: 'count', type: 'quantitative' });
210
212
  expect(spec.encoding.color).toEqual({ field: 'name', type: 'nominal' });
211
213
  expect(spec.encoding.x).toBeUndefined();
@@ -226,7 +228,7 @@ describe('scatterChart', () => {
226
228
  it('creates a scatter chart with both axes quantitative', () => {
227
229
  const spec = scatterChart(numericData, 'x', 'y');
228
230
 
229
- expect(spec.type).toBe('scatter');
231
+ expect(spec.mark).toBe('point');
230
232
  expect(spec.encoding.x).toEqual({ field: 'x', type: 'quantitative' });
231
233
  expect(spec.encoding.y).toEqual({ field: 'y', type: 'quantitative' });
232
234
  });
@@ -12,13 +12,14 @@
12
12
  import type {
13
13
  Annotation,
14
14
  ChartSpec,
15
- ChartType,
16
15
  Chrome,
17
16
  DarkMode,
18
17
  DataRow,
19
18
  Encoding,
20
19
  EncodingChannel,
21
20
  FieldType,
21
+ MarkDef,
22
+ MarkType,
22
23
  TableSpec,
23
24
  ThemeConfig,
24
25
  } from '../types/spec';
@@ -156,12 +157,12 @@ function buildEncoding(
156
157
 
157
158
  /** Build a ChartSpec from the resolved pieces. */
158
159
  function buildChartSpec(
159
- type: ChartType,
160
+ mark: MarkType | MarkDef,
160
161
  data: DataRow[],
161
162
  encoding: Encoding,
162
163
  options?: ChartBuilderOptions,
163
164
  ): ChartSpec {
164
- const spec: ChartSpec = { type, data, encoding };
165
+ const spec: ChartSpec = { mark, data, encoding };
165
166
  if (options?.chrome) spec.chrome = options.chrome;
166
167
  if (options?.annotations) spec.annotations = options.annotations;
167
168
  if (options?.responsive !== undefined) spec.responsive = options.responsive;
@@ -236,7 +237,7 @@ export function columnChart(
236
237
  const xChannel = resolveField(x, data);
237
238
  const yChannel = resolveField(y, data);
238
239
  const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
239
- return buildChartSpec('column', data, encoding, options);
240
+ return buildChartSpec({ type: 'bar' } as MarkDef, data, encoding, options);
240
241
  }
241
242
 
242
243
  /**
@@ -269,7 +270,7 @@ export function pieChart(
269
270
  encoding.size = resolveField(options.size, data);
270
271
  }
271
272
 
272
- return buildChartSpec('pie', data, encoding, options);
273
+ return buildChartSpec('arc', data, encoding, options);
273
274
  }
274
275
 
275
276
  /**
@@ -321,7 +322,7 @@ export function donutChart(
321
322
  encoding.size = resolveField(options.size, data);
322
323
  }
323
324
 
324
- return buildChartSpec('donut', data, encoding, options);
325
+ return buildChartSpec({ type: 'arc', innerRadius: 40 } as MarkDef, data, encoding, options);
325
326
  }
326
327
 
327
328
  /**
@@ -341,7 +342,7 @@ export function dotChart(
341
342
  const xChannel = resolveField(x, data);
342
343
  const yChannel = resolveField(y, data);
343
344
  const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
344
- return buildChartSpec('dot', data, encoding, options);
345
+ return buildChartSpec('circle', data, encoding, options);
345
346
  }
346
347
 
347
348
  /**
@@ -361,7 +362,7 @@ export function scatterChart(
361
362
  const xChannel = resolveField(x, data);
362
363
  const yChannel = resolveField(y, data);
363
364
  const encoding = buildEncoding({ x: xChannel, y: yChannel }, options, data);
364
- return buildChartSpec('scatter', data, encoding, options);
365
+ return buildChartSpec('point', data, encoding, options);
365
366
  }
366
367
 
367
368
  /**
@@ -0,0 +1,267 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { MARK_ENCODING_RULES } from '../encoding';
3
+ import type { MarkType } from '../spec';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function getChannelNames(markType: MarkType): string[] {
10
+ return Object.keys(MARK_ENCODING_RULES[markType]);
11
+ }
12
+
13
+ function getRequiredChannels(markType: MarkType): string[] {
14
+ return Object.entries(MARK_ENCODING_RULES[markType])
15
+ .filter(([, rule]) => rule.required)
16
+ .map(([ch]) => ch);
17
+ }
18
+
19
+ function getOptionalChannels(markType: MarkType): string[] {
20
+ return Object.entries(MARK_ENCODING_RULES[markType])
21
+ .filter(([, rule]) => !rule.required)
22
+ .map(([ch]) => ch);
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Tests
27
+ // ---------------------------------------------------------------------------
28
+
29
+ describe('MARK_ENCODING_RULES', () => {
30
+ it('has entries for all 10 mark types', () => {
31
+ const expectedTypes: MarkType[] = [
32
+ 'bar',
33
+ 'line',
34
+ 'area',
35
+ 'point',
36
+ 'circle',
37
+ 'arc',
38
+ 'text',
39
+ 'rule',
40
+ 'tick',
41
+ 'rect',
42
+ ];
43
+ for (const type of expectedTypes) {
44
+ expect(MARK_ENCODING_RULES[type]).toBeDefined();
45
+ }
46
+ });
47
+
48
+ it('every mark type has x, y, color, size, and detail channels', () => {
49
+ const baseChannels = ['x', 'y', 'color', 'size', 'detail'];
50
+ for (const [_markType, rules] of Object.entries(MARK_ENCODING_RULES)) {
51
+ for (const ch of baseChannels) {
52
+ expect(rules[ch as keyof typeof rules]).toBeDefined();
53
+ }
54
+ }
55
+ });
56
+ });
57
+
58
+ describe('bar encoding rules', () => {
59
+ it('requires x and y', () => {
60
+ expect(getRequiredChannels('bar')).toContain('x');
61
+ expect(getRequiredChannels('bar')).toContain('y');
62
+ });
63
+
64
+ it('supports x2 and y2 as optional', () => {
65
+ expect(getOptionalChannels('bar')).toContain('x2');
66
+ expect(getOptionalChannels('bar')).toContain('y2');
67
+ });
68
+
69
+ it('supports opacity, tooltip, href, order, detail', () => {
70
+ const optionals = getOptionalChannels('bar');
71
+ expect(optionals).toContain('opacity');
72
+ expect(optionals).toContain('tooltip');
73
+ expect(optionals).toContain('href');
74
+ expect(optionals).toContain('order');
75
+ expect(optionals).toContain('detail');
76
+ });
77
+
78
+ it('does not support shape, strokeDash, text, theta, radius', () => {
79
+ const channels = getChannelNames('bar');
80
+ expect(channels).not.toContain('shape');
81
+ expect(channels).not.toContain('strokeDash');
82
+ expect(channels).not.toContain('text');
83
+ expect(channels).not.toContain('theta');
84
+ expect(channels).not.toContain('radius');
85
+ });
86
+ });
87
+
88
+ describe('line encoding rules', () => {
89
+ it('requires x (temporal/ordinal) and y (quantitative)', () => {
90
+ const rules = MARK_ENCODING_RULES.line;
91
+ expect(rules.x.required).toBe(true);
92
+ expect(rules.x.allowedTypes).toContain('temporal');
93
+ expect(rules.y.required).toBe(true);
94
+ expect(rules.y.allowedTypes).toContain('quantitative');
95
+ });
96
+
97
+ it('supports strokeDash as optional', () => {
98
+ expect(getOptionalChannels('line')).toContain('strokeDash');
99
+ });
100
+
101
+ it('does not support shape, text, theta, radius, x2, y2', () => {
102
+ const channels = getChannelNames('line');
103
+ expect(channels).not.toContain('shape');
104
+ expect(channels).not.toContain('text');
105
+ expect(channels).not.toContain('theta');
106
+ expect(channels).not.toContain('radius');
107
+ expect(channels).not.toContain('x2');
108
+ expect(channels).not.toContain('y2');
109
+ });
110
+ });
111
+
112
+ describe('point encoding rules', () => {
113
+ it('supports shape as optional', () => {
114
+ expect(getOptionalChannels('point')).toContain('shape');
115
+ });
116
+
117
+ it('supports size as optional quantitative', () => {
118
+ const rules = MARK_ENCODING_RULES.point;
119
+ expect(rules.size.required).toBe(false);
120
+ expect(rules.size.allowedTypes).toContain('quantitative');
121
+ });
122
+ });
123
+
124
+ describe('arc encoding rules', () => {
125
+ it('requires y (quantitative) and color (nominal/ordinal)', () => {
126
+ const rules = MARK_ENCODING_RULES.arc;
127
+ expect(rules.y.required).toBe(true);
128
+ expect(rules.y.allowedTypes).toContain('quantitative');
129
+ expect(rules.color.required).toBe(true);
130
+ expect(rules.color.allowedTypes).toContain('nominal');
131
+ });
132
+
133
+ it('supports theta and radius as optional', () => {
134
+ const optionals = getOptionalChannels('arc');
135
+ expect(optionals).toContain('theta');
136
+ expect(optionals).toContain('radius');
137
+ });
138
+ });
139
+
140
+ describe('text encoding rules', () => {
141
+ it('requires text channel', () => {
142
+ const rules = MARK_ENCODING_RULES.text;
143
+ expect(rules.text!.required).toBe(true);
144
+ });
145
+
146
+ it('has optional x and y', () => {
147
+ const rules = MARK_ENCODING_RULES.text;
148
+ expect(rules.x.required).toBe(false);
149
+ expect(rules.y.required).toBe(false);
150
+ });
151
+
152
+ it('supports size as optional quantitative', () => {
153
+ const rules = MARK_ENCODING_RULES.text;
154
+ expect(rules.size.required).toBe(false);
155
+ expect(rules.size.allowedTypes).toContain('quantitative');
156
+ });
157
+ });
158
+
159
+ describe('rule encoding rules', () => {
160
+ it('has optional x, y, x2, y2', () => {
161
+ const rules = MARK_ENCODING_RULES.rule;
162
+ expect(rules.x.required).toBe(false);
163
+ expect(rules.y.required).toBe(false);
164
+ expect(rules.x2!.required).toBe(false);
165
+ expect(rules.y2!.required).toBe(false);
166
+ });
167
+
168
+ it('supports strokeDash as optional', () => {
169
+ expect(getOptionalChannels('rule')).toContain('strokeDash');
170
+ });
171
+ });
172
+
173
+ describe('tick encoding rules', () => {
174
+ it('requires x and y', () => {
175
+ expect(getRequiredChannels('tick')).toContain('x');
176
+ expect(getRequiredChannels('tick')).toContain('y');
177
+ });
178
+
179
+ it('supports opacity, tooltip, href, detail', () => {
180
+ const optionals = getOptionalChannels('tick');
181
+ expect(optionals).toContain('opacity');
182
+ expect(optionals).toContain('tooltip');
183
+ expect(optionals).toContain('href');
184
+ expect(optionals).toContain('detail');
185
+ });
186
+ });
187
+
188
+ describe('rect encoding rules', () => {
189
+ it('requires x and y', () => {
190
+ expect(getRequiredChannels('rect')).toContain('x');
191
+ expect(getRequiredChannels('rect')).toContain('y');
192
+ });
193
+
194
+ it('supports x2, y2, opacity, order', () => {
195
+ const optionals = getOptionalChannels('rect');
196
+ expect(optionals).toContain('x2');
197
+ expect(optionals).toContain('y2');
198
+ expect(optionals).toContain('opacity');
199
+ expect(optionals).toContain('order');
200
+ });
201
+ });
202
+
203
+ describe('common channels across marks', () => {
204
+ it('tooltip is optional on all mark types', () => {
205
+ const allTypes: MarkType[] = [
206
+ 'bar',
207
+ 'line',
208
+ 'area',
209
+ 'point',
210
+ 'circle',
211
+ 'arc',
212
+ 'text',
213
+ 'rule',
214
+ 'tick',
215
+ 'rect',
216
+ ];
217
+ for (const type of allTypes) {
218
+ const rules = MARK_ENCODING_RULES[type];
219
+ if (rules.tooltip) {
220
+ expect(rules.tooltip.required).toBe(false);
221
+ }
222
+ }
223
+ });
224
+
225
+ it('href is optional on all mark types that have it', () => {
226
+ const allTypes: MarkType[] = [
227
+ 'bar',
228
+ 'line',
229
+ 'area',
230
+ 'point',
231
+ 'circle',
232
+ 'arc',
233
+ 'text',
234
+ 'rule',
235
+ 'tick',
236
+ 'rect',
237
+ ];
238
+ for (const type of allTypes) {
239
+ const rules = MARK_ENCODING_RULES[type];
240
+ if (rules.href) {
241
+ expect(rules.href.required).toBe(false);
242
+ }
243
+ }
244
+ });
245
+
246
+ it('opacity is optional quantitative on all mark types that have it', () => {
247
+ const allTypes: MarkType[] = [
248
+ 'bar',
249
+ 'line',
250
+ 'area',
251
+ 'point',
252
+ 'circle',
253
+ 'arc',
254
+ 'text',
255
+ 'rule',
256
+ 'tick',
257
+ 'rect',
258
+ ];
259
+ for (const type of allTypes) {
260
+ const rules = MARK_ENCODING_RULES[type];
261
+ if (rules.opacity) {
262
+ expect(rules.opacity.required).toBe(false);
263
+ expect(rules.opacity.allowedTypes).toContain('quantitative');
264
+ }
265
+ }
266
+ });
267
+ });
@@ -5,7 +5,6 @@ import type {
5
5
  GraphSpec,
6
6
  RangeAnnotation,
7
7
  TableSpec,
8
- TextAnnotation,
9
8
  VizSpec,
10
9
  } from '../spec';
11
10
  import {
@@ -16,6 +15,7 @@ import {
16
15
  isRefLineAnnotation,
17
16
  isTableSpec,
18
17
  isTextAnnotation,
18
+ MARK_TYPES,
19
19
  } from '../spec';
20
20
 
21
21
  // ---------------------------------------------------------------------------
@@ -24,7 +24,7 @@ import {
24
24
 
25
25
  function makeChartSpec(overrides?: Partial<ChartSpec>): ChartSpec {
26
26
  return {
27
- type: 'line',
27
+ mark: 'line',
28
28
  data: [
29
29
  { date: '2020-01', value: 42 },
30
30
  { date: '2020-02', value: 45 },
@@ -69,11 +69,22 @@ function makeGraphSpec(overrides?: Partial<GraphSpec>): GraphSpec {
69
69
  // ---------------------------------------------------------------------------
70
70
 
71
71
  describe('isChartSpec', () => {
72
- it('returns true for all chart types', () => {
73
- const chartTypes = ['line', 'area', 'bar', 'column', 'pie', 'donut', 'dot', 'scatter'] as const;
74
-
75
- for (const chartType of chartTypes) {
76
- const spec = makeChartSpec({ type: chartType });
72
+ it('returns true for all mark types', () => {
73
+ const markTypes = [
74
+ 'line',
75
+ 'area',
76
+ 'bar',
77
+ 'point',
78
+ 'circle',
79
+ 'arc',
80
+ 'text',
81
+ 'rule',
82
+ 'tick',
83
+ 'rect',
84
+ ] as const;
85
+
86
+ for (const markType of markTypes) {
87
+ const spec = makeChartSpec({ mark: markType });
77
88
  expect(isChartSpec(spec)).toBe(true);
78
89
  }
79
90
  });
@@ -136,25 +147,40 @@ describe('type guard mutual exclusivity', () => {
136
147
  });
137
148
 
138
149
  // ---------------------------------------------------------------------------
139
- // CHART_TYPES constant tests
150
+ // MARK_TYPES constant tests
140
151
  // ---------------------------------------------------------------------------
141
152
 
142
- describe('CHART_TYPES', () => {
143
- it('contains all 8 chart types', () => {
144
- expect(CHART_TYPES.size).toBe(8);
153
+ describe('MARK_TYPES', () => {
154
+ it('contains all 10 mark types', () => {
155
+ expect(MARK_TYPES.size).toBe(10);
145
156
  });
146
157
 
147
158
  it('contains expected types', () => {
148
- const expected = ['line', 'area', 'bar', 'column', 'pie', 'donut', 'dot', 'scatter'];
159
+ const expected = [
160
+ 'line',
161
+ 'area',
162
+ 'bar',
163
+ 'point',
164
+ 'circle',
165
+ 'arc',
166
+ 'text',
167
+ 'rule',
168
+ 'tick',
169
+ 'rect',
170
+ ];
149
171
  for (const t of expected) {
150
- expect(CHART_TYPES.has(t)).toBe(true);
172
+ expect(MARK_TYPES.has(t)).toBe(true);
151
173
  }
152
174
  });
153
175
 
154
- it('does not contain non-chart types', () => {
155
- expect(CHART_TYPES.has('table')).toBe(false);
156
- expect(CHART_TYPES.has('graph')).toBe(false);
157
- expect(CHART_TYPES.has('map')).toBe(false);
176
+ it('does not contain non-mark types', () => {
177
+ expect(MARK_TYPES.has('table')).toBe(false);
178
+ expect(MARK_TYPES.has('graph')).toBe(false);
179
+ expect(MARK_TYPES.has('map')).toBe(false);
180
+ });
181
+
182
+ it('CHART_TYPES is an alias for MARK_TYPES', () => {
183
+ expect(CHART_TYPES).toBe(MARK_TYPES);
158
184
  });
159
185
  });
160
186
 
@@ -205,7 +231,7 @@ describe('isRangeAnnotation', () => {
205
231
  });
206
232
 
207
233
  it('returns false for text annotations', () => {
208
- const annotation: TextAnnotation = {
234
+ const annotation: Annotation = {
209
235
  type: 'text',
210
236
  x: 10,
211
237
  y: 20,
@@ -227,7 +253,7 @@ describe('isRefLineAnnotation', () => {
227
253
  });
228
254
 
229
255
  it('returns false for text annotations', () => {
230
- const annotation: TextAnnotation = {
256
+ const annotation: Annotation = {
231
257
  type: 'text',
232
258
  x: 10,
233
259
  y: 20,
@@ -273,14 +299,14 @@ describe('annotation type guard mutual exclusivity', () => {
273
299
  describe('type-level spec construction', () => {
274
300
  it('allows a fully featured chart spec', () => {
275
301
  const spec: ChartSpec = {
276
- type: 'line',
302
+ mark: 'line',
277
303
  data: [{ date: '2020-01', value: 42, series: 'US' }],
278
304
  encoding: {
279
305
  x: { field: 'date', type: 'temporal' },
280
306
  y: {
281
307
  field: 'value',
282
308
  type: 'quantitative',
283
- axis: { label: 'GDP Growth (%)' },
309
+ axis: { title: 'GDP Growth (%)' },
284
310
  scale: { zero: true, nice: true },
285
311
  },
286
312
  color: { field: 'series', type: 'nominal' },
@@ -322,12 +348,25 @@ describe('type-level spec construction', () => {
322
348
  };
323
349
 
324
350
  // If this compiles and type-checks, the types are correct.
325
- expect(spec.type).toBe('line');
351
+ expect(spec.mark).toBe('line');
326
352
  expect(spec.data).toHaveLength(1);
327
353
  expect(spec.encoding.x?.field).toBe('date');
328
354
  expect(spec.annotations).toHaveLength(3);
329
355
  });
330
356
 
357
+ it('allows a chart spec with mark object', () => {
358
+ const spec: ChartSpec = {
359
+ mark: { type: 'line', interpolate: 'step', point: true },
360
+ data: [{ x: 1, y: 2 }],
361
+ encoding: {
362
+ x: { field: 'x', type: 'quantitative' },
363
+ y: { field: 'y', type: 'quantitative' },
364
+ },
365
+ };
366
+
367
+ expect(typeof spec.mark).toBe('object');
368
+ });
369
+
331
370
  it('allows a table spec with column configs', () => {
332
371
  const spec: TableSpec = {
333
372
  type: 'table',