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,42 +1,66 @@
1
- import { isPlainObject } from "../../../../core/immutable.js";
2
- import { noOptions } from "../../../../core/validation.js";
1
+ import { validateOptionObject } from "../../../../core/validation.js";
3
2
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
4
- import { unionConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
5
3
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
4
  import { formatVisibleText } from "../../../../core/textMetrics.js";
7
5
  import {
8
6
  assertLegendBoundsInsideCanvas,
9
- formatContinuousValues,
10
7
  resolveContinuousBounds,
11
- resolveLegendBackgroundFromBounds,
12
- resolveLegendTextBounds,
13
- sampleContinuousValues
8
+ resolveLegendBackgroundFromBounds
14
9
  } from "../continuous/common.js";
10
+ import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
11
+ import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
12
+ import { resolveConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
13
+ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
14
+ import { resolveDisplayLabel } from "../../../../grammar/displayLabels.js";
15
15
  import {
16
- alignLegendStart,
17
- measureLegendSymbolHeight,
18
- measureLegendTextWidth,
19
- resolveLegendGrid
20
- } from "../../../../layout/legend.js";
16
+ materializeRectItem,
17
+ requestedRectStyleDetails
18
+ } from "../../../../grammar/roundedRect.js";
19
+ import { requestedStrokeDetails } from "../../../../grammar/strokeStyle.js";
21
20
 
22
- export function activeConfig(program) {
23
- const kinds = ["series", "color"]
21
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
22
+
23
+ export function activeConfig(program, requested) {
24
+ if (requested !== undefined && !CATEGORICAL_KINDS.includes(requested)) {
25
+ throw new Error(`Unknown categorical legend kind "${requested}".`);
26
+ }
27
+ const kinds = CATEGORICAL_KINDS
24
28
  .filter(kind => program.guideConfigs.legend?.[kind] !== undefined);
29
+ if (requested !== undefined) {
30
+ if (!kinds.includes(requested)) {
31
+ throw new Error(`Missing categorical legend config "${requested}".`);
32
+ }
33
+ return {
34
+ kind: requested,
35
+ config: resolveEffectiveLegendBlockConfig(
36
+ program,
37
+ requested,
38
+ program.guideConfigs.legend[requested]
39
+ )
40
+ };
41
+ }
25
42
  if (kinds.length !== 1) {
26
43
  throw new Error("Legend component requires one categorical legend config.");
27
44
  }
28
45
  const kind = kinds[0];
29
- return { kind, config: program.guideConfigs.legend[kind] };
46
+ return {
47
+ kind,
48
+ config: resolveEffectiveLegendBlockConfig(
49
+ program,
50
+ kind,
51
+ program.guideConfigs.legend[kind]
52
+ )
53
+ };
30
54
  }
31
55
 
32
56
  function prefix(config) {
33
- return config.kind === "series" ? "seriesLegend" : "colorLegend";
57
+ return `${config.kind}Legend`;
34
58
  }
35
59
 
36
60
  export function symbolGraphic(config, type) {
37
61
  const onlyDefault = config.symbol.layers.length === 1 &&
38
62
  ((config.kind === "series" && type === "line") ||
39
- (config.kind === "color" && type === "swatch"));
63
+ (["color", "stroke"].includes(config.kind) && type === "swatch"));
40
64
  if (onlyDefault) return `${prefix(config)}Symbols`;
41
65
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
42
66
  return `${prefix(config)}Symbol${suffix}`;
@@ -54,364 +78,109 @@ export function symbolWidth(config) {
54
78
  }));
55
79
  }
56
80
 
57
- function leftGuideBoundary(program, bounds) {
58
- const ids = ["yAxisLine", "yAxisTicks", "yAxisLabels", "yAxisTitle"]
59
- .filter(id => program.graphicSpec.objects[id] !== undefined);
60
- const occupied = unionConcreteGraphicBounds(program.graphicSpec, ids);
61
- return occupied === undefined ? bounds.x : Math.min(bounds.x, occupied.left);
81
+ export function categoricalLegendLabels(config) {
82
+ return config.domain.map(value => resolveDisplayLabel(
83
+ value,
84
+ config.labelMap,
85
+ formatVisibleText
86
+ ));
62
87
  }
63
88
 
64
- function textBounds(x, y, value, style, textAlign = "left") {
65
- return resolveLegendTextBounds(
66
- { x, y, align: textAlign },
67
- formatVisibleText(value),
68
- style
69
- );
70
- }
71
-
72
- function resolveLeftSizeMetrics(program, sizeConfig) {
73
- if (sizeConfig === undefined) return undefined;
74
- const scale = program.resolvedScales[sizeConfig.scale];
75
- if (scale?.type !== "linear") {
76
- throw new Error(`Legend requires resolved linear scale "${sizeConfig.scale}".`);
77
- }
78
- const values = sampleContinuousValues(scale.domain, sizeConfig.count);
79
- const labels = formatContinuousValues(values, scale.domain, "quantitative");
80
- const maximumRadius = Math.sqrt(Math.max(...scale.range) / Math.PI);
81
- return { values, labels, maximumRadius };
82
- }
83
-
84
- function resolveLeftLayout(program, bounds, canvas, config, width, count) {
85
- const sizeConfig = program.guideConfigs.legend?.size;
86
- const size = resolveLeftSizeMetrics(program, sizeConfig);
87
- const padding = config.border === false ? 0 : config.border.padding;
88
- const categoricalWidth = config.domain.reduce((maximum, value) => Math.max(
89
- maximum,
90
- width + config.labels.offset +
91
- measureLegendTextWidth(value, config.labels)
92
- ), config.titleVisible === false
93
- ? 0
94
- : measureLegendTextWidth(config.title, config.titleStyle));
95
- const sizeWidth = size === undefined
96
- ? 0
97
- : size.labels.reduce((maximum, label) => Math.max(
98
- maximum,
99
- 44 + measureLegendTextWidth(label, config.labels)
100
- ), measureLegendTextWidth(sizeConfig.title, config.titleStyle));
101
- const contentWidth = Math.max(categoricalWidth, sizeWidth);
102
- const occupiedRight = bounds.x - config.offset;
103
- const contentRight = occupiedRight - padding;
104
- const start = contentRight - contentWidth;
105
- if (start < padding) {
106
- throw new Error("Legend layout requires more left-margin space.");
107
- }
108
- if (occupiedRight >= leftGuideBoundary(program, bounds)) {
109
- throw new Error("Left legend and y-axis guides require more left-margin space.");
110
- }
111
- const symbolX = Array(count).fill(start);
112
- const itemY = Array.from(
113
- { length: count },
114
- (_, index) => bounds.y + 52 + index * config.itemGap
115
- );
116
- const labelX = symbolX.map(value => value + width + config.labels.offset);
117
- const titleX = start;
118
- const titleY = bounds.y + 20;
119
- const sizeTitleY = bounds.y + 56 + count * 34 + 22;
120
- const sizeItemY = size?.values.map((_, index) => sizeTitleY + 34 + index * 40);
121
- const sizeSymbolX = size?.values.map(() => start + 16);
122
- const sizeLabelX = size?.values.map(() => start + 44);
123
- const categoricalTop = config.titleVisible === false
124
- ? itemY[0] - Math.max(
125
- config.labels.fontSize,
126
- measureLegendSymbolHeight(config)
127
- ) / 2
128
- : titleY - config.titleStyle.fontSize / 2;
129
- const sizeBottom = size === undefined
130
- ? -Infinity
131
- : sizeItemY.at(-1) + Math.max(size.maximumRadius, config.labels.fontSize / 2);
132
- const categoricalBottom = itemY.at(-1) +
133
- Math.max(config.labels.fontSize, measureLegendSymbolHeight(config)) / 2;
134
- const blockTop = Math.min(categoricalTop, sizeTitleY - config.titleStyle.fontSize / 2);
135
- const blockBottom = Math.max(categoricalBottom, sizeBottom);
136
- if (blockTop < 0 || blockBottom > canvas.height) {
137
- throw new Error("Legend layout requires more vertical Canvas space.");
138
- }
139
- let background;
140
- if (config.border !== false) {
141
- const backgroundTop = Math.floor(
142
- blockTop - config.border.padding - config.border.lineWidth
143
- );
144
- const backgroundBottom = Math.ceil(blockBottom + config.border.padding);
145
- background = {
146
- x: start - config.border.padding,
147
- y: backgroundTop,
148
- width: occupiedRight - (start - config.border.padding),
149
- height: backgroundBottom - backgroundTop
150
- };
151
- if (
152
- background.x < 0 || background.y < 0 ||
153
- background.x + background.width > canvas.width ||
154
- background.y + background.height > canvas.height
155
- ) {
156
- throw new Error("Legend background requires more left-margin space.");
157
- }
158
- }
159
- return {
160
- symbolX,
161
- itemY,
162
- labelX,
163
- titleX,
164
- titleY,
165
- background,
166
- blockTop,
167
- blockBottom,
168
- size: size === undefined ? undefined : {
169
- titleX: start,
170
- titleY: sizeTitleY,
171
- itemY: sizeItemY,
172
- symbolX: sizeSymbolX,
173
- labelX: sizeLabelX
174
- }
175
- };
176
- }
177
-
178
- function resolveGridLayout(program, bounds, canvas, config, width, count, top) {
179
- const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
180
- resolveLegendGrid(config, width, count);
181
- const inlineTitle = config.titlePosition === "left";
182
- const titleWidth = config.titleVisible === false
183
- ? 0
184
- : measureLegendTextWidth(config.title, config.titleStyle);
185
- const titleGap = inlineTitle ? 20 : 12;
186
- const totalWidth = inlineTitle
187
- ? titleWidth + titleGap + gridWidth
188
- : Math.max(titleWidth, gridWidth);
189
- const start = alignLegendStart(bounds, totalWidth, config.align);
190
- if (start < 0 || start + totalWidth > canvas.width) {
191
- throw new Error("Legend layout requires more horizontal Canvas space.");
192
- }
193
- const edge = top
194
- ? bounds.y - config.offset
195
- : bounds.y + bounds.height + config.offset;
196
- const gridStart = inlineTitle
197
- ? start + titleWidth + titleGap
198
- : start + (totalWidth - gridWidth) / 2;
199
- const gridTop = top
200
- ? edge - gridHeight
201
- : inlineTitle ? edge : edge + config.titleStyle.fontSize + titleGap;
202
- const blockTop = top
203
- ? inlineTitle
204
- ? Math.min(
205
- gridTop,
206
- edge / 2 + gridTop / 2 - config.titleStyle.fontSize / 2
207
- )
208
- : gridTop - titleGap - config.titleStyle.fontSize
209
- : edge;
210
- const blockBottom = top ? edge : gridTop + gridHeight;
211
- if (top) {
212
- if (blockTop < 0) {
213
- throw new Error("Legend layout requires more top-margin space.");
214
- }
215
- const titleGraphic = program.graphicSpec.objects.chartSubtitle ??
216
- program.graphicSpec.objects.chartTitle;
217
- if (titleGraphic?.type === "text" &&
218
- titleGraphic.properties.y + titleGraphic.properties.fontSize / 2 >= blockTop) {
219
- throw new Error("Top legend and chart title require more top-margin space.");
220
- }
221
- } else {
222
- const occupiedTop = config.border === false
223
- ? blockTop
224
- : blockTop - config.border.padding;
225
- const axisTitle = program.graphicSpec.objects.xAxisTitle;
226
- if (axisTitle?.type === "text" &&
227
- axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop) {
228
- throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
229
- }
230
- const occupiedBottom = config.border === false
231
- ? blockBottom
232
- : blockBottom + config.border.padding;
233
- if (occupiedTop < 0 || occupiedBottom > canvas.height) {
234
- throw new Error("Legend layout requires more bottom-margin space.");
235
- }
236
- }
237
- const columnX = [];
238
- let cursor = gridStart;
239
- for (const columnWidth of columnWidths) {
240
- columnX.push(cursor);
241
- cursor += columnWidth + config.itemGap;
242
- }
243
- const symbolX = cells.map(cell => columnX[cell.column]);
244
- const labelX = symbolX.map(value => value + width + config.labels.offset);
245
- const itemY = cells.map(cell =>
246
- gridTop + rowHeight / 2 + cell.row * (rowHeight + config.itemGap)
247
- );
248
- const titleX = inlineTitle ? start : start + totalWidth / 2;
249
- const titleY = inlineTitle
250
- ? gridTop + gridHeight / 2
251
- : top
252
- ? gridTop - titleGap - config.titleStyle.fontSize / 2
253
- : blockTop + config.titleStyle.fontSize / 2;
254
- let background;
255
- if (config.border !== false) {
256
- background = {
257
- x: start - config.border.padding,
258
- y: blockTop - config.border.padding,
259
- width: totalWidth + config.border.padding * 2,
260
- height: blockBottom - blockTop + config.border.padding * 2
261
- };
262
- if (background.x < 0 || background.y < 0 ||
263
- background.x + background.width > canvas.width ||
264
- (!top && background.y + background.height > canvas.height)) {
265
- throw new Error(`Legend background requires more ${top ? "top" : "bottom"} or horizontal space.`);
266
- }
267
- }
268
- return {
269
- symbolX,
270
- itemY,
271
- labelX,
272
- titleX,
273
- titleY,
274
- background,
275
- blockTop,
276
- blockBottom
277
- };
278
- }
279
-
280
- function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
281
- const labels = config.domain.map(formatVisibleText);
282
- const itemWidths = labels.map(
283
- label => width + config.labels.offset +
284
- measureLegendTextWidth(label, config.labels)
285
- );
286
- const totalWidth = itemWidths.reduce((sum, value) => sum + value, 0) +
287
- config.itemGap * Math.max(0, count - 1);
288
- const start = config.align === "center"
289
- ? (canvas.width - totalWidth) / 2
290
- : alignLegendStart(bounds, totalWidth, config.align);
291
- if (start < 0 || start + totalWidth > canvas.width) {
292
- throw new Error("Legend layout requires more horizontal Canvas space.");
293
- }
294
- const symbolX = [];
295
- const labelX = [];
296
- let cursor = start;
297
- for (let index = 0; index < count; index += 1) {
298
- symbolX.push(cursor);
299
- labelX.push(cursor + width + config.labels.offset);
300
- cursor += itemWidths[index] + config.itemGap;
301
- }
302
- const itemY = Array(count).fill(canvas.height - 28);
303
- const titleX = start + totalWidth / 2;
304
- const titleY = canvas.height - 52;
305
- if (titleY <= bounds.y + bounds.height) {
306
- throw new Error("Legend layout requires more bottom-margin space.");
307
- }
308
- let background;
309
- if (config.border !== false) {
310
- const maxHeight = Math.max(
311
- config.labels.fontSize,
312
- measureLegendSymbolHeight(config)
313
- );
314
- const x = start - config.border.padding;
315
- const y = titleY - config.titleStyle.fontSize / 2 - config.border.padding;
316
- background = {
317
- x,
318
- y,
319
- width: totalWidth + config.border.padding * 2,
320
- height: itemY[0] + maxHeight / 2 + config.border.padding - y
321
- };
322
- if (
323
- background.x < 0 || background.y < 0 ||
324
- background.x + background.width > canvas.width ||
325
- background.y + background.height > canvas.height
326
- ) {
327
- throw new Error("Legend background requires more bottom or horizontal space.");
328
- }
329
- }
330
- return { symbolX, itemY, labelX, titleX, titleY, background };
89
+ function resolveSampleBounds(program, config, width) {
90
+ const appearance = resolveAppearance(program, config);
91
+ return config.domain.map((_, index) => {
92
+ const items = config.symbol.layers.map(layer => {
93
+ if (layer.type === "line") {
94
+ const x = (width - layer.length) / 2;
95
+ return { type: "line", properties: { x1: x, x2: x + layer.length, y1: 0, y2: 0,
96
+ stroke: layer.stroke ?? (config.channels.includes("stroke")
97
+ ? appearance.strokes[index]
98
+ : appearance.colors[index]),
99
+ strokeWidth: layer.lineWidth, opacity: layer.opacity,
100
+ ...requestedStrokeDetails(layer, "Legend line symbol") } };
101
+ }
102
+ if (layer.type === "swatch") {
103
+ const details = requestedRectStyleDetails(layer, "Legend swatch symbol");
104
+ return materializeRectItem({ x: (width - layer.width) / 2, y: -layer.height / 2,
105
+ width: layer.width, height: layer.height, strokeWidth: layer.strokeWidth,
106
+ stroke: config.channels.includes("stroke")
107
+ ? appearance.strokes[index]
108
+ : layer.stroke,
109
+ opacity: layer.opacity,
110
+ ...requestedStrokeDetails(details, "Legend swatch symbol")
111
+ }, details.cornerRadius ?? 0);
112
+ }
113
+ if (config.channels.includes("shape")) {
114
+ return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
115
+ area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
116
+ stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity,
117
+ ...requestedStrokeDetails(layer, "Legend point symbol") });
118
+ }
119
+ return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
120
+ stroke: config.channels.includes("stroke")
121
+ ? appearance.strokes[index]
122
+ : layer.stroke,
123
+ strokeWidth: layer.strokeWidth, opacity: layer.opacity,
124
+ ...requestedStrokeDetails(layer, "Legend point symbol") } };
125
+ });
126
+ return items.map((item, itemIndex) => {
127
+ const bounds = resolveConcreteGraphicBounds({
128
+ objects: { sample: item },
129
+ order: ["sample"]
130
+ }, "sample");
131
+ if (config.symbol.layers[itemIndex].type !== "line") return bounds;
132
+ const extent = config.symbol.layers[itemIndex].lineWidth / 2;
133
+ return {
134
+ ...bounds,
135
+ left: bounds.left - extent,
136
+ right: bounds.right + extent
137
+ };
138
+ }).reduce((result, bounds) => result === undefined
139
+ ? bounds
140
+ : {
141
+ left: Math.min(result.left, bounds.left),
142
+ right: Math.max(result.right, bounds.right),
143
+ top: Math.min(result.top, bounds.top),
144
+ bottom: Math.max(result.bottom, bounds.bottom)
145
+ }, undefined);
146
+ });
331
147
  }
332
148
 
333
149
  export function resolveLayout(program, config) {
334
- const resolved = resolveContinuousBounds(
335
- program,
336
- "Legend layout requires Canvas bounds, width, and height."
337
- );
338
- const bounds = resolved.plot;
339
- const canvas = resolved.canvas;
340
-
341
- const count = config.domain.length;
150
+ const { plot, canvas } = resolveContinuousBounds(program,
151
+ "Legend layout requires Canvas bounds, width, and height.");
342
152
  const width = symbolWidth(config);
343
- if (config.position === "right") {
344
- const symbolX = Array(count).fill(
345
- bounds.x + bounds.width + config.offset
346
- );
347
- const itemY = Array.from(
348
- { length: count },
349
- (_, index) => bounds.y + 52 + index * config.itemGap
350
- );
351
- const labelX = symbolX.map(value => value + width + config.labels.offset);
352
- const titleX = symbolX[0];
353
- const titleY = bounds.y + 20;
354
- const labelBounds = config.domain.map((value, index) => textBounds(
355
- labelX[index], itemY[index], value, config.labels
356
- ));
357
- const titleBounds = config.titleVisible === false
358
- ? undefined
359
- : textBounds(titleX, titleY, config.title, config.titleStyle);
360
- const symbolHeight = measureLegendSymbolHeight(config);
361
- const symbolBounds = itemY.map(y => ({
362
- left: symbolX[0],
363
- right: symbolX[0] + width,
364
- top: y - symbolHeight / 2,
365
- bottom: y + symbolHeight / 2
366
- }));
367
- const contentBounds = [
368
- ...labelBounds,
369
- ...symbolBounds,
370
- ...(titleBounds === undefined ? [] : [titleBounds])
371
- ];
372
- assertLegendBoundsInsideCanvas(
373
- contentBounds,
374
- canvas,
375
- "Right legend layout"
376
- );
377
- const background = resolveLegendBackgroundFromBounds(
378
- contentBounds,
379
- config.border,
380
- canvas,
381
- "Right legend"
382
- );
383
- return { symbolX, itemY, labelX, titleX, titleY, background };
384
- }
385
-
386
- if (config.position === "left") {
387
- return resolveLeftLayout(program, bounds, canvas, config, width, count);
388
- }
389
-
390
- if (config.position === "top") {
391
- return resolveGridLayout(program, bounds, canvas, config, width, count, true);
392
- }
393
-
394
- if (config.bottomGrid !== true) {
395
- return resolveCompactBottomLayout(bounds, canvas, config, width, count);
396
- }
397
- return resolveGridLayout(program, bounds, canvas, config, width, count, false);
153
+ const layout = resolveLegendItemLayout(plot, config, categoricalLegendLabels(config), {
154
+ width, height: 0, itemBounds: resolveSampleBounds(program, config, width)
155
+ }, canvas);
156
+ assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Categorical legend layout", config);
157
+ const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas,
158
+ "Categorical legend", config);
159
+ return { ...layout, titleX: layout.title.x, titleY: layout.title.y, background };
398
160
  }
399
161
 
400
162
  export function resolveAppearance(program, config) {
401
163
  let colors = config.domain.map(() => DEFAULT_COLORS.mark);
164
+ let strokes = config.domain.map(() => "white");
402
165
  let dashes = config.domain.map(() => []);
403
166
  let shapes = config.domain.map(() => "circle");
404
167
  for (let index = 0; index < config.channels.length; index += 1) {
405
168
  const scale = program.resolvedScales[config.scales[index]];
406
169
  const values = mapOrdinalValues(config.domain, scale.domain, scale.range);
407
170
  if (config.channels[index] === "color") colors = values;
171
+ if (config.channels[index] === "stroke") strokes = values;
408
172
  if (config.channels[index] === "strokeDash") dashes = values;
409
173
  if (config.channels[index] === "shape") shapes = values;
410
174
  }
411
- return { colors, dashes, shapes };
175
+ return { colors, strokes, dashes, shapes };
412
176
  }
413
177
 
414
- export { noOptions };
178
+ export function noOptions(args, operation) {
179
+ validateOptionObject(args, ["kind"], operation);
180
+ if (args.kind !== undefined && !CATEGORICAL_KINDS.includes(args.kind)) {
181
+ throw new Error(`Unknown categorical legend kind "${args.kind}".`);
182
+ }
183
+ }
415
184
 
416
185
  export function layerFor(config, type) {
417
186
  const layer = config.symbol.layers.find(item => item.type === type);
@@ -16,7 +16,9 @@ export const CHANNELS = CATEGORICAL_LEGEND_CHANNELS;
16
16
  const OPTIONS = Object.freeze([
17
17
  "target",
18
18
  "channels",
19
+ "order",
19
20
  "position",
21
+ "layout",
20
22
  "align",
21
23
  "direction",
22
24
  "columns",
@@ -34,7 +36,8 @@ const TEXT_OPTIONS = Object.freeze([
34
36
  "color",
35
37
  "fontSize",
36
38
  "fontFamily",
37
- "fontWeight"
39
+ "fontWeight",
40
+ "format"
38
41
  ]);
39
42
  const TITLE_OPTIONS = Object.freeze([
40
43
  "color",
@@ -85,10 +88,13 @@ function normalizeBorder(border) {
85
88
  return normalized;
86
89
  }
87
90
 
88
- export function normalizeOptions(args, kind) {
91
+ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
89
92
  validateOptionObject(args, OPTIONS, "createLegend");
90
93
  if (Object.hasOwn(args, "labels")) {
91
94
  validateObject(args.labels, TEXT_OPTIONS, "createLegend.labels");
95
+ if (args.labels.format !== undefined && args.labels.format !== "auto") {
96
+ throw new Error("Categorical legend labels do not accept format.");
97
+ }
92
98
  }
93
99
  if (Object.hasOwn(args, "titleStyle")) {
94
100
  validateObject(
@@ -108,26 +114,33 @@ export function normalizeOptions(args, kind) {
108
114
  offset: defaults.offset,
109
115
  ...(args.labels ?? {})
110
116
  };
117
+ delete labels.format;
111
118
  const titleStyle = {
112
119
  ...COMMON_DEFAULTS.titleStyle,
113
120
  ...(args.titleStyle ?? {})
114
121
  };
115
122
  const position = args.position ?? defaults.position;
116
123
  const align = args.align ?? defaults.align;
117
- const direction = args.direction ?? (position === "left" ? "vertical" : "horizontal");
124
+ const side = ["left", "right"].includes(position);
125
+ const direction = args.direction ?? (side ? "vertical" : "horizontal");
118
126
  const columns = args.columns;
119
127
  const offset = args.offset ?? 8;
120
128
  const titlePosition = args.titlePosition ?? "top";
121
129
  const itemGap = args.itemGap ?? (["right", "left"].includes(position)
122
130
  ? 28
123
131
  : position === "top" ? 24 : 20);
124
- const bottomGrid = position !== "bottom" || [
125
- "columns",
126
- "direction",
127
- "offset",
128
- "titlePosition",
129
- "itemGap"
130
- ].some(key => Object.hasOwn(args, key));
132
+ const layout = args.layout === undefined ? "edge" : args.layout;
133
+ if (!["edge", "legacy-bottom"].includes(layout)) {
134
+ throw new Error(`Unsupported legend layout "${layout}".`);
135
+ }
136
+ if (layout === "legacy-bottom" && position !== "bottom") {
137
+ throw new Error('Legend layout "legacy-bottom" requires position "bottom".');
138
+ }
139
+ if (layout === "legacy-bottom" && (
140
+ columns !== undefined || direction !== "horizontal" || titlePosition !== "top" || offset !== 8
141
+ )) {
142
+ throw new Error('Legend columns, vertical direction, left title and custom offset require layout "edge".');
143
+ }
131
144
 
132
145
  if (!["right", "left", "bottom", "top"].includes(position)) {
133
146
  throw new Error(`Unsupported legend position "${position}".`);
@@ -141,11 +154,14 @@ export function normalizeOptions(args, kind) {
141
154
  if (!["horizontal", "vertical"].includes(direction)) {
142
155
  throw new Error(`Unsupported legend direction "${direction}".`);
143
156
  }
144
- if (position === "left" && direction !== "vertical") {
145
- throw new Error("Left legends currently require vertical direction.");
157
+ if (side && direction !== "vertical") {
158
+ throw new Error("Side legends require vertical direction.");
159
+ }
160
+ if (side && columns !== undefined && columns !== 1) {
161
+ throw new Error("Side legends require one column.");
146
162
  }
147
- if (position === "left" && columns !== undefined) {
148
- throw new Error("Left legends do not accept columns.");
163
+ if (side && titlePosition !== "top") {
164
+ throw new Error("Side legends require a top title.");
149
165
  }
150
166
  if (columns !== undefined && (!Number.isInteger(columns) || columns <= 0)) {
151
167
  throw new RangeError("Legend columns must be a positive integer.");
@@ -175,11 +191,11 @@ export function normalizeOptions(args, kind) {
175
191
  offset,
176
192
  titlePosition,
177
193
  title: args.title,
178
- symbol: normalizeRecipe(args.symbol, kind),
194
+ symbol: normalizeRecipe(args.symbol, kind, { internal: internalSymbol }),
179
195
  labels,
180
196
  titleStyle,
181
197
  itemGap,
182
- bottomGrid,
198
+ layout,
183
199
  border: normalizeBorder(args.border)
184
200
  };
185
201
  }