ggaction 0.0.21 → 0.0.22

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 (253) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +6 -2
  7. package/src/BasicChartProgram.js +1 -1
  8. package/src/accessibility.js +1 -0
  9. package/src/actions/boxPlots/components.js +1 -96
  10. package/src/actions/boxPlots/create.js +6 -1
  11. package/src/actions/boxPlots/edit.js +8 -0
  12. package/src/actions/boxPlots/materialize.js +3 -0
  13. package/src/actions/boxPlots/options.js +1 -1
  14. package/src/actions/canvas/actions.js +2 -1
  15. package/src/actions/canvas/index.js +3 -0
  16. package/src/actions/canvas/plotBorder.js +2 -1
  17. package/src/actions/canvas/plotOrder.js +14 -0
  18. package/src/actions/charts/area.js +1 -1
  19. package/src/actions/charts/bar.js +4 -4
  20. package/src/actions/charts/density.js +1 -1
  21. package/src/actions/charts/ecdf.js +1 -1
  22. package/src/actions/charts/endpoints.js +54 -5
  23. package/src/actions/charts/heatmap.js +1 -1
  24. package/src/actions/charts/histogram.js +1 -1
  25. package/src/actions/charts/horizon.js +1 -1
  26. package/src/actions/charts/index.js +2 -1
  27. package/src/actions/charts/interval-regression.js +1 -1
  28. package/src/actions/charts/line.js +1 -1
  29. package/src/actions/charts/radar.js +63 -3
  30. package/src/actions/charts/raincloud.js +37 -6
  31. package/src/actions/charts/rug-strip.js +2 -2
  32. package/src/actions/charts/scatter.js +3 -3
  33. package/src/actions/charts/text.js +1 -1
  34. package/src/actions/composition/actions.js +10 -3
  35. package/src/actions/coordinates/actions.js +2 -0
  36. package/src/actions/data/basic.js +6 -0
  37. package/src/actions/data/derived.js +6 -2
  38. package/src/actions/data/edit.js +21 -35
  39. package/src/actions/data/filter.js +1 -376
  40. package/src/actions/data/flatten.js +19 -0
  41. package/src/actions/data/index.js +6 -0
  42. package/src/actions/data/lookup.js +50 -0
  43. package/src/actions/data/raincloudFilter.js +56 -0
  44. package/src/actions/data/shared.js +10 -0
  45. package/src/actions/distributions/revision.js +3 -1
  46. package/src/actions/encodings/angle.js +2 -1
  47. package/src/actions/encodings/appearance.js +38 -13
  48. package/src/actions/encodings/channels.js +14 -3
  49. package/src/actions/encodings/color/continuous.js +26 -10
  50. package/src/actions/encodings/color/index.js +50 -20
  51. package/src/actions/encodings/color/policy.js +57 -4
  52. package/src/actions/encodings/parallel.js +4 -0
  53. package/src/actions/encodings/position/policies/bar.js +10 -6
  54. package/src/actions/encodings/position/policies/line.js +1 -152
  55. package/src/actions/encodings/position/resolve.js +4 -1
  56. package/src/actions/encodings/ranged.js +46 -0
  57. package/src/actions/encodings/remove.js +1 -1
  58. package/src/actions/encodings/ruleAppearance.js +7 -6
  59. package/src/actions/encodings/shared.js +14 -2
  60. package/src/actions/encodings/stroke.js +20 -14
  61. package/src/actions/encodings/strokeDash.js +9 -11
  62. package/src/actions/encodings/text.js +30 -2
  63. package/src/actions/errorBars/edit.js +24 -4
  64. package/src/actions/facets/actions.js +1 -793
  65. package/src/actions/facets/derive.js +34 -5
  66. package/src/actions/facets/replay.js +1 -38
  67. package/src/actions/gradientPlots/components.js +21 -9
  68. package/src/actions/gradientPlots/create.js +4 -2
  69. package/src/actions/gradientPlots/edit.js +18 -1
  70. package/src/actions/gradientPlots/materialize.js +35 -36
  71. package/src/actions/gradientPlots/options.js +1 -1
  72. package/src/actions/gradientPlots/paint.js +13 -7
  73. package/src/actions/guides/applicability.js +2 -0
  74. package/src/actions/guides/axes/axes.js +39 -0
  75. package/src/actions/guides/axes/axis.js +9 -5
  76. package/src/actions/guides/axes/edit.js +11 -8
  77. package/src/actions/guides/axes/labels.js +24 -8
  78. package/src/actions/guides/axes/parallel/policy.js +3 -5
  79. package/src/actions/guides/axes/parallel/resolve.js +4 -3
  80. package/src/actions/guides/axes/policy.js +22 -0
  81. package/src/actions/guides/axes/tickGroups.js +4 -2
  82. package/src/actions/guides/axes/ticks.js +22 -4
  83. package/src/actions/guides/axes/titles.js +19 -6
  84. package/src/actions/guides/facadeAxes.js +3 -0
  85. package/src/actions/guides/grids/grid.js +3 -1
  86. package/src/actions/guides/grids/resolve.js +7 -11
  87. package/src/actions/guides/guides.js +21 -1
  88. package/src/actions/guides/legends/categorical/actions.js +1 -515
  89. package/src/actions/guides/legends/categorical/layout.js +5 -2
  90. package/src/actions/guides/legends/categorical/options.js +4 -0
  91. package/src/actions/guides/legends/categorical/resolve.js +8 -3
  92. package/src/actions/guides/legends/categorical/symbols.js +3 -3
  93. package/src/actions/guides/legends/colorSize.js +2 -0
  94. package/src/actions/guides/legends/continuous/common.js +1 -1
  95. package/src/actions/guides/legends/continuous/gradient.js +1 -360
  96. package/src/actions/guides/legends/continuous/interval.js +1 -1
  97. package/src/actions/guides/legends/continuous/opacity.js +15 -1
  98. package/src/actions/guides/legends/edit.js +1 -1
  99. package/src/actions/guides/legends/focused.js +1 -1
  100. package/src/actions/guides/legends/lane.js +1 -1
  101. package/src/actions/guides/legends/sampling.js +1 -140
  102. package/src/actions/guides/legends/strokeWidth.js +3 -2
  103. package/src/actions/guides/legends/transition.js +19 -2
  104. package/src/actions/guides/polar/axes/facade.js +5 -4
  105. package/src/actions/guides/polar/axes/labels.js +5 -4
  106. package/src/actions/guides/polar/axes/titles.js +5 -4
  107. package/src/actions/guides/polar/resolve.js +2 -1
  108. package/src/actions/guides/tickValues.js +7 -7
  109. package/src/actions/marks/arc/actions.js +5 -7
  110. package/src/actions/marks/area/actions.js +9 -3
  111. package/src/actions/marks/bar/create.js +4 -2
  112. package/src/actions/marks/bar/edit.js +9 -6
  113. package/src/actions/marks/bar/materialize.js +5 -3
  114. package/src/actions/marks/dependentReferences.js +87 -0
  115. package/src/actions/marks/index.js +2 -0
  116. package/src/actions/marks/lifecycle.js +2 -1
  117. package/src/actions/marks/line/actions.js +24 -6
  118. package/src/actions/marks/line/materialize.js +7 -3
  119. package/src/actions/marks/point/create.js +5 -4
  120. package/src/actions/marks/point/edit.js +6 -3
  121. package/src/actions/marks/point/jitter.js +6 -6
  122. package/src/actions/marks/point/materialize.js +36 -39
  123. package/src/actions/marks/rect/actions.js +16 -4
  124. package/src/actions/marks/references.js +183 -35
  125. package/src/actions/marks/rule/actions.js +20 -9
  126. package/src/actions/marks/shared.js +2 -1
  127. package/src/actions/marks/text/actions.js +10 -3
  128. package/src/actions/marks/tick/actions.js +11 -2
  129. package/src/actions/primitives/editGraphics.js +28 -3
  130. package/src/actions/primitives/index.js +9 -0
  131. package/src/actions/primitives/semanticAction.js +13 -7
  132. package/src/actions/primitives/semanticValidation/layer.js +20 -0
  133. package/src/actions/primitives/semanticValidation/scale.js +8 -1
  134. package/src/actions/scales/channels.js +1 -1
  135. package/src/actions/scales/consumers/common.js +19 -1
  136. package/src/actions/scales/consumers/families.js +2 -2
  137. package/src/actions/scales/consumers/seriesLayout.js +4 -3
  138. package/src/actions/scales/edit.js +11 -0
  139. package/src/actions/scales/editPolicy.js +5 -3
  140. package/src/actions/scales/preview.js +2 -1
  141. package/src/actions/scales/quantitativeColor.js +2 -4
  142. package/src/actions/selection/actions.js +10 -1
  143. package/src/actions/theme/reconcile.js +37 -9
  144. package/src/core/compositionState.js +21 -4
  145. package/src/core/datasetReferences.js +17 -0
  146. package/src/core/immutable.js +1 -0
  147. package/src/core/resourceReferences.js +5 -3
  148. package/src/core/validation.js +4 -0
  149. package/src/core/vocabulary.js +24 -2
  150. package/src/grammar/aggregate.js +1 -0
  151. package/src/grammar/arcs.js +1 -1
  152. package/src/grammar/areaSeries.js +1 -1
  153. package/src/grammar/axisTextPlacement.js +1 -1
  154. package/src/grammar/bars/policy.js +2 -2
  155. package/src/grammar/datasetDependencies.js +97 -0
  156. package/src/grammar/datasetSchema.js +31 -1
  157. package/src/grammar/facetHeaders.js +1 -0
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/legendIntent.js +6 -2
  160. package/src/grammar/facets/scales.js +27 -7
  161. package/src/grammar/flatten.js +85 -0
  162. package/src/grammar/histogram.js +13 -1
  163. package/src/grammar/horizon.js +1 -494
  164. package/src/grammar/jitter.js +2 -2
  165. package/src/grammar/legendSpacing.js +45 -0
  166. package/src/grammar/lineMissing.js +90 -0
  167. package/src/grammar/lineSeries.js +1 -376
  168. package/src/grammar/lookup.js +97 -0
  169. package/src/grammar/markFilter.js +4 -2
  170. package/src/grammar/markLabels.js +8 -3
  171. package/src/grammar/numeric.js +1 -1
  172. package/src/grammar/pathCommands.js +1 -1
  173. package/src/grammar/pathSeries.js +1 -109
  174. package/src/grammar/pointPolygon.js +81 -0
  175. package/src/grammar/pointShapes.js +15 -15
  176. package/src/grammar/rects.js +1 -1
  177. package/src/grammar/regression/derive.js +1 -195
  178. package/src/grammar/scales/appearance.js +1 -1
  179. package/src/grammar/scales/color.js +1 -1
  180. package/src/grammar/scales/colorConsumers.js +1 -63
  181. package/src/grammar/scales/continuous.js +58 -1
  182. package/src/grammar/scales/definition.js +1 -1
  183. package/src/grammar/scales/ordinal.js +5 -2
  184. package/src/grammar/scales/policies.js +3 -3
  185. package/src/grammar/schemas/graphicBounds.js +40 -12
  186. package/src/grammar/schemas/graphicClip.js +23 -0
  187. package/src/grammar/schemas/graphicTree.js +16 -0
  188. package/src/grammar/statisticalReference.js +22 -0
  189. package/src/grammar/text.js +65 -1
  190. package/src/grammar/ticks.js +92 -1
  191. package/src/grammar/transformTopology.js +3 -1
  192. package/src/grammar/transforms.js +24 -0
  193. package/src/inspection.js +1 -495
  194. package/src/inspectionDescriptors.js +1 -1
  195. package/src/layout/canvas.js +1 -1
  196. package/src/layout/facetContentFlow.js +101 -0
  197. package/src/layout/facetHeaderTitles.js +1 -0
  198. package/src/layout/facetTopLane.js +29 -0
  199. package/src/layout/facets.js +1 -1
  200. package/src/layout/legendGroup.js +1 -1
  201. package/src/layout/legendItems.js +26 -10
  202. package/src/layout/legendSpacing.js +66 -0
  203. package/src/layout/plotDrawOrder.js +17 -0
  204. package/src/materialization/bars/histogram.js +8 -2
  205. package/src/materialization/bars/resolve.js +1 -1
  206. package/src/materialization/composition.js +1 -0
  207. package/src/materialization/compositionSnapshot.js +22 -2
  208. package/src/materialization/dataProvenance.js +1 -1
  209. package/src/materialization/facetGuides/layout.js +1 -1
  210. package/src/materialization/facetGuides/placement.js +23 -11
  211. package/src/materialization/facetGuides/preparation.js +43 -24
  212. package/src/materialization/facetHeaders.js +1 -1
  213. package/src/materialization/facetTopLane.js +25 -0
  214. package/src/materialization/facets.js +1 -1
  215. package/src/materialization/graphicHierarchy.js +5 -0
  216. package/src/materialization/graphicTranslation.js +2 -0
  217. package/src/materialization/guides/layout.js +1 -1
  218. package/src/materialization/guides/resources.js +1 -1
  219. package/src/materialization/legends.js +1 -1
  220. package/src/materialization/markClip.js +26 -0
  221. package/src/materialization/marks/capabilities.js +3 -2
  222. package/src/materialization/marks/grouping.js +7 -5
  223. package/src/materialization/marks/index.js +4 -2
  224. package/src/materialization/marks/policies.js +4 -0
  225. package/src/materialization/plotDrawOrder.js +44 -0
  226. package/src/materialization/rect.js +11 -3
  227. package/src/materialization/scaleGuideDependencies.js +13 -5
  228. package/src/materialization/selection/items/common.js +1 -1
  229. package/src/materialization/selection/items/path.js +1 -0
  230. package/src/materialization/selection/policies/index.js +2 -1
  231. package/src/materialization/text.js +10 -5
  232. package/src/mcp/cli.js +0 -0
  233. package/src/persistence/validation.js +108 -18
  234. package/src/persistence.js +4 -2
  235. package/src/renderers/canvas/index.js +16 -0
  236. package/src/renderers/canvas/isolated.js +11 -1
  237. package/src/renderers/canvas/native.js +7 -0
  238. package/src/renderers/canvas/scene.js +66 -0
  239. package/src/renderers/canvas/text.js +3 -2
  240. package/src/renderers/svg.js +1 -617
  241. package/src/renderers/svgSerialization.js +643 -0
  242. package/src/renderers/transitionScene.js +67 -0
  243. package/src/scene.js +89 -0
  244. package/src/selectors/axes.js +1 -1
  245. package/src/selectors/datasets.js +1 -0
  246. package/src/selectors/grids.js +9 -0
  247. package/src/selectors/markClip.js +28 -0
  248. package/src/selectors/raincloud.js +7 -0
  249. package/src/version.js +1 -1
  250. package/types/basic.d.ts +7 -0
  251. package/types/index.d.ts +8 -0
  252. package/types/program.d.ts +270 -60
  253. package/types/scene.d.ts +35 -0
@@ -1,515 +1 @@
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";
9
- import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
10
- import { sameGuideValue } from "../../reuse.js";
11
- import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
12
- import { action } from "../../../../core/action.js";
13
- import { validateOptionObject } from "../../../../core/validation.js";
14
- import { noOptions, resolveLayout, activeConfig } from "./layout.js";
15
- import { normalizeOptions } from "./options.js";
16
- import { normalizeRecipe, resolveLegendSymbol } from "./recipes.js";
17
- import { validateLegendChannels } from "../target.js";
18
- import { createCategoricalLegendFromConfig, reconcileCategoricalSymbols } from "../lifecycle.js";
19
- import { findLayer } from "../../../../selectors/layers.js";
20
- import { findSemanticScale } from "../../../../selectors/scales.js";
21
- import { isSizeLegendPoint, resolveSizeLegendConfig } from "../size.js";
22
- import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
23
- import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
24
- import { legendResourcePolicies } from
25
- "../../../../materialization/guides/resources.js";
26
- import {
27
- hasHorizontalLegendLane,
28
- hasLegendLane
29
- } from "../lane.js";
30
- import {
31
- resolveCurrentDefinition,
32
- resolveDefinition,
33
- resolveTarget,
34
- sameValues
35
- } from "./resolve.js";
36
- import { readLegendSampling } from "../sampling.js";
37
- import {
38
- reconcileLegendBlockTitleGraphic,
39
- resolveEffectiveLegendBlockConfig
40
- } from "../blocks.js";
41
-
42
- function finishLegend(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;
45
- }
46
-
47
- function requestedCandidate(program, target, candidates) {
48
- if (target === undefined) {
49
- return candidates.length === 1 ? candidates[0] : undefined;
50
- }
51
- const layer = findLayer(program, target);
52
- return candidates.includes(layer) ? layer : undefined;
53
- }
54
-
55
- function resolveStandaloneLegendStep(args, kind) {
56
- if (Object.hasOwn(args, "inheritAppearance")) throw new Error("createLegend does not accept inheritAppearance.");
57
- const { channels: _channels, ...options } = args;
58
- return { op: kind === "size" ? "createSizeLegend" : "createStrokeWidthLegend", args: options };
59
- }
60
-
61
- export const rematerializeLegend = /* @__PURE__ */ action(
62
- { op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
63
- withGuideLayoutValidation(function (args = {}) {
64
- noOptions(args, "rematerializeLegend");
65
- const oldClipContext=this.context.deferLegendClip;
66
- let next = resetLegendClipGraphics(this)._withContext({deferLegendClip:true});
67
- const categoricalKinds = ["series", "color", "stroke"].filter(
68
- kind => this.guideConfigs.legend?.[kind] !== undefined
69
- );
70
- for (const kind of categoricalKinds) {
71
- const stored = next.guideConfigs.legend[kind];
72
- const definition = resolveCurrentDefinition(next, stored);
73
- const symbol = stored.inferredSymbol
74
- ? normalizeRecipe(
75
- resolveLegendSymbol(
76
- next,
77
- findLayer(next, stored.target),
78
- definition.channels,
79
- undefined,
80
- kind
81
- ),
82
- kind,
83
- { internal: true }
84
- )
85
- : stored.symbol;
86
- const symbolChanged = !sameGuideValue(symbol, stored.symbol);
87
- const changed = symbolChanged ||
88
- !sameValues(stored.channels, definition.channels) ||
89
- !sameValues(stored.domain, definition.domain) ||
90
- !sameValues(stored.scales, definition.scales) ||
91
- stored.field !== definition.field ||
92
- !sameValues(stored.sizeAreas ?? [], definition.sizeAreas ?? []) ||
93
- stored.title !== definition.title;
94
- next = changed
95
- ? next._withLegendConfig(kind, {
96
- ...stored,
97
- symbol,
98
- channels: definition.channels,
99
- scales: definition.scales,
100
- field: definition.field,
101
- ...(definition.combine === undefined ? {} : {combine: definition.combine, sizeAreas: definition.sizeAreas}),
102
- title: definition.title,
103
- domain: definition.domain
104
- })
105
- : next;
106
- const config = resolveEffectiveLegendBlockConfig(
107
- next,
108
- kind,
109
- next.guideConfigs.legend[kind]
110
- );
111
- next = reconcileLegendBlockTitleGraphic(next, kind,
112
- config.titleVisible !== false);
113
- if (kind === "series") {
114
- if (!sameValues(
115
- next.semanticSpec.guides.legend.series.scales,
116
- definition.scales
117
- )) {
118
- next = next.editSemantic({
119
- property: "guide.legend.series.scales",
120
- value: definition.scales
121
- });
122
- }
123
- if (next.semanticSpec.guides.legend.series.title !== definition.title) {
124
- next = next.editSemantic({
125
- property: "guide.legend.series.title",
126
- value: definition.title
127
- });
128
- }
129
- } else {
130
- if (next.semanticSpec.guides.legend[kind].scale !== definition.scales[0]) {
131
- next = next.editSemantic({
132
- property: `guide.legend.${kind}.scale`,
133
- value: definition.scales[0]
134
- });
135
- }
136
- if (next.semanticSpec.guides.legend[kind].title !== definition.title) {
137
- next = next.editSemantic({
138
- property: `guide.legend.${kind}.title`,
139
- value: definition.title
140
- });
141
- }
142
- }
143
- if (symbolChanged) {
144
- resolveLayout(next, config);
145
- next = reconcileCategoricalSymbols(next, stored, config);
146
- }
147
- if (config.border !== false) {
148
- next = next.rematerializeLegendBackground({ kind });
149
- }
150
- next = next
151
- .rematerializeLegendSymbols({ kind })
152
- .rematerializeLegendLabels({ kind });
153
- if (config.titleVisible !== false) {
154
- next = next.rematerializeLegendTitle({ kind });
155
- }
156
- const hasHighlight = Object.values(
157
- next.materializationConfigs.highlights ?? {}
158
- ).some(highlight => highlight.target === config.target);
159
- if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
160
- }
161
- if(next.guideConfigs.legend?.colorSize !== undefined) next=materializeColorSizeLegend(next,next.guideConfigs.legend.colorSize);
162
- for (const policy of legendResourcePolicies()) {
163
- if (
164
- policy.rematerializeOp !== undefined &&
165
- this.guideConfigs.legend?.[policy.kind] !== undefined
166
- ) {
167
- const config = resolveEffectiveLegendBlockConfig(
168
- next,
169
- policy.kind,
170
- next.guideConfigs.legend[policy.kind]
171
- );
172
- next = reconcileLegendBlockTitleGraphic(next, policy.kind,
173
- config.titleVisible !== false);
174
- next = next[policy.rematerializeOp]();
175
- }
176
- }
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);
181
- if (hasHorizontalLegendLane(next)) {
182
- next = next.rematerializeHorizontalLegendLane();
183
- }
184
- return next;
185
- })
186
- );
187
-
188
- export function resolveCategoricalLegendConfig(program, args = {}) {
189
- const layer = resolveTarget(program, args.target);
190
- const definition = resolveDefinition(
191
- program,
192
- layer,
193
- args.channels,
194
- args.title === false ? undefined : args.title,
195
- args.order === undefined ? undefined : normalizeLegendOrder(args.order)
196
- );
197
- const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
198
- const options = normalizeOptions({ ...args,
199
- symbol: resolveLegendSymbol(
200
- program,
201
- layer,
202
- definition.channels,
203
- args.symbol,
204
- definition.kind
205
- )
206
- }, definition.kind, { internalSymbol: inferredSymbol });
207
- const config = {
208
- target: layer.id,
209
- ...definition,
210
- ...normalizeInitialLegendTitle(args.title, definition.title),
211
- inferredTitle: args.title === false || !Object.hasOwn(args, "title"),
212
- inferredSymbol,
213
- position: options.position,
214
- align: options.align,
215
- direction: options.direction,
216
- columns: options.columns,
217
- offset: options.offset,
218
- titlePosition: options.titlePosition,
219
- symbol: options.symbol,
220
- labels: options.labels,
221
- titleStyle: options.titleStyle,
222
- itemGap: options.itemGap,
223
- layout: options.layout,
224
- ...(options.clipHeight === undefined ? {} : {clipHeight:options.clipHeight}),
225
- ...(options.overflow === undefined ? {} : { overflow: options.overflow }),
226
- border: options.border
227
- };
228
- return config;
229
- }
230
-
231
- export const createCategoricalLegend = /* @__PURE__ */ action(
232
- { op: "createCategoricalLegend", description: "Create one categorical legend block." },
233
- withGuideLayoutValidation(function (args = {}) {
234
- const config = resolveCategoricalLegendConfig(this, args);
235
- if (this.semanticSpec.guides.legend?.[config.kind] !== undefined) {
236
- throw new Error(
237
- `createCategoricalLegend requires a missing legend for ${config.kind}.`
238
- );
239
- }
240
- resolveLayout(this, config);
241
- return createCategoricalLegendFromConfig(this, config,
242
- args.order === undefined ? undefined : normalizeLegendOrder(args.order));
243
- })
244
- );
245
-
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
- }
258
- validateOptionObject(args, undefined, "createLegend");
259
- const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
260
- const channels = args.channels;
261
- if (channels !== undefined) validateLegendChannels(channels, "createLegend");
262
- const standalone = [
263
- ["size", isSizeLegendPoint, ["color", "stroke", "shape", "strokeDash", "opacity"]],
264
- ["strokeWidth", isStrokeWidthLegendLayer, args.target === undefined
265
- ? ["color", "stroke", "shape", "strokeDash", "size", "opacity"]
266
- : ["color", "stroke", "shape", "strokeDash", "opacity"]],
267
- ["opacity", isOpacityLegendLayer,
268
- ["color", "stroke", "shape", "strokeDash", "size"]]
269
- ];
270
- for (const [kind, eligible, otherChannels] of standalone) {
271
- const explicit = channels?.length === 1 && channels[0] === kind;
272
- const inferred = channels === undefined && candidates.filter(eligible).length === 1 &&
273
- !candidates.some(layer => otherChannels.some(channel => layer.encoding?.[channel]?.scale !== undefined));
274
- if (explicit || inferred) {
275
- const step = kind === "opacity" ? { op: "createOpacityLegend", args }
276
- : resolveStandaloneLegendStep(args, kind);
277
- return { steps: [step], finish: "auto" };
278
- }
279
- }
280
- const continuousColorCandidates = candidates.filter(layer => {
281
- const encoding = ["point", "bar", "rect", "text", "area"].includes(layer.mark?.type)
282
- ? layer.encoding?.color
283
- : undefined;
284
- const scale = findSemanticScale(program, encoding?.scale);
285
- return isContinuousColorScaleType(scale?.type);
286
- });
287
- const continuousColor = requestedCandidate(
288
- program, args.target, continuousColorCandidates
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
- }
298
- if (
299
- (channels?.length === 1 && channels[0] === "color" && continuousColor) ||
300
- (channels === undefined && continuousColor)
301
- ) {
302
- return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
303
- }
304
- const intervalColorCandidates = candidates.filter(layer => {
305
- const encoding = ["point", "bar", "rect", "text", "area"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
306
- const scale = findSemanticScale(program, encoding?.scale);
307
- return ["quantize", "quantile", "threshold"].includes(scale?.type);
308
- });
309
- if (
310
- (channels?.length === 1 && channels[0] === "color" &&
311
- intervalColorCandidates.length > 0) ||
312
- (channels === undefined && intervalColorCandidates.length > 0)
313
- ) {
314
- return { steps: [{ op: "createIntervalLegend", args }], finish: "auto" };
315
- }
316
- const continuousStrokeCandidates = candidates.filter(layer => {
317
- const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
318
- return isContinuousColorScaleType(scale?.type);
319
- });
320
- const continuousStroke = requestedCandidate(
321
- program,
322
- args.target,
323
- continuousStrokeCandidates
324
- );
325
- if (
326
- (channels?.length === 1 && channels[0] === "stroke" && continuousStroke) ||
327
- (channels === undefined && continuousStroke &&
328
- !["color", "shape", "strokeDash", "size", "opacity", "strokeWidth"].some(
329
- channel => continuousStroke.encoding?.[channel]?.scale !== undefined
330
- ))
331
- ) {
332
- return {
333
- steps: [{ op: "createStrokeGradientLegend", args: {
334
- ...args,
335
- channels: ["stroke"]
336
- } }],
337
- finish: "auto"
338
- };
339
- }
340
- const intervalStrokeCandidates = candidates.filter(layer => {
341
- const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
342
- return ["quantize", "quantile", "threshold"].includes(scale?.type);
343
- });
344
- if (
345
- (channels?.length === 1 && channels[0] === "stroke" &&
346
- intervalStrokeCandidates.length > 0) ||
347
- (channels === undefined && intervalStrokeCandidates.length === 1)
348
- ) {
349
- return {
350
- steps: [{ op: "createStrokeIntervalLegend", args: {
351
- ...args,
352
- channels: ["stroke"]
353
- } }],
354
- finish: "auto"
355
- };
356
- }
357
- const wantsShape = channels?.includes("shape") === true;
358
- const wantsSize = channels?.includes("size") === true;
359
- const pointCandidates = candidates.filter(layer =>
360
- layer.mark?.type === "point" &&
361
- (channels === undefined
362
- ? ["color", "shape"].some(channel => layer.encoding?.[channel]?.scale !== undefined)
363
- : (wantsShape || wantsSize) && channels.every(channel =>
364
- ["color", "stroke", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
365
- );
366
- const requestedPoint = requestedCandidate(program, args.target, pointCandidates);
367
- const inferredSize = channels === undefined && pointCandidates.some(isSizeLegendPoint);
368
- if ((wantsSize || inferredSize) && requestedPoint === undefined) {
369
- throw new Error("Combined size legend requires one eligible point mark or an explicit target.");
370
- }
371
- if (requestedPoint !== undefined) {
372
- if (Object.hasOwn(args, "values")) {
373
- throw new Error(
374
- "Combined and categorical legends require a channel block selector for exact values."
375
- );
376
- }
377
- const { count, ...categoricalArgs } = args;
378
- const combined = requestedPoint.encoding?.size?.scale !== undefined &&
379
- (channels === undefined || wantsSize);
380
- if (count !== undefined && !combined) {
381
- throw new Error("Legend count requires a selected size legend.");
382
- }
383
- if (combined && categoricalArgs.layout === "legacy-bottom") {
384
- throw new Error('Combined size legends require layout "edge".');
385
- }
386
- const inferredChannels = ["color", "stroke", "shape"].filter(
387
- channel => requestedPoint.encoding?.[channel]?.scale !== undefined
388
- );
389
- const { format: sizeFormat, ...categoricalLabels } = categoricalArgs.labels ?? {};
390
- if (!combined && sizeFormat !== undefined && sizeFormat !== "auto") {
391
- throw new Error("Categorical legend labels do not accept format.");
392
- }
393
- const steps = [{ op: "createCategoricalLegend", args: {
394
- ...categoricalArgs,
395
- ...(categoricalArgs.labels === undefined ? {} : { labels: categoricalLabels }),
396
- target: requestedPoint.id,
397
- channels: channels?.filter(channel => channel !== "size") ?? inferredChannels
398
- } }];
399
- if (combined) steps.push({ op: "createSizeLegend", args: {
400
- target: requestedPoint.id,
401
- ...(count === undefined ? {} : { count }),
402
- ...(categoricalArgs.overflow !== undefined &&
403
- findSemanticScale(program, requestedPoint.encoding.size.scale)?.type === "ordinal"
404
- ? { overflow: categoricalArgs.overflow } : {}),
405
- ...(categoricalArgs.labels === undefined ? {} : { labels: {
406
- ...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
407
- ...(sizeFormat === undefined ? {} : { format: sizeFormat })
408
- } }),
409
- inheritAppearance: true
410
- } });
411
- // Validate both content owners before any component action starts.
412
- resolveCategoricalLegendConfig(program, steps[0].args);
413
- if (combined) {
414
- const size = resolveSizeLegendConfig(program, steps[1].args);
415
- const existing = program.guideConfigs.legend?.size;
416
- if (existing !== undefined && existing.target !== size.target) {
417
- throw new Error("Combined point series legend requires the active size legend to share its target.");
418
- }
419
- if (existing !== undefined && count !== undefined &&
420
- readLegendSampling(existing).count !== count) {
421
- throw new Error("Existing size legend count must be edited before recreating the categorical block.");
422
- }
423
- }
424
- return { steps, combined, finish: combined ? "always" : "auto" };
425
- }
426
- return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
427
- }
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
-
464
- export function applyLegendCreationPlan(program, plan) {
465
- if (plan.steps.length === 0) return program;
466
- let next = plan.legendGroup === undefined ? program : program._withMaterializationConfig(
467
- ["guides", "legendGroups", plan.legendGroup.target], plan.legendGroup.config);
468
- for (const step of plan.steps) {
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
- }
473
- // The plan already validated that a retained size block is compatible.
474
- if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
475
- next = step.op === "createColorSizeLegend" ? createColorSizeLegendFromConfig(next,resolveColorSizeConfig(next,step.args)) : next[step.op](step.args);
476
- }
477
- }
478
- return plan.finish === "always" ? next.rematerializeLegend()
479
- : plan.finish === "auto" ? finishLegend(next) : next;
480
- }
481
-
482
- export const createLegend = /* @__PURE__ */ action(
483
- { op: "createLegend", description: "Create an inferred legend for selected channels." },
484
- withGuideLayoutValidation(function (args = {}) {
485
- return recordLegendOffsetIntent(this, applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args)), args, { creation: true });
486
- })
487
- );
488
-
489
- export const removeCategoricalLegend = /* @__PURE__ */ action(
490
- {
491
- op: "removeCategoricalLegend",
492
- description: "Remove the active categorical legend and its concrete components."
493
- },
494
- withGuideLayoutValidation(function (args = {}) {
495
- noOptions(args, "removeCategoricalLegend");
496
- const entries = ["series", "color", "stroke"]
497
- .filter(kind => this.guideConfigs.legend?.[kind] !== undefined);
498
- if (entries.length === 0) return this;
499
- if (entries.length !== 1) {
500
- throw new Error("removeCategoricalLegend requires one active categorical legend.");
501
- }
502
- const kind = entries[0];
503
- const prefix = `${kind}Legend`;
504
- const targets = Object.keys(this.graphicSpec.objects)
505
- .filter(id => id.startsWith(prefix));
506
- let next = this.editSemantic({
507
- property: `guide.legend.${kind}`,
508
- remove: true
509
- });
510
- for (const target of targets) {
511
- next = next.editGraphics({ target, remove: true });
512
- }
513
- return next._withoutMaterializationConfig(["guides", "legend", kind]);
514
- })
515
- );
1
+ import{resolveColorSizeConfig as B,createColorSizeLegendFromConfig as se,materializeColorSizeLegend as de}from"../colorSize.js";import{resetLegendClipGraphics as re,applyLegendClipGraphics as le,restoreLegendClipContext as ce}from"../../../../materialization/legendClip.js";import{validateLegendCombine as ae,resolveSizeShapeCreation as fe}from"../combine.js";import{recordLegendOffsetIntent as ue}from"../group.js";import{prepareLegendGroup as F,creationLegendTarget as N,validateLegendGroupOptions as ge}from"../group.js";import{PLOT_LEGEND_POSITIONS as pe}from"../../../../layout/legend.js";import{normalizeInitialLegendTitle as he}from"../continuous/common.js";import{isContinuousColorScaleType as K}from"../../../../grammar/scales/types.js";import{withGuideLayoutValidation as M}from"../../../../materialization/guides/layout.js";import{sameGuideValue as Y}from"../../reuse.js";import{normalizeLegendOrder as J}from"../../../../grammar/categoryOrder.js";import{action as W}from"../../../../core/action.js";import{validateOptionObject as me}from"../../../../core/validation.js";import{noOptions as Q,resolveLayout as A}from"./layout.js";import{normalizeOptions as Le}from"./options.js";import{normalizeRecipe as Ce,resolveLegendSymbol as U}from"./recipes.js";import{validateLegendChannels as be}from"../target.js";import{createCategoricalLegendFromConfig as ze,reconcileCategoricalSymbols as ye}from"../lifecycle.js";import{findLayer as X}from"../../../../selectors/layers.js";import{findSemanticScale as G}from"../../../../selectors/scales.js";import{isSizeLegendPoint as D,resolveSizeLegendConfig as Z}from"../size.js";import{isStrokeWidthLegendLayer as ke}from"../strokeWidth.js";import{isOpacityLegendLayer as ee}from"../../../../materialization/legends.js";import{legendResourcePolicies as Se}from"../../../../materialization/guides/resources.js";import{hasHorizontalLegendLane as ve,hasLegendLane as we}from"../lane.js";import{resolveCurrentDefinition as Oe,resolveDefinition as xe,resolveTarget as Ge,sameValues as j}from"./resolve.js";import{readLegendSampling as Pe}from"../sampling.js";import{reconcileLegendBlockTitleGraphic as ne,resolveEffectiveLegendBlockConfig as T}from"../blocks.js";function Ee(i){return Object.values(i.guideConfigs.legend??{}).some(e=>e.clipHeight!==void 0)||we(i)||Object.keys(i.guideConfigs.legendGroups??{}).length>0?i.rematerializeLegend():i}function $(i,e,n){if(e===void 0)return n.length===1?n[0]:void 0;const a=X(i,e);return n.includes(a)?a:void 0}function ie(i,e){if(Object.hasOwn(i,"inheritAppearance"))throw new Error("createLegend does not accept inheritAppearance.");const{channels:n,...a}=i;return{op:e==="size"?"createSizeLegend":"createStrokeWidthLegend",args:a}}const fn=W({op:"rematerializeLegend",description:"Rematerialize every existing legend component."},M(function(i={}){Q(i,"rematerializeLegend");const e=this.context.deferLegendClip;let n=re(this)._withContext({deferLegendClip:!0});const a=["series","color","stroke"].filter(t=>this.guideConfigs.legend?.[t]!==void 0);for(const t of a){const d=n.guideConfigs.legend[t],{defaultLabelMap:s,...L}=d,l=Oe(n,d),S=d.inferredSymbol?Ce(U(n,X(n,d.target),l.channels,void 0,t),t,{internal:!0}):d.symbol,b=!Y(S,d.symbol);n=b||!Y(s,l.defaultLabelMap)||!j(d.channels,l.channels)||!j(d.domain,l.domain)||!j(d.scales,l.scales)||d.field!==l.field||!j(d.sizeAreas??[],l.sizeAreas??[])||d.title!==l.title?n._withLegendConfig(t,{...L,symbol:S,channels:l.channels,scales:l.scales,field:l.field,...l.defaultLabelMap===void 0?{}:{defaultLabelMap:l.defaultLabelMap},...l.combine===void 0?{}:{combine:l.combine,sizeAreas:l.sizeAreas},title:l.title,domain:l.domain}):n;const g=T(n,t,n.guideConfigs.legend[t]);n=ne(n,t,g.titleVisible!==!1),t==="series"?(j(n.semanticSpec.guides.legend.series.scales,l.scales)||(n=n.editSemantic({property:"guide.legend.series.scales",value:l.scales})),n.semanticSpec.guides.legend.series.title!==l.title&&(n=n.editSemantic({property:"guide.legend.series.title",value:l.title}))):(n.semanticSpec.guides.legend[t].scale!==l.scales[0]&&(n=n.editSemantic({property:`guide.legend.${t}.scale`,value:l.scales[0]})),n.semanticSpec.guides.legend[t].title!==l.title&&(n=n.editSemantic({property:`guide.legend.${t}.title`,value:l.title}))),b&&(A(n,g),n=ye(n,d,g)),g.border!==!1&&(n=n.rematerializeLegendBackground({kind:t})),n=n.rematerializeLegendSymbols({kind:t}).rematerializeLegendLabels({kind:t}),g.titleVisible!==!1&&(n=n.rematerializeLegendTitle({kind:t})),Object.values(n.materializationConfigs.highlights??{}).some(E=>E.target===g.target)&&(n=n.rematerializeLegendHighlights({kind:t}))}n.guideConfigs.legend?.colorSize!==void 0&&(n=de(n,n.guideConfigs.legend.colorSize));for(const t of Se())if(t.rematerializeOp!==void 0&&this.guideConfigs.legend?.[t.kind]!==void 0){const d=T(n,t.kind,n.guideConfigs.legend[t.kind]);n=ne(n,t.kind,d.titleVisible!==!1),n=n[t.rematerializeOp]()}const r=Object.fromEntries(a.filter(t=>n.guideConfigs.legend[t].clipHeight!==void 0).map(t=>[t,A(n,T(n,t,n.guideConfigs.legend[t])).itemY]));return n=le(n,{force:!0,anchors:r}),n=ce(n,e),ve(n)&&(n=n.rematerializeHorizontalLegendLane()),n}));function I(i,e={}){const n=Ge(i,e.target),a=xe(i,n,e.channels,e.title===!1?void 0:e.title,e.order===void 0?void 0:J(e.order)),r=e.symbol===void 0||e.symbol==="auto",t=Le({...e,symbol:U(i,n,a.channels,e.symbol,a.kind)},a.kind,{internalSymbol:r});return{target:n.id,...a,...he(e.title,a.title),inferredTitle:e.title===!1||!Object.hasOwn(e,"title"),inferredSymbol:r,position:t.position,align:t.align,direction:t.direction,columns:t.columns,offset:t.offset,titlePosition:t.titlePosition,symbol:t.symbol,labels:t.labels,titleStyle:t.titleStyle,itemGap:t.itemGap,...t.rowPadding===void 0?{}:{rowPadding:t.rowPadding},...t.titlePadding===void 0?{}:{titlePadding:t.titlePadding},layout:t.layout,...t.clipHeight===void 0?{}:{clipHeight:t.clipHeight},...t.overflow===void 0?{}:{overflow:t.overflow},border:t.border}}const un=W({op:"createCategoricalLegend",description:"Create one categorical legend block."},M(function(i={}){const e=I(this,i);if(this.semanticSpec.guides.legend?.[e.kind]!==void 0)throw new Error(`createCategoricalLegend requires a missing legend for ${e.kind}.`);return A(this,e),ze(this,e,i.order===void 0?void 0:J(i.order))}));function P(i,e={},n=i.semanticSpec.layers,a){if(Object.hasOwn(e,"combine")){if(ae(e.combine),e.combine==="color-size"){const u=B(i,e),O=F(i,{...e,blockLayout:e.blockLayout??(["left","right"].includes(e.position??"right")?"column":"row")},u.target,{creation:!0,nativeOffset:30});return{steps:[{op:"createColorSizeLegend",args:e}],finish:"always",legendGroup:{target:u.target,config:O.program.guideConfigs.legendGroups[u.target]}}}if(e.combine==="size-shape")return fe(i,e,n,P,a);const{combine:o,...c}=e;return P(i,c,n)}me(e,void 0,"createLegend");const r=e.target===void 0?n:n.filter(o=>o.id===e.target),t=r.length===1&&["area","rect","bar","arc","text","rule","tick"].includes(r[0].mark?.type),d=t?["color","stroke"].filter(o=>r[0].encoding?.[o]?.scale!==void 0):[],s=e.channels!==void 0?e.channels:d.length>0?d:void 0;if(e.channels===void 0&&s?.length>0)return P(i,{...e,channels:s},n);s!==void 0&&be(s,"createLegend");const L=r.length===1&&r[0].mark?.type==="point",l=o=>["color","stroke"].includes(o)&&!["ordinal","band","point"].includes(G(i,r[0]?.encoding?.[o]?.scale)?.type),S=L&&((s??[]).some(l)||new Set((s??[]).filter(o=>!["size","opacity","strokeWidth"].includes(o)).map(o=>r[0].encoding?.[o]?.field)).size>1);if(s?.length>1&&(S||s.some(o=>["opacity","strokeWidth"].includes(o))||t)){const{representation:o,values:c,count:u,symbol:O,gradient:m,overflow:z,...y}=e,p=s.filter(f=>["opacity","strokeWidth"].includes(f)),x=s.filter(f=>!p.includes(f)),q=new Map;for(const f of x.filter(h=>h!=="size")){const h=l(f)?`channel:${f}`:`field:${r[0]?.encoding?.[f]?.field}`;q.has(h)||q.set(h,[]),q.get(h).push(f)}const H=[...q.values()];if(x.includes("size")){const f=H.find(h=>!h.some(l));f?f.push("size"):H.push(["size"])}return{steps:(L?[...H,...p.map(f=>[f])]:s.map(f=>[f])).flatMap(f=>P(i,f.includes("color")&&(o!==void 0||m!==void 0)?{...e,channels:f}:{...y,channels:f,...f.some(l)&&m!==void 0?{gradient:m}:{},...f.some(h=>!["size","opacity","strokeWidth"].includes(h)&&["ordinal","band","point"].includes(G(i,r[0]?.encoding?.[h]?.scale)?.type))&&z!==void 0?{overflow:z}:{},...f.some(h=>["size","opacity","strokeWidth"].includes(h))&&u!==void 0?{count:u}:{}},n).steps),finish:"always"}}if(s===void 0&&r.length===1&&D(r[0])&&ee(r[0])&&!["color","stroke","shape","strokeDash","strokeWidth"].some(o=>r[0].encoding?.[o]?.scale!==void 0)){const o=r[0].id;return{steps:[ie({...e,target:o},"size"),{op:"createOpacityLegend",args:{...e,target:o}}],finish:"auto"}}const b=[["size",D,["color","stroke","shape","strokeDash","opacity"]],["strokeWidth",ke,e.target===void 0?["color","stroke","shape","strokeDash","size","opacity"]:["color","stroke","shape","strokeDash","opacity"]],["opacity",ee,["color","stroke","shape","strokeDash","size"]]];for(const[o,c,u]of b){const O=s?.length===1&&s[0]===o,m=s===void 0&&r.filter(c).length===1&&!r.some(z=>u.some(y=>z.encoding?.[y]?.scale!==void 0));if(O||m)return{steps:[o==="opacity"?{op:"createOpacityLegend",args:e}:ie(e,o)],finish:"auto"}}const v=r.filter(o=>{const c=["point","line","bar","rect","text","area","arc","tick"].includes(o.mark?.type)?o.encoding?.color:void 0,u=G(i,c?.scale);return K(u?.type)}),g=$(i,e.target,v);if(e.representation!==void 0&&s!==void 0&&s.length>1){if(!g||!s.includes("color"))throw new Error("Legend representation requires selected continuous color.");const{representation:o,values:c,count:u,symbol:O,gradient:m,...z}=e;return{steps:s.flatMap(p=>P(i,p==="color"?{...e,channels:[p]}:{...z,channels:[p]},n).steps),finish:"always"}}if(s?.length===1&&s[0]==="color"&&g||s===void 0&&g)return{steps:[{op:"createGradientLegend",args:e}],finish:"auto"};const C=r.filter(o=>{const c=["point","line","bar","rect","text","area","arc","tick"].includes(o.mark?.type)?o.encoding?.color:void 0,u=G(i,c?.scale);return["quantize","quantile","threshold"].includes(u?.type)});if(s?.length===1&&s[0]==="color"&&C.length>0||s===void 0&&C.length>0)return{steps:[{op:"createIntervalLegend",args:e}],finish:"auto"};const E=r.filter(o=>{const c=G(i,o.encoding?.stroke?.scale);return K(c?.type)}),k=$(i,e.target,E);if(s?.length===1&&s[0]==="stroke"&&k||s===void 0&&k&&!["color","shape","strokeDash","size","opacity","strokeWidth"].some(o=>k.encoding?.[o]?.scale!==void 0))return{steps:[{op:"createStrokeGradientLegend",args:{...e,channels:["stroke"]}}],finish:"auto"};const V=r.filter(o=>{const c=G(i,o.encoding?.stroke?.scale);return["quantize","quantile","threshold"].includes(c?.type)});if(s?.length===1&&s[0]==="stroke"&&V.length>0||s===void 0&&V.length===1)return{steps:[{op:"createStrokeIntervalLegend",args:{...e,channels:["stroke"]}}],finish:"auto"};const te=s?.includes("shape")===!0,_=s?.includes("size")===!0,R=r.filter(o=>o.mark?.type==="point"&&(s===void 0?["color","shape"].some(c=>o.encoding?.[c]?.scale!==void 0):(te||_)&&s.every(c=>["color","stroke","shape","size"].includes(c)&&o.encoding?.[c]?.scale!==void 0))),w=$(i,e.target,R),oe=s===void 0&&R.some(D);if((_||oe)&&w===void 0)throw new Error("Combined size legend requires one eligible point mark or an explicit target.");if(w!==void 0){if(Object.hasOwn(e,"values"))throw new Error("Combined and categorical legends require a channel block selector for exact values.");const{count:o,...c}=e,u=w.encoding?.size?.scale!==void 0&&(s===void 0||_);if(o!==void 0&&!u)throw new Error("Legend count requires a selected size legend.");if(u&&c.layout==="legacy-bottom")throw new Error('Combined size legends require layout "edge".');const O=["color","stroke","shape"].filter(p=>w.encoding?.[p]?.scale!==void 0),{format:m,...z}=c.labels??{};if(!u&&m!==void 0&&m!=="auto")throw new Error("Categorical legend labels do not accept format.");const y=[{op:"createCategoricalLegend",args:{...c,...c.labels===void 0?{}:{labels:z},target:w.id,channels:s?.filter(p=>p!=="size")??O}}];if(u&&y.push({op:"createSizeLegend",args:{target:w.id,...o===void 0?{}:{count:o},...c.overflow!==void 0&&G(i,w.encoding.size.scale)?.type==="ordinal"?{overflow:c.overflow}:{},...c.labels===void 0?{}:{labels:{...c.labels.offset===void 0?{}:{offset:c.labels.offset},...m===void 0?{}:{format:m}}},inheritAppearance:!0}}),I(i,y[0].args),u){const p=Z(i,y[1].args),x=i.guideConfigs.legend?.size;if(x!==void 0&&x.target!==p.target)throw new Error("Combined point series legend requires the active size legend to share its target.");if(x!==void 0&&o!==void 0&&Pe(x).count!==o)throw new Error("Existing size legend count must be edited before recreating the categorical block.")}return{steps:y,combined:u,finish:u?"always":"auto"}}return{steps:[{op:"createCategoricalLegend",args:e}],finish:"auto"}}function je(i,e={},n=i.semanticSpec.layers,a){ge(e);const{blockLayout:r,at:t,...d}=e,s=i.guideConfigs.legendGroups??{};if(Object.keys(s).length>0&&d.position===void 0){const E=N(i,e),k=s[E]?.placement;k?.position!==void 0&&(d.position=k.position,d.align=e.align??k.align,d.offset=e.offset??k.offset)}d.position===void 0&&e.direction==="horizontal"&&(e.at!==void 0||Object.keys(s).length>0)&&(d.position="top");const L=P(i,d,n,a),l=Object.keys(i.guideConfigs.legendGroups??{}).length>0,S=pe.includes(e.position)&&(L.steps.length>1||L.steps[0]?.op!=="createCategoricalLegend"||Object.keys(i.guideConfigs.legend??{}).length>0);if(e.blockLayout===void 0&&e.at===void 0&&!l&&!S)return L;const b=N(i,e),v=L.steps[0],g=i.guideConfigs.legendGroups?.[b]!==void 0?30:v?.op==="createCategoricalLegend"?I(i,v.args).offset:v?.op==="createSizeLegend"?Z(i,v.args).offset:30,C=F(i,e,b,{creation:!0,corner:S,nativeOffset:g});return C.grouped?(C.options.position===void 0&&d.position!==void 0&&(C.options.position=d.position),{...P(C.program,C.options,n,a),legendGroup:{target:b,config:C.program.guideConfigs.legendGroups[b]}}):L}function qe(i,e){if(e.steps.length===0)return i;let n=e.legendGroup===void 0?i:i._withMaterializationConfig(["guides","legendGroups",e.legendGroup.target],e.legendGroup.config);for(const a of e.steps){const r=n.guideConfigs.legend?.size;e.combined&&r!==void 0&&a.op==="createCategoricalLegend"&&(n=n._withLegendConfig("size",{...r,inheritPosition:!0})),e.combined&&a.op==="createSizeLegend"&&r!==void 0||(n=a.op==="createColorSizeLegend"?se(n,B(n,a.args)):n[a.op](a.args))}return e.finish==="always"?n.rematerializeLegend():e.finish==="auto"?Ee(n):n}const gn=W({op:"createLegend",description:"Create an inferred legend for selected channels."},M(function(i={}){return ue(this,qe(this,je(this,i)),i,{creation:!0})})),pn=W({op:"removeCategoricalLegend",description:"Remove the active categorical legend and its concrete components."},M(function(i={}){Q(i,"removeCategoricalLegend");const e=["series","color","stroke"].filter(d=>this.guideConfigs.legend?.[d]!==void 0);if(e.length===0)return this;if(e.length!==1)throw new Error("removeCategoricalLegend requires one active categorical legend.");const n=e[0],a=`${n}Legend`,r=Object.keys(this.graphicSpec.objects).filter(d=>d.startsWith(a));let t=this.editSemantic({property:`guide.legend.${n}`,remove:!0});for(const d of r)t=t.editGraphics({target:d,remove:!0});return t._withoutMaterializationConfig(["guides","legend",n])}));export{qe as applyLegendCreationPlan,un as createCategoricalLegend,gn as createLegend,fn as rematerializeLegend,pn as removeCategoricalLegend,I as resolveCategoricalLegendConfig,je as resolveLegendCreationPlan};
@@ -86,7 +86,7 @@ export function categoricalLegendLabels(config) {
86
86
  config = categoricalLegendView(config);
87
87
  const labels = config.domain.map(value => resolveDisplayLabel(
88
88
  value,
89
- config.labelMap,
89
+ [...(config.labelMap ?? []), ...(config.defaultLabelMap ?? [])],
90
90
  formatVisibleText
91
91
  ));
92
92
  return config.hiddenCount > 0 ? [...labels, `…${config.hiddenCount} entries`] : labels;
@@ -174,8 +174,11 @@ export function resolveAppearance(program, config) {
174
174
  target.encoding.color === undefined &&
175
175
  !config.channels.includes("color") &&
176
176
  isLinearGradientPaint(program.markConfigs[target.id]?.fill);
177
+ const fixedLineStroke = target?.mark?.type === "line" &&
178
+ config.channels.includes("strokeDash")
179
+ ? program.markConfigs[target.id]?.stroke : undefined;
177
180
  const fallback = gradientFallback
178
- ? program.markConfigs[target.id].fill : DEFAULT_COLORS.mark;
181
+ ? program.markConfigs[target.id].fill : fixedLineStroke ?? DEFAULT_COLORS.mark;
179
182
  let colors = config.domain.map(() => fallback);
180
183
  let strokes = config.domain.map(() => "white");
181
184
  let dashes = config.domain.map(() => []);
@@ -1,4 +1,5 @@
1
1
  import{normalizeLegendClipHeight}from"../../../../grammar/legendClip.js";
2
+ import {normalizeLegendSpacing,validateLegendSpacing} from "../../../../grammar/legendSpacing.js";
2
3
  import { normalizeLegendBorderDash } from "../../../../grammar/legendBorder.js";
3
4
  import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../../../../grammar/legendTitleStyle.js";
4
5
  import { PLOT_LEGEND_POSITIONS, isPlotLegend } from "../../../../layout/legend.js";
@@ -34,6 +35,7 @@ const OPTIONS = Object.freeze([
34
35
  "labels",
35
36
  "titleStyle",
36
37
  "itemGap",
38
+ "rowPadding", "titlePadding",
37
39
  "border",
38
40
  "overflow"
39
41
  ]);
@@ -147,6 +149,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
147
149
  ? DEFAULT_VERTICAL_LEGEND_ITEM_GAP
148
150
  : position === "top" ? 24 : 20);
149
151
  const layout = args.layout === undefined ? "edge" : args.layout;
152
+ validateLegendSpacing(kind,{...args,layout});
150
153
  if (!["edge", "legacy-bottom"].includes(layout)) {
151
154
  throw new Error(`Unsupported legend layout "${layout}".`);
152
155
  }
@@ -198,6 +201,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
198
201
  const overflow = normalizeLegendOverflow(args.overflow);
199
202
  return {
200
203
  ...(args.clipHeight===undefined?{}:{clipHeight:normalizeLegendClipHeight(args.clipHeight)}),
204
+ ...normalizeLegendSpacing(args),
201
205
  ...(overflow === undefined ? {} : { overflow }),
202
206
  target: args.target,
203
207
  channels: args.channels,
@@ -11,6 +11,7 @@ import { CHANNELS } from "./options.js";
11
11
  import { nonEmptyString } from "./validation.js";
12
12
  import { findLayer } from "../../../../selectors/layers.js";
13
13
  import { findSemanticScale } from "../../../../selectors/scales.js";
14
+ import { horizonLegendPresentation } from "../../../../grammar/horizon.js";
14
15
 
15
16
  function isCategoricalTarget(layer) {
16
17
  if (["line", "point"].includes(layer?.mark?.type)) {
@@ -76,8 +77,9 @@ function resolveOrdinalScales(program, scaleIds) {
76
77
  );
77
78
  return concrete;
78
79
  });
79
- if (scales.slice(1).some(scale => !sameValues(scale.domain, scales[0].domain))) {
80
- throw new Error("Combined legend scales must have identical ordered domains.");
80
+ if (scales.slice(1).some(scale => scale.domain.length !== scales[0].domain.length ||
81
+ scale.domain.some(value => !scales[0].domain.some(other => Object.is(value, other))))) {
82
+ throw new Error("Combined legend scales must contain the same domain values.");
81
83
  }
82
84
  return { scales, domain: scales[0].domain };
83
85
  }
@@ -119,6 +121,8 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
119
121
  const scales = encodings.map(item => item.encoding.scale);
120
122
  const resolved = resolveOrdinalScales(program, scales);
121
123
  const field = [...fields][0];
124
+ const transform = requireDataset(program, layer.data).transform?.[0];
125
+ const horizon = field === transform?.as?.color ? horizonLegendPresentation(transform) : undefined;
122
126
  let linked;
123
127
  if (order?.channel !== undefined) {
124
128
  const position = layer.encoding?.[order.channel];
@@ -132,7 +136,8 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
132
136
  channels,
133
137
  scales,
134
138
  field,
135
- title: nonEmptyString(requestedTitle ?? field, "Legend title"),
139
+ title: nonEmptyString(requestedTitle ?? horizon?.title ?? field, "Legend title"),
140
+ ...(horizon === undefined ? {} : { defaultLabelMap: horizon.labelMap }),
136
141
  domain: resolveLegendOrderDomain(resolved.domain, order, linked,
137
142
  order?.values === undefined ? undefined : readNominalField(requireDataset(program, layer.data).values, field)),
138
143
  ...(sizeShape ? {combine:"size-shape",...resolveSizeShapeMapping({layer,
@@ -55,14 +55,14 @@ function makeEditSymbol(type) {
55
55
  ? "collection"
56
56
  : { line: "line", point: "circle", swatch: "rect" }[type];
57
57
  const actual = this.graphicSpec.objects[id]?.type;
58
- const compatibleStyledSwatch = styledSwatch && ["rect", "collection"].includes(actual);
59
- if (actual !== expected && !compatibleStyledSwatch) {
58
+ const compatibleSwatch = type === "swatch" && ["rect", "collection"].includes(actual);
59
+ if (actual !== expected && !compatibleSwatch) {
60
60
  throw new Error(`${op} requires existing ${type} symbols.`);
61
61
  }
62
62
  const layout = resolveLayout(this, config);
63
63
  const appearance = resolveAppearance(this, config);
64
64
  let next = this;
65
- if (!dynamicPoint && !styledSwatch) {
65
+ if (!dynamicPoint && !styledSwatch && actual !== "collection") {
66
66
  next = next.editGraphics({
67
67
  target: id,
68
68
  property: "length",