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,4 +1,4 @@
1
- import { noOptions } from "../../../../core/validation.js";
1
+ import { validateOptionObject } from "../../../../core/validation.js";
2
2
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
3
3
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
4
4
  import { formatVisibleText } from "../../../../core/textMetrics.js";
@@ -10,25 +10,57 @@ import {
10
10
  import { resolveLegendItemLayout } from "../../../../layout/legendItems.js";
11
11
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
12
12
  import { resolveConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
13
+ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
14
+ import { resolveDisplayLabel } from "../../../../grammar/displayLabels.js";
15
+ import {
16
+ materializeRectItem,
17
+ requestedRectStyleDetails
18
+ } from "../../../../grammar/roundedRect.js";
19
+ import { requestedStrokeDetails } from "../../../../grammar/strokeStyle.js";
20
+
21
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
13
22
 
14
- export function activeConfig(program) {
15
- const kinds = ["series", "color"]
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
16
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
+ }
17
42
  if (kinds.length !== 1) {
18
43
  throw new Error("Legend component requires one categorical legend config.");
19
44
  }
20
45
  const kind = kinds[0];
21
- 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
+ };
22
54
  }
23
55
 
24
56
  function prefix(config) {
25
- return config.kind === "series" ? "seriesLegend" : "colorLegend";
57
+ return `${config.kind}Legend`;
26
58
  }
27
59
 
28
60
  export function symbolGraphic(config, type) {
29
61
  const onlyDefault = config.symbol.layers.length === 1 &&
30
62
  ((config.kind === "series" && type === "line") ||
31
- (config.kind === "color" && type === "swatch"));
63
+ (["color", "stroke"].includes(config.kind) && type === "swatch"));
32
64
  if (onlyDefault) return `${prefix(config)}Symbols`;
33
65
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
34
66
  return `${prefix(config)}Symbol${suffix}`;
@@ -46,6 +78,14 @@ export function symbolWidth(config) {
46
78
  }));
47
79
  }
48
80
 
81
+ export function categoricalLegendLabels(config) {
82
+ return config.domain.map(value => resolveDisplayLabel(
83
+ value,
84
+ config.labelMap,
85
+ formatVisibleText
86
+ ));
87
+ }
88
+
49
89
  function resolveSampleBounds(program, config, width) {
50
90
  const appearance = resolveAppearance(program, config);
51
91
  return config.domain.map((_, index) => {
@@ -53,21 +93,56 @@ function resolveSampleBounds(program, config, width) {
53
93
  if (layer.type === "line") {
54
94
  const x = (width - layer.length) / 2;
55
95
  return { type: "line", properties: { x1: x, x2: x + layer.length, y1: 0, y2: 0,
56
- strokeWidth: layer.lineWidth } };
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") } };
57
101
  }
58
102
  if (layer.type === "swatch") {
59
- return { type: "rect", properties: { x: (width - layer.width) / 2, y: -layer.height / 2,
60
- width: layer.width, height: layer.height, strokeWidth: layer.strokeWidth } };
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);
61
112
  }
62
113
  if (config.channels.includes("shape")) {
63
114
  return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
64
115
  area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
65
- stroke: layer.stroke, strokeWidth: layer.strokeWidth });
116
+ stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity,
117
+ ...requestedStrokeDetails(layer, "Legend point symbol") });
66
118
  }
67
119
  return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
68
- strokeWidth: layer.strokeWidth } };
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") } };
69
125
  });
70
- return resolveConcreteGraphicBounds({ objects: { sample: { type: "collection", items } }, order: ["sample"] }, "sample");
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);
71
146
  });
72
147
  }
73
148
 
@@ -75,9 +150,9 @@ export function resolveLayout(program, config) {
75
150
  const { plot, canvas } = resolveContinuousBounds(program,
76
151
  "Legend layout requires Canvas bounds, width, and height.");
77
152
  const width = symbolWidth(config);
78
- const layout = resolveLegendItemLayout(plot, config, config.domain.map(formatVisibleText), {
153
+ const layout = resolveLegendItemLayout(plot, config, categoricalLegendLabels(config), {
79
154
  width, height: 0, itemBounds: resolveSampleBounds(program, config, width)
80
- }, canvas);
155
+ }, canvas, program.materializationConfigs.textMetrics);
81
156
  assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Categorical legend layout", config);
82
157
  const background = resolveLegendBackgroundFromBounds(layout.bounds, config.border, canvas,
83
158
  "Categorical legend", config);
@@ -86,19 +161,26 @@ export function resolveLayout(program, config) {
86
161
 
87
162
  export function resolveAppearance(program, config) {
88
163
  let colors = config.domain.map(() => DEFAULT_COLORS.mark);
164
+ let strokes = config.domain.map(() => "white");
89
165
  let dashes = config.domain.map(() => []);
90
166
  let shapes = config.domain.map(() => "circle");
91
167
  for (let index = 0; index < config.channels.length; index += 1) {
92
168
  const scale = program.resolvedScales[config.scales[index]];
93
169
  const values = mapOrdinalValues(config.domain, scale.domain, scale.range);
94
170
  if (config.channels[index] === "color") colors = values;
171
+ if (config.channels[index] === "stroke") strokes = values;
95
172
  if (config.channels[index] === "strokeDash") dashes = values;
96
173
  if (config.channels[index] === "shape") shapes = values;
97
174
  }
98
- return { colors, dashes, shapes };
175
+ return { colors, strokes, dashes, shapes };
99
176
  }
100
177
 
101
- 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
+ }
102
184
 
103
185
  export function layerFor(config, type) {
104
186
  const layer = config.symbol.layers.find(item => item.type === type);
@@ -88,7 +88,7 @@ function normalizeBorder(border) {
88
88
  return normalized;
89
89
  }
90
90
 
91
- export function normalizeOptions(args, kind) {
91
+ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
92
92
  validateOptionObject(args, OPTIONS, "createLegend");
93
93
  if (Object.hasOwn(args, "labels")) {
94
94
  validateObject(args.labels, TEXT_OPTIONS, "createLegend.labels");
@@ -191,7 +191,7 @@ export function normalizeOptions(args, kind) {
191
191
  offset,
192
192
  titlePosition,
193
193
  title: args.title,
194
- symbol: normalizeRecipe(args.symbol, kind),
194
+ symbol: normalizeRecipe(args.symbol, kind, { internal: internalSymbol }),
195
195
  labels,
196
196
  titleStyle,
197
197
  itemGap,
@@ -5,6 +5,15 @@ import {
5
5
  positive,
6
6
  validateKeys
7
7
  } from "./validation.js";
8
+ import {
9
+ requestedRectStyleDetails,
10
+ RECT_STYLE_PROPERTIES
11
+ } from "../../../../grammar/roundedRect.js";
12
+ import {
13
+ requestedStrokeDetails,
14
+ STROKE_STYLE_PROPERTIES
15
+ } from "../../../../grammar/strokeStyle.js";
16
+ import { findLayerMatching } from "../../../../selectors/layers.js";
8
17
 
9
18
  const LAYER_OPTIONS = Object.freeze({
10
19
  line: Object.freeze(["type", "length", "lineWidth"]),
@@ -24,19 +33,101 @@ const LAYER_OPTIONS = Object.freeze({
24
33
  "strokeWidth"
25
34
  ])
26
35
  });
36
+ const INTERNAL_LAYER_OPTIONS = Object.freeze({
37
+ line: Object.freeze([...LAYER_OPTIONS.line, ...STROKE_STYLE_PROPERTIES]),
38
+ point: Object.freeze([...LAYER_OPTIONS.point, ...STROKE_STYLE_PROPERTIES]),
39
+ swatch: Object.freeze([...LAYER_OPTIONS.swatch, ...RECT_STYLE_PROPERTIES])
40
+ });
27
41
 
28
- export function resolveLegendSymbol(program, layer, channels, requested) {
42
+ export function resolveLegendSymbol(program, layer, channels, requested, kind) {
29
43
  if (requested !== undefined && requested !== "auto") return requested;
30
- if (layer.mark?.type !== "point" || !channels?.includes("shape")) return undefined;
31
- const color = channels.includes("color") ? layer.encoding?.color : undefined;
32
- const hasMatchingLine = color?.scale !== undefined && program.semanticSpec.layers.some(candidate =>
33
- candidate.mark?.type === "line" && candidate.encoding?.color?.field === color.field &&
34
- candidate.encoding?.color?.scale === color.scale
35
- );
36
- return { layers: [
37
- ...(hasMatchingLine ? [{ type: "line", length: 32, lineWidth: 3 }] : []),
38
- { type: "point", size: Math.sqrt(64 / Math.PI), stroke: "white", strokeWidth: 0 }
39
- ] };
44
+ const config = program.markConfigs[layer.id] ?? {};
45
+ const bar = config.barAppearance ?? {};
46
+ const styleOwner = layer.mark.type === "bar" ? bar : config;
47
+ const strokeDetails = requestedStrokeDetails(styleOwner, "Legend source");
48
+ const rectDetails = ["bar", "rect"].includes(layer.mark.type)
49
+ ? requestedRectStyleDetails(styleOwner, "Legend source")
50
+ : strokeDetails;
51
+ if (channels?.length === 1 && channels[0] === "stroke") {
52
+ const strokeWidth = bar.strokeWidth ?? config.strokeWidth ?? ({
53
+ point: 1,
54
+ line: 2,
55
+ area: 1,
56
+ bar: 0.5,
57
+ rect: 1,
58
+ arc: 1,
59
+ rule: 2,
60
+ tick: 2
61
+ }[layer.mark.type] ?? 1);
62
+ if (["line", "rule", "tick"].includes(layer.mark.type)) {
63
+ return { layers: [{
64
+ type: "line",
65
+ length: 32,
66
+ lineWidth: strokeWidth,
67
+ ...strokeDetails
68
+ }] };
69
+ }
70
+ if (layer.mark.type === "point") {
71
+ return { layers: [{
72
+ type: "point",
73
+ size: 5,
74
+ fill: config.fill ?? "#4c78a8",
75
+ stroke: "white",
76
+ strokeWidth,
77
+ ...strokeDetails
78
+ }] };
79
+ }
80
+ return { layers: [{
81
+ type: "swatch",
82
+ width: 14,
83
+ height: 12,
84
+ stroke: "white",
85
+ strokeWidth,
86
+ ...rectDetails
87
+ }] };
88
+ }
89
+ if (layer.mark?.type === "point" && channels?.includes("shape")) {
90
+ const color = channels.includes("color") ? layer.encoding?.color : undefined;
91
+ const matchingLine = color?.scale === undefined
92
+ ? undefined
93
+ : findLayerMatching(program, candidate =>
94
+ candidate.mark?.type === "line" &&
95
+ candidate.encoding?.color?.field === color.field &&
96
+ candidate.encoding?.color?.scale === color.scale
97
+ );
98
+ const lineDetails = matchingLine === undefined
99
+ ? {}
100
+ : requestedStrokeDetails(
101
+ program.markConfigs[matchingLine.id] ?? {},
102
+ "Legend line source"
103
+ );
104
+ return { layers: [
105
+ ...(matchingLine !== undefined ? [{
106
+ type: "line",
107
+ length: 32,
108
+ lineWidth: 3,
109
+ ...lineDetails
110
+ }] : []),
111
+ {
112
+ type: "point",
113
+ size: Math.sqrt(64 / Math.PI),
114
+ stroke: "white",
115
+ strokeWidth: 0,
116
+ ...strokeDetails
117
+ }
118
+ ] };
119
+ }
120
+ if (Object.keys(rectDetails).length === 0) return undefined;
121
+ return kind === "series"
122
+ ? { layers: [{ type: "line", length: 32, lineWidth: 2, ...strokeDetails }] }
123
+ : { layers: [{
124
+ type: "swatch",
125
+ width: 14,
126
+ height: 12,
127
+ stroke: "white",
128
+ strokeWidth: 0.5,
129
+ ...rectDetails
130
+ }] };
40
131
  }
41
132
 
42
133
  function defaultRecipe(kind) {
@@ -53,20 +144,20 @@ function defaultRecipe(kind) {
53
144
  };
54
145
  }
55
146
 
56
- function normalizeLayer(layer) {
147
+ function normalizeLayer(layer, internal) {
57
148
  if (!isPlainObject(layer) || !Object.hasOwn(LAYER_OPTIONS, layer.type)) {
58
149
  throw new Error("Legend symbol layer type must be line, point, or swatch.");
59
150
  }
60
151
  validateKeys(
61
152
  layer,
62
- LAYER_OPTIONS[layer.type],
153
+ internal ? INTERNAL_LAYER_OPTIONS[layer.type] : LAYER_OPTIONS[layer.type],
63
154
  `createLegend.symbol.${layer.type}`
64
155
  );
65
156
  if (layer.type === "line") {
66
157
  const normalized = { length: 32, lineWidth: 2, ...layer };
67
158
  positive(normalized.length, "Legend line symbol length");
68
159
  nonNegative(normalized.lineWidth, "Legend line symbol lineWidth");
69
- return normalized;
160
+ return { ...normalized, ...requestedStrokeDetails(normalized, "Legend line symbol") };
70
161
  }
71
162
  if (layer.type === "point") {
72
163
  const normalized = {
@@ -85,7 +176,7 @@ function normalizeLayer(layer) {
85
176
  if (normalized.fill !== undefined) {
86
177
  nonEmptyString(normalized.fill, "Legend point symbol fill");
87
178
  }
88
- return normalized;
179
+ return { ...normalized, ...requestedStrokeDetails(normalized, "Legend point symbol") };
89
180
  }
90
181
  const normalized = {
91
182
  width: 14,
@@ -98,10 +189,10 @@ function normalizeLayer(layer) {
98
189
  positive(normalized.height, "Legend swatch height");
99
190
  nonEmptyString(normalized.stroke, "Legend swatch stroke");
100
191
  nonNegative(normalized.strokeWidth, "Legend swatch strokeWidth");
101
- return normalized;
192
+ return { ...normalized, ...requestedRectStyleDetails(normalized, "Legend swatch symbol") };
102
193
  }
103
194
 
104
- export function normalizeRecipe(symbol, kind) {
195
+ export function normalizeRecipe(symbol, kind, { internal = false } = {}) {
105
196
  if (symbol === undefined || symbol === "auto") return defaultRecipe(kind);
106
197
  if (!isPlainObject(symbol)) {
107
198
  throw new TypeError('createLegend.symbol must be "auto" or a plain object.');
@@ -118,17 +209,17 @@ export function normalizeRecipe(symbol, kind) {
118
209
  "Legend symbol recipe supports at most one layer per type."
119
210
  );
120
211
  }
121
- layers = symbol.layers.map(normalizeLayer);
212
+ layers = symbol.layers.map(layer => normalizeLayer(layer, internal));
122
213
  } else if (kind === "series") {
123
214
  validateKeys(symbol, ["length", "lineWidth"], "createLegend.symbol");
124
- layers = [normalizeLayer({ type: "line", ...symbol })];
215
+ layers = [normalizeLayer({ type: "line", ...symbol }, internal)];
125
216
  } else {
126
217
  validateKeys(
127
218
  symbol,
128
219
  ["width", "height", "stroke", "strokeWidth"],
129
220
  "createLegend.symbol"
130
221
  );
131
- layers = [normalizeLayer({ type: "swatch", ...symbol })];
222
+ layers = [normalizeLayer({ type: "swatch", ...symbol }, internal)];
132
223
  }
133
224
 
134
225
  const types = layers.map(layer => layer.type);
@@ -17,8 +17,11 @@ function isCategoricalTarget(layer) {
17
17
  channel => layer.encoding?.[channel]?.scale !== undefined
18
18
  );
19
19
  }
20
- return ["bar", "area", "arc", "rect"].includes(layer?.mark?.type) &&
21
- layer.encoding?.color?.scale !== undefined;
20
+ return ["bar", "area", "arc", "rect", "rule", "tick"].includes(
21
+ layer?.mark?.type
22
+ ) && ["color", "stroke"].some(
23
+ channel => layer.encoding?.[channel]?.scale !== undefined
24
+ );
22
25
  }
23
26
 
24
27
  export function resolveTarget(program, requested) {
@@ -45,6 +48,7 @@ export function resolveTarget(program, requested) {
45
48
  export const sameValues = sameOrderedValues;
46
49
 
47
50
  function resolveLegendKind(layer, requestedChannels) {
51
+ if (sameValues(requestedChannels, ["stroke"])) return "stroke";
48
52
  if (["bar", "area", "arc", "rect"].includes(layer.mark.type)) return "color";
49
53
  if (
50
54
  layer.mark.type === "point" &&
@@ -78,9 +82,9 @@ function resolveOrdinalScales(program, scaleIds) {
78
82
  }
79
83
 
80
84
  export function resolveDefinition(program, layer, requestedChannels, requestedTitle, order) {
81
- const channels = requestedChannels ?? (["line", "point"].includes(layer.mark.type)
82
- ? CHANNELS.filter(channel => layer.encoding?.[channel]?.scale !== undefined)
83
- : ["color"]);
85
+ const channels = requestedChannels ?? CHANNELS.filter(
86
+ channel => layer.encoding?.[channel]?.scale !== undefined
87
+ );
84
88
  const kind = resolveLegendKind(layer, channels);
85
89
  if (
86
90
  !Array.isArray(channels) ||
@@ -92,8 +96,8 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
92
96
  "Legend channels must be a non-empty unique color/strokeDash/shape array."
93
97
  );
94
98
  }
95
- if (kind === "color" && !sameValues(channels, ["color"])) {
96
- throw new Error("Color legends currently support only the color channel.");
99
+ if (["color", "stroke"].includes(kind) && !sameValues(channels, [kind])) {
100
+ throw new Error(`${kind} legends currently support only the ${kind} channel.`);
97
101
  }
98
102
 
99
103
  const encodings = channels.map(channel => {
@@ -138,7 +142,7 @@ export function resolveCurrentDefinition(program, config) {
138
142
  if (guide === undefined) {
139
143
  throw new Error("Legend rematerialization requires semantic guide state.");
140
144
  }
141
- const channels = config.kind === "series" ? guide.channels : ["color"];
145
+ const channels = config.kind === "series" ? guide.channels : [config.kind];
142
146
  return resolveDefinition(
143
147
  program,
144
148
  layer,