ggaction 0.0.12 → 0.0.13

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 (281) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/action-cards.schema.json +1 -1
  6. package/knowledge/extension-authoring.md +2 -1
  7. package/knowledge/intent-taxonomy.json +1 -1
  8. package/knowledge/mcp-resources.json +1 -1
  9. package/knowledge/task-resolver.js +1 -1
  10. package/package.json +4 -2
  11. package/src/actions/basic.js +2 -0
  12. package/src/actions/boxPlots/materialize.js +2 -1
  13. package/src/actions/canvas/actions.js +3 -1
  14. package/src/actions/canvas/fitting.js +211 -0
  15. package/src/actions/canvas/index.js +2 -0
  16. package/src/actions/categoryOrder/index.js +3 -2
  17. package/src/actions/charts/area.js +47 -0
  18. package/src/actions/charts/bar.js +11 -5
  19. package/src/actions/charts/beeswarm.js +70 -0
  20. package/src/actions/charts/density.js +44 -0
  21. package/src/actions/charts/ecdf.js +183 -0
  22. package/src/actions/charts/endpoints.js +451 -0
  23. package/src/actions/charts/heatmap.js +2 -2
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +71 -0
  26. package/src/actions/charts/index.js +37 -0
  27. package/src/actions/charts/interval-regression.js +169 -0
  28. package/src/actions/charts/line.js +5 -5
  29. package/src/actions/charts/parallel.js +1 -1
  30. package/src/actions/charts/pie.js +44 -0
  31. package/src/actions/charts/polar.js +123 -0
  32. package/src/actions/charts/radar.js +299 -0
  33. package/src/actions/charts/radial.js +42 -0
  34. package/src/actions/charts/raincloud.js +422 -0
  35. package/src/actions/charts/rug-strip.js +279 -0
  36. package/src/actions/charts/scatter.js +9 -3
  37. package/src/actions/charts/shared.js +69 -10
  38. package/src/actions/composition/actions.js +118 -1
  39. package/src/actions/data/bin.js +29 -0
  40. package/src/actions/data/computed.js +24 -0
  41. package/src/actions/data/derived.js +66 -0
  42. package/src/actions/data/ecdf.js +38 -0
  43. package/src/actions/data/filter.js +212 -28
  44. package/src/actions/data/fold.js +24 -0
  45. package/src/actions/data/index.js +24 -0
  46. package/src/actions/data/interval.js +2 -0
  47. package/src/actions/data/intervalEdit.js +86 -4
  48. package/src/actions/data/regression.js +5 -2
  49. package/src/actions/data/stack.js +26 -0
  50. package/src/actions/data/summary.js +26 -0
  51. package/src/actions/data/timeUnit.js +1 -1
  52. package/src/actions/encodings/appearance.js +32 -13
  53. package/src/actions/encodings/barWidth.js +8 -0
  54. package/src/actions/encodings/basic.js +2 -0
  55. package/src/actions/encodings/color/continuous.js +19 -59
  56. package/src/actions/encodings/color/index.js +28 -22
  57. package/src/actions/encodings/color/layout.js +3 -97
  58. package/src/actions/encodings/color/policy.js +5 -8
  59. package/src/actions/encodings/density/resolve.js +37 -5
  60. package/src/actions/encodings/density.js +120 -33
  61. package/src/actions/encodings/horizon/resolve.js +8 -4
  62. package/src/actions/encodings/horizon.js +18 -8
  63. package/src/actions/encodings/index.js +2 -0
  64. package/src/actions/encodings/offset.js +10 -16
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +10 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +14 -0
  69. package/src/actions/encodings/position/policies/area.js +4 -42
  70. package/src/actions/encodings/position/policies/bar.js +48 -13
  71. package/src/actions/encodings/position/resolve.js +67 -22
  72. package/src/actions/encodings/ranged.js +123 -97
  73. package/src/actions/encodings/remove.js +37 -32
  74. package/src/actions/encodings/ruleAppearance.js +23 -9
  75. package/src/actions/encodings/seriesLayout.js +104 -0
  76. package/src/actions/encodings/shared.js +25 -23
  77. package/src/actions/encodings/strokeDash.js +9 -6
  78. package/src/actions/encodings/temporal.js +7 -0
  79. package/src/actions/encodings/text.js +30 -15
  80. package/src/actions/errorBands/create.js +18 -5
  81. package/src/actions/errorBands/edit.js +341 -21
  82. package/src/actions/errorBars/create.js +21 -10
  83. package/src/actions/errorBars/edit.js +355 -19
  84. package/src/actions/facets/actions.js +410 -12
  85. package/src/actions/facets/derive.js +57 -8
  86. package/src/actions/gradientPlots/components.js +33 -11
  87. package/src/actions/gradientPlots/materialize.js +5 -7
  88. package/src/actions/guides/applicability.js +20 -17
  89. package/src/actions/guides/axes/axes.js +4 -3
  90. package/src/actions/guides/axes/axis.js +18 -18
  91. package/src/actions/guides/axes/components.js +43 -0
  92. package/src/actions/guides/axes/edit.js +16 -51
  93. package/src/actions/guides/axes/labels.js +129 -19
  94. package/src/actions/guides/axes/lines.js +7 -5
  95. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  96. package/src/actions/guides/axes/parallel/policy.js +137 -0
  97. package/src/actions/guides/axes/parallel/resolve.js +44 -23
  98. package/src/actions/guides/axes/parallel.js +93 -217
  99. package/src/actions/guides/axes/policy.js +21 -67
  100. package/src/actions/guides/axes/tickGroups.js +14 -8
  101. package/src/actions/guides/axes/ticks.js +5 -4
  102. package/src/actions/guides/axes/titles.js +21 -8
  103. package/src/actions/guides/basic.js +2 -0
  104. package/src/actions/guides/facade.js +106 -0
  105. package/src/actions/guides/facadeAxes.js +200 -0
  106. package/src/actions/guides/facadeLegend.js +63 -0
  107. package/src/actions/guides/grids/grid.js +37 -33
  108. package/src/actions/guides/grids/resolve.js +10 -2
  109. package/src/actions/guides/guides.js +35 -28
  110. package/src/actions/guides/legends/categorical/actions.js +193 -249
  111. package/src/actions/guides/legends/categorical/components.js +4 -5
  112. package/src/actions/guides/legends/categorical/layout.js +36 -349
  113. package/src/actions/guides/legends/categorical/options.js +30 -14
  114. package/src/actions/guides/legends/categorical/recipes.js +14 -0
  115. package/src/actions/guides/legends/categorical/resolve.js +21 -10
  116. package/src/actions/guides/legends/categorical/symbols.js +2 -3
  117. package/src/actions/guides/legends/continuous/common.js +69 -14
  118. package/src/actions/guides/legends/continuous/gradient.js +111 -83
  119. package/src/actions/guides/legends/continuous/index.js +6 -2
  120. package/src/actions/guides/legends/continuous/interval.js +71 -92
  121. package/src/actions/guides/legends/continuous/opacity.js +105 -84
  122. package/src/actions/guides/legends/creation.js +27 -0
  123. package/src/actions/guides/legends/edit.js +198 -66
  124. package/src/actions/guides/legends/focused.js +2 -2
  125. package/src/actions/guides/legends/lane.js +98 -86
  126. package/src/actions/guides/legends/lifecycle.js +132 -0
  127. package/src/actions/guides/legends/remove.js +31 -76
  128. package/src/actions/guides/legends/size.js +149 -105
  129. package/src/actions/guides/legends/strokeWidth.js +106 -64
  130. package/src/actions/guides/legends/target.js +12 -0
  131. package/src/actions/guides/legends/transition.js +43 -0
  132. package/src/actions/guides/polar/axes/facade.js +77 -62
  133. package/src/actions/guides/polar/axes/labels.js +5 -4
  134. package/src/actions/guides/polar/axes/lines.js +2 -1
  135. package/src/actions/guides/polar/axes/shared.js +6 -0
  136. package/src/actions/guides/polar/axes/ticks.js +5 -4
  137. package/src/actions/guides/polar/axes/titles.js +5 -4
  138. package/src/actions/guides/polar/grids.js +7 -7
  139. package/src/actions/guides/reuse.js +44 -0
  140. package/src/actions/guides/tickValues.js +19 -21
  141. package/src/actions/index.js +2 -0
  142. package/src/actions/intervals/resolve.js +35 -15
  143. package/src/actions/marks/arc/actions.js +1 -1
  144. package/src/actions/marks/area/actions.js +18 -4
  145. package/src/actions/marks/area/materialize.js +5 -2
  146. package/src/actions/marks/bar/materialize.js +11 -7
  147. package/src/actions/marks/index.js +5 -0
  148. package/src/actions/marks/line/actions.js +43 -108
  149. package/src/actions/marks/line/materialize.js +34 -52
  150. package/src/actions/marks/point/edit.js +11 -15
  151. package/src/actions/marks/point/index.js +1 -0
  152. package/src/actions/marks/point/jitter.js +3 -0
  153. package/src/actions/marks/point/materialize.js +106 -2
  154. package/src/actions/marks/point/packing.js +68 -0
  155. package/src/actions/marks/references.js +87 -0
  156. package/src/actions/marks/remove.js +17 -4
  157. package/src/actions/marks/rule/actions.js +34 -3
  158. package/src/actions/marks/rule/appearance.js +26 -0
  159. package/src/actions/marks/shared.js +12 -12
  160. package/src/actions/marks/text/actions.js +224 -6
  161. package/src/actions/marks/text/layout.js +2 -2
  162. package/src/actions/primitives/createGraphics.js +3 -1
  163. package/src/actions/primitives/semantic.js +1 -1
  164. package/src/actions/primitives/semanticAction.js +39 -2
  165. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  166. package/src/actions/primitives/semanticValidation/layer.js +18 -1
  167. package/src/actions/primitives/semanticValidation/scale.js +10 -1
  168. package/src/actions/regression/create.js +1 -1
  169. package/src/actions/regression/edit.js +19 -6
  170. package/src/actions/regression/resolve.js +1 -1
  171. package/src/actions/scales/consumers/common.js +20 -53
  172. package/src/actions/scales/consumers/families.js +27 -14
  173. package/src/actions/scales/consumers/index.js +23 -2
  174. package/src/actions/scales/consumers/seriesLayout.js +33 -2
  175. package/src/actions/scales/create.js +4 -4
  176. package/src/actions/scales/definitions.js +19 -3
  177. package/src/actions/scales/edit.js +46 -34
  178. package/src/actions/scales/editPolicy.js +21 -31
  179. package/src/actions/scales/materialize.js +10 -46
  180. package/src/actions/scales/patch.js +1 -1
  181. package/src/actions/scales/preview.js +39 -0
  182. package/src/actions/scales/quantitativeColor.js +9 -5
  183. package/src/actions/theme/actions.js +43 -0
  184. package/src/actions/theme/index.js +9 -0
  185. package/src/actions/theme/reconcile.js +882 -0
  186. package/src/actions/titles/actions.js +11 -10
  187. package/src/actions/titles/resolve.js +1 -40
  188. package/src/actions/violinPlots/create.js +86 -42
  189. package/src/actions/violinPlots/edit.js +191 -0
  190. package/src/actions/violinPlots/index.js +2 -0
  191. package/src/core/ChartProgram.js +7 -0
  192. package/src/core/action.js +22 -2
  193. package/src/core/compositionState.js +136 -8
  194. package/src/grammar/aggregate.js +66 -23
  195. package/src/grammar/arcs.js +58 -1
  196. package/src/grammar/areaEndpoints.js +46 -0
  197. package/src/grammar/areaSeries.js +87 -41
  198. package/src/grammar/bars/aggregate.js +33 -12
  199. package/src/grammar/bars/policy.js +9 -7
  200. package/src/grammar/bin.js +181 -0
  201. package/src/grammar/categoryOrder.js +34 -0
  202. package/src/grammar/computed.js +150 -0
  203. package/src/grammar/ecdf.js +198 -0
  204. package/src/grammar/facets/index.js +117 -6
  205. package/src/grammar/fold.js +123 -0
  206. package/src/grammar/horizon.js +6 -4
  207. package/src/grammar/interval.js +37 -20
  208. package/src/grammar/lineSeries.js +20 -63
  209. package/src/grammar/markFilter.js +22 -4
  210. package/src/grammar/markLabels.js +93 -0
  211. package/src/grammar/parallelAxisTitles.js +16 -0
  212. package/src/grammar/pathSeries.js +91 -0
  213. package/src/grammar/pointPacking.js +183 -0
  214. package/src/grammar/positionDatum.js +12 -0
  215. package/src/grammar/rects.js +9 -1
  216. package/src/grammar/regression/derive.js +4 -0
  217. package/src/grammar/regression/index.js +7 -2
  218. package/src/grammar/regression/models.js +22 -7
  219. package/src/grammar/regression/parameters.js +40 -13
  220. package/src/grammar/rotation.js +29 -0
  221. package/src/grammar/rules.js +7 -21
  222. package/src/grammar/scales/color.js +23 -1
  223. package/src/grammar/scales/colorConsumers.js +38 -0
  224. package/src/grammar/scales/definition.js +15 -1
  225. package/src/grammar/scales/discretized.js +11 -5
  226. package/src/grammar/scales/fields.js +38 -8
  227. package/src/grammar/scales/mapping.js +2 -0
  228. package/src/grammar/scales/radial.js +55 -0
  229. package/src/grammar/scales/types.js +1 -0
  230. package/src/grammar/schemas/semanticPath.js +17 -3
  231. package/src/grammar/stack.js +170 -0
  232. package/src/grammar/statistics/confidenceInterval.js +101 -0
  233. package/src/grammar/summary.js +183 -0
  234. package/src/grammar/text.js +22 -16
  235. package/src/grammar/timeUnit.js +21 -11
  236. package/src/grammar/transformTopology.js +6 -0
  237. package/src/grammar/transforms.js +36 -0
  238. package/src/grammar/valueFormat.js +112 -0
  239. package/src/grammar/window.js +13 -7
  240. package/src/layout/facets.js +41 -7
  241. package/src/layout/guideCollisions.js +12 -0
  242. package/src/layout/legend.js +2 -10
  243. package/src/layout/legendItems.js +102 -0
  244. package/src/layout/legendLane.js +100 -11
  245. package/src/materialization/bars/aggregate.js +38 -152
  246. package/src/materialization/bars/histogram.js +24 -0
  247. package/src/materialization/categorySlotOffset.js +40 -0
  248. package/src/materialization/compositionSnapshot.js +1 -1
  249. package/src/materialization/dependencies.js +57 -16
  250. package/src/materialization/encodings.js +1 -1
  251. package/src/materialization/facets.js +8 -1
  252. package/src/materialization/guides/layout.js +57 -0
  253. package/src/materialization/guides/resources.js +2 -2
  254. package/src/materialization/legends.js +10 -1
  255. package/src/materialization/marks/capabilities.js +20 -5
  256. package/src/materialization/marks/grouping.js +52 -0
  257. package/src/materialization/marks/index.js +58 -23
  258. package/src/materialization/marks/policies.js +2 -0
  259. package/src/materialization/rect.js +33 -33
  260. package/src/materialization/rowEncoding.js +13 -13
  261. package/src/materialization/scaleGuideDependencies.js +8 -5
  262. package/src/materialization/scales/map.js +1 -0
  263. package/src/materialization/scales/policies/arc.js +42 -0
  264. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  265. package/src/materialization/scales/policies/series.js +2 -1
  266. package/src/materialization/scales/resolve.js +46 -14
  267. package/src/materialization/selection/compatibility.js +11 -0
  268. package/src/materialization/selection/items/bar.js +12 -68
  269. package/src/materialization/selection/items/common.js +8 -2
  270. package/src/materialization/selection/items/path.js +3 -13
  271. package/src/materialization/selection/items/rect.js +7 -4
  272. package/src/materialization/text.js +64 -5
  273. package/src/mcp/adapter.js +2 -2
  274. package/src/selectors/datasets.js +13 -0
  275. package/src/selectors/layers.js +3 -2
  276. package/src/selectors/scales.js +6 -0
  277. package/src/theme/defaults.js +39 -0
  278. package/types/basic.d.ts +11 -2
  279. package/types/index.d.ts +126 -0
  280. package/types/program.d.ts +1320 -135
  281. package/src/materialization/bars/grouped.js +0 -144
@@ -4,10 +4,22 @@ import { validateKeys } from "../../core/validation.js";
4
4
  import { hasDataset } from "../../selectors/index.js";
5
5
  import { findDataset } from "../../selectors/datasets.js";
6
6
  import { requireLayer } from "../../selectors/layers.js";
7
+ import { applyLayerDataRematerialization } from
8
+ "../../materialization/dependencies.js";
7
9
 
8
10
  const OPTIONS = Object.freeze(["id", "source", "transform"]);
9
11
  const RELEASE_OPTIONS = Object.freeze(["id"]);
10
12
  const REBIND_OPTIONS = Object.freeze(["id", "data"]);
13
+ const BIND_OPTIONS = Object.freeze(["target", "data"]);
14
+ const OWNED_MARK_ROLES = Object.freeze([
15
+ "boxPlot",
16
+ "errorBand",
17
+ "errorBandBoundary",
18
+ "errorBar",
19
+ "gradientPlot",
20
+ "ecdfPlot",
21
+ "regression"
22
+ ]);
11
23
 
12
24
  export const createDerivedData = action(
13
25
  { op: "createDerivedData", description: "Create an immutable derived dataset definition." },
@@ -72,3 +84,57 @@ export const rebindLayerData = action(
72
84
  });
73
85
  }
74
86
  );
87
+
88
+ function assertIndependentMark(program, layer) {
89
+ const config = program.markConfigs[layer.id] ?? {};
90
+ const role = OWNED_MARK_ROLES.find(key => config[key] !== undefined);
91
+ if (role !== undefined) {
92
+ throw new Error(
93
+ `Mark "${layer.id}" is owned by its ${role} lifecycle; use that ` +
94
+ "resource's edit action to change data roles."
95
+ );
96
+ }
97
+ const source = findDataset(program, layer.data);
98
+ const transform = source?.transform?.[0]?.type;
99
+ if (["density", "ecdf", "horizon", "markFilter"].includes(transform)) {
100
+ throw new Error(
101
+ `Mark "${layer.id}" consumes owned ${transform} data; use its ` +
102
+ "data or filter lifecycle action to change the source."
103
+ );
104
+ }
105
+ }
106
+
107
+ function applyMarkDataBinding(program, target, data) {
108
+ const rebound = program.rebindLayerData({ id: target, data });
109
+ return applyLayerDataRematerialization(rebound, target);
110
+ }
111
+
112
+ export const bindMarkData = action(
113
+ {
114
+ op: "bindMarkData",
115
+ description: "Atomically bind one independent mark to materialized data."
116
+ },
117
+ function (args = {}) {
118
+ validateKeys(args, BIND_OPTIONS, "bindMarkData");
119
+ const target = validateUserId(args.target, "Mark target id");
120
+ const data = validateUserId(args.data, "Mark dataset id");
121
+ const layer = requireLayer(this, target);
122
+ const dataset = findDataset(this, data);
123
+ if (dataset === undefined) {
124
+ throw new Error(`Mark dataset "${data}" does not exist.`);
125
+ }
126
+ if (!Array.isArray(dataset.values)) {
127
+ throw new Error(`Mark dataset "${data}" requires materialized values.`);
128
+ }
129
+ if (layer.data === data) {
130
+ throw new Error(`Mark "${target}" already uses dataset "${data}".`);
131
+ }
132
+ assertIndependentMark(this, layer);
133
+
134
+ // Finish the entire dependency plan on an immutable speculative branch.
135
+ // A field, type, coordinate, scale, guide, label, selection, or highlight
136
+ // incompatibility therefore rejects before this action returns any state.
137
+ applyMarkDataBinding(this, target, data);
138
+ return applyMarkDataBinding(this, target, data);
139
+ }
140
+ );
@@ -0,0 +1,38 @@
1
+ import {
2
+ deriveECDF,
3
+ normalizeECDFTransform,
4
+ resolveECDFTransform
5
+ } from "../../grammar/ecdf.js";
6
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
7
+
8
+ const OPTIONS = Object.freeze([
9
+ "id", "source", "field", "groupBy", "weight", "missing", "as"
10
+ ]);
11
+
12
+ export const materializeECDFData = derivedMaterializer(
13
+ "materializeECDFData",
14
+ "Materialize one empirical cumulative distribution dataset.",
15
+ "ecdf",
16
+ deriveECDF,
17
+ resolveECDFTransform
18
+ );
19
+
20
+ export const createECDFData = derivedCreator(
21
+ "createECDFData",
22
+ "Create immutable empirical cumulative distribution values.",
23
+ OPTIONS,
24
+ "ECDF dataset id",
25
+ "ECDF source dataset id",
26
+ (args, id) => normalizeECDFTransform({
27
+ field: args.field,
28
+ groupBy: args.groupBy,
29
+ weight: args.weight,
30
+ missing: args.missing,
31
+ as: args.as ?? {
32
+ value: `__${id}_value`,
33
+ cumulative: `__${id}_cumulative`,
34
+ probability: `__${id}_probability`
35
+ }
36
+ }),
37
+ "materializeECDFData"
38
+ );
@@ -1,22 +1,27 @@
1
1
  import { action } from "../../core/action.js";
2
2
  import { validateUserId } from "../../core/identifiers.js";
3
3
  import { validateKeys } from "../../core/validation.js";
4
+ import { withPreviewDatasetValues } from
5
+ "../primitives/semanticAction.js";
4
6
  import {
5
7
  deriveFilteredRows,
6
8
  normalizeFilterTransform
7
9
  } from "../../grammar/filter.js";
8
10
  import {
9
11
  deriveMarkFilteredRows,
12
+ markFilterSelectors,
10
13
  normalizeMarkFilterTransform
11
14
  } from "../../grammar/markFilter.js";
12
15
  import { resolveMarkFilterSelection } from "../../materialization/selection/filter.js";
13
16
  import { findSelectionPolicy } from
14
17
  "../../materialization/selection/policies/index.js";
15
18
  import {
16
- applyLayerDataRematerialization
19
+ applyLayerDataRematerialization,
20
+ applyLayerEmptyDataView
17
21
  } from "../../materialization/dependencies.js";
18
22
  import {
19
23
  hasDataset,
24
+ findDataset,
20
25
  requireLayer,
21
26
  resolveEligibleLayer
22
27
  } from "../../selectors/index.js";
@@ -29,7 +34,9 @@ const MARK_SELECTOR_KEYS = Object.freeze([
29
34
  "grain", "field", "channel", "property", "op", "value", "values",
30
35
  "min", "max", "inclusive", "count", "groupBy", "ties"
31
36
  ]);
32
- const MARK_OPTIONS = Object.freeze(["target", ...MARK_SELECTOR_KEYS]);
37
+ const MARK_OPTIONS = Object.freeze(["target", "mode", ...MARK_SELECTOR_KEYS]);
38
+ const REMOVE_MARK_FILTER_OPTIONS = Object.freeze(["target"]);
39
+ const EMPTY_MARK_OPTIONS = Object.freeze(["id"]);
33
40
 
34
41
  function retainedHistogramBoundaries(layer, items) {
35
42
  if (layer.mark?.type !== "bar" || layer.encoding?.x?.bin === undefined) {
@@ -96,15 +103,168 @@ export const materializeMarkFilteredData = action(
96
103
  `Mark filter target "${transform.target}" must still use source dataset "${dataset.source}".`
97
104
  );
98
105
  }
99
- const resolved = resolveMarkFilterSelection(
100
- this,
101
- transform.target,
102
- transform.selector
103
- );
104
- return this.editSemantic({
106
+ let preview = this;
107
+ let currentRows = source.values;
108
+ const selectors = markFilterSelectors(transform);
109
+ for (const [index, selector] of selectors.entries()) {
110
+ const resolved = resolveMarkFilterSelection(
111
+ preview,
112
+ transform.target,
113
+ selector
114
+ );
115
+ currentRows = deriveMarkFilteredRows(
116
+ currentRows,
117
+ resolved.items,
118
+ resolved.keys
119
+ );
120
+ if (index < selectors.length - 1) {
121
+ preview = withPreviewDatasetValues(preview, {
122
+ id,
123
+ values: currentRows,
124
+ target: transform.target
125
+ });
126
+ preview = currentRows.length === 0
127
+ ? applyLayerEmptyDataView(preview, transform.target)
128
+ : applyLayerDataRematerialization(preview, transform.target);
129
+ currentRows = findDataset(preview, id).values;
130
+ }
131
+ }
132
+ if (selectors.length > 1) {
133
+ preview = withPreviewDatasetValues(preview, {
134
+ id,
135
+ omitValues: true
136
+ });
137
+ }
138
+ let next = preview.editSemantic({
105
139
  property: `dataset[${id}].values`,
106
- value: deriveMarkFilteredRows(source.values, resolved.items, resolved.keys)
140
+ value: currentRows
141
+ });
142
+ if (requireLayer(next, transform.target).data !== id) {
143
+ next = next.editSemantic({
144
+ property: `layer[${transform.target}].data`,
145
+ value: id
146
+ });
147
+ }
148
+ next = next._withMarkConfig(transform.target, {
149
+ ...next.markConfigs[transform.target],
150
+ markFilter: {
151
+ ...next.markConfigs[transform.target].markFilter,
152
+ empty: currentRows.length === 0
153
+ }
154
+ });
155
+ return currentRows.length === 0
156
+ ? next.materializeEmptyMark({ id: transform.target })
157
+ : applyLayerDataRematerialization(next, transform.target);
158
+ }
159
+ );
160
+
161
+ export const materializeEmptyMark = action(
162
+ {
163
+ op: "materializeEmptyMark",
164
+ description: "Clear one active empty-filter mark without changing its domains."
165
+ },
166
+ function (args = {}) {
167
+ validateKeys(args, EMPTY_MARK_OPTIONS, "materializeEmptyMark");
168
+ const id = validateUserId(args.id, "Empty mark id");
169
+ const layer = requireLayer(this, id);
170
+ const owner = this.markConfigs[id]?.markFilter;
171
+ const dataset = findDataset(this, layer.data);
172
+ if (owner?.empty !== true || dataset?.values?.length !== 0) {
173
+ throw new Error(`Mark "${id}" does not own an active empty filter.`);
174
+ }
175
+ return applyLayerEmptyDataView(this, id);
176
+ }
177
+ );
178
+
179
+ function activeMarkFilter(program, layer) {
180
+ const configured = program.markConfigs[layer.id]?.markFilter;
181
+ const dataset = findDataset(program, layer.data);
182
+ const transform = dataset?.transform?.[0];
183
+ if (
184
+ dataset?.source === undefined ||
185
+ transform?.type !== "markFilter" ||
186
+ transform.target !== layer.id
187
+ ) {
188
+ return undefined;
189
+ }
190
+ return {
191
+ dataset: dataset.id,
192
+ source: dataset.source,
193
+ selectors: markFilterSelectors(transform),
194
+ ...(configured?.originalBin === undefined
195
+ ? {}
196
+ : { originalBin: configured.originalBin })
197
+ };
198
+ }
199
+
200
+ function withoutMarkFilterConfig(program, id) {
201
+ const config = program.markConfigs[id] ?? {};
202
+ const { markFilter: unused, ...remaining } = config;
203
+ void unused;
204
+ return program._withMarkConfig(id, remaining);
205
+ }
206
+
207
+ function restoreHistogramBin(program, layer, originalBin) {
208
+ let next = program;
209
+ const current = layer.encoding?.x?.bin ?? {};
210
+ for (const property of ["maxBins", "step", "boundaries"]) {
211
+ if (Object.hasOwn(originalBin, property)) {
212
+ next = next.editSemantic({
213
+ property: `layer[${layer.id}].encoding.x.bin.${property}`,
214
+ value: originalBin[property]
215
+ });
216
+ } else if (Object.hasOwn(current, property)) {
217
+ next = next.editSemantic({
218
+ property: `layer[${layer.id}].encoding.x.bin.${property}`,
219
+ remove: true
220
+ });
221
+ }
222
+ }
223
+ return next;
224
+ }
225
+
226
+ function removeActiveMarkFilter(program, layer, owner) {
227
+ let next = program.editSemantic({
228
+ property: `layer[${layer.id}].data`,
229
+ value: owner.source
230
+ });
231
+ if (owner.originalBin !== undefined) {
232
+ next = restoreHistogramBin(next, requireLayer(next, layer.id), owner.originalBin);
233
+ }
234
+ next = withoutMarkFilterConfig(next, layer.id)
235
+ .releaseDerivedData({ id: owner.dataset })
236
+ ._withContext({ currentData: owner.source });
237
+ return applyLayerDataRematerialization(next, layer.id);
238
+ }
239
+
240
+ function nextMarkFilterDatasetId(program, layerId) {
241
+ const base = `${layerId}FilteredData`;
242
+ if (!hasDataset(program, base)) return base;
243
+ let revision = 2;
244
+ while (hasDataset(program, `${base}${revision}`)) revision += 1;
245
+ return `${base}${revision}`;
246
+ }
247
+
248
+ function sameSelector(left, right) {
249
+ return JSON.stringify(left) === JSON.stringify(right);
250
+ }
251
+
252
+ export const removeMarkFilter = action(
253
+ {
254
+ op: "removeMarkFilter",
255
+ description: "Remove one active final-item filter and restore its source."
256
+ },
257
+ function (args = {}) {
258
+ validateKeys(args, REMOVE_MARK_FILTER_OPTIONS, "removeMarkFilter");
259
+ const target = args.target === undefined
260
+ ? undefined
261
+ : validateUserId(args.target, "Mark filter target id");
262
+ const layer = resolveEligibleLayer(this, {
263
+ target,
264
+ label: "mark filter",
265
+ predicate: candidate => activeMarkFilter(this, candidate) !== undefined
107
266
  });
267
+ return removeActiveMarkFilter(this, layer, activeMarkFilter(this, layer));
108
268
  }
109
269
  );
110
270
 
@@ -115,6 +275,9 @@ export const filterMarks = action(
115
275
  },
116
276
  function (args = {}) {
117
277
  validateKeys(args, MARK_OPTIONS, "filterMarks");
278
+ if (args.mode !== undefined && !["replace", "compose"].includes(args.mode)) {
279
+ throw new Error(`Unknown mark filter mode "${args.mode}".`);
280
+ }
118
281
  const target = args.target === undefined
119
282
  ? undefined
120
283
  : validateUserId(args.target, "Filter mark target id");
@@ -130,46 +293,67 @@ export const filterMarks = action(
130
293
  `filterMarks does not support composite mark "${layer.id}"; filter its source dataset first.`
131
294
  );
132
295
  }
133
- const derivedId = `${layer.id}FilteredData`;
134
- if (hasDataset(this, derivedId)) {
135
- throw new Error(`Dataset "${derivedId}" already exists.`);
136
- }
296
+ const owner = activeMarkFilter(this, layer);
137
297
  const selector = Object.fromEntries(
138
298
  MARK_SELECTOR_KEYS.flatMap(key =>
139
299
  Object.hasOwn(args, key) ? [[key, args[key]]] : []
140
300
  )
141
301
  );
142
302
  const resolved = resolveMarkFilterSelection(this, layer.id, selector);
143
- if (resolved.keys.length === 0) {
144
- throw new Error("filterMarks requires at least one matching mark item.");
303
+ const previousSelector = owner?.selectors.at(-1);
304
+ if (previousSelector !== undefined && sameSelector(previousSelector, resolved.selector)) {
305
+ return this;
306
+ }
307
+ if (owner !== undefined && args.mode === undefined) {
308
+ throw new Error(
309
+ "Repeated filterMarks requires mode \"replace\" or \"compose\"."
310
+ );
145
311
  }
146
- const transform = normalizeMarkFilterTransform(layer.id, resolved.selector);
147
- const boundaries = retainedHistogramBoundaries(layer, resolved.items);
148
- let next = this
312
+ const selectors = owner === undefined || args.mode === "replace"
313
+ ? [resolved.selector]
314
+ : [...owner.selectors, resolved.selector];
315
+ let next = owner === undefined
316
+ ? this
317
+ : removeActiveMarkFilter(this, layer, owner);
318
+ const baseLayer = requireLayer(next, layer.id);
319
+ const first = resolveMarkFilterSelection(next, layer.id, selectors[0]);
320
+ const transform = normalizeMarkFilterTransform(layer.id, selectors);
321
+ const boundaries = retainedHistogramBoundaries(baseLayer, first.items);
322
+ const originalBin = baseLayer.mark?.type === "bar" &&
323
+ baseLayer.encoding?.x?.bin !== undefined
324
+ ? baseLayer.encoding.x.bin
325
+ : undefined;
326
+ const derivedId = nextMarkFilterDatasetId(next, layer.id);
327
+ next = next
149
328
  .createDerivedData({
150
329
  id: derivedId,
151
- source: layer.data,
330
+ source: baseLayer.data,
152
331
  transform: [transform]
153
- })
154
- .materializeMarkFilteredData({ id: derivedId });
332
+ });
155
333
  if (boundaries !== undefined) {
156
334
  for (const property of ["maxBins", "step"]) {
157
- if (Object.hasOwn(layer.encoding.x.bin, property)) {
335
+ if (Object.hasOwn(baseLayer.encoding.x.bin, property)) {
158
336
  next = next.editSemantic({
159
- property: `layer[${layer.id}].encoding.x.bin.${property}`,
337
+ property: `layer[${baseLayer.id}].encoding.x.bin.${property}`,
160
338
  remove: true
161
339
  });
162
340
  }
163
341
  }
164
342
  next = next.editSemantic({
165
- property: `layer[${layer.id}].encoding.x.bin.boundaries`,
343
+ property: `layer[${baseLayer.id}].encoding.x.bin.boundaries`,
166
344
  value: boundaries
167
345
  });
168
346
  }
169
- next = next.editSemantic({
170
- property: `layer[${layer.id}].data`,
171
- value: derivedId
347
+ next = next._withMarkConfig(baseLayer.id, {
348
+ ...next.markConfigs[baseLayer.id],
349
+ markFilter: {
350
+ dataset: derivedId,
351
+ source: baseLayer.data,
352
+ selectors,
353
+ empty: false,
354
+ ...(originalBin === undefined ? {} : { originalBin })
355
+ }
172
356
  });
173
- return applyLayerDataRematerialization(next, layer.id);
357
+ return next.materializeMarkFilteredData({ id: derivedId });
174
358
  }
175
359
  );
@@ -0,0 +1,24 @@
1
+ import {
2
+ deriveFoldRows,
3
+ normalizeFoldTransform
4
+ } from "../../grammar/fold.js";
5
+ import { derivedCreator, derivedMaterializer } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze(["id", "source", "fields", "as"]);
8
+
9
+ export const materializeFoldData = derivedMaterializer(
10
+ "materializeFoldData",
11
+ "Materialize one immutable wide-to-long dataset.",
12
+ "fold",
13
+ deriveFoldRows
14
+ );
15
+
16
+ export const createFoldData = derivedCreator(
17
+ "createFoldData",
18
+ "Create reusable key/value rows from selected wide fields.",
19
+ OPTIONS,
20
+ "Fold dataset id",
21
+ "Fold source dataset id",
22
+ normalizeFoldTransform,
23
+ "materializeFoldData"
24
+ );
@@ -4,12 +4,15 @@ import {
4
4
  } from "./timeUnit.js";
5
5
  import {
6
6
  createDerivedData,
7
+ bindMarkData,
7
8
  rebindLayerData,
8
9
  releaseDerivedData
9
10
  } from "./derived.js";
10
11
  import {
11
12
  filterData,
12
13
  filterMarks,
14
+ materializeEmptyMark,
15
+ removeMarkFilter,
13
16
  materializeFilteredData,
14
17
  materializeMarkFilteredData
15
18
  } from "./filter.js";
@@ -24,6 +27,7 @@ import {
24
27
  materializeDensityData
25
28
  } from "./density.js";
26
29
  import { createIntervalData, materializeIntervalData } from "./interval.js";
30
+ import { createECDFData, materializeECDFData } from "./ecdf.js";
27
31
  import { createHorizonData, materializeHorizonData } from "./horizon.js";
28
32
  import {
29
33
  createBin2DData,
@@ -32,11 +36,17 @@ import {
32
36
  } from "./bin2d.js";
33
37
  import { createData } from "./create.js";
34
38
  import { createWindowData, materializeWindowData } from "./window.js";
39
+ import { createSummaryData, materializeSummaryData } from "./summary.js";
40
+ import { createBinData, materializeBinData } from "./bin.js";
41
+ import { createFoldData, materializeFoldData } from "./fold.js";
42
+ import { createComputedData, materializeComputedData } from "./computed.js";
43
+ import { createStackData, materializeStackData } from "./stack.js";
35
44
  import { createBoxSummaryData, createBoxOutlierData, materializeBoxSummaryData, materializeBoxOutlierData } from "./box.js";
36
45
 
37
46
  export function registerDataActions(ProgramClass) {
38
47
  ProgramClass.prototype.createData = createData;
39
48
  ProgramClass.prototype.createDerivedData = createDerivedData;
49
+ ProgramClass.prototype.bindMarkData = bindMarkData;
40
50
  ProgramClass.prototype.releaseDerivedData = releaseDerivedData;
41
51
  ProgramClass.prototype.rebindLayerData = rebindLayerData;
42
52
  ProgramClass.prototype.createDensityData = createDensityData;
@@ -46,6 +56,8 @@ export function registerDataActions(ProgramClass) {
46
56
  ProgramClass.prototype.materializeMarkFilteredData = materializeMarkFilteredData;
47
57
  ProgramClass.prototype.filterData = filterData;
48
58
  ProgramClass.prototype.filterMarks = filterMarks;
59
+ ProgramClass.prototype.materializeEmptyMark = materializeEmptyMark;
60
+ ProgramClass.prototype.removeMarkFilter = removeMarkFilter;
49
61
  ProgramClass.prototype.materializeRegressionData = materializeRegressionData;
50
62
  ProgramClass.prototype.materializeDensityData = materializeDensityData;
51
63
  ProgramClass.prototype.createGradientProfileData = createGradientProfileData;
@@ -54,10 +66,22 @@ export function registerDataActions(ProgramClass) {
54
66
  ProgramClass.prototype.createRegressionData = createRegressionData;
55
67
  ProgramClass.prototype.materializeIntervalData = materializeIntervalData;
56
68
  ProgramClass.prototype.createIntervalData = createIntervalData;
69
+ ProgramClass.prototype.createECDFData = createECDFData;
70
+ ProgramClass.prototype.materializeECDFData = materializeECDFData;
57
71
  ProgramClass.prototype.createHorizonData = createHorizonData;
58
72
  ProgramClass.prototype.materializeHorizonData = materializeHorizonData;
59
73
  ProgramClass.prototype.createWindowData = createWindowData;
60
74
  ProgramClass.prototype.materializeWindowData = materializeWindowData;
75
+ ProgramClass.prototype.createSummaryData = createSummaryData;
76
+ ProgramClass.prototype.materializeSummaryData = materializeSummaryData;
77
+ ProgramClass.prototype.createBinData = createBinData;
78
+ ProgramClass.prototype.materializeBinData = materializeBinData;
79
+ ProgramClass.prototype.createFoldData = createFoldData;
80
+ ProgramClass.prototype.materializeFoldData = materializeFoldData;
81
+ ProgramClass.prototype.createComputedData = createComputedData;
82
+ ProgramClass.prototype.materializeComputedData = materializeComputedData;
83
+ ProgramClass.prototype.createStackData = createStackData;
84
+ ProgramClass.prototype.materializeStackData = materializeStackData;
61
85
  ProgramClass.prototype.createTimeUnitData = createTimeUnitData;
62
86
  ProgramClass.prototype.materializeTimeUnitData = materializeTimeUnitData;
63
87
  ProgramClass.prototype.createBin2DData = createBin2DData;
@@ -11,6 +11,7 @@ const OPTIONS = Object.freeze([
11
11
  "groupBy",
12
12
  "center",
13
13
  "extent",
14
+ "method",
14
15
  "level",
15
16
  "as"
16
17
  ]);
@@ -39,6 +40,7 @@ export const createIntervalData = derivedCreator(
39
40
  groupBy: args.groupBy,
40
41
  center: args.center,
41
42
  extent: args.extent,
43
+ method: args.method,
42
44
  level: args.level,
43
45
  as
44
46
  });
@@ -7,7 +7,7 @@ import { planDerivedDataRevision } from
7
7
  import { findDataset } from "../../selectors/datasets.js";
8
8
  import { resolveEligibleLayer } from "../../selectors/layers.js";
9
9
 
10
- const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "level"]);
10
+ const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "method", "level"]);
11
11
 
12
12
  export function ownOptions(value, options) {
13
13
  return Object.fromEntries(
@@ -42,6 +42,7 @@ export function createResolvedIntervalData(program, resolved) {
42
42
  groupBy: resolved.groupBy,
43
43
  center: resolved.interval.center,
44
44
  extent: resolved.interval.extent,
45
+ method: resolved.interval.method,
45
46
  level: resolved.interval.level,
46
47
  as: resolved.fields
47
48
  });
@@ -49,7 +50,9 @@ export function createResolvedIntervalData(program, resolved) {
49
50
 
50
51
  export function applyIntervalRevision(program, interval) {
51
52
  if (!interval.changed) return program;
52
- let next = program.createIntervalData(interval.dataArgs);
53
+ let next = interval.dataArgs === undefined
54
+ ? program
55
+ : program.createIntervalData(interval.dataArgs);
53
56
  for (const rebind of interval.revision.rebinds) {
54
57
  next = next.rebindLayerData(rebind);
55
58
  }
@@ -57,11 +60,84 @@ export function applyIntervalRevision(program, interval) {
57
60
  }
58
61
 
59
62
  export function releaseIntervalRevision(program, interval) {
60
- return interval.changed
63
+ return interval.changed && interval.revision.release !== undefined
61
64
  ? program.releaseDerivedData(interval.revision.release)
62
65
  : program;
63
66
  }
64
67
 
68
+ export function findIntervalTransform(program, data) {
69
+ const dataset = findDataset(program, data);
70
+ return dataset?.transform?.length === 1 &&
71
+ dataset.transform[0].type === "interval"
72
+ ? dataset.transform[0]
73
+ : undefined;
74
+ }
75
+
76
+ export function collectIntervalConsumers(program, owners) {
77
+ const consumers = new Set(owners);
78
+ let changed = true;
79
+ while (changed) {
80
+ changed = false;
81
+ for (const layer of program.semanticSpec.layers ?? []) {
82
+ if (
83
+ layer.source !== undefined &&
84
+ consumers.has(layer.source) &&
85
+ !consumers.has(layer.id)
86
+ ) {
87
+ consumers.add(layer.id);
88
+ changed = true;
89
+ }
90
+ }
91
+ }
92
+ return [...consumers];
93
+ }
94
+
95
+ export function planIntervalRoleData(program, {
96
+ owner,
97
+ currentData,
98
+ candidate,
99
+ consumers
100
+ }) {
101
+ const previousTransform = findIntervalTransform(program, currentData);
102
+ if (candidate.interval.mode === "statistical") {
103
+ const revision = planDerivedDataRevision(program, {
104
+ owner,
105
+ role: "IntervalData",
106
+ ...(previousTransform === undefined ? {} : { previous: currentData }),
107
+ consumers
108
+ });
109
+ return {
110
+ changed: true,
111
+ revision,
112
+ dataId: revision.id,
113
+ dataArgs: {
114
+ id: revision.id,
115
+ source: candidate.source,
116
+ field: candidate.interval.field,
117
+ groupBy: candidate.groupBy,
118
+ center: candidate.interval.center,
119
+ extent: candidate.interval.extent,
120
+ method: candidate.interval.method,
121
+ level: candidate.interval.level,
122
+ as: candidate.fields
123
+ }
124
+ };
125
+ }
126
+ const changed = currentData !== candidate.source;
127
+ return {
128
+ changed,
129
+ dataId: candidate.source,
130
+ revision: {
131
+ rebinds: changed
132
+ ? consumers.map(id => ({ id, data: candidate.source }))
133
+ : [],
134
+ ...(previousTransform === undefined
135
+ ? {}
136
+ : { release: { id: currentData } })
137
+ }
138
+ };
139
+ }
140
+
65
141
  export function planIntervalEdit(program, {
66
142
  owner,
67
143
  data,
@@ -75,7 +151,7 @@ export function planIntervalEdit(program, {
75
151
  validateKeys(statistics, STATISTICS_OPTIONS, `${operation} statistics`);
76
152
  if (!STATISTICS_OPTIONS.some(key => Object.hasOwn(statistics, key))) {
77
153
  throw new Error(
78
- `${operation} statistics requires center, extent, or level.`
154
+ `${operation} statistics requires center, extent, method, or level.`
79
155
  );
80
156
  }
81
157
  const previous = findDataset(program, data);
@@ -95,6 +171,11 @@ export function planIntervalEdit(program, {
95
171
  ? statistics.extent
96
172
  : transform.extent;
97
173
  const raw = { center, extent };
174
+ if (Object.hasOwn(statistics, "method")) {
175
+ raw.method = statistics.method;
176
+ } else if (extent === "ci" && transform.extent === "ci") {
177
+ raw.method = transform.method;
178
+ }
98
179
  if (Object.hasOwn(statistics, "level")) {
99
180
  raw.level = statistics.level;
100
181
  } else if (extent === "ci" && transform.extent === "ci") {
@@ -104,6 +185,7 @@ export function planIntervalEdit(program, {
104
185
  const current = {
105
186
  center: transform.center,
106
187
  extent: transform.extent,
188
+ ...(transform.method === undefined ? {} : { method: transform.method }),
107
189
  ...(transform.level === undefined ? {} : { level: transform.level })
108
190
  };
109
191
  const changed = JSON.stringify(parameters) !== JSON.stringify(current);