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,12 +1,16 @@
1
+ import { stackLayoutMode } from "../seriesLayout.js";
2
+ import { resolveBarGrain } from "../../../grammar/bars/policy.js";
1
3
  import { action } from "../../../core/action.js";
2
4
  import {
3
5
  getPositionEncodingMaterializationSteps
4
6
  } from "../../../materialization/marks/index.js";
5
7
  import { resolvePositionEncoding } from "./resolve.js";
6
8
  import { findLayer } from "../../../selectors/layers.js";
9
+ import { findSemanticScale } from "../../../selectors/scales.js";
7
10
  import { applyPositionSemantics } from "./apply.js";
8
11
  import {
9
12
  applyEncodingScale,
13
+ applyDetachedScaleRematerialization,
10
14
  rebindPositionGuides
11
15
  } from "../shared.js";
12
16
 
@@ -20,12 +24,14 @@ function encodePosition(program, channel, args, operation) {
20
24
  datum,
21
25
  hasField,
22
26
  fieldType,
27
+ temporalUnit,
23
28
  scale,
24
29
  coordinate,
25
30
  bin,
26
31
  aggregate,
27
32
  stack,
28
- weight
33
+ weight,
34
+ companion
29
35
  } = resolvePositionEncoding(program, channel, args, operation);
30
36
 
31
37
  let next = program
@@ -43,6 +49,7 @@ function encodePosition(program, channel, args, operation) {
43
49
  datum,
44
50
  hasField,
45
51
  fieldType,
52
+ temporalUnit,
46
53
  bin,
47
54
  aggregate,
48
55
  stack,
@@ -54,8 +61,12 @@ function encodePosition(program, channel, args, operation) {
54
61
  value: scale.id
55
62
  });
56
63
  next = applyEncodingScale(next, scale, requestedScale, {
57
- reassignment: previous?.scale === scale.id
64
+ reassignment: previous?.scale === scale.id,
65
+ allowTypeChange: layer.mark.type === "area"
58
66
  });
67
+ if (["bar", "area"].includes(layer.mark.type) && Object.hasOwn(args, "stack") && args.stack !== undefined) {
68
+ next = next.editSemantic({ property: `layer[${target}].layout.mode`, value: stackLayoutMode(args.stack) });
69
+ }
59
70
  next = rebindPositionGuides(
60
71
  next,
61
72
  channel,
@@ -64,13 +75,30 @@ function encodePosition(program, channel, args, operation) {
64
75
  target
65
76
  );
66
77
 
78
+ if (companion !== undefined) {
79
+ const encoding = companion.encoding;
80
+ const storedScale = findSemanticScale(next, encoding.scale);
81
+ next = next[companion.channel === "x" ? "encodeX" : "encodeY"]({
82
+ target,
83
+ field: encoding.field,
84
+ fieldType: encoding.fieldType,
85
+ aggregate: encoding.aggregate,
86
+ ...(layer.layout?.mode === undefined ? {} : { stack: encoding.stack }),
87
+ scale: {
88
+ id: encoding.scale,
89
+ ...(storedScale?.zero === undefined ? { zero: encoding.stack !== null } : {})
90
+ }
91
+ });
92
+ }
93
+
67
94
  if (layer.mark.type === "bar") {
68
95
  const updated = findLayer(next, target);
69
96
  const pendingBox = next.markConfigs[target]?.boxPlot;
70
97
  if (pendingBox !== undefined && !pendingBox.materialized) {
71
- return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
98
+ const materialized = updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
72
99
  ? next.materializeBoxPlot({ id: target })
73
100
  : next;
101
+ return applyDetachedScaleRematerialization(materialized, [layer]);
74
102
  }
75
103
  }
76
104
 
@@ -78,13 +106,23 @@ function encodePosition(program, channel, args, operation) {
78
106
  const updated = findLayer(next, target);
79
107
  const pendingGradient = next.markConfigs[target]?.gradientPlot;
80
108
  if (pendingGradient !== undefined && !pendingGradient.materialized) {
81
- return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
109
+ const materialized = updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
82
110
  ? next.materializeGradientPlot({ id: target })
83
111
  : next;
112
+ return applyDetachedScaleRematerialization(materialized, [layer]);
84
113
  }
85
114
  }
86
115
 
87
116
  const updated = findLayer(next, target);
117
+ if (["bar", "area"].includes(layer.mark.type) && next.markConfigs[target]?.boxPlot === undefined &&
118
+ updated.encoding?.x?.scale !== undefined && updated.encoding?.y?.scale !== undefined) {
119
+ const requested = Object.hasOwn(args, "stack") ? args.stack : undefined;
120
+ const mode = requested !== undefined ? stackLayoutMode(requested) : updated.layout?.mode ??
121
+ (layer.mark.type === "bar" && resolveBarGrain(updated) === "histogram" ? stackLayoutMode(updated.encoding.y.stack) : undefined);
122
+ if (mode !== undefined && (layer.mark.type === "area" || resolveBarGrain(updated) !== undefined)) {
123
+ return applyDetachedScaleRematerialization(next.layoutSeries({ target, mode }), [layer]);
124
+ }
125
+ }
88
126
  for (const step of getPositionEncodingMaterializationSteps(
89
127
  next,
90
128
  updated,
@@ -92,7 +130,7 @@ function encodePosition(program, channel, args, operation) {
92
130
  )) {
93
131
  next = next[step.op](step.args);
94
132
  }
95
- return next;
133
+ return applyDetachedScaleRematerialization(next, [layer]);
96
134
  }
97
135
 
98
136
  const encodeX = action(
@@ -1,3 +1,4 @@
1
+ import { validateRadialMapping } from "../../../../grammar/scales/radial.js";
1
2
  import { validateAggregate } from "../../../../grammar/aggregate.js";
2
3
  import { validateNonEmptyString } from "../../../../core/validation.js";
3
4
  import { emptyPositionPolicy } from "./common.js";
@@ -15,6 +16,19 @@ export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
15
16
  "Arc radius encoding cannot be combined with quantitative theta."
16
17
  );
17
18
  }
19
+ if (args.mapping !== undefined) {
20
+ validateRadialMapping(args.mapping);
21
+ if (fieldType !== "quantitative" || !["count", "sum"].includes(args.aggregate)) {
22
+ throw new Error("Measured radius requires quantitative count or sum aggregation.");
23
+ }
24
+ if (args.aggregate === "count" && Object.hasOwn(args, "field")) {
25
+ throw new Error("Measured radius count does not accept a field.");
26
+ }
27
+ if (layer.encoding?.theta?.aggregate !== undefined) {
28
+ throw new Error("Measured radius requires equal-angle theta without aggregation.");
29
+ }
30
+ return { ...emptyPositionPolicy(), aggregate: args.aggregate };
31
+ }
18
32
  if (args.aggregate !== undefined) {
19
33
  throw new Error("Arc radius encoding does not support aggregate.");
20
34
  }
@@ -1,6 +1,4 @@
1
1
  import { validateStack, emptyPositionPolicy } from "./common.js";
2
- import { deriveCenteredAreaSeries } from "../../../../grammar/areaSeries.js";
3
- import { readQuantitativeField } from "../../../../grammar/scales/index.js";
4
2
 
5
3
  export function isCategoricalDensityPosition({
6
4
  layer,
@@ -50,44 +48,8 @@ export function resolveAreaPositionPolicy({
50
48
  throw new Error("Area position encoding does not support aggregate or bin.");
51
49
  }
52
50
  if (args.stack === undefined) return emptyPositionPolicy();
53
- const stack = validateStack(args.stack, "Area y encoding");
54
- if (stack === "center") {
55
- if (channel !== "y") {
56
- throw new Error("Area center stack requires a y encoding.");
57
- }
58
- if (layer.encoding?.group?.fieldType !== "nominal") {
59
- throw new Error("Area center stack requires a nominal group encoding.");
60
- }
61
- if (
62
- layer.encoding?.color?.layout !== undefined &&
63
- layer.encoding.color.layout !== "center"
64
- ) {
65
- throw new Error("Area center stack requires a matching center color layout.");
66
- }
67
- const values = readQuantitativeField(dataset.values, field);
68
- if (values.some(value => value < 0)) {
69
- throw new RangeError("Area center stack requires non-negative values.");
70
- }
71
- if (layer.encoding?.x !== undefined) {
72
- deriveCenteredAreaSeries(dataset.values, {
73
- ...layer,
74
- encoding: {
75
- ...layer.encoding,
76
- y: { field, fieldType, stack: "center" }
77
- }
78
- });
79
- }
80
- return { bin: undefined, aggregate: undefined, stack };
81
- }
82
- if (
83
- channel !== "y" ||
84
- density === undefined
85
- ) {
86
- throw new Error("Area stack currently requires a density y encoding.");
87
- }
88
- return {
89
- bin: undefined,
90
- aggregate: undefined,
91
- stack
92
- };
51
+ const stack = validateStack(args.stack, "Area position encoding");
52
+ if (fieldType !== "quantitative") throw new Error("Area stack requires a quantitative measure.");
53
+ if (stack === "center" && channel !== "y") throw new Error("Area center stack requires a y encoding.");
54
+ return { bin: undefined, aggregate: undefined, stack };
93
55
  }
@@ -4,12 +4,14 @@ import {
4
4
  } from "../../../../grammar/aggregate.js";
5
5
  import {
6
6
  BAR_ORIENTATIONS,
7
+ BAR_GRAINS,
8
+ resolveBarGrain,
7
9
  resolveBarOffsetChannel,
8
10
  resolveBarOrientation
9
11
  } from "../../../../grammar/bars/policy.js";
10
12
  import { resolveBin, validateStack } from "./common.js";
11
13
 
12
- export function resolveBarPositionPolicy({
14
+ function resolveBarChannelPolicy({
13
15
  program,
14
16
  layer,
15
17
  channel,
@@ -72,17 +74,12 @@ export function resolveBarPositionPolicy({
72
74
  if (pendingBoxRange && args.aggregate === undefined) {
73
75
  return { bin, aggregate, stack };
74
76
  }
75
- if (aggregate === undefined) {
76
- throw new Error(
77
- channel === "x"
78
- ? "Quantitative bar x encoding requires bin or aggregate."
79
- : "Bar y encoding requires a binned quantitative or ordinal x category, temporal x category, or aggregate."
80
- );
77
+ stack = Object.hasOwn(args, "stack") ? args.stack : opposite === undefined ? undefined : null;
78
+ if (aggregate !== undefined) {
79
+ aggregate = validateAggregate(aggregate);
80
+ validateAggregateFieldType(aggregate, fieldType);
81
81
  }
82
- stack = Object.hasOwn(args, "stack") ? args.stack : null;
83
- aggregate = validateAggregate(aggregate);
84
- validateAggregateFieldType(aggregate, fieldType);
85
- stack = validateStack(stack, `Bar ${channel} encoding`);
82
+ if (stack !== undefined) stack = validateStack(stack, `Bar ${channel} encoding`);
86
83
  if (stack === "center") {
87
84
  throw new Error("Centered bars are not supported.");
88
85
  }
@@ -92,13 +89,45 @@ export function resolveBarPositionPolicy({
92
89
  );
93
90
  }
94
91
 
92
+ return { bin, aggregate, stack };
93
+ }
94
+
95
+ export function resolveBarPositionPolicy(context) {
96
+ const { program, layer, channel, field, fieldType } = context;
97
+ const policy = resolveBarChannelPolicy(context);
98
+ const oppositeChannel = channel === "x" ? "y" : "x";
99
+ const opposite = layer.encoding?.[oppositeChannel];
100
+ const pendingBoxRange = program.markConfigs[layer.id]?.boxPlot !== undefined;
95
101
  const candidate = {
96
102
  ...layer,
97
103
  encoding: {
98
104
  ...layer.encoding,
99
- [channel]: { field, fieldType, bin, aggregate, stack }
105
+ [channel]: { field, fieldType, ...policy }
100
106
  }
101
107
  };
108
+ // The same channel policy owns a measure whether it is authored first or last.
109
+ // Absence of an aggregate/stack is sufficient to represent an unresolved role.
110
+ let companion;
111
+ if (
112
+ !pendingBoxRange && resolveBarGrain(layer) === undefined &&
113
+ opposite?.fieldType === "quantitative" && opposite.bin === undefined
114
+ ) {
115
+ const resolved = resolveBarChannelPolicy({
116
+ ...context,
117
+ layer: candidate,
118
+ channel: oppositeChannel,
119
+ field: opposite.field,
120
+ fieldType: opposite.fieldType,
121
+ args: opposite
122
+ });
123
+ candidate.encoding[oppositeChannel] = { ...opposite, ...resolved };
124
+ if (
125
+ (opposite.aggregate === undefined && resolved.aggregate !== undefined) ||
126
+ opposite.stack !== resolved.stack
127
+ ) {
128
+ companion = { channel: oppositeChannel, encoding: candidate.encoding[oppositeChannel] };
129
+ }
130
+ }
102
131
  const orientation = resolveBarOrientation(candidate);
103
132
  if (opposite !== undefined && orientation === undefined && !pendingBoxRange) {
104
133
  throw new Error(
@@ -116,5 +145,11 @@ export function resolveBarPositionPolicy({
116
145
  `${orientation} bars require ${expectedOffset}; remove the incompatible ${incompatibleOffset} encoding.`
117
146
  );
118
147
  }
119
- return { bin, aggregate, stack };
148
+ if (
149
+ program.markConfigs[layer.id]?.barWidth !== undefined &&
150
+ resolveBarGrain(candidate) === BAR_GRAINS.histogram
151
+ ) {
152
+ throw new Error("A saved Bar width requires an aggregate or ranged category slot and cannot be applied to histogram bins.");
153
+ }
154
+ return companion === undefined ? policy : { ...policy, companion };
120
155
  }
@@ -1,3 +1,6 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
2
+ import { findSemanticScale } from "../../../selectors/scales.js";
3
+ import { readAreaEndpoint } from "../../../grammar/areaEndpoints.js";
1
4
  import { validateUserId } from "../../../core/identifiers.js";
2
5
  import { getPositionCoordinateDefaults } from "../../../grammar/coordinates.js";
3
6
  import {
@@ -6,6 +9,7 @@ import {
6
9
  readQuantitativeField,
7
10
  readScaleField,
8
11
  readTemporalField,
12
+ resolveTemporalUnit,
9
13
  validateFieldType,
10
14
  validatePositionChannel
11
15
  } from "../../../grammar/scales/index.js";
@@ -21,7 +25,7 @@ import {
21
25
  validateAggregateFieldType,
22
26
  validateAggregateFieldValues,
23
27
  } from "../../../grammar/aggregate.js";
24
- import { normalizeRuleDatum } from "../../../grammar/rules.js";
28
+ import { normalizePositionDatum } from "../../../grammar/positionDatum.js";
25
29
  import {
26
30
  readArcThetaValues,
27
31
  readArcThetaWeights
@@ -34,10 +38,10 @@ import {
34
38
 
35
39
  const POSITION_ENCODING_OPTIONS = Object.freeze([
36
40
  "field", "datum", "target", "fieldType", "scale", "coordinate",
37
- "aggregate", "bin", "stack", "weight"
41
+ "aggregate", "bin", "stack", "weight", "temporalUnit", "mapping"
38
42
  ]);
39
43
 
40
- function inferRuleDatumFieldType(datum, operation) {
44
+ function inferDatumFieldType(datum, operation) {
41
45
  if (Number.isFinite(datum)) return "quantitative";
42
46
  if (isNominalValue(datum)) return "nominal";
43
47
  throw new Error(`${operation} datum requires an explicit fieldType.`);
@@ -93,29 +97,45 @@ export function resolvePositionEncoding(program, channel, args, operation) {
93
97
  args.target,
94
98
  getPositionChannelDefinition(channel).markTypes
95
99
  );
100
+ if (isSourceOwnedText(layer)) {
101
+ throw new Error(`${operation} cannot replace source-owned Text positions; edit the source, use dx/dy, or create independent Text with data.`);
102
+ }
96
103
  if (
97
104
  Object.hasOwn(args, "weight") &&
98
105
  !(layer.mark.type === "arc" && channel === "theta")
99
106
  ) {
100
107
  throw new Error(`${operation} weight is supported only for arc theta encoding.`);
101
108
  }
109
+ if (Object.hasOwn(args, "mapping") && (layer.mark.type !== "arc" || channel !== "radius")) {
110
+ throw new Error(`${operation} mapping requires Arc radius.`);
111
+ }
102
112
  validateCoordinateFamily(layer, channel, operation);
103
113
  const hasField = Object.hasOwn(args, "field");
104
114
  const hasDatum = Object.hasOwn(args, "datum");
105
- if (layer.mark.type === "rule") {
115
+ if (["rule", "rect", "text", "point", "tick"].includes(layer.mark.type)) {
106
116
  if (hasField === hasDatum) {
107
- throw new Error(`${operation} requires exactly one of field or datum for a rule mark.`);
117
+ throw new Error(`${operation} requires exactly one of field or datum for a ${layer.mark.type} mark.`);
108
118
  }
109
- if (hasField && args.fieldType === undefined) {
119
+ if (layer.mark.type === "rule" && hasField && args.fieldType === undefined) {
110
120
  throw new Error(`${operation} requires fieldType for a rule mark.`);
111
121
  }
122
+ } else if (layer.mark.type === "area") {
123
+ if (hasField === hasDatum) throw new Error(`${operation} requires exactly one of field or datum for an area mark.`);
124
+ if (hasDatum && (!Number.isFinite(args.datum) || (args.fieldType ?? "quantitative") !== "quantitative" ||
125
+ ["aggregate", "bin", "stack", "temporalUnit"].some(key => Object.hasOwn(args, key)))) {
126
+ throw new Error("Area datum requires a finite quantitative constant without aggregate, bin, stack, or temporalUnit.");
127
+ }
112
128
  } else if (hasDatum) {
113
129
  throw new Error(`${operation} does not support datum for a ${layer.mark.type} mark.`);
114
130
  }
115
131
  const previous = layer.encoding?.[channel];
132
+ const mapping = channel === "radius" && layer.mark.type === "arc"
133
+ ? args.mapping ?? findSemanticScale(program, previous?.scale)?.radialMapping
134
+ : undefined;
135
+ const countRadius = mapping !== undefined && (args.aggregate ?? previous?.aggregate) === "count";
116
136
  const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
117
- layer.mark.type === "rule" && hasDatum
118
- ? inferRuleDatumFieldType(args.datum, operation)
137
+ ["rule", "rect", "text", "point", "tick"].includes(layer.mark.type) && hasDatum
138
+ ? inferDatumFieldType(args.datum, operation)
119
139
  : layer.mark.type === "arc" && channel === "theta" &&
120
140
  ["count", "sum"].includes(args.aggregate)
121
141
  ? "nominal"
@@ -130,7 +150,22 @@ export function resolvePositionEncoding(program, channel, args, operation) {
130
150
  ? xEncoding.field
131
151
  : args.field;
132
152
  const datum = args.datum;
133
- const effectiveArgs = { ...args };
153
+ const temporalUnit = resolveTemporalUnit({ ...args, ...(hasDatum ? {} : { field }) }, fieldType, previous);
154
+ const usesField = !countRadius && (!["rule", "area", "rect", "text", "point", "tick"].includes(layer.mark.type) || hasField);
155
+ if (usesField && (typeof field !== "string" || field.length === 0)) {
156
+ throw new TypeError(`${operation} field must be a non-empty string.`);
157
+ }
158
+ // A missing field is invalid even before the Bar's measure/category role is known.
159
+ if (layer.mark.type === "bar" && program.markConfigs[target]?.boxPlot === undefined) {
160
+ if (fieldType === "quantitative" && dataset.values.length > 0 &&
161
+ !dataset.values.some(row => Object.hasOwn(row, field))) {
162
+ throw new Error(`${operation} field "${field}" does not exist in the dataset.`);
163
+ }
164
+ if (fieldType === "quantitative") {
165
+ validateAggregateFieldValues(dataset.values, field, fieldType);
166
+ } else readScaleField(dataset.values, field, fieldType, { temporalUnit });
167
+ }
168
+ const effectiveArgs = { ...args, ...(mapping === undefined ? {} : { mapping }) };
134
169
  const directQuantitativeArcTheta =
135
170
  layer.mark.type === "arc" &&
136
171
  channel === "theta" &&
@@ -159,11 +194,6 @@ export function resolvePositionEncoding(program, channel, args, operation) {
159
194
  fieldType,
160
195
  field
161
196
  });
162
- const usesField = layer.mark.type !== "rule" || hasField;
163
- if (usesField && (typeof field !== "string" || field.length === 0)) {
164
- throw new TypeError(`${operation} field must be a non-empty string.`);
165
- }
166
-
167
197
  const aggregateOutput = isAggregate(policy.aggregate) && !(
168
198
  layer.mark.type === "arc" && channel === "theta"
169
199
  );
@@ -176,11 +206,14 @@ export function resolvePositionEncoding(program, channel, args, operation) {
176
206
  channel,
177
207
  aggregateOutput ? "quantitative" : fieldType,
178
208
  requestedScale,
179
- layer.mark.type === "bar"
209
+ mapping !== undefined ? { radialMapping: mapping, zero: true, nice: false }
210
+ : layer.mark.type === "bar"
180
211
  ? program.markConfigs[target]?.boxPlot !== undefined && fieldType === "quantitative"
181
212
  ? { nice: true, zero: false }
182
213
  : fieldType === "quantitative"
183
- ? policy.bin !== undefined || policy.stack === null
214
+ ? policy.bin === undefined && policy.stack === undefined
215
+ ? { nice: true }
216
+ : policy.bin !== undefined || policy.stack === null
184
217
  ? { nice: true, zero: false }
185
218
  : { nice: true, zero: true }
186
219
  : fieldType === "temporal"
@@ -217,8 +250,13 @@ export function resolvePositionEncoding(program, channel, args, operation) {
217
250
  "Position scale unknown currently requires a row-owned point mark."
218
251
  );
219
252
  }
220
- if (layer.mark.type === "rule" && hasDatum) {
221
- normalizeRuleDatum(datum, fieldType, channel);
253
+ if (layer.mark.type === "area" && fieldType === "quantitative") {
254
+ readAreaEndpoint(dataset.values, { ...(hasDatum ? { datum } : { field }), fieldType }, layer.mark.missing);
255
+ } else if (["rule", "rect", "text", "point", "tick"].includes(layer.mark.type) && hasDatum) {
256
+ const mark = layer.mark.type[0].toUpperCase() + layer.mark.type.slice(1);
257
+ normalizePositionDatum(datum, fieldType, channel, temporalUnit, mark);
258
+ } else if (countRadius) {
259
+ // Count has no source measure field. Category-grain validation runs once theta exists.
222
260
  } else if (aggregateOutput) {
223
261
  validateAggregateFieldType(policy.aggregate, fieldType);
224
262
  validateAggregateFieldValues(dataset.values, field, fieldType);
@@ -239,14 +277,20 @@ export function resolvePositionEncoding(program, channel, args, operation) {
239
277
  }
240
278
  }
241
279
  } else if (layer.mark.type === "rect") {
242
- readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
280
+ readScaleField(dataset.values, field, fieldType, { allowUnknown: true, temporalUnit });
243
281
  } else if (Object.hasOwn(scale, "unknown")) {
244
- readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
245
- } else if (fieldType === "temporal") readTemporalField(dataset.values, field);
282
+ readScaleField(dataset.values, field, fieldType, { allowUnknown: true, temporalUnit });
283
+ } else if (fieldType === "temporal") readTemporalField(dataset.values, field, temporalUnit);
246
284
  else if (["ordinal", "nominal"].includes(fieldType)) {
247
285
  readNominalField(dataset.values, field);
248
286
  } else readQuantitativeField(dataset.values, field);
249
287
 
288
+ if (mapping !== undefined) {
289
+ const values = countRadius ? dataset.values.map(() => 1) : readQuantitativeField(dataset.values, field);
290
+ if (values.length === 0 || values.some(value => value < 0) || !values.some(value => value > 0)) {
291
+ throw new Error("Measured radius requires non-negative values and a positive aggregate.");
292
+ }
293
+ }
250
294
  if (
251
295
  ["bar", "rect"].includes(layer.mark.type) &&
252
296
  ["ordinal", "nominal"].includes(fieldType) &&
@@ -258,11 +302,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
258
302
  target,
259
303
  layer,
260
304
  previous,
261
- requestedScale,
305
+ requestedScale: mapping === undefined ? requestedScale : { ...requestedScale, radialMapping: mapping },
262
306
  field,
263
307
  datum,
264
308
  hasField: usesField,
265
309
  fieldType,
310
+ temporalUnit,
266
311
  scale,
267
312
  coordinate: resolveCoordinate(program, channel, layer, args.coordinate),
268
313
  ...policy