ggaction 0.0.12 → 0.0.15

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 (378) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +20 -11
  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 +10 -3
  11. package/src/actions/basic.js +4 -2
  12. package/src/actions/boxPlots/components.js +14 -4
  13. package/src/actions/boxPlots/edit.js +31 -5
  14. package/src/actions/boxPlots/materialize.js +13 -4
  15. package/src/actions/boxPlots/options.js +48 -16
  16. package/src/actions/canvas/actions.js +3 -1
  17. package/src/actions/canvas/fitting.js +211 -0
  18. package/src/actions/canvas/index.js +2 -0
  19. package/src/actions/categoryOrder/index.js +18 -6
  20. package/src/actions/charts/area.js +51 -0
  21. package/src/actions/charts/bar.js +13 -6
  22. package/src/actions/charts/beeswarm.js +70 -0
  23. package/src/actions/charts/density.js +48 -0
  24. package/src/actions/charts/ecdf.js +186 -0
  25. package/src/actions/charts/endpoints.js +473 -0
  26. package/src/actions/charts/heatmap.js +4 -3
  27. package/src/actions/charts/histogram.js +5 -4
  28. package/src/actions/charts/horizon.js +73 -0
  29. package/src/actions/charts/index.js +37 -0
  30. package/src/actions/charts/interval-regression.js +172 -0
  31. package/src/actions/charts/line.js +8 -6
  32. package/src/actions/charts/parallel.js +8 -2
  33. package/src/actions/charts/pie.js +52 -0
  34. package/src/actions/charts/polar.js +134 -0
  35. package/src/actions/charts/radar.js +305 -0
  36. package/src/actions/charts/radial.js +50 -0
  37. package/src/actions/charts/raincloud.js +422 -0
  38. package/src/actions/charts/rug-strip.js +283 -0
  39. package/src/actions/charts/scatter.js +11 -3
  40. package/src/actions/charts/shared.js +69 -10
  41. package/src/actions/composition/actions.js +129 -3
  42. package/src/actions/coordinates/actions.js +1 -1
  43. package/src/actions/coordinates/edit.js +76 -0
  44. package/src/actions/data/bin.js +29 -0
  45. package/src/actions/data/bin2d.js +57 -185
  46. package/src/actions/data/complete.js +26 -0
  47. package/src/actions/data/computed.js +24 -0
  48. package/src/actions/data/create.js +2 -2
  49. package/src/actions/data/density.js +12 -20
  50. package/src/actions/data/derived.js +111 -15
  51. package/src/actions/data/ecdf.js +38 -0
  52. package/src/actions/data/edit.js +539 -0
  53. package/src/actions/data/filter.js +226 -30
  54. package/src/actions/data/fold.js +24 -0
  55. package/src/actions/data/impute.js +26 -0
  56. package/src/actions/data/index.js +38 -0
  57. package/src/actions/data/interval.js +2 -0
  58. package/src/actions/data/intervalEdit.js +86 -4
  59. package/src/actions/data/normalize.js +27 -0
  60. package/src/actions/data/regression.js +19 -21
  61. package/src/actions/data/revisionBindings.js +145 -0
  62. package/src/actions/data/shared.js +20 -4
  63. package/src/actions/data/stack.js +26 -0
  64. package/src/actions/data/summary.js +26 -0
  65. package/src/actions/data/timeUnit.js +4 -1
  66. package/src/actions/data/window.js +1 -1
  67. package/src/actions/encodings/appearance.js +32 -13
  68. package/src/actions/encodings/barWidth.js +8 -0
  69. package/src/actions/encodings/basic.js +2 -0
  70. package/src/actions/encodings/channels.js +415 -0
  71. package/src/actions/encodings/color/continuous.js +19 -59
  72. package/src/actions/encodings/color/index.js +30 -23
  73. package/src/actions/encodings/color/layout.js +3 -97
  74. package/src/actions/encodings/color/policy.js +5 -8
  75. package/src/actions/encodings/density/resolve.js +37 -5
  76. package/src/actions/encodings/density.js +124 -34
  77. package/src/actions/encodings/histogram.js +3 -2
  78. package/src/actions/encodings/horizon/resolve.js +8 -4
  79. package/src/actions/encodings/horizon.js +18 -8
  80. package/src/actions/encodings/index.js +6 -0
  81. package/src/actions/encodings/offset.js +30 -29
  82. package/src/actions/encodings/parallel.js +2 -8
  83. package/src/actions/encodings/position/apply.js +63 -11
  84. package/src/actions/encodings/position/index.js +52 -6
  85. package/src/actions/encodings/position/policies/arc.js +14 -0
  86. package/src/actions/encodings/position/policies/area.js +4 -42
  87. package/src/actions/encodings/position/policies/bar.js +57 -13
  88. package/src/actions/encodings/position/resolve.js +88 -26
  89. package/src/actions/encodings/ranged.js +123 -97
  90. package/src/actions/encodings/remove.js +44 -34
  91. package/src/actions/encodings/ruleAppearance.js +24 -32
  92. package/src/actions/encodings/seriesLayout.js +105 -0
  93. package/src/actions/encodings/shared.js +107 -37
  94. package/src/actions/encodings/stroke.js +226 -0
  95. package/src/actions/encodings/strokeDash.js +9 -6
  96. package/src/actions/encodings/temporal.js +7 -0
  97. package/src/actions/encodings/text.js +30 -15
  98. package/src/actions/errorBands/create.js +41 -8
  99. package/src/actions/errorBands/edit.js +360 -25
  100. package/src/actions/errorBars/create.js +56 -17
  101. package/src/actions/errorBars/edit.js +372 -21
  102. package/src/actions/facets/actions.js +619 -41
  103. package/src/actions/facets/derive.js +139 -25
  104. package/src/actions/gradientPlots/components.js +43 -13
  105. package/src/actions/gradientPlots/edit.js +14 -4
  106. package/src/actions/gradientPlots/materialize.js +8 -8
  107. package/src/actions/gradientPlots/options.js +10 -2
  108. package/src/actions/guides/applicability.js +28 -19
  109. package/src/actions/guides/axes/axes.js +4 -3
  110. package/src/actions/guides/axes/axis.js +18 -18
  111. package/src/actions/guides/axes/components.js +43 -0
  112. package/src/actions/guides/axes/edit.js +16 -51
  113. package/src/actions/guides/axes/labels.js +184 -28
  114. package/src/actions/guides/axes/lines.js +13 -7
  115. package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
  116. package/src/actions/guides/axes/parallel/policy.js +137 -0
  117. package/src/actions/guides/axes/parallel/resolve.js +50 -25
  118. package/src/actions/guides/axes/parallel.js +93 -217
  119. package/src/actions/guides/axes/policy.js +21 -67
  120. package/src/actions/guides/axes/tickGroups.js +15 -8
  121. package/src/actions/guides/axes/ticks.js +11 -6
  122. package/src/actions/guides/axes/titles.js +32 -11
  123. package/src/actions/guides/basic.js +2 -0
  124. package/src/actions/guides/facade.js +106 -0
  125. package/src/actions/guides/facadeAxes.js +200 -0
  126. package/src/actions/guides/facadeLegend.js +63 -0
  127. package/src/actions/guides/grids/grid.js +37 -33
  128. package/src/actions/guides/grids/resolve.js +17 -4
  129. package/src/actions/guides/guides.js +54 -31
  130. package/src/actions/guides/legends/blocks.js +426 -0
  131. package/src/actions/guides/legends/categorical/actions.js +309 -279
  132. package/src/actions/guides/legends/categorical/components.js +21 -17
  133. package/src/actions/guides/legends/categorical/index.js +37 -33
  134. package/src/actions/guides/legends/categorical/layout.js +123 -354
  135. package/src/actions/guides/legends/categorical/options.js +32 -16
  136. package/src/actions/guides/legends/categorical/recipes.js +114 -9
  137. package/src/actions/guides/legends/categorical/resolve.js +30 -15
  138. package/src/actions/guides/legends/categorical/symbols.js +87 -33
  139. package/src/actions/guides/legends/continuous/common.js +99 -26
  140. package/src/actions/guides/legends/continuous/gradient.js +120 -89
  141. package/src/actions/guides/legends/continuous/index.js +8 -2
  142. package/src/actions/guides/legends/continuous/interval.js +79 -95
  143. package/src/actions/guides/legends/continuous/opacity.js +113 -89
  144. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  145. package/src/actions/guides/legends/creation.js +40 -0
  146. package/src/actions/guides/legends/edit.js +321 -90
  147. package/src/actions/guides/legends/focused.js +2 -2
  148. package/src/actions/guides/legends/index.js +2 -0
  149. package/src/actions/guides/legends/lane.js +149 -102
  150. package/src/actions/guides/legends/lifecycle.js +143 -0
  151. package/src/actions/guides/legends/remove.js +43 -78
  152. package/src/actions/guides/legends/sampling.js +140 -0
  153. package/src/actions/guides/legends/size.js +213 -116
  154. package/src/actions/guides/legends/strokeWidth.js +121 -73
  155. package/src/actions/guides/legends/target.js +123 -0
  156. package/src/actions/guides/legends/transition.js +192 -0
  157. package/src/actions/guides/polar/axes/facade.js +81 -66
  158. package/src/actions/guides/polar/axes/labels.js +28 -8
  159. package/src/actions/guides/polar/axes/lines.js +12 -5
  160. package/src/actions/guides/polar/axes/shared.js +22 -0
  161. package/src/actions/guides/polar/axes/ticks.js +6 -5
  162. package/src/actions/guides/polar/axes/titles.js +6 -5
  163. package/src/actions/guides/polar/grids.js +8 -8
  164. package/src/actions/guides/polar/resolve.js +12 -8
  165. package/src/actions/guides/reuse.js +44 -0
  166. package/src/actions/guides/tickValues.js +19 -21
  167. package/src/actions/index.js +15 -2
  168. package/src/actions/intervals/resolve.js +35 -15
  169. package/src/actions/marks/arc/actions.js +78 -20
  170. package/src/actions/marks/area/actions.js +51 -12
  171. package/src/actions/marks/area/materialize.js +22 -3
  172. package/src/actions/marks/bar/create.js +11 -2
  173. package/src/actions/marks/bar/edit.js +20 -3
  174. package/src/actions/marks/bar/materialize.js +33 -9
  175. package/src/actions/marks/index.js +14 -0
  176. package/src/actions/marks/line/actions.js +71 -113
  177. package/src/actions/marks/line/materialize.js +57 -56
  178. package/src/actions/marks/point/create.js +8 -2
  179. package/src/actions/marks/point/edit.js +29 -21
  180. package/src/actions/marks/point/index.js +1 -0
  181. package/src/actions/marks/point/jitter.js +3 -0
  182. package/src/actions/marks/point/materialize.js +154 -16
  183. package/src/actions/marks/point/packing.js +68 -0
  184. package/src/actions/marks/rect/actions.js +33 -12
  185. package/src/actions/marks/references.js +658 -0
  186. package/src/actions/marks/remove.js +87 -4
  187. package/src/actions/marks/rule/actions.js +77 -9
  188. package/src/actions/marks/rule/appearance.js +26 -0
  189. package/src/actions/marks/shared.js +13 -13
  190. package/src/actions/marks/text/actions.js +296 -13
  191. package/src/actions/marks/text/index.js +4 -0
  192. package/src/actions/marks/text/layout.js +156 -8
  193. package/src/actions/marks/text/removal.js +173 -0
  194. package/src/actions/marks/text/selection.js +73 -0
  195. package/src/actions/marks/tick/actions.js +22 -5
  196. package/src/actions/primitives/createGraphics.js +3 -1
  197. package/src/actions/primitives/editGraphics.js +26 -0
  198. package/src/actions/primitives/semantic.js +3 -2
  199. package/src/actions/primitives/semanticAction.js +120 -10
  200. package/src/actions/primitives/semanticValidation/guide.js +10 -0
  201. package/src/actions/primitives/semanticValidation/index.js +9 -1
  202. package/src/actions/primitives/semanticValidation/layer.js +23 -1
  203. package/src/actions/primitives/semanticValidation/scale.js +38 -8
  204. package/src/actions/regression/components.js +26 -11
  205. package/src/actions/regression/create.js +11 -3
  206. package/src/actions/regression/edit.js +40 -13
  207. package/src/actions/regression/resolve.js +1 -1
  208. package/src/actions/resources/index.js +7 -0
  209. package/src/actions/resources/remove.js +172 -0
  210. package/src/actions/scales/channels.js +212 -0
  211. package/src/actions/scales/consumers/common.js +21 -54
  212. package/src/actions/scales/consumers/families.js +28 -14
  213. package/src/actions/scales/consumers/index.js +42 -4
  214. package/src/actions/scales/consumers/seriesLayout.js +57 -12
  215. package/src/actions/scales/create.js +58 -26
  216. package/src/actions/scales/definitions.js +109 -39
  217. package/src/actions/scales/edit.js +101 -34
  218. package/src/actions/scales/editPolicy.js +95 -35
  219. package/src/actions/scales/index.js +6 -0
  220. package/src/actions/scales/materialize.js +14 -49
  221. package/src/actions/scales/offset.js +48 -0
  222. package/src/actions/scales/parallel.js +71 -0
  223. package/src/actions/scales/patch.js +1 -1
  224. package/src/actions/scales/preview.js +140 -0
  225. package/src/actions/scales/quantitativeColor.js +9 -5
  226. package/src/actions/selection/actions.js +154 -30
  227. package/src/actions/selection/index.js +3 -0
  228. package/src/actions/theme/actions.js +98 -0
  229. package/src/actions/theme/composition.js +238 -0
  230. package/src/actions/theme/index.js +16 -0
  231. package/src/actions/theme/reconcile.js +1130 -0
  232. package/src/actions/theme/state.js +160 -0
  233. package/src/actions/titles/actions.js +11 -10
  234. package/src/actions/titles/resolve.js +1 -40
  235. package/src/actions/violinPlots/create.js +90 -43
  236. package/src/actions/violinPlots/edit.js +192 -0
  237. package/src/actions/violinPlots/index.js +2 -0
  238. package/src/core/ChartProgram.js +7 -0
  239. package/src/core/action.js +32 -2
  240. package/src/core/compositionState.js +141 -8
  241. package/src/core/programState.js +20 -0
  242. package/src/core/resourceReferences.js +332 -0
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/grammar/aggregate.js +66 -23
  245. package/src/grammar/arcs.js +58 -1
  246. package/src/grammar/areaEndpoints.js +46 -0
  247. package/src/grammar/areaSeries.js +87 -41
  248. package/src/grammar/bars/aggregate.js +37 -13
  249. package/src/grammar/bars/geometry.js +44 -0
  250. package/src/grammar/bars/policy.js +9 -7
  251. package/src/grammar/bin.js +226 -0
  252. package/src/grammar/categoryOrder.js +34 -0
  253. package/src/grammar/complete.js +279 -0
  254. package/src/grammar/computed.js +351 -0
  255. package/src/grammar/coordinates.js +37 -0
  256. package/src/grammar/density.js +239 -37
  257. package/src/grammar/displayLabels.js +57 -0
  258. package/src/grammar/ecdf.js +198 -0
  259. package/src/grammar/facets/dependencies.js +133 -1
  260. package/src/grammar/facets/guides.js +3 -0
  261. package/src/grammar/facets/index.js +123 -52
  262. package/src/grammar/facets/scales.js +15 -18
  263. package/src/grammar/fold.js +123 -0
  264. package/src/grammar/horizon.js +6 -4
  265. package/src/grammar/impute.js +340 -0
  266. package/src/grammar/interval.js +37 -20
  267. package/src/grammar/lineSeries.js +20 -63
  268. package/src/grammar/markFilter.js +22 -4
  269. package/src/grammar/markLabelSelection.js +57 -0
  270. package/src/grammar/markLabels.js +110 -0
  271. package/src/grammar/normalize.js +366 -0
  272. package/src/grammar/parallelAxisTitles.js +16 -0
  273. package/src/grammar/pathSeries.js +109 -0
  274. package/src/grammar/pointPacking.js +183 -0
  275. package/src/grammar/pointShapes.js +13 -4
  276. package/src/grammar/polar.js +90 -8
  277. package/src/grammar/positionDatum.js +12 -0
  278. package/src/grammar/rects.js +9 -1
  279. package/src/grammar/regression/derive.js +4 -0
  280. package/src/grammar/regression/index.js +8 -2
  281. package/src/grammar/regression/models.js +22 -7
  282. package/src/grammar/regression/parameters.js +63 -13
  283. package/src/grammar/rotation.js +29 -0
  284. package/src/grammar/roundedRect.js +180 -0
  285. package/src/grammar/rules.js +8 -21
  286. package/src/grammar/scales/color.js +23 -1
  287. package/src/grammar/scales/colorConsumers.js +57 -0
  288. package/src/grammar/scales/continuous.js +2 -2
  289. package/src/grammar/scales/definition.js +42 -5
  290. package/src/grammar/scales/discretized.js +11 -5
  291. package/src/grammar/scales/fields.js +38 -8
  292. package/src/grammar/scales/index.js +1 -0
  293. package/src/grammar/scales/mapping.js +2 -0
  294. package/src/grammar/scales/ordinal.js +12 -3
  295. package/src/grammar/scales/policies.js +2 -2
  296. package/src/grammar/scales/radial.js +55 -0
  297. package/src/grammar/scales/size.js +456 -0
  298. package/src/grammar/scales/types.js +1 -0
  299. package/src/grammar/schemas/concreteGraphic.js +14 -2
  300. package/src/grammar/schemas/graphic.js +15 -0
  301. package/src/grammar/schemas/graphicBounds.js +179 -30
  302. package/src/grammar/schemas/semanticPath.js +23 -4
  303. package/src/grammar/stack.js +170 -0
  304. package/src/grammar/statisticalReference.js +103 -0
  305. package/src/grammar/statistics/confidenceInterval.js +101 -0
  306. package/src/grammar/strokeStyle.js +77 -0
  307. package/src/grammar/summary.js +245 -0
  308. package/src/grammar/text.js +22 -16
  309. package/src/grammar/timeUnit.js +234 -13
  310. package/src/grammar/transformTopology.js +10 -0
  311. package/src/grammar/transforms.js +418 -16
  312. package/src/grammar/valueFormat.js +112 -0
  313. package/src/grammar/weightedStatistics.js +374 -0
  314. package/src/grammar/window.js +232 -21
  315. package/src/layout/aspect.js +80 -0
  316. package/src/layout/facets.js +100 -12
  317. package/src/layout/guideCollisions.js +12 -0
  318. package/src/layout/labels.js +343 -0
  319. package/src/layout/legend.js +2 -10
  320. package/src/layout/legendItems.js +102 -0
  321. package/src/layout/legendLane.js +100 -11
  322. package/src/materialization/bars/aggregate.js +56 -147
  323. package/src/materialization/bars/histogram.js +104 -20
  324. package/src/materialization/bars/ranged.js +19 -3
  325. package/src/materialization/bars/resolve.js +7 -4
  326. package/src/materialization/categorySlotOffset.js +40 -0
  327. package/src/materialization/composition.js +3 -1
  328. package/src/materialization/compositionSnapshot.js +1 -1
  329. package/src/materialization/coordinateBounds.js +204 -0
  330. package/src/materialization/dependencies.js +93 -16
  331. package/src/materialization/encodings.js +1 -1
  332. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  333. package/src/materialization/facetGuides/placement.js +8 -2
  334. package/src/materialization/facetGuides/preparation.js +12 -7
  335. package/src/materialization/facetHeaders.js +346 -0
  336. package/src/materialization/facets.js +23 -66
  337. package/src/materialization/guides/layout.js +57 -0
  338. package/src/materialization/guides/resources.js +28 -2
  339. package/src/materialization/legends.js +11 -2
  340. package/src/materialization/marks/capabilities.js +20 -5
  341. package/src/materialization/marks/grouping.js +52 -0
  342. package/src/materialization/marks/index.js +68 -27
  343. package/src/materialization/marks/policies.js +4 -1
  344. package/src/materialization/rect.js +55 -42
  345. package/src/materialization/rectConfig.js +5 -1
  346. package/src/materialization/rowEncoding.js +13 -13
  347. package/src/materialization/scaleGuideDependencies.js +8 -5
  348. package/src/materialization/scales/map.js +6 -3
  349. package/src/materialization/scales/policies/arc.js +42 -0
  350. package/src/materialization/scales/policies/binnedPosition.js +7 -4
  351. package/src/materialization/scales/policies/offset.js +50 -8
  352. package/src/materialization/scales/policies/series.js +5 -1
  353. package/src/materialization/scales/resolve.js +65 -17
  354. package/src/materialization/selection/compatibility.js +11 -0
  355. package/src/materialization/selection/items/arc.js +28 -4
  356. package/src/materialization/selection/items/bar.js +42 -71
  357. package/src/materialization/selection/items/common.js +27 -6
  358. package/src/materialization/selection/items/path.js +3 -13
  359. package/src/materialization/selection/items/rect.js +35 -8
  360. package/src/materialization/selection/path.js +1 -1
  361. package/src/materialization/selection/point.js +3 -1
  362. package/src/materialization/text.js +324 -14
  363. package/src/mcp/adapter.js +2 -2
  364. package/src/renderers/canvas/circle.js +2 -3
  365. package/src/renderers/canvas/line.js +2 -3
  366. package/src/renderers/canvas/native.js +4 -4
  367. package/src/renderers/canvas/path.js +2 -3
  368. package/src/renderers/canvas/rect.js +2 -3
  369. package/src/renderers/canvas/stroke.js +11 -0
  370. package/src/renderers/svg.js +19 -0
  371. package/src/selectors/datasets.js +44 -0
  372. package/src/selectors/layers.js +7 -2
  373. package/src/selectors/scales.js +6 -0
  374. package/src/theme/defaults.js +140 -0
  375. package/types/basic.d.ts +17 -2
  376. package/types/index.d.ts +211 -0
  377. package/types/program.d.ts +2182 -204
  378. package/src/materialization/bars/grouped.js +0 -144
@@ -1,4 +1,20 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
+ import {
3
+ LEGEND_CHANNELS,
4
+ LEGEND_CONFIG_KINDS
5
+ } from "../../../core/vocabulary.js";
6
+ import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
7
+
8
+ export function validateLegendChannels(channels, operation) {
9
+ if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
10
+ if (channels.length === 0) throw new Error(`${operation} channels must select at least one channel.`);
11
+ const seen = new Set();
12
+ for (const channel of channels) {
13
+ if (!LEGEND_CHANNELS.includes(channel)) throw new Error(`Unsupported legend channel "${channel}".`);
14
+ if (seen.has(channel)) throw new Error(`${operation} channels contains duplicate "${channel}".`);
15
+ seen.add(channel);
16
+ }
17
+ }
2
18
 
3
19
  export function legendTargets(program) {
4
20
  return [...new Set(Object.values(program.guideConfigs.legend ?? {})
@@ -23,3 +39,110 @@ export function resolveLegendTarget(program, requested, operation) {
23
39
  }
24
40
  return targets[0];
25
41
  }
42
+
43
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
44
+ const SINGLE_CHANNELS = Object.freeze({
45
+ size: "size",
46
+ opacity: "opacity",
47
+ strokeWidth: "strokeWidth",
48
+ gradient: "color",
49
+ interval: "color",
50
+ strokeGradient: "stroke",
51
+ strokeInterval: "stroke"
52
+ });
53
+
54
+ const asciiCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
55
+
56
+ export function legendBlockChannels(kind, config) {
57
+ const channels = CATEGORICAL_KINDS.includes(kind)
58
+ ? config?.channels
59
+ : SINGLE_CHANNELS[kind] === undefined
60
+ ? undefined
61
+ : [SINGLE_CHANNELS[kind]];
62
+ if (!Array.isArray(channels) || channels.length === 0) {
63
+ throw new Error(`Legend block ${kind} requires at least one channel.`);
64
+ }
65
+ validateLegendChannels(channels, `Legend block ${kind}`);
66
+ return Object.freeze([...channels].sort(asciiCompare));
67
+ }
68
+
69
+ export function legendBlockKey(channels) {
70
+ if (!Array.isArray(channels) || channels.length === 0) {
71
+ throw new TypeError("Legend block key requires a non-empty channel array.");
72
+ }
73
+ return JSON.stringify([...channels].sort(asciiCompare));
74
+ }
75
+
76
+ export function describeLegendBlock(program, kind, config) {
77
+ if (!LEGEND_CONFIG_KINDS.includes(kind)) {
78
+ throw new Error(`Unknown legend block kind "${kind}".`);
79
+ }
80
+ const channels = legendBlockChannels(kind, config);
81
+ const scaleIds = CATEGORICAL_KINDS.includes(kind)
82
+ ? config?.scales
83
+ : [config?.scale];
84
+ if (!Array.isArray(scaleIds) || scaleIds.length === 0 ||
85
+ scaleIds.some(id => typeof id !== "string" || program.resolvedScales[id] === undefined)) {
86
+ throw new Error(`Legend block ${kind} requires resolved scales.`);
87
+ }
88
+ const family = CATEGORICAL_KINDS.includes(kind)
89
+ ? "categorical"
90
+ : kind === "size"
91
+ ? isDiscreteSizeScaleType(program.resolvedScales[scaleIds[0]].type)
92
+ ? "discrete-size"
93
+ : "sampled"
94
+ : ["opacity", "strokeWidth"].includes(kind)
95
+ ? "sampled"
96
+ : ["gradient", "strokeGradient"].includes(kind)
97
+ ? "gradient"
98
+ : "interval";
99
+ return Object.freeze({
100
+ target: config.target,
101
+ kind,
102
+ family,
103
+ key: legendBlockKey(channels),
104
+ channels,
105
+ scaleIds: Object.freeze([...scaleIds]),
106
+ config
107
+ });
108
+ }
109
+
110
+ export function legendBlockDescriptors(program, target) {
111
+ const requested = validateUserId(target, "Legend target id");
112
+ const descriptors = LEGEND_CONFIG_KINDS.flatMap(kind => {
113
+ const config = program.guideConfigs.legend?.[kind];
114
+ return config?.target === requested
115
+ ? [describeLegendBlock(program, kind, config)]
116
+ : [];
117
+ });
118
+ const seen = new Set();
119
+ for (const descriptor of descriptors) {
120
+ if (seen.has(descriptor.key)) {
121
+ throw new Error(
122
+ `Legend target "${requested}" has ambiguous block key ${descriptor.key}.`
123
+ );
124
+ }
125
+ seen.add(descriptor.key);
126
+ }
127
+ return Object.freeze(descriptors);
128
+ }
129
+
130
+ export function resolveLegendBlock(program, { target, channel }, operation = "editLegendBlock") {
131
+ const requested = resolveLegendTarget(program, target, operation);
132
+ if (!LEGEND_CHANNELS.includes(channel)) {
133
+ throw new Error(`${operation} has unsupported legend channel "${channel}".`);
134
+ }
135
+ const matches = legendBlockDescriptors(program, requested)
136
+ .filter(descriptor => descriptor.channels.includes(channel));
137
+ if (matches.length === 0) {
138
+ throw new Error(
139
+ `${operation} target "${requested}" has no legend block for channel "${channel}".`
140
+ );
141
+ }
142
+ if (matches.length !== 1) {
143
+ throw new Error(
144
+ `${operation} target "${requested}" has ambiguous channel "${channel}".`
145
+ );
146
+ }
147
+ return matches[0];
148
+ }
@@ -0,0 +1,192 @@
1
+ import { isDiscretizedColorScaleType } from "../../../grammar/scales/types.js";
2
+ import { normalizeContinuousLegend } from "./continuous/common.js";
3
+ import { DEFAULT_GRADIENT_SIZE } from "./continuous/gradient.js";
4
+ import { normalizeIntervalLegend } from "./continuous/interval.js";
5
+ import {
6
+ describeLegendBlock,
7
+ legendBlockDescriptors,
8
+ resolveLegendBlock
9
+ } from "./target.js";
10
+ import { validateLegendBlockOverride } from "./blocks.js";
11
+
12
+ const family = (channel, type) => type === "sequential"
13
+ ? channel === "stroke" ? "strokeGradient" : "gradient"
14
+ : isDiscretizedColorScaleType(type)
15
+ ? channel === "stroke" ? "strokeInterval" : "interval"
16
+ : type === "ordinal"
17
+ ? channel === "stroke" ? "stroke" : "color"
18
+ : undefined;
19
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
20
+
21
+ function categoricalFamilyCustom(program, kind, stored) {
22
+ const side = ["left", "right"].includes(stored.position);
23
+ const direction = side ? "vertical" : "horizontal";
24
+ const itemGap = side ? 28 : stored.position === "top" ? 24 : 20;
25
+ const order = program.semanticSpec.guides.legend?.[kind]?.order;
26
+ return stored.inferredSymbol !== true ||
27
+ stored.direction !== direction ||
28
+ (stored.columns !== undefined && !(side && stored.columns === 1)) ||
29
+ stored.itemGap !== itemGap || stored.layout !== "edge" ||
30
+ order !== undefined;
31
+ }
32
+
33
+ function planChannelLegendTransition(program, scale, nextType, channel) {
34
+ const from = family(channel, scale.type), to = family(channel, nextType);
35
+ if (from === undefined || to === undefined || from === to) return undefined;
36
+ const stored = program.guideConfigs.legend?.[from];
37
+ if ((stored?.scale ?? stored?.scales?.[0]) !== scale.id) return undefined;
38
+ const oldDescriptor = describeLegendBlock(program, from, stored);
39
+ const overrideKeys = Object.keys(stored.blockOverrides ?? {});
40
+ if (overrideKeys.some(key => key !== oldDescriptor.key)) {
41
+ throw new Error(`${channel} legend transition found stale legend block overrides.`);
42
+ }
43
+ const blockOverride = stored.blockOverrides?.[oldDescriptor.key];
44
+ const side = ["left", "right"].includes(stored.position);
45
+ const categorical = ["color", "stroke"].includes(from);
46
+ const gradientFamily = from.toLowerCase().endsWith("gradient");
47
+ const defaults = gradientFamily ? normalizeContinuousLegend({ position: stored.position }, "gradient")
48
+ : normalizeIntervalLegend({ position: stored.position });
49
+ const comparableSymbol = channel === "stroke" && stored.inferredStrokeWidth === true
50
+ ? { ...stored.symbol, strokeWidth: defaults.symbol?.strokeWidth }
51
+ : stored.symbol;
52
+ const custom = categorical
53
+ ? categoricalFamilyCustom(program, from, stored)
54
+ : gradientFamily
55
+ ? stored.count !== defaults.count || !same(stored.gradient, DEFAULT_GRADIENT_SIZE) || stored.itemGap !== defaults.itemGap
56
+ : !same(comparableSymbol, defaults.symbol) || stored.itemGap !== defaults.itemGap ||
57
+ stored.direction !== defaults.direction || (stored.columns !== undefined && !(side && stored.columns === 1));
58
+ if (custom) {
59
+ throw new Error(`${channel} legend transition cannot discard custom family settings; remove and recreate the legend explicitly.`);
60
+ }
61
+ if (blockOverride?.title !== undefined) {
62
+ throw new Error(`${channel} legend transition cannot distribute a legend block title; edit the destination block explicitly.`);
63
+ }
64
+ if (blockOverride?.labelMap !== undefined) {
65
+ throw new Error(`${channel} legend transition cannot distribute a legend block labelMap; edit the destination block explicitly.`);
66
+ }
67
+ if (blockOverride?.symbol !== undefined && ["gradient", "strokeGradient"].includes(to)) {
68
+ throw new Error(`${channel} legend transition cannot apply symbol overrides to a gradient block.`);
69
+ }
70
+ const args = {
71
+ target: stored.target, channels: [channel], position: stored.position,
72
+ labels: stored.labels, titleStyle: stored.titleStyle, border: stored.border,
73
+ align: stored.align, offset: stored.offset, titlePosition: stored.titlePosition,
74
+ ...(stored.inferredTitle ? {} : { title: stored.title })
75
+ };
76
+ try {
77
+ if (["interval", "strokeInterval"].includes(to)) {
78
+ normalizeIntervalLegend({ ...args, channels: undefined });
79
+ }
80
+ else if (["gradient", "strokeGradient"].includes(to)) {
81
+ normalizeContinuousLegend(args, "gradient");
82
+ }
83
+ } catch (error) {
84
+ throw new Error(`${channel} legend transition has incompatible layout or style: ${error.message}`);
85
+ }
86
+ return { args, channel, titleVisible: stored.titleVisible !== false, blockOverride };
87
+ }
88
+
89
+ export function planColorLegendTransitions(program, scale, nextType) {
90
+ return ["color", "stroke"].flatMap(channel => {
91
+ const plan = planChannelLegendTransition(program, scale, nextType, channel);
92
+ return plan === undefined ? [] : [plan];
93
+ });
94
+ }
95
+
96
+ export function applyColorLegendTransition(program, plan) {
97
+ let next = program.createLegend(plan.args);
98
+ if (!plan.titleVisible) {
99
+ next = next.editLegend({ target: plan.args.target, title: false });
100
+ }
101
+ if (plan.blockOverride === undefined) return next;
102
+ const descriptor = resolveLegendBlock(next, {
103
+ target: plan.args.target,
104
+ channel: plan.channel
105
+ }, `${plan.channel} legend transition`);
106
+ validateLegendBlockOverride(descriptor, plan.blockOverride);
107
+ next = next._withLegendConfig(descriptor.kind, {
108
+ ...descriptor.config,
109
+ blockOverrides: { [descriptor.key]: plan.blockOverride }
110
+ });
111
+ return next.rematerializeLegend();
112
+ }
113
+
114
+ function intersects(left, right) {
115
+ return left.channels.some(channel => right.channels.includes(channel));
116
+ }
117
+
118
+ function explicitOrder(program, descriptor) {
119
+ if (descriptor.family !== "categorical") return undefined;
120
+ return program.semanticSpec.guides.legend?.[descriptor.kind]?.order;
121
+ }
122
+
123
+ function sameBlockValue(left, right) {
124
+ if (Object.is(left, right)) return true;
125
+ if (Array.isArray(left) && Array.isArray(right)) {
126
+ return left.length === right.length &&
127
+ left.every((value, index) => sameBlockValue(value, right[index]));
128
+ }
129
+ if (left !== null && right !== null &&
130
+ typeof left === "object" && typeof right === "object" &&
131
+ !Array.isArray(left) && !Array.isArray(right)) {
132
+ const leftKeys = Object.keys(left).sort();
133
+ const rightKeys = Object.keys(right).sort();
134
+ return leftKeys.length === rightKeys.length &&
135
+ leftKeys.every((key, index) => key === rightKeys[index] &&
136
+ sameBlockValue(left[key], right[key]));
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function transitionError(target, oldDescriptors, nextDescriptor, field) {
142
+ const oldKeys = oldDescriptors.map(item => item.key).join(", ");
143
+ throw new Error(
144
+ `Legend block transition for target "${target}" from ${oldKeys} to ${nextDescriptor.key} conflicts on ${field}.`
145
+ );
146
+ }
147
+
148
+ function withoutBlockOverrides(config) {
149
+ const { blockOverrides: _removed, ...rest } = config;
150
+ return rest;
151
+ }
152
+
153
+ export function planLegendBlockTransitions(program, target, plans) {
154
+ const oldDescriptors = legendBlockDescriptors(program, target);
155
+ const nextDescriptors = plans.map(plan =>
156
+ describeLegendBlock(program, plan.kind, plan.config));
157
+ return plans.map((plan, index) => {
158
+ const descriptor = nextDescriptors[index];
159
+ const incoming = oldDescriptors.filter(old => intersects(old, descriptor));
160
+ let override;
161
+ if (incoming.length === 1 && incoming[0].key === descriptor.key) {
162
+ override = incoming[0].config.blockOverrides?.[incoming[0].key];
163
+ } else if (incoming.length === 1) {
164
+ const old = incoming[0];
165
+ override = old.config.blockOverrides?.[old.key];
166
+ if (override?.title !== undefined) {
167
+ transitionError(target, incoming, descriptor, "title");
168
+ }
169
+ if (override?.labelMap !== undefined) {
170
+ transitionError(target, incoming, descriptor, "labelMap");
171
+ }
172
+ } else if (incoming.length > 1) {
173
+ const overrides = incoming.map(old => old.config.blockOverrides?.[old.key]);
174
+ if (!overrides.every(value => sameBlockValue(value, overrides[0]))) {
175
+ transitionError(target, incoming, descriptor, "block override");
176
+ }
177
+ const orders = incoming.map(old => explicitOrder(program, old));
178
+ if (!orders.every(value => sameBlockValue(value, orders[0]))) {
179
+ transitionError(target, incoming, descriptor, "order");
180
+ }
181
+ override = overrides[0];
182
+ }
183
+ if (override !== undefined) validateLegendBlockOverride(descriptor, override);
184
+ const config = withoutBlockOverrides(plan.config);
185
+ return {
186
+ ...plan,
187
+ config: override === undefined
188
+ ? config
189
+ : { ...config, blockOverrides: { [descriptor.key]: override } }
190
+ };
191
+ });
192
+ }
@@ -1,7 +1,14 @@
1
1
  import { action } from "../../../../core/action.js";
2
- import { polarGuideNames, resolvePolarGuideResources } from "../resolve.js";
2
+ import { resolvePolarGuideResources } from "../resolve.js";
3
3
  import {
4
- LABEL_EDIT_OPTIONS,
4
+ AXIS_COMPONENTS,
5
+ assertRemovableAxisComponent,
6
+ hasAxisComponent,
7
+ removeAxisComponent,
8
+ validateEnabledAxisComponents
9
+ } from "../../axes/components.js";
10
+ import {
11
+ labelEditOptions,
5
12
  LINE_EDIT_OPTIONS,
6
13
  prefix,
7
14
  resolveAngle,
@@ -22,12 +29,15 @@ const AXIS_EDIT_OPTIONS = Object.freeze([
22
29
  "angle", "line", "ticks", "labels", "ticksAndLabels", "title"
23
30
  ]);
24
31
 
25
- function validateAxisArgs(kind, args, operation) {
26
- validateObject(args, AXIS_OPTIONS, operation);
27
- if (Object.hasOwn(args, "line")) {
32
+ export function validatePolarAxisArgs(kind, args, operation) {
33
+ validateObject(args, kind === "theta"
34
+ ? AXIS_OPTIONS.filter(option => option !== "angle")
35
+ : AXIS_OPTIONS, operation);
36
+ validateEnabledAxisComponents(args, operation);
37
+ if (Object.hasOwn(args, "line") && args.line !== false) {
28
38
  validateObject(args.line, LINE_EDIT_OPTIONS, `${operation}.line`);
29
39
  }
30
- if (Object.hasOwn(args, "ticksAndLabels")) {
40
+ if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
31
41
  validateObject(
32
42
  args.ticksAndLabels,
33
43
  TICK_GROUP_OPTIONS,
@@ -44,7 +54,7 @@ function validateAxisArgs(kind, args, operation) {
44
54
  if (Object.hasOwn(args.ticksAndLabels, "labels")) {
45
55
  validateObject(
46
56
  args.ticksAndLabels.labels,
47
- LABEL_EDIT_OPTIONS.filter(key => !["count", "values"].includes(key)),
57
+ labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
48
58
  `${operation}.ticksAndLabels.labels`
49
59
  );
50
60
  }
@@ -68,7 +78,7 @@ function makeCreateAxis(kind) {
68
78
  op: operation,
69
79
  description: `Create the complete Polar ${kind} axis.`
70
80
  }, function (args = {}) {
71
- validateAxisArgs(kind, args, operation);
81
+ validatePolarAxisArgs(kind, args, operation);
72
82
  const resources = resolvePolarGuideResources(this, kind, args, operation);
73
83
  const angle = resolveAngle(this, kind, args);
74
84
  const shared = {
@@ -82,21 +92,16 @@ function makeCreateAxis(kind) {
82
92
  ...(Object.hasOwn(group, "values") ? { values: group.values } : {})
83
93
  };
84
94
  let next = this;
85
- if (kind === "radius") {
86
- next = next._withGuideConfig("radius", "layout", { angle });
95
+ if (args.line !== false) {
96
+ next = next[`create${prefix(kind)}AxisLine`]({ ...shared, ...(args.line ?? {}) });
87
97
  }
88
- next = next
89
- [`create${prefix(kind)}AxisLine`]({ ...shared, ...(args.line ?? {}) })
90
- [`create${prefix(kind)}AxisTicks`]({
91
- ...shared,
92
- ...mode,
93
- ...(group.ticks ?? {})
94
- })
95
- [`create${prefix(kind)}AxisLabels`]({
96
- ...shared,
97
- ...mode,
98
- ...(group.labels ?? {})
98
+ if (args.ticksAndLabels !== false) {
99
+ next = next[`create${prefix(kind)}AxisTicks`]({
100
+ ...shared, ...mode, ...(group.ticks ?? {})
101
+ })[`create${prefix(kind)}AxisLabels`]({
102
+ ...shared, ...mode, ...(group.labels ?? {})
99
103
  });
104
+ }
100
105
  return args.title === false
101
106
  ? next
102
107
  : next[`create${prefix(kind)}AxisTitle`]({
@@ -118,29 +123,28 @@ function validateAxisEditArgs(kind, args, operation) {
118
123
  throw new Error(`${operation} requires at least one axis change.`);
119
124
  }
120
125
  if (Object.hasOwn(args, "angle")) validateAngle(args.angle);
121
- if (Object.hasOwn(args, "line")) {
126
+ if (Object.hasOwn(args, "line") && args.line !== false) {
122
127
  validateObject(args.line, LINE_EDIT_OPTIONS, `${operation}.line`);
123
128
  }
124
- if (Object.hasOwn(args, "ticks")) {
129
+ if (Object.hasOwn(args, "ticks") && args.ticks !== false) {
125
130
  validateObject(args.ticks, TICK_EDIT_OPTIONS, `${operation}.ticks`);
126
131
  validateModeOptions(args.ticks, `${operation}.ticks`);
127
132
  }
128
- if (Object.hasOwn(args, "labels")) {
129
- validateObject(args.labels, LABEL_EDIT_OPTIONS, `${operation}.labels`);
133
+ if (Object.hasOwn(args, "labels") && args.labels !== false) {
134
+ validateObject(args.labels, labelEditOptions(kind), `${operation}.labels`);
130
135
  validateModeOptions(args.labels, `${operation}.labels`);
131
136
  }
132
- if (Object.hasOwn(args, "ticksAndLabels")) {
137
+ if (args.ticksAndLabels !== undefined &&
138
+ (args.ticks !== undefined || args.labels !== undefined)) {
139
+ throw new Error(`${operation} cannot combine ticksAndLabels with ticks or labels.`);
140
+ }
141
+ if (Object.hasOwn(args, "ticksAndLabels") && args.ticksAndLabels !== false) {
133
142
  validateObject(
134
143
  args.ticksAndLabels,
135
144
  TICK_GROUP_OPTIONS,
136
145
  `${operation}.ticksAndLabels`
137
146
  );
138
147
  validateModeOptions(args.ticksAndLabels, `${operation}.ticksAndLabels`);
139
- if (args.ticks !== undefined || args.labels !== undefined) {
140
- throw new Error(
141
- `${operation} cannot combine ticksAndLabels with ticks or labels.`
142
- );
143
- }
144
148
  if (args.ticksAndLabels.ticks !== undefined) {
145
149
  validateObject(
146
150
  args.ticksAndLabels.ticks,
@@ -151,12 +155,12 @@ function validateAxisEditArgs(kind, args, operation) {
151
155
  if (args.ticksAndLabels.labels !== undefined) {
152
156
  validateObject(
153
157
  args.ticksAndLabels.labels,
154
- LABEL_EDIT_OPTIONS.filter(key => !["count", "values"].includes(key)),
158
+ labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
155
159
  `${operation}.ticksAndLabels.labels`
156
160
  );
157
161
  }
158
162
  }
159
- if (Object.hasOwn(args, "title")) {
163
+ if (Object.hasOwn(args, "title") && args.title !== false) {
160
164
  validateObject(
161
165
  args.title,
162
166
  kind === "theta"
@@ -167,6 +171,41 @@ function validateAxisEditArgs(kind, args, operation) {
167
171
  }
168
172
  }
169
173
 
174
+ function applyAxisEdit(program, kind, args) {
175
+ const angleChanged = Object.hasOwn(args, "angle");
176
+ let next = angleChanged
177
+ ? program._withGuideConfig("radius", "layout", { angle: args.angle })
178
+ : program;
179
+ const edit = (component, options) => {
180
+ if (options === false) {
181
+ next = removeAxisComponent(next, kind, component);
182
+ } else if (options !== undefined || angleChanged && hasAxisComponent(next, kind, component)) {
183
+ next = next[`edit${prefix(kind)}Axis${component[0].toUpperCase() + component.slice(1)}`](options ?? {});
184
+ }
185
+ };
186
+ edit("line", args.line);
187
+ if (args.ticksAndLabels === false) {
188
+ edit("ticks", false);
189
+ edit("labels", false);
190
+ } else if (args.ticksAndLabels !== undefined) {
191
+ const group = args.ticksAndLabels;
192
+ const mode = {
193
+ ...(Object.hasOwn(group, "count") ? { count: group.count } : {}),
194
+ ...(Object.hasOwn(group, "values") ? { values: group.values } : {})
195
+ };
196
+ edit("ticks", { ...mode, ...(group.ticks ?? {}) });
197
+ edit("labels", { ...mode, ...(group.labels ?? {}) });
198
+ } else {
199
+ edit("ticks", args.ticks);
200
+ edit("labels", args.labels);
201
+ }
202
+ edit("title", args.title);
203
+ if (!AXIS_COMPONENTS.some(component => hasAxisComponent(next, kind, component))) {
204
+ next = next[`remove${prefix(kind)}Axis`]();
205
+ }
206
+ return next;
207
+ }
208
+
170
209
  function makeEditAxis(kind) {
171
210
  const operation = `edit${prefix(kind)}Axis`;
172
211
  return action({
@@ -174,42 +213,18 @@ function makeEditAxis(kind) {
174
213
  description: `Edit selected Polar ${kind}-axis components.`
175
214
  }, function (args = {}) {
176
215
  validateAxisEditArgs(kind, args, operation);
177
- const names = polarGuideNames(kind);
178
- const angleChanged = Object.hasOwn(args, "angle");
179
- let next = angleChanged
180
- ? this._withGuideConfig("radius", "layout", { angle: args.angle })
181
- : this;
182
- const has = component =>
183
- next.graphicSpec.objects[names[component]] !== undefined;
184
- if (args.line !== undefined || (angleChanged && has("line"))) {
185
- next = next[`edit${prefix(kind)}AxisLine`](args.line ?? {});
216
+ if (!AXIS_COMPONENTS.some(component => hasAxisComponent(this, kind, component))) {
217
+ throw new Error(`${operation} requires an existing ${kind}-axis component.`);
186
218
  }
187
- if (args.ticksAndLabels !== undefined) {
188
- const group = args.ticksAndLabels;
189
- const mode = {
190
- ...(Object.hasOwn(group, "count") ? { count: group.count } : {}),
191
- ...(Object.hasOwn(group, "values") ? { values: group.values } : {})
192
- };
193
- next = next[`edit${prefix(kind)}AxisTicks`]({
194
- ...mode,
195
- ...(group.ticks ?? {})
196
- });
197
- next = next[`edit${prefix(kind)}AxisLabels`]({
198
- ...mode,
199
- ...(group.labels ?? {})
200
- });
201
- } else {
202
- if (args.ticks !== undefined || (angleChanged && has("ticks"))) {
203
- next = next[`edit${prefix(kind)}AxisTicks`](args.ticks ?? {});
219
+ for (const component of AXIS_COMPONENTS) {
220
+ if (args[component] === false ||
221
+ args.ticksAndLabels === false && ["ticks", "labels"].includes(component)) {
222
+ assertRemovableAxisComponent(this, kind, component, operation);
204
223
  }
205
- if (args.labels !== undefined || (angleChanged && has("labels"))) {
206
- next = next[`edit${prefix(kind)}AxisLabels`](args.labels ?? {});
207
- }
208
- }
209
- if (args.title !== undefined || (angleChanged && has("title"))) {
210
- next = next[`edit${prefix(kind)}AxisTitle`](args.title ?? {});
211
224
  }
212
- return next;
225
+ // Validate the whole proposal on an immutable branch before returning changes.
226
+ applyAxisEdit(this, kind, args);
227
+ return applyAxisEdit(this, kind, args);
213
228
  });
214
229
  }
215
230
 
@@ -20,18 +20,21 @@ import {
20
20
  } from "../resolve.js";
21
21
  import {
22
22
  componentResources,
23
- LABEL_CREATE_OPTIONS,
24
- LABEL_EDIT_OPTIONS,
23
+ labelCreateOptions,
24
+ labelEditOptions,
25
25
  operations,
26
26
  prefix,
27
27
  resolveAngle,
28
28
  validateModeOptions,
29
+ validateComponentCreateArgs,
29
30
  validateObject,
30
31
  withAxisSemantics
31
32
  } from "./shared.js";
33
+ import { normalizeDisplayLabelMap } from
34
+ "../../../../grammar/displayLabels.js";
32
35
 
33
- function labelGeometry(program, kind, config) {
34
- const frame = resolvePolarFrameForProgram(program);
36
+ function labelGeometry(program, kind, config, angle = resolveAngle(program, kind, {})) {
37
+ const frame = resolvePolarFrameForProgram(program, config.coordinate);
35
38
  const mapped = mapPolarGuideValues(program, config);
36
39
  const text = formatPolarGuideValues(program, config, mapped.values);
37
40
  const geometry = {
@@ -45,7 +48,7 @@ function labelGeometry(program, kind, config) {
45
48
  })
46
49
  : resolveRadialAxisLabels({
47
50
  frame,
48
- angle: resolveAngle(program, kind, {}),
51
+ angle,
49
52
  radii: mapped.positions,
50
53
  offset: config.offset
51
54
  }))
@@ -114,6 +117,23 @@ function resolveLabelConfig(program, kind, args, resources, previous) {
114
117
  fontWeight: args.fontWeight ?? previous?.fontWeight ??
115
118
  POLAR_AXIS_DEFAULTS.labels.fontWeight
116
119
  };
120
+ const labelScale = program.resolvedScales[resources.scale];
121
+ if (Object.hasOwn(args, "labelMap")) {
122
+ if (kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type)) {
123
+ throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
124
+ }
125
+ if (args.labelMap === "auto") delete config.labelMap;
126
+ else {
127
+ config.labelMap = normalizeDisplayLabelMap(
128
+ args.labelMap,
129
+ `${prefix(kind)} axis labelMap`
130
+ );
131
+ }
132
+ }
133
+ if (config.labelMap !== undefined &&
134
+ (kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type))) {
135
+ throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
136
+ }
117
137
  validatePolarTickConfig(config, `${kind}-axis labels`);
118
138
  validateNonNegativeFinite(config.offset, "Polar axis label offset");
119
139
  validatePolarLabelFormat(config.format);
@@ -127,7 +147,7 @@ function makeEditLabels(kind) {
127
147
  op: operation.edit,
128
148
  description: `Edit the Polar ${kind}-axis labels.`
129
149
  }, function (args = {}) {
130
- validateObject(args, LABEL_EDIT_OPTIONS, operation.edit);
150
+ validateObject(args, labelEditOptions(kind), operation.edit);
131
151
  validateModeOptions(args, operation.edit);
132
152
  const names = polarGuideNames(kind);
133
153
  const previous = this.guideConfigs.axis?.[kind]?.labels;
@@ -171,7 +191,7 @@ function makeCreateLabels(kind) {
171
191
  op: operation.create,
172
192
  description: `Create the Polar ${kind}-axis labels.`
173
193
  }, function (args = {}) {
174
- validateObject(args, LABEL_CREATE_OPTIONS, operation.create);
194
+ validateComponentCreateArgs(kind, args, labelCreateOptions(kind), operation.create);
175
195
  validateModeOptions(args, operation.create);
176
196
  const names = polarGuideNames(kind);
177
197
  if (this.graphicSpec.objects[names.labels] !== undefined) {
@@ -180,7 +200,7 @@ function makeCreateLabels(kind) {
180
200
  const resources = componentResources(this, kind, args, operation.create);
181
201
  const angle = resolveAngle(this, kind, args);
182
202
  const config = resolveLabelConfig(this, kind, args, resources);
183
- labelGeometry(this, kind, config);
203
+ labelGeometry(this, kind, config, angle);
184
204
  let next = withAxisSemantics(this, kind, resources);
185
205
  if (kind === "radius" &&
186
206
  next.guideConfigs.axis?.radius?.layout === undefined) {