ggaction 0.0.7 → 0.0.9
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/CHANGELOG.md +42 -0
- package/README.md +33 -5
- package/knowledge/action-cards.json +10941 -0
- package/knowledge/intent-taxonomy.json +183 -0
- package/knowledge/mcp-resources.json +95 -0
- package/knowledge/task-packet.schema.json +159 -0
- package/knowledge/task-resolver.js +1230 -0
- package/package.json +10 -1
- package/src/actions/basic.js +3 -3
- package/src/actions/categoryOrder/index.js +111 -0
- package/src/actions/coordinates/actions.js +4 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/timeUnit.js +48 -0
- package/src/actions/encodings/angle.js +77 -0
- package/src/actions/encodings/color/index.js +17 -13
- package/src/actions/encodings/color/layout.js +2 -0
- package/src/actions/encodings/color/policy.js +3 -0
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/policies/area.js +40 -2
- package/src/actions/encodings/position/policies/bar.js +6 -0
- package/src/actions/encodings/position/policies/index.js +2 -0
- package/src/actions/encodings/position/policies/line.js +5 -1
- package/src/actions/encodings/position/policies/tick.js +19 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +93 -2
- package/src/actions/guides/legends/categorical/symbols.js +2 -76
- package/src/actions/guides/legends/continuous/common.js +17 -5
- package/src/actions/guides/legends/continuous/gradient.js +12 -8
- package/src/actions/guides/legends/continuous/opacity.js +70 -17
- package/src/actions/guides/legends/edit.js +13 -8
- package/src/actions/guides/legends/lane.js +468 -0
- package/src/actions/guides/legends/remove.js +3 -7
- package/src/actions/index.js +3 -1
- package/src/actions/marks/area/actions.js +3 -1
- package/src/actions/marks/area/materialize.js +12 -3
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/point/materialize.js +17 -5
- package/src/actions/marks/tick/actions.js +225 -0
- package/src/actions/marks/tick/index.js +1 -0
- package/src/actions/primitives/index.js +27 -0
- package/src/actions/primitives/semantic.js +22 -185
- package/src/actions/primitives/semanticAction.js +188 -0
- package/src/actions/primitives/semanticValidation/dataset.js +7 -3
- package/src/actions/primitives/semanticValidation/index.js +28 -15
- package/src/actions/primitives/semanticValidation/layer.js +22 -16
- package/src/actions/scales/consumers/index.js +8 -0
- package/src/actions/scales/consumers/seriesLayout.js +22 -2
- package/src/actions/scales/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- package/src/core/vocabulary.js +7 -3
- package/src/grammar/areaSeries.js +112 -2
- package/src/grammar/categoryOrder.js +138 -0
- package/src/grammar/direction.js +46 -0
- package/src/grammar/facets/index.js +1 -1
- package/src/grammar/pointShapes.js +34 -6
- package/src/grammar/positionCompatibility.js +4 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/seriesLayout.js +10 -2
- package/src/grammar/timeUnit.js +108 -0
- package/src/grammar/transformTopology.js +18 -0
- package/src/grammar/transforms.js +25 -18
- package/src/grammar/window.js +73 -7
- package/src/layout/legendLane.js +338 -0
- package/src/materialization/dataProvenance.js +2 -2
- package/src/materialization/marks/capabilities.js +9 -0
- package/src/materialization/marks/index.js +2 -1
- package/src/materialization/marks/pathOrder.js +2 -2
- package/src/materialization/marks/policies.js +13 -0
- package/src/materialization/scales/policies/series.js +9 -0
- package/src/materialization/scales/resolve.js +25 -3
- package/src/materialization/selection/items/index.js +1 -0
- package/src/materialization/selection/items/path.js +4 -1
- package/src/materialization/selection/items/tick.js +42 -0
- package/src/materialization/selection/policies/index.js +2 -0
- package/src/materialization/selection/policies/tick.js +10 -0
- package/src/mcp/adapter.js +206 -0
- package/src/mcp/cli.js +11 -0
- package/src/mcp/server.js +101 -0
- package/types/index.d.ts +10 -0
- package/types/program.d.ts +96 -3
- package/src/actions/coordinates/index.js +0 -5
- package/src/actions/primitives/semanticValue.js +0 -1
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
6
|
+
const cardsArtifact = JSON.parse(readFileSync(
|
|
7
|
+
path.join(root, "knowledge/action-cards.json"),
|
|
8
|
+
"utf8"
|
|
9
|
+
));
|
|
10
|
+
const taxonomy = JSON.parse(readFileSync(
|
|
11
|
+
path.join(root, "knowledge/intent-taxonomy.json"),
|
|
12
|
+
"utf8"
|
|
13
|
+
));
|
|
14
|
+
|
|
15
|
+
const cards = new Map(cardsArtifact.cards.map(card => [card.name, card]));
|
|
16
|
+
const constraints = new Map(taxonomy.constraints.map(constraint => [constraint.id, constraint]));
|
|
17
|
+
const mainRuntimeImports = Object.freeze(["hconcat", "vconcat", "render"]);
|
|
18
|
+
const rendererImports = Object.freeze({
|
|
19
|
+
renderToSVG: Object.freeze({ entry: "ggaction/svg", output: "svg" }),
|
|
20
|
+
renderToPNG: Object.freeze({ entry: "ggaction/png", output: "png" }),
|
|
21
|
+
renderToPDF: Object.freeze({ entry: "ggaction/pdf", output: "pdf" })
|
|
22
|
+
});
|
|
23
|
+
const authoringPrerequisiteNames = Object.freeze(["createCanvas", "createData"]);
|
|
24
|
+
const authoringCanvasOptions = Object.freeze({
|
|
25
|
+
width: "800",
|
|
26
|
+
height: "600",
|
|
27
|
+
margin: "{ top: 140, right: 220, bottom: 120, left: 260 }"
|
|
28
|
+
});
|
|
29
|
+
const facadeGuideOwners = new Set([
|
|
30
|
+
"createScatterPlot",
|
|
31
|
+
"createLinePlot",
|
|
32
|
+
"createBarPlot",
|
|
33
|
+
"createBoxPlot",
|
|
34
|
+
"createGradientPlot",
|
|
35
|
+
"createViolinPlot"
|
|
36
|
+
]);
|
|
37
|
+
const standaloneGuideNames = new Set([
|
|
38
|
+
"createAxes",
|
|
39
|
+
"createXAxis",
|
|
40
|
+
"createYAxis",
|
|
41
|
+
"createGrid",
|
|
42
|
+
"createLegend",
|
|
43
|
+
"createGuides"
|
|
44
|
+
]);
|
|
45
|
+
const docsResourceByDecision = Object.freeze({
|
|
46
|
+
"chart.type": "ggaction://docs/choose-chart-type",
|
|
47
|
+
"renderer.format": "ggaction://docs/choose-renderer",
|
|
48
|
+
"query.intent": "ggaction://docs/getting-started"
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export class TaskPacketBudgetError extends Error {
|
|
52
|
+
constructor(bytes) {
|
|
53
|
+
super(`Compact task packet is ${bytes} bytes; the hard ceiling is 6144 bytes.`);
|
|
54
|
+
this.name = "TaskPacketBudgetError";
|
|
55
|
+
this.bytes = bytes;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unique(values) {
|
|
60
|
+
return [...new Set(values)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalize(value) {
|
|
64
|
+
return value
|
|
65
|
+
.normalize("NFKC")
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
68
|
+
.trim()
|
|
69
|
+
.replace(/\s+/g, " ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function phraseOccurrences(query, phrase) {
|
|
73
|
+
const normalizedPhrase = normalize(phrase);
|
|
74
|
+
const paddedQuery = ` ${query} `;
|
|
75
|
+
const paddedPhrase = ` ${normalizedPhrase} `;
|
|
76
|
+
const occurrences = [];
|
|
77
|
+
let offset = 0;
|
|
78
|
+
while (offset < paddedQuery.length) {
|
|
79
|
+
const start = paddedQuery.indexOf(paddedPhrase, offset);
|
|
80
|
+
if (start === -1) break;
|
|
81
|
+
occurrences.push({ start: start + 1, end: start + paddedPhrase.length - 1 });
|
|
82
|
+
offset = start + 1;
|
|
83
|
+
}
|
|
84
|
+
return occurrences;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function exactActionNames(query) {
|
|
88
|
+
return cardsArtifact.cards
|
|
89
|
+
.filter(card => new RegExp(
|
|
90
|
+
`(?:^|[^A-Za-z0-9])${card.name}(?:$|[^A-Za-z0-9])`,
|
|
91
|
+
"i"
|
|
92
|
+
).test(query))
|
|
93
|
+
.map(card => card.name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function semanticMatchResult(normalizedQuery) {
|
|
97
|
+
const occurrences = taxonomy.constraints.flatMap(constraint =>
|
|
98
|
+
constraint.phrases.flatMap(phrase =>
|
|
99
|
+
phraseOccurrences(normalizedQuery, phrase).map(span => ({ constraint, ...span }))
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
const visible = occurrences.filter(occurrence =>
|
|
103
|
+
!occurrences.some(candidate =>
|
|
104
|
+
candidate.constraint.shadows?.includes(occurrence.constraint.id) &&
|
|
105
|
+
candidate.start <= occurrence.start &&
|
|
106
|
+
candidate.end >= occurrence.end
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
const matched = taxonomy.constraints.filter(constraint =>
|
|
110
|
+
visible.some(occurrence => occurrence.constraint.id === constraint.id)
|
|
111
|
+
);
|
|
112
|
+
const positions = new Map(matched.map(constraint => [
|
|
113
|
+
constraint.id,
|
|
114
|
+
Math.min(...visible
|
|
115
|
+
.filter(occurrence => occurrence.constraint.id === constraint.id)
|
|
116
|
+
.map(occurrence => occurrence.start))
|
|
117
|
+
]));
|
|
118
|
+
return { matched, positions };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function orderForCard(card) {
|
|
122
|
+
if (card.name === "createCanvas") return 10;
|
|
123
|
+
if (card.name === "createData") return 20;
|
|
124
|
+
if (card.name.endsWith("Data")) return 30;
|
|
125
|
+
if (card.domain === "charts" || (card.domain === "marks" && card.name.startsWith("create"))) return 40;
|
|
126
|
+
if (card.domain === "encodings") {
|
|
127
|
+
if (/^encode(X|Y|X2|Y2|XRange|YRange|Theta|R|Radius|ParallelCoordinates)/.test(card.name)) return 50;
|
|
128
|
+
if (card.name === "encodeGroup" || card.name === "encodePathOrder") return 52;
|
|
129
|
+
return 55;
|
|
130
|
+
}
|
|
131
|
+
if (card.domain === "statistics") return 60;
|
|
132
|
+
if (["axes", "grid", "legend_and_title"].includes(card.domain)) return 70;
|
|
133
|
+
if (card.domain === "mark-selection") return 75;
|
|
134
|
+
if (card.domain === "composition" || /^(layout|jitter|order|replace)/.test(card.name)) return 80;
|
|
135
|
+
return 65;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function exactProvider(name, matchedIds) {
|
|
139
|
+
const card = cards.get(name);
|
|
140
|
+
const related = taxonomy.providers
|
|
141
|
+
.filter(provider => provider.kind === "action" && provider.name === name)
|
|
142
|
+
.map(provider => ({
|
|
143
|
+
provider,
|
|
144
|
+
coverage: provider.covers.filter(constraint => matchedIds.has(constraint)).length
|
|
145
|
+
}))
|
|
146
|
+
.filter(entry => entry.coverage > 0)
|
|
147
|
+
.sort((left, right) =>
|
|
148
|
+
right.coverage - left.coverage ||
|
|
149
|
+
left.provider.order - right.provider.order ||
|
|
150
|
+
left.provider.id.localeCompare(right.provider.id)
|
|
151
|
+
)[0]?.provider;
|
|
152
|
+
|
|
153
|
+
if (!related) {
|
|
154
|
+
return {
|
|
155
|
+
id: `exact.${name}`,
|
|
156
|
+
kind: "action",
|
|
157
|
+
name,
|
|
158
|
+
order: orderForCard(card),
|
|
159
|
+
anchors: [`action.${name}`],
|
|
160
|
+
covers: [`action.${name}`],
|
|
161
|
+
exactCall: card.snippet,
|
|
162
|
+
exactOptionNames: card.options
|
|
163
|
+
.filter(option => new RegExp(`\\b${option.name}\\s*:`).test(card.snippet))
|
|
164
|
+
.map(option => option.name)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...related,
|
|
169
|
+
id: `exact.${name}`,
|
|
170
|
+
anchors: [`action.${name}`],
|
|
171
|
+
covers: unique([
|
|
172
|
+
`action.${name}`,
|
|
173
|
+
...related.covers.filter(constraint => matchedIds.has(constraint))
|
|
174
|
+
])
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function conflictResult(matched) {
|
|
179
|
+
const byGroup = new Map();
|
|
180
|
+
for (const constraint of matched) {
|
|
181
|
+
if (!constraint.exclusiveGroup) continue;
|
|
182
|
+
const group = byGroup.get(constraint.exclusiveGroup) ?? [];
|
|
183
|
+
group.push(constraint);
|
|
184
|
+
byGroup.set(constraint.exclusiveGroup, group);
|
|
185
|
+
}
|
|
186
|
+
const blocked = new Set();
|
|
187
|
+
const unresolved = [];
|
|
188
|
+
for (const [group, entries] of byGroup) {
|
|
189
|
+
if (entries.length < 2) continue;
|
|
190
|
+
for (const entry of entries) blocked.add(entry.id);
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
unresolved.push({
|
|
193
|
+
constraint: entry.id,
|
|
194
|
+
reason: `This conflicts within ${group}: ${entries.map(candidate => candidate.id).join(", ")}.`,
|
|
195
|
+
resources: ["ggaction://docs/legend-layout"]
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { blocked, unresolved };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function candidateProviders(supportedIds, exactNames) {
|
|
203
|
+
const candidates = taxonomy.providers.filter(provider =>
|
|
204
|
+
provider.anchors.some(anchor => supportedIds.has(anchor))
|
|
205
|
+
);
|
|
206
|
+
for (const name of exactNames) candidates.push(exactProvider(name, supportedIds));
|
|
207
|
+
return candidates;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function selectProviders(supportedIds, providers) {
|
|
211
|
+
const uncovered = new Set(supportedIds);
|
|
212
|
+
const selected = [];
|
|
213
|
+
while (uncovered.size > 0) {
|
|
214
|
+
const ranked = providers
|
|
215
|
+
.filter(provider => !selected.some(entry => entry.provider.id === provider.id))
|
|
216
|
+
.map(provider => ({
|
|
217
|
+
provider,
|
|
218
|
+
coverage: provider.covers.filter(constraint => uncovered.has(constraint))
|
|
219
|
+
}))
|
|
220
|
+
.filter(entry => entry.coverage.length > 0)
|
|
221
|
+
.sort((left, right) =>
|
|
222
|
+
right.coverage.length - left.coverage.length ||
|
|
223
|
+
left.provider.order - right.provider.order ||
|
|
224
|
+
left.provider.id.localeCompare(right.provider.id)
|
|
225
|
+
);
|
|
226
|
+
if (ranked.length === 0) break;
|
|
227
|
+
const winner = ranked[0];
|
|
228
|
+
selected.push(winner);
|
|
229
|
+
for (const constraint of winner.coverage) uncovered.delete(constraint);
|
|
230
|
+
}
|
|
231
|
+
return { selected, uncovered };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function adaptProviderDependencies(selected) {
|
|
235
|
+
let adapted = selected;
|
|
236
|
+
const hasStoredSelection = selected.some(entry => entry.provider.name === "selectMarks");
|
|
237
|
+
if (hasStoredSelection) {
|
|
238
|
+
adapted = adapted.map(entry => {
|
|
239
|
+
if (entry.provider.name !== "highlightMarks") return entry;
|
|
240
|
+
return {
|
|
241
|
+
...entry,
|
|
242
|
+
provider: {
|
|
243
|
+
...entry.provider,
|
|
244
|
+
baseOptions: {
|
|
245
|
+
selection: "\"selection-1\"",
|
|
246
|
+
color: "\"#f28e2b\""
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const hasRegression = adapted.some(entry =>
|
|
254
|
+
entry.provider.name === "createRegression" && !entry.provider.id.startsWith("exact.")
|
|
255
|
+
);
|
|
256
|
+
const hasPointSource = adapted.some(entry =>
|
|
257
|
+
["createPointMark", "createScatterPlot"].includes(entry.provider.name)
|
|
258
|
+
);
|
|
259
|
+
if (hasRegression && !hasPointSource) {
|
|
260
|
+
const point = taxonomy.providers.find(provider => provider.name === "createPointMark");
|
|
261
|
+
if (!point) throw new Error("createRegression requires the createPointMark provider.");
|
|
262
|
+
adapted = [{ provider: point, coverage: [] }, ...adapted];
|
|
263
|
+
}
|
|
264
|
+
const legendLayout = adapted.find(entry =>
|
|
265
|
+
entry.provider.name === "editLegendLayout" &&
|
|
266
|
+
entry.coverage.some(constraint => constraint.startsWith("layout.legend."))
|
|
267
|
+
);
|
|
268
|
+
if (legendLayout) {
|
|
269
|
+
const owner = adapted.find(entry =>
|
|
270
|
+
entry.coverage.includes("guide.legend") &&
|
|
271
|
+
(
|
|
272
|
+
entry.provider.name === "createLegend" ||
|
|
273
|
+
entry.provider.optionsByConstraint?.["guide.legend"]?.guides !== undefined
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
if (owner) {
|
|
277
|
+
const position = legendLayout.provider.baseOptions?.position;
|
|
278
|
+
if (position === undefined) {
|
|
279
|
+
throw new Error(`${legendLayout.provider.id} lacks a legend position.`);
|
|
280
|
+
}
|
|
281
|
+
const layoutConstraints = legendLayout.coverage.filter(constraint =>
|
|
282
|
+
constraint.startsWith("layout.legend.")
|
|
283
|
+
);
|
|
284
|
+
adapted = adapted.filter(entry => entry !== legendLayout).map(entry => {
|
|
285
|
+
if (entry !== owner) return entry;
|
|
286
|
+
const provider = entry.provider.name === "createLegend"
|
|
287
|
+
? {
|
|
288
|
+
...entry.provider,
|
|
289
|
+
baseOptions: {
|
|
290
|
+
...(entry.provider.baseOptions ?? {}),
|
|
291
|
+
position,
|
|
292
|
+
...(position === `"left"` ? { offset: "96" } : {})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
: {
|
|
296
|
+
...entry.provider,
|
|
297
|
+
optionsByConstraint: {
|
|
298
|
+
...(entry.provider.optionsByConstraint ?? {}),
|
|
299
|
+
"guide.legend": {
|
|
300
|
+
...(entry.provider.optionsByConstraint?.["guide.legend"] ?? {}),
|
|
301
|
+
guides: `{ legend: { position: ${position}${position === `"left"` ? ", offset: 96" : ""} } }`
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
provider,
|
|
307
|
+
coverage: [...entry.coverage, ...layoutConstraints]
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return adapted;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function withBaseOptions(entry, options) {
|
|
316
|
+
return {
|
|
317
|
+
...entry,
|
|
318
|
+
provider: {
|
|
319
|
+
...entry.provider,
|
|
320
|
+
baseOptions: { ...(entry.provider.baseOptions ?? {}), ...options }
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function dependencyEntry(name, baseOptions, coverage = []) {
|
|
326
|
+
const card = cards.get(name);
|
|
327
|
+
if (!card) throw new Error(`Unknown runtime dependency action ${name}.`);
|
|
328
|
+
return {
|
|
329
|
+
provider: {
|
|
330
|
+
id: `action.${name}`,
|
|
331
|
+
kind: "action",
|
|
332
|
+
name,
|
|
333
|
+
order: orderForCard(card),
|
|
334
|
+
anchors: [],
|
|
335
|
+
covers: [],
|
|
336
|
+
baseOptions
|
|
337
|
+
},
|
|
338
|
+
coverage
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function absorbFacadeGuides(entries) {
|
|
343
|
+
const owner = entries.find(entry =>
|
|
344
|
+
!entry.provider.id.startsWith("exact.") &&
|
|
345
|
+
facadeGuideOwners.has(entry.provider.name)
|
|
346
|
+
);
|
|
347
|
+
if (!owner) return entries;
|
|
348
|
+
const guides = entries.filter(entry =>
|
|
349
|
+
!entry.provider.id.startsWith("exact.") &&
|
|
350
|
+
standaloneGuideNames.has(entry.provider.name)
|
|
351
|
+
);
|
|
352
|
+
if (guides.length === 0) return entries;
|
|
353
|
+
const guideCoverage = guides.flatMap(entry => entry.coverage);
|
|
354
|
+
return entries
|
|
355
|
+
.filter(entry => !guides.includes(entry))
|
|
356
|
+
.map(entry => entry === owner
|
|
357
|
+
? {
|
|
358
|
+
...entry,
|
|
359
|
+
coverage: unique([...entry.coverage, ...guideCoverage])
|
|
360
|
+
}
|
|
361
|
+
: entry);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function replaceEntry(entries, target, replacements) {
|
|
365
|
+
const index = entries.indexOf(target);
|
|
366
|
+
if (index === -1) return entries;
|
|
367
|
+
return [
|
|
368
|
+
...entries.slice(0, index),
|
|
369
|
+
...replacements,
|
|
370
|
+
...entries.slice(index + 1)
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function orderInheritedTextOverlay(entries) {
|
|
375
|
+
const semantic = name => entries.find(entry =>
|
|
376
|
+
entry.provider.name === name && !entry.provider.id.startsWith("exact.")
|
|
377
|
+
);
|
|
378
|
+
const point = semantic("createPointMark");
|
|
379
|
+
const text = semantic("createTextMark");
|
|
380
|
+
const textEncoding = semantic("encodeText");
|
|
381
|
+
const pointEncodingNames = new Set([
|
|
382
|
+
"encodeX",
|
|
383
|
+
"encodeY",
|
|
384
|
+
"encodeColor",
|
|
385
|
+
"encodeSize",
|
|
386
|
+
"encodeShape",
|
|
387
|
+
"encodeAngle",
|
|
388
|
+
"encodeOpacity"
|
|
389
|
+
]);
|
|
390
|
+
const pointEncodings = entries.filter(entry =>
|
|
391
|
+
!entry.provider.id.startsWith("exact.") &&
|
|
392
|
+
pointEncodingNames.has(entry.provider.name)
|
|
393
|
+
);
|
|
394
|
+
if (
|
|
395
|
+
!point ||
|
|
396
|
+
!text ||
|
|
397
|
+
!textEncoding ||
|
|
398
|
+
!pointEncodings.some(entry => entry.provider.name === "encodeX") ||
|
|
399
|
+
!pointEncodings.some(entry => entry.provider.name === "encodeY")
|
|
400
|
+
) return entries;
|
|
401
|
+
|
|
402
|
+
const ordered = [point, ...pointEncodings, text, textEncoding];
|
|
403
|
+
const controlled = new Set(ordered);
|
|
404
|
+
const first = Math.min(...ordered.map(entry => entries.indexOf(entry)));
|
|
405
|
+
const insertion = entries
|
|
406
|
+
.slice(0, first)
|
|
407
|
+
.filter(entry => !controlled.has(entry)).length;
|
|
408
|
+
const remaining = entries.filter(entry => !controlled.has(entry));
|
|
409
|
+
return [
|
|
410
|
+
...remaining.slice(0, insertion),
|
|
411
|
+
...ordered,
|
|
412
|
+
...remaining.slice(insertion)
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function orderBarCategoryBeforeMeasure(entries) {
|
|
417
|
+
const bar = entries.find(entry =>
|
|
418
|
+
entry.provider.name === "createBarMark" &&
|
|
419
|
+
!entry.provider.id.startsWith("exact.")
|
|
420
|
+
);
|
|
421
|
+
if (!bar) return entries;
|
|
422
|
+
const barIndex = entries.indexOf(bar);
|
|
423
|
+
const nextMarkIndex = entries.findIndex((entry, index) =>
|
|
424
|
+
index > barIndex &&
|
|
425
|
+
!entry.provider.id.startsWith("exact.") &&
|
|
426
|
+
entry.provider.name.startsWith("create") &&
|
|
427
|
+
(entry.provider.name.endsWith("Mark") || entry.provider.name.endsWith("Plot"))
|
|
428
|
+
);
|
|
429
|
+
const limit = nextMarkIndex === -1 ? entries.length : nextMarkIndex;
|
|
430
|
+
const positions = entries.slice(barIndex + 1, limit).filter(entry =>
|
|
431
|
+
["encodeX", "encodeY"].includes(entry.provider.name) &&
|
|
432
|
+
!entry.provider.id.startsWith("exact.")
|
|
433
|
+
);
|
|
434
|
+
const categorical = positions.find(entry =>
|
|
435
|
+
['"nominal"', '"ordinal"', '"temporal"'].includes(
|
|
436
|
+
entry.provider.baseOptions?.fieldType
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
const quantitative = positions.find(entry =>
|
|
440
|
+
entry.provider.baseOptions?.fieldType === '"quantitative"'
|
|
441
|
+
);
|
|
442
|
+
if (
|
|
443
|
+
!categorical ||
|
|
444
|
+
!quantitative ||
|
|
445
|
+
entries.indexOf(categorical) < entries.indexOf(quantitative)
|
|
446
|
+
) return entries;
|
|
447
|
+
const reordered = [...entries];
|
|
448
|
+
const categoricalIndex = reordered.indexOf(categorical);
|
|
449
|
+
const quantitativeIndex = reordered.indexOf(quantitative);
|
|
450
|
+
reordered[quantitativeIndex] = categorical;
|
|
451
|
+
reordered[categoricalIndex] = quantitative;
|
|
452
|
+
return reordered;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function closeRuntimeDependencies(entries) {
|
|
456
|
+
let closed = absorbFacadeGuides(entries);
|
|
457
|
+
const semantic = name => closed.find(entry =>
|
|
458
|
+
entry.provider.name === name && !entry.provider.id.startsWith("exact.")
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const explicitData = semantic("createData");
|
|
462
|
+
if (explicitData) {
|
|
463
|
+
closed = closed.map(entry => entry === explicitData
|
|
464
|
+
? withBaseOptions(entry, { values: "values" })
|
|
465
|
+
: entry);
|
|
466
|
+
}
|
|
467
|
+
const explicitCanvas = semantic("createCanvas");
|
|
468
|
+
if (explicitCanvas) {
|
|
469
|
+
closed = closed.map(entry => entry === explicitCanvas
|
|
470
|
+
? withBaseOptions(entry, authoringCanvasOptions)
|
|
471
|
+
: entry);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const scatter = semantic("createScatterPlot");
|
|
475
|
+
if (scatter) {
|
|
476
|
+
closed = closed.map(entry => entry === scatter
|
|
477
|
+
? withBaseOptions(entry, {
|
|
478
|
+
x: `{ field: "x", fieldType: "quantitative" }`,
|
|
479
|
+
y: `{ field: "y", fieldType: "quantitative" }`
|
|
480
|
+
})
|
|
481
|
+
: entry);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const line = semantic("createLinePlot");
|
|
485
|
+
const lineOpacity = semantic("encodeOpacity");
|
|
486
|
+
if (line && lineOpacity) {
|
|
487
|
+
closed = closed
|
|
488
|
+
.filter(entry => entry !== lineOpacity)
|
|
489
|
+
.map(entry => entry === line
|
|
490
|
+
? {
|
|
491
|
+
...withBaseOptions(entry, { line: "{ opacity: 0.8 }" }),
|
|
492
|
+
coverage: unique([...entry.coverage, ...lineOpacity.coverage])
|
|
493
|
+
}
|
|
494
|
+
: entry);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const barPlot = semantic("createBarPlot");
|
|
498
|
+
if (barPlot) {
|
|
499
|
+
const hasColorScale = semantic("createScale")?.provider.id === "action.createColorScale";
|
|
500
|
+
const color = `{ field: "category", scale: { id: "color-scale" } }`;
|
|
501
|
+
closed = closed.map(entry => {
|
|
502
|
+
if (entry !== barPlot) return entry;
|
|
503
|
+
const configured = withBaseOptions(entry, {
|
|
504
|
+
x: `{ field: "category", fieldType: "nominal" }`,
|
|
505
|
+
y: `{ field: "value", fieldType: "quantitative" }`,
|
|
506
|
+
...(hasColorScale ? { color } : {})
|
|
507
|
+
});
|
|
508
|
+
return hasColorScale
|
|
509
|
+
? {
|
|
510
|
+
...configured,
|
|
511
|
+
provider: {
|
|
512
|
+
...configured.provider,
|
|
513
|
+
optionsByConstraint: {
|
|
514
|
+
...(configured.provider.optionsByConstraint ?? {}),
|
|
515
|
+
"encoding.color": { color }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
: configured;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const timeUnitData = semantic("createTimeUnitData");
|
|
524
|
+
const timeUnitBar = semantic("createBarPlot");
|
|
525
|
+
if (timeUnitData && timeUnitBar) {
|
|
526
|
+
closed = closed.map(entry => entry === timeUnitBar
|
|
527
|
+
? withBaseOptions(entry, {
|
|
528
|
+
data: `"monthly"`,
|
|
529
|
+
x: `{ field: "month", fieldType: "ordinal" }`,
|
|
530
|
+
y: `{ field: "value", fieldType: "quantitative" }`
|
|
531
|
+
})
|
|
532
|
+
: entry);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const boxOwner = semantic("createBoxPlot");
|
|
536
|
+
if (boxOwner) {
|
|
537
|
+
closed = closed.map(entry => entry === boxOwner
|
|
538
|
+
? withBaseOptions(entry, {
|
|
539
|
+
x: `{ field: "category", fieldType: "nominal" }`,
|
|
540
|
+
y: `{ field: "value", fieldType: "quantitative" }`
|
|
541
|
+
})
|
|
542
|
+
: entry);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const windowData = semantic("createWindowData");
|
|
546
|
+
const windowLine = semantic("createLinePlot");
|
|
547
|
+
if (windowData && windowLine) {
|
|
548
|
+
const timeScale = semantic("createScale")?.provider.id === "action.createTimeScale";
|
|
549
|
+
closed = closed.map(entry => entry === windowLine
|
|
550
|
+
? withBaseOptions(entry, {
|
|
551
|
+
data: `"windowed"`,
|
|
552
|
+
x: timeScale
|
|
553
|
+
? `{ field: "date", fieldType: "temporal", scale: { id: "scale-1" } }`
|
|
554
|
+
: `{ field: "x", fieldType: "quantitative" }`,
|
|
555
|
+
y: `{ field: "movingMean", fieldType: "quantitative" }`
|
|
556
|
+
})
|
|
557
|
+
: entry);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const intervalData = semantic("createIntervalData");
|
|
561
|
+
const boxPlot = semantic("createBoxPlot");
|
|
562
|
+
const errorBar = semantic("createErrorBar");
|
|
563
|
+
if (intervalData && boxPlot && errorBar) {
|
|
564
|
+
closed = closed.map(entry => {
|
|
565
|
+
if (entry === intervalData) {
|
|
566
|
+
return withBaseOptions(entry, {
|
|
567
|
+
source: `"data"`,
|
|
568
|
+
groupBy: `"category"`
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (entry === boxPlot) {
|
|
572
|
+
return withBaseOptions(entry, {
|
|
573
|
+
data: `"data"`,
|
|
574
|
+
x: `{ field: "category", fieldType: "nominal" }`,
|
|
575
|
+
y: `{ field: "value", fieldType: "quantitative" }`
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (entry === errorBar) {
|
|
579
|
+
return withBaseOptions(entry, {
|
|
580
|
+
data: `"interval"`,
|
|
581
|
+
x: `{ field: "category", fieldType: "nominal", scale: { id: "errorBarX" } }`,
|
|
582
|
+
y: `{ center: "__interval_center", lower: "__interval_lower", upper: "__interval_upper", scale: { id: "errorBarY" } }`
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return entry;
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const densityData = semantic("createDensityData");
|
|
590
|
+
const violinPlot = semantic("createViolinPlot");
|
|
591
|
+
if (densityData && violinPlot) {
|
|
592
|
+
closed = closed
|
|
593
|
+
.filter(entry => entry !== densityData)
|
|
594
|
+
.map(entry => entry === violinPlot
|
|
595
|
+
? {
|
|
596
|
+
...withBaseOptions(entry, {
|
|
597
|
+
data: `"data"`,
|
|
598
|
+
...(entry.coverage.includes("guide.legend")
|
|
599
|
+
? { color: `"category"` }
|
|
600
|
+
: {})
|
|
601
|
+
}),
|
|
602
|
+
coverage: unique([...entry.coverage, ...densityData.coverage])
|
|
603
|
+
}
|
|
604
|
+
: entry);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const gradientPlot = semantic("createGradientPlot");
|
|
608
|
+
const logarithmicScale = semantic("createScale")?.provider.id === "action.createLogScale"
|
|
609
|
+
? semantic("createScale")
|
|
610
|
+
: undefined;
|
|
611
|
+
if (gradientPlot) {
|
|
612
|
+
closed = closed.map(entry => entry === gradientPlot
|
|
613
|
+
? withBaseOptions(entry, {
|
|
614
|
+
data: `"data"`,
|
|
615
|
+
x: `{ field: "value", fieldType: "quantitative"${logarithmicScale ? `, scale: { id: "scale-1" }` : ""} }`,
|
|
616
|
+
y: `{ field: "category", fieldType: "nominal" }`
|
|
617
|
+
})
|
|
618
|
+
: entry);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const density = semantic("encodeDensity");
|
|
622
|
+
const hasArea = semantic("createAreaMark");
|
|
623
|
+
if (density && !hasArea) {
|
|
624
|
+
const area = dependencyEntry("createAreaMark", { id: `"densityArea"` });
|
|
625
|
+
const densityIndex = closed.indexOf(density);
|
|
626
|
+
closed = [
|
|
627
|
+
...closed.slice(0, densityIndex),
|
|
628
|
+
area,
|
|
629
|
+
...closed.slice(densityIndex)
|
|
630
|
+
].map(entry => entry === density
|
|
631
|
+
? withBaseOptions(entry, { target: `"densityArea"` })
|
|
632
|
+
: entry);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const bin2d = semantic("createBin2DData");
|
|
636
|
+
const rect = semantic("createRectMark");
|
|
637
|
+
if (bin2d && rect) {
|
|
638
|
+
closed = closed.map(entry => {
|
|
639
|
+
if (entry === bin2d) return withBaseOptions(entry, { source: `"data"` });
|
|
640
|
+
if (entry === rect) {
|
|
641
|
+
return withBaseOptions(entry, { id: `"rect"`, data: `"bins"` });
|
|
642
|
+
}
|
|
643
|
+
if (entry.provider.id === "action.createColorScale") {
|
|
644
|
+
return withBaseOptions(entry, {
|
|
645
|
+
type: `"sequential"`,
|
|
646
|
+
range: `{ palette: "viridis" }`
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return entry;
|
|
650
|
+
});
|
|
651
|
+
const x = semantic("encodeX");
|
|
652
|
+
const y = semantic("encodeY");
|
|
653
|
+
if (x) {
|
|
654
|
+
closed = replaceEntry(closed, x, [
|
|
655
|
+
withBaseOptions(x, {
|
|
656
|
+
field: `"__bins_x0"`,
|
|
657
|
+
fieldType: `"quantitative"`,
|
|
658
|
+
target: `"rect"`
|
|
659
|
+
}),
|
|
660
|
+
dependencyEntry("encodeX2", {
|
|
661
|
+
field: `"__bins_x1"`,
|
|
662
|
+
fieldType: `"quantitative"`,
|
|
663
|
+
target: `"rect"`
|
|
664
|
+
})
|
|
665
|
+
]);
|
|
666
|
+
}
|
|
667
|
+
if (y) {
|
|
668
|
+
closed = replaceEntry(closed, y, [
|
|
669
|
+
withBaseOptions(y, {
|
|
670
|
+
field: `"__bins_y0"`,
|
|
671
|
+
fieldType: `"quantitative"`,
|
|
672
|
+
target: `"rect"`
|
|
673
|
+
}),
|
|
674
|
+
dependencyEntry("encodeY2", {
|
|
675
|
+
field: `"__bins_y1"`,
|
|
676
|
+
fieldType: `"quantitative"`,
|
|
677
|
+
target: `"rect"`
|
|
678
|
+
})
|
|
679
|
+
]);
|
|
680
|
+
}
|
|
681
|
+
const color = semantic("encodeColor");
|
|
682
|
+
if (color) {
|
|
683
|
+
closed = closed.map(entry => entry === color
|
|
684
|
+
? withBaseOptions(entry, {
|
|
685
|
+
field: `"__bins_count"`,
|
|
686
|
+
fieldType: `"quantitative"`,
|
|
687
|
+
target: `"rect"`,
|
|
688
|
+
scale: `{ id: "color-scale" }`
|
|
689
|
+
})
|
|
690
|
+
: entry);
|
|
691
|
+
}
|
|
692
|
+
const legend = semantic("createLegend");
|
|
693
|
+
if (legend) {
|
|
694
|
+
closed = closed.map(entry => entry === legend
|
|
695
|
+
? withBaseOptions(entry, { target: `"rect"`, channels: `["color"]` })
|
|
696
|
+
: entry);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let currentMark;
|
|
701
|
+
let currentMarkKind;
|
|
702
|
+
let pendingScale;
|
|
703
|
+
const defaultMarkIds = Object.freeze({
|
|
704
|
+
createPointMark: "point",
|
|
705
|
+
createLineMark: "line",
|
|
706
|
+
createAreaMark: "area",
|
|
707
|
+
createBarMark: "bar",
|
|
708
|
+
createRuleMark: "rule",
|
|
709
|
+
createArcMark: "arc",
|
|
710
|
+
createRectMark: "rect",
|
|
711
|
+
createTextMark: "text",
|
|
712
|
+
createScatterPlot: "scatterPlot",
|
|
713
|
+
createLinePlot: "linePlot",
|
|
714
|
+
createBarPlot: "barPlot",
|
|
715
|
+
createBoxPlot: "boxPlot",
|
|
716
|
+
createGradientPlot: "gradientPlot",
|
|
717
|
+
createViolinPlot: "violinPlot"
|
|
718
|
+
});
|
|
719
|
+
const markKinds = Object.freeze({
|
|
720
|
+
createPointMark: "point",
|
|
721
|
+
createLineMark: "line",
|
|
722
|
+
createAreaMark: "area",
|
|
723
|
+
createBarMark: "bar",
|
|
724
|
+
createRuleMark: "rule",
|
|
725
|
+
createArcMark: "arc",
|
|
726
|
+
createRectMark: "rect",
|
|
727
|
+
createTextMark: "text",
|
|
728
|
+
createScatterPlot: "point",
|
|
729
|
+
createLinePlot: "line",
|
|
730
|
+
createBarPlot: "bar",
|
|
731
|
+
createBoxPlot: "bar",
|
|
732
|
+
createGradientPlot: "rect",
|
|
733
|
+
createViolinPlot: "area"
|
|
734
|
+
});
|
|
735
|
+
closed = closed.map(entry => {
|
|
736
|
+
if (entry.provider.id.startsWith("exact.")) return entry;
|
|
737
|
+
if (entry.provider.name === "createScale") {
|
|
738
|
+
pendingScale = entry.provider.id === "action.createColorScale"
|
|
739
|
+
? undefined
|
|
740
|
+
: entry.provider.baseOptions?.id;
|
|
741
|
+
return entry;
|
|
742
|
+
}
|
|
743
|
+
const created = defaultMarkIds[entry.provider.name];
|
|
744
|
+
if (created !== undefined) {
|
|
745
|
+
currentMark = entry.provider.baseOptions?.id?.replaceAll('"', "") ?? created;
|
|
746
|
+
currentMarkKind = markKinds[entry.provider.name];
|
|
747
|
+
return entry;
|
|
748
|
+
}
|
|
749
|
+
if (!currentMark || entry.provider.name === "createErrorBar") return entry;
|
|
750
|
+
const options = {};
|
|
751
|
+
if (
|
|
752
|
+
["area", "rule", "arc"].includes(currentMarkKind) &&
|
|
753
|
+
["encodeX", "encodeY", "encodeR"].includes(entry.provider.name)
|
|
754
|
+
) {
|
|
755
|
+
options.fieldType = `"quantitative"`;
|
|
756
|
+
}
|
|
757
|
+
if (entry.provider.name === "encodeTheta" && currentMarkKind === "arc") {
|
|
758
|
+
options.fieldType = `"ordinal"`;
|
|
759
|
+
}
|
|
760
|
+
if (currentMarkKind === "bar" && entry.provider.name === "encodeX") {
|
|
761
|
+
options.field = `"category"`;
|
|
762
|
+
options.fieldType = `"nominal"`;
|
|
763
|
+
}
|
|
764
|
+
if (currentMarkKind === "bar" && entry.provider.name === "encodeY") {
|
|
765
|
+
options.field = `"value"`;
|
|
766
|
+
options.fieldType = `"quantitative"`;
|
|
767
|
+
}
|
|
768
|
+
if (pendingScale && ["encodeX", "encodeY"].includes(entry.provider.name)) {
|
|
769
|
+
options.scale = `{ id: ${pendingScale} }`;
|
|
770
|
+
pendingScale = undefined;
|
|
771
|
+
}
|
|
772
|
+
return Object.keys(options).length === 0 ? entry : withBaseOptions(entry, options);
|
|
773
|
+
});
|
|
774
|
+
return orderInheritedTextOverlay(orderBarCategoryBeforeMeasure(closed));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function providerRequestPosition(entry, positions) {
|
|
778
|
+
const matchedPositions = entry.coverage
|
|
779
|
+
.map(constraint => positions.get(constraint))
|
|
780
|
+
.filter(position => position !== undefined);
|
|
781
|
+
return matchedPositions.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...matchedPositions);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function mergeOptionValues(provider, covered) {
|
|
785
|
+
const options = new Map(Object.entries(provider.baseOptions ?? {}));
|
|
786
|
+
for (const constraint of covered) {
|
|
787
|
+
for (const [name, value] of Object.entries(
|
|
788
|
+
provider.optionsByConstraint?.[constraint] ?? {}
|
|
789
|
+
)) {
|
|
790
|
+
const previous = options.get(name);
|
|
791
|
+
if (previous !== undefined && previous !== value) {
|
|
792
|
+
if (name === "guides" && (previous === "{}" || value === "{}")) {
|
|
793
|
+
options.set(name, previous === "{}" ? value : previous);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
throw new Error(
|
|
797
|
+
`${provider.id} assigns conflicting values to option ${name}.`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
options.set(name, value);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return options;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function actionCall(provider, options) {
|
|
807
|
+
if (provider.exactCall) return provider.exactCall;
|
|
808
|
+
const card = cards.get(provider.name);
|
|
809
|
+
if (card.options.length === 0) return `program.${provider.name}()`;
|
|
810
|
+
const body = [...options]
|
|
811
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
812
|
+
.join(", ");
|
|
813
|
+
return `program.${provider.name}(${body.length === 0 ? "{}" : `{ ${body} }`})`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function planEntry(entry, step) {
|
|
817
|
+
const { provider, coverage } = entry;
|
|
818
|
+
const options = mergeOptionValues(provider, coverage);
|
|
819
|
+
if (provider.kind === "runtime") {
|
|
820
|
+
return {
|
|
821
|
+
plan: {
|
|
822
|
+
step,
|
|
823
|
+
id: provider.id,
|
|
824
|
+
kind: provider.kind,
|
|
825
|
+
name: provider.name,
|
|
826
|
+
constraints: coverage,
|
|
827
|
+
requiredOptions: [],
|
|
828
|
+
signature: provider.signature,
|
|
829
|
+
route: provider.route
|
|
830
|
+
},
|
|
831
|
+
call: provider.call
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const card = cards.get(provider.name);
|
|
835
|
+
return {
|
|
836
|
+
plan: {
|
|
837
|
+
step,
|
|
838
|
+
id: provider.id,
|
|
839
|
+
kind: provider.kind,
|
|
840
|
+
name: provider.name,
|
|
841
|
+
constraints: coverage,
|
|
842
|
+
requiredOptions: provider.exactOptionNames ?? [...options.keys()],
|
|
843
|
+
signature: card.signature,
|
|
844
|
+
route: card.route
|
|
845
|
+
},
|
|
846
|
+
call: actionCall(provider, options)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function authoringImports(entries) {
|
|
851
|
+
const runtimeNames = new Set(entries
|
|
852
|
+
.filter(entry => entry.plan.kind === "runtime")
|
|
853
|
+
.map(entry => entry.plan.name));
|
|
854
|
+
const mainNames = [
|
|
855
|
+
"chart",
|
|
856
|
+
...mainRuntimeImports.filter(name => runtimeNames.has(name))
|
|
857
|
+
];
|
|
858
|
+
return [
|
|
859
|
+
`import { ${mainNames.join(", ")} } from "ggaction";`,
|
|
860
|
+
...Object.entries(rendererImports)
|
|
861
|
+
.filter(([name]) => runtimeNames.has(name))
|
|
862
|
+
.map(([name, renderer]) => `import { ${name} } from "${renderer.entry}";`)
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function authoringSteps(entries) {
|
|
867
|
+
const outputRenderers = entries.filter(entry => rendererImports[entry.plan.name]);
|
|
868
|
+
return entries.map(entry => {
|
|
869
|
+
if (entry.plan.kind === "action" || ["hconcat", "vconcat"].includes(entry.plan.name)) {
|
|
870
|
+
return `program = ${entry.call}`;
|
|
871
|
+
}
|
|
872
|
+
if (entry.plan.name === "render") return entry.call;
|
|
873
|
+
const renderer = rendererImports[entry.plan.name];
|
|
874
|
+
if (!renderer) throw new Error(`Unknown authoring runtime ${entry.plan.name}.`);
|
|
875
|
+
const outputName = outputRenderers.length === 1
|
|
876
|
+
? "output"
|
|
877
|
+
: `${renderer.output}Output`;
|
|
878
|
+
return `const ${outputName} = ${entry.call}`;
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function authoringBlock(entries) {
|
|
883
|
+
const plannedPrerequisites = new Set(entries
|
|
884
|
+
.map(entry => entry.plan.id)
|
|
885
|
+
.filter(id => authoringPrerequisiteNames.some(name => id === `action.${name}`)));
|
|
886
|
+
return {
|
|
887
|
+
imports: authoringImports(entries),
|
|
888
|
+
initialize: "let program = chart()",
|
|
889
|
+
prerequisites: authoringPrerequisiteNames
|
|
890
|
+
.filter(name => !plannedPrerequisites.has(`action.${name}`))
|
|
891
|
+
.map(name => {
|
|
892
|
+
const card = cards.get(name);
|
|
893
|
+
return {
|
|
894
|
+
id: `action.${name}`,
|
|
895
|
+
signature: card.signature,
|
|
896
|
+
call: name === "createCanvas"
|
|
897
|
+
? "program = program.createCanvas({ width: 800, height: 600, margin: { top: 140, right: 220, bottom: 120, left: 260 } })"
|
|
898
|
+
: "program = program.createData({ values })",
|
|
899
|
+
bindings: name === "createData" ? ["values"] : []
|
|
900
|
+
};
|
|
901
|
+
}),
|
|
902
|
+
steps: authoringSteps(entries)
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function hasIncompleteRulePrimaryPair(entries) {
|
|
907
|
+
let insideRule = false;
|
|
908
|
+
let endpoints = new Set();
|
|
909
|
+
const incomplete = () =>
|
|
910
|
+
insideRule &&
|
|
911
|
+
endpoints.has("encodeX") &&
|
|
912
|
+
endpoints.has("encodeY") &&
|
|
913
|
+
!endpoints.has("encodeX2") &&
|
|
914
|
+
!endpoints.has("encodeY2");
|
|
915
|
+
for (const entry of entries) {
|
|
916
|
+
if (entry.provider.id.startsWith("exact.")) continue;
|
|
917
|
+
const name = entry.provider.name;
|
|
918
|
+
if (
|
|
919
|
+
name.startsWith("create") &&
|
|
920
|
+
(name.endsWith("Mark") || name.endsWith("Plot"))
|
|
921
|
+
) {
|
|
922
|
+
if (incomplete()) return true;
|
|
923
|
+
insideRule = name === "createRuleMark";
|
|
924
|
+
endpoints = new Set();
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (
|
|
928
|
+
insideRule &&
|
|
929
|
+
["encodeX", "encodeY", "encodeX2", "encodeY2"].includes(name)
|
|
930
|
+
) {
|
|
931
|
+
endpoints.add(name);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return incomplete();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function unconsumedScaleIds(entries) {
|
|
938
|
+
const scaleEntries = entries.filter(entry =>
|
|
939
|
+
entry.provider.name === "createScale" &&
|
|
940
|
+
!entry.provider.id.startsWith("exact.")
|
|
941
|
+
);
|
|
942
|
+
return scaleEntries
|
|
943
|
+
.filter(scale => {
|
|
944
|
+
const id = scale.provider.baseOptions?.id;
|
|
945
|
+
if (id === undefined) return false;
|
|
946
|
+
const reference = `id: ${id}`;
|
|
947
|
+
return !entries.some(entry =>
|
|
948
|
+
entry !== scale &&
|
|
949
|
+
[...mergeOptionValues(entry.provider, entry.coverage).values()]
|
|
950
|
+
.some(value => value.includes(reference))
|
|
951
|
+
);
|
|
952
|
+
})
|
|
953
|
+
.map(scale => scale.provider.baseOptions.id.replaceAll('"', ""));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function runtimeClosureDecisions(entries) {
|
|
957
|
+
const names = new Set(entries
|
|
958
|
+
.filter(entry => !entry.provider.id.startsWith("exact."))
|
|
959
|
+
.map(entry => entry.provider.name));
|
|
960
|
+
const unresolved = [];
|
|
961
|
+
const unsupported = [];
|
|
962
|
+
const markCreators = [...names].filter(name =>
|
|
963
|
+
name.startsWith("create") && (name.endsWith("Mark") || name.endsWith("Plot"))
|
|
964
|
+
);
|
|
965
|
+
const hasChartOwner = markCreators.length > 0 ||
|
|
966
|
+
[...names].some(name => ["createHistogram", "createHeatmap", "createParallelCoordinates"].includes(name));
|
|
967
|
+
|
|
968
|
+
if (
|
|
969
|
+
["selectMarks", "filterMarks", "highlightMarks", "facet"].some(name => names.has(name)) &&
|
|
970
|
+
!hasChartOwner
|
|
971
|
+
) {
|
|
972
|
+
unresolved.push(unresolvedDecision(
|
|
973
|
+
"chart.type",
|
|
974
|
+
"Selection and faceting require one explicit chart or mark owner before those actions can be addressed."
|
|
975
|
+
));
|
|
976
|
+
}
|
|
977
|
+
if (
|
|
978
|
+
["hconcat", "vconcat"].some(name => names.has(name)) &&
|
|
979
|
+
!hasChartOwner
|
|
980
|
+
) {
|
|
981
|
+
unresolved.push(unresolvedDecision(
|
|
982
|
+
"composition.children",
|
|
983
|
+
"Composition requires at least two complete child ChartPrograms; name or provide the child charts first."
|
|
984
|
+
));
|
|
985
|
+
}
|
|
986
|
+
if (
|
|
987
|
+
names.has("layoutLabels") && names.has("createTextMark") &&
|
|
988
|
+
!names.has("encodeX") && !names.has("encodeY")
|
|
989
|
+
) {
|
|
990
|
+
unresolved.push(unresolvedDecision(
|
|
991
|
+
"encoding.position",
|
|
992
|
+
"Collision-aware labels require a positioned source layer or explicit x and y encodings."
|
|
993
|
+
));
|
|
994
|
+
}
|
|
995
|
+
if (
|
|
996
|
+
names.has("createLegend") &&
|
|
997
|
+
!["encodeColor", "encodeSize", "encodeShape", "encodeOpacity", "encodeStrokeDash", "encodeStrokeWidth"]
|
|
998
|
+
.some(name => names.has(name))
|
|
999
|
+
) {
|
|
1000
|
+
unresolved.push(unresolvedDecision(
|
|
1001
|
+
"guide.legend.channel",
|
|
1002
|
+
"A legend requires an explicit compatible visual encoding such as color, size, shape, opacity, dash, or width."
|
|
1003
|
+
));
|
|
1004
|
+
}
|
|
1005
|
+
if (hasIncompleteRulePrimaryPair(entries)) {
|
|
1006
|
+
unresolved.push(unresolvedDecision(
|
|
1007
|
+
"encoding.rule.endpoint",
|
|
1008
|
+
"A rule with both x and y primary positions also requires x2 or y2; otherwise choose one primary position for a full-span rule."
|
|
1009
|
+
));
|
|
1010
|
+
}
|
|
1011
|
+
for (const id of unconsumedScaleIds(entries)) {
|
|
1012
|
+
unresolved.push(unresolvedDecision(
|
|
1013
|
+
"scale.consumer",
|
|
1014
|
+
`Scale "${id}" is not connected to a compatible encoding; choose the channel that should consume it.`
|
|
1015
|
+
));
|
|
1016
|
+
}
|
|
1017
|
+
if (names.has("createAreaMark") && names.has("encodeStrokeDash")) {
|
|
1018
|
+
unsupported.push({
|
|
1019
|
+
constraint: "unsupported.areaStrokeDash",
|
|
1020
|
+
reason: "Field-driven stroke dash is not supported for area marks; use a line or rule mark for dash encoding."
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return { unresolved, unsupported };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function unresolvedDecision(constraint, reason) {
|
|
1027
|
+
const resource = docsResourceByDecision[constraint] ??
|
|
1028
|
+
(constraint.startsWith("layout.legend.")
|
|
1029
|
+
? "ggaction://docs/legend-layout"
|
|
1030
|
+
: "ggaction://docs/action-reference");
|
|
1031
|
+
return { constraint, reason, resources: [resource] };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function genericUnresolved(normalizedQuery, matchedIds, exactNames) {
|
|
1035
|
+
const unresolved = [];
|
|
1036
|
+
const taskSpecificMatches = [...matchedIds].filter(id =>
|
|
1037
|
+
!id.startsWith("renderer.") && !id.startsWith("unsupported.")
|
|
1038
|
+
);
|
|
1039
|
+
if (
|
|
1040
|
+
/\b(chart|plot)\b/.test(normalizedQuery) &&
|
|
1041
|
+
taskSpecificMatches.length === 0 &&
|
|
1042
|
+
![...matchedIds].some(id => id.startsWith("unsupported.")) &&
|
|
1043
|
+
exactNames.length === 0
|
|
1044
|
+
) {
|
|
1045
|
+
unresolved.push(unresolvedDecision(
|
|
1046
|
+
"chart.type",
|
|
1047
|
+
"A chart or mark type is required; for example scatter plot, line chart, bar chart, or tick mark."
|
|
1048
|
+
));
|
|
1049
|
+
}
|
|
1050
|
+
if (
|
|
1051
|
+
/\b(render|export|output)\b/.test(normalizedQuery) &&
|
|
1052
|
+
![...matchedIds].some(id => id.startsWith("renderer."))
|
|
1053
|
+
) {
|
|
1054
|
+
unresolved.push(unresolvedDecision(
|
|
1055
|
+
"renderer.format",
|
|
1056
|
+
"A supported output format is required: Browser Canvas, SVG, PNG, or PDF."
|
|
1057
|
+
));
|
|
1058
|
+
}
|
|
1059
|
+
return unresolved;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function validateResolverKnowledge() {
|
|
1063
|
+
if (cardsArtifact.schemaVersion !== 1 || taxonomy.schemaVersion !== 2) {
|
|
1064
|
+
throw new Error("Compact action cards must use schemaVersion 1 and the intent taxonomy schemaVersion 2.");
|
|
1065
|
+
}
|
|
1066
|
+
if (constraints.size !== taxonomy.constraints.length) {
|
|
1067
|
+
throw new Error("Intent constraint IDs must be unique.");
|
|
1068
|
+
}
|
|
1069
|
+
const providerIds = new Set(taxonomy.providers.map(provider => provider.id));
|
|
1070
|
+
if (providerIds.size !== taxonomy.providers.length) {
|
|
1071
|
+
throw new Error("Intent provider IDs must be unique.");
|
|
1072
|
+
}
|
|
1073
|
+
const anchored = new Set();
|
|
1074
|
+
for (const constraint of taxonomy.constraints) {
|
|
1075
|
+
for (const shadowed of constraint.shadows ?? []) {
|
|
1076
|
+
if (!constraints.has(shadowed) || shadowed === constraint.id) {
|
|
1077
|
+
throw new Error(`${constraint.id} shadows invalid constraint ${shadowed}.`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const provider of taxonomy.providers) {
|
|
1082
|
+
for (const id of [...provider.anchors, ...provider.covers]) {
|
|
1083
|
+
if (!constraints.has(id)) throw new Error(`${provider.id} references unknown constraint ${id}.`);
|
|
1084
|
+
}
|
|
1085
|
+
if (provider.anchors.some(anchor => !provider.covers.includes(anchor))) {
|
|
1086
|
+
throw new Error(`${provider.id} does not cover every anchor.`);
|
|
1087
|
+
}
|
|
1088
|
+
for (const anchor of provider.anchors) anchored.add(anchor);
|
|
1089
|
+
if (provider.kind === "action") {
|
|
1090
|
+
const card = cards.get(provider.name);
|
|
1091
|
+
if (!card) throw new Error(`${provider.id} references unknown action ${provider.name}.`);
|
|
1092
|
+
const declaredOptions = new Set(card.options.map(option => option.name));
|
|
1093
|
+
const optionGroups = [
|
|
1094
|
+
provider.baseOptions ?? {},
|
|
1095
|
+
...Object.values(provider.optionsByConstraint ?? {})
|
|
1096
|
+
];
|
|
1097
|
+
for (const group of optionGroups) {
|
|
1098
|
+
for (const [name, value] of Object.entries(group)) {
|
|
1099
|
+
if (!declaredOptions.has(name)) {
|
|
1100
|
+
throw new Error(`${provider.id} references undeclared option ${name}.`);
|
|
1101
|
+
}
|
|
1102
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1103
|
+
throw new Error(`${provider.id}.${name} must be a JavaScript expression string.`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
} else if (
|
|
1108
|
+
provider.kind !== "runtime" ||
|
|
1109
|
+
typeof provider.signature !== "string" ||
|
|
1110
|
+
typeof provider.call !== "string" ||
|
|
1111
|
+
!provider.route.startsWith("/reference/")
|
|
1112
|
+
) {
|
|
1113
|
+
throw new Error(`${provider.id} has an invalid runtime contract.`);
|
|
1114
|
+
} else if (
|
|
1115
|
+
!mainRuntimeImports.includes(provider.name) &&
|
|
1116
|
+
rendererImports[provider.name] === undefined
|
|
1117
|
+
) {
|
|
1118
|
+
throw new Error(`${provider.id} lacks an authoring runtime mapping.`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const missing = taxonomy.constraints.filter(constraint =>
|
|
1122
|
+
constraint.unsupported === undefined && !anchored.has(constraint.id)
|
|
1123
|
+
);
|
|
1124
|
+
if (missing.length > 0) {
|
|
1125
|
+
throw new Error(`Supported constraints lack providers: ${missing.map(entry => entry.id).join(", ")}.`);
|
|
1126
|
+
}
|
|
1127
|
+
const invalidTerminal = taxonomy.constraints.filter(constraint =>
|
|
1128
|
+
(constraint.unsupported !== undefined) !== constraint.id.startsWith("unsupported.") ||
|
|
1129
|
+
(constraint.unsupported !== undefined && anchored.has(constraint.id))
|
|
1130
|
+
);
|
|
1131
|
+
if (invalidTerminal.length > 0) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
`Terminal constraints must use unsupported.* IDs without providers: ${invalidTerminal.map(entry => entry.id).join(", ")}.`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
for (const name of authoringPrerequisiteNames) {
|
|
1137
|
+
if (!cards.has(name)) throw new Error(`Missing authoring prerequisite action card: ${name}.`);
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
cards: cards.size,
|
|
1141
|
+
constraints: constraints.size,
|
|
1142
|
+
providers: taxonomy.providers.length,
|
|
1143
|
+
supported: taxonomy.constraints.filter(entry => entry.unsupported === undefined).length,
|
|
1144
|
+
unsupported: taxonomy.constraints.filter(entry => entry.unsupported !== undefined).length
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function searchGgaction(query) {
|
|
1149
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
1150
|
+
throw new TypeError("searchGgaction query must be a non-empty string.");
|
|
1151
|
+
}
|
|
1152
|
+
if (query.length > 500) {
|
|
1153
|
+
throw new RangeError("searchGgaction query must be at most 500 characters.");
|
|
1154
|
+
}
|
|
1155
|
+
validateResolverKnowledge();
|
|
1156
|
+
const normalizedQuery = normalize(query);
|
|
1157
|
+
const exactNames = exactActionNames(query);
|
|
1158
|
+
const { matched, positions } = semanticMatchResult(normalizedQuery);
|
|
1159
|
+
for (const name of exactNames) {
|
|
1160
|
+
const [occurrence] = phraseOccurrences(normalizedQuery, normalize(name));
|
|
1161
|
+
if (occurrence) positions.set(`action.${name}`, occurrence.start);
|
|
1162
|
+
}
|
|
1163
|
+
const matchedIds = new Set([
|
|
1164
|
+
...matched.map(constraint => constraint.id),
|
|
1165
|
+
...exactNames.map(name => `action.${name}`)
|
|
1166
|
+
]);
|
|
1167
|
+
const unsupported = matched
|
|
1168
|
+
.filter(constraint => constraint.unsupported !== undefined)
|
|
1169
|
+
.map(constraint => ({ constraint: constraint.id, reason: constraint.unsupported }));
|
|
1170
|
+
const unresolved = [];
|
|
1171
|
+
const { blocked, unresolved: conflicts } = conflictResult(matched);
|
|
1172
|
+
unresolved.push(...conflicts);
|
|
1173
|
+
|
|
1174
|
+
const supportedIds = new Set([...matchedIds].filter(id => {
|
|
1175
|
+
const constraint = constraints.get(id);
|
|
1176
|
+
return !blocked.has(id) && (constraint === undefined || constraint.unsupported === undefined);
|
|
1177
|
+
}));
|
|
1178
|
+
const providers = candidateProviders(supportedIds, exactNames);
|
|
1179
|
+
const { selected, uncovered } = selectProviders(supportedIds, providers);
|
|
1180
|
+
for (const constraint of uncovered) {
|
|
1181
|
+
unresolved.push(unresolvedDecision(
|
|
1182
|
+
constraint,
|
|
1183
|
+
"No current action or runtime operation covers this recognized constraint."
|
|
1184
|
+
));
|
|
1185
|
+
}
|
|
1186
|
+
unresolved.push(...genericUnresolved(normalizedQuery, matchedIds, exactNames));
|
|
1187
|
+
if (matchedIds.size === 0 && unresolved.length === 0) {
|
|
1188
|
+
unresolved.push(unresolvedDecision(
|
|
1189
|
+
"query.intent",
|
|
1190
|
+
"No current ggaction constraint was recognized; use an exact action name or a supported chart task."
|
|
1191
|
+
));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const ordered = closeRuntimeDependencies(adaptProviderDependencies(selected).sort((left, right) =>
|
|
1195
|
+
left.provider.order - right.provider.order ||
|
|
1196
|
+
providerRequestPosition(left, positions) - providerRequestPosition(right, positions) ||
|
|
1197
|
+
left.provider.id.localeCompare(right.provider.id)
|
|
1198
|
+
));
|
|
1199
|
+
const closure = runtimeClosureDecisions(ordered);
|
|
1200
|
+
if (unsupported.length === 0 && unresolved.length === 0) {
|
|
1201
|
+
unsupported.push(...closure.unsupported);
|
|
1202
|
+
unresolved.push(...closure.unresolved);
|
|
1203
|
+
}
|
|
1204
|
+
const entries = ordered.map((entry, index) => planEntry(entry, index + 1));
|
|
1205
|
+
const packet = {
|
|
1206
|
+
schemaVersion: 3,
|
|
1207
|
+
query: query.trim(),
|
|
1208
|
+
matchedConstraints: [...matchedIds],
|
|
1209
|
+
actionPlan: entries.map(entry => entry.plan),
|
|
1210
|
+
exactCalls: entries.map(entry => entry.call),
|
|
1211
|
+
authoring: authoringBlock(entries),
|
|
1212
|
+
unsupported,
|
|
1213
|
+
unresolved: unique(unresolved.map(entry => JSON.stringify(entry))).map(JSON.parse),
|
|
1214
|
+
candidates: entries.slice(0, 3).map(entry => ({
|
|
1215
|
+
id: entry.plan.id,
|
|
1216
|
+
kind: entry.plan.kind,
|
|
1217
|
+
name: entry.plan.name,
|
|
1218
|
+
route: entry.plan.route
|
|
1219
|
+
}))
|
|
1220
|
+
};
|
|
1221
|
+
const bytes = Buffer.byteLength(JSON.stringify(packet), "utf8");
|
|
1222
|
+
if (bytes > 6144) throw new TaskPacketBudgetError(bytes);
|
|
1223
|
+
return packet;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export function taskPacketBytes(packet) {
|
|
1227
|
+
return Buffer.byteLength(JSON.stringify(packet), "utf8");
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
validateResolverKnowledge();
|