ggaction 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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(program, parsed, value) {
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") validateDatasetTransforms(value);
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
- const VALIDATORS = Object.freeze({
9
- dataset: validateDatasetSemanticValue,
10
- layer: validateLayerSemanticValue,
11
- scale: validateScaleSemanticValue,
12
- coordinate(_program, parsed, value) {
13
- if (parsed.path[0] === "type") validateCoordinateType(value);
14
- },
15
- guide: validateGuideSemanticValue,
16
- title(_program, parsed, value) {
17
- validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
18
- }
19
- });
20
-
21
- export function validateSemanticValue(program, parsed, value) {
22
- VALIDATORS[parsed.kind]?.(program, parsed, value);
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 (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
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 point, bar, rule, or rect mark.`
33
+ `Layer source "${value}" must be a ${sourceLabel} mark.`
36
34
  );
37
35
  }
38
36
  }
39
37
 
40
- export function validateLayerSemanticValue(program, parsed, value) {
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") validateLayerSource(program, parsed, value);
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 === "encoding.parallel.dimensions") {
56
- validateParallelDimensions(value, { normalized: true });
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 });
@@ -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 { findTransformPolicy } from "../grammar/transforms.js";
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 (findTransformPolicy(transform.type)?.provenanceTransparent !== true) {
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 { findTransformPolicy } from "../../grammar/transforms.js";
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 = findTransformPolicy(current.transform[0].type);
11
+ const policy = findTransformTopology(current.transform[0].type);
12
12
  if (
13
13
  policy?.facetTopology !== "rowPreserving" &&
14
14
  policy?.provenanceTransparent !== true