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,3 +1,5 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
2
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
1
3
  import { action } from "../../../core/action.js";
2
4
  import { validateUserId } from "../../../core/identifiers.js";
3
5
  import {
@@ -5,7 +7,7 @@ import {
5
7
  validateNonEmptyString,
6
8
  validateNonNegativeFinite
7
9
  } from "../../../core/validation.js";
8
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
10
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
9
11
  import {
10
12
  isTransformedScaleType,
11
13
  mapContinuousScaleValues,
@@ -32,6 +34,7 @@ import {
32
34
  import { resolveConcreteGraphicBounds } from
33
35
  "../../../grammar/schemas/graphicBounds.js";
34
36
  import { validateAxisTextStyle } from "./labels.js";
37
+ import { resolveRotation } from "../../../grammar/rotation.js";
35
38
 
36
39
  function titleBounds(geometry, config, text) {
37
40
  return resolveTextBounds({
@@ -47,7 +50,11 @@ function titleBounds(geometry, config, text) {
47
50
  }
48
51
 
49
52
  function inferredTitleOffset(program, channel, config) {
50
- const plot = resolveGraphicBounds(program);
53
+ const plot = resolveGuideCoordinateBounds(program, {
54
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
55
+ scale: config.scale,
56
+ channel
57
+ });
51
58
  const canvas = findCanvasGraphic(program)?.properties;
52
59
  const labels = program.graphicSpec.objects[`${channel}AxisLabels`]
53
60
  ? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`)
@@ -102,7 +109,15 @@ export function inferAxisTitleText(program, channel, scaleId) {
102
109
  const titles = new Set();
103
110
  const primaryTitles = new Set();
104
111
  for (const layer of program.semanticSpec.layers) {
105
- const encoding = layer.encoding?.[channel];
112
+ if (isSourceOwnedText(layer)) continue;
113
+ const primary = layer.encoding?.[channel];
114
+ const encoding = layer.mark?.type === "area" && Object.hasOwn(primary ?? {}, "datum")
115
+ ? layer.encoding?.[`${channel}2`] : primary;
116
+ if (channel === "radius" && layer.mark?.type === "arc" &&
117
+ encoding?.scale === scaleId && encoding.aggregate === "count" && encoding.field === undefined) {
118
+ titles.add(encoding.title ?? "count");
119
+ continue;
120
+ }
106
121
  if (
107
122
  encoding?.scale === scaleId &&
108
123
  typeof encoding.field === "string" &&
@@ -140,7 +155,11 @@ export function inferAxisTitleText(program, channel, scaleId) {
140
155
 
141
156
  function resolveGeometry(program, channel, config) {
142
157
  const scale = program.resolvedScales[config.scale];
143
- const bounds = resolveGraphicBounds(program);
158
+ const bounds = resolveGuideCoordinateBounds(program, {
159
+ coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
160
+ scale: config.scale,
161
+ channel
162
+ });
144
163
  const discrete = ["ordinal", "band", "point"].includes(scale?.type);
145
164
  if ((
146
165
  !["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
@@ -193,12 +212,12 @@ function names(channel) {
193
212
 
194
213
  function makeEdit(channel) {
195
214
  const operation = names(channel);
196
- return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, function (args = {}) {
215
+ return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
197
216
  validateKeys(args, EDIT_OPTIONS, operation.edit);
198
217
  if (this.graphicSpec.objects[operation.graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
199
218
  const previous = this.guideConfigs.axis?.[channel]?.title;
200
219
  if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
201
- const { text, ...appearance } = args;
220
+ const { text, rotation: requestedRotation, ...appearance } = args;
202
221
  const explicitText = Object.hasOwn(args, "text");
203
222
  const explicitRotation = Object.hasOwn(args, "rotation");
204
223
  const position = appearance.position ?? previous.position;
@@ -211,7 +230,9 @@ function makeEdit(channel) {
211
230
  position,
212
231
  rotation: inferredRotation
213
232
  ? defaultAxisTitleRotation(channel, position)
214
- : appearance.rotation ?? previous.rotation,
233
+ : explicitRotation
234
+ ? resolveRotation(requestedRotation, "Axis title rotation")
235
+ : previous.rotation,
215
236
  inferredRotation,
216
237
  inferredOffset: Object.hasOwn(args, "offset")
217
238
  ? false
@@ -245,7 +266,7 @@ function makeEdit(channel) {
245
266
  };
246
267
  for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: operation.graphic, property, value });
247
268
  return next;
248
- });
269
+ }));
249
270
  }
250
271
 
251
272
  const editXAxisTitle = makeEdit("x");
@@ -253,7 +274,7 @@ const editYAxisTitle = makeEdit("y");
253
274
 
254
275
  function makeCreate(channel) {
255
276
  const operation = names(channel);
256
- return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, function (args = {}) {
277
+ return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
257
278
  validateKeys(args, CREATE_OPTIONS, operation.create);
258
279
  const {
259
280
  text: requestedText,
@@ -281,7 +302,7 @@ function makeCreate(channel) {
281
302
  offset: channel === "x" ? 42 : 52,
282
303
  rotation: inferredRotation
283
304
  ? defaultAxisTitleRotation(channel, position)
284
- : requestedRotation,
305
+ : resolveRotation(requestedRotation, "Axis title rotation"),
285
306
  inferredRotation,
286
307
  inferredOffset,
287
308
  color: DEFAULTS.color,
@@ -303,7 +324,7 @@ function makeCreate(channel) {
303
324
  ...resolvePlotGraphicPlacement(this)
304
325
  })
305
326
  ._withGuideConfig(channel, "title", config)[operation.edit]();
306
- });
327
+ }));
307
328
  }
308
329
 
309
330
  const createXAxisTitle = makeCreate("x");
@@ -5,6 +5,7 @@ import {
5
5
  registerBasicCategoricalLegendActions
6
6
  } from "./legends/categorical/index.js";
7
7
  import {
8
+ registerIntervalLegendActions,
8
9
  registerGradientLegendActions
9
10
  } from "./legends/continuous/index.js";
10
11
  import { registerSizeLegendActions } from "./legends/size.js";
@@ -13,6 +14,7 @@ export function registerBasicGuideActions(ProgramClass) {
13
14
  registerBasicCartesianAxisActions(ProgramClass);
14
15
  registerBasicCategoricalLegendActions(ProgramClass);
15
16
  registerGradientLegendActions(ProgramClass);
17
+ registerIntervalLegendActions(ProgramClass);
16
18
  registerSizeLegendActions(ProgramClass);
17
19
  registerBasicGridActions(ProgramClass);
18
20
  registerGuideCollectionActions(ProgramClass);
@@ -0,0 +1,106 @@
1
+ import { findCoordinate } from "../../selectors/coordinates.js";
2
+ import { polarGuideNames } from "./polar/resolve.js";
3
+ import { polarGridOperations, validatePolarGridOptions } from "./polar/grids.js";
4
+ import { findLayer } from "../../selectors/layers.js";
5
+ import { resolveGuideOptions } from "./guides.js";
6
+ import { resolveGridOptions } from "./grids/grid.js";
7
+ import { gridNames, validateGridCreateArgs } from "./grids/resolve.js";
8
+ import { scopeFacadeAxes, planFacadeAxes } from "./facadeAxes.js";
9
+ import { assertGuideOptions, guideConflict, resolveStoredGuideCoordinate } from "./reuse.js";
10
+ import { planFacadeLegend } from "./facadeLegend.js";
11
+ import { applyLegendCreationPlan } from "./legends/categorical/actions.js";
12
+
13
+ function facadeGridNames(direction) {
14
+ return ["theta", "radial"].includes(direction)
15
+ ? { ...polarGridOperations(direction), channel: polarGuideNames(direction).channel }
16
+ : gridNames(direction);
17
+ }
18
+
19
+ function scopeGrid(program, layer, args) {
20
+ const directions = resolveGridOptions(program, args, [layer]);
21
+ const scoped = {};
22
+ const polar = findCoordinate(program, layer.coordinate)?.type === "polar";
23
+ for (const [direction, option] of Object.entries(directions)) {
24
+ if (option === undefined) continue;
25
+ if (polar !== ["theta", "radial"].includes(direction)) {
26
+ guideConflict("grid coordinate family does not belong to this facade");
27
+ }
28
+ const { channel, create } = facadeGridNames(direction);
29
+ const scale = layer.encoding?.[channel]?.scale;
30
+ if (scale === undefined || option.scale !== undefined && option.scale !== scale ||
31
+ option.coordinate !== undefined && option.coordinate !== layer.coordinate) {
32
+ guideConflict(`${direction} grid does not belong to this facade`);
33
+ }
34
+ scoped[direction] = { ...option, scale, coordinate: layer.coordinate };
35
+ if (polar) validatePolarGridOptions(scoped[direction], create, true);
36
+ else validateGridCreateArgs(scoped[direction], create);
37
+ }
38
+ // An explicit vertical-only request must not re-enable horizontal grid defaults.
39
+ if (polar) {
40
+ for (const direction of ["theta", "radial"]) if (scoped[direction] === undefined) scoped[direction] = false;
41
+ } else if (scoped.horizontal === undefined) scoped.horizontal = false;
42
+ return scoped;
43
+ }
44
+
45
+ function planGrid(program, args) {
46
+ return Object.entries(args).flatMap(([direction, option]) => {
47
+ if (option === false) return [];
48
+ const { channel, create } = facadeGridNames(direction);
49
+ const guide = program.semanticSpec.guides.grid?.[direction];
50
+ const config = program.guideConfigs.grid?.[direction];
51
+ if (guide === undefined && config === undefined) return [{ op: create, args: option }];
52
+ if (guide?.scale !== option.scale || config === undefined || config.scale !== option.scale ||
53
+ config.coordinate !== undefined && config.coordinate !== option.coordinate ||
54
+ resolveStoredGuideCoordinate(program, guide, channel) !== option.coordinate) {
55
+ guideConflict(`${direction} grid uses a different coordinate or scale`);
56
+ }
57
+ const { scale, coordinate, ...appearance } = option;
58
+ assertGuideOptions(appearance, config, `${direction} grid`);
59
+ return [];
60
+ });
61
+ }
62
+
63
+ export function fulfillFacadeGuides(program, guides, target, explicitGuides = guides) {
64
+ if (guides === false) return program;
65
+ const layer = findLayer(program, target);
66
+ if (layer === undefined) throw new Error(`Unknown facade guide target "${target}".`);
67
+ const resolved = resolveGuideOptions(program, guides, [layer]);
68
+ const axisDirections = Object.entries(resolved.axes ?? {}).filter(([key]) => key !== "coordinate");
69
+ const scopedAxes = resolved.axes === undefined || axisDirections.length > 0 && axisDirections.every(
70
+ ([, value]) => value === false
71
+ )
72
+ ? undefined
73
+ : scopeFacadeAxes(program, layer, resolved.axes);
74
+ const axes = scopedAxes;
75
+ const scopedGrid = resolved.grid === undefined || Object.values(resolved.grid).every(
76
+ value => value === false
77
+ )
78
+ ? undefined
79
+ : scopeGrid(program, layer, resolved.grid);
80
+ const grid = scopedGrid;
81
+ const legend = resolved.legend === undefined ? undefined : { ...resolved.legend, target };
82
+ if (resolved.legend?.target !== undefined && resolved.legend.target !== target) {
83
+ guideConflict("legend target does not belong to this facade");
84
+ }
85
+ if (axes === undefined && grid === undefined && legend === undefined) return program;
86
+ const legendPlan = legend === undefined ? undefined
87
+ : planFacadeLegend(program, layer, legend, explicitGuides.legend ?? {});
88
+ const steps = [
89
+ ...(axes === undefined ? [] : planFacadeAxes(program, layer, axes, explicitGuides.axes)),
90
+ ...(grid === undefined ? [] : planGrid(program, grid))
91
+ ];
92
+ const hasExisting = [program.guideConfigs.axis, program.guideConfigs.grid, program.guideConfigs.legend,
93
+ program.semanticSpec.guides.axis, program.semanticSpec.guides.grid, program.semanticSpec.guides.legend]
94
+ .some(config => Object.keys(config ?? {}).length > 0);
95
+ const unambiguousAxes = axes?.coordinate.type !== "parallel" ||
96
+ program.semanticSpec.layers.filter(candidate =>
97
+ candidate.coordinate === layer.coordinate && candidate.encoding?.parallel !== undefined
98
+ ).length === 1;
99
+ if (!hasExisting && unambiguousAxes) {
100
+ return program.createGuides({ axes: axes ?? false, grid: grid ?? false, legend: legendPlan?.args ?? false });
101
+ }
102
+ let next = program;
103
+ for (const step of steps) next = next[step.op](step.args);
104
+ if (legendPlan !== undefined) next = applyLegendCreationPlan(next, legendPlan);
105
+ return next;
106
+ }
@@ -0,0 +1,200 @@
1
+ import { validatePolarAxisArgs } from "./polar/axes/facade.js";
2
+ import { prefix as polarPrefix, resolveAngle } from "./polar/axes/shared.js";
3
+ import { validateAxesArgs } from "./axes/axes.js";
4
+ import { validateAxisArgs } from "./axes/axis.js";
5
+ import { axisComponentDisabled, hasAxisComponent } from "./axes/components.js";
6
+ import { validateAxisTickGroupArgs } from "./axes/tickGroups.js";
7
+ import { findCoordinate } from "../../selectors/coordinates.js";
8
+ import {
9
+ assertGuideOptions, guideConflict, resolveStoredGuideCoordinate, sameGuideValue
10
+ } from "./reuse.js";
11
+
12
+ export function scopeFacadeAxes(program, layer, args) {
13
+ const descriptor = validateAxesArgs(args);
14
+ const coordinate = findCoordinate(program, layer.coordinate);
15
+ if (coordinate === undefined) guideConflict(`layer "${layer.id}" has no coordinate`);
16
+ if (descriptor.id !== undefined && descriptor.id !== coordinate.id) {
17
+ guideConflict("axes.coordinate.id does not belong to this facade");
18
+ }
19
+ if (descriptor.type !== undefined && descriptor.type !== "auto" && descriptor.type !== coordinate.type) {
20
+ guideConflict("axes.coordinate.type does not match this facade");
21
+ }
22
+ if (coordinate.type === "polar") {
23
+ if (args.x !== undefined || args.y !== undefined) guideConflict("Polar facades do not own Cartesian axes");
24
+ const scoped = { coordinate: { id: coordinate.id, type: "polar" } };
25
+ for (const channel of ["theta", "radius"]) {
26
+ const option = args[channel];
27
+ const scale = layer.encoding?.[channel]?.scale;
28
+ if (option === false || option === undefined && scale === undefined) { scoped[channel] = false; continue; }
29
+ if (scale === undefined || option?.scale !== undefined && option.scale !== scale ||
30
+ option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
31
+ guideConflict(`axes.${channel} does not belong to this facade`);
32
+ }
33
+ scoped[channel] = { ...option, scale, coordinate: coordinate.id };
34
+ validatePolarAxisArgs(channel, scoped[channel], `create${polarPrefix(channel)}Axis`);
35
+ resolveAngle(program, channel, scoped[channel]);
36
+ }
37
+ return scoped;
38
+ }
39
+ if (args.theta !== undefined || args.radius !== undefined) {
40
+ guideConflict("Cartesian and Parallel facades do not own Polar axes");
41
+ }
42
+ if (coordinate.type === "parallel") {
43
+ if (args.x !== undefined || args.y !== undefined) {
44
+ throw new Error("createAxes channel options are not supported for Parallel axes.");
45
+ }
46
+ return { coordinate: { id: coordinate.id, type: "parallel" } };
47
+ }
48
+ const scoped = { coordinate: { id: coordinate.id, type: "cartesian" } };
49
+ for (const channel of ["x", "y"]) {
50
+ const option = args[channel];
51
+ if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
52
+ scoped[channel] = false;
53
+ continue;
54
+ }
55
+ const scale = layer.encoding?.[channel]?.scale;
56
+ if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
57
+ if (option?.scale !== undefined && option.scale !== scale) {
58
+ guideConflict(`axes.${channel}.scale does not belong to this facade`);
59
+ }
60
+ if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
61
+ guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
62
+ }
63
+ scoped[channel] = { ...option, scale, coordinate: coordinate.id };
64
+ validateAxisArgs(scoped[channel], `create${channel.toUpperCase()}Axis`);
65
+ if (scoped[channel].ticksAndLabels !== false) {
66
+ validateAxisTickGroupArgs(scoped[channel].ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
67
+ }
68
+ }
69
+ return scoped;
70
+ }
71
+
72
+ function tickMode(config) {
73
+ return config?.mode === "values" ? { values: config.values }
74
+ : config?.mode === "count" ? { count: config.count } : {};
75
+ }
76
+
77
+ function planAxis(program, channel, args, explicit) {
78
+ const prefix = channel.toUpperCase();
79
+ const guide = program.semanticSpec.guides.axis?.[channel];
80
+ const configs = program.guideConfigs.axis?.[channel] ?? {};
81
+ if (guide === undefined && Object.keys(configs).length === 0) {
82
+ return [{ op: `create${prefix}Axis`, args }];
83
+ }
84
+ if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
85
+ guideConflict(`${channel} axis uses a different coordinate or scale`);
86
+ }
87
+ const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
88
+ if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
89
+ const position = explicit.position ?? positions[0];
90
+ const shared = { scale: args.scale, ...(position === undefined ? {} : { position }) };
91
+ const explicitShared = explicit.position === undefined ? {} : { position: explicit.position };
92
+ const steps = guide.coordinate === undefined ? [{
93
+ op: "editSemantic", args: { property: `guide.axis.${channel}.coordinate`, value: args.coordinate }
94
+ }] : [];
95
+ const group = args.ticksAndLabels ?? {};
96
+ const explicitGroup = explicit.ticksAndLabels ?? {};
97
+ const explicitMode = Object.fromEntries(["count", "values"].filter(key =>
98
+ Object.hasOwn(explicitGroup, key)
99
+ ).map(key => [key, explicitGroup[key]]));
100
+ const requested = {
101
+ line: { ...explicitShared, ...(explicit.line ?? {}) },
102
+ ticks: { ...explicitShared, ...explicitMode, ...(explicitGroup.ticks ?? {}) },
103
+ labels: { ...explicitShared, ...explicitMode, ...(explicitGroup.labels ?? {}) },
104
+ title: { ...explicitShared, ...(explicit.title ?? {}) }
105
+ };
106
+ for (const component of ["line", "ticks", "labels", "title"]) {
107
+ const config = configs[component];
108
+ if (axisComponentDisabled(args, component)) {
109
+ if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
110
+ continue;
111
+ }
112
+ if (config === undefined) continue;
113
+ if (config.scale !== undefined && config.scale !== args.scale) {
114
+ guideConflict(`${channel} ${component} uses a different scale`);
115
+ }
116
+ assertGuideOptions(requested[component], component === "title"
117
+ ? { ...config, text: guide.title } : config, `${channel} axis ${component}`);
118
+ }
119
+ if (args.line !== false && configs.line === undefined) {
120
+ steps.push({ op: `create${prefix}AxisLine`, args: { ...shared, ...args.line } });
121
+ }
122
+ if (args.ticksAndLabels !== false) {
123
+ if (configs.ticks === undefined && configs.labels === undefined) {
124
+ steps.push({ op: `create${prefix}AxisTicksAndLabels`, args: { ...shared, ...group } });
125
+ } else {
126
+ if (configs.ticks === undefined) {
127
+ steps.push({ op: `create${prefix}AxisTicks`, args: { ...shared, ...tickMode(configs.labels), ...explicitMode, ...group.ticks } });
128
+ }
129
+ if (configs.labels === undefined) {
130
+ steps.push({ op: `create${prefix}AxisLabels`, args: { ...shared, ...group.labels } });
131
+ }
132
+ }
133
+ }
134
+ if (args.title !== false && configs.title === undefined) {
135
+ steps.push({ op: `create${prefix}AxisTitle`, args: { ...shared, ...args.title } });
136
+ }
137
+ return steps;
138
+ }
139
+
140
+ export function planFacadeAxes(program, layer, scoped, explicit = {}) {
141
+ if (scoped.coordinate.type === "parallel") {
142
+ const existing = program.semanticSpec.guides.axis?.parallel;
143
+ if (existing === undefined) return [{
144
+ op: "createParallelAxes", args: { target: layer.id, coordinate: layer.coordinate }
145
+ }];
146
+ const scales = layer.encoding.parallel.dimensions.map(dimension => dimension.scale);
147
+ if (existing.coordinate !== layer.coordinate || !sameGuideValue(existing.scales, scales)) {
148
+ guideConflict("Parallel axes use a different coordinate or dimension scales");
149
+ }
150
+ return [];
151
+ }
152
+ if (scoped.coordinate.type === "polar") {
153
+ return ["theta", "radius"].flatMap(channel => scoped[channel] === false ? []
154
+ : planPolarAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
155
+ }
156
+ return ["x", "y"].flatMap(channel => scoped[channel] === false ? []
157
+ : planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
158
+ }
159
+
160
+ function planPolarAxis(program, channel, args, explicit) {
161
+ const prefix = polarPrefix(channel);
162
+ const guide = program.semanticSpec.guides.axis?.[channel];
163
+ const configs = program.guideConfigs.axis?.[channel] ?? {};
164
+ if (guide === undefined && Object.keys(configs).length === 0) return [{ op: `create${prefix}Axis`, args }];
165
+ if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
166
+ guideConflict(`${channel} axis uses a different coordinate or scale`);
167
+ }
168
+ const shared = { scale: args.scale, coordinate: args.coordinate,
169
+ ...(channel === "radius" ? { angle: resolveAngle(program, channel, args) } : {}) };
170
+ const group = args.ticksAndLabels ?? {};
171
+ const explicitGroup = explicit.ticksAndLabels ?? {};
172
+ const mode = Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(explicitGroup, key))
173
+ .map(key => [key, explicitGroup[key]]));
174
+ const requested = {
175
+ line: explicit.line ?? {},
176
+ ticks: { ...mode, ...explicitGroup.ticks },
177
+ labels: { ...mode, ...explicitGroup.labels },
178
+ title: explicit.title || {}
179
+ };
180
+ const steps = [];
181
+ for (const component of ["line", "ticks", "labels", "title"]) {
182
+ const config = configs[component];
183
+ if (axisComponentDisabled(args, component)) {
184
+ if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
185
+ continue;
186
+ }
187
+ if (config !== undefined) {
188
+ if (config.scale !== args.scale || config.coordinate !== args.coordinate) guideConflict(`${channel} ${component} uses different resources`);
189
+ assertGuideOptions(requested[component], component === "title" ? { ...config, text: guide.title } : config,
190
+ `${channel} axis ${component}`);
191
+ continue;
192
+ }
193
+ const options = component === "line" ? args.line ?? {} : component === "title" ? args.title ?? {}
194
+ : { ...tickMode(configs[component === "ticks" ? "labels" : "ticks"]),
195
+ ...Object.fromEntries(["count", "values"].filter(key => Object.hasOwn(group, key)).map(key => [key, group[key]])),
196
+ ...group[component] };
197
+ steps.push({ op: `create${prefix}Axis${component[0].toUpperCase() + component.slice(1)}`, args: { ...shared, ...options } });
198
+ }
199
+ return steps;
200
+ }
@@ -0,0 +1,63 @@
1
+ import { resolveLegendCreationPlan } from "./legends/categorical/actions.js";
2
+ import { resolveLegendStepConfig } from "./legends/creation.js";
3
+ import { legendResourcePolicy } from "../../materialization/guides/resources.js";
4
+ import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
5
+
6
+ function compatibleLegend(program, kind, requested, existing, explicit) {
7
+ const expectedScales = requested.scales ?? [requested.scale];
8
+ const actualScales = existing.scales ?? [existing.scale];
9
+ if (!sameGuideValue(expectedScales, actualScales) ||
10
+ !sameGuideValue(requested.channels, existing.channels)) {
11
+ guideConflict(`${kind} legend uses different channels or scales`);
12
+ }
13
+ const domain = config => config.domain ?? program.resolvedScales[config.scale]?.domain;
14
+ if (!sameGuideValue(domain(requested), domain(existing))) {
15
+ guideConflict(`${kind} legend uses a different domain or order`);
16
+ }
17
+ if (requested.symbol?.layers !== undefined && !sameGuideValue(
18
+ requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
19
+ )) guideConflict(`${kind} legend uses a different symbol recipe`);
20
+ const appearance = {};
21
+ for (const [key, value] of Object.entries(explicit)) {
22
+ if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
23
+ if (key === "symbol" && requested.symbol?.layers !== undefined) {
24
+ appearance.symbol = value === "auto" ? requested.symbol
25
+ : value.layers !== undefined ? value
26
+ : { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
27
+ } else if (key === "border" && value === true) {
28
+ if (existing.border === false) guideConflict(`${kind} legend border is disabled`);
29
+ appearance.border = {};
30
+ } else appearance[key] = value;
31
+ }
32
+ assertGuideOptions(appearance, existing, `${kind} legend`);
33
+ }
34
+
35
+ export function planFacadeLegend(program, layer, args, explicit = args) {
36
+ const plan = resolveLegendCreationPlan(program, args, [layer]);
37
+ const steps = [];
38
+ for (const step of plan.steps) {
39
+ const { kind, config } = resolveLegendStepConfig(program, step);
40
+ const stored = program.guideConfigs.legend?.[kind];
41
+ const policy = legendResourcePolicy(kind);
42
+ const occupied = program.semanticSpec.guides.legend?.[policy.semanticKind];
43
+ if (stored === undefined) {
44
+ if (occupied !== undefined ||
45
+ policy.family === "categorical" && (
46
+ program.semanticSpec.guides.legend?.series !== undefined ||
47
+ program.semanticSpec.guides.legend?.color !== undefined
48
+ )) {
49
+ guideConflict(`${kind} legend slot belongs to an incompatible legend family`);
50
+ }
51
+ steps.push(step);
52
+ } else {
53
+ if (occupied === undefined) guideConflict(`${kind} legend has no semantic owner`);
54
+ compatibleLegend(program, kind, config, stored, explicit);
55
+ }
56
+ }
57
+ const first = plan.steps[0];
58
+ const scopedArgs = first?.op === "createCategoricalLegend" && first.args.symbol !== undefined
59
+ ? { ...args, symbol: first.args.symbol } : args;
60
+ // Reused blocks retain their original target. The direct createLegend combined
61
+ // owner check is inapplicable to independently verified shared blocks.
62
+ return { ...plan, combined: false, steps, args: scopedArgs };
63
+ }
@@ -217,45 +217,49 @@ const editGrid = action(
217
217
  }
218
218
  );
219
219
 
220
+ export function resolveGridOptions(program, args = {}, layers = program.semanticSpec.layers) {
221
+ validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
222
+ const automatic = Object.keys(args).length === 0
223
+ ? resolveAutomaticGridOptions(program, layers)
224
+ : undefined;
225
+ const hasExplicitPolar = Object.hasOwn(args, "theta") ||
226
+ Object.hasOwn(args, "radial");
227
+ const polarDefault = automatic !== undefined &&
228
+ (Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
229
+ const horizontal = automatic !== undefined
230
+ ? automatic.horizontal === false ? undefined : automatic.horizontal
231
+ : polarDefault ||
232
+ (hasExplicitPolar && args.horizontal === undefined)
233
+ ? undefined
234
+ : normalizeDirection(args.horizontal, "horizontal");
235
+ const vertical = automatic !== undefined
236
+ ? automatic.vertical === false ? undefined : automatic.vertical
237
+ : args.vertical === undefined
238
+ ? undefined
239
+ : normalizeDirection(args.vertical, "vertical");
240
+ const theta = automatic !== undefined
241
+ ? automatic.theta
242
+ : args.theta === undefined
243
+ ? undefined
244
+ : normalizeDirection(args.theta, "theta");
245
+ const radial = automatic !== undefined
246
+ ? automatic.radial
247
+ : args.radial === undefined
248
+ ? undefined
249
+ : normalizeDirection(args.radial, "radial");
250
+ if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
251
+ throw new Error("createGrid requires at least one selected direction.");
252
+ }
253
+ return { horizontal, vertical, theta, radial };
254
+ }
255
+
220
256
  const createGrid = action(
221
257
  {
222
258
  op: "createGrid",
223
259
  description: "Create selected Cartesian grid directions."
224
260
  },
225
261
  function (args = {}) {
226
- validateOptionObject(args, AGGREGATE_OPTIONS, "createGrid");
227
- const automatic = Object.keys(args).length === 0
228
- ? resolveAutomaticGridOptions(this)
229
- : undefined;
230
- const hasExplicitPolar = Object.hasOwn(args, "theta") ||
231
- Object.hasOwn(args, "radial");
232
- const polarDefault = automatic !== undefined &&
233
- (Object.hasOwn(automatic, "theta") || Object.hasOwn(automatic, "radial"));
234
- const horizontal = automatic !== undefined
235
- ? automatic.horizontal === false ? undefined : automatic.horizontal
236
- : polarDefault ||
237
- (hasExplicitPolar && args.horizontal === undefined)
238
- ? undefined
239
- : normalizeDirection(args.horizontal, "horizontal");
240
- const vertical = automatic !== undefined
241
- ? automatic.vertical === false ? undefined : automatic.vertical
242
- : args.vertical === undefined
243
- ? undefined
244
- : normalizeDirection(args.vertical, "vertical");
245
- const theta = automatic !== undefined
246
- ? automatic.theta
247
- : args.theta === undefined
248
- ? undefined
249
- : normalizeDirection(args.theta, "theta");
250
- const radial = automatic !== undefined
251
- ? automatic.radial
252
- : args.radial === undefined
253
- ? undefined
254
- : normalizeDirection(args.radial, "radial");
255
- if ([horizontal, vertical, theta, radial].every(value => value === undefined)) {
256
- throw new Error("createGrid requires at least one selected direction.");
257
- }
258
-
262
+ const { horizontal, vertical, theta, radial } = resolveGridOptions(this, args);
259
263
  let next = this;
260
264
  if (horizontal !== undefined) {
261
265
  next = next.createHorizontalGrid(horizontal);
@@ -1,3 +1,4 @@
1
+ import { isSourceOwnedText } from "../../../grammar/text.js";
1
2
  import { validateUserId } from "../../../core/identifiers.js";
2
3
  import {
3
4
  validateGeneratedItemLimit,
@@ -9,7 +10,7 @@ import {
9
10
  isTransformedScaleType,
10
11
  mapContinuousScaleValues
11
12
  } from "../../../grammar/scales/index.js";
12
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
13
+ import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
13
14
  import { resolveGridLineGeometry } from "../../../layout/grid.js";
14
15
  import { DEFAULT_COLORS } from "../../../theme/defaults.js";
15
16
  import { findCoordinate } from "../../../selectors/coordinates.js";
@@ -126,7 +127,7 @@ export function validateGridEditArgs(args, operation) {
126
127
  export function resolveGridResources(program, direction, args) {
127
128
  const { channel } = gridNames(direction);
128
129
  let layers = program.semanticSpec.layers.filter(
129
- layer => layer.encoding?.[channel]?.scale !== undefined
130
+ layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
130
131
  );
131
132
  if (layers.length === 0) throw new Error(`${direction} grid requires a ${channel} encoding.`);
132
133
  const requestedCoordinate = args.coordinate === undefined
@@ -167,7 +168,14 @@ export function resolveGridResources(program, direction, args) {
167
168
  ) {
168
169
  throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
169
170
  }
170
- const related = new Set(layers.map(layer => layer.id));
171
+ const sourceIds = new Set(layers.map(layer => layer.id));
172
+ const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
173
+ isSourceOwnedText(layer) && sourceIds.has(layer.source)
174
+ )];
175
+ const related = new Set(relatedLayers.flatMap(layer => {
176
+ const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
177
+ return leader === undefined ? [layer.id] : [leader, layer.id];
178
+ }));
171
179
  let before;
172
180
  walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
173
181
  if (before === undefined && related.has(id)) before = id;
@@ -240,7 +248,12 @@ export function editGridConfig(previous, args) {
240
248
 
241
249
  export function resolveGridGeometry(program, config) {
242
250
  const scale = program.resolvedScales[config.scale];
243
- const bounds = resolveGraphicBounds(program);
251
+ const channel = config.direction === "horizontal" ? "y" : "x";
252
+ const bounds = resolveGuideCoordinateBounds(program, {
253
+ coordinate: config.coordinate,
254
+ scale: config.scale,
255
+ channel
256
+ });
244
257
  if ((
245
258
  !["linear", "time"].includes(scale?.type) &&
246
259
  !isTransformedScaleType(scale?.type)