ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -1,3 +1,11 @@
1
+ import {resolveColorSizeConfig,createColorSizeLegendFromConfig,materializeColorSizeLegend} from "../colorSize.js";
2
+ import {resetLegendClipGraphics,applyLegendClipGraphics,restoreLegendClipContext}from"../../../../materialization/legendClip.js";
3
+ import {validateLegendCombine,resolveSizeShapeCreation} from "../combine.js";
4
+ import { recordLegendOffsetIntent } from "../group.js";
5
+ import { prepareLegendGroup, creationLegendTarget, validateLegendGroupOptions } from "../group.js";
6
+ import { PLOT_LEGEND_POSITIONS } from "../../../../layout/legend.js";
7
+ import { normalizeInitialLegendTitle } from "../continuous/common.js";
8
+ import { isContinuousColorScaleType } from "../../../../grammar/scales/types.js";
1
9
  import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
2
10
  import { sameGuideValue } from "../../reuse.js";
3
11
  import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
@@ -17,8 +25,7 @@ import { legendResourcePolicies } from
17
25
  "../../../../materialization/guides/resources.js";
18
26
  import {
19
27
  hasHorizontalLegendLane,
20
- hasLegendLane,
21
- hasMultiSideLegendLane
28
+ hasLegendLane
22
29
  } from "../lane.js";
23
30
  import {
24
31
  resolveCurrentDefinition,
@@ -33,7 +40,8 @@ import {
33
40
  } from "../blocks.js";
34
41
 
35
42
  function finishLegend(program) {
36
- return hasLegendLane(program) ? program.rematerializeLegend() : program;
43
+ return Object.values(program.guideConfigs.legend??{}).some(config=>config.clipHeight!==undefined)||hasLegendLane(program) || Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0
44
+ ? program.rematerializeLegend() : program;
37
45
  }
38
46
 
39
47
  function requestedCandidate(program, target, candidates) {
@@ -54,7 +62,8 @@ export const rematerializeLegend = /* @__PURE__ */ action(
54
62
  { op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
55
63
  withGuideLayoutValidation(function (args = {}) {
56
64
  noOptions(args, "rematerializeLegend");
57
- let next = this;
65
+ const oldClipContext=this.context.deferLegendClip;
66
+ let next = resetLegendClipGraphics(this)._withContext({deferLegendClip:true});
58
67
  const categoricalKinds = ["series", "color", "stroke"].filter(
59
68
  kind => this.guideConfigs.legend?.[kind] !== undefined
60
69
  );
@@ -80,6 +89,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
80
89
  !sameValues(stored.domain, definition.domain) ||
81
90
  !sameValues(stored.scales, definition.scales) ||
82
91
  stored.field !== definition.field ||
92
+ !sameValues(stored.sizeAreas ?? [], definition.sizeAreas ?? []) ||
83
93
  stored.title !== definition.title;
84
94
  next = changed
85
95
  ? next._withLegendConfig(kind, {
@@ -88,6 +98,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
88
98
  channels: definition.channels,
89
99
  scales: definition.scales,
90
100
  field: definition.field,
101
+ ...(definition.combine === undefined ? {} : {combine: definition.combine, sizeAreas: definition.sizeAreas}),
91
102
  title: definition.title,
92
103
  domain: definition.domain
93
104
  })
@@ -147,6 +158,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
147
158
  ).some(highlight => highlight.target === config.target);
148
159
  if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
149
160
  }
161
+ if(next.guideConfigs.legend?.colorSize !== undefined) next=materializeColorSizeLegend(next,next.guideConfigs.legend.colorSize);
150
162
  for (const policy of legendResourcePolicies()) {
151
163
  if (
152
164
  policy.rematerializeOp !== undefined &&
@@ -162,9 +174,10 @@ export const rematerializeLegend = /* @__PURE__ */ action(
162
174
  next = next[policy.rematerializeOp]();
163
175
  }
164
176
  }
165
- if (hasMultiSideLegendLane(next)) {
166
- next = next.rematerializeSideLegendLane();
167
- }
177
+ const clipAnchors = Object.fromEntries(categoricalKinds.filter(kind => next.guideConfigs.legend[kind].clipHeight !== undefined)
178
+ .map(kind => [kind, resolveLayout(next, resolveEffectiveLegendBlockConfig(next, kind, next.guideConfigs.legend[kind])).itemY]));
179
+ next=applyLegendClipGraphics(next,{force:true,anchors:clipAnchors});
180
+ next=restoreLegendClipContext(next,oldClipContext);
168
181
  if (hasHorizontalLegendLane(next)) {
169
182
  next = next.rematerializeHorizontalLegendLane();
170
183
  }
@@ -178,7 +191,7 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
178
191
  program,
179
192
  layer,
180
193
  args.channels,
181
- args.title,
194
+ args.title === false ? undefined : args.title,
182
195
  args.order === undefined ? undefined : normalizeLegendOrder(args.order)
183
196
  );
184
197
  const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
@@ -194,7 +207,8 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
194
207
  const config = {
195
208
  target: layer.id,
196
209
  ...definition,
197
- inferredTitle: !Object.hasOwn(args, "title"),
210
+ ...normalizeInitialLegendTitle(args.title, definition.title),
211
+ inferredTitle: args.title === false || !Object.hasOwn(args, "title"),
198
212
  inferredSymbol,
199
213
  position: options.position,
200
214
  align: options.align,
@@ -207,8 +221,9 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
207
221
  titleStyle: options.titleStyle,
208
222
  itemGap: options.itemGap,
209
223
  layout: options.layout,
210
- border: options.border,
211
- titleVisible: true
224
+ ...(options.clipHeight === undefined ? {} : {clipHeight:options.clipHeight}),
225
+ ...(options.overflow === undefined ? {} : { overflow: options.overflow }),
226
+ border: options.border
212
227
  };
213
228
  return config;
214
229
  }
@@ -228,7 +243,18 @@ export const createCategoricalLegend = /* @__PURE__ */ action(
228
243
  })
229
244
  );
230
245
 
231
- export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
246
+ function resolveRawLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
247
+ if (Object.hasOwn(args, "combine")) {
248
+ validateLegendCombine(args.combine);
249
+ if (args.combine === "color-size") {
250
+ const config=resolveColorSizeConfig(program,args);
251
+ const prepared=prepareLegendGroup(program,{...args,blockLayout:args.blockLayout??(["left","right"].includes(args.position??"right")?"column":"row")},config.target,{creation:true,nativeOffset:30});
252
+ return {steps:[{op:"createColorSizeLegend",args}],finish:"always",legendGroup:{target:config.target,config:prepared.program.guideConfigs.legendGroups[config.target]}};
253
+ }
254
+ if (args.combine === "size-shape") return resolveSizeShapeCreation(program, args, layers, resolveRawLegendCreationPlan, internalChannels);
255
+ const {combine: _combine, ...separate} = args;
256
+ return resolveRawLegendCreationPlan(program, separate, layers);
257
+ }
232
258
  validateOptionObject(args, undefined, "createLegend");
233
259
  const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
234
260
  const channels = args.channels;
@@ -252,15 +278,23 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
252
278
  }
253
279
  }
254
280
  const continuousColorCandidates = candidates.filter(layer => {
255
- const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
281
+ const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type)
256
282
  ? layer.encoding?.color
257
283
  : undefined;
258
284
  const scale = findSemanticScale(program, encoding?.scale);
259
- return scale?.type === "sequential";
285
+ return isContinuousColorScaleType(scale?.type);
260
286
  });
261
287
  const continuousColor = requestedCandidate(
262
288
  program, args.target, continuousColorCandidates
263
289
  );
290
+ if (args.representation !== undefined && channels !== undefined && channels.length > 1) {
291
+ if (!continuousColor || !channels.includes("color")) throw new Error("Legend representation requires selected continuous color.");
292
+ const { representation, values, count, symbol, gradient, ...common } = args;
293
+ const steps = channels.flatMap(channel => resolveRawLegendCreationPlan(program,
294
+ channel === "color" ? { ...args, channels: [channel] }
295
+ : { ...common, channels: [channel] }, layers).steps);
296
+ return { steps, finish: "always" };
297
+ }
264
298
  if (
265
299
  (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
266
300
  (channels === undefined && continuousColor)
@@ -268,7 +302,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
268
302
  return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
269
303
  }
270
304
  const intervalColorCandidates = candidates.filter(layer => {
271
- const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
305
+ const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
272
306
  const scale = findSemanticScale(program, encoding?.scale);
273
307
  return ["quantize", "quantile", "threshold"].includes(scale?.type);
274
308
  });
@@ -281,7 +315,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
281
315
  }
282
316
  const continuousStrokeCandidates = candidates.filter(layer => {
283
317
  const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
284
- return scale?.type === "sequential";
318
+ return isContinuousColorScaleType(scale?.type);
285
319
  });
286
320
  const continuousStroke = requestedCandidate(
287
321
  program,
@@ -365,6 +399,9 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
365
399
  if (combined) steps.push({ op: "createSizeLegend", args: {
366
400
  target: requestedPoint.id,
367
401
  ...(count === undefined ? {} : { count }),
402
+ ...(categoricalArgs.overflow !== undefined &&
403
+ findSemanticScale(program, requestedPoint.encoding.size.scale)?.type === "ordinal"
404
+ ? { overflow: categoricalArgs.overflow } : {}),
368
405
  ...(categoricalArgs.labels === undefined ? {} : { labels: {
369
406
  ...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
370
407
  ...(sizeFormat === undefined ? {} : { format: sizeFormat })
@@ -389,14 +426,53 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
389
426
  return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
390
427
  }
391
428
 
429
+ export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
430
+ validateLegendGroupOptions(args);
431
+ const { blockLayout: _layout, at: _at, ...raw } = args;
432
+ const groups = program.guideConfigs.legendGroups ?? {};
433
+ if (Object.keys(groups).length > 0 && raw.position === undefined) {
434
+ const target = creationLegendTarget(program, args);
435
+ const placement = groups[target]?.placement;
436
+ if (placement?.position !== undefined) {
437
+ raw.position = placement.position;
438
+ raw.align = args.align ?? placement.align;
439
+ raw.offset = args.offset ?? placement.offset;
440
+ }
441
+ }
442
+ if (raw.position === undefined && args.direction === "horizontal" &&
443
+ (args.at !== undefined || Object.keys(groups).length > 0)) raw.position = "top";
444
+ const initial = resolveRawLegendCreationPlan(program, raw, layers, internalChannels);
445
+ const inherited = Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0;
446
+ const corner = PLOT_LEGEND_POSITIONS.includes(args.position) &&
447
+ (initial.steps.length > 1 || initial.steps[0]?.op !== "createCategoricalLegend" ||
448
+ Object.keys(program.guideConfigs.legend ?? {}).length > 0);
449
+ if (args.blockLayout === undefined && args.at === undefined && !inherited && !corner) return initial;
450
+ const target = creationLegendTarget(program, args);
451
+ const first = initial.steps[0];
452
+ const nativeOffset = program.guideConfigs.legendGroups?.[target] !== undefined ? 30 : first?.op === "createCategoricalLegend"
453
+ ? resolveCategoricalLegendConfig(program, first.args).offset
454
+ : first?.op === "createSizeLegend" ? resolveSizeLegendConfig(program, first.args).offset : 30;
455
+ const prepared = prepareLegendGroup(program, args, target, { creation: true, corner, nativeOffset });
456
+ if (!prepared.grouped) return initial;
457
+ if (prepared.options.position === undefined && raw.position !== undefined) {
458
+ prepared.options.position = raw.position;
459
+ }
460
+ return { ...resolveRawLegendCreationPlan(prepared.program, prepared.options, layers, internalChannels),
461
+ legendGroup: { target, config: prepared.program.guideConfigs.legendGroups[target] } };
462
+ }
463
+
392
464
  export function applyLegendCreationPlan(program, plan) {
393
465
  if (plan.steps.length === 0) return program;
394
- let next = program;
466
+ let next = plan.legendGroup === undefined ? program : program._withMaterializationConfig(
467
+ ["guides", "legendGroups", plan.legendGroup.target], plan.legendGroup.config);
395
468
  for (const step of plan.steps) {
396
469
  const existingSize = next.guideConfigs.legend?.size;
470
+ if (plan.combined && existingSize !== undefined && step.op === "createCategoricalLegend") {
471
+ next = next._withLegendConfig("size", { ...existingSize, inheritPosition: true });
472
+ }
397
473
  // The plan already validated that a retained size block is compatible.
398
474
  if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
399
- next = next[step.op](step.args);
475
+ next = step.op === "createColorSizeLegend" ? createColorSizeLegendFromConfig(next,resolveColorSizeConfig(next,step.args)) : next[step.op](step.args);
400
476
  }
401
477
  }
402
478
  return plan.finish === "always" ? next.rematerializeLegend()
@@ -406,7 +482,7 @@ export function applyLegendCreationPlan(program, plan) {
406
482
  export const createLegend = /* @__PURE__ */ action(
407
483
  { op: "createLegend", description: "Create an inferred legend for selected channels." },
408
484
  withGuideLayoutValidation(function (args = {}) {
409
- return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
485
+ return recordLegendOffsetIntent(this, applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args)), args, { creation: true });
410
486
  })
411
487
  );
412
488
 
@@ -1,4 +1,6 @@
1
+ import { materializeLegendBorder } from "../../../../materialization/legendBorder.js";
1
2
  import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
3
+ import { materializeLegendTitleLines } from "../../../../materialization/legendText.js";
2
4
  import { action } from "../../../../core/action.js";
3
5
  import {
4
6
  activeConfig,
@@ -19,15 +21,17 @@ export const rematerializeLegendLabels = /* @__PURE__ */ action(
19
21
  throw new Error("rematerializeLegendLabels requires existing legend labels.");
20
22
  }
21
23
  const layout = resolveLayout(this, config);
24
+ const labels = categoricalLegendLabels(config);
25
+ const visible = labels.flatMap((text, index) => text === "" ? [] : [index]);
22
26
  return editGraphicProperties(editGraphicProperties(this, id, {
23
- length: config.domain.length,
24
- x: layout.labelX,
25
- y: layout.itemY
27
+ length: visible.length,
28
+ x: visible.map(index => layout.labelX[index]),
29
+ y: visible.map(index => layout.itemY[index])
26
30
  })
27
31
  .editGraphics({
28
32
  target: id,
29
33
  property: "text",
30
- value: categoricalLegendLabels(config)
34
+ value: visible.map(index => labels[index])
31
35
  }), id, {
32
36
  fill: config.labels.color,
33
37
  fontSize: config.labels.fontSize,
@@ -52,7 +56,7 @@ export const createLegendLabels = /* @__PURE__ */ action(
52
56
  .createGraphics({
53
57
  id,
54
58
  type: "text",
55
- length: config.domain.length,
59
+ length: categoricalLegendLabels(config).length,
56
60
  ...resolveCategoricalLegendPlacement(this)
57
61
  })
58
62
  .rematerializeLegendLabels({ kind: config.kind });
@@ -69,7 +73,7 @@ export const rematerializeLegendTitle = /* @__PURE__ */ action(
69
73
  throw new Error("rematerializeLegendTitle requires an existing legend title.");
70
74
  }
71
75
  const layout = resolveLayout(this, config);
72
- return editGraphicProperties(this, id, {
76
+ const next = editGraphicProperties(this, id, {
73
77
  x: layout.titleX,
74
78
  y: layout.titleY,
75
79
  text: config.title,
@@ -81,12 +85,13 @@ export const rematerializeLegendTitle = /* @__PURE__ */ action(
81
85
  .editGraphics({
82
86
  target: id,
83
87
  property: "textAlign",
84
- value: ["top", "bottom"].includes(config.position) &&
88
+ value: config.titleStyle.align ?? (["top", "bottom"].includes(config.position) &&
85
89
  config.titlePosition === "top"
86
90
  ? "center"
87
- : "left"
91
+ : "left")
88
92
  })
89
93
  .editGraphics({ target: id, property: "textBaseline", value: "middle" });
94
+ return materializeLegendTitleLines(next, id, config.title, config.titleStyle);
90
95
  }
91
96
  );
92
97
 
@@ -118,19 +123,11 @@ export const rematerializeLegendBackground = /* @__PURE__ */ action(
118
123
  throw new Error("rematerializeLegendBackground requires border configuration.");
119
124
  }
120
125
  const id = graphic(config, "Background");
121
- if (this.graphicSpec.objects[id]?.type !== "rect") {
126
+ if (!["rect", "path"].includes(this.graphicSpec.objects[id]?.type)) {
122
127
  throw new Error("rematerializeLegendBackground requires an existing background.");
123
128
  }
124
129
  const layout = resolveLayout(this, config).background;
125
- return editGraphicProperties(this, id, {
126
- x: layout.x,
127
- y: layout.y,
128
- width: layout.width,
129
- height: layout.height,
130
- fill: config.border.background,
131
- stroke: config.border.color,
132
- strokeWidth: config.border.lineWidth
133
- });
130
+ return materializeLegendBorder(this, id, layout, config.border);
134
131
  }
135
132
  );
136
133
 
@@ -1,3 +1,4 @@
1
+ import{hasLegendClipGraphics}from"../../../../materialization/legendClip.js";
1
2
  import { action } from "../../../../core/action.js";
2
3
  import { editLegend } from "../edit.js";
3
4
  import { resolveStoredSelection } from
@@ -29,6 +30,7 @@ import {
29
30
  } from "./symbols.js";
30
31
  import { activeConfig, noOptions, symbolGraphic } from "./layout.js";
31
32
  import {
33
+ rematerializeLegendGroups,
32
34
  rematerializeHorizontalLegendLane,
33
35
  rematerializeSideLegendLane
34
36
  } from "../lane.js";
@@ -68,6 +70,11 @@ function createLegendHighlightAction() {
68
70
  },
69
71
  function (args = {}) {
70
72
  noOptions(args, "rematerializeLegendHighlights");
73
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
74
+ if (this.context.deferGuideLayoutValidation !== true &&
75
+ Object.values(this.guideConfigs.legend ?? {}).some(config => ["left", "right"].includes(config.position))) {
76
+ return this.rematerializeLegend();
77
+ }
71
78
  const kinds = args.kind === undefined
72
79
  ? ["series", "color", "stroke"].filter(
73
80
  kind => this.guideConfigs.legend?.[kind] !== undefined
@@ -134,6 +141,7 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
134
141
  ProgramClass.prototype.createLegendTitle = createLegendTitle;
135
142
  ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
136
143
  ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
144
+ ProgramClass.prototype.rematerializeLegendGroups = rematerializeLegendGroups;
137
145
  ProgramClass.prototype.rematerializeSideLegendLane =
138
146
  rematerializeSideLegendLane;
139
147
  ProgramClass.prototype.rematerializeHorizontalLegendLane =
@@ -1,3 +1,9 @@
1
+ import{validateLegendClip,clippedSampleBounds}from"../../../../grammar/legendClip.js";
2
+ import {categoricalLegendSymbolId as symbolGraphic}from"../../../../grammar/legendBlocks.js";
3
+ export {symbolGraphic};
4
+ import { findLayer } from "../../../../selectors/layers.js";
5
+ import { isLinearGradientPaint } from "../../../../grammar/paint.js";
6
+ import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
1
7
  import { validateOptionObject } from "../../../../core/validation.js";
2
8
  import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
3
9
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
@@ -57,14 +63,6 @@ function prefix(config) {
57
63
  return `${config.kind}Legend`;
58
64
  }
59
65
 
60
- export function symbolGraphic(config, type) {
61
- const onlyDefault = config.symbol.layers.length === 1 &&
62
- ((config.kind === "series" && type === "line") ||
63
- (["color", "stroke"].includes(config.kind) && type === "swatch"));
64
- if (onlyDefault) return `${prefix(config)}Symbols`;
65
- const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
66
- return `${prefix(config)}Symbol${suffix}`;
67
- }
68
66
 
69
67
  export function graphic(config, component) {
70
68
  return `${prefix(config)}${component}`;
@@ -73,17 +71,25 @@ export function graphic(config, component) {
73
71
  export function symbolWidth(config) {
74
72
  return Math.max(...config.symbol.layers.map(layer => {
75
73
  if (layer.type === "line") return layer.length;
76
- if (layer.type === "point") return layer.size * 2;
74
+ if (layer.type === "point") return config.combine === "size-shape" ? 2 * Math.sqrt(Math.max(...config.sizeAreas) / Math.PI) : layer.size * 2;
77
75
  return layer.width;
78
76
  }));
79
77
  }
80
78
 
79
+ export function categoricalLegendView(config) {
80
+ if (config.hiddenCount !== undefined || !config.overflow || config.domain.length <= config.overflow.maxItems) return config;
81
+ return { ...config, domain: config.domain.slice(0, config.overflow.maxItems),
82
+ hiddenCount: config.domain.length - config.overflow.maxItems };
83
+ }
84
+
81
85
  export function categoricalLegendLabels(config) {
82
- return config.domain.map(value => resolveDisplayLabel(
86
+ config = categoricalLegendView(config);
87
+ const labels = config.domain.map(value => resolveDisplayLabel(
83
88
  value,
84
89
  config.labelMap,
85
90
  formatVisibleText
86
91
  ));
92
+ return config.hiddenCount > 0 ? [...labels, `…${config.hiddenCount} entries`] : labels;
87
93
  }
88
94
 
89
95
  function resolveSampleBounds(program, config, width) {
@@ -96,7 +102,7 @@ function resolveSampleBounds(program, config, width) {
96
102
  stroke: layer.stroke ?? (config.channels.includes("stroke")
97
103
  ? appearance.strokes[index]
98
104
  : appearance.colors[index]),
99
- strokeWidth: layer.lineWidth, opacity: layer.opacity,
105
+ strokeWidth: layer.lineWidth, opacity: layer.opacity ?? appearance.opacity,
100
106
  ...requestedStrokeDetails(layer, "Legend line symbol") } };
101
107
  }
102
108
  if (layer.type === "swatch") {
@@ -106,24 +112,24 @@ function resolveSampleBounds(program, config, width) {
106
112
  stroke: config.channels.includes("stroke")
107
113
  ? appearance.strokes[index]
108
114
  : layer.stroke,
109
- opacity: layer.opacity,
115
+ opacity: layer.opacity ?? appearance.opacity,
110
116
  ...requestedStrokeDetails(details, "Legend swatch symbol")
111
- }, details.cornerRadius ?? 0);
117
+ }, details);
112
118
  }
113
119
  if (config.channels.includes("shape")) {
114
120
  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,
121
+ area: appearance.areas?.[index] ?? Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
122
+ stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
117
123
  ...requestedStrokeDetails(layer, "Legend point symbol") });
118
124
  }
119
125
  return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
120
126
  stroke: config.channels.includes("stroke")
121
127
  ? appearance.strokes[index]
122
128
  : layer.stroke,
123
- strokeWidth: layer.strokeWidth, opacity: layer.opacity,
129
+ strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
124
130
  ...requestedStrokeDetails(layer, "Legend point symbol") } };
125
131
  });
126
- return items.map((item, itemIndex) => {
132
+ return clippedSampleBounds(items.map((item, itemIndex) => {
127
133
  const bounds = resolveConcreteGraphicBounds({
128
134
  objects: { sample: item },
129
135
  order: ["sample"]
@@ -142,11 +148,14 @@ function resolveSampleBounds(program, config, width) {
142
148
  right: Math.max(result.right, bounds.right),
143
149
  top: Math.min(result.top, bounds.top),
144
150
  bottom: Math.max(result.bottom, bounds.bottom)
145
- }, undefined);
151
+ }, undefined),config.clipHeight);
146
152
  });
147
153
  }
148
154
 
149
155
  export function resolveLayout(program, config) {
156
+ validateLegendClip(config.kind,config);
157
+ config = intrinsicLegendConfig(program, config);
158
+ config = categoricalLegendView(config);
150
159
  const { plot, canvas } = resolveContinuousBounds(program,
151
160
  "Legend layout requires Canvas bounds, width, and height.");
152
161
  const width = symbolWidth(config);
@@ -160,7 +169,14 @@ export function resolveLayout(program, config) {
160
169
  }
161
170
 
162
171
  export function resolveAppearance(program, config) {
163
- let colors = config.domain.map(() => DEFAULT_COLORS.mark);
172
+ const target = findLayer(program, config.target);
173
+ const gradientFallback = target?.mark?.type === "area" &&
174
+ target.encoding.color === undefined &&
175
+ !config.channels.includes("color") &&
176
+ isLinearGradientPaint(program.markConfigs[target.id]?.fill);
177
+ const fallback = gradientFallback
178
+ ? program.markConfigs[target.id].fill : DEFAULT_COLORS.mark;
179
+ let colors = config.domain.map(() => fallback);
164
180
  let strokes = config.domain.map(() => "white");
165
181
  let dashes = config.domain.map(() => []);
166
182
  let shapes = config.domain.map(() => "circle");
@@ -172,7 +188,8 @@ export function resolveAppearance(program, config) {
172
188
  if (config.channels[index] === "strokeDash") dashes = values;
173
189
  if (config.channels[index] === "shape") shapes = values;
174
190
  }
175
- return { colors, strokes, dashes, shapes };
191
+ return { colors, strokes, dashes, shapes, ...(config.combine === "size-shape" ? {areas: config.sizeAreas} : {}),
192
+ ...(gradientFallback ? { opacity: program.markConfigs[target.id].opacity } : {}) };
176
193
  }
177
194
 
178
195
  export function noOptions(args, operation) {
@@ -189,3 +206,11 @@ export function layerFor(config, type) {
189
206
  }
190
207
  return layer;
191
208
  }
209
+
210
+
211
+ export function resolveSymbolLayout(program, config) {
212
+ const view = categoricalLegendView(config);
213
+ const layout = resolveLayout(program, view);
214
+ return { ...layout, symbolX: layout.symbolX.slice(0, view.domain.length),
215
+ itemY: layout.itemY.slice(0, view.domain.length) };
216
+ }
@@ -1,3 +1,7 @@
1
+ import{normalizeLegendClipHeight}from"../../../../grammar/legendClip.js";
2
+ import { normalizeLegendBorderDash } from "../../../../grammar/legendBorder.js";
3
+ import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../../../../grammar/legendTitleStyle.js";
4
+ import { PLOT_LEGEND_POSITIONS, isPlotLegend } from "../../../../layout/legend.js";
1
5
  import { validateOptionObject } from "../../../../core/validation.js";
2
6
  import { CATEGORICAL_LEGEND_CHANNELS } from "../../../../core/vocabulary.js";
3
7
  import { normalizeRecipe } from "./recipes.js";
@@ -9,11 +13,12 @@ import {
9
13
  validateKeys,
10
14
  validateObject
11
15
  } from "./validation.js";
12
- import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
16
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_LEGEND_ITEM_GAP } from
13
17
  "../../../../theme/defaults.js";
14
18
 
15
19
  export const CHANNELS = CATEGORICAL_LEGEND_CHANNELS;
16
20
  const OPTIONS = Object.freeze([
21
+ "clipHeight",
17
22
  "target",
18
23
  "channels",
19
24
  "order",
@@ -29,7 +34,8 @@ const OPTIONS = Object.freeze([
29
34
  "labels",
30
35
  "titleStyle",
31
36
  "itemGap",
32
- "border"
37
+ "border",
38
+ "overflow"
33
39
  ]);
34
40
  const TEXT_OPTIONS = Object.freeze([
35
41
  "offset",
@@ -39,17 +45,25 @@ const TEXT_OPTIONS = Object.freeze([
39
45
  "fontWeight",
40
46
  "format"
41
47
  ]);
42
- const TITLE_OPTIONS = Object.freeze([
43
- "color",
44
- "fontSize",
45
- "fontFamily",
46
- "fontWeight"
47
- ]);
48
+
49
+ export function normalizeLegendOverflow(value) {
50
+ if (value === undefined || value === false) return value;
51
+ validateObject(value, ["maxItems", "summary"], "Legend overflow");
52
+ if (!Number.isInteger(value.maxItems) || value.maxItems < 1) {
53
+ throw new RangeError("Legend overflow.maxItems must be a positive integer.");
54
+ }
55
+ if (value.summary !== undefined && value.summary !== "ellipsis-count") {
56
+ throw new Error("Legend overflow.summary must be ellipsis-count.");
57
+ }
58
+ return { maxItems: value.maxItems, summary: "ellipsis-count" };
59
+ }
60
+
48
61
  const BORDER_OPTIONS = Object.freeze([
49
62
  "color",
50
63
  "lineWidth",
51
64
  "padding",
52
- "background"
65
+ "background",
66
+ "strokeDash"
53
67
  ]);
54
68
  const COMMON_DEFAULTS = Object.freeze({
55
69
  labels: Object.freeze({
@@ -85,7 +99,8 @@ function normalizeBorder(border) {
85
99
  nonEmptyString(normalized.background, "Legend border background");
86
100
  nonNegative(normalized.lineWidth, "Legend border lineWidth");
87
101
  nonNegative(normalized.padding, "Legend border padding");
88
- return normalized;
102
+ const dash = normalizeLegendBorderDash(normalized.strokeDash);
103
+ return { ...normalized, ...(dash === undefined ? {} : { strokeDash: dash }) };
89
104
  }
90
105
 
91
106
  export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
@@ -99,7 +114,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
99
114
  if (Object.hasOwn(args, "titleStyle")) {
100
115
  validateObject(
101
116
  args.titleStyle,
102
- TITLE_OPTIONS,
117
+ LEGEND_TITLE_STYLE_KEYS,
103
118
  "createLegend.titleStyle"
104
119
  );
105
120
  }
@@ -121,13 +136,15 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
121
136
  };
122
137
  const position = args.position ?? defaults.position;
123
138
  const align = args.align ?? defaults.align;
139
+ const inside = isPlotLegend({ position });
124
140
  const side = ["left", "right"].includes(position);
125
- const direction = args.direction ?? (side ? "vertical" : "horizontal");
141
+ const direction = args.direction ?? (side || inside ? "vertical" : "horizontal");
126
142
  const columns = args.columns;
127
143
  const offset = args.offset ?? 8;
128
144
  const titlePosition = args.titlePosition ?? "top";
129
- const itemGap = args.itemGap ?? (["right", "left"].includes(position)
130
- ? 28
145
+ validateLegendTitleAlignment(titleStyle, titlePosition);
146
+ const itemGap = args.itemGap ?? (direction === "vertical"
147
+ ? DEFAULT_VERTICAL_LEGEND_ITEM_GAP
131
148
  : position === "top" ? 24 : 20);
132
149
  const layout = args.layout === undefined ? "edge" : args.layout;
133
150
  if (!["edge", "legacy-bottom"].includes(layout)) {
@@ -142,14 +159,14 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
142
159
  throw new Error('Legend columns, vertical direction, left title and custom offset require layout "edge".');
143
160
  }
144
161
 
145
- if (!["right", "left", "bottom", "top"].includes(position)) {
162
+ if (!["right", "left", "bottom", "top", ...PLOT_LEGEND_POSITIONS].includes(position)) {
146
163
  throw new Error(`Unsupported legend position "${position}".`);
147
164
  }
148
165
  if (!["left", "center", "right"].includes(align)) {
149
166
  throw new Error(`Unsupported legend alignment "${align}".`);
150
167
  }
151
- if (["right", "left"].includes(position) && align !== "center") {
152
- throw new Error("Side legends currently require center alignment.");
168
+ if ((side || inside) && align !== "center") {
169
+ throw new Error(inside ? "In-plot legends require center alignment." : "Side legends currently require center alignment.");
153
170
  }
154
171
  if (!["horizontal", "vertical"].includes(direction)) {
155
172
  throw new Error(`Unsupported legend direction "${direction}".`);
@@ -157,11 +174,8 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
157
174
  if (side && direction !== "vertical") {
158
175
  throw new Error("Side legends require vertical direction.");
159
176
  }
160
- if (side && columns !== undefined && columns !== 1) {
161
- throw new Error("Side legends require one column.");
162
- }
163
- if (side && titlePosition !== "top") {
164
- throw new Error("Side legends require a top title.");
177
+ if ((side || inside) && titlePosition !== "top") {
178
+ throw new Error(inside ? "In-plot legends require a top title." : "Side legends require a top title.");
165
179
  }
166
180
  if (columns !== undefined && (!Number.isInteger(columns) || columns <= 0)) {
167
181
  throw new RangeError("Legend columns must be a positive integer.");
@@ -181,7 +195,10 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
181
195
  validateFontWeight(titleStyle.fontWeight, "Legend title fontWeight");
182
196
  positive(itemGap, "Legend itemGap");
183
197
 
198
+ const overflow = normalizeLegendOverflow(args.overflow);
184
199
  return {
200
+ ...(args.clipHeight===undefined?{}:{clipHeight:normalizeLegendClipHeight(args.clipHeight)}),
201
+ ...(overflow === undefined ? {} : { overflow }),
185
202
  target: args.target,
186
203
  channels: args.channels,
187
204
  position,