ggaction 0.0.8 → 0.0.10
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 +40 -0
- package/README.md +34 -6
- package/knowledge/action-cards.json +10941 -0
- package/knowledge/extension-authoring.md +248 -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 +11 -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/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/extension.js +8 -1
- 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/types/extension.d.ts +19 -0
- package/types/program.d.ts +4 -0
- package/src/actions/coordinates/index.js +0 -5
- package/src/actions/primitives/semanticValue.js +0 -1
|
@@ -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
|
}
|
|
@@ -5,19 +5,32 @@ import { validateLayerSemanticValue } from "./layer.js";
|
|
|
5
5
|
import { validateScaleSemanticValue } from "./scale.js";
|
|
6
6
|
import { validateNonEmptySemanticString } from "./shared.js";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
export function createSemanticValueValidator({
|
|
9
|
+
validateDatasetTransforms,
|
|
10
|
+
validateParallel,
|
|
11
|
+
sourceMarkTypes
|
|
12
|
+
}) {
|
|
13
|
+
return function validateSemanticValue(program, parsed, value) {
|
|
14
|
+
if (parsed.kind === "dataset") {
|
|
15
|
+
validateDatasetSemanticValue(
|
|
16
|
+
program,
|
|
17
|
+
parsed,
|
|
18
|
+
value,
|
|
19
|
+
validateDatasetTransforms
|
|
20
|
+
);
|
|
21
|
+
} else if (parsed.kind === "layer") {
|
|
22
|
+
validateLayerSemanticValue(program, parsed, value, {
|
|
23
|
+
sourceMarkTypes,
|
|
24
|
+
validateParallel
|
|
25
|
+
});
|
|
26
|
+
} else if (parsed.kind === "scale") {
|
|
27
|
+
validateScaleSemanticValue(program, parsed, value);
|
|
28
|
+
} else if (parsed.kind === "coordinate" && parsed.path[0] === "type") {
|
|
29
|
+
validateCoordinateType(value);
|
|
30
|
+
} else if (parsed.kind === "guide") {
|
|
31
|
+
validateGuideSemanticValue(program, parsed, value);
|
|
32
|
+
} else if (parsed.kind === "title") {
|
|
33
|
+
validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
23
36
|
}
|
|
@@ -10,18 +10,13 @@ import {
|
|
|
10
10
|
validateHistogramBinBoundaries,
|
|
11
11
|
validateHistogramBinStep
|
|
12
12
|
} from "../../../grammar/histogram.js";
|
|
13
|
-
import {
|
|
14
|
-
validateParallelDimensions,
|
|
15
|
-
validateParallelKeyField,
|
|
16
|
-
validateParallelMissingPolicy
|
|
17
|
-
} from "../../../grammar/parallelCoordinates.js";
|
|
18
13
|
import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
|
|
19
14
|
import { normalizeCategoryOrder } from "../../../grammar/categoryOrder.js";
|
|
20
15
|
import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
|
|
21
16
|
import { findLayer } from "../../../selectors/layers.js";
|
|
22
17
|
import { validateNonEmptySemanticString } from "./shared.js";
|
|
23
18
|
|
|
24
|
-
function validateLayerSource(program, parsed, value) {
|
|
19
|
+
function validateLayerSource(program, parsed, value, sourceMarkTypes) {
|
|
25
20
|
validateUserId(value, "Layer source id");
|
|
26
21
|
if (value === parsed.id) {
|
|
27
22
|
throw new Error("A layer cannot use itself as its source.");
|
|
@@ -30,19 +25,32 @@ function validateLayerSource(program, parsed, value) {
|
|
|
30
25
|
if (source === undefined) {
|
|
31
26
|
throw new Error(`Unknown source layer "${value}".`);
|
|
32
27
|
}
|
|
33
|
-
if (!
|
|
28
|
+
if (!sourceMarkTypes.includes(source.mark?.type)) {
|
|
29
|
+
const sourceLabel = sourceMarkTypes.length === 1
|
|
30
|
+
? sourceMarkTypes[0]
|
|
31
|
+
: `${sourceMarkTypes.slice(0, -1).join(", ")}, or ${sourceMarkTypes.at(-1)}`;
|
|
34
32
|
throw new Error(
|
|
35
|
-
`Layer source "${value}" must be a
|
|
33
|
+
`Layer source "${value}" must be a ${sourceLabel} mark.`
|
|
36
34
|
);
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
export function validateLayerSemanticValue(
|
|
38
|
+
export function validateLayerSemanticValue(
|
|
39
|
+
program,
|
|
40
|
+
parsed,
|
|
41
|
+
value,
|
|
42
|
+
{
|
|
43
|
+
sourceMarkTypes = ["point", "bar", "rule", "rect"],
|
|
44
|
+
validateParallel
|
|
45
|
+
} = {}
|
|
46
|
+
) {
|
|
41
47
|
const property = parsed.path.join(".");
|
|
42
48
|
if (property === "mark.type" && !MARK_TYPES.includes(value)) {
|
|
43
49
|
throw new Error(`Unknown mark type "${value}".`);
|
|
44
50
|
}
|
|
45
|
-
if (property === "source")
|
|
51
|
+
if (property === "source") {
|
|
52
|
+
validateLayerSource(program, parsed, value, sourceMarkTypes);
|
|
53
|
+
}
|
|
46
54
|
if (property.endsWith(".title")) {
|
|
47
55
|
validateNonEmptySemanticString(value, "Encoding title");
|
|
48
56
|
}
|
|
@@ -52,12 +60,8 @@ export function validateLayerSemanticValue(program, parsed, value) {
|
|
|
52
60
|
if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
|
|
53
61
|
if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
|
|
54
62
|
if (property.endsWith(".categoryOrder")) normalizeCategoryOrder(value);
|
|
55
|
-
if (property
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (property === "encoding.parallel.key") validateParallelKeyField(value);
|
|
59
|
-
if (property === "encoding.parallel.missing") {
|
|
60
|
-
validateParallelMissingPolicy(value);
|
|
63
|
+
if (property.startsWith("encoding.parallel.")) {
|
|
64
|
+
validateParallel?.(property, value);
|
|
61
65
|
}
|
|
62
66
|
if (property.endsWith(".aggregate")) validateAggregate(value);
|
|
63
67
|
if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
|
package/src/core/action.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
|
|
2
2
|
|
|
3
|
+
const metadataByWrappedAction = new WeakMap();
|
|
4
|
+
|
|
5
|
+
export function getWrappedActionMetadata(value) {
|
|
6
|
+
return metadataByWrappedAction.get(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
function summarizeObject(value, ancestors = new WeakSet()) {
|
|
4
10
|
if (ancestors.has(value)) {
|
|
5
11
|
throw new TypeError("Action arguments must not contain circular references.");
|
|
@@ -107,24 +113,33 @@ export function action(metadata, implementation) {
|
|
|
107
113
|
throw new Error(`Unknown action scope "${scope}".`);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
const ownedMetadata = freezeOwned({
|
|
117
|
+
op: metadata.op,
|
|
118
|
+
description: metadata.description,
|
|
119
|
+
scope
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const wrappedAction = function wrappedAction(args = {}) {
|
|
111
123
|
if (!isPlainObject(args)) {
|
|
112
124
|
throw new TypeError("Action arguments must be a plain object.");
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
if (scope === "unit") this._assertUnitProgram(
|
|
116
|
-
if (scope === "composition") this._assertCompositionProgram(
|
|
127
|
+
if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
|
|
128
|
+
if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
|
|
117
129
|
|
|
118
130
|
const entered = this._enterAction({
|
|
119
|
-
...
|
|
131
|
+
...ownedMetadata,
|
|
120
132
|
args: summarizeArgs(args)
|
|
121
133
|
});
|
|
122
134
|
const result = implementation.call(entered, args);
|
|
123
135
|
|
|
124
136
|
if (!(result instanceof this.constructor)) {
|
|
125
|
-
throw new TypeError(`${
|
|
137
|
+
throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
return result._exitAction();
|
|
129
141
|
};
|
|
142
|
+
|
|
143
|
+
metadataByWrappedAction.set(wrappedAction, ownedMetadata);
|
|
144
|
+
return wrappedAction;
|
|
130
145
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getWrappedActionMetadata } from "./action.js";
|
|
2
|
+
import { isPlainObject } from "./immutable.js";
|
|
3
|
+
|
|
4
|
+
const ACTION_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
const DEFINITION_KEYS = new Set(["name", "actions"]);
|
|
6
|
+
const registeredExtensionNames = new Set();
|
|
7
|
+
|
|
8
|
+
function readDataProperty(object, key, owner) {
|
|
9
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
10
|
+
if (descriptor === undefined) {
|
|
11
|
+
throw new TypeError(`${owner} requires a ${key} property.`);
|
|
12
|
+
}
|
|
13
|
+
if (!("value" in descriptor)) {
|
|
14
|
+
throw new TypeError(`${owner} ${key} must be a data property.`);
|
|
15
|
+
}
|
|
16
|
+
return descriptor.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateDefinitionShape(definition) {
|
|
20
|
+
if (!isPlainObject(definition)) {
|
|
21
|
+
throw new TypeError("Extension definition must be a plain object.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const key of Reflect.ownKeys(definition)) {
|
|
25
|
+
if (typeof key !== "string" || !DEFINITION_KEYS.has(key)) {
|
|
26
|
+
throw new TypeError(`Unknown extension definition property "${String(key)}".`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const name = readDataProperty(definition, "name", "Extension definition");
|
|
31
|
+
const actions = readDataProperty(
|
|
32
|
+
definition,
|
|
33
|
+
"actions",
|
|
34
|
+
"Extension definition"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
typeof name !== "string" ||
|
|
39
|
+
name.length === 0 ||
|
|
40
|
+
name !== name.trim()
|
|
41
|
+
) {
|
|
42
|
+
throw new TypeError("Extension name must be a non-empty trimmed string.");
|
|
43
|
+
}
|
|
44
|
+
if (!isPlainObject(actions)) {
|
|
45
|
+
throw new TypeError("Extension actions must be a plain object.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { name, actions };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerProgramExtension(ProgramClass, definition) {
|
|
52
|
+
const { name, actions } = validateDefinitionShape(definition);
|
|
53
|
+
|
|
54
|
+
if (registeredExtensionNames.has(name)) {
|
|
55
|
+
throw new Error(`Extension "${name}" is already registered.`);
|
|
56
|
+
}
|
|
57
|
+
if (!Object.isExtensible(ProgramClass.prototype)) {
|
|
58
|
+
throw new Error("ChartProgram prototype does not accept extension actions.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const program = new ProgramClass();
|
|
62
|
+
const descriptors = Object.create(null);
|
|
63
|
+
const actionNames = Reflect.ownKeys(actions);
|
|
64
|
+
if (actionNames.length === 0) {
|
|
65
|
+
throw new TypeError("Extension actions must contain at least one action.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const actionName of actionNames) {
|
|
69
|
+
if (
|
|
70
|
+
typeof actionName !== "string" ||
|
|
71
|
+
!ACTION_NAME_PATTERN.test(actionName)
|
|
72
|
+
) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`Extension action name "${String(actionName)}" must be a JavaScript identifier.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const action = readDataProperty(actions, actionName, "Extension actions");
|
|
79
|
+
const metadata = getWrappedActionMetadata(action);
|
|
80
|
+
if (metadata === undefined) {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`Extension action "${actionName}" must be created with action().`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (metadata.op !== actionName) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Extension action "${actionName}" must use the same action op.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (actionName in program) {
|
|
91
|
+
throw new Error(`ChartProgram action "${actionName}" is already defined.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
descriptors[actionName] = {
|
|
95
|
+
value: action,
|
|
96
|
+
configurable: false,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
writable: false
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Object.defineProperties(ProgramClass.prototype, descriptors);
|
|
103
|
+
registeredExtensionNames.add(name);
|
|
104
|
+
}
|
package/src/extension.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export { action } from "./core/action.js";
|
|
2
|
-
|
|
2
|
+
import { ChartProgram } from "./ChartProgram.js";
|
|
3
|
+
import { registerProgramExtension } from "./core/extensionRegistry.js";
|
|
4
|
+
|
|
5
|
+
export { ChartProgram };
|
|
6
|
+
|
|
7
|
+
export function registerExtension(definition) {
|
|
8
|
+
registerProgramExtension(ChartProgram, definition);
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const TRANSFORM_TOPOLOGY = Object.freeze({
|
|
2
|
+
bin2d: Object.freeze({ facetTopology: "statistical" }),
|
|
3
|
+
boxOutlier: Object.freeze({ facetTopology: "statistical" }),
|
|
4
|
+
boxSummary: Object.freeze({ facetTopology: "statistical" }),
|
|
5
|
+
density: Object.freeze({ facetTopology: "statistical" }),
|
|
6
|
+
filter: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
7
|
+
gradientProfile: Object.freeze({ facetTopology: "statistical" }),
|
|
8
|
+
horizon: Object.freeze({ facetTopology: "statistical" }),
|
|
9
|
+
interval: Object.freeze({ facetTopology: "statistical" }),
|
|
10
|
+
markFilter: Object.freeze({ provenanceTransparent: true }),
|
|
11
|
+
regression: Object.freeze({ facetTopology: "statistical" }),
|
|
12
|
+
timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
13
|
+
window: Object.freeze({ facetTopology: "statistical" })
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function findTransformTopology(type) {
|
|
17
|
+
return TRANSFORM_TOPOLOGY[type];
|
|
18
|
+
}
|
|
@@ -19,6 +19,7 @@ import { validateMarkFilterTransform } from "./markFilter.js";
|
|
|
19
19
|
import { validateRegressionTransform } from "./regression/index.js";
|
|
20
20
|
import { validateWindowTransform } from "./window.js";
|
|
21
21
|
import { validateTimeUnitTransform } from "./timeUnit.js";
|
|
22
|
+
import { findTransformTopology } from "./transformTopology.js";
|
|
22
23
|
|
|
23
24
|
function requestedDensityTransform(transform) {
|
|
24
25
|
const { resolved: _resolved, ...requested } = transform;
|
|
@@ -41,69 +42,69 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
|
|
|
41
42
|
|
|
42
43
|
const TRANSFORM_POLICIES = Object.freeze({
|
|
43
44
|
bin2d: Object.freeze({
|
|
45
|
+
...findTransformTopology("bin2d"),
|
|
44
46
|
validate: validateBin2DTransform,
|
|
45
47
|
materializeOp: "materializeBin2DData",
|
|
46
|
-
facetTopology: "statistical",
|
|
47
48
|
replayTransform: requestedBin2DTransform
|
|
48
49
|
}),
|
|
49
50
|
boxOutlier: Object.freeze({
|
|
51
|
+
...findTransformTopology("boxOutlier"),
|
|
50
52
|
validate: validateBoxTransform,
|
|
51
|
-
materializeOp: "materializeBoxOutlierData"
|
|
52
|
-
facetTopology: "statistical"
|
|
53
|
+
materializeOp: "materializeBoxOutlierData"
|
|
53
54
|
}),
|
|
54
55
|
boxSummary: Object.freeze({
|
|
56
|
+
...findTransformTopology("boxSummary"),
|
|
55
57
|
validate: validateBoxTransform,
|
|
56
|
-
materializeOp: "materializeBoxSummaryData"
|
|
57
|
-
facetTopology: "statistical"
|
|
58
|
+
materializeOp: "materializeBoxSummaryData"
|
|
58
59
|
}),
|
|
59
60
|
density: Object.freeze({
|
|
61
|
+
...findTransformTopology("density"),
|
|
60
62
|
validate: validateDensityTransform,
|
|
61
63
|
materializeOp: "materializeDensityData",
|
|
62
|
-
facetTopology: "statistical",
|
|
63
64
|
replayTransform: requestedDensityTransform
|
|
64
65
|
}),
|
|
65
66
|
filter: Object.freeze({
|
|
67
|
+
...findTransformTopology("filter"),
|
|
66
68
|
validate: validateFilterTransform,
|
|
67
|
-
materializeOp: "materializeFilteredData"
|
|
68
|
-
facetTopology: "rowPreserving"
|
|
69
|
+
materializeOp: "materializeFilteredData"
|
|
69
70
|
}),
|
|
70
71
|
gradientProfile: Object.freeze({
|
|
72
|
+
...findTransformTopology("gradientProfile"),
|
|
71
73
|
validate: validateGradientProfileTransform,
|
|
72
74
|
materializeOp: "materializeGradientProfileData",
|
|
73
|
-
facetTopology: "statistical",
|
|
74
75
|
replayTransform: requestedGradientProfileTransform
|
|
75
76
|
}),
|
|
76
77
|
horizon: Object.freeze({
|
|
78
|
+
...findTransformTopology("horizon"),
|
|
77
79
|
validate: validateHorizonTransform,
|
|
78
80
|
materializeOp: "materializeHorizonData",
|
|
79
|
-
facetTopology: "statistical",
|
|
80
81
|
replayTransform: requestedHorizonTransform,
|
|
81
82
|
facetReplayTransform: facetHorizonTransform
|
|
82
83
|
}),
|
|
83
84
|
interval: Object.freeze({
|
|
85
|
+
...findTransformTopology("interval"),
|
|
84
86
|
validate: validateIntervalTransform,
|
|
85
|
-
materializeOp: "materializeIntervalData"
|
|
86
|
-
facetTopology: "statistical"
|
|
87
|
+
materializeOp: "materializeIntervalData"
|
|
87
88
|
}),
|
|
88
89
|
markFilter: Object.freeze({
|
|
90
|
+
...findTransformTopology("markFilter"),
|
|
89
91
|
validate: validateMarkFilterTransform,
|
|
90
|
-
materializeOp: "materializeMarkFilteredData"
|
|
91
|
-
provenanceTransparent: true
|
|
92
|
+
materializeOp: "materializeMarkFilteredData"
|
|
92
93
|
}),
|
|
93
94
|
regression: Object.freeze({
|
|
95
|
+
...findTransformTopology("regression"),
|
|
94
96
|
validate: validateRegressionTransform,
|
|
95
|
-
materializeOp: "materializeRegressionData"
|
|
96
|
-
facetTopology: "statistical"
|
|
97
|
+
materializeOp: "materializeRegressionData"
|
|
97
98
|
}),
|
|
98
99
|
timeUnit: Object.freeze({
|
|
100
|
+
...findTransformTopology("timeUnit"),
|
|
99
101
|
validate: validateTimeUnitTransform,
|
|
100
|
-
materializeOp: "materializeTimeUnitData"
|
|
101
|
-
facetTopology: "rowPreserving"
|
|
102
|
+
materializeOp: "materializeTimeUnitData"
|
|
102
103
|
}),
|
|
103
104
|
window: Object.freeze({
|
|
105
|
+
...findTransformTopology("window"),
|
|
104
106
|
validate: validateWindowTransform,
|
|
105
|
-
materializeOp: "materializeWindowData"
|
|
106
|
-
facetTopology: "statistical"
|
|
107
|
+
materializeOp: "materializeWindowData"
|
|
107
108
|
})
|
|
108
109
|
});
|
|
109
110
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
-
import {
|
|
3
|
+
import { findTransformTopology } from "../grammar/transformTopology.js";
|
|
4
4
|
import { findDataset, hasDataset } from "../selectors/datasets.js";
|
|
5
5
|
import { requireLayer } from "../selectors/layers.js";
|
|
6
6
|
|
|
@@ -60,7 +60,7 @@ export function findUpstreamTransform(program, dataset, type) {
|
|
|
60
60
|
if (current.transform?.length !== 1) return undefined;
|
|
61
61
|
const transform = current.transform[0];
|
|
62
62
|
if (transform.type === type) return transform;
|
|
63
|
-
if (
|
|
63
|
+
if (findTransformTopology(transform.type)?.provenanceTransparent !== true) {
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
66
66
|
current = findDataset(program, current.source);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAggregate } from "../../grammar/aggregate.js";
|
|
2
|
-
import {
|
|
2
|
+
import { findTransformTopology } from "../../grammar/transformTopology.js";
|
|
3
3
|
import { findDataset } from "../../selectors/datasets.js";
|
|
4
4
|
|
|
5
5
|
function hasRowPreservingProvenance(program, dataset) {
|
|
@@ -8,7 +8,7 @@ function hasRowPreservingProvenance(program, dataset) {
|
|
|
8
8
|
while (current?.source !== undefined) {
|
|
9
9
|
if (visited.has(current.id) || current.transform?.length !== 1) return false;
|
|
10
10
|
visited.add(current.id);
|
|
11
|
-
const policy =
|
|
11
|
+
const policy = findTransformTopology(current.transform[0].type);
|
|
12
12
|
if (
|
|
13
13
|
policy?.facetTopology !== "rowPreserving" &&
|
|
14
14
|
policy?.provenanceTransparent !== true
|