ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,3 +1,4 @@
1
+ import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
1
2
  import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
2
3
  import { findLayer } from "../../selectors/layers.js";
3
4
  import { findSemanticScale } from "../../selectors/scales.js";
@@ -296,39 +297,44 @@ export function clearCartesianPositions(program, id) {
296
297
  return next;
297
298
  }
298
299
 
299
- function rebindAxis(program, channel, {
300
+ function rebindAxis(program, channel, options) {
301
+ return cartesianAxisOwners(program).filter(owner => owner.channel === channel)
302
+ .reduce((next, owner) => rebindAxisOwner(next, channel, owner.id, options), program);
303
+ }
304
+
305
+ function rebindAxisOwner(program, channel, axisId, {
300
306
  previousScale,
301
307
  nextScale,
302
308
  previousTitle,
303
309
  nextTitle
304
310
  }) {
305
- const semantic = program.semanticSpec.guides.axis?.[channel];
311
+ const semantic = program.semanticSpec.guides.axis?.[axisId];
306
312
  if (semantic?.scale !== previousScale) return program;
307
313
  let next = program.editSemantic({
308
- property: `guide.axis.${channel}.scale`,
314
+ property: `guide.axis.${axisId}.scale`,
309
315
  value: nextScale
310
316
  });
311
317
  if (semantic.title === previousTitle) {
312
318
  next = next.editSemantic({
313
- property: `guide.axis.${channel}.title`,
319
+ property: `guide.axis.${axisId}.title`,
314
320
  value: nextTitle
315
321
  });
316
322
  }
317
323
  const scale = next.resolvedScales[nextScale];
318
324
  const ticks = normalizeTickConfig(
319
- next.guideConfigs.axis?.[channel]?.ticks,
325
+ next.guideConfigs.axis?.[axisId]?.ticks,
320
326
  scale
321
327
  );
322
328
  if (ticks !== undefined) {
323
- next = next._withGuideConfig(channel, "ticks", {
329
+ next = next._withGuideConfig(axisId, "ticks", {
324
330
  ...ticks,
325
331
  scale: nextScale
326
332
  });
327
333
  }
328
- const labels = next.guideConfigs.axis?.[channel]?.labels;
334
+ const labels = next.guideConfigs.axis?.[axisId]?.labels;
329
335
  if (labels !== undefined) {
330
- const tickConfig = next.guideConfigs.axis[channel].ticks;
331
- next = next._withGuideConfig(channel, "labels", {
336
+ const tickConfig = next.guideConfigs.axis[axisId].ticks;
337
+ next = next._withGuideConfig(axisId, "labels", {
332
338
  ...labels,
333
339
  scale: nextScale,
334
340
  mode: tickConfig.mode,
@@ -338,30 +344,30 @@ function rebindAxis(program, channel, {
338
344
  : { count: tickConfig.count })
339
345
  });
340
346
  if (tickConfig.mode === "values") {
341
- const config = next.guideConfigs.axis[channel].labels;
347
+ const config = next.guideConfigs.axis[axisId].labels;
342
348
  if (Object.hasOwn(config, "count")) {
343
349
  const { count: _count, ...withoutCount } = config;
344
- next = next._withGuideConfig(channel, "labels", withoutCount);
350
+ next = next._withGuideConfig(axisId, "labels", withoutCount);
345
351
  }
346
352
  } else {
347
- const config = next.guideConfigs.axis[channel].labels;
353
+ const config = next.guideConfigs.axis[axisId].labels;
348
354
  if (Object.hasOwn(config, "values")) {
349
355
  const { values: _values, ...withoutValues } = config;
350
- next = next._withGuideConfig(channel, "labels", withoutValues);
356
+ next = next._withGuideConfig(axisId, "labels", withoutValues);
351
357
  }
352
358
  }
353
359
  }
354
- const title = next.guideConfigs.axis?.[channel]?.title;
360
+ const title = next.guideConfigs.axis?.[axisId]?.title;
355
361
  if (title !== undefined) {
356
- next = next._withGuideConfig(channel, "title", {
362
+ next = next._withGuideConfig(axisId, "title", {
357
363
  ...title,
358
364
  scale: nextScale
359
365
  });
360
366
  }
361
367
  const methods = axisMethods(channel);
362
- if (ticks !== undefined) next = next[methods.ticks]();
363
- if (labels !== undefined) next = next[methods.labels]();
364
- if (title !== undefined) next = next[methods.title]();
368
+ if (ticks !== undefined) next = next[methods.ticks](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
369
+ if (labels !== undefined) next = next[methods.labels](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
370
+ if (title !== undefined) next = next[methods.title](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
365
371
  return next;
366
372
  }
367
373
 
@@ -1,9 +1,8 @@
1
+ import { canMaterializeBar } from "../../materialization/marks/index.js";
1
2
  import { action } from "../../core/action.js";
2
3
  import { validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
3
4
  import { assertEncodingSelectionCompatibility } from "../../materialization/selection/compatibility.js";
4
5
  import {
5
- readNominalField,
6
- readQuantitativeField,
7
6
  readScaleField,
8
7
  validateOpacityValue
9
8
  } from "../../grammar/scales/index.js";
@@ -42,11 +41,11 @@ function encodeAppearanceField(program, channel, args, operation) {
42
41
  ["point"],
43
42
  "point mark"
44
43
  );
45
- const expectedFieldType = channel === "shape" ? "nominal" : "quantitative";
44
+ const fieldTypes = channel === "shape" ? ["nominal"] : ["quantitative", "nominal", "ordinal"];
46
45
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
47
- const fieldType = args.fieldType ?? expectedFieldType;
48
- if (fieldType !== expectedFieldType) {
49
- throw new Error(`${operation} requires a ${expectedFieldType} field.`);
46
+ const fieldType = args.fieldType ?? fieldTypes[0];
47
+ if (!fieldTypes.includes(fieldType)) {
48
+ throw new Error(`${operation} requires a ${fieldTypes.join(" or ")} field.`);
50
49
  }
51
50
  if (channel === "size" && program.markConfigs[target]?.radius !== undefined) {
52
51
  throw new Error("encodeSize cannot be combined with a constant radius.");
@@ -59,17 +58,12 @@ function encodeAppearanceField(program, channel, args, operation) {
59
58
  const scale = resolveAppearanceScaleDefinition(
60
59
  program,
61
60
  channel,
62
- requestedScale
61
+ requestedScale,
62
+ fieldType
63
63
  );
64
- if (Object.hasOwn(scale, "unknown")) {
65
- readScaleField(dataset.values, args.field, fieldType, {
66
- allowUnknown: true
67
- });
68
- } else if (channel === "shape") {
69
- readNominalField(dataset.values, args.field);
70
- } else {
71
- readQuantitativeField(dataset.values, args.field);
72
- }
64
+ readScaleField(dataset.values, args.field, fieldType, {
65
+ allowUnknown: Object.hasOwn(scale, "unknown")
66
+ });
73
67
 
74
68
  let next = setEncodingProperties(program, target, channel, {
75
69
  field: args.field,
@@ -152,7 +146,7 @@ const removePointRadius = /* @__PURE__ */ action(
152
146
  const encodeSize = /* @__PURE__ */ action(
153
147
  {
154
148
  op: "encodeSize",
155
- description: "Encode a quantitative field as equal-area point size."
149
+ description: "Encode a field as equal-area point size."
156
150
  },
157
151
  function (args = {}) {
158
152
  return encodeAppearanceField(this, "size", args, "encodeSize");
@@ -184,6 +178,17 @@ const clearOpacityEncoding = /* @__PURE__ */ action(
184
178
  }
185
179
  );
186
180
 
181
+ function withoutConstantOpacity(program, target) {
182
+ const { opacity, ...config } = program.markConfigs[target] ?? {};
183
+ void opacity;
184
+ if (config.barAppearance?.opacity !== undefined) {
185
+ const { opacity: barOpacity, ...appearance } = config.barAppearance;
186
+ void barOpacity;
187
+ config.barAppearance = appearance;
188
+ }
189
+ return program._withMarkConfig(target, config);
190
+ }
191
+
187
192
  const encodeOpacity = /* @__PURE__ */ action(
188
193
  {
189
194
  op: "encodeOpacity",
@@ -202,14 +207,13 @@ const encodeOpacity = /* @__PURE__ */ action(
202
207
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
203
208
  this,
204
209
  args.target,
205
- ["point", "rule", "line"],
206
- "point, rule or line mark"
210
+ ["point", "rule", "line", "bar"],
211
+ "point, rule, line or bar mark"
207
212
  );
208
213
  if (hasValue) {
209
214
  validateOpacityValue(args.value, "encodeOpacity");
210
215
  assertEncodingSelectionCompatibility(this, target, ["opacity"]);
211
- const { opacity, ...config } = this.markConfigs[target] ?? {};
212
- void opacity;
216
+ const config = withoutConstantOpacity(this, target).markConfigs[target];
213
217
  const withoutLegend = this.guideConfigs.legend?.opacity?.target === target
214
218
  ? this.removeOpacityLegend()
215
219
  : this;
@@ -220,6 +224,11 @@ const encodeOpacity = /* @__PURE__ */ action(
220
224
  if (layer.mark.type === "line") {
221
225
  return rematerializeEncoding(next, target, "opacity", undefined, layer);
222
226
  }
227
+ if (layer.mark.type === "bar") {
228
+ return canMaterializeBar(next, findLayer(next, target))
229
+ ? applyDetachedScaleRematerialization(next.rematerializeBarMark({ id: target }), [layer])
230
+ : next;
231
+ }
223
232
  const materialized = layer.mark.type === "rule"
224
233
  ? next.rematerializeRuleMark({ id: target })
225
234
  : next.rematerializePointMark({ id: target });
@@ -244,17 +253,13 @@ const encodeOpacity = /* @__PURE__ */ action(
244
253
  ...layer, encoding: { ...layer.encoding, opacity: { field: args.field, fieldType } }
245
254
  });
246
255
  }
247
- if (Object.hasOwn(scale, "unknown")) {
248
- readScaleField(dataset.values, args.field, fieldType, {
249
- allowUnknown: true
250
- });
251
- } else {
252
- readQuantitativeField(dataset.values, args.field);
253
- }
256
+ readScaleField(dataset.values, args.field, fieldType, {
257
+ allowUnknown: Object.hasOwn(scale, "unknown")
258
+ });
254
259
  const { opacity, ...config } = this.markConfigs[target] ?? {};
255
260
  void opacity;
256
261
  let next = setEncodingProperties(
257
- this._withoutMaterializationConfig(["marks", target, "opacity"]),
262
+ withoutConstantOpacity(this, target),
258
263
  target,
259
264
  "opacity",
260
265
  { field: args.field, fieldType, scale: scale.id }
@@ -262,6 +267,9 @@ const encodeOpacity = /* @__PURE__ */ action(
262
267
  next = applyEncodingScale(next, scale, requestedScale, {
263
268
  reassignment: previous?.scale === scale.id
264
269
  });
270
+ if (layer.mark.type === "bar" && !canMaterializeBar(next, findLayer(next, target))) {
271
+ return next.rematerializeScale({ id: scale.id });
272
+ }
265
273
  return rematerializeEncoding(next, target, "opacity", scale.id, layer);
266
274
  }
267
275
  );
@@ -4,11 +4,33 @@ import {
4
4
  BAR_GRAINS,
5
5
  resolveBarColorLayout,
6
6
  resolveBarOffsetChannel,
7
+ resolveBarChannels,
7
8
  resolveBarGrain
8
9
  } from "../../grammar/bars/policy.js";
9
10
  import { normalizeBarWidth } from "../../grammar/bars/geometry.js";
11
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
12
+ import {
13
+ getScaleConsumerMarkSteps,
14
+ getSourceDependentMarkSteps
15
+ } from "../../materialization/marks/index.js";
16
+
17
+ function rematerializeWidth(program, target, layer, width) {
18
+ const next = program._withMarkConfig(target, {
19
+ ...program.markConfigs[target], barWidth: width
20
+ }).rematerializeBarMark({ id: target });
21
+ const category = resolveBarChannels(layer)?.category;
22
+ const encoding = layer.encoding?.[category];
23
+ if (encoding?.fieldType !== "temporal") return next;
24
+ const marks = getScaleConsumerMarkSteps(next, [encoding.scale])
25
+ .filter(step => step.args.id !== target);
26
+ return applyMaterializationPlan(next, [
27
+ ...marks,
28
+ ...[target, ...marks.map(step => step.args.id)]
29
+ .flatMap(id => getSourceDependentMarkSteps(next, id))
30
+ ]);
31
+ }
10
32
 
11
- const OPTIONS = Object.freeze(["band", "pixels", "target"]);
33
+ const OPTIONS = Object.freeze(["band", "pixels", "interval", "target"]);
12
34
 
13
35
  const encodeBarWidth = /* @__PURE__ */ action(
14
36
  {
@@ -26,22 +48,30 @@ const encodeBarWidth = /* @__PURE__ */ action(
26
48
  const layout = resolveBarColorLayout(layer);
27
49
  const width = normalizeBarWidth(
28
50
  args,
29
- this.markConfigs[target]?.barWidth
51
+ this.markConfigs[target]?.barWidth ?? (layer.mark.orientation === undefined ? undefined : { pixels: 5 })
30
52
  );
31
53
  const grouped = layout === "group";
32
54
  const offsetChannel = resolveBarOffsetChannel(layer);
33
55
  const grain = resolveBarGrain(layer);
56
+ if (layer.mark.orientation !== undefined && width.band !== undefined) {
57
+ throw new Error("Numeric-center bars require pixel width.");
58
+ }
34
59
  if (grain === undefined && this.markConfigs[target]?.boxPlot === undefined && (
35
60
  layer.encoding?.x === undefined || layer.encoding?.y === undefined
36
61
  )) {
37
62
  return this._withMarkConfig(target, { ...this.markConfigs[target], barWidth: width });
38
63
  }
64
+ if (width.interval !== undefined && grain !== undefined) {
65
+ const category = resolveBarChannels(layer)?.category;
66
+ if (grain !== BAR_GRAINS.aggregate || grouped || layer.encoding?.[category]?.fieldType !== "temporal") {
67
+ throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
68
+ }
69
+ }
39
70
  if (grain === BAR_GRAINS.histogram) {
40
71
  throw new Error("encodeBarWidth requires an aggregate or ranged category slot, not histogram bins.");
41
72
  }
42
- if (grain === BAR_GRAINS.ranged) {
43
- return this._withMarkConfig(target, { ...this.markConfigs[target], barWidth: width })
44
- .rematerializeBarMark({ id: target });
73
+ if ([BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)) {
74
+ return rematerializeWidth(this, target, layer, width);
45
75
  }
46
76
  if (
47
77
  grain !== BAR_GRAINS.aggregate ||
@@ -56,12 +86,7 @@ const encodeBarWidth = /* @__PURE__ */ action(
56
86
  );
57
87
  }
58
88
 
59
- return this
60
- ._withMarkConfig(target, {
61
- ...this.markConfigs[target],
62
- barWidth: width
63
- })
64
- .rematerializeBarMark({ id: target });
89
+ return rematerializeWidth(this, target, layer, width);
65
90
  }
66
91
  );
67
92
 
@@ -155,7 +155,6 @@ function resolveScaleOnly(program, id) {
155
155
  }
156
156
  const consumers = findScaleConsumers(program, id);
157
157
  if (
158
- semanticScale.radialMapping !== undefined &&
159
158
  consumers.length > 0 &&
160
159
  consumers.every(isPendingMeasuredRadiusConsumer)
161
160
  ) {
@@ -39,7 +39,7 @@ export function encodeContinuousColor(program, args) {
39
39
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
40
40
  program,
41
41
  args.target,
42
- ["point", "bar", "rect"],
42
+ ["point", "bar", "rect", "text"],
43
43
  "continuous color mark"
44
44
  );
45
45
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
@@ -63,7 +63,7 @@ const encodeColor = /* @__PURE__ */ action(
63
63
  const { id: target, dataset: sourceDataset, layer } = resolveTarget(
64
64
  this,
65
65
  args.target,
66
- ["point", "line", "bar", "area", "arc", "rect"],
66
+ ["point", "line", "bar", "area", "arc", "rect", "text"],
67
67
  "color mark"
68
68
  );
69
69
  const dataset = applyRequestedItemMissingPolicy(layer, sourceDataset, args.field);
@@ -20,6 +20,9 @@ export function resolveColorScaleOptions(args) {
20
20
  }
21
21
 
22
22
  export function assertNoConstantColor(program, layer) {
23
+ if (layer.mark.type === "text" && layer.source !== undefined) {
24
+ throw new Error("Source-owned text uses inheritColor or constant fill; field color requires row-backed text.");
25
+ }
23
26
  const config = program.markConfigs[layer.id];
24
27
  const hasConstant = {
25
28
  point: config?.fill !== undefined,
@@ -27,7 +30,8 @@ export function assertNoConstantColor(program, layer) {
27
30
  area: config?.errorBand?.fill !== undefined,
28
31
  bar: config?.barAppearance?.fill !== undefined,
29
32
  arc: config?.fill !== undefined,
30
- rect: config?.fillExplicit === true
33
+ rect: config?.fillExplicit === true,
34
+ text: config?.fillExplicit === true
31
35
  }[layer.mark.type] ?? false;
32
36
  if (hasConstant) {
33
37
  throw new Error(
@@ -58,15 +62,18 @@ export function resolveColorLayout(layer, requested, barGrain) {
58
62
  layer.mark.type === "bar"
59
63
  ? barGrain === BAR_GRAINS.histogram
60
64
  ? "stack"
61
- : barGrain === BAR_GRAINS.ranged ? "overlay" : "group"
65
+ : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(barGrain) ? "overlay" : "group"
62
66
  : layer.mark.type === "area"
63
67
  ? "overlay"
64
68
  : undefined
65
69
  );
66
70
 
67
- if (["point", "line", "rect"].includes(layer.mark.type) && layout !== undefined) {
71
+ if (["point", "line", "rect", "text"].includes(layer.mark.type) && layout !== undefined) {
68
72
  throw new Error(`Color layout is not supported for ${layer.mark.type} marks.`);
69
73
  }
74
+ if (barGrain === BAR_GRAINS.centered && layout !== "overlay") {
75
+ throw new Error("Numeric-center bars support only overlay layout.");
76
+ }
70
77
  if (layer.mark.type === "area" && layout === "group") {
71
78
  throw new Error('Area color layout does not support "group".');
72
79
  }
@@ -1,12 +1,7 @@
1
+ export { requireStringValue as requireDensityField } from "../../../core/validation.js";
1
2
  import { isPlainObject } from "../../../core/immutable.js";
2
3
  import { findSemanticScale } from "../../../selectors/scales.js";
3
4
 
4
- export function requireDensityField(value, label) {
5
- if (typeof value !== "string" || value.length === 0) {
6
- throw new TypeError(`${label} must be a non-empty string.`);
7
- }
8
- return value;
9
- }
10
5
 
11
6
  export function resolveDensityScaleOptions(value, defaults, label) {
12
7
  if (value !== undefined && !isPlainObject(value)) {
@@ -11,11 +11,6 @@ export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
11
11
  throw new Error(`Arc ${channel} encoding does not support stack.`);
12
12
  }
13
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
14
  if (args.mapping !== undefined) {
20
15
  validateRadialMapping(args.mapping);
21
16
  if (fieldType !== "quantitative" || !["count", "sum"].includes(args.aggregate)) {
@@ -30,7 +25,16 @@ export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
30
25
  return { ...emptyPositionPolicy(), aggregate: args.aggregate };
31
26
  }
32
27
  if (args.aggregate !== undefined) {
33
- throw new Error("Arc radius encoding does not support aggregate.");
28
+ if (!["count", "sum"].includes(args.aggregate)) throw new Error("Arc radius aggregate supports count or sum.");
29
+ const theta = layer.encoding?.theta;
30
+ if (theta !== undefined && !["count", "sum"].includes(theta.aggregate)) {
31
+ throw new Error("Grouped arc radius requires aggregated categorical theta.");
32
+ }
33
+ if (args.aggregate === "count" && Object.hasOwn(args, "field")) {
34
+ throw new Error("Arc radius count does not accept a field.");
35
+ }
36
+ if (args.weight !== undefined) throw new Error("Arc radius encoding does not support weight.");
37
+ return { ...emptyPositionPolicy(), aggregate: args.aggregate };
34
38
  }
35
39
  if (args.weight !== undefined) {
36
40
  throw new Error("Arc radius encoding does not support weight.");
@@ -38,10 +42,8 @@ export function resolveArcPositionPolicy({ channel, args, fieldType, layer }) {
38
42
  return emptyPositionPolicy();
39
43
  }
40
44
  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
+ if (layer.encoding?.radius?.aggregate !== undefined) {
46
+ throw new Error("Grouped arc radius requires aggregated categorical theta.");
45
47
  }
46
48
  if (args.aggregate !== undefined) {
47
49
  throw new Error("Quantitative arc theta does not support aggregate.");
@@ -26,16 +26,16 @@ export function resolveAreaPositionPolicy({
26
26
  const density = dataset.transform?.find(transform => transform.type === "density");
27
27
  if (["nominal", "ordinal"].includes(fieldType)) {
28
28
  if (
29
- density?.placement?.type !== "category" ||
29
+ density !== undefined && (density.placement?.type !== "category" ||
30
30
  density.placement.channel !== channel ||
31
- density.placement.categoryField !== field
31
+ density.placement.categoryField !== field)
32
32
  ) {
33
33
  throw new Error(
34
34
  "Categorical area position requires a matching category density placement."
35
35
  );
36
36
  }
37
37
  if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
38
- throw new Error("Categorical density position does not support aggregate, bin, or stack.");
38
+ throw new Error("Categorical area position does not support aggregate, bin, or stack.");
39
39
  }
40
40
  return emptyPositionPolicy();
41
41
  }
@@ -26,7 +26,15 @@ function resolveBarChannelPolicy({
26
26
  let stack;
27
27
  const xEncoding = layer.encoding?.x;
28
28
  const opposite = layer.encoding?.[channel === "x" ? "y" : "x"];
29
- const pendingBoxRange = program.markConfigs[layer.id]?.boxPlot !== undefined;
29
+ const pendingRange = program.markConfigs[layer.id]?.boxPlot !== undefined ||
30
+ layer.encoding?.[`${channel}2`] !== undefined;
31
+
32
+ if (layer.mark?.orientation !== undefined) {
33
+ if (fieldType !== "quantitative" || ["bin", "aggregate", "stack", "weight"].some(key => args[key] !== undefined)) {
34
+ throw new Error("Oriented numeric bars require raw quantitative positions without bin, aggregate, stack, or weight.");
35
+ }
36
+ return { bin, aggregate, stack };
37
+ }
30
38
 
31
39
  if (["nominal", "ordinal", "temporal"].includes(fieldType)) {
32
40
  if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
@@ -73,11 +81,11 @@ function resolveBarChannelPolicy({
73
81
  }
74
82
  aggregate = args.aggregate ?? (
75
83
  ["nominal", "ordinal", "temporal"].includes(opposite?.fieldType) &&
76
- !pendingBoxRange
84
+ !pendingRange
77
85
  ? "mean"
78
86
  : undefined
79
87
  );
80
- if (pendingBoxRange && args.aggregate === undefined) {
88
+ if (pendingRange && args.aggregate === undefined) {
81
89
  return { bin, aggregate, stack };
82
90
  }
83
91
  stack = Object.hasOwn(args, "stack") ? args.stack : opposite === undefined ? undefined : null;
@@ -106,7 +114,8 @@ export function resolveBarPositionPolicy(context) {
106
114
  const policy = resolveBarChannelPolicy(context);
107
115
  const oppositeChannel = channel === "x" ? "y" : "x";
108
116
  const opposite = layer.encoding?.[oppositeChannel];
109
- const pendingBoxRange = program.markConfigs[layer.id]?.boxPlot !== undefined;
117
+ const pendingRange = program.markConfigs[layer.id]?.boxPlot !== undefined ||
118
+ layer.encoding?.[`${channel}2`] !== undefined;
110
119
  const candidate = {
111
120
  ...layer,
112
121
  encoding: {
@@ -118,7 +127,7 @@ export function resolveBarPositionPolicy(context) {
118
127
  // Absence of an aggregate/stack is sufficient to represent an unresolved role.
119
128
  let companion;
120
129
  if (
121
- !pendingBoxRange && resolveBarGrain(layer) === undefined &&
130
+ !pendingRange && resolveBarGrain(layer) === undefined &&
122
131
  opposite?.fieldType === "quantitative" && opposite.bin === undefined
123
132
  ) {
124
133
  const resolved = resolveBarChannelPolicy({
@@ -138,7 +147,7 @@ export function resolveBarPositionPolicy(context) {
138
147
  }
139
148
  }
140
149
  const orientation = resolveBarOrientation(candidate);
141
- if (opposite !== undefined && orientation === undefined && !pendingBoxRange) {
150
+ if (opposite !== undefined && orientation === undefined && !pendingRange) {
142
151
  throw new Error(
143
152
  `Bar ${channel} encoding requires a quantitative field opposite a categorical position.`
144
153
  );
@@ -68,6 +68,13 @@ export function resolveLinePositionPolicy({
68
68
  stack: undefined
69
69
  };
70
70
  }
71
+ if (["nominal", "ordinal"].includes(fieldType)) {
72
+ if (layer.encoding?.y !== undefined && layer.encoding.y.aggregate === undefined &&
73
+ layer.encoding.y.fieldType !== "quantitative") {
74
+ throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");
75
+ }
76
+ return emptyPositionPolicy();
77
+ }
71
78
  const directPair =
72
79
  layer.encoding?.y?.aggregate === undefined &&
73
80
  ["quantitative", "temporal"].includes(layer.encoding?.y?.fieldType) &&
@@ -82,7 +89,7 @@ export function resolveLinePositionPolicy({
82
89
  )
83
90
  ) {
84
91
  throw new Error(
85
- "Line x encoding requires a temporal field or a compatible derived quantitative field."
92
+ "Line x encoding requires a categorical or temporal field, or a compatible quantitative field."
86
93
  );
87
94
  }
88
95
  return emptyPositionPolicy();
@@ -93,15 +100,19 @@ export function resolveLinePositionPolicy({
93
100
  if (layer.encoding?.x?.bin !== undefined && args.aggregate === undefined) {
94
101
  throw new Error("Binned line x encoding requires an aggregate y encoding.");
95
102
  }
96
- const quantitativePair =
103
+ if (["nominal", "ordinal"].includes(layer.encoding?.x?.fieldType) &&
104
+ args.aggregate === undefined && fieldType !== "quantitative") {
105
+ throw new Error("Categorical line x requires a quantitative value or aggregate y encoding.");
106
+ }
107
+ const continuousPair =
97
108
  fieldType === "quantitative" &&
98
- layer.encoding?.x?.fieldType === "quantitative" &&
109
+ ["quantitative", "temporal", "nominal", "ordinal"].includes(layer.encoding?.x?.fieldType) &&
99
110
  layer.encoding.x.bin === undefined;
100
111
  const prospectiveDirect =
101
112
  args.aggregate === undefined &&
102
113
  (fieldType === "temporal" ||
103
114
  (fieldType === "quantitative" &&
104
- (layer.encoding?.x === undefined || quantitativePair)));
115
+ (layer.encoding?.x === undefined || continuousPair)));
105
116
  if (interval || (windowOutput && args.aggregate === undefined) || prospectiveDirect) {
106
117
  if (!["quantitative", "temporal"].includes(fieldType)) {
107
118
  throw new Error(
@@ -125,12 +136,12 @@ export function resolveLinePositionPolicy({
125
136
 
126
137
  if (layer.encoding?.x !== undefined) {
127
138
  const compatibleAggregateX =
128
- layer.encoding.x.fieldType === "temporal" ||
139
+ ["temporal", "nominal", "ordinal"].includes(layer.encoding.x.fieldType) ||
129
140
  (layer.encoding.x.fieldType === "quantitative" &&
130
141
  layer.encoding.x.bin !== undefined);
131
142
  if (!compatibleAggregateX) {
132
143
  throw new Error(
133
- "Aggregate line y encoding requires a temporal or binned quantitative x encoding."
144
+ "Aggregate line y encoding requires a temporal, categorical, or binned quantitative x encoding."
134
145
  );
135
146
  }
136
147
  }
@@ -142,7 +142,8 @@ export function resolvePositionEncoding(program, channel, args, operation) {
142
142
  ? undefined
143
143
  : args.mapping ?? findSemanticScale(program, previous?.scale)?.radialMapping
144
144
  : undefined;
145
- const countRadius = mapping !== undefined && (args.aggregate ?? previous?.aggregate) === "count";
145
+ const countRadius = channel === "radius" && layer.mark.type === "arc" &&
146
+ !clearRadialMapping && (args.aggregate ?? previous?.aggregate) === "count";
146
147
  const requestedFieldType = args.fieldType ?? previous?.fieldType ?? (
147
148
  ["rule", "rect", "text", "point", "tick"].includes(layer.mark.type) && hasDatum
148
149
  ? inferDatumFieldType(args.datum, operation)
@@ -179,7 +180,7 @@ export function resolvePositionEncoding(program, channel, args, operation) {
179
180
  } else readScaleField(dataset.values, field, fieldType, { temporalUnit });
180
181
  }
181
182
  const effectiveArgs = { ...args };
182
- if (clearRadialMapping) delete effectiveArgs.mapping;
183
+ if (clearRadialMapping && args.aggregate === undefined) delete effectiveArgs.mapping;
183
184
  else if (mapping !== undefined) effectiveArgs.mapping = mapping;
184
185
  const directQuantitativeArcTheta =
185
186
  layer.mark.type === "arc" &&
@@ -127,6 +127,9 @@ function encodeSecondaryPosition(program, channel, args, operation, types) {
127
127
 
128
128
  let next = program;
129
129
  if (layer.mark.type === "bar") {
130
+ if (layer.mark.orientation !== undefined && primaryChannel !== (layer.mark.orientation === "vertical" ? "y" : "x")) {
131
+ throw new Error("Numeric bar endpoints must use the measure channel.");
132
+ }
130
133
  if (layer.layout?.mode !== undefined && layer.layout.mode !== "overlay") throw new Error("Ranged bars support only overlay layout; clear series layout before assigning endpoints.");
131
134
  for (const property of ["aggregate", "stack", "bin"]) {
132
135
  if (layer.encoding?.[primaryChannel]?.[property] !== undefined) {
@@ -210,6 +213,20 @@ function rangeAction(channel) {
210
213
  const { layer, dataset } = resolveTarget(this, args.target, ["area", "bar", "rect", "rule"], "ranged mark");
211
214
  let next = this;
212
215
  let lower = { field: args.lower }, upper = { field: args.upper };
216
+ if (layer.mark.type === "bar") {
217
+ if (layer.mark.orientation !== undefined && channel !== (layer.mark.orientation === "vertical" ? "y" : "x")) {
218
+ throw new Error("Numeric bar endpoints must use the measure channel.");
219
+ }
220
+ common.target = layer.id;
221
+ const resolved = resolvePositionEncoding(this, channel, {
222
+ ...common, ...lower,
223
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
224
+ ...(args.scale === undefined ? {} : { scale: args.scale })
225
+ }, op);
226
+ next = setEncodingProperties(next, layer.id, `${channel}2`, {
227
+ ...upper, fieldType: common.fieldType, scale: resolved.scale.id
228
+ });
229
+ }
213
230
  if (layer.mark.type === "area") {
214
231
  common.target = layer.id;
215
232
  const prepared = prepareAreaRange(this, layer, dataset, channel, args, common);