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
@@ -9,24 +9,32 @@ import {
9
9
  validateGeneratedItemLimit
10
10
  } from "../../../../core/validation.js";
11
11
  import { formatTimeTick } from "../../../../grammar/ticks.js";
12
+ import {
13
+ formatValue,
14
+ validateValueFormat
15
+ } from "../../../../grammar/valueFormat.js";
12
16
  import {
13
17
  formatDistinctNumericSamples,
14
18
  sampleNumericRange
15
19
  } from "../../../../grammar/numeric.js";
16
20
  import { resolveGraphicBounds } from "../../../../layout/canvas.js";
21
+ import { isHorizontalEdgeLegend } from "../../../../layout/legendLane.js";
17
22
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
18
23
  "../../../../theme/defaults.js";
19
24
  import { findLayer } from "../../../../selectors/layers.js";
25
+ import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
20
26
  import { findCanvasGraphic } from
21
27
  "../../../../materialization/graphicHierarchy.js";
28
+ import { normalizeLegendSampling } from "../sampling.js";
22
29
 
23
30
  const OPTIONS = [
24
31
  "target", "channels", "position", "align", "offset", "title", "count",
32
+ "values",
25
33
  "gradient", "symbol", "labels", "titleStyle", "itemGap", "border",
26
34
  "direction", "columns", "titlePosition"
27
35
  ];
28
36
  const TEXT_OPTIONS = [
29
- "offset", "color", "fontSize", "fontFamily", "fontWeight"
37
+ "offset", "color", "fontSize", "fontFamily", "fontWeight", "format"
30
38
  ];
31
39
  const BORDER_OPTIONS = [
32
40
  "color", "lineWidth", "padding", "background"
@@ -82,9 +90,19 @@ export function normalizeLegendTextOptions(value, label, defaults) {
82
90
  ) {
83
91
  throw new TypeError(`${label} fontWeight must be a string or finite number.`);
84
92
  }
93
+ if (Object.hasOwn(result, "format")) {
94
+ result.format = validateValueFormat(result.format, `${label} format`);
95
+ }
85
96
  return result;
86
97
  }
87
98
 
99
+ export function normalizeLegendTitleOptions(value, label, defaults) {
100
+ if (value !== undefined) {
101
+ validateOptionObject(value, ["color", "fontSize", "fontFamily", "fontWeight"], label);
102
+ }
103
+ return normalizeLegendTextOptions(value, label, defaults);
104
+ }
105
+
88
106
  export function normalizeLegendBorder(value) {
89
107
  if (value === undefined || value === false) return false;
90
108
  if (value !== true && !isPlainObject(value)) {
@@ -110,13 +128,27 @@ export function normalizeContinuousLegend(args, kind) {
110
128
  if (!["left", "center", "right"].includes(align)) {
111
129
  throw new Error(`Unsupported legend alignment "${align}".`);
112
130
  }
113
- const count = args.count ?? 5;
114
- if (!Number.isInteger(count) || count < 2) {
115
- throw new RangeError(
116
- "Continuous legend count must be an integer of at least 2."
117
- );
131
+ if (["left", "right"].includes(position) && align !== "center") {
132
+ throw new Error("Side continuous legends require center alignment.");
133
+ }
134
+ if (kind === "gradient" && Object.hasOwn(args, "values")) {
135
+ throw new Error("Gradient legends do not support exact sample values.");
136
+ }
137
+ const sampling = kind === "opacity"
138
+ ? normalizeLegendSampling(args, {
139
+ operation: "create",
140
+ label: "Opacity legend"
141
+ })
142
+ : undefined;
143
+ const count = kind === "gradient" ? args.count ?? 5 : undefined;
144
+ if (kind === "gradient") {
145
+ if (!Number.isInteger(count) || count < 2) {
146
+ throw new RangeError(
147
+ "Continuous legend count must be an integer of at least 2."
148
+ );
149
+ }
150
+ validateGeneratedItemLimit(count, "Continuous legend count");
118
151
  }
119
- validateGeneratedItemLimit(count, "Continuous legend count");
120
152
  const offset = args.offset ?? 30;
121
153
  validateNonNegative(offset, "Legend offset");
122
154
  const titlePosition = args.titlePosition ?? "top";
@@ -152,7 +184,7 @@ export function normalizeContinuousLegend(args, kind) {
152
184
  position,
153
185
  align,
154
186
  offset,
155
- count,
187
+ ...(kind === "gradient" ? { count } : { sampling }),
156
188
  title: args.title,
157
189
  inferredTitle: args.title === undefined,
158
190
  labels: normalizeLegendTextOptions(
@@ -162,7 +194,7 @@ export function normalizeContinuousLegend(args, kind) {
162
194
  ? { ...DEFAULT_LABELS, offset: 8 }
163
195
  : DEFAULT_LABELS
164
196
  ),
165
- titleStyle: normalizeLegendTextOptions(
197
+ titleStyle: normalizeLegendTitleOptions(
166
198
  args.titleStyle,
167
199
  "createLegend.titleStyle",
168
200
  DEFAULT_TITLE
@@ -182,35 +214,40 @@ export function selectLegendLayer(program, requested, predicate) {
182
214
  return candidates.includes(candidate) ? candidate : undefined;
183
215
  }
184
216
 
185
- export function resolveContinuousPoint(program, requested, channel) {
217
+ export function resolveContinuousLegendLayer(program, requested, channel) {
186
218
  const layer = selectLegendLayer(
187
219
  program,
188
220
  requested,
189
- candidate => candidate.mark?.type === "point" &&
190
- candidate.encoding?.[channel]?.scale !== undefined
221
+ channel === "opacity" ? isOpacityLegendLayer : candidate =>
222
+ candidate.mark?.type === "point" && candidate.encoding?.[channel]?.scale !== undefined
191
223
  );
192
224
  if (layer === undefined) {
193
225
  throw new Error(
194
226
  requested === undefined
195
- ? `${channel} legend requires one eligible point mark.`
227
+ ? `${channel} legend requires one eligible ${channel === "opacity" ? "point or line" : "point"} mark.`
196
228
  : `Unknown ${channel} legend target "${requested}".`
197
229
  );
198
230
  }
199
231
  return layer;
200
232
  }
201
233
 
202
- export function resolveContinuousColorLayer(program, requested) {
234
+ export function resolveContinuousColorLayer(program, requested, channel = "color") {
203
235
  const layer = selectLegendLayer(
204
236
  program,
205
237
  requested,
206
- candidate => ["point", "bar", "rect"].includes(candidate.mark?.type) &&
207
- candidate.encoding?.color?.scale !== undefined
238
+ candidate => (channel === "stroke"
239
+ ? ["point", "line", "area", "bar", "rect", "arc", "rule", "tick"]
240
+ : ["point", "bar", "rect"]
241
+ ).includes(candidate.mark?.type) &&
242
+ candidate.encoding?.[channel]?.scale !== undefined
208
243
  );
209
244
  if (layer === undefined) {
210
245
  throw new Error(
211
246
  requested === undefined
212
- ? "color legend requires one eligible point, bar, or rect mark."
213
- : `Unknown color legend target "${requested}".`
247
+ ? channel === "color"
248
+ ? "color legend requires one eligible point, bar, or rect mark."
249
+ : "stroke legend requires one eligible mark."
250
+ : `Unknown ${channel} legend target "${requested}".`
214
251
  );
215
252
  }
216
253
  return layer;
@@ -251,10 +288,18 @@ export function sampleContinuousValues(domain, count) {
251
288
  );
252
289
  }
253
290
 
254
- export function formatContinuousValues(values, domain, fieldType) {
255
- return fieldType === "temporal"
256
- ? values.map(value => formatTimeTick(value, domain))
257
- : formatDistinctNumericSamples(values);
291
+ export function formatContinuousValues(values, domain, fieldType, format = "auto") {
292
+ const resolved = validateValueFormat(format, "Legend label format");
293
+ if (resolved === "auto") {
294
+ return fieldType === "temporal"
295
+ ? values.map(value => formatTimeTick(value, domain))
296
+ : formatDistinctNumericSamples(values);
297
+ }
298
+ return values.map(value => formatValue(value, {
299
+ format: resolved,
300
+ valueType: fieldType === "temporal" ? "temporal" : "quantitative",
301
+ label: "Legend label format"
302
+ }));
258
303
  }
259
304
 
260
305
  export function styleContinuousText(
@@ -284,7 +329,10 @@ export function resolveLegendTextBounds(position, text, style) {
284
329
  });
285
330
  }
286
331
 
287
- export function assertLegendBoundsInsideCanvas(bounds, canvas, label) {
332
+ export function assertLegendBoundsInsideCanvas(bounds, canvas, label, config) {
333
+ // Horizontal content is intrinsic until the shared lane places its actual
334
+ // concrete bounds. Only that final placement can determine Canvas fit.
335
+ if (isHorizontalEdgeLegend(config)) return;
288
336
  if (bounds.some(item =>
289
337
  item.left < 0 || item.right > canvas.width ||
290
338
  item.top < 0 || item.bottom > canvas.height
@@ -297,7 +345,8 @@ export function resolveLegendBackgroundFromBounds(
297
345
  bounds,
298
346
  border,
299
347
  canvas,
300
- label
348
+ label,
349
+ config
301
350
  ) {
302
351
  if (border === false) return undefined;
303
352
  const strokeExtent = border.lineWidth / 2;
@@ -315,11 +364,11 @@ export function resolveLegendBackgroundFromBounds(
315
364
  y -= border.padding;
316
365
  right += border.padding;
317
366
  bottom += border.padding;
318
- if (
367
+ if (!isHorizontalEdgeLegend(config) && (
319
368
  x - strokeExtent < 0 || y - strokeExtent < 0 ||
320
369
  right + strokeExtent > canvas.width ||
321
370
  bottom + strokeExtent > canvas.height
322
- ) {
371
+ )) {
323
372
  throw new Error(`${label} background requires more Canvas margin space.`);
324
373
  }
325
374
  return { x, y, width: right - x, height: bottom - y };
@@ -334,3 +383,27 @@ export function editLegendBackground(program, id, bounds, border) {
334
383
  strokeWidth: border.lineWidth
335
384
  });
336
385
  }
386
+
387
+ export function normalizeItemLegendLayout(args) {
388
+ const position = args.position ?? "right";
389
+ if (!["right", "left", "top", "bottom"].includes(position)) throw new Error(`Unsupported legend position "${position}".`);
390
+ const side = ["left", "right"].includes(position);
391
+ const layout = args.layout === undefined ? "edge" : args.layout;
392
+ if (layout !== "edge") throw new Error('This legend requires layout "edge".');
393
+ const align = args.align ?? "center";
394
+ if (!["left", "center", "right"].includes(align)) throw new Error(`Unsupported legend alignment "${align}".`);
395
+ const direction = args.direction ?? (side ? "vertical" : "horizontal");
396
+ if (!["horizontal", "vertical"].includes(direction)) throw new Error(`Unsupported legend direction "${direction}".`);
397
+ const columns = args.columns;
398
+ if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) throw new RangeError("Legend columns must be a positive integer.");
399
+ const titlePosition = args.titlePosition ?? "top";
400
+ if (!["top", "left"].includes(titlePosition)) throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
401
+ if (side && (direction !== "vertical" || align !== "center" || (columns !== undefined && columns !== 1) || titlePosition !== "top")) {
402
+ throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");
403
+ }
404
+ const offset = args.offset ?? 30;
405
+ const itemGap = args.itemGap ?? 28;
406
+ validateNonNegative(offset, "Legend offset");
407
+ validatePositive(itemGap, "Legend itemGap");
408
+ return { position, layout, align, direction, columns, titlePosition, offset, itemGap };
409
+ }
@@ -1,7 +1,8 @@
1
1
  import { action } from "../../../../core/action.js";
2
2
  import { isPlainObject } from "../../../../core/immutable.js";
3
3
  import { validateKeys } from "../../../../core/validation.js";
4
- import { interpolateColorStops } from "../../../../grammar/scales/color.js";
4
+ import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
5
+ import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
5
6
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
7
  import {
7
8
  assertLegendBoundsInsideCanvas,
@@ -19,22 +20,26 @@ import {
19
20
  } from "./common.js";
20
21
  import { resolveLegendGraphicPlacement } from
21
22
  "../../../../materialization/graphicHierarchy.js";
23
+ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
24
+
25
+ export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12 });
22
26
 
23
27
  const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
24
28
 
25
- function resolveGradientLayout(program, config, scale) {
29
+ export function resolveGradientLayout(program, config, scale) {
26
30
  const { plot, canvas } = resolveContinuousBounds(program);
27
31
  const vertical = ["right", "left"].includes(config.position);
28
32
  const length = config.gradient.length;
29
33
  const thickness = config.gradient.thickness;
34
+ const titleGap = config.blockGap ?? 12;
30
35
  let x;
31
36
  let y;
32
37
  if (config.position === "right") {
33
38
  x = plot.x + plot.width + config.offset;
34
- y = plot.y + 46;
39
+ y = plot.y + 46 + titleGap - 12;
35
40
  } else if (config.position === "left") {
36
41
  x = plot.x - config.offset - thickness;
37
- y = plot.y + 46;
42
+ y = plot.y + 46 + titleGap - 12;
38
43
  } else {
39
44
  x = config.align === "left" ? plot.x
40
45
  : config.align === "right" ? plot.x + plot.width - length
@@ -43,18 +48,29 @@ function resolveGradientLayout(program, config, scale) {
43
48
  ? plot.y - config.offset - thickness - config.labels.offset -
44
49
  config.labels.fontSize
45
50
  : plot.y + plot.height + config.offset +
46
- config.titleStyle.fontSize + 12;
51
+ (config.titleVisible === false ? 0 : config.titleStyle.fontSize + titleGap);
47
52
  }
48
53
  const title = vertical
49
54
  ? { x, y: plot.y + 20, align: "left" }
50
55
  : {
51
56
  x: x + length / 2,
52
- y: y - 12 - config.titleStyle.fontSize / 2,
57
+ y: y - titleGap - config.titleStyle.fontSize / 2,
53
58
  align: "center"
54
59
  };
55
- const values = sampleContinuousValues(scale.domain, config.count);
56
- const texts = formatContinuousValues(values, scale.domain, config.fieldType);
57
- const fractions = values.map((_, index) => index / (values.length - 1));
60
+ const values = [...sampleContinuousValues(scale.domain, config.count)];
61
+ if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
62
+ values.push(scale.midpoint);
63
+ values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
64
+ }
65
+ const texts = formatContinuousValues(
66
+ values,
67
+ scale.domain,
68
+ config.fieldType,
69
+ config.labels.format
70
+ );
71
+ const fractions = values.map((value, index) => scale.midpoint === undefined
72
+ ? index / (values.length - 1)
73
+ : inverseLerp(value, ...scale.domain));
58
74
  const labelOffset = config.labels.offset;
59
75
  const labels = vertical
60
76
  ? fractions.map(fraction => ({
@@ -97,22 +113,23 @@ function resolveGradientLayout(program, config, scale) {
97
113
  const labelBounds = labels.map((label, index) =>
98
114
  resolveLegendTextBounds(label, texts[index], config.labels)
99
115
  );
100
- const titleBounds = resolveLegendTextBounds(
116
+ const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
101
117
  title,
102
118
  config.title,
103
119
  config.titleStyle
104
120
  );
105
- const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds, titleBounds];
121
+ const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
122
+ ...(config.titleVisible === false ? [] : [titleBounds])];
106
123
  assertLegendBoundsInsideCanvas(
107
124
  occupiedBounds,
108
125
  canvas,
109
- "Gradient legend layout"
126
+ "Gradient legend layout", config
110
127
  );
111
128
  const background = resolveLegendBackgroundFromBounds(
112
129
  occupiedBounds,
113
130
  config.border,
114
131
  canvas,
115
- "Gradient legend"
132
+ "Gradient legend", config
116
133
  );
117
134
  return {
118
135
  vertical, x, y, length, thickness, values, texts, labels, ticks, title,
@@ -157,17 +174,18 @@ export const rematerializeGradientLegend = action(
157
174
  if (stored === undefined) {
158
175
  throw new Error("Gradient legend requires stored configuration.");
159
176
  }
160
- const { scale, encoding, config } = resolveGradientConfig(this, stored);
177
+ const { scale, encoding, config: currentConfig } = resolveGradientConfig(this, stored);
178
+ const config = resolveEffectiveLegendBlockConfig(this, "gradient", currentConfig);
161
179
  const layout = resolveGradientLayout(this, config, scale);
162
180
  const stripCount = 60;
163
181
  const stripSize = layout.length / stripCount;
164
182
  const strips = Array.from({ length: stripCount }, (_, index) => {
165
183
  const fraction = (index + 0.5) / stripCount;
166
- const color = interpolateColorStops(
167
- scale.range,
168
- layout.vertical ? 1 - fraction : fraction,
169
- scale.interpolate
170
- );
184
+ const position = layout.vertical ? 1 - fraction : fraction;
185
+ // Keep legacy uniform samples exact without a value/domain round trip.
186
+ const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
187
+ const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
188
+ const [color] = mapScaleConsumerValues([value], samplingScale, "color");
171
189
  return {
172
190
  x: layout.x + (layout.vertical ? 0 : index * stripSize),
173
191
  y: layout.y + (layout.vertical ? index * stripSize : 0),
@@ -187,7 +205,7 @@ export const rematerializeGradientLegend = action(
187
205
  property: "guide.legend.color.title",
188
206
  value: config.title
189
207
  })
190
- ._withLegendConfig("gradient", config)
208
+ ._withLegendConfig("gradient", currentConfig)
191
209
  .editGraphics({
192
210
  target: "colorGradientStrips",
193
211
  property: "length",
@@ -283,86 +301,99 @@ export const rematerializeGradientLegend = action(
283
301
  }
284
302
  );
285
303
 
304
+
305
+ export function resolveGradientLegendCreation(program, args = {}) {
306
+ const config = normalizeContinuousLegend(args, "gradient");
307
+ if (args.channels !== undefined && (
308
+ !Array.isArray(args.channels) ||
309
+ args.channels.length !== 1 ||
310
+ args.channels[0] !== "color"
311
+ )) {
312
+ throw new Error('Gradient legend requires channels: ["color"].');
313
+ }
314
+ if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
315
+ throw new TypeError("createLegend.gradient must be a plain object.");
316
+ }
317
+ validateKeys(
318
+ args.gradient ?? {},
319
+ GRADIENT_OPTIONS,
320
+ "createLegend.gradient"
321
+ );
322
+ config.gradient = {
323
+ length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
324
+ thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
325
+ };
326
+ config.titleVisible = true;
327
+ validatePositive(config.gradient.length, "Gradient length");
328
+ validatePositive(config.gradient.thickness, "Gradient thickness");
329
+ const resolved = resolveGradientConfig(program, config);
330
+ return resolved;
331
+ }
332
+
333
+ export function createGradientLegendFromConfig(program, config) {
334
+ const resolved = resolveGradientConfig(program, config);
335
+ resolveGradientLayout(program, resolved.config, resolved.scale);
336
+ let next = program
337
+ .editSemantic({
338
+ property: "guide.legend.color.scale",
339
+ value: resolved.encoding.scale
340
+ })
341
+ .editSemantic({
342
+ property: "guide.legend.color.title",
343
+ value: resolved.config.title
344
+ })
345
+ ._withLegendConfig("gradient", resolved.config);
346
+ if (resolved.config.border !== false) {
347
+ next = next.createGraphics({
348
+ id: "colorGradientBackground",
349
+ type: "rect",
350
+ ...resolveLegendGraphicPlacement(next)
351
+ });
352
+ }
353
+ next = next
354
+ .createGraphics({
355
+ id: "colorGradientStrips",
356
+ type: "rect",
357
+ length: 0,
358
+ ...resolveLegendGraphicPlacement(next, resolved.config.border === false
359
+ ? {}
360
+ : { after: "colorGradientBackground" })
361
+ })
362
+ .createGraphics({
363
+ id: "colorGradientTicks",
364
+ type: "line",
365
+ length: 0,
366
+ ...resolveLegendGraphicPlacement(next)
367
+ })
368
+ .createGraphics({
369
+ id: "colorGradientLabels",
370
+ type: "text",
371
+ length: 0,
372
+ ...resolveLegendGraphicPlacement(next)
373
+ });
374
+ if (resolved.config.titleVisible !== false) {
375
+ next = next.createGraphics({
376
+ id: "colorGradientTitle",
377
+ type: "text",
378
+ ...resolveLegendGraphicPlacement(next)
379
+ });
380
+ }
381
+ return next.rematerializeGradientLegend();
382
+ }
383
+
286
384
  export const createGradientLegend = action(
287
385
  {
288
386
  op: "createGradientLegend",
289
387
  description: "Create a continuous color gradient legend."
290
388
  },
291
389
  function (args = {}) {
292
- const config = normalizeContinuousLegend(args, "gradient");
293
- if (args.channels !== undefined && (
294
- !Array.isArray(args.channels) ||
295
- args.channels.length !== 1 ||
296
- args.channels[0] !== "color"
297
- )) {
298
- throw new Error('Gradient legend requires channels: ["color"].');
299
- }
300
- if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
301
- throw new TypeError("createLegend.gradient must be a plain object.");
302
- }
303
- validateKeys(
304
- args.gradient ?? {},
305
- GRADIENT_OPTIONS,
306
- "createLegend.gradient"
307
- );
308
- config.gradient = {
309
- length: args.gradient?.length ?? 120,
310
- thickness: args.gradient?.thickness ?? 12
311
- };
312
- config.titleVisible = true;
313
- validatePositive(config.gradient.length, "Gradient length");
314
- validatePositive(config.gradient.thickness, "Gradient thickness");
315
- const resolved = resolveGradientConfig(this, config);
390
+ const resolved = resolveGradientLegendCreation(this, args);
316
391
  resolveGradientLayout(this, resolved.config, resolved.scale);
317
392
  if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
318
393
  throw new Error(
319
394
  "createGradientLegend requires a missing gradient legend."
320
395
  );
321
396
  }
322
- let next = this
323
- .editSemantic({
324
- property: "guide.legend.color.scale",
325
- value: resolved.encoding.scale
326
- })
327
- .editSemantic({
328
- property: "guide.legend.color.title",
329
- value: resolved.config.title
330
- })
331
- ._withLegendConfig("gradient", resolved.config);
332
- if (resolved.config.border !== false) {
333
- next = next.createGraphics({
334
- id: "colorGradientBackground",
335
- type: "rect",
336
- ...resolveLegendGraphicPlacement(next)
337
- });
338
- }
339
- return next
340
- .createGraphics({
341
- id: "colorGradientStrips",
342
- type: "rect",
343
- length: 0,
344
- ...resolveLegendGraphicPlacement(next),
345
- ...(resolved.config.border === false
346
- ? {}
347
- : { after: "colorGradientBackground" })
348
- })
349
- .createGraphics({
350
- id: "colorGradientTicks",
351
- type: "line",
352
- length: 0,
353
- ...resolveLegendGraphicPlacement(next)
354
- })
355
- .createGraphics({
356
- id: "colorGradientLabels",
357
- type: "text",
358
- length: 0,
359
- ...resolveLegendGraphicPlacement(next)
360
- })
361
- .createGraphics({
362
- id: "colorGradientTitle",
363
- type: "text",
364
- ...resolveLegendGraphicPlacement(next)
365
- })
366
- .rematerializeGradientLegend();
397
+ return createGradientLegendFromConfig(this, resolved.config);
367
398
  }
368
399
  );
@@ -5,6 +5,7 @@ import {
5
5
  removeOpacityLegend
6
6
  } from "./opacity.js";
7
7
  import { createIntervalLegend, rematerializeIntervalLegend } from "./interval.js";
8
+ import { registerStrokeColorLegendActions } from "./stroke.js";
8
9
 
9
10
  export { createGradientLegend, rematerializeGradientLegend } from "./gradient.js";
10
11
  export {
@@ -19,11 +20,16 @@ export function registerContinuousLegendActions(ProgramClass) {
19
20
  ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
20
21
  ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
21
22
  ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
22
- ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
23
- ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
23
+ registerIntervalLegendActions(ProgramClass);
24
+ registerStrokeColorLegendActions(ProgramClass);
24
25
  }
25
26
 
26
27
  export function registerGradientLegendActions(ProgramClass) {
27
28
  ProgramClass.prototype.createGradientLegend = createGradientLegend;
28
29
  ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
29
30
  }
31
+
32
+ export function registerIntervalLegendActions(ProgramClass) {
33
+ ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
34
+ ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
35
+ }