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,30 +1,52 @@
1
+ import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
2
+ import { resolveLegendStepConfig } from "./creation.js";
3
+ import { normalizeLegendOrder } from "../../../grammar/categoryOrder.js";
4
+ import { resolveDefinition } from "./categorical/resolve.js";
1
5
  import { action } from "../../../core/action.js";
2
6
  import {
3
- validateGeneratedItemLimit,
4
7
  validateOptionObject
5
8
  } from "../../../core/validation.js";
6
9
  import { normalizeOptions } from "./categorical/options.js";
7
- import { symbolGraphic } from "./categorical/layout.js";
10
+ import { resolveLegendSymbol } from "./categorical/recipes.js";
11
+ import { resolveLayout } from "./categorical/layout.js";
12
+ import { resolveLegendCreationPlan } from "./categorical/actions.js";
13
+ import { categoricalSymbolIds, reconcileCategoricalSymbols, createCategoricalLegendFromConfig, resolveCategoricalLegendRevision, removeLegendKinds } from "./lifecycle.js";
14
+ import { createGradientLegendFromConfig } from "./continuous/gradient.js";
8
15
  import {
16
+ normalizeItemLegendLayout,
17
+ normalizeLegendBorder,
9
18
  normalizeLegendTextOptions,
19
+ normalizeLegendTitleOptions,
10
20
  normalizeContinuousLegend,
11
21
  validatePositive
12
22
  } from "./continuous/common.js";
13
- import { normalizeOpacitySymbol } from "./continuous/opacity.js";
14
- import { normalizeIntervalLegend } from "./continuous/interval.js";
23
+ import { normalizeOpacitySymbol, createOpacityLegendFromConfig } from "./continuous/opacity.js";
24
+ import { normalizeIntervalLegend, resolveIntervalConfig, resolveIntervalLayout, createIntervalLegendFromConfig } from "./continuous/interval.js";
25
+ import {
26
+ createStrokeGradientLegendFromConfig,
27
+ createStrokeIntervalLegendFromConfig,
28
+ resolveStrokeIntervalConfig,
29
+ resolveStrokeIntervalLayout
30
+ } from "./continuous/stroke.js";
15
31
  import { findLayer } from "../../../selectors/layers.js";
16
32
  import { resolveLegendGraphicPlacement } from
17
33
  "../../../materialization/graphicHierarchy.js";
18
- import { resolveLegendTarget } from "./target.js";
34
+ import { resolveLegendTarget, validateLegendChannels } from "./target.js";
35
+ import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
36
+ import { SIZE_LEGEND_LABELS, SIZE_LEGEND_TITLE_STYLE, resolveSizeLegendLayout, createSizeLegendFromConfig } from "./size.js";
19
37
  import {
20
38
  STROKE_WIDTH_LEGEND_LABELS,
21
- STROKE_WIDTH_LEGEND_TITLE_STYLE
39
+ STROKE_WIDTH_LEGEND_TITLE_STYLE,
40
+ resolveStrokeWidthLegendLayout,
41
+ createStrokeWidthLegendFromConfig
22
42
  } from "./strokeWidth.js";
43
+ import { normalizeLegendSampling } from "./sampling.js";
44
+ import { planLegendBlockTransitions } from "./transition.js";
23
45
 
24
46
  const OPTIONS = Object.freeze([
25
- "target", "position", "align", "direction", "columns", "offset",
47
+ "target", "channels", "position", "layout", "align", "direction", "columns", "offset",
26
48
  "titlePosition", "title", "symbol", "labels", "titleStyle", "itemGap",
27
- "border", "count", "gradient"
49
+ "border", "count", "values", "gradient", "order"
28
50
  ]);
29
51
 
30
52
  function mergeObject(previous, patch) {
@@ -46,18 +68,22 @@ function reconcileGraphic(program, id, shouldExist, definition) {
46
68
  return program.createGraphics({
47
69
  id,
48
70
  ...definition,
49
- ...resolveLegendGraphicPlacement(program)
71
+ ...resolveLegendGraphicPlacement(program, {
72
+ ...(definition.before === undefined ? {} : { before: definition.before }),
73
+ ...(definition.after === undefined ? {} : { after: definition.after })
74
+ })
50
75
  });
51
76
  }
52
77
  return program;
53
78
  }
54
79
 
55
- function editContinuous(program, kind, previous, args) {
56
- const allowed = kind === "gradient"
80
+ function resolveContinuousEdit(program, kind, previous, args) {
81
+ const gradient = ["gradient", "strokeGradient"].includes(kind);
82
+ const allowed = gradient
57
83
  ? ["target", "position", "align", "offset", "title", "labels",
58
- "titleStyle", "border", "count", "gradient"]
84
+ "titleStyle", "titlePosition", "border", "count", "gradient"]
59
85
  : ["target", "position", "align", "offset", "title", "symbol", "labels",
60
- "titleStyle", "titlePosition", "itemGap", "border", "count"];
86
+ "titleStyle", "titlePosition", "itemGap", "border", "count", "values"];
61
87
  for (const key of Object.keys(args)) {
62
88
  if (!allowed.includes(key)) throw new Error(`${kind} legend does not accept ${key}.`);
63
89
  }
@@ -68,7 +94,9 @@ function editContinuous(program, kind, previous, args) {
68
94
  const titleVisible = titleMode === false ? false
69
95
  : titleMode === undefined ? previous.titleVisible !== false : true;
70
96
  const layer = findLayer(program, previous.target);
71
- const inferred = layer?.encoding?.[kind === "gradient" ? "color" : "opacity"]?.field;
97
+ const channel = kind === "gradient" ? "color"
98
+ : kind === "strokeGradient" ? "stroke" : "opacity";
99
+ const inferred = layer?.encoding?.[channel]?.field;
72
100
  const title = titleMode === "auto"
73
101
  ? inferred
74
102
  : typeof titleMode === "string" ? titleMode : previous.title;
@@ -76,12 +104,17 @@ function editContinuous(program, kind, previous, args) {
76
104
  const enteringInline = kind === "opacity" &&
77
105
  titlePosition === "left" && previous.titlePosition !== "left";
78
106
  const labels = mergeObject(previous.labels, args.labels);
107
+ const sampling = gradient ? undefined : normalizeLegendSampling(args, {
108
+ previous,
109
+ operation: "edit",
110
+ label: "Opacity legend"
111
+ });
79
112
  const normalized = normalizeContinuousLegend({
80
113
  target: previous.target,
81
114
  position: args.position ?? previous.position,
82
115
  align: args.align ?? previous.align,
83
116
  offset: args.offset ?? previous.offset,
84
- count: args.count ?? previous.count,
117
+ ...(gradient ? { count: args.count ?? previous.count } : {}),
85
118
  titlePosition,
86
119
  title,
87
120
  labels: enteringInline && args.labels?.offset === undefined
@@ -93,14 +126,16 @@ function editContinuous(program, kind, previous, args) {
93
126
  itemGap: args.itemGap ?? (enteringInline ? 20 : previous.itemGap),
94
127
  symbol: args.symbol ?? previous.symbol
95
128
  } : {})
96
- }, kind);
129
+ }, gradient ? "gradient" : kind);
130
+ const { count: _legacyCount, ...canonicalPrevious } = previous;
97
131
  const config = {
98
- ...previous,
132
+ ...(gradient ? previous : canonicalPrevious),
99
133
  ...normalized,
134
+ ...(gradient ? {} : { sampling }),
100
135
  inferredTitle,
101
136
  titleVisible
102
137
  };
103
- if (kind === "gradient") {
138
+ if (gradient) {
104
139
  const gradient = mergeObject(previous.gradient, args.gradient);
105
140
  validatePositive(gradient.length, "Gradient length");
106
141
  validatePositive(gradient.thickness, "Gradient thickness");
@@ -108,28 +143,38 @@ function editContinuous(program, kind, previous, args) {
108
143
  } else {
109
144
  config.symbol = normalizeOpacitySymbol(args.symbol ?? previous.symbol);
110
145
  }
111
- const prefix = kind === "gradient" ? "colorGradient" : "opacityLegend";
146
+ return { config, titleMode, title, titleVisible };
147
+ }
148
+
149
+ function editContinuous(program, kind, previous, args) {
150
+ const { config, titleMode, title, titleVisible } = resolveContinuousEdit(program, kind, previous, args);
151
+ const prefix = kind === "gradient" ? "colorGradient"
152
+ : kind === "strokeGradient" ? "strokeGradient" : "opacityLegend";
153
+ const semanticKind = kind === "gradient" ? "color"
154
+ : kind === "strokeGradient" ? "stroke" : "opacity";
112
155
  let next = program;
113
156
  if (titleMode === "auto" || typeof titleMode === "string") {
114
157
  next = next.editSemantic({
115
- property: `guide.legend.${kind === "gradient" ? "color" : "opacity"}.title`,
158
+ property: `guide.legend.${semanticKind}.title`,
116
159
  value: title
117
160
  });
118
161
  }
119
162
  next = next._withLegendConfig(kind, config);
120
163
  next = reconcileGraphic(next, `${prefix}Background`, config.border !== false, {
121
164
  type: "rect",
122
- before: kind === "gradient" ? "colorGradientStrips" : "opacityLegendSymbols"
165
+ before: ["gradient", "strokeGradient"].includes(kind)
166
+ ? `${prefix}Strips`
167
+ : "opacityLegendSymbols"
123
168
  });
124
169
  next = reconcileGraphic(next, `${prefix}Title`, titleVisible, { type: "text" });
125
170
  return next.rematerializeLegend();
126
171
  }
127
172
 
128
- function editInterval(program, previous, args) {
173
+ function resolveIntervalEdit(program, previous, args, channel = "color") {
129
174
  for (const key of Object.keys(args)) {
130
175
  if (![
131
176
  "target", "position", "align", "direction", "offset", "title",
132
- "symbol", "labels", "titleStyle", "itemGap", "border"
177
+ "symbol", "labels", "titleStyle", "itemGap", "border", "layout", "columns", "titlePosition"
133
178
  ].includes(key)) {
134
179
  throw new Error(`interval legend does not accept ${key}.`);
135
180
  }
@@ -143,13 +188,17 @@ function editInterval(program, previous, args) {
143
188
  ? false
144
189
  : titleMode === undefined ? previous.titleVisible !== false : true;
145
190
  const title = titleMode === "auto"
146
- ? layer?.encoding?.color?.field
191
+ ? layer?.encoding?.[channel]?.field
147
192
  : typeof titleMode === "string" ? titleMode : previous.title;
148
193
  const normalized = normalizeIntervalLegend({
149
194
  target: previous.target,
150
195
  position: args.position ?? previous.position,
151
196
  align: args.align ?? previous.align,
152
- direction: args.direction ?? previous.direction,
197
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
198
+ ? undefined : previous.direction),
199
+ layout: args.layout === undefined ? previous.layout : args.layout,
200
+ columns: args.columns ?? previous.columns,
201
+ titlePosition: args.titlePosition ?? previous.titlePosition,
153
202
  offset: args.offset ?? previous.offset,
154
203
  title,
155
204
  symbol: mergeObject(previous.symbol, args.symbol),
@@ -158,44 +207,69 @@ function editInterval(program, previous, args) {
158
207
  itemGap: args.itemGap ?? previous.itemGap,
159
208
  border: mergeBorder(previous.border, args.border)
160
209
  });
161
- let next = program._withLegendConfig("interval", {
210
+ const config = {
162
211
  ...previous,
163
212
  ...normalized,
164
213
  inferredTitle,
165
- titleVisible
166
- });
214
+ titleVisible,
215
+ ...(channel === "stroke" && args.symbol?.strokeWidth !== undefined
216
+ ? { inferredStrokeWidth: false }
217
+ : {})
218
+ };
219
+ const resolved = channel === "stroke"
220
+ ? resolveStrokeIntervalConfig(program, config)
221
+ : resolveIntervalConfig(program, config);
222
+ (channel === "stroke" ? resolveStrokeIntervalLayout : resolveIntervalLayout)(
223
+ program,
224
+ resolved.config,
225
+ resolved.scale
226
+ );
227
+ return { normalized, config, titleMode, title, titleVisible };
228
+ }
229
+
230
+ function editInterval(program, previous, args, channel = "color") {
231
+ const kind = channel === "stroke" ? "strokeInterval" : "interval";
232
+ const prefix = channel === "stroke" ? "strokeInterval" : "colorLegend";
233
+ const { normalized, config, titleMode, title, titleVisible } = resolveIntervalEdit(program, previous, args, channel);
234
+ let next = program._withLegendConfig(kind, config);
167
235
  if (titleMode === "auto" || typeof titleMode === "string") {
168
236
  next = next.editSemantic({
169
- property: "guide.legend.color.title",
237
+ property: `guide.legend.${channel}.title`,
170
238
  value: title
171
239
  });
172
240
  }
173
- next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
241
+ next = reconcileGraphic(next, `${prefix}Title`, titleVisible, {
174
242
  type: "text"
175
243
  });
176
244
  next = reconcileGraphic(
177
245
  next,
178
- "colorLegendBackground",
246
+ `${prefix}Background`,
179
247
  normalized.border !== false,
180
- { type: "rect", before: "colorLegendSymbols" }
248
+ { type: "rect", before: `${prefix}Symbols` }
181
249
  );
182
250
  return next.rematerializeLegend();
183
251
  }
184
252
 
185
- function editStrokeWidth(program, previous, args) {
186
- const allowed = ["target", "title", "count", "labels", "titleStyle"];
253
+ function resolveSampledLegendEdit(program, kind, previous, args) {
254
+ const size = kind === "size";
255
+ const label = size ? "size" : "stroke-width";
256
+ const allowed = ["target", "title", "count", "values", "labels", "titleStyle",
257
+ "position", "layout", "align", "direction", "columns", "titlePosition", "offset", "itemGap", "border"];
187
258
  for (const key of Object.keys(args)) {
188
259
  if (!allowed.includes(key)) {
189
- throw new Error(`stroke-width legend does not accept ${key}.`);
260
+ throw new Error(`${label} legend does not accept ${key}.`);
190
261
  }
191
262
  }
192
- const count = args.count ?? previous.count;
193
- if (!Number.isInteger(count) || count < 2) {
194
- throw new RangeError(
195
- "Stroke-width legend count must be an integer of at least 2."
196
- );
263
+ const discrete = size &&
264
+ isDiscreteSizeScaleType(program.resolvedScales[previous.scale]?.type);
265
+ if (discrete && (args.count !== undefined || args.values !== undefined)) {
266
+ throw new Error("Discrete size legends do not support count or exact values.");
197
267
  }
198
- validateGeneratedItemLimit(count, "Stroke-width legend count");
268
+ const sampling = discrete ? undefined : normalizeLegendSampling(args, {
269
+ previous,
270
+ operation: "edit",
271
+ label: `${size ? "Size" : "Stroke-width"} legend`
272
+ });
199
273
  const layer = findLayer(program, previous.target);
200
274
  const titleMode = args.title;
201
275
  const inferredTitle = titleMode === "auto"
@@ -205,44 +279,91 @@ function editStrokeWidth(program, previous, args) {
205
279
  ? false
206
280
  : titleMode === undefined ? previous.titleVisible !== false : true;
207
281
  const title = titleMode === "auto"
208
- ? layer?.encoding?.strokeWidth?.field
282
+ ? layer?.encoding?.[kind]?.field
209
283
  : typeof titleMode === "string" ? titleMode : previous.title;
284
+ const { count: _legacyCount, ...continuousPrevious } = previous;
285
+ const canonicalPrevious = discrete ? previous : continuousPrevious;
210
286
  const config = {
211
- ...previous,
287
+ ...canonicalPrevious,
212
288
  title,
213
289
  inferredTitle,
214
290
  titleVisible,
215
- count,
291
+ ...(discrete ? {} : { sampling }),
292
+ ...(size ? { inheritAppearance: false } : {}),
216
293
  labels: normalizeLegendTextOptions(
217
294
  args.labels,
218
295
  "editLegend.labels",
219
- previous.labels ?? STROKE_WIDTH_LEGEND_LABELS
296
+ previous.labels ?? (size ? SIZE_LEGEND_LABELS : STROKE_WIDTH_LEGEND_LABELS)
220
297
  ),
221
- titleStyle: normalizeLegendTextOptions(
298
+ titleStyle: normalizeLegendTitleOptions(
222
299
  args.titleStyle,
223
300
  "editLegend.titleStyle",
224
- previous.titleStyle ?? STROKE_WIDTH_LEGEND_TITLE_STYLE
301
+ previous.titleStyle ?? (size ? SIZE_LEGEND_TITLE_STYLE : STROKE_WIDTH_LEGEND_TITLE_STYLE)
225
302
  )
226
303
  };
304
+ Object.assign(config, normalizeItemLegendLayout({ ...previous, ...args,
305
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
306
+ ? undefined : previous.direction) }));
307
+ config.border = normalizeLegendBorder(mergeBorder(previous.border, args.border));
308
+ (size ? resolveSizeLegendLayout : resolveStrokeWidthLegendLayout)(program, config);
309
+ return { config, titleMode, title, titleVisible };
310
+ }
311
+
312
+ function editSampledLegend(program, kind, previous, args) {
313
+ const { config, titleMode, title, titleVisible } = resolveSampledLegendEdit(program, kind, previous, args);
314
+ const prefix = kind === "size" ? "sizeLegend" : "strokeWidthLegend";
227
315
  let next = program;
228
316
  if (titleMode === "auto" || typeof titleMode === "string") {
229
317
  next = next.editSemantic({
230
- property: "guide.legend.strokeWidth.title",
318
+ property: `guide.legend.${kind}.title`,
231
319
  value: title
232
320
  });
233
321
  }
234
- next = next._withLegendConfig("strokeWidth", config);
235
- next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
236
- type: "text"
322
+ next = next._withLegendConfig(kind, config);
323
+ next = reconcileGraphic(next, `${prefix}Background`, config.border !== false, {
324
+ type: "rect", before: `${prefix}Symbols`
325
+ });
326
+ next = reconcileGraphic(next, `${prefix}Title`, titleVisible, {
327
+ type: "text", after: `${prefix}Labels`
237
328
  });
238
329
  return next.rematerializeLegend();
239
330
  }
240
331
 
241
- function categoricalSymbolIds(config) {
242
- return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
332
+ function resolveCompanionSizeEdit(program, previous, size, args) {
333
+ if (size === undefined) return undefined;
334
+ const discrete = isDiscreteSizeScaleType(
335
+ program.resolvedScales[size.scale]?.type
336
+ );
337
+ if (discrete && (args.count !== undefined || args.values !== undefined)) {
338
+ throw new Error("Discrete size legends do not support count or exact values.");
339
+ }
340
+ const sampling = discrete ? undefined : normalizeLegendSampling(args, {
341
+ previous: size,
342
+ operation: "edit",
343
+ label: "Size legend"
344
+ });
345
+ const { count: _legacyCount, ...continuousSize } = size;
346
+ const canonicalSize = discrete ? size : continuousSize;
347
+ const config = {
348
+ ...canonicalSize,
349
+ ...(discrete ? {} : { sampling })
350
+ };
351
+ if (args.labels === undefined && args.titleStyle === undefined) return config;
352
+ const labels = size.inheritAppearance
353
+ ? { ...previous.labels, offset: size.labels.offset } : size.labels ?? SIZE_LEGEND_LABELS;
354
+ const titleStyle = size.inheritAppearance
355
+ ? previous.titleStyle : size.titleStyle ?? SIZE_LEGEND_TITLE_STYLE;
356
+ return { ...config, inheritAppearance: false,
357
+ labels: normalizeLegendTextOptions(args.labels, "editLegend.labels", labels),
358
+ titleStyle: normalizeLegendTitleOptions(args.titleStyle, "editLegend.titleStyle", titleStyle) };
243
359
  }
244
360
 
245
- function editCategorical(program, kind, previous, size, args) {
361
+ function resolveCategoricalEdit(program, kind, previous, size, args, storedOrder = program.semanticSpec.guides.legend?.[kind]?.order) {
362
+ if (args.values !== undefined) {
363
+ throw new Error(
364
+ "Combined and categorical legends require a channel block selector for exact values."
365
+ );
366
+ }
246
367
  if (args.gradient !== undefined) {
247
368
  throw new Error("Categorical legends do not accept gradient.");
248
369
  }
@@ -258,32 +379,57 @@ function editCategorical(program, kind, previous, size, args) {
258
379
  const title = titleMode === "auto"
259
380
  ? previous.field
260
381
  : typeof titleMode === "string" ? titleMode : previous.title;
382
+ const mergedLabels = mergeObject(previous.labels, args.labels);
383
+ if (mergedLabels.format !== undefined && mergedLabels.format !== "auto" && size === undefined) {
384
+ throw new Error("Categorical legend labels do not accept format.");
385
+ }
386
+ const { format: _sizeFormat, ...categoricalLabels } = mergedLabels;
387
+ const inferredSymbol = args.symbol === undefined
388
+ ? previous.inferredSymbol
389
+ : args.symbol === "auto";
261
390
  const normalized = normalizeOptions({
262
391
  target: previous.target,
263
392
  channels: previous.channels,
393
+ layout: args.layout === undefined ? previous.layout : args.layout,
264
394
  position: args.position ?? previous.position,
265
395
  align: args.align ?? previous.align,
266
- direction: args.direction ?? (args.position === "left"
267
- ? "vertical"
268
- : previous.direction),
396
+ direction: args.direction ?? (args.position !== undefined && args.position !== previous.position
397
+ ? undefined : previous.direction),
269
398
  ...(args.columns === undefined && previous.columns === undefined
270
399
  ? {} : { columns: args.columns ?? previous.columns }),
271
400
  offset: args.offset ?? previous.offset,
272
401
  titlePosition: args.titlePosition ?? previous.titlePosition,
273
402
  title,
274
- symbol: args.symbol ?? previous.symbol,
275
- labels: mergeObject(previous.labels, args.labels),
403
+ symbol: args.symbol === undefined ? previous.symbol
404
+ : resolveLegendSymbol(
405
+ program,
406
+ findLayer(program, previous.target),
407
+ previous.channels,
408
+ args.symbol,
409
+ kind
410
+ ),
411
+ labels: categoricalLabels,
276
412
  titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
277
413
  itemGap: args.itemGap ?? previous.itemGap,
278
414
  border: mergeBorder(previous.border, args.border)
279
- }, kind);
415
+ }, kind, { internalSymbol: inferredSymbol });
280
416
  const config = {
281
417
  ...previous,
282
418
  ...normalized,
283
419
  inferredTitle,
420
+ inferredSymbol,
284
421
  titleVisible
285
422
  };
286
- const oldSymbols = categoricalSymbolIds(previous);
423
+ const order = args.order === undefined
424
+ ? storedOrder
425
+ : normalizeLegendOrder(args.order);
426
+ // Resolve the final domain before changing semantic/config/graphic state.
427
+ resolveDefinition(program, findLayer(program, previous.target), previous.channels, title, order);
428
+ return { config, order, titleMode, title, titleVisible, sizeConfig: resolveCompanionSizeEdit(program, previous, size, args) };
429
+ }
430
+
431
+ function editCategorical(program, kind, previous, size, args) {
432
+ const { config, order, titleMode, title, titleVisible, sizeConfig } = resolveCategoricalEdit(program, kind, previous, size, args);
287
433
  const newSymbols = categoricalSymbolIds(config);
288
434
  let next = program;
289
435
  if (titleMode === "auto" || typeof titleMode === "string") {
@@ -292,49 +438,113 @@ function editCategorical(program, kind, previous, size, args) {
292
438
  value: title
293
439
  });
294
440
  }
295
- next = next._withLegendConfig(kind, config);
296
- for (const id of oldSymbols) {
297
- if (!newSymbols.has(id) && next.graphicSpec.objects[id] !== undefined) {
298
- next = next.editGraphics({ target: id, remove: true });
441
+ if (args.order !== undefined) {
442
+ if (order === "scale") {
443
+ if (program.semanticSpec.guides.legend?.[kind]?.order !== undefined) {
444
+ next = next.editSemantic({ property: `guide.legend.${kind}.order`, remove: true });
445
+ }
446
+ } else {
447
+ next = next.editSemantic({ property: `guide.legend.${kind}.order`, value: order });
299
448
  }
300
449
  }
301
- next = reconcileGraphic(next, `${kind === "series" ? "series" : "color"}LegendBackground`,
450
+ next = next._withLegendConfig(kind, config);
451
+ next = reconcileCategoricalSymbols(next, previous, config);
452
+ next = reconcileGraphic(next, `${kind}LegendBackground`,
302
453
  config.border !== false, {
303
454
  type: "rect",
304
455
  before: [...newSymbols][0]
305
456
  });
306
- for (const layer of config.symbol.layers) {
307
- const id = symbolGraphic(config, layer.type);
308
- if (next.graphicSpec.objects[id] === undefined) {
309
- const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[layer.type];
310
- next = next[`createLegendSymbol${suffix}`]();
311
- }
312
- }
313
- const titleId = `${kind === "series" ? "series" : "color"}LegendTitle`;
457
+ const titleId = `${kind}LegendTitle`;
314
458
  next = reconcileGraphic(next, titleId, titleVisible, {
315
459
  type: "text",
316
460
  ...(next.graphicSpec.objects.sizeLegendSymbols === undefined
317
- ? {} : { before: "sizeLegendSymbols" })
461
+ ? {} : { before: next.graphicSpec.objects.sizeLegendBackground === undefined
462
+ ? "sizeLegendSymbols" : "sizeLegendBackground" })
318
463
  });
319
464
  if (size !== undefined) {
320
- if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 2)) {
321
- throw new RangeError("Size legend count must be an integer of at least 2.");
465
+ next = next._withLegendConfig("size", sizeConfig);
466
+ }
467
+ return next.rematerializeLegend();
468
+ }
469
+
470
+
471
+ function editLegendContent(program, target, args) {
472
+ validateLegendChannels(args.channels, "editLegend");
473
+ const { channels, target: _target, ...patch } = args;
474
+ const previous = program.guideConfigs.legend ?? {};
475
+ const removed = Object.keys(previous).filter(kind => previous[kind].target === target);
476
+ let view = removed.reduce((next, kind) => next._withoutMaterializationConfig(["guides", "legend", kind]), program);
477
+ const creation = resolveLegendCreationPlan(view, { target, channels });
478
+ const oldCategorical = removed.find(kind =>
479
+ ["series", "color", "stroke"].includes(kind)
480
+ );
481
+ let plans = creation.steps.map(step => {
482
+ const descriptor = resolveLegendStepConfig(view, step);
483
+ const { kind } = descriptor;
484
+ if (previous[kind] !== undefined && previous[kind].target !== target) {
485
+ throw new Error(`Legend ${kind} content already belongs to another target.`);
322
486
  }
323
- if (args.count !== undefined) {
324
- validateGeneratedItemLimit(args.count, "Size legend count");
487
+ if (["series", "color", "stroke"].includes(kind) && oldCategorical !== undefined) {
488
+ const order = patch.order === undefined ? program.semanticSpec.guides.legend?.[oldCategorical]?.order
489
+ : normalizeLegendOrder(patch.order);
490
+ return { ...resolveCategoricalLegendRevision(view, oldCategorical, previous[oldCategorical],
491
+ descriptor.config.channels, { order, validateLayout: false }), kind };
325
492
  }
326
- next = next._withLegendConfig("size", {
327
- ...size,
328
- count: args.count ?? size.count,
329
- inheritAppearance: true
330
- });
493
+ return { ...descriptor, config: previous[kind]?.target === target ? previous[kind] : descriptor.config };
494
+ });
495
+ const categorical = plans.find(plan =>
496
+ ["series", "color", "stroke"].includes(plan.kind)
497
+ );
498
+ const size = plans.find(plan => plan.kind === "size");
499
+ if (categorical !== undefined) {
500
+ if (size !== undefined && previous.size?.target !== target) {
501
+ size.config = { ...size.config, inheritAppearance: categorical.config.position === "left" ||
502
+ patch.position === "left" || patch.labels !== undefined || patch.titleStyle !== undefined };
503
+ }
504
+ const edited = resolveCategoricalEdit(view, categorical.kind, categorical.config, size?.config, patch, categorical.order);
505
+ categorical.config = edited.config;
506
+ categorical.order = edited.order;
507
+ if (size !== undefined) {
508
+ size.config = edited.sizeConfig;
509
+ if (categorical.config.layout === "legacy-bottom") {
510
+ throw new Error('Combined size legends require layout "edge".');
511
+ }
512
+ }
513
+ } else {
514
+ const plan = plans[0];
515
+ if (plan.kind === "size" || plan.kind === "strokeWidth") {
516
+ plan.config = resolveSampledLegendEdit(view, plan.kind, plan.config, patch).config;
517
+ } else if (["interval", "strokeInterval"].includes(plan.kind)) {
518
+ plan.config = resolveIntervalEdit(
519
+ view,
520
+ plan.config,
521
+ patch,
522
+ plan.kind === "strokeInterval" ? "stroke" : "color"
523
+ ).config;
524
+ }
525
+ else plan.config = resolveContinuousEdit(view, plan.kind, plan.config, patch).config;
526
+ }
527
+ plans = planLegendBlockTransitions(program, target, plans);
528
+ // The complete final content and styles are validated before removing resources.
529
+ for (const { kind, config } of plans) view = view._withLegendConfig(kind, config);
530
+ if (categorical !== undefined) resolveLayout(view, categorical.config);
531
+ let next = removeLegendKinds(program, removed);
532
+ for (const { kind, config, order } of plans) {
533
+ if (["series", "color", "stroke"].includes(kind)) {
534
+ next = createCategoricalLegendFromConfig(next, config, order);
535
+ }
536
+ else next = ({ size: createSizeLegendFromConfig, strokeWidth: createStrokeWidthLegendFromConfig,
537
+ gradient: createGradientLegendFromConfig, opacity: createOpacityLegendFromConfig,
538
+ interval: createIntervalLegendFromConfig,
539
+ strokeGradient: createStrokeGradientLegendFromConfig,
540
+ strokeInterval: createStrokeIntervalLegendFromConfig })[kind](next, config);
331
541
  }
332
542
  return next.rematerializeLegend();
333
543
  }
334
544
 
335
545
  export const editLegend = action(
336
- { op: "editLegend", description: "Edit one stable legend layout or appearance." },
337
- function (args = {}) {
546
+ { op: "editLegend", description: "Edit one stable legend content, layout or appearance." },
547
+ withGuideLayoutValidation(function (args = {}) {
338
548
  validateOptionObject(args, OPTIONS, "editLegend");
339
549
  const changes = Object.keys(args).filter(key => key !== "target");
340
550
  if (changes.length === 0) {
@@ -346,10 +556,25 @@ export const editLegend = action(
346
556
  throw new TypeError('editLegend title must be a non-empty string, "auto", or false.');
347
557
  }
348
558
  const target = resolveLegendTarget(this, args.target, "editLegend");
559
+ if (args.channels !== undefined) return editLegendContent(this, target, args);
349
560
  const configs = this.guideConfigs.legend ?? {};
350
- const categoricalKind = ["series", "color"].find(
561
+ const ownedKinds = Object.keys(configs).filter(
562
+ kind => configs[kind]?.target === target
563
+ );
564
+ if (args.values !== undefined && ownedKinds.length > 1) {
565
+ throw new Error(
566
+ "A target with multiple legend blocks requires a channel block selector for exact values."
567
+ );
568
+ }
569
+ const categoricalKinds = ["series", "color", "stroke"].filter(
351
570
  kind => configs[kind]?.target === target
352
571
  );
572
+ if (categoricalKinds.length > 1) {
573
+ throw new Error(
574
+ "editLegend requires channels when a target owns multiple categorical legend blocks."
575
+ );
576
+ }
577
+ const categoricalKind = categoricalKinds[0];
353
578
  if (categoricalKind !== undefined) {
354
579
  return editCategorical(
355
580
  this,
@@ -362,12 +587,18 @@ export const editLegend = action(
362
587
  if (configs.interval?.target === target) {
363
588
  return editInterval(this, configs.interval, args);
364
589
  }
590
+ if (configs.strokeInterval?.target === target) {
591
+ return editInterval(this, configs.strokeInterval, args, "stroke");
592
+ }
365
593
  if (configs.strokeWidth?.target === target) {
366
- return editStrokeWidth(this, configs.strokeWidth, args);
594
+ return editSampledLegend(this, "strokeWidth", configs.strokeWidth, args);
367
595
  }
368
- const continuousKind = ["gradient", "opacity"].find(
596
+ if (configs.size?.target === target) {
597
+ return editSampledLegend(this, "size", configs.size, args);
598
+ }
599
+ const continuousKind = ["gradient", "strokeGradient", "opacity"].find(
369
600
  kind => configs[kind]?.target === target
370
601
  );
371
602
  return editContinuous(this, continuousKind, configs[continuousKind], args);
372
- }
603
+ })
373
604
  );
@@ -20,7 +20,7 @@ function makeFocusedLegendEdit({ name, options, map }) {
20
20
  const editLegendLayout = makeFocusedLegendEdit({
21
21
  name: "editLegendLayout",
22
22
  options: [
23
- "position", "align", "direction", "columns", "offset",
23
+ "position", "layout", "align", "direction", "columns", "offset",
24
24
  "titlePosition", "itemGap"
25
25
  ],
26
26
  map: args => args
@@ -28,7 +28,7 @@ const editLegendLayout = makeFocusedLegendEdit({
28
28
 
29
29
  const editLegendLabels = makeFocusedLegendEdit({
30
30
  name: "editLegendLabels",
31
- options: ["color", "fontSize", "fontFamily", "fontWeight"],
31
+ options: ["offset", "color", "fontSize", "fontFamily", "fontWeight", "format"],
32
32
  map: ({ target, ...labels }) => ({ target, labels })
33
33
  });
34
34