ggaction 0.0.11 → 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 (289) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +3 -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 +33 -27
  65. package/src/actions/encodings/parallel.js +2 -8
  66. package/src/actions/encodings/position/apply.js +40 -4
  67. package/src/actions/encodings/position/index.js +43 -5
  68. package/src/actions/encodings/position/policies/arc.js +37 -2
  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 +79 -24
  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 +153 -11
  83. package/src/actions/errorBars/edit.js +365 -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 +138 -14
  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 +60 -4
  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/core/vocabulary.js +2 -2
  195. package/src/grammar/aggregate.js +66 -23
  196. package/src/grammar/arcs.js +213 -65
  197. package/src/grammar/areaEndpoints.js +46 -0
  198. package/src/grammar/areaSeries.js +87 -41
  199. package/src/grammar/bars/aggregate.js +33 -12
  200. package/src/grammar/bars/policy.js +9 -7
  201. package/src/grammar/bin.js +181 -0
  202. package/src/grammar/categoryOrder.js +34 -0
  203. package/src/grammar/computed.js +150 -0
  204. package/src/grammar/ecdf.js +198 -0
  205. package/src/grammar/facets/index.js +117 -6
  206. package/src/grammar/fold.js +123 -0
  207. package/src/grammar/horizon.js +6 -4
  208. package/src/grammar/interval.js +37 -20
  209. package/src/grammar/lineSeries.js +20 -63
  210. package/src/grammar/markFilter.js +22 -4
  211. package/src/grammar/markLabels.js +93 -0
  212. package/src/grammar/parallelAxisTitles.js +16 -0
  213. package/src/grammar/pathSeries.js +91 -0
  214. package/src/grammar/pointPacking.js +183 -0
  215. package/src/grammar/positionCompatibility.js +1 -1
  216. package/src/grammar/positionDatum.js +12 -0
  217. package/src/grammar/rects.js +9 -1
  218. package/src/grammar/regression/derive.js +4 -0
  219. package/src/grammar/regression/index.js +7 -2
  220. package/src/grammar/regression/models.js +22 -7
  221. package/src/grammar/regression/parameters.js +40 -13
  222. package/src/grammar/rotation.js +29 -0
  223. package/src/grammar/rules.js +9 -21
  224. package/src/grammar/scales/color.js +23 -1
  225. package/src/grammar/scales/colorConsumers.js +38 -0
  226. package/src/grammar/scales/definition.js +15 -1
  227. package/src/grammar/scales/discretized.js +11 -5
  228. package/src/grammar/scales/fields.js +38 -8
  229. package/src/grammar/scales/mapping.js +2 -0
  230. package/src/grammar/scales/ordinal.js +23 -0
  231. package/src/grammar/scales/radial.js +55 -0
  232. package/src/grammar/scales/types.js +1 -0
  233. package/src/grammar/schemas/semanticPath.js +17 -3
  234. package/src/grammar/stack.js +170 -0
  235. package/src/grammar/statistics/confidenceInterval.js +101 -0
  236. package/src/grammar/summary.js +183 -0
  237. package/src/grammar/text.js +22 -16
  238. package/src/grammar/timeUnit.js +21 -11
  239. package/src/grammar/transformTopology.js +6 -0
  240. package/src/grammar/transforms.js +36 -0
  241. package/src/grammar/valueFormat.js +112 -0
  242. package/src/grammar/window.js +13 -7
  243. package/src/layout/facets.js +86 -23
  244. package/src/layout/guideCollisions.js +12 -0
  245. package/src/layout/legend.js +2 -10
  246. package/src/layout/legendItems.js +102 -0
  247. package/src/layout/legendLane.js +100 -11
  248. package/src/materialization/bars/aggregate.js +38 -152
  249. package/src/materialization/bars/histogram.js +24 -0
  250. package/src/materialization/categorySlotOffset.js +40 -0
  251. package/src/materialization/compositionSnapshot.js +1 -1
  252. package/src/materialization/dependencies.js +57 -16
  253. package/src/materialization/encodings.js +1 -1
  254. package/src/materialization/facetGuides/legacyCategorical.js +7 -1
  255. package/src/materialization/facetGuides/placement.js +16 -1
  256. package/src/materialization/facetGuides/preparation.js +26 -1
  257. package/src/materialization/facets.js +11 -2
  258. package/src/materialization/guides/layout.js +57 -0
  259. package/src/materialization/guides/resources.js +2 -2
  260. package/src/materialization/legends.js +10 -1
  261. package/src/materialization/marks/capabilities.js +30 -7
  262. package/src/materialization/marks/grouping.js +52 -0
  263. package/src/materialization/marks/index.js +63 -25
  264. package/src/materialization/marks/policies.js +2 -0
  265. package/src/materialization/rect.js +33 -33
  266. package/src/materialization/rowEncoding.js +70 -14
  267. package/src/materialization/scaleGuideDependencies.js +8 -5
  268. package/src/materialization/scales/map.js +1 -0
  269. package/src/materialization/scales/policies/arc.js +42 -0
  270. package/src/materialization/scales/policies/bar.js +0 -43
  271. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  272. package/src/materialization/scales/policies/offset.js +54 -0
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +48 -18
  275. package/src/materialization/selection/compatibility.js +11 -0
  276. package/src/materialization/selection/items/bar.js +12 -68
  277. package/src/materialization/selection/items/common.js +8 -2
  278. package/src/materialization/selection/items/path.js +3 -13
  279. package/src/materialization/selection/items/rect.js +7 -4
  280. package/src/materialization/text.js +64 -5
  281. package/src/mcp/adapter.js +2 -2
  282. package/src/selectors/datasets.js +13 -0
  283. package/src/selectors/layers.js +3 -2
  284. package/src/selectors/scales.js +6 -0
  285. package/src/theme/defaults.js +39 -0
  286. package/types/basic.d.ts +11 -2
  287. package/types/index.d.ts +126 -0
  288. package/types/program.d.ts +1327 -124
  289. 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,8 +1,9 @@
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";
4
5
 
5
- export function resolveArcPositionPolicy({ channel, args, fieldType }) {
6
+ export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
6
7
  if (args.bin !== undefined) {
7
8
  throw new Error(`Arc ${channel} encoding does not support bin.`);
8
9
  }
@@ -10,6 +11,24 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
10
11
  throw new Error(`Arc ${channel} encoding does not support stack.`);
11
12
  }
12
13
  if (channel === "radius") {
14
+ if (layer.encoding?.theta?.fieldType === "quantitative") {
15
+ throw new Error(
16
+ "Arc radius encoding cannot be combined with quantitative theta."
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
+ }
13
32
  if (args.aggregate !== undefined) {
14
33
  throw new Error("Arc radius encoding does not support aggregate.");
15
34
  }
@@ -18,8 +37,24 @@ export function resolveArcPositionPolicy({ channel, args, fieldType }) {
18
37
  }
19
38
  return emptyPositionPolicy();
20
39
  }
40
+ if (fieldType === "quantitative") {
41
+ if (layer.encoding?.radius !== undefined) {
42
+ throw new Error(
43
+ "Quantitative arc theta cannot be combined with radius encoding."
44
+ );
45
+ }
46
+ if (args.aggregate !== undefined) {
47
+ throw new Error("Quantitative arc theta does not support aggregate.");
48
+ }
49
+ if (args.weight !== undefined) {
50
+ throw new Error("Quantitative arc theta does not support weight.");
51
+ }
52
+ return emptyPositionPolicy();
53
+ }
21
54
  if (!["ordinal", "nominal"].includes(fieldType)) {
22
- throw new Error("Arc theta encoding requires an ordinal or nominal field.");
55
+ throw new Error(
56
+ "Arc theta encoding requires a quantitative, ordinal, or nominal field."
57
+ );
23
58
  }
24
59
  if (args.aggregate === undefined) {
25
60
  if (args.weight !== undefined) {
@@ -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,8 +25,11 @@ import {
21
25
  validateAggregateFieldType,
22
26
  validateAggregateFieldValues,
23
27
  } from "../../../grammar/aggregate.js";
24
- import { normalizeRuleDatum } from "../../../grammar/rules.js";
25
- import { readArcThetaWeights } from "../../../grammar/arcs.js";
28
+ import { normalizePositionDatum } from "../../../grammar/positionDatum.js";
29
+ import {
30
+ readArcThetaValues,
31
+ readArcThetaWeights
32
+ } from "../../../grammar/arcs.js";
26
33
  import { resolveMarkPositionPolicy } from "./policies/index.js";
27
34
  import {
28
35
  getPositionChannelDefinition,
@@ -31,10 +38,10 @@ import {
31
38
 
32
39
  const POSITION_ENCODING_OPTIONS = Object.freeze([
33
40
  "field", "datum", "target", "fieldType", "scale", "coordinate",
34
- "aggregate", "bin", "stack", "weight"
41
+ "aggregate", "bin", "stack", "weight", "temporalUnit", "mapping"
35
42
  ]);
36
43
 
37
- function inferRuleDatumFieldType(datum, operation) {
44
+ function inferDatumFieldType(datum, operation) {
38
45
  if (Number.isFinite(datum)) return "quantitative";
39
46
  if (isNominalValue(datum)) return "nominal";
40
47
  throw new Error(`${operation} datum requires an explicit fieldType.`);
@@ -90,29 +97,45 @@ export function resolvePositionEncoding(program, channel, args, operation) {
90
97
  args.target,
91
98
  getPositionChannelDefinition(channel).markTypes
92
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
+ }
93
103
  if (
94
104
  Object.hasOwn(args, "weight") &&
95
105
  !(layer.mark.type === "arc" && channel === "theta")
96
106
  ) {
97
107
  throw new Error(`${operation} weight is supported only for arc theta encoding.`);
98
108
  }
109
+ if (Object.hasOwn(args, "mapping") && (layer.mark.type !== "arc" || channel !== "radius")) {
110
+ throw new Error(`${operation} mapping requires Arc radius.`);
111
+ }
99
112
  validateCoordinateFamily(layer, channel, operation);
100
113
  const hasField = Object.hasOwn(args, "field");
101
114
  const hasDatum = Object.hasOwn(args, "datum");
102
- if (layer.mark.type === "rule") {
115
+ if (["rule", "rect", "text", "point", "tick"].includes(layer.mark.type)) {
103
116
  if (hasField === hasDatum) {
104
- 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.`);
105
118
  }
106
- if (hasField && args.fieldType === undefined) {
119
+ if (layer.mark.type === "rule" && hasField && args.fieldType === undefined) {
107
120
  throw new Error(`${operation} requires fieldType for a rule mark.`);
108
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
+ }
109
128
  } else if (hasDatum) {
110
129
  throw new Error(`${operation} does not support datum for a ${layer.mark.type} mark.`);
111
130
  }
112
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";
113
136
  const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
114
- layer.mark.type === "rule" && hasDatum
115
- ? inferRuleDatumFieldType(args.datum, operation)
137
+ ["rule", "rect", "text", "point", "tick"].includes(layer.mark.type) && hasDatum
138
+ ? inferDatumFieldType(args.datum, operation)
116
139
  : layer.mark.type === "arc" && channel === "theta" &&
117
140
  ["count", "sum"].includes(args.aggregate)
118
141
  ? "nominal"
@@ -127,10 +150,30 @@ export function resolvePositionEncoding(program, channel, args, operation) {
127
150
  ? xEncoding.field
128
151
  : args.field;
129
152
  const datum = args.datum;
130
- 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 }) };
169
+ const directQuantitativeArcTheta =
170
+ layer.mark.type === "arc" &&
171
+ channel === "theta" &&
172
+ fieldType === "quantitative";
131
173
  for (const property of ["aggregate", "bin", "stack"]) {
132
174
  if (!Object.hasOwn(effectiveArgs, property) && previous !== undefined &&
133
- Object.hasOwn(previous, property)) {
175
+ Object.hasOwn(previous, property) &&
176
+ !(directQuantitativeArcTheta && property === "aggregate")) {
134
177
  effectiveArgs[property] = previous[property];
135
178
  }
136
179
  }
@@ -151,11 +194,6 @@ export function resolvePositionEncoding(program, channel, args, operation) {
151
194
  fieldType,
152
195
  field
153
196
  });
154
- const usesField = layer.mark.type !== "rule" || hasField;
155
- if (usesField && (typeof field !== "string" || field.length === 0)) {
156
- throw new TypeError(`${operation} field must be a non-empty string.`);
157
- }
158
-
159
197
  const aggregateOutput = isAggregate(policy.aggregate) && !(
160
198
  layer.mark.type === "arc" && channel === "theta"
161
199
  );
@@ -168,11 +206,14 @@ export function resolvePositionEncoding(program, channel, args, operation) {
168
206
  channel,
169
207
  aggregateOutput ? "quantitative" : fieldType,
170
208
  requestedScale,
171
- layer.mark.type === "bar"
209
+ mapping !== undefined ? { radialMapping: mapping, zero: true, nice: false }
210
+ : layer.mark.type === "bar"
172
211
  ? program.markConfigs[target]?.boxPlot !== undefined && fieldType === "quantitative"
173
212
  ? { nice: true, zero: false }
174
213
  : fieldType === "quantitative"
175
- ? policy.bin !== undefined || policy.stack === null
214
+ ? policy.bin === undefined && policy.stack === undefined
215
+ ? { nice: true }
216
+ : policy.bin !== undefined || policy.stack === null
176
217
  ? { nice: true, zero: false }
177
218
  : { nice: true, zero: true }
178
219
  : fieldType === "temporal"
@@ -209,8 +250,13 @@ export function resolvePositionEncoding(program, channel, args, operation) {
209
250
  "Position scale unknown currently requires a row-owned point mark."
210
251
  );
211
252
  }
212
- if (layer.mark.type === "rule" && hasDatum) {
213
- 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.
214
260
  } else if (aggregateOutput) {
215
261
  validateAggregateFieldType(policy.aggregate, fieldType);
216
262
  validateAggregateFieldValues(dataset.values, field, fieldType);
@@ -220,6 +266,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
220
266
  policy.aggregate === "sum"
221
267
  ) {
222
268
  readArcThetaWeights(dataset.values, policy.weight, layer.id);
269
+ } else if (directQuantitativeArcTheta) {
270
+ readArcThetaValues(dataset.values, field, layer.id);
223
271
  } else if (program.markConfigs[target]?.boxPlot !== undefined) {
224
272
  for (const [index, row] of dataset.values.entries()) {
225
273
  const value = row[field];
@@ -229,14 +277,20 @@ export function resolvePositionEncoding(program, channel, args, operation) {
229
277
  }
230
278
  }
231
279
  } else if (layer.mark.type === "rect") {
232
- readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
280
+ readScaleField(dataset.values, field, fieldType, { allowUnknown: true, temporalUnit });
233
281
  } else if (Object.hasOwn(scale, "unknown")) {
234
- readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
235
- } 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);
236
284
  else if (["ordinal", "nominal"].includes(fieldType)) {
237
285
  readNominalField(dataset.values, field);
238
286
  } else readQuantitativeField(dataset.values, field);
239
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
+ }
240
294
  if (
241
295
  ["bar", "rect"].includes(layer.mark.type) &&
242
296
  ["ordinal", "nominal"].includes(fieldType) &&
@@ -248,11 +302,12 @@ export function resolvePositionEncoding(program, channel, args, operation) {
248
302
  target,
249
303
  layer,
250
304
  previous,
251
- requestedScale,
305
+ requestedScale: mapping === undefined ? requestedScale : { ...requestedScale, radialMapping: mapping },
252
306
  field,
253
307
  datum,
254
308
  hasField: usesField,
255
309
  fieldType,
310
+ temporalUnit,
256
311
  scale,
257
312
  coordinate: resolveCoordinate(program, channel, layer, args.coordinate),
258
313
  ...policy