ggaction 0.0.13 → 0.0.16

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 (359) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +41 -12
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +39 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/basic.js +2 -2
  13. package/src/actions/boxPlots/components.js +20 -12
  14. package/src/actions/boxPlots/create.js +4 -5
  15. package/src/actions/boxPlots/edit.js +34 -9
  16. package/src/actions/boxPlots/materialize.js +12 -4
  17. package/src/actions/boxPlots/options.js +48 -16
  18. package/src/actions/canvas/actions.js +3 -3
  19. package/src/actions/canvas/fitting.js +1 -1
  20. package/src/actions/categoryOrder/index.js +21 -12
  21. package/src/actions/charts/area.js +6 -2
  22. package/src/actions/charts/bar.js +3 -2
  23. package/src/actions/charts/beeswarm.js +1 -1
  24. package/src/actions/charts/density.js +7 -3
  25. package/src/actions/charts/ecdf.js +6 -3
  26. package/src/actions/charts/endpoints.js +29 -7
  27. package/src/actions/charts/heatmap.js +3 -2
  28. package/src/actions/charts/histogram.js +5 -4
  29. package/src/actions/charts/horizon.js +4 -2
  30. package/src/actions/charts/interval-regression.js +7 -4
  31. package/src/actions/charts/line.js +4 -2
  32. package/src/actions/charts/parallel.js +9 -3
  33. package/src/actions/charts/pie.js +11 -3
  34. package/src/actions/charts/polar.js +17 -6
  35. package/src/actions/charts/radar.js +9 -3
  36. package/src/actions/charts/radial.js +12 -4
  37. package/src/actions/charts/raincloud.js +2 -2
  38. package/src/actions/charts/rug-strip.js +8 -4
  39. package/src/actions/charts/scatter.js +4 -2
  40. package/src/actions/composition/actions.js +29 -24
  41. package/src/actions/coordinates/actions.js +2 -2
  42. package/src/actions/coordinates/edit.js +76 -0
  43. package/src/actions/data/basicBin2d.js +4 -5
  44. package/src/actions/data/bin.js +3 -3
  45. package/src/actions/data/bin2d.js +63 -193
  46. package/src/actions/data/bin2dMaterialize.js +4 -5
  47. package/src/actions/data/box.js +4 -4
  48. package/src/actions/data/complete.js +26 -0
  49. package/src/actions/data/computed.js +4 -4
  50. package/src/actions/data/create.js +7 -14
  51. package/src/actions/data/density.js +15 -23
  52. package/src/actions/data/derived.js +57 -31
  53. package/src/actions/data/ecdf.js +2 -2
  54. package/src/actions/data/edit.js +551 -0
  55. package/src/actions/data/filter.js +28 -22
  56. package/src/actions/data/fold.js +2 -2
  57. package/src/actions/data/gradientProfile.js +2 -2
  58. package/src/actions/data/horizon.js +2 -2
  59. package/src/actions/data/impute.js +26 -0
  60. package/src/actions/data/index.js +62 -47
  61. package/src/actions/data/interval.js +2 -2
  62. package/src/actions/data/intervalEdit.js +26 -18
  63. package/src/actions/data/normalize.js +27 -0
  64. package/src/actions/data/regression.js +21 -27
  65. package/src/actions/data/revise.js +48 -0
  66. package/src/actions/data/revisionBindings.js +125 -0
  67. package/src/actions/data/shared.js +20 -4
  68. package/src/actions/data/stack.js +2 -2
  69. package/src/actions/data/summary.js +3 -3
  70. package/src/actions/data/timeUnit.js +6 -3
  71. package/src/actions/data/window.js +3 -3
  72. package/src/actions/encodings/angle.js +1 -1
  73. package/src/actions/encodings/appearance.js +4 -4
  74. package/src/actions/encodings/barWidth.js +1 -1
  75. package/src/actions/encodings/channels.js +415 -0
  76. package/src/actions/encodings/color/index.js +3 -2
  77. package/src/actions/encodings/density.js +7 -4
  78. package/src/actions/encodings/histogram.js +4 -3
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/index.js +4 -0
  81. package/src/actions/encodings/offset.js +22 -15
  82. package/src/actions/encodings/parallel.js +4 -5
  83. package/src/actions/encodings/pathOrder.js +6 -11
  84. package/src/actions/encodings/position/apply.js +53 -7
  85. package/src/actions/encodings/position/index.js +14 -6
  86. package/src/actions/encodings/position/policies/bar.js +9 -0
  87. package/src/actions/encodings/position/resolve.js +24 -7
  88. package/src/actions/encodings/ranged.js +5 -5
  89. package/src/actions/encodings/remove.js +13 -9
  90. package/src/actions/encodings/ruleAppearance.js +2 -24
  91. package/src/actions/encodings/seriesLayout.js +2 -1
  92. package/src/actions/encodings/shared.js +82 -14
  93. package/src/actions/encodings/stroke.js +226 -0
  94. package/src/actions/encodings/strokeDash.js +2 -2
  95. package/src/actions/encodings/text.js +1 -1
  96. package/src/actions/errorBands/create.js +34 -20
  97. package/src/actions/errorBands/edit.js +34 -137
  98. package/src/actions/errorBars/create.js +43 -20
  99. package/src/actions/errorBars/edit.js +29 -125
  100. package/src/actions/facets/actions.js +267 -73
  101. package/src/actions/facets/derive.js +99 -28
  102. package/src/actions/facets/guides.js +4 -5
  103. package/src/actions/facets/replay.js +4 -5
  104. package/src/actions/gradientPlots/components.js +15 -8
  105. package/src/actions/gradientPlots/create.js +4 -5
  106. package/src/actions/gradientPlots/edit.js +18 -9
  107. package/src/actions/gradientPlots/materialize.js +5 -3
  108. package/src/actions/gradientPlots/options.js +10 -2
  109. package/src/actions/gradientPlots/rebind.js +4 -5
  110. package/src/actions/guides/applicability.js +8 -2
  111. package/src/actions/guides/axes/axes.js +23 -4
  112. package/src/actions/guides/axes/axis.js +2 -2
  113. package/src/actions/guides/axes/edit.js +2 -2
  114. package/src/actions/guides/axes/labels.js +67 -21
  115. package/src/actions/guides/axes/lines.js +15 -9
  116. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  117. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  118. package/src/actions/guides/axes/parallel.js +5 -5
  119. package/src/actions/guides/axes/remove.js +4 -4
  120. package/src/actions/guides/axes/tickGroups.js +6 -5
  121. package/src/actions/guides/axes/ticks.js +19 -6
  122. package/src/actions/guides/axes/titles.js +21 -13
  123. package/src/actions/guides/grids/grid.js +18 -29
  124. package/src/actions/guides/grids/resolve.js +7 -2
  125. package/src/actions/guides/guides.js +20 -4
  126. package/src/actions/guides/legends/blocks.js +426 -0
  127. package/src/actions/guides/legends/categorical/actions.js +138 -52
  128. package/src/actions/guides/legends/categorical/components.js +55 -45
  129. package/src/actions/guides/legends/categorical/index.js +37 -33
  130. package/src/actions/guides/legends/categorical/layout.js +98 -16
  131. package/src/actions/guides/legends/categorical/options.js +2 -2
  132. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  133. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  134. package/src/actions/guides/legends/categorical/symbols.js +104 -52
  135. package/src/actions/guides/legends/continuous/common.js +76 -20
  136. package/src/actions/guides/legends/continuous/gradient.js +95 -158
  137. package/src/actions/guides/legends/continuous/index.js +2 -0
  138. package/src/actions/guides/legends/continuous/interval.js +21 -16
  139. package/src/actions/guides/legends/continuous/opacity.js +37 -83
  140. package/src/actions/guides/legends/continuous/stroke.js +225 -0
  141. package/src/actions/guides/legends/creation.js +14 -1
  142. package/src/actions/guides/legends/edit.js +159 -60
  143. package/src/actions/guides/legends/focused.js +5 -5
  144. package/src/actions/guides/legends/index.js +2 -0
  145. package/src/actions/guides/legends/lane.js +59 -24
  146. package/src/actions/guides/legends/lifecycle.js +19 -8
  147. package/src/actions/guides/legends/remove.js +17 -8
  148. package/src/actions/guides/legends/sampling.js +140 -0
  149. package/src/actions/guides/legends/size.js +100 -108
  150. package/src/actions/guides/legends/strokeWidth.js +38 -92
  151. package/src/actions/guides/legends/target.js +112 -1
  152. package/src/actions/guides/legends/transition.js +165 -16
  153. package/src/actions/guides/polar/axes/facade.js +8 -8
  154. package/src/actions/guides/polar/axes/labels.js +29 -10
  155. package/src/actions/guides/polar/axes/lines.js +19 -19
  156. package/src/actions/guides/polar/axes/shared.js +16 -0
  157. package/src/actions/guides/polar/axes/ticks.js +10 -16
  158. package/src/actions/guides/polar/axes/titles.js +6 -6
  159. package/src/actions/guides/polar/grids.js +12 -14
  160. package/src/actions/guides/polar/resolve.js +12 -8
  161. package/src/actions/index.js +16 -3
  162. package/src/actions/intervals/revision.js +126 -0
  163. package/src/actions/marks/arc/actions.js +80 -22
  164. package/src/actions/marks/area/actions.js +38 -13
  165. package/src/actions/marks/area/materialize.js +17 -1
  166. package/src/actions/marks/bar/create.js +12 -3
  167. package/src/actions/marks/bar/edit.js +21 -4
  168. package/src/actions/marks/bar/materialize.js +34 -40
  169. package/src/actions/marks/index.js +10 -1
  170. package/src/actions/marks/line/actions.js +40 -17
  171. package/src/actions/marks/line/materialize.js +29 -10
  172. package/src/actions/marks/point/create.js +9 -3
  173. package/src/actions/marks/point/edit.js +19 -7
  174. package/src/actions/marks/point/jitter.js +6 -8
  175. package/src/actions/marks/point/materialize.js +48 -22
  176. package/src/actions/marks/point/packing.js +6 -8
  177. package/src/actions/marks/rect/actions.js +36 -15
  178. package/src/actions/marks/references.js +602 -33
  179. package/src/actions/marks/remove.js +67 -41
  180. package/src/actions/marks/rule/actions.js +51 -14
  181. package/src/actions/marks/shared.js +1 -1
  182. package/src/actions/marks/text/actions.js +79 -14
  183. package/src/actions/marks/text/index.js +4 -0
  184. package/src/actions/marks/text/layout.js +157 -17
  185. package/src/actions/marks/text/removal.js +173 -0
  186. package/src/actions/marks/text/selection.js +72 -0
  187. package/src/actions/marks/tick/actions.js +25 -8
  188. package/src/actions/primitives/createGraphics.js +5 -4
  189. package/src/actions/primitives/editGraphics.js +31 -4
  190. package/src/actions/primitives/graphicProperties.js +6 -0
  191. package/src/actions/primitives/semantic.js +11 -3
  192. package/src/actions/primitives/semanticAction.js +90 -14
  193. package/src/actions/primitives/semanticValidation/dataset.js +2 -4
  194. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  195. package/src/actions/primitives/semanticValidation/index.js +9 -1
  196. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  197. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  198. package/src/actions/regression/components.js +36 -25
  199. package/src/actions/regression/create.js +13 -6
  200. package/src/actions/regression/edit.js +24 -11
  201. package/src/actions/resources/index.js +7 -0
  202. package/src/actions/resources/remove.js +173 -0
  203. package/src/actions/scales/channels.js +212 -0
  204. package/src/actions/scales/consumers/common.js +1 -1
  205. package/src/actions/scales/consumers/families.js +3 -2
  206. package/src/actions/scales/consumers/index.js +19 -2
  207. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  208. package/src/actions/scales/create.js +55 -23
  209. package/src/actions/scales/definitions.js +94 -40
  210. package/src/actions/scales/edit.js +67 -12
  211. package/src/actions/scales/editPolicy.js +81 -11
  212. package/src/actions/scales/index.js +6 -0
  213. package/src/actions/scales/materialize.js +5 -4
  214. package/src/actions/scales/offset.js +48 -0
  215. package/src/actions/scales/parallel.js +71 -0
  216. package/src/actions/scales/preview.js +111 -10
  217. package/src/actions/scales/quantitativeColor.js +4 -5
  218. package/src/actions/selection/actions.js +179 -66
  219. package/src/actions/selection/index.js +3 -0
  220. package/src/actions/textMetrics/index.js +56 -0
  221. package/src/actions/theme/actions.js +70 -15
  222. package/src/actions/theme/composition.js +238 -0
  223. package/src/actions/theme/index.js +9 -2
  224. package/src/actions/theme/reconcile.js +313 -121
  225. package/src/actions/theme/state.js +160 -0
  226. package/src/actions/titles/actions.js +8 -8
  227. package/src/actions/titles/resolve.js +3 -3
  228. package/src/actions/violinPlots/create.js +6 -3
  229. package/src/actions/violinPlots/edit.js +2 -1
  230. package/src/core/action.js +117 -35
  231. package/src/core/compositionState.js +6 -1
  232. package/src/core/diagnostics.js +16 -0
  233. package/src/core/extensionRegistry.js +27 -0
  234. package/src/core/font.js +18 -0
  235. package/src/core/identifiers.js +5 -4
  236. package/src/core/immutable.js +10 -3
  237. package/src/core/optionRequirements.js +8 -0
  238. package/src/core/programState.js +21 -1
  239. package/src/core/resourceReferences.js +341 -0
  240. package/src/core/textMetricProfile.js +28 -0
  241. package/src/core/textMetrics.js +24 -3
  242. package/src/core/validation.js +22 -10
  243. package/src/core/vocabulary.js +16 -3
  244. package/src/diagnostics.js +1 -0
  245. package/src/grammar/bars/aggregate.js +4 -1
  246. package/src/grammar/bars/geometry.js +44 -0
  247. package/src/grammar/bin.js +54 -9
  248. package/src/grammar/complete.js +279 -0
  249. package/src/grammar/computed.js +265 -64
  250. package/src/grammar/coordinates.js +37 -0
  251. package/src/grammar/density.js +239 -37
  252. package/src/grammar/displayLabels.js +57 -0
  253. package/src/grammar/facets/dependencies.js +133 -1
  254. package/src/grammar/facets/guides.js +3 -0
  255. package/src/grammar/facets/index.js +7 -47
  256. package/src/grammar/facets/scales.js +15 -18
  257. package/src/grammar/impute.js +341 -0
  258. package/src/grammar/markLabelSelection.js +57 -0
  259. package/src/grammar/markLabels.js +18 -1
  260. package/src/grammar/normalize.js +366 -0
  261. package/src/grammar/pathSeries.js +26 -8
  262. package/src/grammar/pointShapes.js +13 -4
  263. package/src/grammar/polar.js +90 -8
  264. package/src/grammar/regression/index.js +1 -0
  265. package/src/grammar/regression/parameters.js +23 -0
  266. package/src/grammar/roundedRect.js +180 -0
  267. package/src/grammar/rules.js +1 -0
  268. package/src/grammar/scales/colorConsumers.js +20 -1
  269. package/src/grammar/scales/continuous.js +2 -2
  270. package/src/grammar/scales/definition.js +27 -4
  271. package/src/grammar/scales/index.js +1 -0
  272. package/src/grammar/scales/ordinal.js +12 -3
  273. package/src/grammar/scales/policies.js +2 -2
  274. package/src/grammar/scales/size.js +456 -0
  275. package/src/grammar/scales/types.js +8 -0
  276. package/src/grammar/schemas/concreteGraphic.js +14 -2
  277. package/src/grammar/schemas/graphic.js +15 -0
  278. package/src/grammar/schemas/graphicBounds.js +190 -41
  279. package/src/grammar/schemas/semanticPath.js +7 -2
  280. package/src/grammar/statisticalReference.js +103 -0
  281. package/src/grammar/strokeStyle.js +77 -0
  282. package/src/grammar/summary.js +78 -16
  283. package/src/grammar/timeUnit.js +216 -5
  284. package/src/grammar/transformTopology.js +4 -0
  285. package/src/grammar/transforms.js +394 -28
  286. package/src/grammar/weightedStatistics.js +374 -0
  287. package/src/grammar/window.js +221 -16
  288. package/src/layout/aspect.js +80 -0
  289. package/src/layout/facets.js +59 -5
  290. package/src/layout/labels.js +351 -7
  291. package/src/layout/legend.js +4 -4
  292. package/src/layout/legendItems.js +6 -6
  293. package/src/layout/text.js +10 -9
  294. package/src/layout/title.js +9 -9
  295. package/src/materialization/bars/aggregate.js +25 -2
  296. package/src/materialization/bars/histogram.js +84 -24
  297. package/src/materialization/bars/ranged.js +19 -3
  298. package/src/materialization/bars/resolve.js +7 -4
  299. package/src/materialization/composition.js +3 -1
  300. package/src/materialization/coordinateBounds.js +204 -0
  301. package/src/materialization/dependencies.js +36 -0
  302. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  303. package/src/materialization/facetGuides/placement.js +9 -3
  304. package/src/materialization/facetGuides/preparation.js +13 -8
  305. package/src/materialization/facetHeaders.js +347 -0
  306. package/src/materialization/facets.js +15 -65
  307. package/src/materialization/guides/layout.js +3 -3
  308. package/src/materialization/guides/resources.js +26 -0
  309. package/src/materialization/legends.js +1 -1
  310. package/src/materialization/marks/index.js +11 -5
  311. package/src/materialization/marks/policies.js +2 -1
  312. package/src/materialization/rect.js +25 -12
  313. package/src/materialization/rectConfig.js +5 -1
  314. package/src/materialization/scaleGuideDependencies.js +10 -0
  315. package/src/materialization/scales/map.js +5 -3
  316. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  317. package/src/materialization/scales/policies/offset.js +50 -8
  318. package/src/materialization/scales/policies/series.js +4 -1
  319. package/src/materialization/scales/resolve.js +22 -6
  320. package/src/materialization/selection/items/arc.js +28 -4
  321. package/src/materialization/selection/items/bar.js +30 -3
  322. package/src/materialization/selection/items/common.js +20 -5
  323. package/src/materialization/selection/items/rect.js +28 -4
  324. package/src/materialization/selection/path.js +1 -1
  325. package/src/materialization/selection/point.js +3 -1
  326. package/src/materialization/text.js +265 -14
  327. package/src/materialization/typography.js +31 -0
  328. package/src/mcp/cli.js +5 -3
  329. package/src/persistence/codec.js +65 -0
  330. package/src/persistence/validation.js +185 -0
  331. package/src/persistence.js +74 -0
  332. package/src/renderers/canvas/circle.js +2 -3
  333. package/src/renderers/canvas/index.js +5 -4
  334. package/src/renderers/canvas/line.js +2 -3
  335. package/src/renderers/canvas/native.js +4 -4
  336. package/src/renderers/canvas/path.js +2 -3
  337. package/src/renderers/canvas/rect.js +2 -3
  338. package/src/renderers/canvas/stroke.js +11 -0
  339. package/src/renderers/nativeBackend.js +10 -0
  340. package/src/renderers/options.js +13 -0
  341. package/src/renderers/pdf.js +25 -54
  342. package/src/renderers/png.js +22 -17
  343. package/src/renderers/svg.js +29 -14
  344. package/src/renderers/text.js +1 -11
  345. package/src/selectors/coordinates.js +3 -1
  346. package/src/selectors/datasets.js +38 -5
  347. package/src/selectors/layers.js +10 -4
  348. package/src/selectors/markOwners.js +46 -0
  349. package/src/selectors/scales.js +6 -4
  350. package/src/theme/defaults.js +103 -1
  351. package/src/version.js +2 -0
  352. package/types/accessibility.d.ts +37 -0
  353. package/types/basic.d.ts +10 -2
  354. package/types/diagnostics.d.ts +15 -0
  355. package/types/index.d.ts +90 -0
  356. package/types/pdf.d.ts +9 -0
  357. package/types/persistence.d.ts +7 -0
  358. package/types/png.d.ts +9 -0
  359. package/types/program.d.ts +944 -118
@@ -1,10 +1,14 @@
1
- import { action } from "../../../core/action.js";
1
+ import { action, closedAction } from "../../../core/action.js";
2
2
  import {
3
- validateGeneratedItemLimit,
4
3
  validateNonEmptyString,
5
4
  validateKeys
6
5
  } from "../../../core/validation.js";
7
- import { mapLinearValues } from "../../../grammar/scales/index.js";
6
+ import {
7
+ formatDiscretizedIntervals,
8
+ isDiscreteSizeScaleType,
9
+ isSizeScaleType,
10
+ mapSizeValues
11
+ } from "../../../grammar/scales/index.js";
8
12
  import { resolveLegendItemLayout } from "../../../layout/legendItems.js";
9
13
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
10
14
  "../../../theme/defaults.js";
@@ -13,7 +17,8 @@ import { resolveLegendGraphicPlacement } from
13
17
  "../../../materialization/graphicHierarchy.js";
14
18
  import {
15
19
  assertLegendBoundsInsideCanvas,
16
- editLegendBackground,
20
+ materializeItemLegend,
21
+ createItemLegendGraphics,
17
22
  normalizeItemLegendLayout,
18
23
  normalizeLegendBorder,
19
24
  normalizeLegendTextOptions,
@@ -21,13 +26,17 @@ import {
21
26
  resolveContinuousBounds,
22
27
  resolveLegendBackgroundFromBounds,
23
28
  formatContinuousValues,
24
- sampleContinuousValues,
25
- selectLegendLayer,
26
- styleContinuousText
29
+ selectLegendLayer
27
30
  } from "./continuous/common.js";
28
31
  import { legendResourcePolicies } from "../../../materialization/guides/resources.js";
32
+ import {
33
+ normalizeLegendSampling,
34
+ readLegendSampling,
35
+ resolveLegendSampleValues
36
+ } from "./sampling.js";
37
+ import { resolveEffectiveLegendBlockConfig } from "./blocks.js";
29
38
 
30
- const SIZE_OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
39
+ const SIZE_OPTIONS = Object.freeze(["target", "count", "values", "position", "layout", "align",
31
40
  "direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
32
41
 
33
42
  export const SIZE_LEGEND_LABELS = Object.freeze({
@@ -61,21 +70,32 @@ export function resolveSizeLegendPoint(program, requested) {
61
70
  return layer;
62
71
  }
63
72
 
64
- function requireScale(program, id, type) {
73
+ function requireScale(program, id) {
65
74
  const scale = program.resolvedScales[id];
66
- if (scale?.type !== type) {
67
- throw new Error(`Legend requires resolved ${type} scale "${id}".`);
75
+ if (!isSizeScaleType(scale?.type)) {
76
+ throw new Error(`Legend requires resolved size scale "${id}".`);
68
77
  }
69
78
  return scale;
70
79
  }
71
80
 
72
81
  export function resolveSizeLegendLayout(program, config) {
73
- const scale = requireScale(program, config.scale, "linear");
74
- const categorical = [program.guideConfigs.legend?.series, program.guideConfigs.legend?.color]
82
+ const scale = requireScale(program, config.scale);
83
+ const categorical = [
84
+ program.guideConfigs.legend?.series,
85
+ program.guideConfigs.legend?.color,
86
+ program.guideConfigs.legend?.stroke
87
+ ]
75
88
  .find(candidate => candidate?.target === config.target);
76
89
  const inherit = config.inheritAppearance === true && categorical !== undefined;
77
- const labels = inherit ? { ...categorical.labels, offset: config.labels.offset } : config.labels;
78
- const titleStyle = inherit ? categorical.titleStyle : config.titleStyle;
90
+ const inheritedLabels = inherit ? { ...categorical.labels, offset: config.labels.offset } : config.labels;
91
+ const inheritedTitleStyle = inherit ? categorical.titleStyle : config.titleStyle;
92
+ const effective = resolveEffectiveLegendBlockConfig(program, "size", {
93
+ ...config,
94
+ labels: inheritedLabels,
95
+ titleStyle: inheritedTitleStyle
96
+ });
97
+ const labels = effective.labels;
98
+ const titleStyle = effective.titleStyle;
79
99
  const position = categorical?.position ?? config.position;
80
100
  if (categorical?.layout === "legacy-bottom") {
81
101
  throw new Error('Combined size legends require layout "edge".');
@@ -84,36 +104,54 @@ export function resolveSizeLegendLayout(program, config) {
84
104
  const geometry = horizontal ? {
85
105
  position, align: categorical.align, direction: categorical.direction,
86
106
  columns: categorical.columns, titlePosition: categorical.titlePosition,
87
- offset: categorical.offset, itemGap: categorical.itemGap
107
+ offset: categorical.offset, itemGap: effective.blockGap ?? categorical.itemGap
88
108
  } : { position };
89
- const values = sampleContinuousValues(scale.domain, config.count);
90
- const areas = mapLinearValues(values, scale.domain, scale.range, { clamp: scale.clamp ?? false });
109
+ const discrete = isDiscreteSizeScaleType(scale.type);
110
+ if (discrete && readLegendSampling(config).mode === "values") {
111
+ throw new Error("Discrete size legends do not support exact values.");
112
+ }
113
+ const values = discrete
114
+ ? undefined
115
+ : resolveLegendSampleValues(effective, scale, "Size legend");
116
+ const areas = discrete
117
+ ? scale.range
118
+ : mapSizeValues(values, scale);
91
119
  const radii = areas.map(area => Math.sqrt(area / Math.PI));
92
120
  const radius = Math.max(...radii);
93
121
  const width = Math.max(32, radius * 2);
94
122
  const { plot, canvas } = resolveContinuousBounds(program);
95
- const text = formatContinuousValues(
96
- values,
97
- scale.domain,
98
- "quantitative",
99
- config.labels.format
100
- );
101
- const layout = resolveLegendItemLayout(plot, { ...config, ...geometry, labels, titleStyle }, text, {
102
- width, height: radius * 2,
103
- itemBounds: radii.map(r => ({ left: width / 2 - r, right: width / 2 + r, top: -r, bottom: r }))
104
- });
105
- assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Size legend layout", { ...config, ...geometry });
106
- const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas, "Size legend", { ...config, ...geometry });
107
- return { ...layout, symbolX: layout.symbolX.map(x => x + width / 2), radii, text, labels, titleStyle, background };
123
+ const text = discrete
124
+ ? formatDiscretizedIntervals(scale.thresholds, config.labels.format)
125
+ : formatContinuousValues(
126
+ values,
127
+ scale.domain,
128
+ "quantitative",
129
+ config.labels.format
130
+ );
131
+ const symbolStroke = effective.blockSymbol?.strokeWidth ?? 0;
132
+ const symbolExtent = symbolStroke / 2;
133
+ const sampleWidth = width + symbolStroke;
134
+ const layout = resolveLegendItemLayout(plot, { ...effective, ...geometry, labels, titleStyle }, text, {
135
+ width: sampleWidth, height: radius * 2 + symbolStroke,
136
+ itemBounds: radii.map(r => ({
137
+ left: sampleWidth / 2 - r - symbolExtent,
138
+ right: sampleWidth / 2 + r + symbolExtent,
139
+ top: -r - symbolExtent,
140
+ bottom: r + symbolExtent
141
+ }))
142
+ }, undefined, program.materializationConfigs.textMetrics);
143
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Size legend layout", { ...effective, ...geometry });
144
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, effective.border, canvas, "Size legend", { ...effective, ...geometry });
145
+ return { ...layout, symbolX: layout.symbolX.map(x => x + sampleWidth / 2), radii, text,
146
+ labels, titleStyle, background, config: effective };
108
147
  }
109
148
 
110
- export const rematerializeSizeLegend = action(
149
+ export const rematerializeSizeLegend = /* @__PURE__ */ closedAction(
111
150
  {
112
151
  op: "rematerializeSizeLegend",
113
152
  description: "Rematerialize a quantitative point-size legend."
114
- },
153
+ }, [],
115
154
  function (args = {}) {
116
- validateKeys(args, [], "rematerializeSizeLegend");
117
155
  const config = this.guideConfigs.legend?.size;
118
156
  if (config === undefined) throw new Error("Size legend requires stored configuration.");
119
157
  const layer = findLayer(this, config.target);
@@ -121,7 +159,7 @@ export const rematerializeSizeLegend = action(
121
159
  if (encoding?.scale === undefined) {
122
160
  throw new Error("Size legend target requires a size encoding.");
123
161
  }
124
- const scale = requireScale(this, encoding.scale, "linear");
162
+ const scale = requireScale(this, encoding.scale);
125
163
  const title = config.inferredTitle === true ? encoding.field : config.title;
126
164
  const currentConfig = {
127
165
  ...config,
@@ -130,41 +168,18 @@ export const rematerializeSizeLegend = action(
130
168
  domain: scale.domain
131
169
  };
132
170
  const layout = resolveSizeLegendLayout(this, currentConfig);
133
- const { itemY, symbolX, labelX, radii, labels, titleStyle } = layout;
134
- let next = this
135
- .editSemantic({ property: "guide.legend.size.scale", value: encoding.scale })
136
- .editSemantic({ property: "guide.legend.size.title", value: title })
137
- ._withLegendConfig("size", currentConfig)
138
- .editGraphics({ target: "sizeLegendSymbols", property: "length", value: radii.length })
139
- .editGraphics({ target: "sizeLegendSymbols", property: "x", value: symbolX })
140
- .editGraphics({ target: "sizeLegendSymbols", property: "y", value: itemY })
141
- .editGraphics({
142
- target: "sizeLegendSymbols",
143
- property: "radius",
144
- value: radii
145
- })
146
- .editGraphics({
147
- target: "sizeLegendSymbols",
148
- property: "fill",
149
- value: DEFAULT_COLORS.sizeSymbol
150
- })
151
- .editGraphics({ target: "sizeLegendSymbols", property: "opacity", value: 0.7 })
152
- .editGraphics({ target: "sizeLegendLabels", property: "length", value: radii.length })
153
- .editGraphics({ target: "sizeLegendLabels", property: "x", value: labelX })
154
- .editGraphics({ target: "sizeLegendLabels", property: "y", value: itemY })
155
- .editGraphics({
156
- target: "sizeLegendLabels",
157
- property: "text",
158
- value: layout.text
159
- });
160
- next = editLegendBackground(next, "sizeLegendBackground", layout.background, currentConfig.border);
161
- next = styleContinuousText(next, "sizeLegendLabels", labels);
162
- if (currentConfig.titleVisible === false) return next;
163
- next = next
164
- .editGraphics({ target: "sizeLegendTitle", property: "x", value: layout.title.x })
165
- .editGraphics({ target: "sizeLegendTitle", property: "y", value: layout.title.y })
166
- .editGraphics({ target: "sizeLegendTitle", property: "text", value: title });
167
- return styleContinuousText(next, "sizeLegendTitle", titleStyle, { align: layout.title.align });
171
+ return materializeItemLegend(this, "size", currentConfig, layout, {
172
+ text: layout.text, labels: layout.labels, titleStyle: layout.titleStyle,
173
+ symbols: {
174
+ x: layout.symbolX, y: layout.itemY, radius: layout.radii,
175
+ fill: layout.config.blockSymbol?.fill ?? DEFAULT_COLORS.sizeSymbol,
176
+ opacity: layout.config.blockSymbol?.opacity ?? 0.7
177
+ },
178
+ optional: {
179
+ stroke: layout.config.blockSymbol?.stroke,
180
+ strokeWidth: layout.config.blockSymbol?.strokeWidth
181
+ }
182
+ });
168
183
  }
169
184
  );
170
185
 
@@ -177,12 +192,16 @@ export function resolveSizeLegendConfig(program, args = {}) {
177
192
  if (encoding?.scale === undefined) {
178
193
  throw new Error(`Point mark "${layer.id}" requires a size encoding.`);
179
194
  }
180
- const scale = requireScale(program, encoding.scale, "linear");
181
- const count = args.count ?? 5;
182
- if (!Number.isInteger(count) || count < 2) {
183
- throw new RangeError("Size legend count must be an integer of at least 2.");
195
+ const scale = requireScale(program, encoding.scale);
196
+ const discrete = isDiscreteSizeScaleType(scale.type);
197
+ if (discrete && (Object.hasOwn(args, "count") || Object.hasOwn(args, "values"))) {
198
+ throw new Error("Discrete size legends do not support count or exact values.");
184
199
  }
185
- validateGeneratedItemLimit(count, "Size legend count");
200
+ const sampling = discrete ? undefined : normalizeLegendSampling(args, {
201
+ operation: "create",
202
+ label: "Size legend"
203
+ });
204
+ const count = discrete ? scale.range.length : undefined;
186
205
  return {
187
206
  target: layer.id,
188
207
  scale: encoding.scale,
@@ -190,7 +209,7 @@ export function resolveSizeLegendConfig(program, args = {}) {
190
209
  title: args.title ?? encoding.field,
191
210
  inferredTitle: args.title === undefined,
192
211
  domain: scale.domain,
193
- count,
212
+ ...(discrete ? { count } : { sampling }),
194
213
  inheritAppearance: args.inheritAppearance === true,
195
214
  labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", SIZE_LEGEND_LABELS),
196
215
  titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", SIZE_LEGEND_TITLE_STYLE),
@@ -200,48 +219,21 @@ export function resolveSizeLegendConfig(program, args = {}) {
200
219
  }
201
220
 
202
221
  export function createSizeLegendFromConfig(program, config) {
203
- resolveSizeLegendLayout(program, config);
204
- const { count } = config;
222
+ const { radii } = resolveSizeLegendLayout(program, config);
223
+ const count = radii.length;
205
224
  const following = new Set(legendResourcePolicies().filter(policy =>
206
225
  policy.kind !== "size" && policy.family !== "categorical" &&
207
226
  program.guideConfigs.legend?.[policy.kind] !== undefined
208
227
  ).flatMap(policy => policy.graphicIds));
209
228
  const before = program.graphicSpec.objects.canvas?.children?.find(id => following.has(id));
210
229
  const placement = resolveLegendGraphicPlacement(program, before === undefined ? {} : { before });
211
- let next = program
212
- .editSemantic({ property: "guide.legend.size.scale", value: config.scale })
213
- .editSemantic({ property: "guide.legend.size.title", value: config.title })
214
- ._withLegendConfig("size", config);
215
- if (config.border !== false) {
216
- next = next.createGraphics({ id: "sizeLegendBackground", type: "rect",
217
- ...placement });
218
- }
219
- next = next.createGraphics({
220
- id: "sizeLegendSymbols",
221
- type: "circle",
222
- length: count,
223
- ...placement
224
- })
225
- .createGraphics({
226
- id: "sizeLegendLabels",
227
- type: "text",
228
- length: count,
229
- ...placement
230
- });
231
- if (config.titleVisible !== false) {
232
- next = next.createGraphics({
233
- id: "sizeLegendTitle",
234
- type: "text",
235
- ...placement
236
- });
237
- }
238
- return next.rematerializeSizeLegend();
230
+ return createItemLegendGraphics(program, "size", config, count, "circle", placement).rematerializeSizeLegend();
239
231
  }
240
232
 
241
- export const createSizeLegend = action(
233
+ export const createSizeLegend = /* @__PURE__ */ action(
242
234
  {
243
235
  op: "createSizeLegend",
244
- description: "Create a quantitative equal-area point-size legend."
236
+ description: "Create an equal-area point-size legend."
245
237
  },
246
238
  function (args = {}) {
247
239
  const config = resolveSizeLegendConfig(this, args);
@@ -1,6 +1,5 @@
1
- import { action } from "../../../core/action.js";
1
+ import { action, closedAction } from "../../../core/action.js";
2
2
  import {
3
- validateGeneratedItemLimit,
4
3
  validateNonEmptyString,
5
4
  validateKeys
6
5
  } from "../../../core/validation.js";
@@ -13,7 +12,8 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
13
12
  "../../../theme/defaults.js";
14
13
  import {
15
14
  assertLegendBoundsInsideCanvas,
16
- editLegendBackground,
15
+ materializeItemLegend,
16
+ createItemLegendGraphics,
17
17
  normalizeItemLegendLayout,
18
18
  normalizeLegendBorder,
19
19
  normalizeLegendTextOptions,
@@ -21,12 +21,15 @@ import {
21
21
  resolveContinuousBounds,
22
22
  resolveLegendBackgroundFromBounds,
23
23
  formatContinuousValues,
24
- sampleContinuousValues,
25
- selectLegendLayer,
26
- styleContinuousText
24
+ selectLegendLayer
27
25
  } from "./continuous/common.js";
26
+ import {
27
+ normalizeLegendSampling,
28
+ resolveLegendSampleValues
29
+ } from "./sampling.js";
30
+ import { resolveEffectiveLegendBlockConfig } from "./blocks.js";
28
31
 
29
- const OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
32
+ const OPTIONS = Object.freeze(["target", "count", "values", "position", "layout", "align",
30
33
  "direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
31
34
 
32
35
  export const STROKE_WIDTH_LEGEND_LABELS = Object.freeze({
@@ -74,29 +77,29 @@ function requireScale(program, id) {
74
77
  }
75
78
 
76
79
  export function resolveStrokeWidthLegendLayout(program, config) {
80
+ const effective = resolveEffectiveLegendBlockConfig(program, "strokeWidth", config);
77
81
  const scale = requireScale(program, config.scale);
78
82
  const { plot, canvas } = resolveContinuousBounds(program);
79
- const values = sampleContinuousValues(scale.domain, config.count);
83
+ const values = resolveLegendSampleValues(effective, scale, "Stroke-width legend");
80
84
  const widths = mapContinuousScaleValues(values, scale);
81
85
  const labels = formatContinuousValues(
82
86
  values,
83
87
  scale.domain,
84
88
  "quantitative",
85
- config.labels.format
89
+ effective.labels.format
86
90
  );
87
- const layout = resolveLegendItemLayout(plot, config, labels, { width: 32, height: 0, strokeWidth: widths });
88
- assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Stroke-width legend layout", config);
89
- const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas, "Stroke-width legend", config);
90
- return { ...layout, widths, labels, background };
91
+ const layout = resolveLegendItemLayout(plot, effective, labels, { width: 32, height: 0, strokeWidth: widths }, undefined, program.materializationConfigs.textMetrics);
92
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Stroke-width legend layout", effective);
93
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, effective.border, canvas, "Stroke-width legend", effective);
94
+ return { ...layout, widths, labels, background, config: effective };
91
95
  }
92
96
 
93
- export const rematerializeStrokeWidthLegend = /* @__PURE__ */ action(
97
+ export const rematerializeStrokeWidthLegend = /* @__PURE__ */ closedAction(
94
98
  {
95
99
  op: "rematerializeStrokeWidthLegend",
96
100
  description: "Rematerialize a quantitative stroke-width legend."
97
- },
101
+ }, [],
98
102
  function (args = {}) {
99
- validateKeys(args, [], "rematerializeStrokeWidthLegend");
100
103
  const stored = this.guideConfigs.legend?.strokeWidth;
101
104
  if (stored === undefined) {
102
105
  throw new Error("Stroke-width legend requires stored configuration.");
@@ -114,44 +117,17 @@ export const rematerializeStrokeWidthLegend = /* @__PURE__ */ action(
114
117
  }
115
118
  const scale = requireScale(this, encoding.scale);
116
119
  const title = config.inferredTitle === true ? encoding.field : config.title;
117
- const layout = resolveStrokeWidthLegendLayout(this, { ...config, scale: encoding.scale, title });
118
- const { itemY: y, widths } = layout;
119
- let next = this
120
- .editSemantic({ property: "guide.legend.strokeWidth.scale", value: encoding.scale })
121
- .editSemantic({ property: "guide.legend.strokeWidth.title", value: title })
122
- ._withLegendConfig("strokeWidth", {
123
- ...config,
124
- scale: encoding.scale,
125
- title,
126
- domain: scale.domain
127
- })
128
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "length", value: widths.length })
129
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "x1", value: layout.symbolX })
130
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "x2", value: layout.symbolX.map(x => x + 32) })
131
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "y1", value: y })
132
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "y2", value: y })
133
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "stroke", value: DEFAULT_COLORS.mark })
134
- .editGraphics({ target: "strokeWidthLegendSymbols", property: "strokeWidth", value: widths })
135
- .editGraphics({ target: "strokeWidthLegendLabels", property: "length", value: widths.length })
136
- .editGraphics({
137
- target: "strokeWidthLegendLabels",
138
- property: "x",
139
- value: layout.labelX
140
- })
141
- .editGraphics({ target: "strokeWidthLegendLabels", property: "y", value: y })
142
- .editGraphics({
143
- target: "strokeWidthLegendLabels",
144
- property: "text",
145
- value: layout.labels
146
- });
147
- next = editLegendBackground(next, "strokeWidthLegendBackground", layout.background, config.border);
148
- next = styleContinuousText(next, "strokeWidthLegendLabels", config.labels);
149
- if (config.titleVisible === false) return next;
150
- next = next
151
- .editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: layout.title.x })
152
- .editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: layout.title.y })
153
- .editGraphics({ target: "strokeWidthLegendTitle", property: "text", value: title });
154
- return styleContinuousText(next, "strokeWidthLegendTitle", config.titleStyle, { align: layout.title.align });
120
+ const currentConfig = { ...config, scale: encoding.scale, title };
121
+ const layout = resolveStrokeWidthLegendLayout(this, currentConfig);
122
+ return materializeItemLegend(this, "strokeWidth", { ...currentConfig, domain: scale.domain }, layout, {
123
+ symbols: {
124
+ x1: layout.symbolX, x2: layout.symbolX.map(x => x + 32),
125
+ y1: layout.itemY, y2: layout.itemY,
126
+ stroke: layout.config.blockSymbol?.stroke ?? DEFAULT_COLORS.mark,
127
+ strokeWidth: layout.widths
128
+ },
129
+ optional: { opacity: layout.config.blockSymbol?.opacity }
130
+ });
155
131
  }
156
132
  );
157
133
 
@@ -162,20 +138,17 @@ export function resolveStrokeWidthLegendConfig(program, args = {}) {
162
138
  const layer = resolveLayer(program, args.target);
163
139
  const encoding = layer.encoding.strokeWidth;
164
140
  requireScale(program, encoding.scale);
165
- const count = args.count ?? 5;
166
- if (!Number.isInteger(count) || count < 2) {
167
- throw new RangeError(
168
- "Stroke-width legend count must be an integer of at least 2."
169
- );
170
- }
171
- validateGeneratedItemLimit(count, "Stroke-width legend count");
141
+ const sampling = normalizeLegendSampling(args, {
142
+ operation: "create",
143
+ label: "Stroke-width legend"
144
+ });
172
145
  return {
173
146
  target: layer.id,
174
147
  scale: encoding.scale,
175
148
  ...normalizeItemLegendLayout({ ...args, itemGap: args.itemGap ?? 32 }),
176
149
  title: args.title ?? encoding.field,
177
150
  inferredTitle: args.title === undefined,
178
- count,
151
+ sampling,
179
152
  labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", STROKE_WIDTH_LEGEND_LABELS),
180
153
  titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", STROKE_WIDTH_LEGEND_TITLE_STYLE),
181
154
  border: normalizeLegendBorder(args.border),
@@ -184,36 +157,9 @@ export function resolveStrokeWidthLegendConfig(program, args = {}) {
184
157
  }
185
158
 
186
159
  export function createStrokeWidthLegendFromConfig(program, config) {
187
- resolveStrokeWidthLegendLayout(program, config);
188
- const { count } = config;
189
- let next = program
190
- .editSemantic({ property: "guide.legend.strokeWidth.scale", value: config.scale })
191
- .editSemantic({ property: "guide.legend.strokeWidth.title", value: config.title })
192
- ._withLegendConfig("strokeWidth", config);
193
- if (config.border !== false) {
194
- next = next.createGraphics({ id: "strokeWidthLegendBackground", type: "rect",
195
- ...resolveLegendGraphicPlacement(program) });
196
- }
197
- next = next.createGraphics({
198
- id: "strokeWidthLegendSymbols",
199
- type: "line",
200
- length: count,
201
- ...resolveLegendGraphicPlacement(program)
202
- })
203
- .createGraphics({
204
- id: "strokeWidthLegendLabels",
205
- type: "text",
206
- length: count,
207
- ...resolveLegendGraphicPlacement(program)
208
- });
209
- if (config.titleVisible !== false) {
210
- next = next.createGraphics({
211
- id: "strokeWidthLegendTitle",
212
- type: "text",
213
- ...resolveLegendGraphicPlacement(program)
214
- });
215
- }
216
- return next.rematerializeStrokeWidthLegend();
160
+ const layout = resolveStrokeWidthLegendLayout(program, config);
161
+ const count = layout.widths.length;
162
+ return createItemLegendGraphics(program, "strokeWidth", config, count, "line", resolveLegendGraphicPlacement(program)).rematerializeStrokeWidthLegend();
217
163
  }
218
164
 
219
165
  export const createStrokeWidthLegend = /* @__PURE__ */ action(
@@ -1,5 +1,9 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
- import { LEGEND_CHANNELS } from "../../../core/vocabulary.js";
2
+ import {
3
+ LEGEND_CHANNELS,
4
+ LEGEND_CONFIG_KINDS
5
+ } from "../../../core/vocabulary.js";
6
+ import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
3
7
 
4
8
  export function validateLegendChannels(channels, operation) {
5
9
  if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
@@ -35,3 +39,110 @@ export function resolveLegendTarget(program, requested, operation) {
35
39
  }
36
40
  return targets[0];
37
41
  }
42
+
43
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
44
+ const SINGLE_CHANNELS = Object.freeze({
45
+ size: "size",
46
+ opacity: "opacity",
47
+ strokeWidth: "strokeWidth",
48
+ gradient: "color",
49
+ interval: "color",
50
+ strokeGradient: "stroke",
51
+ strokeInterval: "stroke"
52
+ });
53
+
54
+ const asciiCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
55
+
56
+ export function legendBlockChannels(kind, config) {
57
+ const channels = CATEGORICAL_KINDS.includes(kind)
58
+ ? config?.channels
59
+ : SINGLE_CHANNELS[kind] === undefined
60
+ ? undefined
61
+ : [SINGLE_CHANNELS[kind]];
62
+ if (!Array.isArray(channels) || channels.length === 0) {
63
+ throw new Error(`Legend block ${kind} requires at least one channel.`);
64
+ }
65
+ validateLegendChannels(channels, `Legend block ${kind}`);
66
+ return Object.freeze([...channels].sort(asciiCompare));
67
+ }
68
+
69
+ export function legendBlockKey(channels) {
70
+ if (!Array.isArray(channels) || channels.length === 0) {
71
+ throw new TypeError("Legend block key requires a non-empty channel array.");
72
+ }
73
+ return JSON.stringify([...channels].sort(asciiCompare));
74
+ }
75
+
76
+ export function describeLegendBlock(program, kind, config) {
77
+ if (!LEGEND_CONFIG_KINDS.includes(kind)) {
78
+ throw new Error(`Unknown legend block kind "${kind}".`);
79
+ }
80
+ const channels = legendBlockChannels(kind, config);
81
+ const scaleIds = CATEGORICAL_KINDS.includes(kind)
82
+ ? config?.scales
83
+ : [config?.scale];
84
+ if (!Array.isArray(scaleIds) || scaleIds.length === 0 ||
85
+ scaleIds.some(id => typeof id !== "string" || program.resolvedScales[id] === undefined)) {
86
+ throw new Error(`Legend block ${kind} requires resolved scales.`);
87
+ }
88
+ const family = CATEGORICAL_KINDS.includes(kind)
89
+ ? "categorical"
90
+ : kind === "size"
91
+ ? isDiscreteSizeScaleType(program.resolvedScales[scaleIds[0]].type)
92
+ ? "discrete-size"
93
+ : "sampled"
94
+ : ["opacity", "strokeWidth"].includes(kind)
95
+ ? "sampled"
96
+ : ["gradient", "strokeGradient"].includes(kind)
97
+ ? "gradient"
98
+ : "interval";
99
+ return Object.freeze({
100
+ target: config.target,
101
+ kind,
102
+ family,
103
+ key: legendBlockKey(channels),
104
+ channels,
105
+ scaleIds: Object.freeze([...scaleIds]),
106
+ config
107
+ });
108
+ }
109
+
110
+ export function legendBlockDescriptors(program, target) {
111
+ const requested = validateUserId(target, "Legend target id");
112
+ const descriptors = LEGEND_CONFIG_KINDS.flatMap(kind => {
113
+ const config = program.guideConfigs.legend?.[kind];
114
+ return config?.target === requested
115
+ ? [describeLegendBlock(program, kind, config)]
116
+ : [];
117
+ });
118
+ const seen = new Set();
119
+ for (const descriptor of descriptors) {
120
+ if (seen.has(descriptor.key)) {
121
+ throw new Error(
122
+ `Legend target "${requested}" has ambiguous block key ${descriptor.key}.`
123
+ );
124
+ }
125
+ seen.add(descriptor.key);
126
+ }
127
+ return Object.freeze(descriptors);
128
+ }
129
+
130
+ export function resolveLegendBlock(program, { target, channel }, operation = "editLegendBlock") {
131
+ const requested = resolveLegendTarget(program, target, operation);
132
+ if (!LEGEND_CHANNELS.includes(channel)) {
133
+ throw new Error(`${operation} has unsupported legend channel "${channel}".`);
134
+ }
135
+ const matches = legendBlockDescriptors(program, requested)
136
+ .filter(descriptor => descriptor.channels.includes(channel));
137
+ if (matches.length === 0) {
138
+ throw new Error(
139
+ `${operation} target "${requested}" has no legend block for channel "${channel}".`
140
+ );
141
+ }
142
+ if (matches.length !== 1) {
143
+ throw new Error(
144
+ `${operation} target "${requested}" has ambiguous channel "${channel}".`
145
+ );
146
+ }
147
+ return matches[0];
148
+ }