ggaction 0.0.4 → 0.0.6

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.
Files changed (169) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -16,7 +16,9 @@ import { materializeFacetGraphics } from "../../materialization/facets.js";
16
16
  const CONCAT_OPTIONS = Object.freeze([
17
17
  "id", "programs", "gap", "align", "padding"
18
18
  ]);
19
- const LAYOUT_EDIT_OPTIONS = Object.freeze(["gap", "align", "padding"]);
19
+ const LAYOUT_EDIT_OPTIONS = Object.freeze([
20
+ "columns", "gap", "align", "padding"
21
+ ]);
20
22
  const REPLACEMENT_OPTIONS = Object.freeze(["target", "program"]);
21
23
 
22
24
  function normalizeChildEntry(entry, index) {
@@ -149,7 +151,7 @@ export const vconcatAction = concatAction("vertical", "vconcat");
149
151
  const editCompositionLayout = action(
150
152
  {
151
153
  op: "editCompositionLayout",
152
- description: "Edit composition spacing and alignment.",
154
+ description: "Edit composition layout.",
153
155
  scope: "composition"
154
156
  },
155
157
  function (args = {}) {
@@ -164,7 +166,9 @@ const editCompositionLayout = action(
164
166
  ...childDescriptor({ id, program: this.children[id] }),
165
167
  value: current.facet.values[index]
166
168
  })),
167
- columns: current.columns,
169
+ columns: Object.hasOwn(args, "columns")
170
+ ? args.columns
171
+ : current.columns,
168
172
  gap: Object.hasOwn(args, "gap") ? args.gap : current.gap,
169
173
  align: Object.hasOwn(args, "align") ? args.align : current.align,
170
174
  padding: Object.hasOwn(args, "padding")
@@ -176,12 +180,18 @@ const editCompositionLayout = action(
176
180
  children: this.children,
177
181
  compositionSpec: {
178
182
  ...current,
183
+ columns: layout.columns,
179
184
  gap: layout.gap,
180
185
  align: layout.align,
181
186
  padding: layout.padding
182
187
  }
183
188
  }).materializeComposition();
184
189
  }
190
+ if (Object.hasOwn(args, "columns")) {
191
+ throw new Error(
192
+ "editCompositionLayout columns is available only on a facet composition."
193
+ );
194
+ }
185
195
  const padding = Object.hasOwn(args, "padding")
186
196
  ? normalizeCompositionPadding(args.padding, current.padding)
187
197
  : current.padding;
@@ -0,0 +1,50 @@
1
+ import { validateUserId } from "../../core/identifiers.js";
2
+ import { findCoordinate } from "../../selectors/coordinates.js";
3
+
4
+ export function resolveParallelCoordinate(program, {
5
+ requested,
6
+ layer,
7
+ operation,
8
+ useCurrent = false
9
+ }) {
10
+ const explicit = requested === undefined
11
+ ? undefined
12
+ : validateUserId(requested, "Parallel coordinate id");
13
+ if (layer?.coordinate !== undefined) {
14
+ if (explicit !== undefined && explicit !== layer.coordinate) {
15
+ throw new Error(
16
+ `Layer "${layer.id}" already uses coordinate "${layer.coordinate}".`
17
+ );
18
+ }
19
+ const coordinate = findCoordinate(program, layer.coordinate);
20
+ if (coordinate?.type !== "parallel") {
21
+ throw new Error(`Coordinate "${layer.coordinate}" is not Parallel.`);
22
+ }
23
+ return { id: layer.coordinate, create: false };
24
+ }
25
+ if (explicit !== undefined) {
26
+ const coordinate = findCoordinate(program, explicit);
27
+ if (coordinate !== undefined && coordinate.type !== "parallel") {
28
+ throw new Error(`Coordinate "${explicit}" is not Parallel.`);
29
+ }
30
+ return { id: explicit, create: coordinate === undefined };
31
+ }
32
+ const current = useCurrent ? program.context.currentCoordinate : undefined;
33
+ if (current !== undefined && findCoordinate(program, current)?.type === "parallel") {
34
+ return { id: current, create: false };
35
+ }
36
+ const compatible = program.semanticSpec.coordinates.filter(
37
+ coordinate => coordinate.type === "parallel"
38
+ );
39
+ if (compatible.length > 1) {
40
+ throw new Error(
41
+ `${operation} requires coordinate when multiple Parallel coordinates are available.`
42
+ );
43
+ }
44
+ if (compatible.length === 1) return { id: compatible[0].id, create: false };
45
+ const conflict = findCoordinate(program, "parallel");
46
+ if (conflict !== undefined) {
47
+ throw new Error('Coordinate "parallel" already exists with a different type.');
48
+ }
49
+ return { id: "parallel", create: true };
50
+ }
@@ -0,0 +1,260 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveBin2DRows,
6
+ normalizeBin2DTransform,
7
+ requestedBin2DTransform
8
+ } from "../../grammar/bin2d.js";
9
+ import { applyLayerDataRematerialization } from
10
+ "../../materialization/dependencies.js";
11
+ import { planDerivedDataRevision } from
12
+ "../../materialization/dataProvenance.js";
13
+ import {
14
+ findDataset,
15
+ findDatasetConsumer
16
+ } from "../../selectors/datasets.js";
17
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
18
+
19
+ const OPTIONS = Object.freeze([
20
+ "id", "source", "x", "y", "bins", "extent", "includeEmpty", "members", "as"
21
+ ]);
22
+ const EDIT_OPTIONS = Object.freeze([
23
+ "target", "source", "x", "y", "bins", "extent", "includeEmpty", "members", "as"
24
+ ]);
25
+ const EDITABLE = Object.freeze(EDIT_OPTIONS.slice(1));
26
+ const OUTPUT_FIELDS = Object.freeze([
27
+ "x0", "x1", "y0", "y1", "count"
28
+ ]);
29
+
30
+ function ownerConfig(program, id) {
31
+ return program.materializationConfigs.data?.bin2d?.[id];
32
+ }
33
+
34
+ function ownerEntries(program) {
35
+ return Object.entries(program.materializationConfigs.data?.bin2d ?? {});
36
+ }
37
+
38
+ function resolveBin2DOwner(program, requested) {
39
+ if (requested !== undefined) {
40
+ const owner = validateUserId(requested, "2D bin owner id");
41
+ if (ownerConfig(program, owner) === undefined) {
42
+ throw new Error(`Unknown 2D bin owner "${owner}".`);
43
+ }
44
+ return owner;
45
+ }
46
+ const owners = ownerEntries(program);
47
+ const current = owners.filter(([, config]) =>
48
+ config?.current === program.context.currentData
49
+ );
50
+ if (current.length === 1) return current[0][0];
51
+ if (current.length > 1) {
52
+ throw new Error("2D bin owner is ambiguous; provide target.");
53
+ }
54
+ if (owners.length === 1) return owners[0][0];
55
+ if (owners.length === 0) throw new Error("No 2D bin owner is available.");
56
+ throw new Error("2D bin owner is ambiguous; provide target.");
57
+ }
58
+
59
+ function requireCurrentBin2D(program, id, config) {
60
+ const current = findDataset(program, config.current);
61
+ if (
62
+ current?.source === undefined ||
63
+ current.transform?.length !== 1 ||
64
+ current.transform[0].type !== "bin2d"
65
+ ) {
66
+ throw new Error(`2D bin owner "${id}" has no current derived dataset.`);
67
+ }
68
+ return current;
69
+ }
70
+
71
+ function directLayerConsumers(program, data) {
72
+ return program.semanticSpec.layers
73
+ .filter(layer => layer.data === data)
74
+ .map(layer => layer.id);
75
+ }
76
+
77
+ function rejectDerivedConsumers(program, data) {
78
+ const dependent = findDatasetConsumer(program, data);
79
+ if (dependent !== undefined) {
80
+ throw new Error(
81
+ `Cannot replace 2D bin dataset "${data}" while derived dataset ` +
82
+ `"${dependent.id}" depends on it.`
83
+ );
84
+ }
85
+ }
86
+
87
+ function preflight(program, sourceId, transform) {
88
+ const source = findDataset(program, sourceId);
89
+ if (source?.values === undefined) {
90
+ throw new Error(`Source dataset "${sourceId}" has no values.`);
91
+ }
92
+ deriveBin2DRows(source.values, transform);
93
+ }
94
+
95
+ function requireCompleteEditOutputFields(value, members) {
96
+ const required = [...OUTPUT_FIELDS, ...(members ? ["members"] : [])];
97
+ const missing = required.find(field => !Object.hasOwn(value, field));
98
+ if (missing !== undefined) {
99
+ throw new Error(
100
+ `editBin2DData as requires the complete "${missing}" output field.`
101
+ );
102
+ }
103
+ }
104
+
105
+ function editedTransform(owner, previous, args) {
106
+ const prior = requestedBin2DTransform(previous.transform[0]);
107
+ const option = property => Object.hasOwn(args, property)
108
+ ? args[property]
109
+ : prior[property];
110
+ const members = option("members");
111
+ let as = option("as");
112
+ if (!Object.hasOwn(args, "as")) {
113
+ as = { ...as };
114
+ if (members && as.members === undefined) {
115
+ as.members = `__${owner}_members`;
116
+ } else if (!members) {
117
+ delete as.members;
118
+ }
119
+ }
120
+ const transform = normalizeBin2DTransform({
121
+ id: owner,
122
+ x: option("x"),
123
+ y: option("y"),
124
+ bins: option("bins"),
125
+ extent: option("extent"),
126
+ includeEmpty: option("includeEmpty"),
127
+ members,
128
+ as
129
+ });
130
+ if (Object.hasOwn(args, "as")) {
131
+ requireCompleteEditOutputFields(args.as, members);
132
+ }
133
+ return transform;
134
+ }
135
+
136
+ function sameRequestedTransform(left, right) {
137
+ return JSON.stringify(left) === JSON.stringify(right);
138
+ }
139
+
140
+ function applyBin2DRevision(program, { owner, previous, source, transform }) {
141
+ rejectDerivedConsumers(program, previous.id);
142
+ const consumers = directLayerConsumers(program, previous.id);
143
+ const revision = planDerivedDataRevision(program, {
144
+ owner,
145
+ role: "Bin2DData",
146
+ previous: previous.id,
147
+ consumers
148
+ });
149
+ let next = program
150
+ .createDerivedData({ id: revision.id, source, transform: [transform] })
151
+ .materializeBin2DData({ id: revision.id });
152
+ for (const rebind of revision.rebinds) {
153
+ next = next.rebindLayerData(rebind);
154
+ next = applyLayerDataRematerialization(next, rebind.id);
155
+ }
156
+ next = next.releaseDerivedData(revision.release);
157
+ return next._withMaterializationConfig(
158
+ ["data", "bin2d", owner],
159
+ { current: revision.id }
160
+ );
161
+ }
162
+
163
+ export const materializeBin2DData = action(
164
+ {
165
+ op: "materializeBin2DData",
166
+ description: "Materialize one immutable rectangular 2D-bin dataset."
167
+ },
168
+ function (args = {}) {
169
+ validateKeys(args, MATERIALIZE_OPTIONS, "materializeBin2DData");
170
+ const { id, source, transform } = requireDerivedDataset(
171
+ this,
172
+ args.id,
173
+ "bin2d"
174
+ );
175
+ const result = deriveBin2DRows(source.values, transform);
176
+ return this
177
+ .editSemantic({
178
+ property: `dataset[${id}].transform`,
179
+ value: [{ ...transform, resolved: result.resolved }]
180
+ })
181
+ .editSemantic({
182
+ property: `dataset[${id}].values`,
183
+ value: result.values
184
+ });
185
+ }
186
+ );
187
+
188
+ export const createBin2DData = action(
189
+ {
190
+ op: "createBin2DData",
191
+ description: "Create or revise immutable rectangular 2D-bin values."
192
+ },
193
+ function (args = {}) {
194
+ validateKeys(args, OPTIONS, "createBin2DData");
195
+ const owner = validateUserId(args.id, "2D bin dataset id");
196
+ const config = ownerConfig(this, owner);
197
+ const previous = config === undefined
198
+ ? undefined
199
+ : requireCurrentBin2D(this, owner, config);
200
+ const source = validateUserId(
201
+ args.source ?? previous?.source ?? this.context.currentData,
202
+ "Source dataset id"
203
+ );
204
+ const transform = normalizeBin2DTransform({ ...args, id: owner });
205
+ preflight(this, source, transform);
206
+
207
+ if (previous === undefined) {
208
+ return this
209
+ .createDerivedData({ id: owner, source, transform: [transform] })
210
+ .materializeBin2DData({ id: owner })
211
+ ._withMaterializationConfig(
212
+ ["data", "bin2d", owner],
213
+ { current: owner }
214
+ );
215
+ }
216
+
217
+ return applyBin2DRevision(this, {
218
+ owner,
219
+ previous,
220
+ source,
221
+ transform
222
+ });
223
+ }
224
+ );
225
+
226
+ export const editBin2DData = action(
227
+ {
228
+ op: "editBin2DData",
229
+ description: "Partially revise one logical rectangular 2D-bin owner."
230
+ },
231
+ function (args = {}) {
232
+ validateKeys(args, EDIT_OPTIONS, "editBin2DData");
233
+ if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
234
+ throw new Error("editBin2DData requires at least one transform or source option.");
235
+ }
236
+ const owner = resolveBin2DOwner(this, args.target);
237
+ const previous = requireCurrentBin2D(this, owner, ownerConfig(this, owner));
238
+ const source = validateUserId(
239
+ args.source ?? previous.source,
240
+ "2D bin source dataset id"
241
+ );
242
+ const transform = editedTransform(owner, previous, args);
243
+ if (
244
+ source === previous.source &&
245
+ sameRequestedTransform(
246
+ transform,
247
+ requestedBin2DTransform(previous.transform[0])
248
+ )
249
+ ) {
250
+ throw new Error("editBin2DData requires an actual transform or source change.");
251
+ }
252
+ preflight(this, source, transform);
253
+ const revision = { owner, previous, source, transform };
254
+
255
+ // Execute one speculative immutable branch so every consumer plan is known
256
+ // to succeed before the returned action trace records its first child.
257
+ applyBin2DRevision(this, revision);
258
+ return applyBin2DRevision(this, revision);
259
+ }
260
+ );
@@ -8,6 +8,23 @@ const OPTIONS = Object.freeze([
8
8
  "id", "source", "field", "groupBy", "bandwidth", "extent", "steps",
9
9
  "kernel", "normalization", "as"
10
10
  ]);
11
+ const CATEGORICAL_OPTIONS = Object.freeze([...OPTIONS, "placement"]);
12
+
13
+ function densityTransform(args, placement) {
14
+ return {
15
+ type: "density",
16
+ field: args.field,
17
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
18
+ bandwidth: args.bandwidth ?? "auto",
19
+ extent: args.extent ?? "auto",
20
+ steps: args.steps ?? 100,
21
+ kernel: args.kernel ?? "gaussian",
22
+ normalization: args.normalization ?? "unit",
23
+ as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
24
+ resolve: "shared",
25
+ ...(placement === undefined ? {} : { placement })
26
+ };
27
+ }
11
28
 
12
29
  export const materializeDensityData = action(
13
30
  { op: "materializeDensityData", description: "Materialize one grouped kernel-density dataset." },
@@ -26,7 +43,10 @@ export const materializeDensityData = action(
26
43
  ...transform,
27
44
  resolved: {
28
45
  bandwidth: result.bandwidth,
29
- extent: result.extent
46
+ extent: result.extent,
47
+ ...(result.splitDomain === undefined
48
+ ? {}
49
+ : { splitDomain: result.splitDomain })
30
50
  }
31
51
  }]
32
52
  })
@@ -49,20 +69,41 @@ export const createDensityData = action(
49
69
  if (typeof args.field !== "string" || args.field.length === 0) {
50
70
  throw new TypeError("createDensityData requires a non-empty field string.");
51
71
  }
52
- const transform = {
53
- type: "density",
54
- field: args.field,
55
- ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
56
- bandwidth: args.bandwidth ?? "auto",
57
- extent: args.extent ?? "auto",
58
- steps: args.steps ?? 100,
59
- kernel: args.kernel ?? "gaussian",
60
- normalization: args.normalization ?? "unit",
61
- as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
62
- resolve: "shared"
63
- };
72
+ const transform = densityTransform(args);
64
73
  return this
65
74
  .createDerivedData({ id, source, transform: [transform] })
66
75
  .materializeDensityData({ id });
67
76
  }
68
77
  );
78
+
79
+ export const createCategoricalDensityData = action(
80
+ {
81
+ op: "createCategoricalDensityData",
82
+ description: "Create category-placed kernel-density values."
83
+ },
84
+ function (args = {}) {
85
+ validateKeys(args, CATEGORICAL_OPTIONS, "createCategoricalDensityData");
86
+ const id = validateUserId(args.id, "Density dataset id");
87
+ const source = validateUserId(
88
+ args.source ?? this.context.currentData,
89
+ "Source dataset id"
90
+ );
91
+ if (typeof args.field !== "string" || args.field.length === 0) {
92
+ throw new TypeError(
93
+ "createCategoricalDensityData requires a non-empty field string."
94
+ );
95
+ }
96
+ if (args.placement?.type !== "category") {
97
+ throw new Error(
98
+ "createCategoricalDensityData requires normalized category placement."
99
+ );
100
+ }
101
+ return this
102
+ .createDerivedData({
103
+ id,
104
+ source,
105
+ transform: [densityTransform(args, args.placement)]
106
+ })
107
+ .materializeDensityData({ id });
108
+ }
109
+ );
@@ -3,9 +3,11 @@ import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
4
  import { hasDataset } from "../../selectors/index.js";
5
5
  import { findDataset } from "../../selectors/datasets.js";
6
+ import { requireLayer } from "../../selectors/layers.js";
6
7
 
7
8
  const OPTIONS = Object.freeze(["id", "source", "transform"]);
8
9
  const RELEASE_OPTIONS = Object.freeze(["id"]);
10
+ const REBIND_OPTIONS = Object.freeze(["id", "data"]);
9
11
 
10
12
  export const createDerivedData = action(
11
13
  { op: "createDerivedData", description: "Create an immutable derived dataset definition." },
@@ -50,3 +52,23 @@ export const releaseDerivedData = action(
50
52
  });
51
53
  }
52
54
  );
55
+
56
+ export const rebindLayerData = action(
57
+ {
58
+ op: "rebindLayerData",
59
+ description: "Rebind one semantic layer to an existing dataset."
60
+ },
61
+ function (args = {}) {
62
+ validateKeys(args, REBIND_OPTIONS, "rebindLayerData");
63
+ const id = validateUserId(args.id, "Layer id");
64
+ const data = validateUserId(args.data, "Layer dataset id");
65
+ requireLayer(this, id);
66
+ if (!hasDataset(this, data)) {
67
+ throw new Error(`Layer dataset "${data}" does not exist.`);
68
+ }
69
+ return this.editSemantic({
70
+ property: `layer[${id}].data`,
71
+ value: data
72
+ });
73
+ }
74
+ );
@@ -125,6 +125,11 @@ export const filterMarks = action(
125
125
  candidate.data !== undefined &&
126
126
  findSelectionPolicy(candidate.mark?.type) !== undefined
127
127
  });
128
+ if (this.markConfigs[layer.id]?.itemFilterable === false) {
129
+ throw new Error(
130
+ `filterMarks does not support composite mark "${layer.id}"; filter its source dataset first.`
131
+ );
132
+ }
128
133
  const derivedId = `${layer.id}FilteredData`;
129
134
  if (hasDataset(this, derivedId)) {
130
135
  throw new Error(`Dataset "${derivedId}" already exists.`);
@@ -0,0 +1,65 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveGradientProfiles,
6
+ normalizeGradientProfileTransform
7
+ } from "../../grammar/gradientProfile.js";
8
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
9
+
10
+ const OPTIONS = Object.freeze([
11
+ "id", "source", "category", "field", "bandwidth", "extent", "steps",
12
+ "kernel", "normalization", "center", "as"
13
+ ]);
14
+
15
+ export const materializeGradientProfileData = action(
16
+ {
17
+ op: "materializeGradientProfileData",
18
+ description: "Materialize one categorical density-profile dataset."
19
+ },
20
+ function (args = {}) {
21
+ validateKeys(
22
+ args,
23
+ MATERIALIZE_OPTIONS,
24
+ "materializeGradientProfileData"
25
+ );
26
+ const { id, source, transform } = requireDerivedDataset(
27
+ this,
28
+ args.id,
29
+ "gradientProfile"
30
+ );
31
+ const result = deriveGradientProfiles(source.values, transform);
32
+ return this
33
+ .editSemantic({
34
+ property: `dataset[${id}].transform`,
35
+ value: [{
36
+ ...transform,
37
+ resolved: {
38
+ bandwidth: result.bandwidth,
39
+ extent: result.extent,
40
+ intensityDomain: result.intensityDomain
41
+ }
42
+ }]
43
+ })
44
+ .editSemantic({ property: `dataset[${id}].values`, value: result.values });
45
+ }
46
+ );
47
+
48
+ export const createGradientProfileData = action(
49
+ {
50
+ op: "createGradientProfileData",
51
+ description: "Create one immutable categorical density-profile dataset."
52
+ },
53
+ function (args = {}) {
54
+ validateKeys(args, OPTIONS, "createGradientProfileData");
55
+ const id = validateUserId(args.id, "Gradient profile dataset id");
56
+ const source = validateUserId(
57
+ args.source ?? this.context.currentData,
58
+ "Gradient profile source dataset id"
59
+ );
60
+ const transform = normalizeGradientProfileTransform(args);
61
+ return this
62
+ .createDerivedData({ id, source, transform: [transform] })
63
+ .materializeGradientProfileData({ id });
64
+ }
65
+ );
@@ -0,0 +1,71 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import {
5
+ deriveHorizon,
6
+ validateHorizonTransform
7
+ } from "../../grammar/horizon.js";
8
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
9
+
10
+ const OPTIONS = Object.freeze([
11
+ "id", "source", "x", "y", "groupBy", "bands", "baseline", "extent",
12
+ "resolve", "missing", "overflow", "palette", "as"
13
+ ]);
14
+
15
+ function requestedTransform(args) {
16
+ return validateHorizonTransform({
17
+ type: "horizon",
18
+ x: args.x,
19
+ y: args.y,
20
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
21
+ bands: args.bands ?? 3,
22
+ baseline: args.baseline ?? 0,
23
+ extent: args.extent ?? "auto",
24
+ resolve: args.resolve ?? "shared",
25
+ missing: args.missing ?? "break",
26
+ overflow: args.overflow ?? "clip",
27
+ palette: args.palette ?? {},
28
+ as: args.as
29
+ });
30
+ }
31
+
32
+ export const materializeHorizonData = action(
33
+ {
34
+ op: "materializeHorizonData",
35
+ description: "Materialize one immutable Horizon band dataset."
36
+ },
37
+ function (args = {}) {
38
+ validateKeys(args, MATERIALIZE_OPTIONS, "materializeHorizonData");
39
+ const { id, source, transform } = requireDerivedDataset(
40
+ this,
41
+ args.id,
42
+ "horizon"
43
+ );
44
+ const result = deriveHorizon(source.values, transform);
45
+ return this
46
+ .editSemantic({
47
+ property: `dataset[${id}].transform`,
48
+ value: [{ ...result.transform, resolved: result.resolved }]
49
+ })
50
+ .editSemantic({
51
+ property: `dataset[${id}].values`,
52
+ value: result.values
53
+ });
54
+ }
55
+ );
56
+
57
+ export const createHorizonData = action(
58
+ {
59
+ op: "createHorizonData",
60
+ description: "Create one immutable Horizon band dataset."
61
+ },
62
+ function (args = {}) {
63
+ validateKeys(args, OPTIONS, "createHorizonData");
64
+ const id = validateUserId(args.id, "Horizon dataset id");
65
+ const source = validateUserId(args.source, "Horizon source dataset id");
66
+ const transform = requestedTransform(args);
67
+ return this
68
+ .createDerivedData({ id, source, transform: [transform] })
69
+ .materializeHorizonData({ id });
70
+ }
71
+ );