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,28 +1,39 @@
1
+ import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
2
+ import { sameGuideValue } from "../../reuse.js";
3
+ import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
1
4
  import { action } from "../../../../core/action.js";
2
5
  import { validateOptionObject } from "../../../../core/validation.js";
3
6
  import { noOptions, resolveLayout, activeConfig } from "./layout.js";
4
7
  import { normalizeOptions } from "./options.js";
8
+ import { normalizeRecipe, resolveLegendSymbol } from "./recipes.js";
9
+ import { validateLegendChannels } from "../target.js";
10
+ import { createCategoricalLegendFromConfig, reconcileCategoricalSymbols } from "../lifecycle.js";
5
11
  import { findLayer } from "../../../../selectors/layers.js";
6
12
  import { findSemanticScale } from "../../../../selectors/scales.js";
7
- import { isSizeLegendPoint } from "../size.js";
13
+ import { isSizeLegendPoint, resolveSizeLegendConfig } from "../size.js";
8
14
  import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
15
+ import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
9
16
  import { legendResourcePolicies } from
10
17
  "../../../../materialization/guides/resources.js";
11
18
  import {
12
- hasMultiHorizontalLegendLane,
13
- hasMultiLegendLane,
19
+ hasHorizontalLegendLane,
20
+ hasLegendLane,
14
21
  hasMultiSideLegendLane
15
22
  } from "../lane.js";
16
23
  import {
17
24
  resolveCurrentDefinition,
18
25
  resolveDefinition,
19
- resolveLegendKind,
20
26
  resolveTarget,
21
27
  sameValues
22
28
  } from "./resolve.js";
29
+ import { readLegendSampling } from "../sampling.js";
30
+ import {
31
+ reconcileLegendBlockTitleGraphic,
32
+ resolveEffectiveLegendBlockConfig
33
+ } from "../blocks.js";
23
34
 
24
35
  function finishLegend(program) {
25
- return hasMultiLegendLane(program) ? program.rematerializeLegend() : program;
36
+ return hasLegendLane(program) ? program.rematerializeLegend() : program;
26
37
  }
27
38
 
28
39
  function requestedCandidate(program, target, candidates) {
@@ -33,58 +44,64 @@ function requestedCandidate(program, target, candidates) {
33
44
  return candidates.includes(layer) ? layer : undefined;
34
45
  }
35
46
 
36
- function createStandaloneLegend(program, args, kind) {
37
- const { target, count, position, channels: _channels, ...unsupported } = args;
38
- const label = kind === "size" ? "size" : "stroke-width";
39
- const unsupportedKeys = Object.keys(unsupported);
40
- if (unsupportedKeys.length > 0) {
41
- throw new Error(
42
- `Standalone ${label} legend does not support option "${unsupportedKeys[0]}".`
43
- );
44
- }
45
- if (position !== undefined && position !== "right") {
46
- throw new Error(
47
- `Standalone ${label} legends currently require position "right".`
48
- );
49
- }
50
- return finishLegend(program[kind === "size"
51
- ? "createSizeLegend"
52
- : "createStrokeWidthLegend"]({
53
- ...(target === undefined ? {} : { target }),
54
- ...(count === undefined ? {} : { count })
55
- }));
47
+ function resolveStandaloneLegendStep(args, kind) {
48
+ if (Object.hasOwn(args, "inheritAppearance")) throw new Error("createLegend does not accept inheritAppearance.");
49
+ const { channels: _channels, ...options } = args;
50
+ return { op: kind === "size" ? "createSizeLegend" : "createStrokeWidthLegend", args: options };
56
51
  }
57
52
 
58
53
  export const rematerializeLegend = action(
59
54
  { op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
60
- function (args = {}) {
55
+ withGuideLayoutValidation(function (args = {}) {
61
56
  noOptions(args, "rematerializeLegend");
62
57
  let next = this;
63
- const hasCategorical =
64
- this.guideConfigs.legend?.series !== undefined ||
65
- this.guideConfigs.legend?.color !== undefined;
66
- if (hasCategorical) {
67
- const { kind, config } = activeConfig(this);
68
- const definition = resolveCurrentDefinition(this, config);
69
- const changed =
70
- !sameValues(config.channels, definition.channels) ||
71
- !sameValues(config.domain, definition.domain) ||
72
- !sameValues(config.scales, definition.scales) ||
73
- config.field !== definition.field ||
74
- config.title !== definition.title;
58
+ const categoricalKinds = ["series", "color", "stroke"].filter(
59
+ kind => this.guideConfigs.legend?.[kind] !== undefined
60
+ );
61
+ for (const kind of categoricalKinds) {
62
+ const stored = next.guideConfigs.legend[kind];
63
+ const definition = resolveCurrentDefinition(next, stored);
64
+ const symbol = stored.inferredSymbol
65
+ ? normalizeRecipe(
66
+ resolveLegendSymbol(
67
+ next,
68
+ findLayer(next, stored.target),
69
+ definition.channels,
70
+ undefined,
71
+ kind
72
+ ),
73
+ kind,
74
+ { internal: true }
75
+ )
76
+ : stored.symbol;
77
+ const symbolChanged = !sameGuideValue(symbol, stored.symbol);
78
+ const changed = symbolChanged ||
79
+ !sameValues(stored.channels, definition.channels) ||
80
+ !sameValues(stored.domain, definition.domain) ||
81
+ !sameValues(stored.scales, definition.scales) ||
82
+ stored.field !== definition.field ||
83
+ stored.title !== definition.title;
75
84
  next = changed
76
- ? this._withLegendConfig(kind, {
77
- ...config,
85
+ ? next._withLegendConfig(kind, {
86
+ ...stored,
87
+ symbol,
78
88
  channels: definition.channels,
79
89
  scales: definition.scales,
80
90
  field: definition.field,
81
91
  title: definition.title,
82
92
  domain: definition.domain
83
93
  })
84
- : this;
94
+ : next;
95
+ const config = resolveEffectiveLegendBlockConfig(
96
+ next,
97
+ kind,
98
+ next.guideConfigs.legend[kind]
99
+ );
100
+ next = reconcileLegendBlockTitleGraphic(next, kind,
101
+ config.titleVisible !== false);
85
102
  if (kind === "series") {
86
103
  if (!sameValues(
87
- this.semanticSpec.guides.legend.series.scales,
104
+ next.semanticSpec.guides.legend.series.scales,
88
105
  definition.scales
89
106
  )) {
90
107
  next = next.editSemantic({
@@ -92,292 +109,305 @@ export const rematerializeLegend = action(
92
109
  value: definition.scales
93
110
  });
94
111
  }
95
- if (this.semanticSpec.guides.legend.series.title !== definition.title) {
112
+ if (next.semanticSpec.guides.legend.series.title !== definition.title) {
96
113
  next = next.editSemantic({
97
114
  property: "guide.legend.series.title",
98
115
  value: definition.title
99
116
  });
100
117
  }
101
118
  } else {
102
- if (this.semanticSpec.guides.legend.color.scale !== definition.scales[0]) {
119
+ if (next.semanticSpec.guides.legend[kind].scale !== definition.scales[0]) {
103
120
  next = next.editSemantic({
104
- property: "guide.legend.color.scale",
121
+ property: `guide.legend.${kind}.scale`,
105
122
  value: definition.scales[0]
106
123
  });
107
124
  }
108
- if (this.semanticSpec.guides.legend.color.title !== definition.title) {
125
+ if (next.semanticSpec.guides.legend[kind].title !== definition.title) {
109
126
  next = next.editSemantic({
110
- property: "guide.legend.color.title",
127
+ property: `guide.legend.${kind}.title`,
111
128
  value: definition.title
112
129
  });
113
130
  }
114
131
  }
115
- if (config.border !== false) next = next.rematerializeLegendBackground();
132
+ if (symbolChanged) {
133
+ resolveLayout(next, config);
134
+ next = reconcileCategoricalSymbols(next, stored, config);
135
+ }
136
+ if (config.border !== false) {
137
+ next = next.rematerializeLegendBackground({ kind });
138
+ }
116
139
  next = next
117
- .rematerializeLegendSymbols()
118
- .rematerializeLegendLabels();
119
- if (config.titleVisible !== false) next = next.rematerializeLegendTitle();
140
+ .rematerializeLegendSymbols({ kind })
141
+ .rematerializeLegendLabels({ kind });
142
+ if (config.titleVisible !== false) {
143
+ next = next.rematerializeLegendTitle({ kind });
144
+ }
120
145
  const hasHighlight = Object.values(
121
146
  next.materializationConfigs.highlights ?? {}
122
147
  ).some(highlight => highlight.target === config.target);
123
- if (hasHighlight) next = next.rematerializeLegendHighlights();
148
+ if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
124
149
  }
125
150
  for (const policy of legendResourcePolicies()) {
126
151
  if (
127
152
  policy.rematerializeOp !== undefined &&
128
153
  this.guideConfigs.legend?.[policy.kind] !== undefined
129
154
  ) {
155
+ const config = resolveEffectiveLegendBlockConfig(
156
+ next,
157
+ policy.kind,
158
+ next.guideConfigs.legend[policy.kind]
159
+ );
160
+ next = reconcileLegendBlockTitleGraphic(next, policy.kind,
161
+ config.titleVisible !== false);
130
162
  next = next[policy.rematerializeOp]();
131
163
  }
132
164
  }
133
165
  if (hasMultiSideLegendLane(next)) {
134
166
  next = next.rematerializeSideLegendLane();
135
167
  }
136
- if (hasMultiHorizontalLegendLane(next)) {
168
+ if (hasHorizontalLegendLane(next)) {
137
169
  next = next.rematerializeHorizontalLegendLane();
138
170
  }
139
171
  return next;
140
- }
172
+ })
141
173
  );
142
174
 
175
+ export function resolveCategoricalLegendConfig(program, args = {}) {
176
+ const layer = resolveTarget(program, args.target);
177
+ const definition = resolveDefinition(
178
+ program,
179
+ layer,
180
+ args.channels,
181
+ args.title,
182
+ args.order === undefined ? undefined : normalizeLegendOrder(args.order)
183
+ );
184
+ const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
185
+ const options = normalizeOptions({ ...args,
186
+ symbol: resolveLegendSymbol(
187
+ program,
188
+ layer,
189
+ definition.channels,
190
+ args.symbol,
191
+ definition.kind
192
+ )
193
+ }, definition.kind, { internalSymbol: inferredSymbol });
194
+ const config = {
195
+ target: layer.id,
196
+ ...definition,
197
+ inferredTitle: !Object.hasOwn(args, "title"),
198
+ inferredSymbol,
199
+ position: options.position,
200
+ align: options.align,
201
+ direction: options.direction,
202
+ columns: options.columns,
203
+ offset: options.offset,
204
+ titlePosition: options.titlePosition,
205
+ symbol: options.symbol,
206
+ labels: options.labels,
207
+ titleStyle: options.titleStyle,
208
+ itemGap: options.itemGap,
209
+ layout: options.layout,
210
+ border: options.border,
211
+ titleVisible: true
212
+ };
213
+ return config;
214
+ }
215
+
143
216
  export const createCategoricalLegend = action(
144
217
  { op: "createCategoricalLegend", description: "Create one categorical legend block." },
145
- function (args = {}) {
146
- const layer = resolveTarget(this, args.target);
147
- const kind = resolveLegendKind(layer, args.channels);
148
- const options = normalizeOptions(args, kind);
149
- if (
150
- this.semanticSpec.guides.legend?.series !== undefined ||
151
- this.semanticSpec.guides.legend?.color !== undefined
152
- ) {
153
- throw new Error("createCategoricalLegend requires a missing legend.");
218
+ withGuideLayoutValidation(function (args = {}) {
219
+ const config = resolveCategoricalLegendConfig(this, args);
220
+ if (this.semanticSpec.guides.legend?.[config.kind] !== undefined) {
221
+ throw new Error(
222
+ `createCategoricalLegend requires a missing legend for ${config.kind}.`
223
+ );
154
224
  }
155
- const definition = resolveDefinition(
156
- this,
157
- layer,
158
- options.channels,
159
- options.title
160
- );
161
- const config = {
162
- target: layer.id,
163
- ...definition,
164
- inferredTitle: !Object.hasOwn(args, "title"),
165
- position: options.position,
166
- align: options.align,
167
- direction: options.direction,
168
- columns: options.columns,
169
- offset: options.offset,
170
- titlePosition: options.titlePosition,
171
- symbol: options.symbol,
172
- labels: options.labels,
173
- titleStyle: options.titleStyle,
174
- itemGap: options.itemGap,
175
- bottomGrid: options.bottomGrid,
176
- border: options.border,
177
- titleVisible: true
178
- };
179
225
  resolveLayout(this, config);
180
- let next = this;
181
- if (kind === "series") {
182
- next = next
183
- .editSemantic({
184
- property: "guide.legend.series.channels",
185
- value: definition.channels
186
- })
187
- .editSemantic({
188
- property: "guide.legend.series.scales",
189
- value: definition.scales
190
- })
191
- .editSemantic({
192
- property: "guide.legend.series.title",
193
- value: definition.title
194
- });
195
- } else {
196
- next = next
197
- .editSemantic({
198
- property: "guide.legend.color.scale",
199
- value: definition.scales[0]
200
- })
201
- .editSemantic({
202
- property: "guide.legend.color.title",
203
- value: definition.title
204
- });
205
- }
206
- next = next._withLegendConfig(kind, config);
207
- if (config.border !== false) next = next.createLegendBackground();
208
- return next
209
- .createLegendSymbols()
210
- .createLegendLabels()
211
- .createLegendTitle();
212
- }
226
+ return createCategoricalLegendFromConfig(this, config,
227
+ args.order === undefined ? undefined : normalizeLegendOrder(args.order));
228
+ })
213
229
  );
214
230
 
215
- export const createLegend = action(
216
- { op: "createLegend", description: "Create an inferred legend for selected channels." },
217
- function (args = {}) {
218
- validateOptionObject(args, undefined, "createLegend");
219
- const channels = args.channels;
220
- if (channels !== undefined && !Array.isArray(channels)) {
221
- throw new TypeError("createLegend channels must be an array.");
231
+ export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
232
+ validateOptionObject(args, undefined, "createLegend");
233
+ const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
234
+ const channels = args.channels;
235
+ if (channels !== undefined) validateLegendChannels(channels, "createLegend");
236
+ const standalone = [
237
+ ["size", isSizeLegendPoint, ["color", "stroke", "shape", "strokeDash", "opacity"]],
238
+ ["strokeWidth", isStrokeWidthLegendLayer, args.target === undefined
239
+ ? ["color", "stroke", "shape", "strokeDash", "size", "opacity"]
240
+ : ["color", "stroke", "shape", "strokeDash", "opacity"]],
241
+ ["opacity", isOpacityLegendLayer,
242
+ ["color", "stroke", "shape", "strokeDash", "size"]]
243
+ ];
244
+ for (const [kind, eligible, otherChannels] of standalone) {
245
+ const explicit = channels?.length === 1 && channels[0] === kind;
246
+ const inferred = channels === undefined && candidates.filter(eligible).length === 1 &&
247
+ !candidates.some(layer => otherChannels.some(channel => layer.encoding?.[channel]?.scale !== undefined));
248
+ if (explicit || inferred) {
249
+ const step = kind === "opacity" ? { op: "createOpacityLegend", args }
250
+ : resolveStandaloneLegendStep(args, kind);
251
+ return { steps: [step], finish: "auto" };
222
252
  }
223
- const explicitSize = channels?.length === 1 && channels[0] === "size";
224
- const explicitStrokeWidth = channels?.length === 1 &&
225
- channels[0] === "strokeWidth";
226
- const requestedSizeLayer = args.target === undefined
227
- ? undefined
228
- : findLayer(this, args.target);
229
- const sizeCandidates = this.semanticSpec.layers.filter(isSizeLegendPoint);
230
- const hasNonSizeLegendEncoding = layer =>
231
- ["color", "shape", "strokeDash", "opacity"].some(channel =>
232
- layer.encoding?.[channel]?.scale !== undefined
253
+ }
254
+ const continuousColorCandidates = candidates.filter(layer => {
255
+ const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
256
+ ? layer.encoding?.color
257
+ : undefined;
258
+ const scale = findSemanticScale(program, encoding?.scale);
259
+ return scale?.type === "sequential";
260
+ });
261
+ const continuousColor = requestedCandidate(
262
+ program, args.target, continuousColorCandidates
263
+ );
264
+ if (
265
+ (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
266
+ (channels === undefined && continuousColor)
267
+ ) {
268
+ return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
269
+ }
270
+ const intervalColorCandidates = candidates.filter(layer => {
271
+ const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
272
+ const scale = findSemanticScale(program, encoding?.scale);
273
+ return ["quantize", "quantile", "threshold"].includes(scale?.type);
274
+ });
275
+ if (
276
+ (channels?.length === 1 && channels[0] === "color" &&
277
+ intervalColorCandidates.length > 0) ||
278
+ (channels === undefined && intervalColorCandidates.length > 0)
279
+ ) {
280
+ return { steps: [{ op: "createIntervalLegend", args }], finish: "auto" };
281
+ }
282
+ const continuousStrokeCandidates = candidates.filter(layer => {
283
+ const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
284
+ return scale?.type === "sequential";
285
+ });
286
+ const continuousStroke = requestedCandidate(
287
+ program,
288
+ args.target,
289
+ continuousStrokeCandidates
290
+ );
291
+ if (
292
+ (channels?.length === 1 && channels[0] === "stroke" && continuousStroke) ||
293
+ (channels === undefined && continuousStroke &&
294
+ !["color", "shape", "strokeDash", "size", "opacity", "strokeWidth"].some(
295
+ channel => continuousStroke.encoding?.[channel]?.scale !== undefined
296
+ ))
297
+ ) {
298
+ return {
299
+ steps: [{ op: "createStrokeGradientLegend", args: {
300
+ ...args,
301
+ channels: ["stroke"]
302
+ } }],
303
+ finish: "auto"
304
+ };
305
+ }
306
+ const intervalStrokeCandidates = candidates.filter(layer => {
307
+ const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
308
+ return ["quantize", "quantile", "threshold"].includes(scale?.type);
309
+ });
310
+ if (
311
+ (channels?.length === 1 && channels[0] === "stroke" &&
312
+ intervalStrokeCandidates.length > 0) ||
313
+ (channels === undefined && intervalStrokeCandidates.length === 1)
314
+ ) {
315
+ return {
316
+ steps: [{ op: "createStrokeIntervalLegend", args: {
317
+ ...args,
318
+ channels: ["stroke"]
319
+ } }],
320
+ finish: "auto"
321
+ };
322
+ }
323
+ const wantsShape = channels?.includes("shape") === true;
324
+ const wantsSize = channels?.includes("size") === true;
325
+ const pointCandidates = candidates.filter(layer =>
326
+ layer.mark?.type === "point" &&
327
+ (channels === undefined
328
+ ? ["color", "shape"].some(channel => layer.encoding?.[channel]?.scale !== undefined)
329
+ : (wantsShape || wantsSize) && channels.every(channel =>
330
+ ["color", "stroke", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
331
+ );
332
+ const requestedPoint = requestedCandidate(program, args.target, pointCandidates);
333
+ const inferredSize = channels === undefined && pointCandidates.some(isSizeLegendPoint);
334
+ if ((wantsSize || inferredSize) && requestedPoint === undefined) {
335
+ throw new Error("Combined size legend requires one eligible point mark or an explicit target.");
336
+ }
337
+ if (requestedPoint !== undefined) {
338
+ if (Object.hasOwn(args, "values")) {
339
+ throw new Error(
340
+ "Combined and categorical legends require a channel block selector for exact values."
233
341
  );
234
- const hasNonSizeLegendCandidate = this.semanticSpec.layers.some(
235
- hasNonSizeLegendEncoding
236
- );
237
- const inferredSize = channels === undefined && (
238
- (args.target !== undefined && isSizeLegendPoint(requestedSizeLayer) &&
239
- !hasNonSizeLegendEncoding(requestedSizeLayer)) ||
240
- (args.target === undefined && sizeCandidates.length === 1 &&
241
- !hasNonSizeLegendCandidate)
242
- );
243
- if (explicitSize || inferredSize) {
244
- return createStandaloneLegend(this, args, "size");
245
342
  }
246
- const strokeWidthCandidates = this.semanticSpec.layers.filter(
247
- isStrokeWidthLegendLayer
248
- );
249
- const requestedStrokeWidthLayer = args.target === undefined
250
- ? undefined
251
- : findLayer(this, args.target);
252
- const hasOtherStrokeWidthLegendCandidate = this.semanticSpec.layers.some(
253
- layer => ["color", "shape", "strokeDash", "size", "opacity"].some(
254
- channel => layer.encoding?.[channel]?.scale !== undefined
255
- )
256
- );
257
- const inferredStrokeWidth = channels === undefined && (
258
- (args.target !== undefined &&
259
- isStrokeWidthLegendLayer(requestedStrokeWidthLayer) &&
260
- !hasNonSizeLegendEncoding(requestedStrokeWidthLayer)) ||
261
- (args.target === undefined && strokeWidthCandidates.length === 1 &&
262
- !hasOtherStrokeWidthLegendCandidate)
263
- );
264
- if (explicitStrokeWidth || inferredStrokeWidth) {
265
- return createStandaloneLegend(this, args, "strokeWidth");
343
+ const { count, ...categoricalArgs } = args;
344
+ const combined = requestedPoint.encoding?.size?.scale !== undefined &&
345
+ (channels === undefined || wantsSize);
346
+ if (count !== undefined && !combined) {
347
+ throw new Error("Legend count requires a selected size legend.");
266
348
  }
267
- const opacityCandidates = this.semanticSpec.layers.filter(layer =>
268
- layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
269
- );
270
- const hasOtherLegendCandidate = this.semanticSpec.layers.some(layer =>
271
- ["color", "shape", "strokeDash", "size"].some(channel =>
272
- layer.encoding?.[channel]?.scale !== undefined
273
- )
274
- );
275
- if (
276
- (channels?.length === 1 && channels[0] === "opacity") ||
277
- (channels === undefined && opacityCandidates.length === 1 &&
278
- !hasOtherLegendCandidate)
279
- ) {
280
- return finishLegend(this.createOpacityLegend(args));
349
+ if (combined && categoricalArgs.layout === "legacy-bottom") {
350
+ throw new Error('Combined size legends require layout "edge".');
281
351
  }
282
- const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
283
- const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
284
- ? layer.encoding?.color
285
- : undefined;
286
- const scale = findSemanticScale(this, encoding?.scale);
287
- return scale?.type === "sequential";
288
- });
289
- const continuousColor = requestedCandidate(
290
- this, args.target, continuousColorCandidates
352
+ const inferredChannels = ["color", "stroke", "shape"].filter(
353
+ channel => requestedPoint.encoding?.[channel]?.scale !== undefined
291
354
  );
292
- if (
293
- (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
294
- (channels === undefined && continuousColor)
295
- ) {
296
- return finishLegend(this.createGradientLegend(args));
355
+ const { format: sizeFormat, ...categoricalLabels } = categoricalArgs.labels ?? {};
356
+ if (!combined && sizeFormat !== undefined && sizeFormat !== "auto") {
357
+ throw new Error("Categorical legend labels do not accept format.");
297
358
  }
298
- const intervalColorCandidates = this.semanticSpec.layers.filter(layer => {
299
- const encoding = layer.mark?.type === "point" ? layer.encoding?.color : undefined;
300
- const scale = findSemanticScale(this, encoding?.scale);
301
- return ["quantize", "quantile", "threshold"].includes(scale?.type);
302
- });
303
- if (
304
- (channels?.length === 1 && channels[0] === "color" &&
305
- intervalColorCandidates.length > 0) ||
306
- (channels === undefined && intervalColorCandidates.length > 0)
307
- ) {
308
- return finishLegend(this.createIntervalLegend(args));
309
- }
310
- const wantsShape = channels?.includes("shape") === true;
311
- const pointCandidates = this.semanticSpec.layers.filter(layer =>
312
- layer.mark?.type === "point" &&
313
- layer.encoding?.shape?.scale !== undefined &&
314
- (wantsShape || layer.encoding?.color?.scale !== undefined)
315
- );
316
- const requestedPoint = requestedCandidate(this, args.target, pointCandidates);
317
- if (requestedPoint !== undefined) {
318
- const { count, ...categoricalArgs } = args;
319
- if (
320
- requestedPoint.encoding?.size?.scale !== undefined &&
321
- categoricalArgs.position !== undefined &&
322
- !["right", "left"].includes(categoricalArgs.position)
323
- ) {
324
- throw new Error(
325
- "Combined point series and size legends currently require a side position."
326
- );
359
+ const steps = [{ op: "createCategoricalLegend", args: {
360
+ ...categoricalArgs,
361
+ ...(categoricalArgs.labels === undefined ? {} : { labels: categoricalLabels }),
362
+ target: requestedPoint.id,
363
+ channels: channels?.filter(channel => channel !== "size") ?? inferredChannels
364
+ } }];
365
+ if (combined) steps.push({ op: "createSizeLegend", args: {
366
+ target: requestedPoint.id,
367
+ ...(count === undefined ? {} : { count }),
368
+ ...(categoricalArgs.labels === undefined ? {} : { labels: {
369
+ ...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
370
+ ...(sizeFormat === undefined ? {} : { format: sizeFormat })
371
+ } }),
372
+ inheritAppearance: true
373
+ } });
374
+ // Validate both content owners before any component action starts.
375
+ resolveCategoricalLegendConfig(program, steps[0].args);
376
+ if (combined) {
377
+ const size = resolveSizeLegendConfig(program, steps[1].args);
378
+ const existing = program.guideConfigs.legend?.size;
379
+ if (existing !== undefined && existing.target !== size.target) {
380
+ throw new Error("Combined point series legend requires the active size legend to share its target.");
327
381
  }
328
- const hasMatchingLine = this.semanticSpec.layers.some(candidate =>
329
- candidate.mark?.type === "line" &&
330
- candidate.encoding?.color?.field === requestedPoint.encoding.color.field &&
331
- candidate.encoding.color.scale === requestedPoint.encoding.color.scale
332
- );
333
- const symbol = categoricalArgs.symbol ?? {
334
- layers: [
335
- ...(hasMatchingLine
336
- ? [{ type: "line", length: 32, lineWidth: 3 }]
337
- : []),
338
- {
339
- type: "point",
340
- size: Math.sqrt(64 / Math.PI),
341
- stroke: "white",
342
- strokeWidth: 0
343
- }
344
- ]
345
- };
346
- const inferredChannels = ["color", "shape"].filter(
347
- channel => requestedPoint.encoding?.[channel]?.scale !== undefined
348
- );
349
- let next = this.createCategoricalLegend({
350
- ...categoricalArgs,
351
- target: requestedPoint.id,
352
- channels: categoricalArgs.channels ?? inferredChannels,
353
- symbol
354
- });
355
- if (requestedPoint.encoding?.size?.scale !== undefined) {
356
- const existingSize = next.guideConfigs.legend?.size;
357
- if (existingSize === undefined) {
358
- next = next.createSizeLegend({
359
- target: requestedPoint.id,
360
- ...(count === undefined ? {} : { count }),
361
- inheritAppearance:
362
- categoricalArgs.position === "left" ||
363
- categoricalArgs.labels !== undefined ||
364
- categoricalArgs.titleStyle !== undefined
365
- });
366
- } else if (existingSize.target !== requestedPoint.id) {
367
- throw new Error(
368
- "Combined point series legend requires the active size legend to share its target."
369
- );
370
- } else if (count !== undefined && count !== existingSize.count) {
371
- throw new Error(
372
- "Existing size legend count must be edited before recreating the categorical block."
373
- );
374
- }
375
- next = next.rematerializeLegend();
382
+ if (existing !== undefined && count !== undefined &&
383
+ readLegendSampling(existing).count !== count) {
384
+ throw new Error("Existing size legend count must be edited before recreating the categorical block.");
376
385
  }
377
- return next;
378
386
  }
379
- return finishLegend(this.createCategoricalLegend(args));
387
+ return { steps, combined, finish: combined ? "always" : "auto" };
380
388
  }
389
+ return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
390
+ }
391
+
392
+ export function applyLegendCreationPlan(program, plan) {
393
+ if (plan.steps.length === 0) return program;
394
+ let next = program;
395
+ for (const step of plan.steps) {
396
+ const existingSize = next.guideConfigs.legend?.size;
397
+ // The plan already validated that a retained size block is compatible.
398
+ if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
399
+ next = next[step.op](step.args);
400
+ }
401
+ }
402
+ return plan.finish === "always" ? next.rematerializeLegend()
403
+ : plan.finish === "auto" ? finishLegend(next) : next;
404
+ }
405
+
406
+ export const createLegend = action(
407
+ { op: "createLegend", description: "Create an inferred legend for selected channels." },
408
+ withGuideLayoutValidation(function (args = {}) {
409
+ return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
410
+ })
381
411
  );
382
412
 
383
413
  export const removeCategoricalLegend = action(
@@ -385,16 +415,16 @@ export const removeCategoricalLegend = action(
385
415
  op: "removeCategoricalLegend",
386
416
  description: "Remove the active categorical legend and its concrete components."
387
417
  },
388
- function (args = {}) {
418
+ withGuideLayoutValidation(function (args = {}) {
389
419
  noOptions(args, "removeCategoricalLegend");
390
- const entries = ["series", "color"]
420
+ const entries = ["series", "color", "stroke"]
391
421
  .filter(kind => this.guideConfigs.legend?.[kind] !== undefined);
392
422
  if (entries.length === 0) return this;
393
423
  if (entries.length !== 1) {
394
424
  throw new Error("removeCategoricalLegend requires one active categorical legend.");
395
425
  }
396
426
  const kind = entries[0];
397
- const prefix = kind === "series" ? "seriesLegend" : "colorLegend";
427
+ const prefix = `${kind}Legend`;
398
428
  const targets = Object.keys(this.graphicSpec.objects)
399
429
  .filter(id => id.startsWith(prefix));
400
430
  let next = this.editSemantic({
@@ -405,5 +435,5 @@ export const removeCategoricalLegend = action(
405
435
  next = next.editGraphics({ target, remove: true });
406
436
  }
407
437
  return next._withoutMaterializationConfig(["guides", "legend", kind]);
408
- }
438
+ })
409
439
  );