@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/dist/index.d.ts +495 -46
- package/dist/index.js +157 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/accessibility/__tests__/alt-text.test.ts +4 -4
- package/src/accessibility/alt-text.ts +13 -16
- package/src/helpers/__tests__/spec-builders.test.ts +8 -6
- package/src/helpers/spec-builders.ts +9 -8
- package/src/types/__tests__/encoding.test.ts +267 -0
- package/src/types/__tests__/spec.test.ts +61 -22
- package/src/types/encoding.ts +116 -35
- package/src/types/index.ts +32 -0
- package/src/types/layout.ts +131 -1
- package/src/types/spec.ts +492 -45
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
208
|
-
//
|
|
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.
|
|
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
|
-
|
|
160
|
+
mark: MarkType | MarkDef,
|
|
160
161
|
data: DataRow[],
|
|
161
162
|
encoding: Encoding,
|
|
162
163
|
options?: ChartBuilderOptions,
|
|
163
164
|
): ChartSpec {
|
|
164
|
-
const spec: ChartSpec = {
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
150
|
+
// MARK_TYPES constant tests
|
|
140
151
|
// ---------------------------------------------------------------------------
|
|
141
152
|
|
|
142
|
-
describe('
|
|
143
|
-
it('contains all
|
|
144
|
-
expect(
|
|
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 = [
|
|
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(
|
|
172
|
+
expect(MARK_TYPES.has(t)).toBe(true);
|
|
151
173
|
}
|
|
152
174
|
});
|
|
153
175
|
|
|
154
|
-
it('does not contain non-
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
157
|
-
expect(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
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',
|