ggaction 0.0.8 → 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 +22 -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/coordinates/actions.js +4 -0
- package/src/actions/encodings/position/policies/line.js +5 -1
- package/src/actions/guides/legends/categorical/index.js +85 -2
- package/src/actions/guides/legends/categorical/symbols.js +2 -76
- package/src/actions/index.js +1 -1
- 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 +20 -16
- package/src/grammar/transformTopology.js +18 -0
- package/src/grammar/transforms.js +21 -20
- package/src/materialization/dataProvenance.js +2 -2
- package/src/materialization/marks/pathOrder.js +2 -2
- package/src/mcp/adapter.js +206 -0
- package/src/mcp/cli.js +11 -0
- package/src/mcp/server.js +101 -0
- package/src/actions/coordinates/index.js +0 -5
- package/src/actions/primitives/semanticValue.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ggaction",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Build charts through immutable, traceable graphical actions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hyeon Jeon",
|
|
@@ -24,11 +24,19 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"src/",
|
|
26
26
|
"!src/**/AGENTS.md",
|
|
27
|
+
"knowledge/action-cards.json",
|
|
28
|
+
"knowledge/intent-taxonomy.json",
|
|
29
|
+
"knowledge/mcp-resources.json",
|
|
30
|
+
"knowledge/task-packet.schema.json",
|
|
31
|
+
"knowledge/task-resolver.js",
|
|
27
32
|
"types/",
|
|
28
33
|
"README.md",
|
|
29
34
|
"CHANGELOG.md",
|
|
30
35
|
"LICENSE"
|
|
31
36
|
],
|
|
37
|
+
"bin": {
|
|
38
|
+
"ggaction-mcp": "./src/mcp/cli.js"
|
|
39
|
+
},
|
|
32
40
|
"types": "./types/index.d.ts",
|
|
33
41
|
"exports": {
|
|
34
42
|
".": {
|
|
@@ -110,6 +118,7 @@
|
|
|
110
118
|
"tag": "latest"
|
|
111
119
|
},
|
|
112
120
|
"dependencies": {
|
|
121
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
113
122
|
"@napi-rs/canvas": "^1.0.2"
|
|
114
123
|
},
|
|
115
124
|
"devDependencies": {
|
package/src/actions/basic.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { registerBasicCanvasActions } from "./canvas/index.js";
|
|
2
2
|
import { registerBasicChartActions } from "./charts/basic.js";
|
|
3
|
-
import { registerCoordinateActions } from "./coordinates/
|
|
3
|
+
import { registerCoordinateActions } from "./coordinates/actions.js";
|
|
4
4
|
import { registerBasicDataActions } from "./data/basic.js";
|
|
5
5
|
import { registerBasicEncodingActions } from "./encodings/basic.js";
|
|
6
6
|
import { registerBasicGuideActions } from "./guides/basic.js";
|
|
7
7
|
import { registerBasicMarkActions } from "./marks/basic.js";
|
|
8
|
-
import {
|
|
8
|
+
import { registerBasicPrimitiveActions } from "./primitives/index.js";
|
|
9
9
|
import { registerBasicScaleActions } from "./scales/index.js";
|
|
10
10
|
|
|
11
11
|
export function registerBasicActions(ProgramClass) {
|
|
12
|
-
|
|
12
|
+
registerBasicPrimitiveActions(ProgramClass);
|
|
13
13
|
registerBasicCanvasActions(ProgramClass);
|
|
14
14
|
registerBasicDataActions(ProgramClass);
|
|
15
15
|
registerBasicMarkActions(ProgramClass);
|
|
@@ -38,6 +38,10 @@ export function resolveLinePositionPolicy({
|
|
|
38
38
|
}
|
|
39
39
|
const regression = dataset.transform?.some(item => item.type === "regression");
|
|
40
40
|
const interval = dataset.transform?.some(item => item.type === "interval");
|
|
41
|
+
const windowOutput = dataset.transform?.some(item =>
|
|
42
|
+
item.type === "window" &&
|
|
43
|
+
item.operations?.some(operation => operation.as === args.field)
|
|
44
|
+
);
|
|
41
45
|
|
|
42
46
|
if (channel === "x") {
|
|
43
47
|
const directPair =
|
|
@@ -73,7 +77,7 @@ export function resolveLinePositionPolicy({
|
|
|
73
77
|
(fieldType === "temporal" ||
|
|
74
78
|
(fieldType === "quantitative" &&
|
|
75
79
|
(layer.encoding?.x === undefined || quantitativePair)));
|
|
76
|
-
if (interval || prospectiveDirect) {
|
|
80
|
+
if (interval || (windowOutput && args.aggregate === undefined) || prospectiveDirect) {
|
|
77
81
|
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
78
82
|
throw new Error(
|
|
79
83
|
"Direct line y encoding requires a quantitative or temporal field."
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { action } from "../../../../core/action.js";
|
|
2
|
+
import { resolveStoredSelection } from
|
|
3
|
+
"../../../../materialization/selection/state.js";
|
|
1
4
|
import {
|
|
2
5
|
createCategoricalLegend,
|
|
3
6
|
createLegend,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
rematerializeLegendLabels,
|
|
13
16
|
rematerializeLegendTitle
|
|
14
17
|
} from "./components.js";
|
|
18
|
+
import { activeConfig, noOptions, symbolGraphic } from "./layout.js";
|
|
15
19
|
import {
|
|
16
20
|
createLegendSymbolLines,
|
|
17
21
|
createLegendSymbolPoints,
|
|
@@ -21,7 +25,7 @@ import {
|
|
|
21
25
|
rematerializeLegendSymbolPoints,
|
|
22
26
|
rematerializeLegendSymbolSwatches,
|
|
23
27
|
rematerializeLegendSymbols,
|
|
24
|
-
|
|
28
|
+
rematerializeBasicLegendHighlights
|
|
25
29
|
} from "./symbols.js";
|
|
26
30
|
import { editLegend } from "../edit.js";
|
|
27
31
|
import {
|
|
@@ -29,6 +33,82 @@ import {
|
|
|
29
33
|
rematerializeSideLegendLane
|
|
30
34
|
} from "../lane.js";
|
|
31
35
|
|
|
36
|
+
function exactLegendSelection(program, config, highlight) {
|
|
37
|
+
if (highlight.target !== config.target) return undefined;
|
|
38
|
+
const resolved = resolveStoredSelection(program, highlight.selection);
|
|
39
|
+
const selected = new Set(resolved.keys);
|
|
40
|
+
const groups = config.domain.map(value =>
|
|
41
|
+
resolved.items.filter(item => item.fields?.[config.field] === value)
|
|
42
|
+
);
|
|
43
|
+
if (
|
|
44
|
+
groups.some(group => group.length === 0) ||
|
|
45
|
+
resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
|
|
46
|
+
) return undefined;
|
|
47
|
+
const states = groups.map(group => {
|
|
48
|
+
const count = group.filter(item => selected.has(item.key)).length;
|
|
49
|
+
return count === 0 ? false : count === group.length ? true : undefined;
|
|
50
|
+
});
|
|
51
|
+
return states.includes(undefined) ? undefined : states;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function legendLayerStyle(layer, style) {
|
|
55
|
+
const properties = layer.type === "line"
|
|
56
|
+
? ["stroke", "strokeWidth", "strokeDash", "opacity"]
|
|
57
|
+
: ["fill", "stroke", "strokeWidth", "opacity"];
|
|
58
|
+
return Object.fromEntries(
|
|
59
|
+
properties.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createLegendHighlightAction() {
|
|
64
|
+
return action(
|
|
65
|
+
{
|
|
66
|
+
op: "rematerializeLegendHighlights",
|
|
67
|
+
description: "Reflect exact categorical mark highlights in legend symbols."
|
|
68
|
+
},
|
|
69
|
+
function (args = {}) {
|
|
70
|
+
noOptions(args, "rematerializeLegendHighlights");
|
|
71
|
+
const hasCategorical =
|
|
72
|
+
this.guideConfigs.legend?.series !== undefined ||
|
|
73
|
+
this.guideConfigs.legend?.color !== undefined;
|
|
74
|
+
if (!hasCategorical) return this;
|
|
75
|
+
const { config } = activeConfig(this);
|
|
76
|
+
const highlights = Object.values(
|
|
77
|
+
this.materializationConfigs.highlights ?? {}
|
|
78
|
+
).map(highlight => ({
|
|
79
|
+
highlight,
|
|
80
|
+
states: exactLegendSelection(this, config, highlight)
|
|
81
|
+
})).filter(entry => entry.states !== undefined);
|
|
82
|
+
if (highlights.length === 0) return this;
|
|
83
|
+
|
|
84
|
+
let next = this.rematerializeLegendSymbols();
|
|
85
|
+
for (const { highlight, states } of highlights) {
|
|
86
|
+
for (const layer of config.symbol.layers) {
|
|
87
|
+
const id = symbolGraphic(config, layer.type);
|
|
88
|
+
const graphic = next.graphicSpec.objects[id];
|
|
89
|
+
const selectedStyle = legendLayerStyle(layer, highlight.style);
|
|
90
|
+
const dimOpacity = highlight.dimOthers === false
|
|
91
|
+
? undefined
|
|
92
|
+
: highlight.dimOthers.opacity;
|
|
93
|
+
next = next.editGraphics({
|
|
94
|
+
target: id,
|
|
95
|
+
property: "items",
|
|
96
|
+
value: graphic.items.map((child, index) => ({
|
|
97
|
+
type: child.type ?? graphic.type,
|
|
98
|
+
properties: states[index]
|
|
99
|
+
? { ...child.properties, ...selectedStyle }
|
|
100
|
+
: dimOpacity === undefined
|
|
101
|
+
? child.properties
|
|
102
|
+
: { ...child.properties, opacity: dimOpacity }
|
|
103
|
+
}))
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
32
112
|
export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
33
113
|
ProgramClass.prototype.createLegend = createLegend;
|
|
34
114
|
ProgramClass.prototype.createCategoricalLegend = createCategoricalLegend;
|
|
@@ -37,7 +117,8 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
|
37
117
|
ProgramClass.prototype.rematerializeLegendBackground = rematerializeLegendBackground;
|
|
38
118
|
ProgramClass.prototype.createLegendSymbols = createLegendSymbols;
|
|
39
119
|
ProgramClass.prototype.rematerializeLegendSymbols = rematerializeLegendSymbols;
|
|
40
|
-
ProgramClass.prototype.rematerializeLegendHighlights =
|
|
120
|
+
ProgramClass.prototype.rematerializeLegendHighlights =
|
|
121
|
+
rematerializeBasicLegendHighlights;
|
|
41
122
|
ProgramClass.prototype.createLegendSymbolLines = createLegendSymbolLines;
|
|
42
123
|
ProgramClass.prototype.rematerializeLegendSymbolLines = rematerializeLegendSymbolLines;
|
|
43
124
|
ProgramClass.prototype.createLegendSymbolPoints = createLegendSymbolPoints;
|
|
@@ -57,5 +138,7 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
|
57
138
|
|
|
58
139
|
export function registerCategoricalLegendActions(ProgramClass) {
|
|
59
140
|
registerBasicCategoricalLegendActions(ProgramClass);
|
|
141
|
+
ProgramClass.prototype.rematerializeLegendHighlights =
|
|
142
|
+
createLegendHighlightAction();
|
|
60
143
|
ProgramClass.prototype.editLegend = editLegend;
|
|
61
144
|
}
|
|
@@ -12,44 +12,6 @@ import {
|
|
|
12
12
|
import { resolveLegendGraphicPlacement } from
|
|
13
13
|
"../../../../materialization/graphicHierarchy.js";
|
|
14
14
|
import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
|
|
15
|
-
import { resolveStoredSelection } from "../../../../materialization/selection/state.js";
|
|
16
|
-
|
|
17
|
-
function exactLegendSelection(program, config, highlight) {
|
|
18
|
-
if (highlight.target !== config.target) return undefined;
|
|
19
|
-
const resolved = resolveStoredSelection(program, highlight.selection);
|
|
20
|
-
const selected = new Set(resolved.keys);
|
|
21
|
-
const groups = config.domain.map(value =>
|
|
22
|
-
resolved.items.filter(item => item.fields?.[config.field] === value)
|
|
23
|
-
);
|
|
24
|
-
if (
|
|
25
|
-
groups.some(group => group.length === 0) ||
|
|
26
|
-
resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
|
|
27
|
-
) return undefined;
|
|
28
|
-
const states = groups.map(group => {
|
|
29
|
-
const count = group.filter(item => selected.has(item.key)).length;
|
|
30
|
-
return count === 0 ? false : count === group.length ? true : undefined;
|
|
31
|
-
});
|
|
32
|
-
return states.includes(undefined) ? undefined : states;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function legendLayerStyle(layer, style) {
|
|
36
|
-
if (layer.type === "line") {
|
|
37
|
-
return Object.fromEntries(
|
|
38
|
-
["stroke", "strokeWidth", "strokeDash", "opacity"]
|
|
39
|
-
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (layer.type === "swatch") {
|
|
43
|
-
return Object.fromEntries(
|
|
44
|
-
["fill", "stroke", "strokeWidth", "opacity"]
|
|
45
|
-
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return Object.fromEntries(
|
|
49
|
-
["fill", "stroke", "strokeWidth", "opacity"]
|
|
50
|
-
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
15
|
|
|
54
16
|
function makeEditSymbol(type) {
|
|
55
17
|
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
|
|
@@ -232,49 +194,13 @@ export const rematerializeLegendSymbols = action(
|
|
|
232
194
|
}
|
|
233
195
|
);
|
|
234
196
|
|
|
235
|
-
export const
|
|
197
|
+
export const rematerializeBasicLegendHighlights = action(
|
|
236
198
|
{
|
|
237
199
|
op: "rematerializeLegendHighlights",
|
|
238
200
|
description: "Reflect exact categorical mark highlights in legend symbols."
|
|
239
201
|
},
|
|
240
202
|
function (args = {}) {
|
|
241
203
|
noOptions(args, "rematerializeLegendHighlights");
|
|
242
|
-
|
|
243
|
-
this.guideConfigs.legend?.series !== undefined ||
|
|
244
|
-
this.guideConfigs.legend?.color !== undefined;
|
|
245
|
-
if (!hasCategorical) return this;
|
|
246
|
-
const { config } = activeConfig(this);
|
|
247
|
-
const highlights = Object.values(
|
|
248
|
-
this.materializationConfigs.highlights ?? {}
|
|
249
|
-
).map(highlight => ({
|
|
250
|
-
highlight,
|
|
251
|
-
states: exactLegendSelection(this, config, highlight)
|
|
252
|
-
})).filter(entry => entry.states !== undefined);
|
|
253
|
-
if (highlights.length === 0) return this;
|
|
254
|
-
|
|
255
|
-
let next = this.rematerializeLegendSymbols();
|
|
256
|
-
for (const { highlight, states } of highlights) {
|
|
257
|
-
for (const layer of config.symbol.layers) {
|
|
258
|
-
const id = symbolGraphic(config, layer.type);
|
|
259
|
-
const graphic = next.graphicSpec.objects[id];
|
|
260
|
-
const selectedStyle = legendLayerStyle(layer, highlight.style);
|
|
261
|
-
const dimOpacity = highlight.dimOthers === false
|
|
262
|
-
? undefined
|
|
263
|
-
: highlight.dimOthers.opacity;
|
|
264
|
-
next = next.editGraphics({
|
|
265
|
-
target: id,
|
|
266
|
-
property: "items",
|
|
267
|
-
value: graphic.items.map((child, index) => ({
|
|
268
|
-
type: child.type ?? graphic.type,
|
|
269
|
-
properties: states[index]
|
|
270
|
-
? { ...child.properties, ...selectedStyle }
|
|
271
|
-
: dimOpacity === undefined
|
|
272
|
-
? child.properties
|
|
273
|
-
: { ...child.properties, opacity: dimOpacity }
|
|
274
|
-
}))
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return next;
|
|
204
|
+
return this;
|
|
279
205
|
}
|
|
280
206
|
);
|
package/src/actions/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerCanvasActions } from "./canvas/index.js";
|
|
2
2
|
import { registerChartActions } from "./charts/index.js";
|
|
3
3
|
import { registerCategoryOrderActions } from "./categoryOrder/index.js";
|
|
4
|
-
import { registerCoordinateActions } from "./coordinates/
|
|
4
|
+
import { registerCoordinateActions } from "./coordinates/actions.js";
|
|
5
5
|
import { registerCompositionActions } from "./composition/index.js";
|
|
6
6
|
import { registerDataActions } from "./data/index.js";
|
|
7
7
|
import { registerEncodingActions } from "./encodings/index.js";
|
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { validateBin2DTransform } from "../../grammar/bin2d.js";
|
|
1
3
|
import { registerCreateGraphicsAction } from "./createGraphics.js";
|
|
2
4
|
import { registerEditGraphicsAction } from "./editGraphics.js";
|
|
3
5
|
import { registerSemanticPrimitiveAction } from "./semantic.js";
|
|
6
|
+
import { createSemanticPrimitiveAction } from "./semanticAction.js";
|
|
7
|
+
import { createSemanticValueValidator } from "./semanticValidation/index.js";
|
|
8
|
+
|
|
9
|
+
function validateBasicDatasetTransforms(value) {
|
|
10
|
+
if (!Array.isArray(value) || value.length !== 1 || !isPlainObject(value[0])) {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
"Dataset transform must contain exactly one plain object."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (value[0].type !== "bin2d") {
|
|
16
|
+
throw new Error(`Unsupported dataset transform "${value[0].type}".`);
|
|
17
|
+
}
|
|
18
|
+
validateBin2DTransform(value[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerBasicPrimitiveActions(ProgramClass) {
|
|
22
|
+
const validateBasicSemanticValue = createSemanticValueValidator({
|
|
23
|
+
validateDatasetTransforms: validateBasicDatasetTransforms,
|
|
24
|
+
sourceMarkTypes: ["point", "bar", "rect"]
|
|
25
|
+
});
|
|
26
|
+
ProgramClass.prototype.editSemantic =
|
|
27
|
+
createSemanticPrimitiveAction(validateBasicSemanticValue);
|
|
28
|
+
registerCreateGraphicsAction(ProgramClass);
|
|
29
|
+
registerEditGraphicsAction(ProgramClass);
|
|
30
|
+
}
|
|
4
31
|
|
|
5
32
|
export function registerPrimitiveActions(ProgramClass) {
|
|
6
33
|
registerSemanticPrimitiveAction(ProgramClass);
|
|
@@ -1,191 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSemanticPrimitiveAction } from "./semanticAction.js";
|
|
2
|
+
import { validateDatasetTransforms } from "../../grammar/transforms.js";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
guide: "currentGuide"
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function setNestedProperty(source, path, value) {
|
|
20
|
-
const [key, ...rest] = path;
|
|
21
|
-
|
|
22
|
-
if (rest.length === 0) {
|
|
23
|
-
return freezeOwned({ ...source, [key]: cloneAndFreeze(value) });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const child = isPlainObject(source[key]) ? source[key] : {};
|
|
27
|
-
return freezeOwned({
|
|
28
|
-
...source,
|
|
29
|
-
[key]: setNestedProperty(child, rest, value)
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function updateEntity(spec, parsed, value) {
|
|
34
|
-
const collection = spec[parsed.collection];
|
|
35
|
-
const index = collection.findIndex(item => item.id === parsed.id);
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
parsed.kind === "dataset" &&
|
|
39
|
-
index !== -1 &&
|
|
40
|
-
Object.hasOwn(collection[index], "values")
|
|
41
|
-
) {
|
|
42
|
-
throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const current = index === -1 ? { id: parsed.id } : collection[index];
|
|
46
|
-
const updated = setNestedProperty(current, parsed.path, value);
|
|
47
|
-
const nextCollection = [...collection];
|
|
48
|
-
|
|
49
|
-
if (index === -1) {
|
|
50
|
-
nextCollection.push(updated);
|
|
51
|
-
} else {
|
|
52
|
-
nextCollection[index] = updated;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return freezeOwned({
|
|
56
|
-
...spec,
|
|
57
|
-
[parsed.collection]: freezeOwned(nextCollection)
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function updateGuides(spec, parsed, value) {
|
|
62
|
-
return freezeOwned({
|
|
63
|
-
...spec,
|
|
64
|
-
guides: setNestedProperty(spec.guides, parsed.path, value)
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function updateTitle(spec, parsed, value) {
|
|
69
|
-
return freezeOwned({
|
|
70
|
-
...spec,
|
|
71
|
-
title: setNestedProperty(spec.title, parsed.path, value)
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function removeEntity(spec, parsed) {
|
|
76
|
-
const collection = spec[parsed.collection];
|
|
77
|
-
const index = collection.findIndex(item => item.id === parsed.id);
|
|
78
|
-
if (index === -1) return spec;
|
|
79
|
-
if (parsed.kind === "layer" && parsed.path.length === 0) {
|
|
80
|
-
const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
|
|
81
|
-
return freezeOwned({
|
|
82
|
-
...spec,
|
|
83
|
-
[parsed.collection]: freezeOwned(nextCollection)
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (parsed.kind === "dataset" && parsed.path.length === 0) {
|
|
87
|
-
const dataset = collection[index];
|
|
88
|
-
if (dataset.source === undefined) {
|
|
89
|
-
throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
|
|
90
|
-
}
|
|
91
|
-
const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
|
|
92
|
-
spec.datasets.some(candidate => candidate.source === parsed.id);
|
|
93
|
-
if (referenced) {
|
|
94
|
-
throw new Error(`Derived dataset "${parsed.id}" is still referenced.`);
|
|
95
|
-
}
|
|
96
|
-
const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
|
|
97
|
-
return freezeOwned({
|
|
98
|
-
...spec,
|
|
99
|
-
[parsed.collection]: freezeOwned(nextCollection)
|
|
100
|
-
});
|
|
4
|
+
validateParallelDimensions,
|
|
5
|
+
validateParallelKeyField,
|
|
6
|
+
validateParallelMissingPolicy
|
|
7
|
+
} from "../../grammar/parallelCoordinates.js";
|
|
8
|
+
import { createSemanticValueValidator } from "./semanticValidation/index.js";
|
|
9
|
+
|
|
10
|
+
function validateParallel(property, value) {
|
|
11
|
+
if (property === "encoding.parallel.dimensions") {
|
|
12
|
+
validateParallelDimensions(value, { normalized: true });
|
|
13
|
+
} else if (property === "encoding.parallel.key") {
|
|
14
|
+
validateParallelKeyField(value);
|
|
15
|
+
} else if (property === "encoding.parallel.missing") {
|
|
16
|
+
validateParallelMissingPolicy(value);
|
|
101
17
|
}
|
|
102
|
-
if (parsed.kind === "dataset") {
|
|
103
|
-
throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
|
|
104
|
-
}
|
|
105
|
-
const removed = removeOwnedPath(collection[index], parsed.path);
|
|
106
|
-
if (!removed.removed) return spec;
|
|
107
|
-
const nextCollection = [...collection];
|
|
108
|
-
nextCollection[index] = removed.value;
|
|
109
|
-
return freezeOwned({
|
|
110
|
-
...spec,
|
|
111
|
-
[parsed.collection]: freezeOwned(nextCollection)
|
|
112
|
-
});
|
|
113
18
|
}
|
|
114
19
|
|
|
115
|
-
function removeRootProperty(spec, root, path) {
|
|
116
|
-
const removed = removeOwnedPath(spec[root], path);
|
|
117
|
-
return removed.removed
|
|
118
|
-
? freezeOwned({ ...spec, [root]: removed.value })
|
|
119
|
-
: spec;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const editSemantic = action(
|
|
123
|
-
{
|
|
124
|
-
op: "editSemantic",
|
|
125
|
-
description: "Create, replace, or remove one semantic property.",
|
|
126
|
-
scope: "any"
|
|
127
|
-
},
|
|
128
|
-
function ({ property, value, remove = false } = {}) {
|
|
129
|
-
if (typeof remove !== "boolean") {
|
|
130
|
-
throw new TypeError("editSemantic remove must be a boolean.");
|
|
131
|
-
}
|
|
132
|
-
if (remove && value !== undefined) {
|
|
133
|
-
throw new Error("editSemantic cannot combine value and remove.");
|
|
134
|
-
}
|
|
135
|
-
if (!remove && value === undefined) {
|
|
136
|
-
throw new TypeError("editSemantic requires a value.");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const parsed = parseSemanticPath(property, { allowContainer: remove });
|
|
140
|
-
if (this.compositionSpec !== undefined && !(
|
|
141
|
-
this.compositionSpec.type === "facet" && parsed.kind === "title"
|
|
142
|
-
)) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
"editSemantic on a composition parent currently supports only facet title state."
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
if (remove) {
|
|
148
|
-
const semanticSpec = parsed.kind === "guide"
|
|
149
|
-
? removeRootProperty(this.semanticSpec, "guides", parsed.path)
|
|
150
|
-
: parsed.kind === "title"
|
|
151
|
-
? removeRootProperty(this.semanticSpec, "title", parsed.path)
|
|
152
|
-
: removeEntity(this.semanticSpec, parsed);
|
|
153
|
-
if (semanticSpec === this.semanticSpec) return this;
|
|
154
|
-
const clearsCurrentData =
|
|
155
|
-
parsed.kind === "dataset" &&
|
|
156
|
-
parsed.path.length === 0 &&
|
|
157
|
-
this.context.currentData === parsed.id;
|
|
158
|
-
const clearsCurrentMark =
|
|
159
|
-
parsed.kind === "layer" &&
|
|
160
|
-
parsed.path.length === 0 &&
|
|
161
|
-
this.context.currentMark === parsed.id;
|
|
162
|
-
return this._clone({
|
|
163
|
-
semanticSpec,
|
|
164
|
-
...(clearsCurrentData || clearsCurrentMark
|
|
165
|
-
? { context: freezeOwned({
|
|
166
|
-
...this.context,
|
|
167
|
-
...(clearsCurrentData ? { currentData: undefined } : {}),
|
|
168
|
-
...(clearsCurrentMark ? { currentMark: undefined } : {})
|
|
169
|
-
}) }
|
|
170
|
-
: {})
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
validateSemanticValue(this, parsed, value);
|
|
174
|
-
|
|
175
|
-
const semanticSpec = parsed.kind === "guide"
|
|
176
|
-
? updateGuides(this.semanticSpec, parsed, value)
|
|
177
|
-
: parsed.kind === "title"
|
|
178
|
-
? updateTitle(this.semanticSpec, parsed, value)
|
|
179
|
-
: updateEntity(this.semanticSpec, parsed, value);
|
|
180
|
-
const contextKey = CONTEXT_KEYS[parsed.kind];
|
|
181
|
-
const context = contextKey === undefined
|
|
182
|
-
? this.context
|
|
183
|
-
: freezeOwned({ ...this.context, [contextKey]: parsed.id });
|
|
184
|
-
|
|
185
|
-
return this._clone({ semanticSpec, context });
|
|
186
|
-
}
|
|
187
|
-
);
|
|
188
|
-
|
|
189
20
|
export function registerSemanticPrimitiveAction(ProgramClass) {
|
|
190
|
-
|
|
21
|
+
const validateSemanticValue = createSemanticValueValidator({
|
|
22
|
+
validateDatasetTransforms,
|
|
23
|
+
validateParallel,
|
|
24
|
+
sourceMarkTypes: ["point", "bar", "rule", "rect"]
|
|
25
|
+
});
|
|
26
|
+
ProgramClass.prototype.editSemantic =
|
|
27
|
+
createSemanticPrimitiveAction(validateSemanticValue);
|
|
191
28
|
}
|