ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { cloneAndFreeze, freezeOwned, isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import {
|
|
4
|
+
isPrimitiveDrawableGraphicType,
|
|
5
|
+
isStructuralGraphicType,
|
|
6
|
+
validateGraphicProperty,
|
|
7
|
+
validateGraphicType
|
|
8
|
+
} from "../../grammar/schemas/graphic.js";
|
|
9
|
+
import { validateConcreteGraphicValue } from
|
|
10
|
+
"../../grammar/schemas/concreteGraphic.js";
|
|
11
|
+
import {
|
|
12
|
+
collectGraphicSubtreeIds,
|
|
13
|
+
findGraphicParent,
|
|
14
|
+
requireGraphic
|
|
15
|
+
} from "../../grammar/schemas/graphicTree.js";
|
|
16
|
+
|
|
17
|
+
function setGraphicProperty(properties, property, value) {
|
|
18
|
+
return freezeOwned({ ...properties, [property]: cloneAndFreeze(value) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateCollectionItem(item, ownerId, index) {
|
|
22
|
+
if (!isPlainObject(item)) {
|
|
23
|
+
throw new TypeError(`collection.items[${index}] must be a plain object.`);
|
|
24
|
+
}
|
|
25
|
+
const unknown = Object.keys(item).find(key => !["type", "properties"].includes(key));
|
|
26
|
+
if (unknown !== undefined) throw new Error(`Unknown collection item property "${unknown}".`);
|
|
27
|
+
const type = validateGraphicType(item.type);
|
|
28
|
+
if (!isPrimitiveDrawableGraphicType(type)) {
|
|
29
|
+
throw new Error(`Collection item "${ownerId}:${index}" requires a primitive drawable type.`);
|
|
30
|
+
}
|
|
31
|
+
if (!isPlainObject(item.properties)) {
|
|
32
|
+
throw new TypeError(`Collection item "${ownerId}:${index}" requires plain properties.`);
|
|
33
|
+
}
|
|
34
|
+
for (const [property, value] of Object.entries(item.properties)) {
|
|
35
|
+
validateGraphicProperty(type, property);
|
|
36
|
+
validateConcreteGraphicValue(type, property, value);
|
|
37
|
+
}
|
|
38
|
+
return freezeOwned({
|
|
39
|
+
id: `${ownerId}:${index}`,
|
|
40
|
+
type,
|
|
41
|
+
properties: cloneAndFreeze(item.properties)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function replaceCollectionItems(value, id) {
|
|
46
|
+
if (!Array.isArray(value)) throw new TypeError("collection.items must be an array.");
|
|
47
|
+
return freezeOwned(value.map((item, index) => validateCollectionItem(item, id, index)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function replaceDrawableItems(value, id, type) {
|
|
51
|
+
const items = replaceCollectionItems(value, id);
|
|
52
|
+
if (!items.every(item => item.type === type)) {
|
|
53
|
+
return freezeOwned({ type: "collection", items });
|
|
54
|
+
}
|
|
55
|
+
return freezeOwned({
|
|
56
|
+
type,
|
|
57
|
+
items: freezeOwned(items.map(item => freezeOwned({
|
|
58
|
+
id: item.id,
|
|
59
|
+
properties: item.properties
|
|
60
|
+
})))
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function editGeneratedItem(owner, itemIndex, property, value) {
|
|
65
|
+
const items = [...owner.items];
|
|
66
|
+
const item = items[itemIndex];
|
|
67
|
+
const type = item.type ?? owner.type;
|
|
68
|
+
validateGraphicProperty(type, property);
|
|
69
|
+
validateConcreteGraphicValue(type, property, value);
|
|
70
|
+
items[itemIndex] = freezeOwned({
|
|
71
|
+
...item,
|
|
72
|
+
properties: setGraphicProperty(item.properties, property, value)
|
|
73
|
+
});
|
|
74
|
+
return freezeOwned({ ...owner, items: freezeOwned(items) });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function editItems(owner, property, value, id) {
|
|
78
|
+
if (owner.type === "collection" && property === "items") {
|
|
79
|
+
return freezeOwned({ ...owner, items: replaceCollectionItems(value, id) });
|
|
80
|
+
}
|
|
81
|
+
if (property === "length") {
|
|
82
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
83
|
+
throw new TypeError("Graphic collection length must be a non-negative integer.");
|
|
84
|
+
}
|
|
85
|
+
const items = owner.items.slice(0, value);
|
|
86
|
+
for (let index = items.length; index < value; index += 1) {
|
|
87
|
+
if (owner.type === "collection") {
|
|
88
|
+
throw new Error("A heterogeneous collection can only grow by replacing its items.");
|
|
89
|
+
}
|
|
90
|
+
items.push(freezeOwned({ id: `${id}:${index}`, properties: freezeOwned({}) }));
|
|
91
|
+
}
|
|
92
|
+
return freezeOwned({ ...owner, items: freezeOwned(items) });
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value) && value.length !== owner.items.length) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${owner.type}.${property} requires ${owner.items.length} values, received ${value.length}.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const items = owner.items.map((item, index) => {
|
|
100
|
+
const itemValue = Array.isArray(value) ? value[index] : value;
|
|
101
|
+
const type = item.type ?? owner.type;
|
|
102
|
+
validateGraphicProperty(type, property);
|
|
103
|
+
validateConcreteGraphicValue(type, property, itemValue);
|
|
104
|
+
return freezeOwned({
|
|
105
|
+
...item,
|
|
106
|
+
properties: setGraphicProperty(item.properties, property, itemValue)
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return freezeOwned({ ...owner, items: freezeOwned(items) });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function removeGraphicTree(graphicSpec, id) {
|
|
113
|
+
if (graphicSpec.objects[id].type === "canvas") {
|
|
114
|
+
throw new Error("The canvas root cannot be removed.");
|
|
115
|
+
}
|
|
116
|
+
const removedIds = new Set(collectGraphicSubtreeIds(graphicSpec, id));
|
|
117
|
+
const objects = Object.fromEntries(
|
|
118
|
+
Object.entries(graphicSpec.objects).filter(([objectId]) => !removedIds.has(objectId))
|
|
119
|
+
);
|
|
120
|
+
const parent = findGraphicParent(graphicSpec, id);
|
|
121
|
+
let order = graphicSpec.order;
|
|
122
|
+
if (parent?.kind === "parent") {
|
|
123
|
+
objects[parent.id] = freezeOwned({
|
|
124
|
+
...objects[parent.id],
|
|
125
|
+
children: freezeOwned(parent.object.children.filter(childId => childId !== id))
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
order = freezeOwned(order.filter(objectId => objectId !== id));
|
|
129
|
+
}
|
|
130
|
+
return freezeOwned({ objects: freezeOwned(objects), order });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const editGraphics = action(
|
|
134
|
+
{ op: "editGraphics", description: "Replace or remove concrete graphic state." },
|
|
135
|
+
function ({ target, property, value, remove = false } = {}) {
|
|
136
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
137
|
+
throw new TypeError("editGraphics requires a non-empty target string.");
|
|
138
|
+
}
|
|
139
|
+
if (typeof remove !== "boolean") throw new TypeError("editGraphics remove must be a boolean.");
|
|
140
|
+
if (remove && (property !== undefined || value !== undefined)) {
|
|
141
|
+
throw new Error("editGraphics target removal cannot include property or value.");
|
|
142
|
+
}
|
|
143
|
+
if (!remove && value === undefined) throw new TypeError("editGraphics requires a value.");
|
|
144
|
+
|
|
145
|
+
const found = requireGraphic(this.graphicSpec, target);
|
|
146
|
+
if (remove) {
|
|
147
|
+
if (found.kind === "item") {
|
|
148
|
+
throw new Error("editGraphics cannot remove a generated item independently.");
|
|
149
|
+
}
|
|
150
|
+
return this._clone({ graphicSpec: removeGraphicTree(this.graphicSpec, found.id) });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ownerId = found.kind === "item" ? found.ownerId : found.id;
|
|
154
|
+
const owner = found.kind === "item" ? found.owner : found.object;
|
|
155
|
+
const replacesDrawableItems =
|
|
156
|
+
found.kind === "object" && property === "items" &&
|
|
157
|
+
owner.items !== undefined && owner.type !== "collection";
|
|
158
|
+
const validatedProperty = replacesDrawableItems
|
|
159
|
+
? "items"
|
|
160
|
+
: validateGraphicProperty(found.kind === "item"
|
|
161
|
+
? (found.object.type ?? owner.type)
|
|
162
|
+
: owner.type, property);
|
|
163
|
+
let updated;
|
|
164
|
+
if (replacesDrawableItems) {
|
|
165
|
+
updated = replaceDrawableItems(value, ownerId, owner.type);
|
|
166
|
+
} else if (found.kind === "item") {
|
|
167
|
+
if (validatedProperty === "length" || validatedProperty === "items") {
|
|
168
|
+
throw new Error(`${validatedProperty} can only target an owning graphic.`);
|
|
169
|
+
}
|
|
170
|
+
updated = editGeneratedItem(owner, found.itemIndex, validatedProperty, value);
|
|
171
|
+
} else if (isStructuralGraphicType(owner.type)) {
|
|
172
|
+
validateConcreteGraphicValue(owner.type, validatedProperty, value);
|
|
173
|
+
updated = freezeOwned({
|
|
174
|
+
...owner,
|
|
175
|
+
properties: setGraphicProperty(owner.properties, validatedProperty, value)
|
|
176
|
+
});
|
|
177
|
+
} else if (owner.items !== undefined) {
|
|
178
|
+
updated = editItems(owner, validatedProperty, value, ownerId);
|
|
179
|
+
} else if (validatedProperty === "length") {
|
|
180
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
181
|
+
throw new TypeError("Graphic collection length must be a non-negative integer.");
|
|
182
|
+
}
|
|
183
|
+
updated = freezeOwned({
|
|
184
|
+
type: owner.type,
|
|
185
|
+
items: freezeOwned(Array.from({ length: value }, (_, index) => freezeOwned({
|
|
186
|
+
id: `${ownerId}:${index}`,
|
|
187
|
+
properties: index === 0 ? owner.properties : freezeOwned({})
|
|
188
|
+
})))
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
validateConcreteGraphicValue(owner.type, validatedProperty, value);
|
|
192
|
+
updated = freezeOwned({
|
|
193
|
+
...owner,
|
|
194
|
+
properties: setGraphicProperty(owner.properties, validatedProperty, value)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return this._clone({
|
|
198
|
+
graphicSpec: freezeOwned({
|
|
199
|
+
...this.graphicSpec,
|
|
200
|
+
objects: freezeOwned({ ...this.graphicSpec.objects, [ownerId]: updated })
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
export function registerEditGraphicsAction(ProgramClass) {
|
|
207
|
+
ProgramClass.prototype.editGraphics = editGraphics;
|
|
208
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerCreateGraphicsAction } from "./createGraphics.js";
|
|
2
|
+
import { registerEditGraphicsAction } from "./editGraphics.js";
|
|
3
|
+
import { registerSemanticPrimitiveAction } from "./semantic.js";
|
|
4
|
+
|
|
5
|
+
export function registerPrimitiveActions(ProgramClass) {
|
|
6
|
+
registerSemanticPrimitiveAction(ProgramClass);
|
|
7
|
+
registerCreateGraphicsAction(ProgramClass);
|
|
8
|
+
registerEditGraphicsAction(ProgramClass);
|
|
9
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
import { validateSemanticValue } from "./semanticValue.js";
|
|
10
|
+
|
|
11
|
+
const CONTEXT_KEYS = Object.freeze({
|
|
12
|
+
dataset: "currentData",
|
|
13
|
+
layer: "currentMark",
|
|
14
|
+
scale: "currentScale",
|
|
15
|
+
coordinate: "currentCoordinate",
|
|
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 === "dataset" && parsed.path.length === 0) {
|
|
80
|
+
const dataset = collection[index];
|
|
81
|
+
if (dataset.source === undefined) {
|
|
82
|
+
throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
|
|
83
|
+
}
|
|
84
|
+
const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
|
|
85
|
+
spec.datasets.some(candidate => candidate.source === parsed.id);
|
|
86
|
+
if (referenced) {
|
|
87
|
+
throw new Error(`Derived dataset "${parsed.id}" is still referenced.`);
|
|
88
|
+
}
|
|
89
|
+
const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
|
|
90
|
+
return freezeOwned({
|
|
91
|
+
...spec,
|
|
92
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (parsed.kind === "dataset") {
|
|
96
|
+
throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
|
|
97
|
+
}
|
|
98
|
+
const removed = removeOwnedPath(collection[index], parsed.path);
|
|
99
|
+
if (!removed.removed) return spec;
|
|
100
|
+
const nextCollection = [...collection];
|
|
101
|
+
nextCollection[index] = removed.value;
|
|
102
|
+
return freezeOwned({
|
|
103
|
+
...spec,
|
|
104
|
+
[parsed.collection]: freezeOwned(nextCollection)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function removeRootProperty(spec, root, path) {
|
|
109
|
+
const removed = removeOwnedPath(spec[root], path);
|
|
110
|
+
return removed.removed
|
|
111
|
+
? freezeOwned({ ...spec, [root]: removed.value })
|
|
112
|
+
: spec;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const editSemantic = action(
|
|
116
|
+
{
|
|
117
|
+
op: "editSemantic",
|
|
118
|
+
description: "Create, replace, or remove one semantic property."
|
|
119
|
+
},
|
|
120
|
+
function ({ property, value, remove = false } = {}) {
|
|
121
|
+
if (typeof remove !== "boolean") {
|
|
122
|
+
throw new TypeError("editSemantic remove must be a boolean.");
|
|
123
|
+
}
|
|
124
|
+
if (remove && value !== undefined) {
|
|
125
|
+
throw new Error("editSemantic cannot combine value and remove.");
|
|
126
|
+
}
|
|
127
|
+
if (!remove && value === undefined) {
|
|
128
|
+
throw new TypeError("editSemantic requires a value.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parsed = parseSemanticPath(property, { allowContainer: remove });
|
|
132
|
+
if (remove) {
|
|
133
|
+
const semanticSpec = parsed.kind === "guide"
|
|
134
|
+
? removeRootProperty(this.semanticSpec, "guides", parsed.path)
|
|
135
|
+
: parsed.kind === "title"
|
|
136
|
+
? removeRootProperty(this.semanticSpec, "title", parsed.path)
|
|
137
|
+
: removeEntity(this.semanticSpec, parsed);
|
|
138
|
+
if (semanticSpec === this.semanticSpec) return this;
|
|
139
|
+
const clearsCurrentData =
|
|
140
|
+
parsed.kind === "dataset" &&
|
|
141
|
+
parsed.path.length === 0 &&
|
|
142
|
+
this.context.currentData === parsed.id;
|
|
143
|
+
return this._clone({
|
|
144
|
+
semanticSpec,
|
|
145
|
+
...(clearsCurrentData
|
|
146
|
+
? { context: freezeOwned({ ...this.context, currentData: undefined }) }
|
|
147
|
+
: {})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
validateSemanticValue(this, parsed, value);
|
|
151
|
+
|
|
152
|
+
const semanticSpec = parsed.kind === "guide"
|
|
153
|
+
? updateGuides(this.semanticSpec, parsed, value)
|
|
154
|
+
: parsed.kind === "title"
|
|
155
|
+
? updateTitle(this.semanticSpec, parsed, value)
|
|
156
|
+
: updateEntity(this.semanticSpec, parsed, value);
|
|
157
|
+
const contextKey = CONTEXT_KEYS[parsed.kind];
|
|
158
|
+
const context = contextKey === undefined
|
|
159
|
+
? this.context
|
|
160
|
+
: freezeOwned({ ...this.context, [contextKey]: parsed.id });
|
|
161
|
+
|
|
162
|
+
return this._clone({ semanticSpec, context });
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
export function registerSemanticPrimitiveAction(ProgramClass) {
|
|
167
|
+
ProgramClass.prototype.editSemantic = editSemantic;
|
|
168
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import {
|
|
4
|
+
COLOR_LAYOUTS,
|
|
5
|
+
STACK_MODES,
|
|
6
|
+
CATEGORICAL_LEGEND_CHANNELS,
|
|
7
|
+
MARK_TYPES
|
|
8
|
+
} from "../../core/vocabulary.js";
|
|
9
|
+
import { validateAggregate } from "../../grammar/aggregate.js";
|
|
10
|
+
import { findSemanticScale, hasDataset } from "../../selectors/index.js";
|
|
11
|
+
import { validateCoordinateType } from "../../grammar/coordinates.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizeHistogramBin,
|
|
14
|
+
validateHistogramBinBoundaries,
|
|
15
|
+
validateHistogramBinStep
|
|
16
|
+
} from "../../grammar/histogram.js";
|
|
17
|
+
import { validateDatasetTransforms } from "../../grammar/transforms.js";
|
|
18
|
+
import {
|
|
19
|
+
validateSemanticFieldType,
|
|
20
|
+
validateContinuousColorInterpolation,
|
|
21
|
+
validateScalePropertyForType,
|
|
22
|
+
validateSemanticScaleDomain,
|
|
23
|
+
validateSemanticScaleRange,
|
|
24
|
+
validateSemanticScaleType
|
|
25
|
+
} from "../../grammar/scales.js";
|
|
26
|
+
|
|
27
|
+
function nonEmptyString(value, label) {
|
|
28
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
29
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateLegend(property, value) {
|
|
34
|
+
if (property === "title") {
|
|
35
|
+
nonEmptyString(value, "Legend title");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
39
|
+
throw new TypeError(`Legend ${property} must be a non-empty array.`);
|
|
40
|
+
}
|
|
41
|
+
if (new Set(value).size !== value.length) {
|
|
42
|
+
throw new Error(`Legend ${property} must not contain duplicates.`);
|
|
43
|
+
}
|
|
44
|
+
if (property === "channels") {
|
|
45
|
+
if (!value.every(channel => CATEGORICAL_LEGEND_CHANNELS.includes(channel))) {
|
|
46
|
+
throw new Error("Legend channels support only color, strokeDash, and shape.");
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const id of value) validateUserId(id, "Legend scale id");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateSemanticValue(program, parsed, value) {
|
|
54
|
+
if (parsed.kind === "dataset" && parsed.path[0] === "values") {
|
|
55
|
+
if (!Array.isArray(value) || !value.every(isPlainObject)) {
|
|
56
|
+
throw new TypeError("Dataset values must be an array of plain row objects.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (parsed.kind === "dataset" && parsed.path[0] === "source") {
|
|
60
|
+
validateUserId(value, "Dataset source id");
|
|
61
|
+
if (!hasDataset(program, value)) {
|
|
62
|
+
throw new Error(`Unknown source dataset "${value}".`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (parsed.kind === "dataset" && parsed.path[0] === "transform") {
|
|
66
|
+
validateDatasetTransforms(value);
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
parsed.kind === "layer" &&
|
|
70
|
+
parsed.path.join(".") === "mark.type" &&
|
|
71
|
+
!MARK_TYPES.includes(value)
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`Unknown mark type "${value}".`);
|
|
74
|
+
}
|
|
75
|
+
if (parsed.kind === "layer") {
|
|
76
|
+
const property = parsed.path.join(".");
|
|
77
|
+
if (property.endsWith(".title")) {
|
|
78
|
+
nonEmptyString(value, "Encoding title");
|
|
79
|
+
}
|
|
80
|
+
if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
|
|
81
|
+
if (property.endsWith(".aggregate")) validateAggregate(value);
|
|
82
|
+
if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
|
|
83
|
+
if (property.endsWith(".bin.step")) validateHistogramBinStep(value);
|
|
84
|
+
if (property.endsWith(".bin.boundaries")) {
|
|
85
|
+
validateHistogramBinBoundaries(value);
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
property.endsWith(".stack") &&
|
|
89
|
+
value !== null &&
|
|
90
|
+
!STACK_MODES.includes(value)
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(`Unsupported stack "${value}".`);
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
property === "encoding.color.layout" &&
|
|
96
|
+
!COLOR_LAYOUTS.includes(value)
|
|
97
|
+
) {
|
|
98
|
+
throw new Error(`Unsupported color layout "${value}".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (parsed.kind === "scale") {
|
|
102
|
+
const property = parsed.path.join(".");
|
|
103
|
+
const existing = findSemanticScale(program, parsed.id);
|
|
104
|
+
if (property === "type") {
|
|
105
|
+
validateSemanticScaleType(value);
|
|
106
|
+
for (const owned of [
|
|
107
|
+
"nice", "zero", "clamp", "base", "exponent", "constant",
|
|
108
|
+
"paddingInner", "paddingOuter", "padding", "align"
|
|
109
|
+
]) {
|
|
110
|
+
if (existing?.[owned] !== undefined) {
|
|
111
|
+
validateScalePropertyForType(value, owned);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else if (property === "domain") validateSemanticScaleDomain(value);
|
|
115
|
+
else if (property === "range") validateSemanticScaleRange(value);
|
|
116
|
+
else if (property === "nice") {
|
|
117
|
+
if (typeof value !== "boolean") throw new TypeError("Scale nice must be a boolean.");
|
|
118
|
+
if (existing?.type !== undefined) {
|
|
119
|
+
validateScalePropertyForType(existing.type, "nice");
|
|
120
|
+
}
|
|
121
|
+
} else if (property === "zero") {
|
|
122
|
+
if (typeof value !== "boolean") throw new TypeError("Scale zero must be a boolean.");
|
|
123
|
+
if (existing?.type !== undefined) {
|
|
124
|
+
validateScalePropertyForType(existing.type, "zero");
|
|
125
|
+
}
|
|
126
|
+
} else if (property === "clamp") {
|
|
127
|
+
if (typeof value !== "boolean") throw new TypeError("Scale clamp must be a boolean.");
|
|
128
|
+
if (existing?.type !== undefined) {
|
|
129
|
+
validateScalePropertyForType(existing.type, "clamp");
|
|
130
|
+
}
|
|
131
|
+
} else if (property === "reverse") {
|
|
132
|
+
if (typeof value !== "boolean") throw new TypeError("Scale reverse must be a boolean.");
|
|
133
|
+
} else if (["base", "exponent", "constant"].includes(property)) {
|
|
134
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
135
|
+
throw new RangeError(`Scale ${property} must be a positive finite number.`);
|
|
136
|
+
}
|
|
137
|
+
if (property === "base" && value === 1) {
|
|
138
|
+
throw new RangeError("Scale base must not equal 1.");
|
|
139
|
+
}
|
|
140
|
+
if (existing?.type !== undefined) {
|
|
141
|
+
validateScalePropertyForType(existing.type, property);
|
|
142
|
+
}
|
|
143
|
+
} else if (property === "interpolate") {
|
|
144
|
+
validateContinuousColorInterpolation(value);
|
|
145
|
+
} else if (property === "paddingInner") {
|
|
146
|
+
if (!Number.isFinite(value) || value < 0 || value >= 1) {
|
|
147
|
+
throw new RangeError(
|
|
148
|
+
"Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (existing?.type !== undefined) {
|
|
152
|
+
validateScalePropertyForType(existing.type, property);
|
|
153
|
+
}
|
|
154
|
+
} else if (property === "paddingOuter" || property === "padding") {
|
|
155
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
156
|
+
throw new RangeError(`Scale ${property} must be a non-negative finite number.`);
|
|
157
|
+
}
|
|
158
|
+
if (existing?.type !== undefined) {
|
|
159
|
+
validateScalePropertyForType(existing.type, property);
|
|
160
|
+
}
|
|
161
|
+
} else if (property === "align") {
|
|
162
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
163
|
+
throw new RangeError("Scale align must be between 0 and 1.");
|
|
164
|
+
}
|
|
165
|
+
if (existing?.type !== undefined) {
|
|
166
|
+
validateScalePropertyForType(existing.type, property);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (parsed.kind === "coordinate" && parsed.path[0] === "type") {
|
|
171
|
+
validateCoordinateType(value);
|
|
172
|
+
}
|
|
173
|
+
if (parsed.kind === "guide" && parsed.id === "legend.series") {
|
|
174
|
+
validateLegend(parsed.path.at(-1), value);
|
|
175
|
+
}
|
|
176
|
+
if (parsed.kind === "guide" && parsed.id.startsWith("grid.")) {
|
|
177
|
+
validateUserId(value, `Grid ${parsed.path.at(-1)} id`);
|
|
178
|
+
}
|
|
179
|
+
if (parsed.kind === "title") {
|
|
180
|
+
nonEmptyString(value, `Chart title ${parsed.path[0]}`);
|
|
181
|
+
}
|
|
182
|
+
}
|