@uwdata/mosaic-spec 0.5.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/LICENSE +28 -0
- package/README.md +57 -0
- package/dist/mosaic-spec.js +45445 -0
- package/dist/mosaic-spec.min.js +89 -0
- package/package.json +37 -0
- package/src/ast/ASTNode.js +18 -0
- package/src/ast/DataNode.js +226 -0
- package/src/ast/ExpressionNode.js +65 -0
- package/src/ast/HConcatNode.js +29 -0
- package/src/ast/HSpaceNode.js +25 -0
- package/src/ast/InputNode.js +34 -0
- package/src/ast/LiteralNode.js +21 -0
- package/src/ast/OptionsNode.js +52 -0
- package/src/ast/ParamNode.js +55 -0
- package/src/ast/ParamRefNode.js +22 -0
- package/src/ast/PlotAttributeNode.js +55 -0
- package/src/ast/PlotFromNode.js +44 -0
- package/src/ast/PlotInteractorNode.js +34 -0
- package/src/ast/PlotLegendNode.js +36 -0
- package/src/ast/PlotMarkNode.js +75 -0
- package/src/ast/PlotNode.js +66 -0
- package/src/ast/SelectionNode.js +26 -0
- package/src/ast/SpecNode.js +47 -0
- package/src/ast/TransformNode.js +118 -0
- package/src/ast/VConcatNode.js +28 -0
- package/src/ast/VSpaceNode.js +25 -0
- package/src/ast-to-dom.js +64 -0
- package/src/ast-to-esm.js +178 -0
- package/src/config/components.js +28 -0
- package/src/config/extensions.js +23 -0
- package/src/config/inputs.js +12 -0
- package/src/config/plots.js +58 -0
- package/src/config/transforms.js +35 -0
- package/src/constants.js +51 -0
- package/src/index.js +36 -0
- package/src/parse-spec.js +111 -0
- package/src/util.js +47 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uwdata/mosaic-spec",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Declarative specification of Mosaic-powered applications.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mosaic",
|
|
7
|
+
"visualization",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"declarative",
|
|
10
|
+
"specification"
|
|
11
|
+
],
|
|
12
|
+
"license": "BSD-3-Clause",
|
|
13
|
+
"author": "Jeffrey Heer (http://idl.cs.washington.edu)",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "src/index.js",
|
|
16
|
+
"module": "src/index.js",
|
|
17
|
+
"jsdelivr": "dist/mosaic-spec.min.js",
|
|
18
|
+
"unpkg": "dist/mosaic-spec.min.js",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/uwdata/mosaic.git"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"prebuild": "rimraf dist && mkdir dist",
|
|
25
|
+
"build": "node ../../esbuild.js mosaic-spec",
|
|
26
|
+
"lint": "eslint src test --ext .js",
|
|
27
|
+
"test": "mocha 'test/**/*-test.js'",
|
|
28
|
+
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@uwdata/mosaic-core": "^0.5.0",
|
|
32
|
+
"@uwdata/mosaic-sql": "^0.5.0",
|
|
33
|
+
"@uwdata/vgplot": "^0.5.0",
|
|
34
|
+
"isoformat": "^0.2.1"
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "92886dddfb126c1439924c5a0189e4639c3519a7"
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class ASTNode {
|
|
2
|
+
constructor(type, children = null) {
|
|
3
|
+
this.type = type;
|
|
4
|
+
this.children = children;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
instantiate(/* ctx */) {
|
|
8
|
+
throw Error('instantiate not implemented');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
codegen(/* ctx */) {
|
|
12
|
+
return Error('codegen not implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON() {
|
|
16
|
+
return Error('toJSON not implemented');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { create } from '@uwdata/mosaic-sql';
|
|
2
|
+
import { DATA } from '../constants.js';
|
|
3
|
+
import { isArray, isString } from '../util.js';
|
|
4
|
+
import { ASTNode } from './ASTNode.js';
|
|
5
|
+
import { parseOptions } from './OptionsNode.js';
|
|
6
|
+
|
|
7
|
+
export const TABLE_DATA = 'table';
|
|
8
|
+
export const PARQUET_DATA = 'parquet';
|
|
9
|
+
export const CSV_DATA = 'csv';
|
|
10
|
+
export const JSON_DATA = 'json';
|
|
11
|
+
export const SPATIAL_DATA = 'spatial';
|
|
12
|
+
|
|
13
|
+
const dataFormats = new Map([
|
|
14
|
+
[TABLE_DATA, parseTableData],
|
|
15
|
+
[PARQUET_DATA, parseParquetData],
|
|
16
|
+
[CSV_DATA, parseCSVData],
|
|
17
|
+
[JSON_DATA, parseJSONData],
|
|
18
|
+
[SPATIAL_DATA, parseSpatialData]
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function parseData(name, spec, ctx) {
|
|
22
|
+
spec = resolveDataSpec(spec);
|
|
23
|
+
if (dataFormats.has(spec.type)) {
|
|
24
|
+
const parse = dataFormats.get(spec.type);
|
|
25
|
+
return parse(name, spec, ctx);
|
|
26
|
+
} else {
|
|
27
|
+
ctx.error(`Unrecognized data format type.`, spec);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseTableData(name, spec, ctx) {
|
|
32
|
+
// eslint-disable-next-line no-unused-vars
|
|
33
|
+
const { query, type, ...options } = spec;
|
|
34
|
+
return new TableDataNode(name, query, parseOptions(options, ctx));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseParquetData(name, spec, ctx) {
|
|
38
|
+
// eslint-disable-next-line no-unused-vars
|
|
39
|
+
const { file, type, ...options } = spec;
|
|
40
|
+
return new ParquetDataNode(name, file, parseOptions(options, ctx));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseCSVData(name, spec, ctx) {
|
|
44
|
+
// eslint-disable-next-line no-unused-vars
|
|
45
|
+
const { file, type, ...options } = spec;
|
|
46
|
+
return new CSVDataNode(name, file, parseOptions(options, ctx));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseJSONData(name, spec, ctx) {
|
|
50
|
+
// eslint-disable-next-line no-unused-vars
|
|
51
|
+
const { data, file, type, ...options } = spec;
|
|
52
|
+
const opt = parseOptions(options, ctx);
|
|
53
|
+
return data
|
|
54
|
+
? new LiteralJSONDataNode(name, data, opt)
|
|
55
|
+
: new JSONDataNode(name, file, opt);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseSpatialData(name, spec, ctx) {
|
|
59
|
+
// eslint-disable-next-line no-unused-vars
|
|
60
|
+
const { file, type, ...options } = spec;
|
|
61
|
+
return new SpatialDataNode(name, file, parseOptions(options, ctx));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveDataSpec(spec) {
|
|
65
|
+
if (isArray(spec)) spec = { type: 'json', data: spec };
|
|
66
|
+
if (isString(spec)) spec = { type: 'table', query: spec };
|
|
67
|
+
return { ...spec, type: inferType(spec) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferType(spec) {
|
|
71
|
+
return spec.type
|
|
72
|
+
|| fileExtension(spec.file)
|
|
73
|
+
|| 'table';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fileExtension(file) {
|
|
77
|
+
const idx = file?.lastIndexOf('.');
|
|
78
|
+
return idx > 0 ? file.slice(idx + 1) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveFileURL(file, baseURL) {
|
|
82
|
+
return baseURL ? new URL(file, baseURL).toString() : file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function codegenOptions(options, ctx) {
|
|
86
|
+
const code = options?.codegen(ctx);
|
|
87
|
+
return code ? `, ${code}` : '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class DataNode extends ASTNode {
|
|
91
|
+
constructor(name, format) {
|
|
92
|
+
super(DATA);
|
|
93
|
+
this.name = name;
|
|
94
|
+
this.format = format;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class QueryDataNode extends DataNode {
|
|
99
|
+
constructor(name, format) {
|
|
100
|
+
super(name, format);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
instantiateQuery(ctx) {
|
|
104
|
+
ctx.error('instantiateQuery not implemented');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
codegenQuery(ctx) {
|
|
108
|
+
ctx.error('codegenQuery not implemented');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
instantiate(ctx) {
|
|
112
|
+
const query = this.instantiateQuery(ctx);
|
|
113
|
+
if (query) return query;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
codegen(ctx) {
|
|
117
|
+
const query = this.codegenQuery(ctx);
|
|
118
|
+
if (query) return query;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class TableDataNode extends QueryDataNode {
|
|
123
|
+
constructor(name, query, options) {
|
|
124
|
+
super(name, TABLE_DATA);
|
|
125
|
+
this.query = query?.trim();
|
|
126
|
+
this.options = options;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
instantiateQuery(ctx) {
|
|
130
|
+
const { name, query, options } = this;
|
|
131
|
+
if (query) {
|
|
132
|
+
return ctx.api.create(name, query, options.instantiate(ctx));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
codegenQuery(ctx) {
|
|
137
|
+
const { name, query, options } = this;
|
|
138
|
+
if (query) {
|
|
139
|
+
return `\`${create(name, query, options.instantiate(ctx))}\``;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toJSON() {
|
|
144
|
+
const { format, query, options } = this;
|
|
145
|
+
return { type: format, query, ...options.toJSON() };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class FileDataNode extends QueryDataNode {
|
|
150
|
+
constructor(name, format, method, file, options) {
|
|
151
|
+
super(name, format);
|
|
152
|
+
this.file = file;
|
|
153
|
+
this.method = method;
|
|
154
|
+
this.options = options;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
instantiateQuery(ctx) {
|
|
158
|
+
const { name, method, file, options } = this;
|
|
159
|
+
const url = resolveFileURL(file, ctx.baseURL);
|
|
160
|
+
const opt = options?.instantiate(ctx)
|
|
161
|
+
return ctx.api[method](name, url, opt);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
codegenQuery(ctx) {
|
|
165
|
+
const { name, method, file, options } = this;
|
|
166
|
+
const url = resolveFileURL(file, ctx.baseURL);
|
|
167
|
+
const opt = codegenOptions(options, ctx);
|
|
168
|
+
return `${ctx.ns()}${method}("${name}", "${url}"${opt})`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
toJSON() {
|
|
172
|
+
const { format, file, options } = this;
|
|
173
|
+
return { type: format, file, ...options.toJSON() };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class SpatialDataNode extends FileDataNode {
|
|
178
|
+
constructor(name, file, options) {
|
|
179
|
+
super(name, SPATIAL_DATA, 'loadSpatial', file, options);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class ParquetDataNode extends FileDataNode {
|
|
184
|
+
constructor(name, file, options) {
|
|
185
|
+
super(name, PARQUET_DATA, 'loadParquet', file, options);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class CSVDataNode extends FileDataNode {
|
|
190
|
+
constructor(name, file, options) {
|
|
191
|
+
super(name, CSV_DATA, 'loadCSV', file, options);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export class JSONDataNode extends FileDataNode {
|
|
196
|
+
constructor(name, file, options) {
|
|
197
|
+
super(name, JSON_DATA, 'loadJSON', file, options);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class LiteralJSONDataNode extends QueryDataNode {
|
|
202
|
+
constructor(name, data, options) {
|
|
203
|
+
super(name, JSON_DATA);
|
|
204
|
+
this.data = data;
|
|
205
|
+
this.options = options;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
instantiateQuery(ctx) {
|
|
209
|
+
const { name, data, options } = this;
|
|
210
|
+
return ctx.api.loadObjects(name, data, options.instantiate(ctx));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
codegenQuery(ctx) {
|
|
214
|
+
const { name, data, options } = this;
|
|
215
|
+
const opt = options ? ',' + options.codegen(ctx) : '';
|
|
216
|
+
const d = '[\n '
|
|
217
|
+
+ data.map(d => JSON.stringify(d)).join(',\n ')
|
|
218
|
+
+ '\n ]';
|
|
219
|
+
return `${ctx.ns()}loadObjects("${name}", ${d}${opt})`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
toJSON() {
|
|
223
|
+
const { format, data, options } = this;
|
|
224
|
+
return { type: format, data, ...options.toJSON() };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AGG, EXPRESSION, SQL } from '../constants.js';
|
|
2
|
+
import { ASTNode } from './ASTNode.js';
|
|
3
|
+
|
|
4
|
+
export function parseExpression(spec, ctx) {
|
|
5
|
+
const { label } = spec;
|
|
6
|
+
const key = spec[SQL] ? SQL
|
|
7
|
+
: spec[AGG] ? AGG
|
|
8
|
+
: ctx.error('Unrecognized expression type', spec);
|
|
9
|
+
|
|
10
|
+
const expr = spec[key];
|
|
11
|
+
const tokens = expr.split(/(\\'|\\"|"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\$\w+)/g);
|
|
12
|
+
const spans = [''];
|
|
13
|
+
const params = [];
|
|
14
|
+
|
|
15
|
+
for (let i = 0, k = 0; i < tokens.length; ++i) {
|
|
16
|
+
const tok = tokens[i];
|
|
17
|
+
if (tok.startsWith('$')) {
|
|
18
|
+
params[k] = ctx.maybeParam(tok);
|
|
19
|
+
spans[++k] = '';
|
|
20
|
+
} else {
|
|
21
|
+
spans[k] += tok;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new ExpressionNode(expr, spans, params, label, key === AGG);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ExpressionNode extends ASTNode {
|
|
29
|
+
constructor(value, spans, params, label, aggregate) {
|
|
30
|
+
super(EXPRESSION);
|
|
31
|
+
this.value = value;
|
|
32
|
+
this.spans = spans;
|
|
33
|
+
this.params = params;
|
|
34
|
+
this.label = label;
|
|
35
|
+
this.aggregate = aggregate;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
instantiate(ctx) {
|
|
39
|
+
const { spans, params, label, aggregate } = this;
|
|
40
|
+
const tag = ctx.api[aggregate ? AGG : SQL];
|
|
41
|
+
const args = params.map(e => e.instantiate(ctx));
|
|
42
|
+
return tag(spans, ...args).annotate({ label });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
codegen(ctx) {
|
|
46
|
+
const { spans, params, label, aggregate } = this;
|
|
47
|
+
const method = aggregate ? AGG : SQL;
|
|
48
|
+
|
|
49
|
+
// reconstitute expression string
|
|
50
|
+
let str = '';
|
|
51
|
+
const n = params.length;
|
|
52
|
+
for (let i = 0; i < n; ++i) {
|
|
53
|
+
str += spans[i] + '${' + params[i].codegen(ctx) + '}';
|
|
54
|
+
}
|
|
55
|
+
str += spans[n];
|
|
56
|
+
|
|
57
|
+
return `${ctx.ns()}${method}\`${str}\``
|
|
58
|
+
+ (label ? `.annotate({ label: ${JSON.stringify(label)} })` : '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toJSON() {
|
|
62
|
+
const key = this.aggregate ? AGG : SQL;
|
|
63
|
+
return { [key]: this.value };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { HCONCAT } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
export function parseHConcat(spec, ctx) {
|
|
5
|
+
const children = spec[HCONCAT].map(s => ctx.parseComponent(s));
|
|
6
|
+
return new HConcatNode(children);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class HConcatNode extends ASTNode {
|
|
10
|
+
|
|
11
|
+
constructor(children) {
|
|
12
|
+
super(HCONCAT, children);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
instantiate(ctx) {
|
|
16
|
+
return ctx.api[HCONCAT](this.children.map(c => c.instantiate(ctx)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
codegen(ctx) {
|
|
20
|
+
ctx.indent();
|
|
21
|
+
const items = this.children.map(c => c.codegen(ctx));
|
|
22
|
+
ctx.undent();
|
|
23
|
+
return `${ctx.tab()}${ctx.ns()}${this.type}(\n${items.join(',\n')}\n${ctx.tab()})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toJSON() {
|
|
27
|
+
return { [this.type]: this.children.map(c => c.toJSON()) };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { HSPACE } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
export function parseHSpace(spec) {
|
|
5
|
+
return new HSpaceNode(spec[HSPACE]);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class HSpaceNode extends ASTNode {
|
|
9
|
+
constructor(value) {
|
|
10
|
+
super(HSPACE);
|
|
11
|
+
this.value = value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
instantiate(ctx) {
|
|
15
|
+
return ctx.api[HSPACE](this.value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
codegen(ctx) {
|
|
19
|
+
return `${ctx.tab()}${ctx.ns()}${this.type}(${this.value})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJSON() {
|
|
23
|
+
return { [this.type]: this.value };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { INPUT } from '../constants.js';
|
|
3
|
+
import { parseOptions } from './OptionsNode.js';
|
|
4
|
+
|
|
5
|
+
export function parseInput(spec, ctx) {
|
|
6
|
+
const { [INPUT]: name, ...options } = spec;
|
|
7
|
+
if (!ctx.inputs?.has(name)) {
|
|
8
|
+
ctx.error(`Unrecognized input type: ${name}`, spec);
|
|
9
|
+
}
|
|
10
|
+
return new InputNode(name, parseOptions(options, ctx));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class InputNode extends ASTNode {
|
|
14
|
+
constructor(name, options) {
|
|
15
|
+
super(INPUT);
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
instantiate(ctx) {
|
|
21
|
+
const fn = ctx.api[this.name];
|
|
22
|
+
return fn(this.options.instantiate(ctx));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
codegen(ctx) {
|
|
26
|
+
const opt = this.options.codegen(ctx);
|
|
27
|
+
return `${ctx.tab()}${ctx.ns()}${this.name}(${opt})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
toJSON() {
|
|
31
|
+
const { type, name, options } = this;
|
|
32
|
+
return { [type]: name, ...options.toJSON() };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { LITERAL } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
export class LiteralNode extends ASTNode {
|
|
5
|
+
constructor(value) {
|
|
6
|
+
super(LITERAL);
|
|
7
|
+
this.value = value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
instantiate() {
|
|
11
|
+
return this.value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
codegen(ctx) {
|
|
15
|
+
return ctx.stringify(this.value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toJSON() {
|
|
19
|
+
return this.value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { OPTIONS } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
export function parseOptions(spec, ctx) {
|
|
5
|
+
const options = {};
|
|
6
|
+
for (const key in spec) {
|
|
7
|
+
options[key] = ctx.maybeSelection(spec[key]);
|
|
8
|
+
}
|
|
9
|
+
return new OptionsNode(options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class OptionsNode extends ASTNode {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(OPTIONS);
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
filter(predicate) {
|
|
19
|
+
const opt = Object.fromEntries(
|
|
20
|
+
Object.entries(this.options)
|
|
21
|
+
.filter(([key, value]) => predicate(key, value))
|
|
22
|
+
);
|
|
23
|
+
return new OptionsNode(opt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
instantiate(ctx) {
|
|
27
|
+
const { options } = this;
|
|
28
|
+
const opt = {};
|
|
29
|
+
for (const key in options) {
|
|
30
|
+
opt[key] = options[key].instantiate(ctx);
|
|
31
|
+
}
|
|
32
|
+
return opt;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
codegen(ctx) {
|
|
36
|
+
const { options } = this;
|
|
37
|
+
const opt = [];
|
|
38
|
+
for (const key in options) {
|
|
39
|
+
opt.push(`${key}: ${options[key].codegen(ctx)}`);
|
|
40
|
+
}
|
|
41
|
+
return opt.length ? `{${ctx.maybeLineWrap(opt)}}` : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toJSON() {
|
|
45
|
+
const { options } = this;
|
|
46
|
+
const opt = {};
|
|
47
|
+
for (const key in options) {
|
|
48
|
+
opt[key] = options[key].toJSON();
|
|
49
|
+
}
|
|
50
|
+
return opt;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parse as isoparse } from 'isoformat';
|
|
2
|
+
import { isArray, isObject } from '../util.js';
|
|
3
|
+
import { ASTNode } from './ASTNode.js';
|
|
4
|
+
import { CROSSFILTER, INTERSECT, PARAM, SINGLE, UNION, VALUE } from '../constants.js';
|
|
5
|
+
import { SelectionNode } from './SelectionNode.js';
|
|
6
|
+
|
|
7
|
+
const paramTypes = new Set([VALUE, SINGLE, CROSSFILTER, INTERSECT, UNION]);
|
|
8
|
+
|
|
9
|
+
export function parseParam(spec, ctx) {
|
|
10
|
+
const param = isObject(spec) ? spec : { value: spec };
|
|
11
|
+
const { select = VALUE, cross, date, value } = param;
|
|
12
|
+
if (!paramTypes.has(select)) {
|
|
13
|
+
ctx.error(`Unrecognized param type: ${select}`, param);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (select !== VALUE) {
|
|
17
|
+
return new SelectionNode(select, cross);
|
|
18
|
+
} else if (isArray(value)) {
|
|
19
|
+
return new ParamNode(value.map(v => ctx.maybeParam(v)));
|
|
20
|
+
} else {
|
|
21
|
+
return new ParamNode(value, date);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ParamNode extends ASTNode {
|
|
26
|
+
constructor(value, date) {
|
|
27
|
+
super(PARAM);
|
|
28
|
+
this.value = value;
|
|
29
|
+
this.date = date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
instantiate(ctx) {
|
|
33
|
+
const { date, value } = this;
|
|
34
|
+
const { Param } = ctx.api;
|
|
35
|
+
return isArray(value)
|
|
36
|
+
? Param.array(value.map(v => v.instantiate(ctx)))
|
|
37
|
+
: Param.value(isoparse(date, value));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
codegen(ctx) {
|
|
41
|
+
const { value, date } = this;
|
|
42
|
+
const prefix = `${ctx.ns()}Param.`;
|
|
43
|
+
return isArray(value)
|
|
44
|
+
? `${prefix}array([${value.map(v => v.codegen(ctx)).join(', ')}])`
|
|
45
|
+
: date ? `${prefix}value(new Date(${JSON.stringify(date)}))`
|
|
46
|
+
: `${prefix}value(${JSON.stringify(value)})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toJSON() {
|
|
50
|
+
const { date, value } = this;
|
|
51
|
+
return isArray(value) ? value.map(v => v.toJSON())
|
|
52
|
+
: date ? { date }
|
|
53
|
+
: value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { PARAMREF } from '../constants.js';
|
|
3
|
+
import { toParamRef } from '../util.js';
|
|
4
|
+
|
|
5
|
+
export class ParamRefNode extends ASTNode {
|
|
6
|
+
constructor(name) {
|
|
7
|
+
super(PARAMREF);
|
|
8
|
+
this.name = name;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
instantiate(ctx) {
|
|
12
|
+
return ctx.activeParams?.get(this.name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
codegen() {
|
|
16
|
+
return toParamRef(this.name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toJSON() {
|
|
20
|
+
return toParamRef(this.name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ASTNode } from './ASTNode.js';
|
|
2
|
+
import { ATTRIBUTE, FIXED, LITERAL } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
export function parseAttribute(key, value, ctx) {
|
|
5
|
+
if (!ctx.plot?.attributes?.has(key)) {
|
|
6
|
+
ctx.error(`Unrecognized attribute: ${key}`);
|
|
7
|
+
}
|
|
8
|
+
return new PlotAttributeNode(
|
|
9
|
+
key,
|
|
10
|
+
value === FIXED ? new PlotFixedNode : ctx.maybeParam(value)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PlotAttributeNode extends ASTNode {
|
|
15
|
+
constructor(name, value) {
|
|
16
|
+
super(ATTRIBUTE);
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.value = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
instantiate(ctx) {
|
|
22
|
+
const { name, value } = this;
|
|
23
|
+
const fn = ctx.api[name];
|
|
24
|
+
return fn(value.instantiate(ctx));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
codegen(ctx) {
|
|
28
|
+
const { name, value } = this;
|
|
29
|
+
return `${ctx.tab()}${ctx.ns()}${name}(${value.codegen(ctx)})`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toJSON() {
|
|
33
|
+
const { name, value } = this;
|
|
34
|
+
return { [name]: value.toJSON() };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class PlotFixedNode extends ASTNode {
|
|
39
|
+
constructor() {
|
|
40
|
+
super(LITERAL);
|
|
41
|
+
this.value = FIXED;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
instantiate(ctx) {
|
|
45
|
+
return ctx.api[FIXED];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
codegen(ctx) {
|
|
49
|
+
return `${ctx.ns()}${FIXED}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toJSON() {
|
|
53
|
+
return this.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isArray } from '../util.js';
|
|
2
|
+
import { ASTNode } from './ASTNode.js';
|
|
3
|
+
import { FROM } from '../constants.js';
|
|
4
|
+
import { LiteralNode } from './LiteralNode.js';
|
|
5
|
+
import { parseOptions } from './OptionsNode.js';
|
|
6
|
+
|
|
7
|
+
export function parseMarkData(spec, ctx) {
|
|
8
|
+
if (!spec) {
|
|
9
|
+
// no data, likely a decoration mark
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isArray(spec)) {
|
|
14
|
+
// data provided directly, treat as JSON literal
|
|
15
|
+
return new LiteralNode(spec);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { from: table, ...options } = spec;
|
|
19
|
+
return new PlotFromNode(table, parseOptions(options, ctx));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PlotFromNode extends ASTNode {
|
|
23
|
+
constructor(table, options) {
|
|
24
|
+
super(FROM);
|
|
25
|
+
this.table = table;
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
instantiate(ctx) {
|
|
30
|
+
const { table, options } = this;
|
|
31
|
+
return ctx.api[FROM](table, options.instantiate(ctx));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
codegen(ctx) {
|
|
35
|
+
const { type, table, options } = this;
|
|
36
|
+
const opt = options.codegen(ctx);
|
|
37
|
+
return `${ctx.ns()}${type}("${table}"${opt ? ', ' + opt : ''})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toJSON() {
|
|
41
|
+
const { type, table, options } = this;
|
|
42
|
+
return { [type]: table, ...options.toJSON() };
|
|
43
|
+
}
|
|
44
|
+
}
|