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
@@ -11,12 +11,22 @@ import {
11
11
  import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
12
12
  import { normalizeStrokeDashPattern } from "../../grammar/scales/index.js";
13
13
  import { findLayer } from "../../selectors/layers.js";
14
+ import { findSemanticScale } from "../../selectors/scales.js";
14
15
  import { removeOwnedMark } from "../marks/remove.js";
15
16
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
17
+ import { resolveIntervalComposite } from "../intervals/resolve.js";
18
+ import {
19
+ resolveDistributionScalePlan,
20
+ setCartesianPosition,
21
+ updateDistributionPositions
22
+ } from "../distributions/revision.js";
16
23
  import {
17
24
  applyIntervalRevision,
25
+ collectIntervalConsumers,
26
+ findIntervalTransform,
18
27
  ownOptions,
19
28
  planIntervalEdit,
29
+ planIntervalRoleData,
20
30
  releaseIntervalRevision,
21
31
  resolveIntervalOwner
22
32
  } from "../data/intervalEdit.js";
@@ -43,8 +53,26 @@ export function resolveBoundaryAppearance(value, { defaults, operation }) {
43
53
  }
44
54
 
45
55
  const EDIT_OPTIONS = Object.freeze([
46
- "target", "fill", "opacity", "curve", "statistics", "boundaries"
56
+ "target", "data", "x", "y", "groupBy", "fill", "opacity", "curve",
57
+ "statistics", "boundaries"
47
58
  ]);
59
+ const STATISTICS_OPTIONS = Object.freeze(["center", "extent", "method", "level"]);
60
+ const EDIT_POLICY = Object.freeze({
61
+ operation: "editErrorBand",
62
+ resourceLabel: "error-band",
63
+ defaultId: "errorBand",
64
+ ownerLabel: "Error-band id",
65
+ positionTypes: Object.freeze(["quantitative", "temporal"]),
66
+ defaultPositionType: "quantitative",
67
+ defaultIntervalChannel: "y",
68
+ scaleDefaults: fieldType => fieldType === "temporal"
69
+ ? { nice: true }
70
+ : { nice: true, zero: false },
71
+ intervalScaleDefaults: Object.freeze({ nice: true, zero: false }),
72
+ allowExplicitGrouping: true,
73
+ ambiguousMessage:
74
+ "editErrorBand cannot infer the interval axis when both positions are quantitative; provide an interval option."
75
+ });
48
76
  const BOUNDARY_EDIT_OPTIONS = Object.freeze([
49
77
  "target", "boundary", ...ERROR_BAND_BOUNDARY_OPTIONS
50
78
  ]);
@@ -76,6 +104,223 @@ function resolveOwner(program, requested) {
76
104
  });
77
105
  }
78
106
 
107
+ function currentRoleArgs(program, owner, current) {
108
+ const transform = findIntervalTransform(program, current.data);
109
+ const intervalChannel = current.orientation === "vertical" ? "y" : "x";
110
+ const positionChannel = intervalChannel === "x" ? "y" : "x";
111
+ const position = {
112
+ field: current.position.field,
113
+ fieldType: current.position.fieldType,
114
+ ...(current.position.temporalUnit === undefined
115
+ ? {}
116
+ : { temporalUnit: current.position.temporalUnit }),
117
+ scale: { id: current.positionScale }
118
+ };
119
+ const interval = transform === undefined
120
+ ? {
121
+ center: current.centerField ?? owner.encoding[intervalChannel].title,
122
+ lower: current.lowerField,
123
+ upper: current.upperField,
124
+ scale: { id: current.intervalScale }
125
+ }
126
+ : {
127
+ field: transform.field,
128
+ center: transform.center,
129
+ extent: transform.extent,
130
+ ...(transform.method === undefined ? {} : { method: transform.method }),
131
+ ...(transform.level === undefined ? {} : { level: transform.level }),
132
+ scale: { id: current.intervalScale }
133
+ };
134
+ return {
135
+ source: current.source ?? transform?.source ?? current.data,
136
+ x: positionChannel === "x" ? position : interval,
137
+ y: positionChannel === "y" ? position : interval,
138
+ groupBy: current.groupBy
139
+ };
140
+ }
141
+
142
+ function channelArgs(requested, fallback) {
143
+ if (requested === undefined) return fallback;
144
+ return requested;
145
+ }
146
+
147
+ function resolveRoleCandidate(program, owner, current, args) {
148
+ const previous = currentRoleArgs(program, owner, current);
149
+ const full = {
150
+ data: Object.hasOwn(args, "data") ? args.data : previous.source,
151
+ x: channelArgs(args.x, previous.x),
152
+ y: channelArgs(args.y, previous.y),
153
+ ...(Object.hasOwn(args, "groupBy")
154
+ ? { groupBy: args.groupBy }
155
+ : previous.groupBy === undefined ? {} : { groupBy: previous.groupBy }),
156
+ coordinate: current.coordinate
157
+ };
158
+ const policy = {
159
+ ...EDIT_POLICY,
160
+ existingId: owner.id,
161
+ coordinate: current.coordinate,
162
+ generatedFields: findIntervalTransform(program, current.data)?.as
163
+ };
164
+ let resolved = resolveIntervalComposite(program, full, policy);
165
+ if (resolved.groupField === resolved.position.field) {
166
+ throw new Error(
167
+ "editErrorBand groupBy must differ from the independent position field."
168
+ );
169
+ }
170
+ if (Object.hasOwn(args, "statistics")) {
171
+ if (!isPlainObject(args.statistics)) {
172
+ throw new TypeError("editErrorBand statistics must be a plain object.");
173
+ }
174
+ validateKeys(args.statistics, STATISTICS_OPTIONS, "editErrorBand statistics");
175
+ if (!STATISTICS_OPTIONS.some(key => Object.hasOwn(args.statistics, key))) {
176
+ throw new Error(
177
+ "editErrorBand statistics requires center, extent, method, or level."
178
+ );
179
+ }
180
+ full[resolved.interval.channel] = {
181
+ ...full[resolved.interval.channel],
182
+ ...args.statistics
183
+ };
184
+ resolved = resolveIntervalComposite(program, full, policy);
185
+ }
186
+ const positionRole = {
187
+ field: resolved.position.field,
188
+ fieldType: resolved.position.fieldType,
189
+ scale: resolved.position.scale
190
+ };
191
+ const intervalTitle = resolved.interval.mode === "statistical"
192
+ ? resolved.interval.field
193
+ : resolved.interval.title;
194
+ const intervalRole = {
195
+ field: intervalTitle,
196
+ fieldType: "quantitative",
197
+ scale: resolved.interval.scale
198
+ };
199
+ const x = resolved.position.channel === "x" ? positionRole : intervalRole;
200
+ const y = resolved.position.channel === "y" ? positionRole : intervalRole;
201
+ const plan = (channel, role) => {
202
+ const fallback = role === intervalRole
203
+ ? current.intervalScale
204
+ : current.positionScale;
205
+ const stored = findSemanticScale(program, fallback);
206
+ const compatible = role.fieldType === "temporal"
207
+ ? stored?.type === "time"
208
+ : ["linear", "log", "pow", "sqrt", "symlog"].includes(stored?.type);
209
+ const requested = role.scale.type !== undefined || compatible
210
+ ? role.scale
211
+ : { ...role.scale, type: role.fieldType === "temporal" ? "time" : "linear" };
212
+ return resolveDistributionScalePlan(program, {
213
+ channel,
214
+ fieldType: role.fieldType,
215
+ requested,
216
+ fallback,
217
+ defaults: role === intervalRole
218
+ ? EDIT_POLICY.intervalScaleDefaults
219
+ : EDIT_POLICY.scaleDefaults(role.fieldType)
220
+ });
221
+ };
222
+ const xScale = plan("x", x);
223
+ const yScale = plan("y", y);
224
+ return {
225
+ ...resolved,
226
+ x: { ...x, scale: xScale.id },
227
+ y: { ...y, scale: yScale.id },
228
+ xScale,
229
+ yScale,
230
+ category: resolved.position.field,
231
+ categoryType: resolved.position.fieldType,
232
+ measure: intervalTitle,
233
+ previous: {
234
+ x: { field: previous.x.center ?? previous.x.field,
235
+ fieldType: owner.encoding.x.fieldType, scale: owner.encoding.x.scale },
236
+ y: { field: previous.y.center ?? previous.y.field,
237
+ fieldType: owner.encoding.y.fieldType, scale: owner.encoding.y.scale }
238
+ }
239
+ };
240
+ }
241
+
242
+ function updateGrouping(program, ids, previous, nextField) {
243
+ let next = program;
244
+ for (const id of ids) {
245
+ const layer = findLayer(next, id);
246
+ if (layer === undefined) continue;
247
+ if (nextField === undefined) {
248
+ if (layer.encoding?.group !== undefined) {
249
+ next = next.editSemantic({
250
+ property: `layer[${id}].encoding.group`, remove: true
251
+ });
252
+ }
253
+ } else if (layer.encoding?.group === undefined) {
254
+ next = next.encodeGroup({ target: id, field: nextField, fieldType: "nominal" });
255
+ } else if (layer.encoding.group.field !== nextField) {
256
+ next = next.editSemantic({
257
+ property: `layer[${id}].encoding.group.field`, value: nextField
258
+ });
259
+ }
260
+ }
261
+ return next;
262
+ }
263
+
264
+ function updateErrorBandRoles(program, owner, current, candidate, dataId, boundaryIds) {
265
+ const owned = [owner.id, ...boundaryIds];
266
+ let next = updateDistributionPositions(program, owner, current, candidate, {
267
+ owned,
268
+ lower: candidate.fields.lower,
269
+ upper: candidate.fields.upper,
270
+ title: candidate.measure,
271
+ update(value, { category, measure, categoryChannel, measureChannel }) {
272
+ let updated = value;
273
+ for (const [id, field] of [
274
+ [current.lowerBoundaryId, candidate.fields.lower],
275
+ [current.upperBoundaryId, candidate.fields.upper]
276
+ ]) {
277
+ if (findLayer(updated, id) === undefined) continue;
278
+ updated = setCartesianPosition(updated, id, categoryChannel, {
279
+ field: candidate.position.field,
280
+ fieldType: candidate.position.fieldType,
281
+ scale: category.scale
282
+ });
283
+ updated = setCartesianPosition(updated, id, measureChannel, {
284
+ field,
285
+ fieldType: "quantitative",
286
+ scale: measure.scale
287
+ });
288
+ }
289
+ return updated;
290
+ }
291
+ });
292
+ next = updateGrouping(next, owned, current.groupBy, candidate.groupField);
293
+ const revisedConfig = {
294
+ ...current,
295
+ source: candidate.source,
296
+ data: dataId,
297
+ intervalMode: candidate.interval.mode,
298
+ orientation: candidate.orientation,
299
+ position: candidate.position,
300
+ positionScale: candidate.position.channel === "x"
301
+ ? candidate.xScale.id : candidate.yScale.id,
302
+ intervalScale: candidate.interval.channel === "x"
303
+ ? candidate.xScale.id : candidate.yScale.id,
304
+ groupBy: candidate.groupField,
305
+ transformGroupBy: candidate.groupBy,
306
+ lowerField: candidate.fields.lower,
307
+ upperField: candidate.fields.upper
308
+ };
309
+ delete revisedConfig.intervalField;
310
+ delete revisedConfig.centerField;
311
+ if (candidate.interval.mode === "statistical") {
312
+ revisedConfig.intervalField = candidate.interval.field;
313
+ } else {
314
+ revisedConfig.centerField = candidate.fields.center;
315
+ }
316
+ next = next._withMarkConfig(owner.id, {
317
+ ...next.markConfigs[owner.id],
318
+ errorBand: revisedConfig
319
+ }).rematerializeAreaMark({ id: owner.id });
320
+ for (const id of boundaryIds) next = next.rematerializeLineMark({ id });
321
+ return next;
322
+ }
323
+
79
324
  function currentBoundaryAppearance(program, id) {
80
325
  const config = program.markConfigs[id];
81
326
  if (findLayer(program, id)?.mark?.type !== "line" || config === undefined) {
@@ -166,11 +411,12 @@ export const rematerializeErrorBandBoundary = action(
166
411
  export const editErrorBand = action(
167
412
  {
168
413
  op: "editErrorBand",
169
- description: "Edit one error-band body appearance."
414
+ description: "Revise one error band's roles, statistics, body, and boundaries."
170
415
  },
171
416
  function (args = {}) {
172
417
  validateOptionObject(args, EDIT_OPTIONS, "editErrorBand");
173
- if (!["fill", "opacity", "curve", "statistics", "boundaries"]
418
+ if (!["data", "x", "y", "groupBy", "fill", "opacity", "curve",
419
+ "statistics", "boundaries"]
174
420
  .some(key => Object.hasOwn(args, key))) {
175
421
  throw new Error("editErrorBand requires at least one change.");
176
422
  }
@@ -178,7 +424,16 @@ export const editErrorBand = action(
178
424
  const config = { ...this.markConfigs[owner.id] };
179
425
  const errorBand = { ...config.errorBand };
180
426
  if (Object.hasOwn(args, "fill")) {
181
- errorBand.fill = validateNonEmptyString(args.fill, "Error-band fill");
427
+ if (args.fill === false) {
428
+ delete errorBand.fill;
429
+ config.fill = DEFAULT_COLORS.mark;
430
+ } else {
431
+ if (owner.encoding?.color !== undefined) {
432
+ throw new Error("editErrorBand fill conflicts with a color encoding; remove the color encoding first.");
433
+ }
434
+ errorBand.fill = validateNonEmptyString(args.fill, "Error-band fill");
435
+ config.fill = errorBand.fill;
436
+ }
182
437
  }
183
438
  if (Object.hasOwn(args, "opacity")) {
184
439
  config.opacity = validateUnitInterval(args.opacity, "Error-band opacity");
@@ -186,16 +441,30 @@ export const editErrorBand = action(
186
441
  if (Object.hasOwn(args, "curve")) {
187
442
  config.curve = validateCurveInterpolation(args.curve);
188
443
  }
189
- const interval = Object.hasOwn(args, "statistics")
444
+ const roleRequested = ["data", "x", "y", "groupBy"].some(
445
+ key => Object.hasOwn(args, key)
446
+ );
447
+ const candidate = roleRequested
448
+ ? resolveRoleCandidate(this, owner, errorBand, args)
449
+ : undefined;
450
+ const existingBoundaryIds = errorBandBoundaries(errorBand)
451
+ .map(([id]) => id)
452
+ .filter(id => findLayer(this, id) !== undefined);
453
+ const consumers = collectIntervalConsumers(
454
+ this, [owner.id, ...existingBoundaryIds]
455
+ );
456
+ const interval = roleRequested
457
+ ? planIntervalRoleData(this, {
458
+ owner: owner.id,
459
+ currentData: errorBand.data,
460
+ candidate,
461
+ consumers
462
+ })
463
+ : Object.hasOwn(args, "statistics")
190
464
  ? planIntervalEdit(this, {
191
465
  owner: owner.id,
192
466
  data: errorBand.data,
193
- consumers: [
194
- owner.id,
195
- ...errorBandBoundaries(errorBand)
196
- .map(([id]) => id)
197
- .filter(id => findLayer(this, id) !== undefined)
198
- ],
467
+ consumers,
199
468
  statistics: args.statistics,
200
469
  operation: "editErrorBand"
201
470
  })
@@ -210,17 +479,68 @@ export const editErrorBand = action(
210
479
  );
211
480
  }
212
481
 
213
- let next = applyIntervalRevision(this, interval);
214
- if (interval.changed) errorBand.data = interval.revision.id;
215
- next = next
216
- ._withMarkConfig(owner.id, { ...config, errorBand })
217
- .rematerializeAreaMark({ id: owner.id });
482
+ let base = this;
483
+ if (roleRequested && owner.encoding?.color?.field !== undefined) {
484
+ const colorField = owner.encoding.color.field;
485
+ const followsGroup = colorField === this.markConfigs[owner.id].errorBand.groupBy;
486
+ if (followsGroup && candidate.groupField !== undefined) {
487
+ const referenced = Object.entries(
488
+ this.materializationConfigs.selections ?? {}
489
+ ).find(([, selection]) =>
490
+ selection.target === owner.id &&
491
+ ["group", "color"].includes(selection.selector?.channel)
492
+ );
493
+ if (referenced !== undefined && candidate.groupField !== colorField) {
494
+ throw new Error(
495
+ `editErrorBand cannot change groupBy while selection "${referenced[0]}" ` +
496
+ `references ${referenced[1].selector.channel}.`
497
+ );
498
+ }
499
+ base = base.editSemantic({
500
+ property: `layer[${owner.id}].encoding.color.field`,
501
+ value: candidate.groupField
502
+ });
503
+ } else if (
504
+ candidate.interval.mode === "statistical" &&
505
+ !candidate.groupBy.includes(colorField)
506
+ ) {
507
+ throw new Error(
508
+ `editErrorBand roles would remove color field "${colorField}" from the interval data.`
509
+ );
510
+ }
511
+ }
512
+ if (roleRequested && candidate.groupField !== undefined) {
513
+ for (const id of [owner.id, ...existingBoundaryIds]) {
514
+ const layer = findLayer(base, id);
515
+ if (
516
+ layer?.encoding?.group !== undefined &&
517
+ layer.encoding.group.field !== candidate.groupField
518
+ ) {
519
+ base = base.editSemantic({
520
+ property: `layer[${id}].encoding.group.field`,
521
+ value: candidate.groupField
522
+ });
523
+ }
524
+ }
525
+ }
526
+ let next = applyIntervalRevision(base, interval);
527
+ if (interval.changed) {
528
+ errorBand.data = interval.dataId ?? interval.revision.id;
529
+ }
530
+ next = next._withMarkConfig(owner.id, { ...config, errorBand });
531
+ next = roleRequested
532
+ ? updateErrorBandRoles(
533
+ next, owner, this.markConfigs[owner.id].errorBand,
534
+ candidate, interval.dataId, existingBoundaryIds
535
+ )
536
+ : next.rematerializeAreaMark({ id: owner.id });
537
+ const revisedErrorBand = next.markConfigs[owner.id].errorBand;
218
538
  if (Object.hasOwn(args, "boundaries")) {
219
539
  if (args.boundaries === false) {
220
- next = removeBoundary(next, errorBand.lowerBoundaryId);
221
- next = removeBoundary(next, errorBand.upperBoundaryId);
540
+ next = removeBoundary(next, revisedErrorBand.lowerBoundaryId);
541
+ next = removeBoundary(next, revisedErrorBand.upperBoundaryId);
222
542
  } else if (Object.keys(args.boundaries).length === 0) {
223
- for (const [id, bound] of errorBandBoundaries(errorBand)) {
543
+ for (const [id, bound] of errorBandBoundaries(revisedErrorBand)) {
224
544
  next = findLayer(next, id) === undefined
225
545
  ? createBoundary(
226
546
  next,
@@ -239,8 +559,8 @@ export const editErrorBand = action(
239
559
  ...args.boundaries
240
560
  });
241
561
  }
242
- } else if (interval.changed) {
243
- for (const [id] of errorBandBoundaries(errorBand)) {
562
+ } else if (interval.changed && !roleRequested) {
563
+ for (const [id] of errorBandBoundaries(revisedErrorBand)) {
244
564
  if (findLayer(next, id) !== undefined) {
245
565
  next = next.rematerializeLineMark({ id });
246
566
  }
@@ -38,7 +38,7 @@ const OFFSET_OPTIONS = Object.freeze([
38
38
  "field", "fieldType", "scale", "paddingInner", "paddingOuter"
39
39
  ]);
40
40
 
41
- const ERROR_BAR_POLICY = Object.freeze({
41
+ export const ERROR_BAR_POLICY = Object.freeze({
42
42
  operation: "createErrorBar",
43
43
  resourceLabel: "error-bar",
44
44
  defaultId: "errorBar",
@@ -55,9 +55,9 @@ const ERROR_BAR_POLICY = Object.freeze({
55
55
  "createErrorBar requires one quantitative interval axis and one compatible position axis; use interval options to disambiguate two quantitative channels."
56
56
  });
57
57
 
58
- function resolveErrorBar(program, args) {
59
- const resolved = resolveIntervalComposite(program, args, ERROR_BAR_POLICY);
60
- const offset = resolveErrorBarOffset(program, args, resolved);
58
+ export function resolveErrorBar(program, args, policy = ERROR_BAR_POLICY) {
59
+ const resolved = resolveIntervalComposite(program, args, policy);
60
+ const offset = resolveErrorBarOffset(program, args, resolved, policy.operation);
61
61
  const groupBy = resolved.interval.mode === "statistical" && offset !== undefined
62
62
  ? [...new Set([...resolved.groupBy, offset.field])]
63
63
  : resolved.groupBy;
@@ -70,27 +70,27 @@ function resolveErrorBar(program, args) {
70
70
  };
71
71
  }
72
72
 
73
- function resolveErrorBarOffset(program, args, resolved) {
73
+ function resolveErrorBarOffset(program, args, resolved, operation) {
74
74
  const channel = `${resolved.position.channel}Offset`;
75
75
  const incompatible = channel === "xOffset" ? "yOffset" : "xOffset";
76
76
  if (args[incompatible] !== undefined) {
77
77
  throw new Error(
78
- `createErrorBar ${incompatible} does not match the ${resolved.orientation} interval orientation.`
78
+ `${operation} ${incompatible} does not match the ${resolved.orientation} interval orientation.`
79
79
  );
80
80
  }
81
81
  const explicit = args[channel] === undefined
82
82
  ? undefined
83
- : validateOptionObject(args[channel], OFFSET_OPTIONS, `createErrorBar ${channel}`);
83
+ : validateOptionObject(args[channel], OFFSET_OPTIONS, `${operation} ${channel}`);
84
84
  const inferred = resolved.sourceLayer?.encoding?.[channel];
85
85
  if (explicit === undefined && inferred === undefined) return undefined;
86
86
  if (!["nominal", "ordinal"].includes(resolved.position.fieldType)) {
87
87
  throw new Error(
88
- `createErrorBar ${channel} requires a categorical ${resolved.position.channel} position.`
88
+ `${operation} ${channel} requires a categorical ${resolved.position.channel} position.`
89
89
  );
90
90
  }
91
91
  const field = explicit?.field ?? inferred?.field;
92
92
  if (typeof field !== "string" || field.length === 0) {
93
- throw new TypeError(`createErrorBar ${channel} field must be a non-empty string.`);
93
+ throw new TypeError(`${operation} ${channel} field must be a non-empty string.`);
94
94
  }
95
95
  const fieldType = validateCategoricalFieldType(
96
96
  explicit?.fieldType ?? inferred?.fieldType ?? "nominal"
@@ -140,7 +140,7 @@ export const createErrorBarCap = action(
140
140
  function (args = {}) {
141
141
  validateKeys(args, [
142
142
  "id", "data", "orientation", "positionField", "positionFieldType",
143
- "intervalField", "coordinate", "positionScale", "intervalScale",
143
+ "intervalField", "coordinate", "positionScale", "intervalScale", "positionTemporalUnit",
144
144
  "offsetChannel", "offsetField", "offsetFieldType", "offsetScale",
145
145
  "offsetPaddingInner", "offsetPaddingOuter",
146
146
  "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
@@ -166,6 +166,7 @@ export const createErrorBarCap = action(
166
166
  target: args.id,
167
167
  field: args.positionField,
168
168
  fieldType: args.positionFieldType,
169
+ ...(args.positionTemporalUnit === undefined ? {} : { temporalUnit: args.positionTemporalUnit }),
169
170
  coordinate: args.coordinate,
170
171
  scale: { id: args.positionScale }
171
172
  })
@@ -207,6 +208,7 @@ function positionArgs(resolved) {
207
208
  target: resolved.id,
208
209
  field: resolved.position.field,
209
210
  fieldType: resolved.position.fieldType,
211
+ ...(resolved.position.temporalUnit === undefined ? {} : { temporalUnit: resolved.position.temporalUnit }),
210
212
  coordinate: resolved.coordinate,
211
213
  scale: resolved.position.scale
212
214
  };
@@ -282,6 +284,7 @@ export const createErrorBar = action(
282
284
  orientation: resolved.orientation,
283
285
  positionField: resolved.position.field,
284
286
  positionFieldType: resolved.position.fieldType,
287
+ ...(resolved.position.temporalUnit === undefined ? {} : { positionTemporalUnit: resolved.position.temporalUnit }),
285
288
  intervalField: field,
286
289
  coordinate: resolved.coordinate,
287
290
  positionScale: intervalLayer.encoding[resolved.position.channel].scale,
@@ -307,10 +310,18 @@ export const createErrorBar = action(
307
310
  return next._withMarkConfig(resolved.id, {
308
311
  ...next.markConfigs[resolved.id],
309
312
  errorBar: {
313
+ source: resolved.source,
314
+ intervalMode: resolved.interval.mode,
315
+ ...(resolved.interval.mode === "statistical"
316
+ ? { intervalField: resolved.interval.field }
317
+ : { centerField: resolved.fields.center }),
318
+ groupField: resolved.groupField,
319
+ groupBy: resolved.groupBy,
310
320
  data: resolved.dataId,
311
321
  orientation: resolved.orientation,
312
322
  positionField: resolved.position.field,
313
323
  positionFieldType: resolved.position.fieldType,
324
+ ...(resolved.position.temporalUnit === undefined ? {} : { positionTemporalUnit: resolved.position.temporalUnit }),
314
325
  lowerField: resolved.fields.lower,
315
326
  upperField: resolved.fields.upper,
316
327
  coordinate: resolved.coordinate,