ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -0,0 +1,539 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import {
6
+ findTransformPolicy,
7
+ normalizeDatasetTransformEdit,
8
+ requestedDatasetTransform
9
+ } from "../../grammar/transforms.js";
10
+ import {
11
+ findDataset,
12
+ hasDataset
13
+ } from "../../selectors/datasets.js";
14
+ import { applyLayerDataRematerialization } from
15
+ "../../materialization/dependencies.js";
16
+ import {
17
+ outputFieldChanges,
18
+ rebindLayerOutputFields,
19
+ rebindMaterializationDataReferences
20
+ } from "./revisionBindings.js";
21
+
22
+ const GENERIC_OPTIONS = Object.freeze([
23
+ "target", "definition", "dependents"
24
+ ]);
25
+ const DEPENDENT_POLICIES = new Set(["reject", "recompute"]);
26
+ const CREATOR_BY_TYPE = Object.freeze({
27
+ bin: "createBinData",
28
+ bin2d: "createBin2DData",
29
+ complete: "createCompleteData",
30
+ computed: "createComputedData",
31
+ density: "createDensityData",
32
+ ecdf: "createECDFData",
33
+ filter: "filterData",
34
+ fold: "createFoldData",
35
+ impute: "createImputedData",
36
+ interval: "createIntervalData",
37
+ normalize: "createNormalizedData",
38
+ regression: "createRegressionData",
39
+ stack: "createStackData",
40
+ summary: "createSummaryData",
41
+ timeUnit: "createTimeUnitData",
42
+ window: "createWindowData"
43
+ });
44
+ const REVISION_ROLE = Object.freeze({
45
+ bin: "BinData",
46
+ bin2d: "Bin2DData",
47
+ complete: "CompleteData",
48
+ computed: "ComputedData",
49
+ density: "DensityData",
50
+ ecdf: "ECDFData",
51
+ filter: "FilteredData",
52
+ fold: "FoldData",
53
+ impute: "ImputedData",
54
+ interval: "IntervalData",
55
+ normalize: "NormalizedData",
56
+ regression: "RegressionData",
57
+ stack: "StackData",
58
+ summary: "SummaryData",
59
+ timeUnit: "TimeUnitData",
60
+ window: "WindowData"
61
+ });
62
+
63
+ function ownerRecords(program, type) {
64
+ const families = program.materializationConfigs.data ?? {};
65
+ return Object.entries(families).flatMap(([family, owners]) =>
66
+ Object.entries(owners ?? {}).flatMap(([owner, config]) => {
67
+ if (typeof config?.current !== "string") return [];
68
+ const dataset = findDataset(program, config.current);
69
+ const currentType = dataset?.transform?.[0]?.type ?? family;
70
+ if (type !== undefined && currentType !== type) return [];
71
+ return [{ family, owner, current: config.current, dataset }];
72
+ })
73
+ );
74
+ }
75
+
76
+ function escapedPattern(value) {
77
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+ }
79
+
80
+ function staleOwnerRecord(program, requested, type) {
81
+ return ownerRecords(program, type).find(record => {
82
+ const transformType = record.dataset?.transform?.[0]?.type ?? record.family;
83
+ const role = REVISION_ROLE[transformType];
84
+ if (role === undefined || requested === record.current) return false;
85
+ return new RegExp(
86
+ `^${escapedPattern(record.owner)}${role}Revision\\d+$`
87
+ ).test(requested);
88
+ });
89
+ }
90
+
91
+ function topLevelCreatorRecorded(program, dataset) {
92
+ const creator = CREATOR_BY_TYPE[dataset.transform?.[0]?.type];
93
+ return creator !== undefined && program.trace.children.some(node =>
94
+ node.op === creator && node.args?.id === dataset.id
95
+ );
96
+ }
97
+
98
+ export function resolveDerivedDataOwner(program, target, type) {
99
+ const requested = validateUserId(target, "Derived dataset target");
100
+ const matching = ownerRecords(program, type).filter(record =>
101
+ record.owner === requested || record.current === requested
102
+ );
103
+ if (matching.length > 1) {
104
+ throw new Error(`Derived dataset target "${requested}" is ambiguous.`);
105
+ }
106
+ if (matching.length === 1) {
107
+ const record = matching[0];
108
+ if (record.dataset === undefined) {
109
+ throw new Error(
110
+ `Derived dataset owner "${record.owner}" has no current dataset.`
111
+ );
112
+ }
113
+ return { ...record, lazy: false };
114
+ }
115
+
116
+ const dataset = findDataset(program, requested);
117
+ if (dataset === undefined) {
118
+ throw new Error(staleOwnerRecord(program, requested, type) !== undefined
119
+ ? `Derived dataset revision "${requested}" is stale.`
120
+ : `Unknown derived dataset owner "${requested}".`
121
+ );
122
+ }
123
+ if (staleOwnerRecord(program, requested, type) !== undefined) {
124
+ throw new Error(`Derived dataset revision "${requested}" is stale.`);
125
+ }
126
+ if (dataset.source === undefined || dataset.transform?.length !== 1) {
127
+ throw new Error(
128
+ `Dataset "${requested}" is a source dataset and cannot be edited.`
129
+ );
130
+ }
131
+ const currentType = dataset.transform[0].type;
132
+ if (type !== undefined && currentType !== type) {
133
+ throw new Error(
134
+ `Derived dataset "${requested}" is ${currentType}, not ${type}.`
135
+ );
136
+ }
137
+ if (
138
+ findTransformPolicy(currentType)?.editable !== true ||
139
+ !topLevelCreatorRecorded(program, dataset)
140
+ ) {
141
+ throw new Error(
142
+ `Derived dataset "${requested}" is chart-owned; use its owning chart editor.`
143
+ );
144
+ }
145
+ return {
146
+ family: currentType,
147
+ owner: dataset.id,
148
+ current: dataset.id,
149
+ dataset,
150
+ lazy: true
151
+ };
152
+ }
153
+
154
+ export function normalizeDerivedDependentPolicy(value) {
155
+ const policy = value ?? "reject";
156
+ if (!DEPENDENT_POLICIES.has(policy)) {
157
+ throw new Error(
158
+ 'Derived data dependents must be "reject" or "recompute".'
159
+ );
160
+ }
161
+ return policy;
162
+ }
163
+
164
+ function downstreamDatasets(program, root) {
165
+ const order = new Map(
166
+ program.semanticSpec.datasets.map((dataset, index) => [dataset.id, index])
167
+ );
168
+ const bySource = new Map();
169
+ for (const dataset of program.semanticSpec.datasets) {
170
+ if (dataset.source === undefined) continue;
171
+ const children = bySource.get(dataset.source) ?? [];
172
+ children.push(dataset);
173
+ bySource.set(dataset.source, children);
174
+ }
175
+ for (const children of bySource.values()) {
176
+ children.sort((left, right) => order.get(left.id) - order.get(right.id));
177
+ }
178
+
179
+ const result = [];
180
+ const visiting = new Set();
181
+ const visited = new Set();
182
+ function visit(id) {
183
+ if (visiting.has(id)) {
184
+ throw new Error(`Derived dataset dependency graph contains a cycle at "${id}".`);
185
+ }
186
+ if (visited.has(id)) return;
187
+ visiting.add(id);
188
+ for (const child of bySource.get(id) ?? []) {
189
+ if (child.transform?.length !== 1) {
190
+ throw new Error(
191
+ `Derived dataset "${child.id}" requires one replayable transform.`
192
+ );
193
+ }
194
+ const policy = findTransformPolicy(child.transform[0].type);
195
+ if (typeof policy?.materializeOp !== "string") {
196
+ throw new Error(
197
+ `Derived dataset "${child.id}" has unsupported transform ` +
198
+ `"${child.transform[0].type ?? "unknown"}".`
199
+ );
200
+ }
201
+ // Statistical reference data is an owned cache. Its mark lifecycle
202
+ // reconnects and recomputes it after the visible source mark is rebound.
203
+ if (child.transform[0].type === "statisticalReference") continue;
204
+ result.push(child);
205
+ visit(child.id);
206
+ }
207
+ visiting.delete(id);
208
+ visited.add(id);
209
+ }
210
+ visit(root);
211
+ return result;
212
+ }
213
+
214
+ function ownerForCurrent(program, dataset) {
215
+ const matches = ownerRecords(program).filter(record =>
216
+ record.current === dataset.id
217
+ );
218
+ if (matches.length > 1) {
219
+ throw new Error(
220
+ `Derived dataset "${dataset.id}" has ambiguous logical ownership.`
221
+ );
222
+ }
223
+ return matches[0];
224
+ }
225
+
226
+ function nextRevisionId(program, owner, role, reserved) {
227
+ let revision = 1;
228
+ const prefix = `${owner}${role}Revision`;
229
+ while (
230
+ hasDataset(program, `${prefix}${revision}`) ||
231
+ reserved.has(`${prefix}${revision}`)
232
+ ) revision += 1;
233
+ const id = `${prefix}${revision}`;
234
+ reserved.add(id);
235
+ return id;
236
+ }
237
+
238
+ function buildRevisionPlan(
239
+ program,
240
+ resolved,
241
+ definition,
242
+ dependents,
243
+ rootSource = resolved.dataset.source
244
+ ) {
245
+ const downstream = downstreamDatasets(program, resolved.current);
246
+ if (dependents === "reject" && downstream.length > 0) {
247
+ throw new Error(
248
+ `Cannot edit derived dataset "${resolved.owner}" while derived dataset ` +
249
+ `"${downstream[0].id}" depends on it; use dependents: "recompute".`
250
+ );
251
+ }
252
+ if (downstream.some(dataset => dataset.id === rootSource)) {
253
+ throw new Error(
254
+ `Derived dataset "${resolved.owner}" cannot use one of its dependents as source.`
255
+ );
256
+ }
257
+ const reserved = new Set();
258
+ const revisions = [];
259
+ const replacements = new Map();
260
+ const add = (dataset, ownerRecord, transform, requestedSource) => {
261
+ const owner = ownerRecord?.owner ?? dataset.id;
262
+ const role = REVISION_ROLE[transform.type] ?? "DerivedData";
263
+ const id = nextRevisionId(program, owner, role, reserved);
264
+ const originalSource = requestedSource ?? dataset.source;
265
+ const source = replacements.get(originalSource) ?? originalSource;
266
+ const revision = {
267
+ old: dataset.id,
268
+ id,
269
+ source,
270
+ transform,
271
+ family: ownerRecord?.family,
272
+ owner: ownerRecord?.owner
273
+ };
274
+ revisions.push(revision);
275
+ replacements.set(dataset.id, id);
276
+ };
277
+ add(resolved.dataset, resolved, definition, rootSource);
278
+ if (dependents === "recompute") {
279
+ for (const dataset of downstream) {
280
+ const policy = findTransformPolicy(dataset.transform[0].type);
281
+ const transform = policy.replayTransform?.(dataset.transform[0]) ??
282
+ dataset.transform[0];
283
+ add(dataset, ownerForCurrent(program, dataset), transform);
284
+ }
285
+ }
286
+ return { revisions, replacements };
287
+ }
288
+
289
+ function directLayerConsumers(program, data) {
290
+ return program.semanticSpec.layers
291
+ .filter(layer => layer.data === data)
292
+ .map(layer => layer.id);
293
+ }
294
+
295
+ function applyRevisionPlan(program, resolved, plan) {
296
+ const root = plan.revisions[0];
297
+ const changes = outputFieldChanges(
298
+ resolved.dataset.transform[0],
299
+ root.transform
300
+ );
301
+ let next = program;
302
+ for (const revision of plan.revisions) {
303
+ const policy = findTransformPolicy(revision.transform.type);
304
+ next = next.createDerivedData({
305
+ id: revision.id,
306
+ source: revision.source,
307
+ transform: [revision.transform]
308
+ });
309
+ if (revision.transform.type === "markFilter") {
310
+ next = next.rebindLayerData({
311
+ id: revision.transform.target,
312
+ data: revision.source
313
+ });
314
+ }
315
+ next = next[policy.materializeOp]({ id: revision.id });
316
+ }
317
+
318
+ const consumers = plan.revisions.flatMap(revision =>
319
+ directLayerConsumers(program, revision.old).map(id => ({
320
+ id,
321
+ old: revision.old,
322
+ data: revision.id,
323
+ root: revision.old === root.old
324
+ }))
325
+ );
326
+ for (const consumer of consumers) {
327
+ next = next.rebindLayerData({ id: consumer.id, data: consumer.data });
328
+ }
329
+ for (const consumer of consumers) {
330
+ if (consumer.root) {
331
+ next = rebindLayerOutputFields(next, consumer.id, changes);
332
+ }
333
+ }
334
+
335
+ next = rebindMaterializationDataReferences(next, plan.replacements);
336
+ for (const revision of plan.revisions) {
337
+ if (revision.owner !== undefined) {
338
+ next = next._withMaterializationConfig(
339
+ ["data", revision.family, revision.owner],
340
+ { current: revision.id }
341
+ );
342
+ }
343
+ }
344
+ for (const consumer of consumers) {
345
+ next = applyLayerDataRematerialization(next, consumer.id);
346
+ }
347
+ for (const revision of [...plan.revisions].reverse()) {
348
+ next = next.releaseDerivedData({ id: revision.old });
349
+ }
350
+
351
+ const currentData = plan.replacements.get(program.context.currentData) ??
352
+ program.context.currentData;
353
+ return next._withContext({ ...program.context, currentData });
354
+ }
355
+
356
+ function sameRequested(left, right) {
357
+ return JSON.stringify(left) === JSON.stringify(right);
358
+ }
359
+
360
+ export function reviseDerivedData(program, {
361
+ resolved,
362
+ definition,
363
+ dependents = "reject",
364
+ source = resolved.dataset.source
365
+ }) {
366
+ const policy = normalizeDerivedDependentPolicy(dependents);
367
+ const plan = buildRevisionPlan(
368
+ program,
369
+ resolved,
370
+ definition,
371
+ policy,
372
+ source
373
+ );
374
+ applyRevisionPlan(program, resolved, plan);
375
+ return applyRevisionPlan(program, resolved, plan);
376
+ }
377
+
378
+ export const editDerivedData = action(
379
+ {
380
+ op: "editDerivedData",
381
+ description: "Atomically revise one standalone derived-data definition."
382
+ },
383
+ function (args = {}) {
384
+ validateKeys(args, GENERIC_OPTIONS, "editDerivedData");
385
+ if (!Object.hasOwn(args, "target")) {
386
+ throw new TypeError("editDerivedData requires target.");
387
+ }
388
+ if (!isPlainObject(args.definition)) {
389
+ throw new TypeError("editDerivedData definition must be a plain object.");
390
+ }
391
+ for (const forbidden of ["id", "source", "resolved", "current"]) {
392
+ if (Object.hasOwn(args.definition, forbidden)) {
393
+ throw new Error(`editDerivedData definition cannot include ${forbidden}.`);
394
+ }
395
+ }
396
+ const dependents = normalizeDerivedDependentPolicy(args.dependents);
397
+ const resolved = resolveDerivedDataOwner(
398
+ this,
399
+ args.target,
400
+ args.definition.type
401
+ );
402
+ const definition = requestedDatasetTransform(args.definition);
403
+ findTransformPolicy(definition.type).validate(definition);
404
+ const previous = requestedDatasetTransform(resolved.dataset.transform[0]);
405
+ if (sameRequested(definition, previous)) return this;
406
+
407
+ return reviseDerivedData(this, { resolved, definition, dependents });
408
+ }
409
+ );
410
+
411
+ function focusedEditor(op, type, properties) {
412
+ const supported = Object.freeze(["target", "dependents", ...properties]);
413
+ return action(
414
+ { op, description: `Partially revise one standalone ${type} dataset.` },
415
+ function (args = {}) {
416
+ validateKeys(args, supported, op);
417
+ if (!Object.hasOwn(args, "target")) {
418
+ throw new TypeError(`${op} requires target.`);
419
+ }
420
+ const patch = Object.fromEntries(Object.entries(args).filter(
421
+ ([key]) => key !== "target" && key !== "dependents"
422
+ ));
423
+ if (Object.keys(patch).length === 0) {
424
+ throw new Error(`${op} requires at least one transform option.`);
425
+ }
426
+ const undefinedKey = Object.keys(patch).find(
427
+ key => patch[key] === undefined
428
+ );
429
+ if (undefinedKey !== undefined) {
430
+ throw new TypeError(
431
+ `${op} ${undefinedKey} cannot be undefined; omission preserves it.`
432
+ );
433
+ }
434
+ const resolved = resolveDerivedDataOwner(this, args.target, type);
435
+ const definition = normalizeDatasetTransformEdit(
436
+ resolved.dataset.transform[0],
437
+ patch,
438
+ resolved.owner
439
+ );
440
+ return this.editDerivedData({
441
+ target: args.target,
442
+ definition,
443
+ ...(args.dependents === undefined
444
+ ? {}
445
+ : { dependents: args.dependents })
446
+ });
447
+ }
448
+ );
449
+ }
450
+
451
+ export const editComputedData = focusedEditor(
452
+ "editComputedData", "computed", ["as", "expression"]
453
+ );
454
+ export const editFilteredData = focusedEditor(
455
+ "editFilteredData", "filter", ["field", "oneOf", "predicate", "range"]
456
+ );
457
+ export const editFoldData = focusedEditor(
458
+ "editFoldData", "fold", ["fields", "as"]
459
+ );
460
+ export const editSummaryData = focusedEditor(
461
+ "editSummaryData", "summary", ["groupBy", "aggregates", "members", "weight"]
462
+ );
463
+ export const editBinData = focusedEditor(
464
+ "editBinData", "bin", [
465
+ "field", "maxBins", "step", "boundaries", "extent", "nice", "zero",
466
+ "includeEmpty", "members", "as", "weight"
467
+ ]
468
+ );
469
+ export const editTimeUnitData = focusedEditor(
470
+ "editTimeUnitData", "timeUnit", [
471
+ "field", "unit", "as", "temporalUnit", "timeZone", "weekStartsOn",
472
+ "weekRule"
473
+ ]
474
+ );
475
+ export const editWindowData = focusedEditor(
476
+ "editWindowData", "window", [
477
+ "partitionBy", "sortBy", "operations", "temporalUnit"
478
+ ]
479
+ );
480
+ export const editDensityData = focusedEditor(
481
+ "editDensityData", "density", [
482
+ "field", "groupBy", "bandwidth", "extent", "steps", "kernel",
483
+ "normalization", "as", "weight"
484
+ ]
485
+ );
486
+ export const editStackData = focusedEditor(
487
+ "editStackData", "stack", ["category", "group", "value", "mode", "as"]
488
+ );
489
+ export const editRegressionData = focusedEditor(
490
+ "editRegressionData", "regression", [
491
+ "x", "y", "groupBy", "method", "degree", "span", "confidenceMethod",
492
+ "level", "confidence", "interval"
493
+ ]
494
+ );
495
+ export const editIntervalData = focusedEditor(
496
+ "editIntervalData", "interval", [
497
+ "field", "groupBy", "center", "extent", "method", "level", "as"
498
+ ]
499
+ );
500
+ export const editECDFData = focusedEditor(
501
+ "editECDFData", "ecdf", [
502
+ "field", "groupBy", "weight", "missing", "as"
503
+ ]
504
+ );
505
+ export const editNormalizedData = focusedEditor(
506
+ "editNormalizedData", "normalize", [
507
+ "field", "as", "groupBy", "method", "variance", "zeroDenominator",
508
+ "baseline", "sortBy"
509
+ ]
510
+ );
511
+ export const editCompleteData = focusedEditor(
512
+ "editCompleteData", "complete", [
513
+ "key", "groupBy", "values", "sequence", "fill", "members"
514
+ ]
515
+ );
516
+ export const editImputedData = focusedEditor(
517
+ "editImputedData", "impute", [
518
+ "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
519
+ ]
520
+ );
521
+
522
+ export const EDIT_DERIVED_DATA_ACTIONS = Object.freeze({
523
+ editDerivedData,
524
+ editComputedData,
525
+ editFilteredData,
526
+ editFoldData,
527
+ editSummaryData,
528
+ editBinData,
529
+ editTimeUnitData,
530
+ editWindowData,
531
+ editDensityData,
532
+ editStackData,
533
+ editRegressionData,
534
+ editIntervalData,
535
+ editECDFData,
536
+ editNormalizedData,
537
+ editCompleteData,
538
+ editImputedData
539
+ });
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  hasDataset,
24
24
  findDataset,
25
+ resolveDatasetReference,
25
26
  requireLayer,
26
27
  resolveEligibleLayer
27
28
  } from "../../selectors/index.js";
@@ -70,18 +71,29 @@ export const filterData = action(
70
71
  function (args = {}) {
71
72
  validateKeys(args, OPTIONS, "filterData");
72
73
  const id = validateUserId(args.id, "Filtered dataset id");
73
- const source = validateUserId(
74
+ const requestedSource = validateUserId(
74
75
  args.source ?? this.context.currentData,
75
76
  "Source dataset id"
76
77
  );
78
+ const source = resolveDatasetReference(
79
+ this,
80
+ requestedSource,
81
+ "Filter source dataset"
82
+ ).id;
77
83
  const transform = normalizeFilterTransform(args);
78
- return this
84
+ const next = this
79
85
  .createDerivedData({
80
86
  id,
81
87
  source,
82
88
  transform: [transform]
83
89
  })
84
90
  .materializeFilteredData({ id });
91
+ return this.actionStack.length === 1
92
+ ? next._withMaterializationConfig(
93
+ ["data", "filter", id],
94
+ { current: id }
95
+ )
96
+ : next;
85
97
  }
86
98
  );
87
99
 
@@ -0,0 +1,26 @@
1
+ import {
2
+ deriveImputedRows,
3
+ normalizeImputeTransform
4
+ } from "../../grammar/impute.js";
5
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze([
8
+ "id", "source", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
9
+ ]);
10
+
11
+ export const materializeImputedData = derivedMaterializer(
12
+ "materializeImputedData",
13
+ "Materialize one immutable missing-value dataset.",
14
+ "impute",
15
+ deriveImputedRows
16
+ );
17
+
18
+ export const createImputedData = derivedCreator(
19
+ "createImputedData",
20
+ "Replace explicit missing cells within independent groups.",
21
+ OPTIONS,
22
+ "Imputed dataset id",
23
+ "Imputed source dataset id",
24
+ normalizeImputeTransform,
25
+ "materializeImputedData"
26
+ );
@@ -40,10 +40,18 @@ import { createSummaryData, materializeSummaryData } from "./summary.js";
40
40
  import { createBinData, materializeBinData } from "./bin.js";
41
41
  import { createFoldData, materializeFoldData } from "./fold.js";
42
42
  import { createComputedData, materializeComputedData } from "./computed.js";
43
+ import {
44
+ createNormalizedData,
45
+ materializeNormalizedData
46
+ } from "./normalize.js";
47
+ import { createCompleteData, materializeCompleteData } from "./complete.js";
48
+ import { createImputedData, materializeImputedData } from "./impute.js";
43
49
  import { createStackData, materializeStackData } from "./stack.js";
50
+ import { EDIT_DERIVED_DATA_ACTIONS } from "./edit.js";
44
51
  import { createBoxSummaryData, createBoxOutlierData, materializeBoxSummaryData, materializeBoxOutlierData } from "./box.js";
45
52
 
46
53
  export function registerDataActions(ProgramClass) {
54
+ Object.assign(ProgramClass.prototype, EDIT_DERIVED_DATA_ACTIONS);
47
55
  ProgramClass.prototype.createData = createData;
48
56
  ProgramClass.prototype.createDerivedData = createDerivedData;
49
57
  ProgramClass.prototype.bindMarkData = bindMarkData;
@@ -80,6 +88,12 @@ export function registerDataActions(ProgramClass) {
80
88
  ProgramClass.prototype.materializeFoldData = materializeFoldData;
81
89
  ProgramClass.prototype.createComputedData = createComputedData;
82
90
  ProgramClass.prototype.materializeComputedData = materializeComputedData;
91
+ ProgramClass.prototype.createNormalizedData = createNormalizedData;
92
+ ProgramClass.prototype.materializeNormalizedData = materializeNormalizedData;
93
+ ProgramClass.prototype.createCompleteData = createCompleteData;
94
+ ProgramClass.prototype.materializeCompleteData = materializeCompleteData;
95
+ ProgramClass.prototype.createImputedData = createImputedData;
96
+ ProgramClass.prototype.materializeImputedData = materializeImputedData;
83
97
  ProgramClass.prototype.createStackData = createStackData;
84
98
  ProgramClass.prototype.materializeStackData = materializeStackData;
85
99
  ProgramClass.prototype.createTimeUnitData = createTimeUnitData;
@@ -0,0 +1,27 @@
1
+ import {
2
+ deriveNormalizedRows,
3
+ normalizeNormalizeTransform
4
+ } from "../../grammar/normalize.js";
5
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze([
8
+ "id", "source", "field", "as", "groupBy", "method",
9
+ "variance", "zeroDenominator", "baseline", "sortBy"
10
+ ]);
11
+
12
+ export const materializeNormalizedData = derivedMaterializer(
13
+ "materializeNormalizedData",
14
+ "Materialize one immutable grouped normalization dataset.",
15
+ "normalize",
16
+ deriveNormalizedRows
17
+ );
18
+
19
+ export const createNormalizedData = derivedCreator(
20
+ "createNormalizedData",
21
+ "Create a row-preserving grouped normalization dataset.",
22
+ OPTIONS,
23
+ "Normalized dataset id",
24
+ "Normalized source dataset id",
25
+ normalizeNormalizeTransform,
26
+ "materializeNormalizedData"
27
+ );