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
@@ -1,34 +1,27 @@
1
+ import { readAreaEndpoint, validateAreaEndpointPair, normalizeAreaBound } from "../../grammar/areaEndpoints.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validateUserId } from "../../core/identifiers.js";
3
4
  import { isPlainObject } from "../../core/immutable.js";
4
- import {
5
- readNominalField,
6
- readQuantitativeField,
7
- readScaleField,
8
- readTemporalField,
9
- validateSemanticFieldType
10
- } from "../../grammar/scales/index.js";
11
- import { normalizeRuleDatum } from "../../grammar/rules.js";
12
- import {
13
- canMaterializeArea,
14
- canMaterializeLine,
15
- getPositionEncodingMaterializationSteps
16
- } from "../../materialization/marks/index.js";
5
+ import { readNominalField, readQuantitativeField, readScaleField, readTemporalField, resolveTemporalUnit, validateSemanticFieldType } from "../../grammar/scales/index.js";
6
+ import { normalizePositionDatum } from "../../grammar/positionDatum.js";
7
+ import { normalizeGroupFields } from "../../grammar/pathSeries.js";
8
+ import { assertPathGroupCompatible, validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
9
+ import { canMaterializeArea, canMaterializeLine, getPositionEncodingMaterializationSteps } from "../../materialization/marks/index.js";
17
10
  import { findLayer } from "../../selectors/layers.js";
18
- import {
19
- resolveTarget,
20
- setEncodingProperties,
21
- validateLineSeriesCompatibility,
22
- validateOptions
23
- } from "./shared.js";
11
+ import { resolveTarget, setEncodingProperties, validateOptions } from "./shared.js";
12
+ import { applyTemporalUnit } from "./temporal.js";
13
+ import { deriveAreaSeries } from "../../grammar/areaSeries.js";
14
+ import { resolvePositionEncoding } from "./position/resolve.js";
15
+ import { resolveScalePreview } from "../scales/preview.js";
16
+ import { findUpstreamTransform } from "../../materialization/dataProvenance.js";
24
17
 
25
18
  const SECONDARY_OPTIONS = Object.freeze([
26
- "field", "datum", "target", "fieldType", "scale", "coordinate"
19
+ "field", "datum", "target", "fieldType", "scale", "coordinate", "temporalUnit"
27
20
  ]);
28
21
  const RANGE_OPTIONS = Object.freeze([
29
- "lower", "upper", "target", "fieldType", "coordinate", "scale"
22
+ "lower", "upper", "target", "fieldType", "coordinate", "scale", "temporalUnit"
30
23
  ]);
31
- const GROUP_OPTIONS = Object.freeze(["field", "target", "fieldType"]);
24
+ const GROUP_OPTIONS = Object.freeze(["field", "fields", "target", "fieldType"]);
32
25
 
33
26
  function validateSecondaryScale(args, primaryScale, axisLabel) {
34
27
  if (args.scale === undefined) return;
@@ -41,11 +34,11 @@ function validateSecondaryScale(args, primaryScale, axisLabel) {
41
34
  }
42
35
  }
43
36
 
44
- function validateSecondaryField(dataset, field, fieldType) {
37
+ function validateSecondaryField(dataset, field, fieldType, temporalUnit) {
45
38
  if (["nominal", "ordinal"].includes(fieldType)) {
46
39
  readNominalField(dataset.values, field);
47
40
  } else if (fieldType === "temporal") {
48
- readTemporalField(dataset.values, field);
41
+ readTemporalField(dataset.values, field, temporalUnit);
49
42
  } else {
50
43
  readQuantitativeField(dataset.values, field);
51
44
  }
@@ -75,12 +68,14 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
75
68
  validateSecondaryScale(args, primary.scale, primaryChannel);
76
69
 
77
70
  const rule = layer.mark.type === "rule";
71
+ const area = layer.mark.type === "area";
72
+ const rect = layer.mark.type === "rect";
78
73
  const hasField = Object.hasOwn(args, "field");
79
74
  const hasDatum = Object.hasOwn(args, "datum");
80
- if (rule && hasField === hasDatum) {
81
- throw new Error(`${operation} requires exactly one of field or datum for a rule mark.`);
75
+ if ((rule || area || rect) && hasField === hasDatum) {
76
+ throw new Error(`${operation} requires exactly one of field or datum for a ${layer.mark.type} mark.`);
82
77
  }
83
- if (!rule && (!hasField || hasDatum)) {
78
+ if (!rule && !area && !rect && (!hasField || hasDatum)) {
84
79
  throw new Error(`${operation} requires a field for a ranged mark.`);
85
80
  }
86
81
  if (rule && args.fieldType === undefined) {
@@ -114,14 +109,20 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
114
109
  `${operation} fieldType must match the primary ${primaryChannel} fieldType.`
115
110
  );
116
111
  }
117
- if (hasField && layer.mark.type === "rect") {
118
- readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true });
119
- } else if (hasField) validateSecondaryField(dataset, args.field, fieldType);
120
- else normalizeRuleDatum(args.datum, fieldType, channel);
121
-
122
112
  const previous = layer.encoding?.[channel];
113
+ const temporalUnit = resolveTemporalUnit(args, fieldType, previous);
114
+ if (area) {
115
+ const endpoint = { ...(hasField ? { field: args.field } : { datum: args.datum }), fieldType };
116
+ validateAreaEndpointPair(primary, endpoint);
117
+ readAreaEndpoint(dataset.values, endpoint, layer.mark.missing);
118
+ } else if (hasField && layer.mark.type === "rect") {
119
+ readScaleField(dataset.values, args.field, fieldType, { allowUnknown: true, temporalUnit });
120
+ } else if (hasField) validateSecondaryField(dataset, args.field, fieldType, temporalUnit);
121
+ else normalizePositionDatum(args.datum, fieldType, channel, temporalUnit, rule ? "Rule" : "Rect");
122
+
123
123
  let next = program;
124
124
  if (layer.mark.type === "bar") {
125
+ if (layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Ranged bars support only overlay layout; clear series layout before assigning endpoints.");
125
126
  for (const property of ["aggregate", "stack", "bin"]) {
126
127
  if (layer.encoding?.[primaryChannel]?.[property] !== undefined) {
127
128
  next = next.editSemantic({
@@ -146,6 +147,7 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
146
147
  scale: primary.scale
147
148
  });
148
149
 
150
+ next = applyTemporalUnit(next, target, channel, temporalUnit, previous);
149
151
  const updated = findLayer(next, target);
150
152
  for (const step of getPositionEncodingMaterializationSteps(
151
153
  next,
@@ -189,91 +191,91 @@ const encodeY2 = action(
189
191
  }
190
192
  );
191
193
 
192
- const encodeYRange = action(
193
- {
194
- op: "encodeYRange",
195
- description: "Atomically encode lower and upper area bounds."
196
- },
197
- function (args = {}) {
198
- validateOptions(args, RANGE_OPTIONS, "encodeYRange");
199
- const target = args.target;
200
- const lower = this.encodeY({
201
- field: args.lower,
202
- ...(target === undefined ? {} : { target }),
203
- fieldType: args.fieldType ?? "quantitative",
204
- ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
205
- ...(args.scale === undefined ? {} : { scale: args.scale })
206
- });
207
- return lower.encodeY2({
208
- field: args.upper,
209
- ...(target === undefined ? {} : { target }),
194
+ function rangeAction(channel) {
195
+ const primary = channel === "x" ? "encodeX" : "encodeY";
196
+ const secondary = channel === "x" ? "encodeX2" : "encodeY2";
197
+ const op = channel === "x" ? "encodeXRange" : "encodeYRange";
198
+ return action({ op, description: "Atomically encode lower and upper bounds." }, function (args = {}) {
199
+ validateOptions(args, RANGE_OPTIONS, op);
200
+ const common = {
201
+ ...(args.target === undefined ? {} : { target: args.target }),
202
+ ...(Object.hasOwn(args, "temporalUnit") ? { temporalUnit: args.temporalUnit } : {}),
210
203
  fieldType: args.fieldType ?? "quantitative"
211
- });
212
- }
213
- );
214
-
215
- const encodeXRange = action(
216
- {
217
- op: "encodeXRange",
218
- description: "Atomically encode lower and upper horizontal area bounds."
219
- },
220
- function (args = {}) {
221
- validateOptions(args, RANGE_OPTIONS, "encodeXRange");
222
- const target = args.target;
223
- const lower = this.encodeX({
224
- field: args.lower,
225
- ...(target === undefined ? {} : { target }),
226
- fieldType: args.fieldType ?? "quantitative",
204
+ };
205
+ const { layer, dataset } = resolveTarget(this, args.target, ["area", "bar", "rect", "rule"], "ranged mark");
206
+ let next = this;
207
+ let lower = { field: args.lower }, upper = { field: args.upper };
208
+ if (layer.mark.type === "area") {
209
+ common.target = layer.id;
210
+ const prepared = prepareAreaRange(this, layer, dataset, channel, args, common);
211
+ lower = prepared.lower; upper = prepared.upper;
212
+ const secondaryChannel = `${channel}2`;
213
+ if (layer.encoding?.[secondaryChannel] !== undefined) next = next.editSemantic({ property: `layer[${layer.id}].encoding.${secondaryChannel}`, remove: true });
214
+ next = setEncodingProperties(next, layer.id, secondaryChannel, prepared.secondary);
215
+ common.target = layer.id;
216
+ }
217
+ return next[primary]({
218
+ ...common, ...lower,
227
219
  ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
228
220
  ...(args.scale === undefined ? {} : { scale: args.scale })
229
- });
230
- return lower.encodeX2({
231
- field: args.upper,
232
- ...(target === undefined ? {} : { target }),
233
- fieldType: args.fieldType ?? "quantitative"
234
- });
235
- }
236
- );
221
+ })[secondary]({ ...common, ...upper });
222
+ });
223
+ }
224
+
225
+ const encodeYRange = rangeAction("y");
226
+ const encodeXRange = rangeAction("x");
237
227
 
238
228
  const encodeGroup = action(
239
229
  {
240
230
  op: "encodeGroup",
241
- description: "Split path geometry by a nominal field without a scale."
231
+ description: "Split path geometry by nominal identity fields without a scale."
242
232
  },
243
233
  function (args = {}) {
244
234
  validateOptions(args, GROUP_OPTIONS, "encodeGroup");
245
235
  const { id: target, dataset, layer } = resolveTarget(
246
236
  this,
247
237
  args.target,
248
- ["line", "area"],
249
- "path mark"
238
+ ["line", "area", "bar"],
239
+ "series mark"
250
240
  );
251
241
  if ((args.fieldType ?? "nominal") !== "nominal") {
252
242
  throw new Error("encodeGroup requires a nominal field.");
253
243
  }
254
- const densityTransform = dataset.transform?.length === 1 &&
255
- dataset.transform[0].type === "density"
256
- ? dataset.transform[0]
257
- : undefined;
258
- if (densityTransform !== undefined && densityTransform.groupBy !== args.field) {
259
- throw new Error(
260
- densityTransform.groupBy === undefined
261
- ? `Ungrouped density area mark "${target}" cannot encode group.`
262
- : `Density area mark "${target}" must group by "${densityTransform.groupBy}".`
263
- );
244
+ const hasFields = Object.hasOwn(args, "fields");
245
+ if (hasFields === Object.hasOwn(args, "field")) {
246
+ throw new Error("encodeGroup requires exactly one of field or fields.");
264
247
  }
265
- readNominalField(dataset.values, args.field);
266
- validateLineSeriesCompatibility(layer, "group", args.field);
267
- const next = this
268
- .editSemantic({
269
- property: `layer[${target}].encoding.group.field`,
270
- value: args.field
271
- })
272
- .editSemantic({
273
- property: `layer[${target}].encoding.group.fieldType`,
274
- value: "nominal"
248
+ if (hasFields ? !Array.isArray(args.fields) : typeof args.field !== "string") {
249
+ throw new TypeError("encodeGroup field must be a string and fields must be an array.");
250
+ }
251
+ const fields = normalizeGroupFields(hasFields ? args.fields : args.field);
252
+ const group = {
253
+ ...(fields.length === 1 ? { field: fields[0] } : { fields }),
254
+ fieldType: "nominal"
255
+ };
256
+ assertPathGroupCompatible(this, layer, dataset, group);
257
+ for (const field of fields) readNominalField(dataset.values, field);
258
+ if (layer.mark.type !== "bar") validatePathGroupAppearance(this, {
259
+ ...layer, encoding: { ...layer.encoding, group }
260
+ }, dataset);
261
+ let next = this;
262
+ const alternate = fields.length === 1 ? "fields" : "field";
263
+ if (layer.encoding?.group?.[alternate] !== undefined) {
264
+ next = next.editSemantic({
265
+ property: `layer[${target}].encoding.group.${alternate}`, remove: true
275
266
  });
267
+ }
268
+ if (layer.encoding?.group?.inferredFrom !== undefined) next = next.editSemantic({ property: `layer[${target}].encoding.group.inferredFrom`, remove: true });
269
+ next = setEncodingProperties(next, target, "group", group);
270
+ if (layer.mark.type === "bar") {
271
+ const mode = layer.layout?.mode;
272
+ if (mode !== undefined) return next.layoutSeries({ target, mode });
273
+ if (layer.encoding?.x?.scale === undefined || layer.encoding?.y?.scale === undefined) return next;
274
+ for (const step of getPositionEncodingMaterializationSteps(next, findLayer(next, target), layer.encoding?.y?.scale)) next = next[step.op](step.args);
275
+ return next;
276
+ }
276
277
  if (layer.mark.type === "area") {
278
+ if (layer.layout?.mode !== undefined) return next.layoutSeries({ target, mode: layer.layout.mode });
277
279
  const updated = findLayer(next, target);
278
280
  return canMaterializeArea(next, updated)
279
281
  ? next.rematerializeAreaMark({ id: target })
@@ -297,3 +299,27 @@ export function registerRangedEncodingActions(ProgramClass) {
297
299
  ProgramClass.prototype.encodeXRange = encodeXRange;
298
300
  ProgramClass.prototype.encodeYRange = encodeYRange;
299
301
  }
302
+
303
+ export function prepareAreaRange(program, layer, dataset, channel, args, common) {
304
+ const lower = normalizeAreaBound(args.lower), upper = normalizeAreaBound(args.upper);
305
+ validateAreaEndpointPair(lower, upper);
306
+ if (common.fieldType !== "quantitative") throw new Error("Area range requires quantitative endpoints.");
307
+ readAreaEndpoint(dataset.values, { ...upper, fieldType: "quantitative" }, layer.mark.missing);
308
+ const resolved = resolvePositionEncoding(program, channel, { ...common, ...lower,
309
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
310
+ ...(args.scale === undefined ? {} : { scale: args.scale }) }, `encode${channel.toUpperCase()}Range`);
311
+ const secondary = { ...upper, fieldType: "quantitative", scale: resolved.scale.id };
312
+ const primary = { ...lower, fieldType: "quantitative", scale: resolved.scale.id };
313
+ const updated = { ...layer, coordinate: resolved.coordinate.id,
314
+ encoding: { ...layer.encoding, [channel]: primary, [`${channel}2`]: secondary } };
315
+ const scales = program.semanticSpec.scales.filter(scale => scale.id !== resolved.scale.id);
316
+ const preview = { ...program, markConfigs: program.markConfigs, canvasConfig: program.canvasConfig,
317
+ semanticSpec: { ...program.semanticSpec,
318
+ layers: program.semanticSpec.layers.map(item => item.id === layer.id ? updated : item),
319
+ scales: [...scales, resolved.scale] } };
320
+ resolveScalePreview(preview, resolved.scale.id);
321
+ if (canMaterializeArea(preview, updated) && !findUpstreamTransform(program, dataset, "density")) {
322
+ deriveAreaSeries(dataset.values, updated);
323
+ }
324
+ return { lower, upper, secondary };
325
+ }
@@ -15,8 +15,16 @@ import {
15
15
  getSourceDependentMarkSteps
16
16
  } from "../../materialization/marks/index.js";
17
17
  import { findLayer } from "../../selectors/layers.js";
18
- import { removeLegendKinds } from "../guides/legends/remove.js";
18
+ import {
19
+ removeLegendKinds,
20
+ resolveCategoricalLegendRevision,
21
+ createCategoricalLegendFromConfig
22
+ } from "../guides/legends/lifecycle.js";
19
23
  import { clearMarkGraphic } from "./shared.js";
24
+ import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
25
+ import { findDataset } from "../../selectors/datasets.js";
26
+ import { assertPathGroupCompatible, validatePathGroupAppearance } from "../../materialization/marks/grouping.js";
27
+ import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
20
28
 
21
29
  const OPTIONS = Object.freeze(["target", "channel"]);
22
30
  const REMOVABLE_CHANNELS = Object.freeze([
@@ -36,10 +44,6 @@ const SPECIALIZED_LEGEND_KIND = Object.freeze({
36
44
  opacity: Object.freeze(["opacity"]),
37
45
  strokeWidth: Object.freeze(["strokeWidth"])
38
46
  });
39
- const LEGEND_OPTION_KEYS = Object.freeze([
40
- "position", "align", "direction", "columns", "offset", "titlePosition",
41
- "title", "symbol", "labels", "titleStyle", "itemGap", "border"
42
- ]);
43
47
 
44
48
  function resolveTarget(program, requested, channel) {
45
49
  const candidates = program.semanticSpec.layers.filter(
@@ -77,7 +81,7 @@ function activeCascade(layer, channel) {
77
81
  channels.add("y2");
78
82
  channels.add("yOffset");
79
83
  }
80
- if (channel === "color" && layer.mark?.type === "bar") {
84
+ if (channel === "color" && layer.mark?.type === "bar" && layer.layout === undefined) {
81
85
  if (resolveBarColorLayout(layer) === "group") {
82
86
  channels.add(resolveBarOffsetChannel(layer));
83
87
  }
@@ -85,6 +89,7 @@ function activeCascade(layer, channel) {
85
89
  if (
86
90
  channel === "group" &&
87
91
  layer.mark?.type === "area" &&
92
+ layer.layout === undefined &&
88
93
  layer.encoding?.color?.field !== undefined &&
89
94
  layer.encoding.color.field === layer.encoding.group?.field
90
95
  ) {
@@ -93,21 +98,6 @@ function activeCascade(layer, channel) {
93
98
  return [...channels].filter(active => layer.encoding?.[active] !== undefined);
94
99
  }
95
100
 
96
- function assertSelectionCompatibility(program, target, channels) {
97
- for (const [id, selection] of Object.entries(
98
- program.materializationConfigs.selections ?? {}
99
- )) {
100
- if (
101
- selection.target === target &&
102
- channels.includes(selection.selector?.channel)
103
- ) {
104
- throw new Error(
105
- `Cannot remove ${selection.selector.channel} encoding while selection "${id}" references that channel.`
106
- );
107
- }
108
- }
109
- }
110
-
111
101
  function removeChannelConfigs(program, target, channels) {
112
102
  let next = program;
113
103
  for (const channel of channels) {
@@ -182,16 +172,9 @@ function reconcileCategoricalLegend(program, target, channels) {
182
172
  if (remaining.length === 0) {
183
173
  return removeLegendKinds(program, [kind]);
184
174
  }
175
+ const revision = resolveCategoricalLegendRevision(program, kind, config, remaining);
185
176
  const next = removeLegendKinds(program, [kind]);
186
- const options = { target, channels: remaining };
187
- for (const property of LEGEND_OPTION_KEYS) {
188
- if (
189
- (property === "columns" && config[property] === undefined) ||
190
- (property === "title" && config.inferredTitle)
191
- ) continue;
192
- options[property] = config[property];
193
- }
194
- return next.createLegend(options);
177
+ return createCategoricalLegendFromConfig(next, revision.config, revision.order);
195
178
  }
196
179
 
197
180
  function cleanupLegends(program, target, channels) {
@@ -228,8 +211,30 @@ export const removeEncoding = action(
228
211
  );
229
212
  }
230
213
  const layer = resolveTarget(this, args.target, args.channel);
214
+ if (args.channel === "group" && layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Remove active series layout before removing its group.");
215
+ if (args.channel === "theta" && layer.mark.type === "arc" &&
216
+ ["count", "sum"].includes(layer.encoding?.radius?.aggregate)) {
217
+ throw new Error("Remove measured radius before removing its category theta encoding.");
218
+ }
231
219
  const channels = activeCascade(layer, args.channel);
232
- assertSelectionCompatibility(this, layer.id, channels);
220
+ assertEncodingSelectionCompatibility(this, layer.id, channels);
221
+ for (const kind of ["series", "color"]) {
222
+ const config = this.guideConfigs.legend?.[kind];
223
+ const order = this.semanticSpec.guides.legend?.[kind]?.order;
224
+ if (config?.target === layer.id && channels.includes(order?.channel) &&
225
+ config.channels.some(channel => !channels.includes(channel))) {
226
+ throw new Error('Reset linked legend order to "scale" before removing its position encoding.');
227
+ }
228
+ }
229
+ if (["line", "area"].includes(layer.mark?.type)) {
230
+ const dataset = findDataset(this, layer.data);
231
+ if (args.channel === "group") {
232
+ assertPathGroupCompatible(this, layer, dataset, undefined);
233
+ }
234
+ const encoding = { ...layer.encoding };
235
+ for (const channel of channels) delete encoding[channel];
236
+ validatePathGroupAppearance(this, { ...layer, encoding }, dataset);
237
+ }
233
238
  const removedPositions = channels
234
239
  .filter(channel => POSITION_ENCODING_CHANNELS.includes(channel))
235
240
  .map(channel => ({
@@ -249,7 +254,7 @@ export const removeEncoding = action(
249
254
  next = restoreBarBaseline(next, layer, channels);
250
255
  next = cleanupLegends(next, layer.id, channels);
251
256
  next = cleanupPositionGuides(next, removedPositions);
252
- return rematerializeTarget(next, layer.id);
257
+ return applyDetachedScaleRematerialization(rematerializeTarget(next, layer.id), [layer]);
253
258
  }
254
259
  );
255
260
 
@@ -7,6 +7,7 @@ import { readQuantitativeField } from "../../grammar/scales/index.js";
7
7
  import { resolveStrokeWidthScaleDefinition } from "../scales/definitions.js";
8
8
  import {
9
9
  applyEncodingScale,
10
+ applyDetachedScaleRematerialization,
10
11
  rematerializeEncoding,
11
12
  resolveReassignmentScaleOptions,
12
13
  resolveTarget,
@@ -14,6 +15,8 @@ import {
14
15
  validateOptions
15
16
  } from "./shared.js";
16
17
  import { findLayer } from "../../selectors/layers.js";
18
+ import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
19
+ import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
17
20
 
18
21
  const STROKE_OPTIONS = Object.freeze(["target", "value"]);
19
22
  const WIDTH_OPTIONS = Object.freeze([
@@ -40,7 +43,7 @@ const encodeStroke = action(
40
43
  const encodeStrokeWidth = action(
41
44
  {
42
45
  op: "encodeStrokeWidth",
43
- description: "Set a constant graphical stroke width on a rule mark."
46
+ description: "Assign constant or field-driven line or rule stroke width."
44
47
  },
45
48
  function (args = {}) {
46
49
  validateOptions(args, WIDTH_OPTIONS, "encodeStrokeWidth");
@@ -50,9 +53,14 @@ const encodeStrokeWidth = action(
50
53
  throw new Error("encodeStrokeWidth requires exactly one of value or field.");
51
54
  }
52
55
  if (hasValue) {
53
- const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
56
+ if (args.fieldType !== undefined || args.scale !== undefined) {
57
+ throw new Error("Constant stroke width does not accept fieldType or scale.");
58
+ }
59
+ validateRuleStrokeWidth(args.value);
60
+ const { id, layer } = resolveTarget(this, args.target, ["rule", "line"], "rule or line mark");
61
+ assertEncodingSelectionCompatibility(this, id, ["strokeWidth"]);
54
62
  let next = this.guideConfigs.legend?.strokeWidth?.target === id
55
- ? this.removeLegend({ target: id })
63
+ ? this.removeLegend({ target: id, channels: ["strokeWidth"] })
56
64
  : this;
57
65
  if (findLayer(next, id)?.encoding?.strokeWidth !== undefined) {
58
66
  next = next.editSemantic({
@@ -60,12 +68,15 @@ const encodeStrokeWidth = action(
60
68
  remove: true
61
69
  });
62
70
  }
63
- return next
64
- ._withMarkConfig(id, {
71
+ next = next._withMarkConfig(id, {
65
72
  ...next.markConfigs[id],
66
- strokeWidth: validateRuleStrokeWidth(args.value)
67
- })
68
- .rematerializeRuleMark({ id });
73
+ strokeWidth: args.value
74
+ });
75
+ if (layer.mark.type === "line") {
76
+ return rematerializeEncoding(next, id, "strokeWidth", undefined, layer);
77
+ }
78
+ next = next.rematerializeRuleMark({ id });
79
+ return applyDetachedScaleRematerialization(next, [layer]);
69
80
  }
70
81
  const { id, dataset, layer } = resolveTarget(
71
82
  this,
@@ -83,6 +94,9 @@ const encodeStrokeWidth = action(
83
94
  `encodeStrokeWidth field "${args.field}" cannot contain negative values.`
84
95
  );
85
96
  }
97
+ validatePathSeriesAppearance(dataset.values, {
98
+ ...layer, encoding: { ...layer.encoding, strokeWidth: { field: args.field, fieldType } }
99
+ });
86
100
  const previous = layer.encoding?.strokeWidth;
87
101
  const requestedScale = resolveReassignmentScaleOptions(
88
102
  previous,
@@ -100,7 +114,7 @@ const encodeStrokeWidth = action(
100
114
  next = applyEncodingScale(next, scale, requestedScale, {
101
115
  reassignment: previous?.scale === scale.id
102
116
  });
103
- return rematerializeEncoding(next, id, "strokeWidth", scale.id);
117
+ return rematerializeEncoding(next, id, "strokeWidth", scale.id, layer);
104
118
  }
105
119
  );
106
120
 
@@ -0,0 +1,104 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateColorLayout } from "../../grammar/seriesLayout.js";
3
+ import { deriveAreaSeries, deriveDensityAreaSeries, layoutDensityAreaSeries } from "../../grammar/areaSeries.js";
4
+ import { readSeriesIdentity } from "../../grammar/pathSeries.js";
5
+ import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../../grammar/bars/policy.js";
6
+ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
7
+ import { findSemanticScale, hasSemanticScaleReferences } from "../../selectors/scales.js";
8
+ import { findUpstreamTransform } from "../../materialization/dataProvenance.js";
9
+ import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
10
+ import { resolveScalePreview } from "../scales/preview.js";
11
+ import { getPositionEncodingMaterializationSteps } from "../../materialization/marks/index.js";
12
+ import { resolveTarget, setEncodingProperties, validateOptions } from "./shared.js";
13
+
14
+ export function stackLayoutMode(stack) {
15
+ return stack === "normalize" ? "fill" : stack === "center" ? "center" : stack === null ? "overlay" : "stack";
16
+ }
17
+
18
+ export const layoutSeries = action({ op: "layoutSeries", description: "Assign series placement independently of color." }, function (args = {}) {
19
+ validateOptions(args, ["target", "mode"], "layoutSeries");
20
+ const mode = validateColorLayout(args.mode);
21
+ const { id: target, layer, dataset } = resolveTarget(this, args.target, ["bar", "area"], "series mark");
22
+ const encoding = Object.fromEntries(Object.entries(layer.encoding ?? {}).map(([key, value]) => [key, { ...value }]));
23
+ const group = encoding.group;
24
+ const color = encoding.color;
25
+ if (group === undefined && color?.field !== undefined && ["nominal", "ordinal"].includes(color.fieldType)) {
26
+ encoding.group = { field: color.field, fieldType: "nominal", inferredFrom: "color" };
27
+ }
28
+ for (const channel of ["x", "y"]) if (encoding[channel]) delete encoding[channel].stack;
29
+ if (color) delete color.layout;
30
+ const updated = { ...layer, layout: { mode }, encoding };
31
+ const density = findUpstreamTransform(this, dataset, "density");
32
+ const horizon = findUpstreamTransform(this, dataset, "horizon");
33
+ let endpoint, offset, offsetScale, offsetChannel;
34
+ if (layer.mark.type === "area") {
35
+ if (horizon !== undefined) throw new Error("Horizon owns its layout; layoutSeries is not supported.");
36
+ if (mode === "group") throw new Error("Area does not support group layout.");
37
+ if (density !== undefined) layoutDensityAreaSeries(deriveDensityAreaSeries(dataset.values, updated, density), mode);
38
+ else {
39
+ if (mode !== "overlay" && encoding.x2 === undefined && encoding.y2 === undefined && encoding.y?.fieldType === "quantitative") {
40
+ endpoint = { channel: "y2", datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
41
+ encoding.y2 = { datum: 0, fieldType: "quantitative", scale: encoding.y.scale };
42
+ }
43
+ if (encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Area layout requires complete positions.");
44
+ deriveAreaSeries(dataset.values, updated);
45
+ }
46
+ } else {
47
+ const channels = resolveBarChannels(updated), grain = resolveBarGrain(updated);
48
+ if (grain === undefined || encoding.x?.scale === undefined || encoding.y?.scale === undefined) throw new Error("Series layout requires a complete bar.");
49
+ if (mode === "center") throw new Error("Centered bars are not supported.");
50
+ if (grain === BAR_GRAINS.ranged && mode !== "overlay") throw new Error("Ranged bars support only overlay layout.");
51
+ if (grain === BAR_GRAINS.aggregate) deriveBarAggregates(dataset.values, updated);
52
+ if (mode === "group" && grain === BAR_GRAINS.aggregate) {
53
+ if (encoding.group === undefined) throw new Error("Grouped bars require a group encoding.");
54
+ readSeriesIdentity(dataset.values, updated);
55
+ offsetChannel = `${channels.category}Offset`;
56
+ offsetScale = resolveOffsetScaleDefinition(this, { id: encoding[offsetChannel]?.scale ?? offsetChannel }, offsetChannel);
57
+ offset = { ...(encoding.group.field === undefined ? {} : { field: encoding.group.field }), fieldType: encoding[offsetChannel]?.fieldType ?? "nominal", scale: offsetScale.id };
58
+ encoding[offsetChannel] = offset;
59
+ } else for (const channel of ["xOffset", "yOffset"]) delete encoding[channel];
60
+ }
61
+ const scales = offsetScale === undefined ? this.semanticSpec.scales
62
+ : [...this.semanticSpec.scales.filter(scale => scale.id !== offsetScale.id), offsetScale];
63
+ const preview = { ...this, markConfigs: this.markConfigs, canvasConfig: this.canvasConfig,
64
+ resolvedScales: { ...this.resolvedScales },
65
+ semanticSpec: { ...this.semanticSpec, scales,
66
+ layers: this.semanticSpec.layers.map(item => item.id === target ? updated : item) } };
67
+ for (const id of new Set([encoding.x?.scale, encoding.y?.scale, offset?.scale].filter(Boolean))) {
68
+ preview.resolvedScales[id] = resolveScalePreview(preview, id).resolvedScale;
69
+ }
70
+ let next = this.editSemantic({ property: `layer[${target}].layout.mode`, value: mode });
71
+ if (group === undefined && encoding.group !== undefined) next = setEncodingProperties(next, target, "group", encoding.group);
72
+ for (const channel of ["x", "y"]) if (Object.hasOwn(layer.encoding?.[channel] ?? {}, "stack")) {
73
+ next = next.editSemantic({ property: `layer[${target}].encoding.${channel}.stack`, remove: true });
74
+ }
75
+ if (color && Object.hasOwn(layer.encoding.color, "layout")) next = next.editSemantic({ property: `layer[${target}].encoding.color.layout`, remove: true });
76
+ if (endpoint !== undefined) {
77
+ next = next[endpoint.channel === "y2" ? "encodeY2" : "encodeX2"]({ target, datum: 0 });
78
+ }
79
+ if (layer.mark.type === "bar") {
80
+ for (const channel of ["xOffset", "yOffset"]) {
81
+ if (layer.encoding?.[channel] !== undefined) next = next.editSemantic({ property: `layer[${target}].encoding.${channel}`, remove: true });
82
+ if (channel !== offsetChannel) next = next._withoutMaterializationConfig(["marks", target, channel]);
83
+ }
84
+ if (offset !== undefined) {
85
+ next = offset.field === undefined
86
+ ? setEncodingProperties(next, target, offsetChannel, offset).createScale(offsetScale)
87
+ : next[offsetChannel === "xOffset" ? "encodeXOffset" : "encodeYOffset"]({ target, field: offset.field, fieldType: offset.fieldType, scale: offsetScale });
88
+ if (findSemanticScale(this, offsetScale.id) === undefined) next = next._withMarkConfig(target, { ...next.markConfigs[target], seriesOffsetScale: offsetScale.id });
89
+ } else {
90
+ const owned = next.markConfigs[target]?.seriesOffsetScale;
91
+ if (owned !== undefined) {
92
+ const referenced = hasSemanticScaleReferences(next.semanticSpec, owned);
93
+ if (!referenced) next = next.editSemantic({ property: `scale[${owned}]`, remove: true });
94
+ next = next._withoutMaterializationConfig(["marks", target, "seriesOffsetScale"]);
95
+ }
96
+ }
97
+ }
98
+ for (const step of getPositionEncodingMaterializationSteps(next, updated, encoding.y.scale)) next = next[step.op](step.args);
99
+ return next;
100
+ });
101
+
102
+ export function registerSeriesLayoutAction(ProgramClass) {
103
+ ProgramClass.prototype.layoutSeries = layoutSeries;
104
+ }