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,225 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite,
|
|
7
|
+
validateUnitInterval
|
|
8
|
+
} from "../../../core/validation.js";
|
|
9
|
+
import {
|
|
10
|
+
centeredDirectionalSegment,
|
|
11
|
+
resolveDirectionValues
|
|
12
|
+
} from "../../../grammar/direction.js";
|
|
13
|
+
import { resolveMarkGraphicPlacement } from
|
|
14
|
+
"../../../materialization/graphicHierarchy.js";
|
|
15
|
+
import { resolveRowEncodingValues } from
|
|
16
|
+
"../../../materialization/rowEncoding.js";
|
|
17
|
+
import { findDataset } from "../../../selectors/datasets.js";
|
|
18
|
+
import { findLayer } from "../../../selectors/layers.js";
|
|
19
|
+
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
20
|
+
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
21
|
+
import {
|
|
22
|
+
applyLayeredMarkInheritance,
|
|
23
|
+
assertMarkAvailable,
|
|
24
|
+
materializeInheritedMark,
|
|
25
|
+
resolveLayeredMarkInheritance,
|
|
26
|
+
resolveMarkData,
|
|
27
|
+
resolveMarkId,
|
|
28
|
+
validateMarkOptions
|
|
29
|
+
} from "../shared.js";
|
|
30
|
+
|
|
31
|
+
const CREATE_OPTIONS = Object.freeze([
|
|
32
|
+
"id", "data", "length", "stroke", "strokeWidth", "opacity"
|
|
33
|
+
]);
|
|
34
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
35
|
+
"target", "length", "stroke", "strokeWidth", "opacity"
|
|
36
|
+
]);
|
|
37
|
+
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_TICK_CONFIG = Object.freeze({
|
|
40
|
+
length: 14,
|
|
41
|
+
stroke: DEFAULT_COLORS.mark,
|
|
42
|
+
strokeWidth: 2,
|
|
43
|
+
opacity: 1
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function resolveTick(program, requested, operation) {
|
|
47
|
+
const candidates = program.semanticSpec.layers.filter(
|
|
48
|
+
layer => layer.mark?.type === "tick"
|
|
49
|
+
);
|
|
50
|
+
if (requested !== undefined) {
|
|
51
|
+
const id = validateUserId(requested, "Tick mark id");
|
|
52
|
+
const layer = findLayer(program, id);
|
|
53
|
+
if (layer?.mark?.type !== "tick") {
|
|
54
|
+
throw new Error(`Unknown tick mark "${id}".`);
|
|
55
|
+
}
|
|
56
|
+
return layer;
|
|
57
|
+
}
|
|
58
|
+
const current = findLayer(program, program.context.currentMark);
|
|
59
|
+
if (current?.mark?.type === "tick") return current;
|
|
60
|
+
if (candidates.length === 1) return candidates[0];
|
|
61
|
+
if (candidates.length === 0) {
|
|
62
|
+
throw new Error(`${operation} requires an existing tick mark.`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`${operation} target is ambiguous; provide target.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateTickConfig(args, current = DEFAULT_TICK_CONFIG) {
|
|
68
|
+
return {
|
|
69
|
+
length: Object.hasOwn(args, "length")
|
|
70
|
+
? validatePositiveFinite(args.length, "Tick length")
|
|
71
|
+
: current.length,
|
|
72
|
+
stroke: Object.hasOwn(args, "stroke")
|
|
73
|
+
? validateNonEmptyString(args.stroke, "Tick stroke")
|
|
74
|
+
: current.stroke,
|
|
75
|
+
strokeWidth: Object.hasOwn(args, "strokeWidth")
|
|
76
|
+
? validateNonNegativeFinite(args.strokeWidth, "Tick strokeWidth")
|
|
77
|
+
: current.strokeWidth,
|
|
78
|
+
opacity: Object.hasOwn(args, "opacity")
|
|
79
|
+
? validateUnitInterval(args.opacity, "Tick opacity")
|
|
80
|
+
: current.opacity
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const createTickMark = action(
|
|
85
|
+
{
|
|
86
|
+
op: "createTickMark",
|
|
87
|
+
description: "Create a centered fixed-length Tick mark."
|
|
88
|
+
},
|
|
89
|
+
function (args = {}) {
|
|
90
|
+
validateMarkOptions(args, CREATE_OPTIONS, "createTickMark");
|
|
91
|
+
const id = resolveMarkId(this, args.id, {
|
|
92
|
+
defaultId: "tick",
|
|
93
|
+
label: "Tick mark id",
|
|
94
|
+
markType: "tick",
|
|
95
|
+
operation: "createTickMark"
|
|
96
|
+
});
|
|
97
|
+
const inherited = resolveLayeredMarkInheritance(this, args, "tick");
|
|
98
|
+
const { data } = resolveMarkData(this, {
|
|
99
|
+
...args,
|
|
100
|
+
...(args.data === undefined && this.context.currentData === undefined &&
|
|
101
|
+
inherited?.data !== undefined ? { data: inherited.data } : {})
|
|
102
|
+
});
|
|
103
|
+
const config = validateTickConfig(args);
|
|
104
|
+
assertMarkAvailable(this, id);
|
|
105
|
+
|
|
106
|
+
let created = this
|
|
107
|
+
.editSemantic({ property: `layer[${id}].mark.type`, value: "tick" })
|
|
108
|
+
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
109
|
+
created = applyLayeredMarkInheritance(created, id, inherited)
|
|
110
|
+
.createGraphics({
|
|
111
|
+
id,
|
|
112
|
+
type: "line",
|
|
113
|
+
length: 0,
|
|
114
|
+
...resolveMarkGraphicPlacement(created, { data, markType: "tick" })
|
|
115
|
+
})
|
|
116
|
+
._withMarkConfig(id, DEFAULT_TICK_CONFIG);
|
|
117
|
+
const materialized = materializeInheritedMark(created, id);
|
|
118
|
+
const appearance = Object.fromEntries(
|
|
119
|
+
["length", "stroke", "strokeWidth", "opacity"]
|
|
120
|
+
.filter(property => Object.hasOwn(args, property))
|
|
121
|
+
.map(property => [property, config[property]])
|
|
122
|
+
);
|
|
123
|
+
return Object.keys(appearance).length === 0
|
|
124
|
+
? materialized
|
|
125
|
+
: materialized.editTickMark({ target: id, ...appearance });
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
export const editTickMark = action(
|
|
130
|
+
{
|
|
131
|
+
op: "editTickMark",
|
|
132
|
+
description: "Edit Tick length and constant line appearance."
|
|
133
|
+
},
|
|
134
|
+
function (args = {}) {
|
|
135
|
+
validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
|
|
136
|
+
const editable = ["length", "stroke", "strokeWidth", "opacity"];
|
|
137
|
+
if (!editable.some(property => Object.hasOwn(args, property))) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"editTickMark requires length, stroke, strokeWidth, or opacity."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const layer = resolveTick(this, args.target, "editTickMark");
|
|
143
|
+
const config = validateTickConfig(args, {
|
|
144
|
+
...DEFAULT_TICK_CONFIG,
|
|
145
|
+
...this.markConfigs[layer.id]
|
|
146
|
+
});
|
|
147
|
+
return this
|
|
148
|
+
._withMarkConfig(layer.id, config)
|
|
149
|
+
.rematerializeTickMark({ id: layer.id });
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
export const rematerializeTickMark = action(
|
|
154
|
+
{
|
|
155
|
+
op: "rematerializeTickMark",
|
|
156
|
+
description: "Recompute concrete centered Tick endpoints and appearance."
|
|
157
|
+
},
|
|
158
|
+
function (args = {}) {
|
|
159
|
+
validateMarkOptions(
|
|
160
|
+
args,
|
|
161
|
+
REMATERIALIZE_OPTIONS,
|
|
162
|
+
"rematerializeTickMark"
|
|
163
|
+
);
|
|
164
|
+
const id = validateUserId(args.id, "Tick mark id");
|
|
165
|
+
const highlighted = rematerializeHighlightBaseline(this, {
|
|
166
|
+
target: id,
|
|
167
|
+
operation: "rematerializeTickMark",
|
|
168
|
+
resetProperty: "length",
|
|
169
|
+
resetValue: 0
|
|
170
|
+
});
|
|
171
|
+
if (highlighted !== undefined) return highlighted;
|
|
172
|
+
const layer = findLayer(this, id);
|
|
173
|
+
if (layer?.mark?.type !== "tick") {
|
|
174
|
+
throw new Error(`Unknown tick mark "${id}".`);
|
|
175
|
+
}
|
|
176
|
+
const graphic = this.graphicSpec.objects[id];
|
|
177
|
+
if (graphic?.type !== "line" || !Array.isArray(graphic.items)) {
|
|
178
|
+
throw new Error(`Tick mark "${id}" requires line collection graphics.`);
|
|
179
|
+
}
|
|
180
|
+
const dataset = findDataset(this, layer.data);
|
|
181
|
+
if (dataset === undefined) {
|
|
182
|
+
throw new Error(`Tick mark "${id}" requires an existing dataset.`);
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
layer.encoding?.x?.scale === undefined ||
|
|
186
|
+
layer.encoding?.y?.scale === undefined
|
|
187
|
+
) {
|
|
188
|
+
return graphic.items.length === 0
|
|
189
|
+
? this
|
|
190
|
+
: this.editGraphics({ target: id, property: "length", value: 0 });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const x = resolveRowEncodingValues(this, layer, dataset, "x");
|
|
194
|
+
const y = resolveRowEncodingValues(this, layer, dataset, "y");
|
|
195
|
+
const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
|
|
196
|
+
const config = validateTickConfig({}, {
|
|
197
|
+
...DEFAULT_TICK_CONFIG,
|
|
198
|
+
...this.markConfigs[id]
|
|
199
|
+
});
|
|
200
|
+
const segments = dataset.values.map((_, index) =>
|
|
201
|
+
centeredDirectionalSegment({
|
|
202
|
+
x: x[index],
|
|
203
|
+
y: y[index],
|
|
204
|
+
degrees: angles?.[index] ?? 0,
|
|
205
|
+
length: config.length
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return this
|
|
210
|
+
.editGraphics({ target: id, property: "length", value: segments.length })
|
|
211
|
+
.editGraphics({ target: id, property: "x1", value: segments.map(item => item.x1) })
|
|
212
|
+
.editGraphics({ target: id, property: "y1", value: segments.map(item => item.y1) })
|
|
213
|
+
.editGraphics({ target: id, property: "x2", value: segments.map(item => item.x2) })
|
|
214
|
+
.editGraphics({ target: id, property: "y2", value: segments.map(item => item.y2) })
|
|
215
|
+
.editGraphics({ target: id, property: "stroke", value: config.stroke })
|
|
216
|
+
.editGraphics({ target: id, property: "strokeWidth", value: config.strokeWidth })
|
|
217
|
+
.editGraphics({ target: id, property: "opacity", value: config.opacity });
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
export function registerTickMarkActions(ProgramClass) {
|
|
222
|
+
ProgramClass.prototype.createTickMark = createTickMark;
|
|
223
|
+
ProgramClass.prototype.editTickMark = editTickMark;
|
|
224
|
+
ProgramClass.prototype.rematerializeTickMark = rematerializeTickMark;
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerTickMarkActions } from "./actions.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
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
cloneAndFreeze,
|
|
4
|
+
freezeOwned,
|
|
5
|
+
isPlainObject,
|
|
6
|
+
removeOwnedPath
|
|
7
|
+
} from "../../core/immutable.js";
|
|
8
|
+
import { parseSemanticPath } from "../../grammar/schemas/semanticPath.js";
|
|
9
|
+
|
|
10
|
+
const CONTEXT_KEYS = Object.freeze({
|
|
11
|
+
dataset: "currentData",
|
|
12
|
+
layer: "currentMark",
|
|
13
|
+
scale: "currentScale",
|
|
14
|
+
coordinate: "currentCoordinate",
|
|
15
|
+
guide: "currentGuide"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function setNestedProperty(source, path, value) {
|
|
19
|
+
const [key, ...rest] = path;
|
|
20
|
+
|
|
21
|
+
if (rest.length === 0) {
|
|
22
|
+
return freezeOwned({ ...source, [key]: cloneAndFreeze(value) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const child = isPlainObject(source[key]) ? source[key] : {};
|
|
26
|
+
return freezeOwned({
|
|
27
|
+
...source,
|
|
28
|
+
[key]: setNestedProperty(child, rest, value)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function updateEntity(spec, parsed, value) {
|
|
33
|
+
const collection = spec[parsed.collection];
|
|
34
|
+
const index = collection.findIndex(item => item.id === parsed.id);
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
parsed.kind === "dataset" &&
|
|
38
|
+
index !== -1 &&
|
|
39
|
+
Object.hasOwn(collection[index], "values")
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const current = index === -1 ? { id: parsed.id } : collection[index];
|
|
45
|
+
const updated = setNestedProperty(current, parsed.path, value);
|
|
46
|
+
const nextCollection = [...collection];
|
|
47
|
+
|
|
48
|
+
if (index === -1) {
|
|
49
|
+
nextCollection.push(updated);
|
|
50
|
+
} else {
|
|
51
|
+
nextCollection[index] = updated;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return freezeOwned({
|
|
55
|
+
...spec,
|
|
56
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateGuides(spec, parsed, value) {
|
|
61
|
+
return freezeOwned({
|
|
62
|
+
...spec,
|
|
63
|
+
guides: setNestedProperty(spec.guides, parsed.path, value)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function updateTitle(spec, parsed, value) {
|
|
68
|
+
return freezeOwned({
|
|
69
|
+
...spec,
|
|
70
|
+
title: setNestedProperty(spec.title, parsed.path, value)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeEntity(spec, parsed) {
|
|
75
|
+
const collection = spec[parsed.collection];
|
|
76
|
+
const index = collection.findIndex(item => item.id === parsed.id);
|
|
77
|
+
if (index === -1) return spec;
|
|
78
|
+
if (parsed.kind === "layer" && parsed.path.length === 0) {
|
|
79
|
+
const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
|
|
80
|
+
return freezeOwned({
|
|
81
|
+
...spec,
|
|
82
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (parsed.kind === "dataset" && parsed.path.length === 0) {
|
|
86
|
+
const dataset = collection[index];
|
|
87
|
+
if (dataset.source === undefined) {
|
|
88
|
+
throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
|
|
89
|
+
}
|
|
90
|
+
const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
|
|
91
|
+
spec.datasets.some(candidate => candidate.source === parsed.id);
|
|
92
|
+
if (referenced) {
|
|
93
|
+
throw new Error(`Derived dataset "${parsed.id}" is still referenced.`);
|
|
94
|
+
}
|
|
95
|
+
const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
|
|
96
|
+
return freezeOwned({
|
|
97
|
+
...spec,
|
|
98
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (parsed.kind === "dataset") {
|
|
102
|
+
throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
|
|
103
|
+
}
|
|
104
|
+
const removed = removeOwnedPath(collection[index], parsed.path);
|
|
105
|
+
if (!removed.removed) return spec;
|
|
106
|
+
const nextCollection = [...collection];
|
|
107
|
+
nextCollection[index] = removed.value;
|
|
108
|
+
return freezeOwned({
|
|
109
|
+
...spec,
|
|
110
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function removeRootProperty(spec, root, path) {
|
|
115
|
+
const removed = removeOwnedPath(spec[root], path);
|
|
116
|
+
return removed.removed
|
|
117
|
+
? freezeOwned({ ...spec, [root]: removed.value })
|
|
118
|
+
: spec;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createSemanticPrimitiveAction(validateSemanticValue) {
|
|
122
|
+
return 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
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
2
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
|
-
import { validateDatasetTransforms } from "../../../grammar/transforms.js";
|
|
4
3
|
import { hasDataset } from "../../../selectors/datasets.js";
|
|
5
4
|
|
|
6
|
-
export function validateDatasetSemanticValue(
|
|
5
|
+
export function validateDatasetSemanticValue(
|
|
6
|
+
program,
|
|
7
|
+
parsed,
|
|
8
|
+
value,
|
|
9
|
+
validateTransforms
|
|
10
|
+
) {
|
|
7
11
|
const property = parsed.path[0];
|
|
8
12
|
if (property === "values") {
|
|
9
13
|
if (!Array.isArray(value) || !value.every(isPlainObject)) {
|
|
@@ -18,5 +22,5 @@ export function validateDatasetSemanticValue(program, parsed, value) {
|
|
|
18
22
|
}
|
|
19
23
|
return;
|
|
20
24
|
}
|
|
21
|
-
if (property === "transform")
|
|
25
|
+
if (property === "transform") validateTransforms(value);
|
|
22
26
|
}
|