ggaction 0.0.23 → 0.0.26

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 (228) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -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/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +6 -4
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/basicBin2d.js +1 -1
  27. package/src/actions/data/bin2d.js +2 -2
  28. package/src/actions/data/derived.js +3 -0
  29. package/src/actions/data/filter.js +1 -1
  30. package/src/actions/data/raincloudFilter.js +3 -1
  31. package/src/actions/encodings/appearance.js +26 -7
  32. package/src/actions/encodings/channels.js +1 -0
  33. package/src/actions/encodings/color/continuous.js +37 -34
  34. package/src/actions/encodings/color/index.js +57 -43
  35. package/src/actions/encodings/color/policy.js +15 -0
  36. package/src/actions/encodings/density/resolve.js +3 -2
  37. package/src/actions/encodings/density.js +2 -2
  38. package/src/actions/encodings/offset.js +2 -0
  39. package/src/actions/encodings/pathOrder.js +2 -0
  40. package/src/actions/encodings/position/areaAlignment.js +42 -0
  41. package/src/actions/encodings/position/index.js +37 -3
  42. package/src/actions/encodings/position/policies/line.js +1 -1
  43. package/src/actions/encodings/ranged.js +4 -2
  44. package/src/actions/encodings/remove.js +13 -10
  45. package/src/actions/encodings/ruleAppearance.js +14 -2
  46. package/src/actions/facets/actions.js +1 -1
  47. package/src/actions/guides/axes/axes.js +2 -1
  48. package/src/actions/guides/axes/components.js +5 -0
  49. package/src/actions/guides/axes/edit.js +1 -188
  50. package/src/actions/guides/axes/labels.js +2 -537
  51. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  52. package/src/actions/guides/axes/parallel/policy.js +5 -2
  53. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  54. package/src/actions/guides/axes/parallel.js +9 -4
  55. package/src/actions/guides/axes/tickGroups.js +16 -1
  56. package/src/actions/guides/axes/ticks.js +33 -24
  57. package/src/actions/guides/axes/titles.js +1 -389
  58. package/src/actions/guides/facadeLegend.js +3 -2
  59. package/src/actions/guides/grids/grid.js +3 -2
  60. package/src/actions/guides/grids/resolve.js +2 -1
  61. package/src/actions/guides/guides.js +3 -2
  62. package/src/actions/guides/legends/automatic.js +28 -0
  63. package/src/actions/guides/legends/blocks.js +15 -21
  64. package/src/actions/guides/legends/categorical/actions.js +1 -1
  65. package/src/actions/guides/legends/categorical/components.js +4 -3
  66. package/src/actions/guides/legends/categorical/layout.js +5 -2
  67. package/src/actions/guides/legends/categorical/options.js +9 -14
  68. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  69. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  70. package/src/actions/guides/legends/colorSize.js +51 -14
  71. package/src/actions/guides/legends/continuous/common.js +1 -1
  72. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  73. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  74. package/src/actions/guides/legends/edit.js +1 -1
  75. package/src/actions/guides/legends/focused.js +2 -2
  76. package/src/actions/guides/legends/group.js +1 -1
  77. package/src/actions/guides/legends/lane.js +1 -1
  78. package/src/actions/guides/legends/lifecycle.js +2 -2
  79. package/src/actions/guides/legends/remove.js +18 -4
  80. package/src/actions/guides/legends/size.js +1 -1
  81. package/src/actions/guides/legends/transition.js +15 -6
  82. package/src/actions/guides/polar/axes/facade.js +4 -2
  83. package/src/actions/guides/polar/axes/labels.js +6 -1
  84. package/src/actions/guides/polar/axes/shared.js +4 -4
  85. package/src/actions/guides/polar/axes/titles.js +13 -2
  86. package/src/actions/guides/polar/grids.js +6 -3
  87. package/src/actions/guides/tickValues.js +7 -1
  88. package/src/actions/index.js +2 -0
  89. package/src/actions/marks/area/actions.js +9 -2
  90. package/src/actions/marks/area/materialize.js +1 -1
  91. package/src/actions/marks/bar/materialize.js +4 -4
  92. package/src/actions/marks/lifecycle.js +14 -0
  93. package/src/actions/marks/line/actions.js +3 -0
  94. package/src/actions/marks/line/materialize.js +4 -2
  95. package/src/actions/marks/point/edit.js +2 -0
  96. package/src/actions/marks/point/jitter.js +2 -0
  97. package/src/actions/marks/point/materialize.js +5 -4
  98. package/src/actions/marks/point/packing.js +2 -0
  99. package/src/actions/marks/references.js +5 -3
  100. package/src/actions/marks/remove.js +16 -0
  101. package/src/actions/marks/shared.js +2 -1
  102. package/src/actions/marks/text/actions.js +5 -2
  103. package/src/actions/overlays/index.js +168 -0
  104. package/src/actions/overlays/snapshot.js +45 -0
  105. package/src/actions/primitives/editGraphics.js +21 -27
  106. package/src/actions/primitives/semanticAction.js +4 -5
  107. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  108. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  109. package/src/actions/scales/consumers/common.js +10 -0
  110. package/src/actions/scales/consumers/families.js +1 -1
  111. package/src/actions/scales/preview.js +1 -1
  112. package/src/actions/selection/actions.js +63 -11
  113. package/src/actions/titles/actions.js +3 -14
  114. package/src/actions/titles/resolve.js +23 -10
  115. package/src/core/action.js +41 -25
  116. package/src/core/compositionScales.js +12 -0
  117. package/src/core/compositionState.js +20 -3
  118. package/src/core/facetGap.js +44 -0
  119. package/src/core/immutable.js +11 -6
  120. package/src/core/programState.js +16 -0
  121. package/src/core/resourceReferences.js +19 -3
  122. package/src/core/validation.js +7 -0
  123. package/src/core/vocabulary.js +8 -2
  124. package/src/grammar/actionTargets.js +24 -0
  125. package/src/grammar/axisTextPlacement.js +1 -1
  126. package/src/grammar/bin2d.js +41 -4
  127. package/src/grammar/colorSizeLegend.js +56 -5
  128. package/src/grammar/displayLabels.js +10 -0
  129. package/src/grammar/facetHeaders.js +1 -1
  130. package/src/grammar/facets/dependencies.js +10 -3
  131. package/src/grammar/facets/guides.js +2 -1
  132. package/src/grammar/facets/legendIntent.js +7 -5
  133. package/src/grammar/legendBlocks.js +1 -1
  134. package/src/grammar/legendCombinations.js +1 -1
  135. package/src/grammar/legendGroups.js +1 -1
  136. package/src/grammar/legendItemsOffset.js +12 -0
  137. package/src/grammar/legendOverflow.js +25 -0
  138. package/src/grammar/lineSeries.js +1 -1
  139. package/src/grammar/markSelection.js +23 -0
  140. package/src/grammar/overlay.js +51 -0
  141. package/src/grammar/parallelCoordinates.js +1 -208
  142. package/src/grammar/pathCommands.js +8 -2
  143. package/src/grammar/pathSeries.js +1 -1
  144. package/src/grammar/plotComposition.js +152 -0
  145. package/src/grammar/pointShapes.js +18 -0
  146. package/src/grammar/scales/transformed.js +1 -1
  147. package/src/grammar/schemas/concreteGraphic.js +8 -1
  148. package/src/grammar/schemas/graphicBounds.js +8 -1
  149. package/src/grammar/schemas/graphicTree.js +18 -2
  150. package/src/grammar/schemas/semanticPath.js +3 -0
  151. package/src/grammar/strokeStyle.js +14 -8
  152. package/src/grammar/text.js +72 -0
  153. package/src/inspection.js +1 -1
  154. package/src/inspectionDescriptors.js +1 -1
  155. package/src/layout/composition.js +32 -10
  156. package/src/layout/compositionLegend.js +91 -0
  157. package/src/layout/facetContentFlow.js +15 -7
  158. package/src/layout/facetHeaderTitles.js +1 -1
  159. package/src/layout/facets.js +1 -1
  160. package/src/layout/labels.js +55 -27
  161. package/src/layout/legend.js +3 -2
  162. package/src/layout/legendGroup.js +1 -1
  163. package/src/layout/legendItems.js +7 -6
  164. package/src/layout/legendLane.js +1 -1
  165. package/src/layout/legendText.js +1 -1
  166. package/src/layout/title.js +1 -1
  167. package/src/materialization/adjacentSource.js +27 -0
  168. package/src/materialization/composition.js +165 -52
  169. package/src/materialization/compositionLegends.js +120 -0
  170. package/src/materialization/compositionScales.js +125 -0
  171. package/src/materialization/compositionTitle.js +96 -0
  172. package/src/materialization/facetGuides/edit.js +22 -2
  173. package/src/materialization/facetGuides/layout.js +1 -1
  174. package/src/materialization/facetGuides/owner.js +4 -3
  175. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  176. package/src/materialization/facetGuides/placement.js +19 -22
  177. package/src/materialization/facetGuides/preparation.js +17 -5
  178. package/src/materialization/facetHeaders.js +5 -1
  179. package/src/materialization/facets.js +1 -1
  180. package/src/materialization/guides/layout.js +1 -1
  181. package/src/materialization/guides/resources.js +1 -1
  182. package/src/materialization/guides/visibility.js +105 -0
  183. package/src/materialization/legendClip.js +1 -1
  184. package/src/materialization/legendGroups.js +1 -1
  185. package/src/materialization/legendItemsOffset.js +68 -0
  186. package/src/materialization/legendSuppression.js +22 -0
  187. package/src/materialization/legends.js +3 -2
  188. package/src/materialization/marks/capabilities.js +4 -0
  189. package/src/materialization/marks/index.js +5 -1
  190. package/src/materialization/marks/pathOrder.js +1 -1
  191. package/src/materialization/marks/policies.js +2 -1
  192. package/src/materialization/overlayItems.js +154 -0
  193. package/src/materialization/planner.js +19 -4
  194. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  195. package/src/materialization/scales/policies/series.js +12 -4
  196. package/src/materialization/scales/resolve.js +2 -0
  197. package/src/materialization/selection/items/bar.js +21 -3
  198. package/src/materialization/selection/items/common.js +18 -5
  199. package/src/materialization/selection/items/overlay.js +15 -0
  200. package/src/materialization/selection/items/path.js +1 -0
  201. package/src/materialization/selection/items/point.js +2 -3
  202. package/src/materialization/selection/items/tick.js +3 -12
  203. package/src/materialization/selection/path.js +2 -2
  204. package/src/materialization/selection/policies/bar.js +2 -0
  205. package/src/materialization/selection/policies/index.js +35 -1
  206. package/src/materialization/selection/policies/point.js +2 -0
  207. package/src/materialization/selection/policies/text.js +17 -0
  208. package/src/materialization/selection/rows.js +103 -0
  209. package/src/materialization/selection/state.js +19 -3
  210. package/src/materialization/selection/styles.js +8 -0
  211. package/src/materialization/text.js +6 -489
  212. package/src/materialization/textGeometry.js +503 -0
  213. package/src/materialization/textLines.js +20 -0
  214. package/src/persistence/validation.js +116 -25
  215. package/src/renderers/canvas/index.js +12 -7
  216. package/src/renderers/canvas/text.js +5 -5
  217. package/src/renderers/nativeBackend.js +25 -0
  218. package/src/renderers/pdf.js +3 -3
  219. package/src/renderers/png.js +4 -3
  220. package/src/renderers/svgSerialization.js +1 -643
  221. package/src/scene.js +5 -3
  222. package/src/selectors/plotComposition.js +115 -0
  223. package/src/selectors/selectionTargets.js +19 -0
  224. package/src/version.js +1 -1
  225. package/types/basic.d.ts +4 -2
  226. package/types/index.d.ts +14 -0
  227. package/types/inspection.d.ts +34 -0
  228. package/types/program.d.ts +572 -372
@@ -0,0 +1,28 @@
1
+ import { resolveLegendCreationPlan } from "./categorical/actions.js";
2
+ import { resolveLegendStepConfig } from "./creation.js";
3
+ import { legendBlockChannels } from "../../../grammar/legendBlocks.js";
4
+
5
+ // Resolve the ordinary legend inference first, then exclude only suppressed
6
+ // owner/channel pairs. Explicit createLegend bypasses this automatic policy.
7
+ export function automaticLegendOptions(program, args, layers = program.semanticSpec.layers) {
8
+ if (args === undefined || !Object.keys(program.guideConfigs.suppressedLegends ?? {}).length) return args;
9
+ const plan = resolveLegendCreationPlan(program, args, layers);
10
+ const owners = new Map();
11
+ let changed = false;
12
+ for (const step of plan.steps) {
13
+ const { kind, config } = resolveLegendStepConfig(program, step);
14
+ const suppressed = program.guideConfigs.suppressedLegends?.[config.target]?.channels ?? [];
15
+ const channels = legendBlockChannels(kind, config);
16
+ const retained = channels.filter(channel => !suppressed.includes(channel));
17
+ changed ||= retained.length !== channels.length;
18
+ if (!owners.has(config.target)) owners.set(config.target, new Set());
19
+ retained.forEach(channel => owners.get(config.target).add(channel));
20
+ }
21
+ if (!changed) return args;
22
+ const active = [...owners].filter(([, channels]) => channels.size);
23
+ if (!active.length) return undefined;
24
+ if (active.length !== 1) throw new Error("Automatic legend requires one unambiguous target.");
25
+ const [target, channels] = active[0];
26
+ const { combine: _combine, ...options } = args;
27
+ return { ...options, target, channels: [...channels] };
28
+ }
@@ -1,3 +1,4 @@
1
+ import {colorSizeChannels,validateColorSizeShapeSamples} from "../../../grammar/colorSizeLegend.js";
1
2
  import { editFacetParentLegend } from "../../../materialization/facetGuides/edit.js";
2
3
 
3
4
  import {patchLegendClipHeight, validateLegendClip} from "../../../grammar/legendClip.js";
@@ -27,7 +28,7 @@ import {
27
28
  resolveLegendBlock
28
29
  } from "./target.js";
29
30
  import { validateFontWeight } from "./categorical/validation.js";
30
- import { normalizeDisplayLabelMap, normalizeNumericDisplayLabelMap } from "../../../grammar/displayLabels.js";
31
+ import { normalizeLegendDisplayLabelMap } from "../../../grammar/displayLabels.js";
31
32
 
32
33
  const OPTIONS = Object.freeze([
33
34
  "target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
@@ -67,7 +68,7 @@ function categoricalSupports(config, property) {
67
68
 
68
69
  function validateSymbolSupport(descriptor, property) {
69
70
  const { channels, family, kind, config } = descriptor;
70
- if(kind === "colorSize" && ["fill","size"].includes(property)) throw new Error("Color-size symbols cannot override mapped fill or size.");
71
+ if(kind === "colorSize" && ['size',config.colorChannel==='stroke'?'stroke':'fill'].includes(property)) throw new Error("Color-size symbols cannot override mapped hue or size.");
71
72
  if (family === "gradient") {
72
73
  throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
73
74
  }
@@ -131,21 +132,12 @@ function normalizeSymbolPatch(value, descriptor) {
131
132
  return { ...value };
132
133
  }
133
134
 
134
- function normalizeBlockLabelMap(descriptor, map) {
135
- if (["size","colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
136
- return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
137
- }
138
- if (descriptor.family !== "categorical") {
139
- throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
140
- }
141
- return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
142
- }
143
-
144
- export function validateLegendBlockOverride(descriptor, override) {
135
+ export function validateLegendBlockOverride(descriptor, override, scaleType) {
136
+ if (override.alongOffset !== undefined && !Number.isFinite(override.alongOffset)) throw new TypeError("Legend alongOffset must be finite.");
145
137
  if (override.position !== undefined && !LEGEND_POSITIONS.includes(override.position)) throw new Error("Invalid local legend position.");
146
138
  if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
147
139
  if (override.labelMap !== undefined) {
148
- normalizeBlockLabelMap(descriptor, override.labelMap);
140
+ normalizeLegendDisplayLabelMap(descriptor, override.labelMap, scaleType);
149
141
  }
150
142
  return override;
151
143
  }
@@ -191,7 +183,8 @@ export function patchLegendBlockOverride(config, key, patch) {
191
183
  }
192
184
  if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
193
185
  if (Object.hasOwn(patch, "position")) entry.position = patch.position;
194
- if (patch.removePosition === true) delete entry.position;
186
+ if (Object.hasOwn(patch, "alongOffset")) entry.alongOffset = patch.alongOffset;
187
+ if (patch.removePosition === true) { delete entry.position; delete entry.alongOffset; }
195
188
  if (patch.removeLabelMap === true) delete entry.labelMap;
196
189
  const blockOverrides = { ...(config.blockOverrides ?? {}) };
197
190
  if (Object.keys(entry).length === 0) delete blockOverrides[key];
@@ -224,7 +217,7 @@ function applyCategoricalSymbol(symbol, patch) {
224
217
  }
225
218
 
226
219
  export function resolveEffectiveLegendBlockConfig(program, kind, config) {
227
- const channels = kind === "colorSize" ? ["color","size"] : kind === "series" || kind === "color" || kind === "stroke"
220
+ const channels = kind === "colorSize" ? colorSizeChannels(config) : kind === "series" || kind === "color" || kind === "stroke"
228
221
  ? config.channels
229
222
  : [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
230
223
  gradient: "color", interval: "color", strokeGradient: "stroke",
@@ -339,10 +332,10 @@ function recreateSymbolGraphics(program, descriptor) {
339
332
  const placement = preserveGraphicPlacement(program, symbol.id);
340
333
  return program
341
334
  .editGraphics({ target: symbol.id, remove: true })
342
- .createGraphics({ id: symbol.id, type: symbol.type, length: 0, ...placement });
335
+ .createGraphics({ id: symbol.id, ...(descriptor.config.shapeScale===undefined?{type:symbol.type,length:0}:{type:'collection'}), ...placement });
343
336
  }
344
337
 
345
- function normalizeBlockPatch(args, descriptor) {
338
+ function normalizeBlockPatch(args, descriptor, scaleType) {
346
339
  const patch = {};
347
340
  if (Object.hasOwn(args, "position")) {
348
341
  if (args.position === "inherit") patch.removePosition = true;
@@ -367,7 +360,7 @@ function normalizeBlockPatch(args, descriptor) {
367
360
  }
368
361
  if (Object.hasOwn(args, "labelMap")) {
369
362
  // Validate eligibility even for a reset, without introducing an empty map owner.
370
- const map = normalizeBlockLabelMap(descriptor, args.labelMap === "auto" ? [] : args.labelMap);
363
+ const map = normalizeLegendDisplayLabelMap(descriptor, args.labelMap === "auto" ? [] : args.labelMap, scaleType);
371
364
  if (args.labelMap === "auto") patch.removeLabelMap = true;
372
365
  else patch.labelMap = map;
373
366
  }
@@ -396,7 +389,7 @@ export const editLegendBlock = /* @__PURE__ */ action(
396
389
  descriptor.family !== "sampled") {
397
390
  throw new Error("editLegendBlock values and count require a sampled legend block.");
398
391
  }
399
- const patch = normalizeBlockPatch(args, descriptor);
392
+ const patch = normalizeBlockPatch(args, descriptor, this.resolvedScales[descriptor.scaleIds[0]]?.type);
400
393
  let config = Object.hasOwn(args, "clipHeight")
401
394
  ? patchLegendClipHeight(descriptor.config, args.clipHeight) : descriptor.config;
402
395
  validateLegendClip(descriptor.kind, config);
@@ -413,7 +406,8 @@ export const editLegendBlock = /* @__PURE__ */ action(
413
406
  operation: "edit",
414
407
  label: `editLegendBlock ${descriptor.channel ?? args.channel} legend`
415
408
  });
416
- validateSamplingAgainstScale(sampling, this.resolvedScales[descriptor.scaleIds[0]],
409
+ if(config.combine==='color-size-shape')validateColorSizeShapeSamples(sampling,this.resolvedScales[config.colorScale]);
410
+ else validateSamplingAgainstScale(sampling, this.resolvedScales[descriptor.scaleIds[0]],
417
411
  "editLegendBlock values");
418
412
  const { count: _legacyCount, ...withoutCount } = config;
419
413
  config = { ...withoutCount, sampling };
@@ -1 +1 @@
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};
1
+ import{resolveColorSizeConfig as Y,createColorSizeLegendFromConfig as fe,materializeColorSizeLegend as ue,resolveColorSizeLayout as ge}from"../colorSize.js";import{resetLegendClipGraphics as pe,applyLegendClipGraphics as he,restoreLegendClipContext as me}from"../../../../materialization/legendClip.js";import{validateLegendCombine as Le,resolveSizeShapeCreation as Ce}from"../combine.js";import{recordLegendOffsetIntent as be}from"../group.js";import{prepareLegendGroup as J,creationLegendTarget as Q,validateLegendGroupOptions as ye}from"../group.js";import{PLOT_LEGEND_POSITIONS as ze}from"../../../../layout/legend.js";import{normalizeInitialLegendTitle as ke}from"../continuous/common.js";import{isContinuousColorScaleType as U}from"../../../../grammar/scales/types.js";import{withGuideLayoutValidation as H}from"../../../../materialization/guides/layout.js";import{sameGuideValue as X}from"../../reuse.js";import{normalizeLegendOrder as I}from"../../../../grammar/categoryOrder.js";import{action as W}from"../../../../core/action.js";import{validateOptionObject as Se}from"../../../../core/validation.js";import{noOptions as Z,resolveLayout as q}from"./layout.js";import{normalizeOptions as ve}from"./options.js";import{normalizeRecipe as we,resolveLegendSymbol as ee}from"./recipes.js";import{validateLegendChannels as Oe}from"../target.js";import{createCategoricalLegendFromConfig as D,reconcileCategoricalSymbols as xe,resolveCategoricalLegendRevision as ne,removeLegendKinds as ie}from"../lifecycle.js";import{findLayer as te}from"../../../../selectors/layers.js";import{findSemanticScale as G}from"../../../../selectors/scales.js";import{isSizeLegendPoint as R,resolveSizeLegendConfig as oe}from"../size.js";import{isStrokeWidthLegendLayer as Ge}from"../strokeWidth.js";import{isOpacityLegendLayer as se}from"../../../../materialization/legends.js";import{legendResourcePolicies as Pe}from"../../../../materialization/guides/resources.js";import{hasHorizontalLegendLane as Ee,hasLegendLane as je}from"../lane.js";import{resolveCurrentDefinition as qe,resolveDefinition as Me,resolveTarget as _e,sameValues as E}from"./resolve.js";import{readLegendSampling as He}from"../sampling.js";import{restoreCreatedLegendChannels as We}from"../../../../materialization/legendSuppression.js";import{reconcileLegendBlockTitleGraphic as re,resolveEffectiveLegendBlockConfig as V}from"../blocks.js";function De(t){return Object.values(t.guideConfigs.legend??{}).some(e=>e.clipHeight!==void 0)||je(t)||Object.keys(t.guideConfigs.legendGroups??{}).length>0?t.rematerializeLegend():t}function B(t,e,n){if(e===void 0)return n.length===1?n[0]:void 0;const d=te(t,e);return n.includes(d)?d:void 0}function de(t,e){if(Object.hasOwn(t,"inheritAppearance"))throw new Error("createLegend does not accept inheritAppearance.");const{channels:n,...d}=t;return{op:e==="size"?"createSizeLegend":"createStrokeWidthLegend",args:d}}const bn=W({op:"rematerializeLegend",description:"Rematerialize every existing legend component."},H(function(t={}){Z(t,"rematerializeLegend");const e=this.context.deferLegendClip;let n=pe(this)._withContext({deferLegendClip:!0});const d=["series","color","stroke"].filter(o=>this.guideConfigs.legend?.[o]!==void 0);for(const o of d){const c=n.guideConfigs.legend[o],{defaultLabelMap:g,...z}=c,i=qe(n,c),j=c.inferredSymbol?we(ee(n,te(n,c.target),i.channels,void 0,o),o,{internal:!0}):c.symbol,k=!X(j,c.symbol);n=k||!X(g,i.defaultLabelMap)||!E(c.channels,i.channels)||!E(c.domain,i.domain)||!E(c.scales,i.scales)||c.field!==i.field||!E(c.sizeAreas??[],i.sizeAreas??[])||c.title!==i.title?n._withLegendConfig(o,{...z,symbol:j,channels:i.channels,scales:i.scales,field:i.field,...i.defaultLabelMap===void 0?{}:{defaultLabelMap:i.defaultLabelMap},...i.combine===void 0?{}:{combine:i.combine,sizeAreas:i.sizeAreas},title:i.title,domain:i.domain}):n;const u=V(n,o,n.guideConfigs.legend[o]);n=re(n,o,u.titleVisible!==!1),o==="series"?(E(n.semanticSpec.guides.legend.series.scales,i.scales)||(n=n.editSemantic({property:"guide.legend.series.scales",value:i.scales})),n.semanticSpec.guides.legend.series.title!==i.title&&(n=n.editSemantic({property:"guide.legend.series.title",value:i.title}))):(n.semanticSpec.guides.legend[o].scale!==i.scales[0]&&(n=n.editSemantic({property:`guide.legend.${o}.scale`,value:i.scales[0]})),n.semanticSpec.guides.legend[o].title!==i.title&&(n=n.editSemantic({property:`guide.legend.${o}.title`,value:i.title}))),k&&(q(n,u),n=xe(n,c,u)),u.border!==!1&&(n=n.rematerializeLegendBackground({kind:o})),n=n.rematerializeLegendSymbols({kind:o}).rematerializeLegendLabels({kind:o}),u.titleVisible!==!1&&(n=n.rematerializeLegendTitle({kind:o})),Object.values(n.materializationConfigs.highlights??{}).some(C=>C.target===u.target)&&(n=n.rematerializeLegendHighlights({kind:o}))}n.guideConfigs.legend?.colorSize!==void 0&&(n=ue(n,n.guideConfigs.legend.colorSize));for(const o of Pe())if(o.rematerializeOp!==void 0&&this.guideConfigs.legend?.[o.kind]!==void 0){const c=V(n,o.kind,n.guideConfigs.legend[o.kind]);n=re(n,o.kind,c.titleVisible!==!1),n=n[o.rematerializeOp]()}const r=Object.fromEntries(d.filter(o=>n.guideConfigs.legend[o].clipHeight!==void 0).map(o=>[o,q(n,V(n,o,n.guideConfigs.legend[o])).itemY]));return n.guideConfigs.legend?.colorSize?.clipHeight!==void 0&&(r.colorSize=ge(n,n.guideConfigs.legend.colorSize).itemY),n=he(n,{force:!0,anchors:r}),n=me(n,e),Ee(n)&&(n=n.rematerializeHorizontalLegendLane()),n}));function F(t,e={}){const n=_e(t,e.target),d=Me(t,n,e.channels,e.title===!1?void 0:e.title,e.order===void 0?void 0:I(e.order)),r=e.symbol===void 0||e.symbol==="auto",o=ve({...e,symbol:ee(t,n,d.channels,e.symbol,d.kind)},d.kind,{internalSymbol:r});return{target:n.id,...d,...ke(e.title,d.title),inferredTitle:e.title===!1||!Object.hasOwn(e,"title"),inferredSymbol:r,position:o.position,align:o.align,direction:o.direction,columns:o.columns,offset:o.offset,titlePosition:o.titlePosition,symbol:o.symbol,labels:o.labels,titleStyle:o.titleStyle,itemGap:o.itemGap,...o.rowPadding===void 0?{}:{rowPadding:o.rowPadding},...o.titlePadding===void 0?{}:{titlePadding:o.titlePadding},layout:o.layout,...o.clipHeight===void 0?{}:{clipHeight:o.clipHeight},...o.overflow===void 0?{}:{overflow:o.overflow},border:o.border}}const yn=W({op:"createCategoricalLegend",description:"Create one categorical legend block."},H(function(t={}){const e=F(this,t);if(this.semanticSpec.guides.legend?.[e.kind]!==void 0){const n=this.guideConfigs.legend?.[e.kind];if(e.kind==="series"&&n?.target===e.target&&n.channels.every(d=>!e.channels.includes(d))){if(n.field===e.field){const d=ne(this,"series",n,[...n.channels,...e.channels]);return q(this,d.config),D(ie(this,["series"]),d.config,d.order)}if(E(n.channels,["color"])&&this.semanticSpec.guides.legend?.color===void 0){const d=ne(this,"series",n,["color"],{preferredKind:"color"});if(d.config.kind==="color"){let r=ie(this,["series"]);return r=D(r,d.config,d.order),q(r,e),D(r,e,t.order===void 0?void 0:I(t.order))}}}throw new Error(`createCategoricalLegend requires a missing legend for ${e.kind}.`)}return q(this,e),D(this,e,t.order===void 0?void 0:I(t.order))}));function P(t,e={},n=t.semanticSpec.layers,d){if(Object.hasOwn(e,"combine")){if(Le(e.combine),["color-size","color-size-shape"].includes(e.combine)){const f=Y(t,e),O=J(t,{...e,blockLayout:e.blockLayout??(["left","right"].includes(e.position??"right")?"column":"row")},f.target,{creation:!0,nativeOffset:30});return{steps:[{op:"createColorSizeLegend",args:e}],finish:"always",legendGroup:{target:f.target,config:O.program.guideConfigs.legendGroups[f.target]}}}if(e.combine==="size-shape")return Ce(t,e,n,P,d);const{combine:s,...l}=e;return P(t,l,n)}Se(e,void 0,"createLegend");const r=e.target===void 0?n:n.filter(s=>s.id===e.target),o=r.length===1&&["area","rect","bar","arc","text","rule","tick"].includes(r[0].mark?.type),c=r.length===1&&["line","point"].includes(r[0].mark?.type)?["color","stroke","shape","strokeDash"].filter(s=>r[0].encoding?.[s]?.scale!==void 0):[],z=new Set(c.map(s=>r[0].encoding[s].field)).size>1?c:o?["color","stroke"].filter(s=>r[0].encoding?.[s]?.scale!==void 0):[],i=e.channels!==void 0?e.channels:z.length>0?z:void 0;if(e.channels===void 0&&i?.length>0)return P(t,{...e,channels:i},n);i!==void 0&&Oe(i,"createLegend");const j=r.length===1&&r[0].mark?.type==="point",k=r.length===1&&["point","line"].includes(r[0].mark?.type),L=s=>["color","stroke"].includes(s)&&!["ordinal","band","point"].includes(G(t,r[0]?.encoding?.[s]?.scale)?.type),u=k&&((i??[]).some(L)||new Set((i??[]).filter(s=>!["size","opacity","strokeWidth"].includes(s)).map(s=>r[0].encoding?.[s]?.field)).size>1);if(i?.length>1&&(u||i.some(s=>["opacity","strokeWidth"].includes(s))||o)){const{representation:s,values:l,count:f,symbol:O,gradient:m,overflow:b,...y}=e,p=i.filter(a=>["opacity","strokeWidth"].includes(a)),x=i.filter(a=>!p.includes(a)),_=new Map;for(const a of x.filter(h=>h!=="size")){const h=L(a)?`channel:${a}`:`field:${r[0]?.encoding?.[a]?.field}`;_.has(h)||_.set(h,[]),_.get(h).push(a)}const $=[..._.values()];if(x.includes("size")){const a=$.find(h=>!h.some(L));a?a.push("size"):$.push(["size"])}return{steps:(k?[...$,...p.map(a=>[a])]:i.map(a=>[a])).flatMap(a=>P(t,a.includes("color")&&(s!==void 0||m!==void 0)?{...e,channels:a}:{...y,channels:a,...a.some(L)&&m!==void 0?{gradient:m}:{},...a.some(h=>!["size","opacity","strokeWidth"].includes(h)&&["ordinal","band","point"].includes(G(t,r[0]?.encoding?.[h]?.scale)?.type))&&b!==void 0?{overflow:b}:{},...a.some(h=>["size","opacity","strokeWidth"].includes(h))&&f!==void 0?{count:f}:{}},n).steps),finish:"always"}}if(i===void 0&&r.length===1&&R(r[0])&&se(r[0])&&!["color","stroke","shape","strokeDash","strokeWidth"].some(s=>r[0].encoding?.[s]?.scale!==void 0)){const s=r[0].id;return{steps:[de({...e,target:s},"size"),{op:"createOpacityLegend",args:{...e,target:s}}],finish:"auto"}}const M=[["size",R,["color","stroke","shape","strokeDash","opacity"]],["strokeWidth",Ge,e.target===void 0?["color","stroke","shape","strokeDash","size","opacity"]:["color","stroke","shape","strokeDash","opacity"]],["opacity",se,["color","stroke","shape","strokeDash","size"]]];for(const[s,l,f]of M){const O=i?.length===1&&i[0]===s,m=i===void 0&&r.filter(l).length===1&&!r.some(b=>f.some(y=>b.encoding?.[y]?.scale!==void 0));if(O||m)return{steps:[s==="opacity"?{op:"createOpacityLegend",args:e}:de(e,s)],finish:"auto"}}const C=r.filter(s=>{const l=["point","line","bar","rect","text","area","arc","tick"].includes(s.mark?.type)?s.encoding?.color:void 0,f=G(t,l?.scale);return U(f?.type)}),S=B(t,e.target,C);if(e.representation!==void 0&&i!==void 0&&i.length>1){if(!S||!i.includes("color"))throw new Error("Legend representation requires selected continuous color.");const{representation:s,values:l,count:f,symbol:O,gradient:m,...b}=e;return{steps:i.flatMap(p=>P(t,p==="color"?{...e,channels:[p]}:{...b,channels:[p]},n).steps),finish:"always"}}if(i?.length===1&&i[0]==="color"&&S||i===void 0&&S)return{steps:[{op:"createGradientLegend",args:e}],finish:"auto"};const v=r.filter(s=>{const l=["point","line","bar","rect","text","area","arc","tick"].includes(s.mark?.type)?s.encoding?.color:void 0,f=G(t,l?.scale);return["quantize","quantile","threshold"].includes(f?.type)});if(i?.length===1&&i[0]==="color"&&v.length>0||i===void 0&&v.length>0)return{steps:[{op:"createIntervalLegend",args:e}],finish:"auto"};const le=r.filter(s=>{const l=G(t,s.encoding?.stroke?.scale);return U(l?.type)}),A=B(t,e.target,le);if(i?.length===1&&i[0]==="stroke"&&A||i===void 0&&A&&!["color","shape","strokeDash","size","opacity","strokeWidth"].some(s=>A.encoding?.[s]?.scale!==void 0))return{steps:[{op:"createStrokeGradientLegend",args:{...e,channels:["stroke"]}}],finish:"auto"};const K=r.filter(s=>{const l=G(t,s.encoding?.stroke?.scale);return["quantize","quantile","threshold"].includes(l?.type)});if(i?.length===1&&i[0]==="stroke"&&K.length>0||i===void 0&&K.length===1)return{steps:[{op:"createStrokeIntervalLegend",args:{...e,channels:["stroke"]}}],finish:"auto"};const ce=i?.includes("shape")===!0,T=i?.includes("size")===!0,N=r.filter(s=>s.mark?.type==="point"&&(i===void 0?["color","shape"].some(l=>s.encoding?.[l]?.scale!==void 0):(ce||T)&&i.every(l=>["color","stroke","shape","size"].includes(l)&&s.encoding?.[l]?.scale!==void 0))),w=B(t,e.target,N),ae=i===void 0&&N.some(R);if((T||ae)&&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:s,...l}=e,f=w.encoding?.size?.scale!==void 0&&(i===void 0||T);if(s!==void 0&&!f)throw new Error("Legend count requires a selected size legend.");if(f&&l.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,...b}=l.labels??{};if(!f&&m!==void 0&&m!=="auto")throw new Error("Categorical legend labels do not accept format.");const y=[{op:"createCategoricalLegend",args:{...l,...l.labels===void 0?{}:{labels:b},target:w.id,channels:i?.filter(p=>p!=="size")??O}}];if(f&&y.push({op:"createSizeLegend",args:{target:w.id,...s===void 0?{}:{count:s},...l.overflow!==void 0&&G(t,w.encoding.size.scale)?.type==="ordinal"?{overflow:l.overflow}:{},...l.labels===void 0?{}:{labels:{...l.labels.offset===void 0?{}:{offset:l.labels.offset},...m===void 0?{}:{format:m}}},inheritAppearance:!0}}),F(t,y[0].args),f){const p=oe(t,y[1].args),x=t.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&&s!==void 0&&He(x).count!==s)throw new Error("Existing size legend count must be edited before recreating the categorical block.")}return{steps:y,combined:f,finish:f?"always":"auto"}}return{steps:[{op:"createCategoricalLegend",args:e}],finish:"auto"}}function Ae(t,e={},n=t.semanticSpec.layers,d){ye(e);const{blockLayout:r,at:o,alongOffset:c,...g}=e,z=t.guideConfigs.legendGroups??{};if(Object.keys(z).length>0&&g.position===void 0){const S=Q(t,e),v=z[S]?.placement;v?.position!==void 0&&(g.position=v.position,g.align=e.align??v.align,g.offset=e.offset??v.offset)}g.position===void 0&&e.direction==="horizontal"&&(e.at!==void 0||Object.keys(z).length>0)&&(g.position="top");const i=P(t,g,n,d);if(e.overflow?.summarySymbol!==void 0&&!i.steps.some(S=>S.op==="createCategoricalLegend"))throw new Error("Legend overflow.summarySymbol requires a categorical line-only sample recipe.");const j=Object.keys(t.guideConfigs.legendGroups??{}).length>0,k=ze.includes(e.position)&&(i.steps.length>1||i.steps[0]?.op!=="createCategoricalLegend"||Object.keys(t.guideConfigs.legend??{}).length>0);if(e.blockLayout===void 0&&e.at===void 0&&e.alongOffset===void 0&&!j&&!k)return i;const L=Q(t,e),u=i.steps[0],M=t.guideConfigs.legendGroups?.[L]!==void 0?30:u?.op==="createCategoricalLegend"?F(t,u.args).offset:u?.op==="createSizeLegend"?oe(t,u.args).offset:30,C=J(t,e,L,{creation:!0,corner:k,nativeOffset:M});return C.grouped?(C.options.position===void 0&&g.position!==void 0&&(C.options.position=g.position),{...P(C.program,C.options,n,d),legendGroup:{target:L,config:C.program.guideConfigs.legendGroups[L]}}):i}function Te(t,e){if(e.steps.length===0)return t;let n=e.legendGroup===void 0?t:t._withMaterializationConfig(["guides","legendGroups",e.legendGroup.target],e.legendGroup.config);for(const d of e.steps){const r=n.guideConfigs.legend?.size;e.combined&&r!==void 0&&d.op==="createCategoricalLegend"&&(n=n._withLegendConfig("size",{...r,inheritPosition:!0})),e.combined&&d.op==="createSizeLegend"&&r!==void 0||(n=d.op==="createColorSizeLegend"?fe(n,Y(n,d.args)):n[d.op](d.args))}return e.finish==="always"?n.rematerializeLegend():e.finish==="auto"?De(n):n}const zn=W({op:"createLegend",description:"Create an inferred legend for selected channels."},H(function(t={}){return We(be(this,Te(this,Ae(this,t)),t,{creation:!0}))})),kn=W({op:"removeCategoricalLegend",description:"Remove the active categorical legend and its concrete components."},H(function(t={}){Z(t,"removeCategoricalLegend");const e=["series","color","stroke"].filter(c=>this.guideConfigs.legend?.[c]!==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],d=`${n}Legend`,r=Object.keys(this.graphicSpec.objects).filter(c=>c.startsWith(d));let o=this.editSemantic({property:`guide.legend.${n}`,remove:!0});for(const c of r)o=o.editGraphics({target:c,remove:!0});return o._withoutMaterializationConfig(["guides","legend",n])}));export{Te as applyLegendCreationPlan,yn as createCategoricalLegend,zn as createLegend,bn as rematerializeLegend,kn as removeCategoricalLegend,F as resolveCategoricalLegendConfig,Ae as resolveLegendCreationPlan};
@@ -1,3 +1,4 @@
1
+ import { materializeHardTextLines } from "../../../../materialization/textLines.js";
1
2
  import { materializeLegendBorder } from "../../../../materialization/legendBorder.js";
2
3
  import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
3
4
  import { materializeLegendTitleLines } from "../../../../materialization/legendText.js";
@@ -23,7 +24,7 @@ export const rematerializeLegendLabels = /* @__PURE__ */ action(
23
24
  const layout = resolveLayout(this, config);
24
25
  const labels = categoricalLegendLabels(config);
25
26
  const visible = labels.flatMap((text, index) => text === "" ? [] : [index]);
26
- return editGraphicProperties(editGraphicProperties(this, id, {
27
+ return materializeHardTextLines(editGraphicProperties(editGraphicProperties(this, id, {
27
28
  length: visible.length,
28
29
  x: visible.map(index => layout.labelX[index]),
29
30
  y: visible.map(index => layout.itemY[index])
@@ -37,9 +38,9 @@ export const rematerializeLegendLabels = /* @__PURE__ */ action(
37
38
  fontSize: config.labels.fontSize,
38
39
  fontFamily: config.labels.fontFamily,
39
40
  fontWeight: config.labels.fontWeight,
40
- textAlign: "left",
41
+ textAlign: config.labels.align ?? "left",
41
42
  textBaseline: "middle"
42
- });
43
+ }), id);
43
44
  }
44
45
  );
45
46
 
@@ -1,3 +1,4 @@
1
+ import { validateLegendOverflow } from "../../../../grammar/legendOverflow.js";
1
2
  import{validateLegendClip,clippedSampleBounds}from"../../../../grammar/legendClip.js";
2
3
  import {categoricalLegendSymbolId as symbolGraphic}from"../../../../grammar/legendBlocks.js";
3
4
  export {symbolGraphic};
@@ -78,13 +79,14 @@ export function symbolWidth(config) {
78
79
 
79
80
  export function categoricalLegendView(config) {
80
81
  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
+ return { ...config, domain: config.domain.slice(0, config.overflow.maxItems + (config.overflow.summarySymbol === "first-hidden" ? 1 : 0)),
82
83
  hiddenCount: config.domain.length - config.overflow.maxItems };
83
84
  }
84
85
 
85
86
  export function categoricalLegendLabels(config) {
86
87
  config = categoricalLegendView(config);
87
- const labels = config.domain.map(value => resolveDisplayLabel(
88
+ const visible = config.hiddenCount > 0 ? config.domain.slice(0, config.overflow.maxItems) : config.domain;
89
+ const labels = visible.map(value => resolveDisplayLabel(
88
90
  value,
89
91
  [...(config.labelMap ?? []), ...(config.defaultLabelMap ?? [])],
90
92
  formatVisibleText
@@ -153,6 +155,7 @@ function resolveSampleBounds(program, config, width) {
153
155
  }
154
156
 
155
157
  export function resolveLayout(program, config) {
158
+ validateLegendOverflow(config.kind, config);
156
159
  validateLegendClip(config.kind,config);
157
160
  config = intrinsicLegendConfig(program, config);
158
161
  config = categoricalLegendView(config);
@@ -1,3 +1,6 @@
1
+ import { normalizeLegendOverflow, validateLegendOverflow } from "../../../../grammar/legendOverflow.js";
2
+ export { normalizeLegendOverflow } from "../../../../grammar/legendOverflow.js";
3
+ import { validateTextAlignment } from "../../../../core/validation.js";
1
4
  import{normalizeLegendClipHeight}from"../../../../grammar/legendClip.js";
2
5
  import {normalizeLegendSpacing,validateLegendSpacing} from "../../../../grammar/legendSpacing.js";
3
6
  import { normalizeLegendBorderDash } from "../../../../grammar/legendBorder.js";
@@ -44,21 +47,10 @@ const TEXT_OPTIONS = Object.freeze([
44
47
  "color",
45
48
  "fontSize",
46
49
  "fontFamily",
47
- "fontWeight",
50
+ "fontWeight", "align",
48
51
  "format"
49
52
  ]);
50
53
 
51
- export function normalizeLegendOverflow(value) {
52
- if (value === undefined || value === false) return value;
53
- validateObject(value, ["maxItems", "summary"], "Legend overflow");
54
- if (!Number.isInteger(value.maxItems) || value.maxItems < 1) {
55
- throw new RangeError("Legend overflow.maxItems must be a positive integer.");
56
- }
57
- if (value.summary !== undefined && value.summary !== "ellipsis-count") {
58
- throw new Error("Legend overflow.summary must be ellipsis-count.");
59
- }
60
- return { maxItems: value.maxItems, summary: "ellipsis-count" };
61
- }
62
54
 
63
55
  const BORDER_OPTIONS = Object.freeze([
64
56
  "color",
@@ -187,6 +179,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
187
179
  if (!["top", "left"].includes(titlePosition)) {
188
180
  throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
189
181
  }
182
+ validateTextAlignment(labels, "Legend labels");
190
183
  nonNegative(labels.offset, "Legend label offset");
191
184
  nonEmptyString(labels.color, "Legend label color");
192
185
  positive(labels.fontSize, "Legend label fontSize");
@@ -198,7 +191,9 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
198
191
  validateFontWeight(titleStyle.fontWeight, "Legend title fontWeight");
199
192
  positive(itemGap, "Legend itemGap");
200
193
 
201
- const overflow = normalizeLegendOverflow(args.overflow);
194
+ const overflow = normalizeLegendOverflow(args.overflow, { summarySymbol: true });
195
+ const symbol = normalizeRecipe(args.symbol, kind, { internal: internalSymbol });
196
+ validateLegendOverflow(kind, { overflow, symbol });
202
197
  return {
203
198
  ...(args.clipHeight===undefined?{}:{clipHeight:normalizeLegendClipHeight(args.clipHeight)}),
204
199
  ...normalizeLegendSpacing(args),
@@ -212,7 +207,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
212
207
  offset,
213
208
  titlePosition,
214
209
  title: args.title,
215
- symbol: normalizeRecipe(args.symbol, kind, { internal: internalSymbol }),
210
+ symbol,
216
211
  labels,
217
212
  titleStyle,
218
213
  itemGap,
@@ -120,6 +120,9 @@ export function resolveLegendSymbol(program, layer, channels, requested, kind) {
120
120
  }
121
121
  ] };
122
122
  }
123
+ if (layer.mark.type === "line" && channels?.length === 1 && channels[0] === "color") {
124
+ return { layers: [{ type: "line", length: 32, lineWidth: 2, ...strokeDetails }] };
125
+ }
123
126
  if (Object.keys(rectDetails).length === 0) return undefined;
124
127
  return kind === "series"
125
128
  ? { layers: [{ type: "line", length: 32, lineWidth: 2, ...strokeDetails }] }
@@ -49,12 +49,14 @@ export function resolveTarget(program, requested) {
49
49
 
50
50
  export const sameValues = sameOrderedValues;
51
51
 
52
- function resolveLegendKind(layer, requestedChannels) {
52
+ function resolveLegendKind(program, layer, requestedChannels) {
53
53
  if (sameValues(requestedChannels, ["stroke"])) return "stroke";
54
54
  if (["bar", "area", "arc", "rect", "text"].includes(layer.mark.type)) return "color";
55
55
  if (
56
- layer.mark.type === "point" &&
57
- sameValues(requestedChannels, ["color"])
56
+ sameValues(requestedChannels, ["color"]) &&
57
+ (layer.mark.type === "point" || program.guideConfigs.legend?.color?.target === layer.id ||
58
+ (program.guideConfigs.legend?.series?.target === layer.id &&
59
+ program.guideConfigs.legend.series.field !== layer.encoding.color?.field))
58
60
  ) {
59
61
  return "color";
60
62
  }
@@ -84,7 +86,7 @@ function resolveOrdinalScales(program, scaleIds) {
84
86
  return { scales, domain: scales[0].domain };
85
87
  }
86
88
 
87
- export function resolveDefinition(program, layer, requestedChannels, requestedTitle, order) {
89
+ export function resolveDefinition(program, layer, requestedChannels, requestedTitle, order, preferredKind) {
88
90
  const channels = requestedChannels ?? CHANNELS.filter(
89
91
  channel => layer.encoding?.[channel]?.scale !== undefined
90
92
  );
@@ -92,7 +94,7 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
92
94
  if (sizeShape && (layer.mark.type !== "point" || !["nominal", "ordinal"].includes(layer.encoding.size?.fieldType))) {
93
95
  throw new Error("Combined size-shape requires one categorical point owner.");
94
96
  }
95
- const kind = resolveLegendKind(layer, channels);
97
+ const kind = preferredKind ?? resolveLegendKind(program, layer, channels);
96
98
  if (
97
99
  !Array.isArray(channels) ||
98
100
  channels.length === 0 ||
@@ -4,7 +4,10 @@ import {findCoordinate} from '../../../selectors/coordinates.js';
4
4
  import {legendBlockChannels} from '../../../grammar/legendBlocks.js';
5
5
  import {mergeLegendTitleStyle} from '../../../grammar/legendTitleStyle.js';
6
6
  import {validateOptionObject} from '../../../core/validation.js';
7
- import {resolveColorSizeMapping,normalizeColorSizeSymbol,isColorSizeChannels} from '../../../grammar/colorSizeLegend.js';
7
+ import {resolveColorSizeMapping,normalizeColorSizeSymbol,isColorSizeChannels,validateColorSizeShapeSamples,colorSizeChannels} from '../../../grammar/colorSizeLegend.js';
8
+ import {formatDiscretizedIntervals} from '../../../grammar/scales/index.js';
9
+ import {createPointShapeGraphic} from '../../../grammar/pointShapes.js';
10
+ import {resolveConcreteGraphicBounds} from '../../../grammar/schemas/graphicBounds.js';
8
11
  import {mapSizeValues} from '../../../grammar/scales/size.js';
9
12
  import {mapScaleConsumerValues} from '../../../materialization/scales/map.js';
10
13
  import {intrinsicLegendConfig} from '../../../materialization/legendGroups.js';
@@ -17,10 +20,11 @@ import {normalizeLegendSampling,resolveLegendSampleValues,validateSamplingAgains
17
20
  import {resolveEffectiveLegendBlockConfig} from './blocks.js';
18
21
  import {applyLegendClipGraphics} from '../../../materialization/legendClip.js';
19
22
 
20
- export function resolveColorSizeOwner(program,target) {
21
- const candidates=(target===undefined?program.semanticSpec.layers:[findLayer(program,target)]).filter(layer=>layer?.mark?.type==='point'&&layer.encoding?.color&&layer.encoding?.size);
23
+ export function resolveColorSizeOwner(program,target,combine='color-size',channels) {
24
+ const candidates=(target===undefined?program.semanticSpec.layers:[findLayer(program,target)]).filter(layer=>layer?.mark?.type==='point'&&
25
+ (layer.encoding?.color||combine==='color-size-shape'&&layer.encoding?.stroke)&&layer.encoding?.size);
22
26
  const mapping=layer=>resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer.id],
23
- coordinate:findCoordinate(program,layer.coordinate)});
27
+ coordinate:findCoordinate(program,layer.coordinate),combine,channels});
24
28
  if(target!==undefined){if(candidates.length!==1)throw new Error('Color-size combination requires one point owner or an explicit target.');return mapping(candidates[0]);}
25
29
  const eligible=[];for(const layer of candidates){try{eligible.push(mapping(layer));}catch{/* Ineligible owners do not participate in inference. */}}
26
30
  if(eligible.length!==1){if(candidates.length===1)return mapping(candidates[0]);throw new Error('Color-size combination requires one eligible point owner or an explicit target.');}
@@ -29,14 +33,17 @@ export function resolveColorSizeOwner(program,target) {
29
33
 
30
34
  export function resolveColorSizeConfig(program,args={}) {
31
35
  validateOptionObject(args,[...ITEM_LEGEND_OPTIONS.filter(key=>key!=='overflow'),'channels','combine','symbol','blockLayout','at'],'Color-size legend');
32
- if(args.channels!==undefined&&!isColorSizeChannels(args.channels))throw new Error('Color-size combination requires exactly color and size channels.');
33
- const mapping=resolveColorSizeOwner(program,args.target);
36
+ const combine=args.combine??'color-size';
37
+ if(args.channels!==undefined&&!isColorSizeChannels(args.channels,combine))throw new Error('Combined legend requires exactly its color/stroke, size and optional shape channels.');
38
+ const mapping=resolveColorSizeOwner(program,args.target,combine,args.channels);
39
+ const sampling=normalizeLegendSampling(args,{label:'Color-size legend'});
40
+ if(combine==='color-size-shape')validateColorSizeShapeSamples(sampling,program.resolvedScales[mapping.colorScale]);
34
41
  return {...mapping,...normalizeItemLegendConfig(args,{field:mapping.field},40),
35
- sampling:normalizeLegendSampling(args,{label:'Color-size legend'}),symbol:normalizeColorSizeSymbol(args.symbol),combine:'color-size'};
42
+ sampling,symbol:normalizeColorSizeSymbol(args.symbol,undefined,{...mapping,combine}),combine};
36
43
  }
37
44
 
38
45
  export function refreshColorSizeConfig(program,stored) {
39
- const mapping=resolveColorSizeOwner(program,stored.target);
46
+ const mapping=resolveColorSizeOwner(program,stored.target,stored.combine,colorSizeChannels(stored));
40
47
  if(mapping.field!==stored.field)throw new Error('Color-size combination field changed; explicitly recreate the block.');
41
48
  return {...stored,...mapping,title:stored.inferredTitle?mapping.field:stored.title};
42
49
  }
@@ -45,6 +52,7 @@ export function resolveColorSizeLayout(program,stored) {
45
52
  validateLegendClip('colorSize',stored);
46
53
  const config=intrinsicLegendConfig(program,resolveEffectiveLegendBlockConfig(program,'colorSize',stored));
47
54
  const color=program.resolvedScales[stored.colorScale],size=program.resolvedScales[stored.sizeScale];
55
+ if(config.combine==='color-size-shape')return resolveShapeLayout(program,config,color,size);
48
56
  const sampling=readLegendSampling(config);validateSamplingAgainstScale(sampling,color,'Color-size samples');validateSamplingAgainstScale(sampling,size,'Color-size samples');
49
57
  const values=resolveLegendSampleValues(config,size,'Color-size legend');
50
58
  const colors=mapScaleConsumerValues(values,color,'color'),areas=mapSizeValues(values,size),radii=areas.map(area=>Math.sqrt(area/Math.PI));
@@ -58,16 +66,45 @@ export function resolveColorSizeLayout(program,stored) {
58
66
  return {...layout,background,values,colors,areas,radii,text:labels,config,symbolX:layout.symbolX.map(x=>x+width/2)};
59
67
  }
60
68
 
69
+ function resolveShapeLayout(program,config,color,size) {
70
+ const values=validateColorSizeShapeSamples(readLegendSampling(config),color);
71
+ const colors=mapScaleConsumerValues(values,color,config.colorChannel),areas=mapSizeValues(values,size);
72
+ const shape=program.resolvedScales[config.shapeScale];
73
+ const shapes=mapScaleConsumerValues(values,shape,'shape');
74
+ const point=program.markConfigs[config.target];
75
+ const symbol={fill:point.fill??'none',stroke:point.stroke,strokeWidth:point.strokeWidth??0,opacity:point.opacity??1,...config.symbol};
76
+ const sample=(i,x,y)=>createPointShapeGraphic({shape:shapes[i],area:areas[i],x,y,
77
+ fill:config.colorChannel==='color'?colors[i]:symbol.fill,
78
+ stroke:config.colorChannel==='stroke'?colors[i]:symbol.stroke,strokeWidth:symbol.strokeWidth,opacity:symbol.opacity});
79
+ const bounds=values.map((_,i)=>resolveConcreteGraphicBounds({objects:{sample:sample(i,0,0)},order:['sample']},'sample'));
80
+ const left=Math.min(...bounds.map(b=>b.left)),right=Math.max(...bounds.map(b=>b.right));
81
+ const top=Math.min(...bounds.map(b=>b.top)),bottom=Math.max(...bounds.map(b=>b.bottom));
82
+ const width=right-left,height=Math.min(bottom-top,config.clipHeight??Infinity);
83
+ const labels=formatDiscretizedIntervals(color.thresholds,config.labels.format)
84
+ .map((text,i)=>resolveDisplayLabel(values[i],config.labelMap,()=>text));
85
+ const {plot,canvas}=resolveContinuousBounds(program);
86
+ const layout=resolveLegendItemLayout(plot,config,labels,{width,height,itemBounds:bounds.map(b=>
87
+ clippedSampleBounds({...b,left:b.left-left,right:b.right-left},config.clipHeight))},canvas,program.materializationConfigs.textMetrics);
88
+ assertLegendBoundsInsideCanvas(layout.bounds,canvas,'Color-size-shape legend',config);
89
+ const background=resolveLegendBackgroundFromBounds(layout.bounds,config.border,canvas,'Color-size-shape legend',config);
90
+ return {...layout,background,values,colors,areas,text:labels,config,
91
+ items:values.map((_,i)=>sample(i,layout.symbolX[i]-left,layout.itemY[i]))};
92
+ }
93
+
61
94
  function writeSemantic(program,config) {
62
- return program.editSemantic({property:'guide.legend.colorSize.colorScale',value:config.colorScale})
95
+ let next=program.editSemantic({property:'guide.legend.colorSize.colorScale',value:config.colorScale})
63
96
  .editSemantic({property:'guide.legend.colorSize.sizeScale',value:config.sizeScale})
64
97
  .editSemantic({property:'guide.legend.colorSize.title',value:config.title})._withLegendConfig('colorSize',config);
98
+ if(config.shapeScale!==undefined)next=next.editSemantic({property:'guide.legend.colorSize.shapeScale',value:config.shapeScale})
99
+ .editSemantic({property:'guide.legend.colorSize.colorChannel',value:config.colorChannel});
100
+ return next;
65
101
  }
66
102
 
67
103
  export function materializeColorSizeLegend(program,stored) {
68
104
  const config=refreshColorSizeConfig(program,stored),layout=resolveColorSizeLayout(program,config),effective=layout.config;
69
105
  let next=writeSemantic(program,config);
70
- next=editGraphicProperties(next,'colorSizeLegendSymbols',{length:layout.values.length,x:layout.symbolX,y:layout.itemY,radius:layout.radii,
106
+ if(layout.items)next=next.editGraphics({target:'colorSizeLegendSymbols',property:'items',value:layout.items});
107
+ else next=editGraphicProperties(next,'colorSizeLegendSymbols',{length:layout.values.length,x:layout.symbolX,y:layout.itemY,radius:layout.radii,
71
108
  fill:layout.colors,stroke:effective.symbol.stroke??layout.colors,strokeWidth:effective.symbol.strokeWidth,opacity:effective.symbol.opacity});
72
109
  const visible=layout.text.flatMap((text,index)=>text===''?[]:[index]);
73
110
  next=editGraphicProperties(next,'colorSizeLegendLabels',{length:visible.length,x:visible.map(i=>layout.labelX[i]),y:visible.map(i=>layout.itemY[i]),text:visible.map(i=>layout.text[i])});
@@ -77,15 +114,15 @@ export function materializeColorSizeLegend(program,stored) {
77
114
  next=editGraphicProperties(next,'colorSizeLegendTitle',{x:layout.title.x,y:layout.title.y,text:effective.title});
78
115
  next=styleContinuousText(next,'colorSizeLegendTitle',effective.titleStyle,{align:layout.title.align});
79
116
  }
80
- return applyLegendClipGraphics(next);
117
+ return applyLegendClipGraphics(next,{anchors:{colorSize:layout.itemY}});
81
118
  }
82
119
 
83
120
  export function createColorSizeLegendFromConfig(program,config) {
84
- if(Object.entries(program.guideConfigs.legend??{}).some(([kind,stored])=>stored.target===config.target&&legendBlockChannels(kind,stored).some(channel=>['color','size'].includes(channel))))throw new Error('Color-size content already has a legend; edit its combination explicitly.');
121
+ if(Object.entries(program.guideConfigs.legend??{}).some(([kind,stored])=>stored.target===config.target&&legendBlockChannels(kind,stored).some(channel=>colorSizeChannels(config).includes(channel))))throw new Error('Color-size content already has a legend; edit its combination explicitly.');
85
122
  const layout=resolveColorSizeLayout(program,config),placement=resolveLegendGraphicPlacement(program);
86
123
  let next=writeSemantic(program,config);
87
124
  if(config.border!==false)next=next.createGraphics({id:'colorSizeLegendBackground',type:'rect',...placement});
88
- next=next.createGraphics({id:'colorSizeLegendSymbols',type:'circle',length:layout.values.length,...placement})
125
+ next=next.createGraphics({id:'colorSizeLegendSymbols',...(config.shapeScale===undefined?{type:'circle',length:layout.values.length}:{type:'collection'}),...placement})
89
126
  .createGraphics({id:'colorSizeLegendLabels',type:'text',length:layout.values.length,...placement});
90
127
  if(config.titleVisible!==false)next=next.createGraphics({id:'colorSizeLegendTitle',type:'text',...placement});
91
128
  return materializeColorSizeLegend(next,config);
@@ -105,7 +142,7 @@ export function resolveColorSizeEdit(program,previous,args) {
105
142
  const config={...previous,...normalized,title,
106
143
  inferredTitle:args.title==='auto'?true:typeof args.title==='string'?false:previous.inferredTitle,
107
144
  titleVisible:args.title===false?false:args.title===undefined?previous.titleVisible:true,
108
- symbol:normalizeColorSizeSymbol(args.symbol,previous.symbol),
145
+ symbol:normalizeColorSizeSymbol(args.symbol,previous.symbol,previous),
109
146
  sampling:normalizeLegendSampling(args,{previous,operation:'edit',label:'Color-size legend'})};
110
147
  resolveColorSizeLayout(program,config);
111
148
  return config;
@@ -1 +1 @@
1
- import{normalizeLegendClipHeight as O}from"../../../../grammar/legendClip.js";import{normalizeLegendSpacing as $}from"../../../../grammar/legendSpacing.js";import{applyLegendClipGraphics as v}from"../../../../materialization/legendClip.js";import{normalizeGradientDirection as R}from"../../../../grammar/legendGradient.js";import{normalizeLegendBorderDash as q}from"../../../../grammar/legendBorder.js";import{materializeLegendBorder as M}from"../../../../materialization/legendBorder.js";import{LEGEND_TITLE_STYLE_KEYS as V,mergeLegendTitleStyle as Y,validateLegendTitleAlignment as I}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as K,isPlotLegend as m}from"../../../../layout/legend.js";import{isContinuousColorScaleType as X}from"../../../../grammar/scales/types.js";import{isPlainObject as G}from"../../../../core/immutable.js";import{resolveTextBounds as J}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as Q}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as Z}from"../../../../materialization/legendText.js";import{validateKeys as z,validateFontWeight as ee,validateOptionObject as C,validateNonEmptyString as y,validateNonNegativeFinite as te,validatePositiveFinite as ie,validateGeneratedItemLimit as ne}from"../../../../core/validation.js";import{formatTimeTick as oe}from"../../../../grammar/ticks.js";import{formatValue as re,validateValueFormat as P}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as le,sampleNumericRange as de}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as ae,canvasOverflowError as _}from"../../../../layout/canvas.js";import{isHorizontalEdgeLegend as N}from"../../../../layout/legendLane.js";import{DEFAULT_COLORS as h,DEFAULT_FONT_FAMILY as F,DEFAULT_VERTICAL_LEGEND_ITEM_GAP as w}from"../../../../theme/defaults.js";import{findLayer as se}from"../../../../selectors/layers.js";import{isOpacityLegendLayer as ce}from"../../../../materialization/legends.js";import{findCanvasGraphic as fe}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as pe}from"../sampling.js";import{editGraphicProperties as p}from"../../../primitives/graphicProperties.js";const ue=["clipHeight","target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","rowPadding","titlePadding","border","direction","columns","titlePosition"],Me=Object.freeze(["clipHeight","target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","rowPadding","titlePadding","title","labels","titleStyle","border","overflow"]),me=["offset","color","fontSize","fontFamily","fontWeight","format"],ge=["color","lineWidth","padding","background","strokeDash"],D=["right","left","top","bottom",...K],x={offset:12,color:h.text,fontSize:12,fontFamily:F,fontWeight:"normal"},B={color:h.text,fontSize:13,fontFamily:F,fontWeight:600},he=Object.freeze(x),Le=Object.freeze({...B,color:h.strongText});function A(e,t){return e!==void 0&&e!==!1&&y(e,"Legend title"),{title:e===!1||e===void 0?t:e,inferredTitle:e===!1||e===void 0,titleVisible:e!==!1}}function Ve(e,t,i){const n=["right","left"].includes(e.position??"right")||m(e),r=(e.direction??(n?"vertical":"horizontal"))==="vertical"?w:i;return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},...we({...e,itemGap:e.itemGap??r}),...A(e.title,t.field),labels:T(e.labels,"createLegend.labels",he),titleStyle:j(e.titleStyle,"createLegend.titleStyle",Le),border:k(e.border)}}const be={color:h.border,lineWidth:1,padding:12,background:"transparent"},E=ie,g=te;function T(e,t,i){if(e===void 0)return{...i};if(!G(e))throw new TypeError(`${t} must be a plain object.`);z(e,me,t);const n={...i,...e};Object.hasOwn(n,"offset")&&g(n.offset,`${t} offset`),E(n.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])y(n[o],`${t} ${o}`);return ee(n.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(n,"format")&&(n.format=P(n.format,`${t} format`)),n}function j(e,t,i,n=!1){e!==void 0&&C(e,V,t);const o=Y(i,e,{edit:n}),{align:r,...d}=o;return{...T(d,t,{}),...r===void 0?{}:{align:r}}}function k(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!G(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&z(e,ge,"createLegend.border");const t={...be,...e===!0?{}:e};for(const n of["color","background"])y(t[n],`Legend border ${n}`);g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding");const i=q(t.strokeDash);return{...t,...i===void 0?{}:{strokeDash:i}}}function Ye(e,t){C(e,ue,"createLegend");const i=e.position??"right";if(!D.includes(i))throw new Error(`Unsupported legend position "${i}".`);const n=e.align??"center";if(!["left","center","right"].includes(n))throw new Error(`Unsupported legend alignment "${n}".`);if(["left","right"].includes(i)&&n!=="center")throw new Error("Side continuous legends require center alignment.");const o=t==="opacity"||Object.hasOwn(e,"values")?pe(e,{operation:"create",label:t==="gradient"?"Gradient legend":"Opacity legend"}):void 0,r=t==="gradient"?o?.count??e.count??5:void 0;if(t==="gradient"){if(!Number.isInteger(r)||r<2)throw new RangeError("Continuous legend count must be an integer of at least 2.");ne(r,"Continuous legend count")}const d=e.offset??(m({position:i})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(I(e.titleStyle,l),!(t==="gradient"?["top","left","bottom"]:["top","left"]).includes(l))throw new Error(`Unsupported legend titlePosition "${l}".`);if(l==="left"&&(!["top","bottom"].includes(i)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const s=e.itemGap??(t==="opacity"&&(["right","left"].includes(i)||m({position:i}))?w:l==="left"?20:28);if(E(s,"Legend itemGap"),t==="gradient"){for(const a of["symbol","columns","itemGap"])if(Object.hasOwn(e,a))throw new Error(`Gradient legend does not accept ${a}.`)}else for(const a of["columns","direction","gradient"])if(Object.hasOwn(e,a))throw new Error(`Opacity legend does not accept ${a}.`);return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},target:e.target,...$(e),position:i,align:n,offset:d,...t==="gradient"?{count:r,...o?.mode==="values"?{sampling:o}:{},...e.direction===void 0?{}:{gradientDirection:R(e.direction)}}:{sampling:o},...A(e.title),labels:T(e.labels,"createLegend.labels",l==="left"?{...x,offset:8}:x),titleStyle:j(e.titleStyle,"createLegend.titleStyle",B),itemGap:s,titlePosition:l,border:k(e.border)}}function U(e,t,i){const n=e.semanticSpec.layers.filter(i);if(t===void 0)return n.length===1?n[0]:void 0;const o=se(e,t);return n.includes(o)?o:void 0}function Ke(e,t,i){const n=U(e,t,i==="opacity"?ce:o=>o.mark?.type==="point"&&o.encoding?.[i]?.scale!==void 0);if(n===void 0)throw new Error(t===void 0?`${i} legend requires one eligible ${i==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${i} legend target "${t}".`);return n}function Xe(e,t,i="color"){const n=U(e,t,o=>(i==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","line","bar","rect","text","area","arc","tick"]).includes(o.mark?.type)&&o.encoding?.[i]?.scale!==void 0&&(i!=="color"||["quantitative","temporal"].includes(o.encoding[i].fieldType)));if(n===void 0)throw new Error(t===void 0?i==="color"?"color legend requires one eligible point, line, bar, rect, text, area, arc, or tick mark.":"stroke legend requires one eligible mark.":`Unknown ${i} legend target "${t}".`);return n}function Je(e,t,i){const n=e.resolvedScales[t];if(i==="sequential"?!X(n?.type):n?.type!==i)throw new Error(`Legend requires resolved ${i} scale "${t}".`);return n}function Qe(e,t="Continuous legend layout requires Canvas bounds."){const i=ae(e),n=fe(e);if(i===void 0||![i.x,i.y,i.width,i.height].every(Number.isFinite)||n?.type!=="canvas"||!Number.isFinite(n.properties.width)||!Number.isFinite(n.properties.height))throw new Error(t);return{plot:i,canvas:n.properties}}function Ze(e,t){return de(e[0],e[1],t,"Continuous legend domain")}function et(e,t,i,n="auto"){const o=P(n,"Legend label format");return o==="auto"?i==="temporal"?e.map(r=>oe(r,t)):le(e):e.map(r=>re(r,{format:o,valueType:i==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function W(e,t,i,{align:n="left"}={}){const o=p(e,t,{fill:i.color,fontSize:i.fontSize,fontFamily:i.fontFamily,fontWeight:i.fontWeight,textAlign:n,textBaseline:"middle"});return t.endsWith("Title")?Z(o,t,o.graphicSpec.objects[t].properties.text,i):o}function tt(e,t,i,n,{hardBreaks:o=!1}={}){return J({x:e.x,y:e.y,text:String(t),...i,...o?{lines:Q(t,i,n).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},n)}function it(e,t,i,n){if(!(n?.groupIntrinsic||N(n)||["left","right"].includes(n?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw _(`${i} requires more Canvas margin space.`,e,t)}function nt(e,t,i,n,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,s=-1/0,a=-1/0;for(const f of e)d=Math.min(d,f.left),l=Math.min(l,f.top),s=Math.max(s,f.right),a=Math.max(a,f.bottom);if(d-=t.padding,l-=t.padding,s+=t.padding,a+=t.padding,!o?.groupIntrinsic&&!N(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||s+r>i.width||a+r>i.height))throw _(`${n} background requires more Canvas margin space.`,[{left:d-r,right:s+r,top:l-r,bottom:a+r}],i);return{x:d,y:l,width:s-d,height:a-l}}function ye(e,t,i,n){return i===void 0?e:M(e,t,i,n)}function we(e,{multiColumnSide:t=!1}={}){const i=e.position??"right";if(!D.includes(i))throw new Error(`Unsupported legend position "${i}".`);const n=["left","right"].includes(i)||m({position:i}),o=e.layout===void 0?"edge":e.layout;if(o!=="edge")throw new Error('This legend requires layout "edge".');const r=e.align??"center";if(!["left","center","right"].includes(r))throw new Error(`Unsupported legend alignment "${r}".`);const d=e.direction??(n?"vertical":"horizontal");if(!["horizontal","vertical"].includes(d))throw new Error(`Unsupported legend direction "${d}".`);const l=e.columns;if(l!==void 0&&(!Number.isInteger(l)||l<1))throw new RangeError("Legend columns must be a positive integer.");const s=e.titlePosition??"top";if(I(e.titleStyle,s),!["top","left"].includes(s))throw new Error(`Unsupported legend titlePosition "${s}".`);if(n&&(r!=="center"||s!=="top"||!t&&(d!=="vertical"||l!==void 0&&l!==1)))throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");const a=e.offset??(m({position:i})?8:30),f=e.itemGap??(d==="vertical"?w:28);return g(a,"Legend offset"),E(f,"Legend itemGap"),{position:i,layout:o,align:r,direction:d,columns:l,titlePosition:s,offset:a,itemGap:f,...$(e)}}function ot(e,t,i,n,{symbols:o,optional:r={},text:d=n.labels,labels:l=n.config.labels,titleStyle:s=n.config.titleStyle}){const a=`${t}Legend`,f=Object.values(o).find(Array.isArray)?.length??d.length;let c=e.editSemantic({property:`guide.legend.${t}.scale`,value:i.scale}).editSemantic({property:`guide.legend.${t}.title`,value:i.title})._withLegendConfig(t,i);c=p(c,`${a}Symbols`,{length:f,...o});const L=n.labelItemIndices,b=u=>L===void 0||!Array.isArray(u)?u:L.map(H=>u[H]);c=p(c,`${a}Labels`,{length:L?.length??d.length,x:b(n.labelX),y:b(n.itemY),text:b(d)}),c=ye(c,`${a}Background`,n.background,i.border),c=p(c,`${a}Symbols`,Object.fromEntries(Object.entries(r).filter(([,u])=>u!==void 0))),c=W(c,`${a}Labels`,l);const S=n.config;return S.titleVisible===!1?v(c):(c=p(c,`${a}Title`,{x:n.title.x,y:n.title.y,text:S.title}),v(W(c,`${a}Title`,s,{align:n.title.align})))}function rt(e,t,i,n,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:i.scale}).editSemantic({property:`guide.legend.${t}.title`,value:i.title})._withLegendConfig(t,i);return i.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:n,...r}).createGraphics({id:`${d}Labels`,type:"text",length:n,...r}),i.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{he as ITEM_LEGEND_LABELS,Me as ITEM_LEGEND_OPTIONS,Le as ITEM_LEGEND_TITLE_STYLE,it as assertLegendBoundsInsideCanvas,rt as createItemLegendGraphics,p as editGraphicProperties,ye as editLegendBackground,et as formatContinuousValues,ot as materializeItemLegend,Ye as normalizeContinuousLegend,A as normalizeInitialLegendTitle,Ve as normalizeItemLegendConfig,we as normalizeItemLegendLayout,k as normalizeLegendBorder,T as normalizeLegendTextOptions,j as normalizeLegendTitleOptions,Je as requireResolvedLegendScale,Qe as resolveContinuousBounds,Xe as resolveContinuousColorLayer,Ke as resolveContinuousLegendLayer,nt as resolveLegendBackgroundFromBounds,tt as resolveLegendTextBounds,Ze as sampleContinuousValues,U as selectLegendLayer,W as styleContinuousText,g as validateNonNegative,E as validatePositive};
1
+ import{validateTextAlignment as R}from"../../../../core/validation.js";import{materializeHardTextLines as q}from"../../../../materialization/textLines.js";import{normalizeLegendClipHeight as O}from"../../../../grammar/legendClip.js";import{normalizeLegendSpacing as $}from"../../../../grammar/legendSpacing.js";import{applyLegendClipGraphics as v}from"../../../../materialization/legendClip.js";import{normalizeGradientDirection as M}from"../../../../grammar/legendGradient.js";import{normalizeLegendBorderDash as V}from"../../../../grammar/legendBorder.js";import{materializeLegendBorder as Y}from"../../../../materialization/legendBorder.js";import{LEGEND_TITLE_STYLE_KEYS as K,mergeLegendTitleStyle as X,validateLegendTitleAlignment as I}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as J,isPlotLegend as m}from"../../../../layout/legend.js";import{isContinuousColorScaleType as Q}from"../../../../grammar/scales/types.js";import{isPlainObject as G}from"../../../../core/immutable.js";import{resolveTextBounds as Z}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as ee}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as te}from"../../../../materialization/legendText.js";import{validateKeys as z,validateFontWeight as ie,validateOptionObject as C,validateNonEmptyString as y,validateNonNegativeFinite as ne,validatePositiveFinite as oe,validateGeneratedItemLimit as re}from"../../../../core/validation.js";import{formatTimeTick as le}from"../../../../grammar/ticks.js";import{formatValue as de,validateValueFormat as P}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as ae,sampleNumericRange as se}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as ce,canvasOverflowError as _}from"../../../../layout/canvas.js";import{isHorizontalEdgeLegend as N}from"../../../../layout/legendLane.js";import{DEFAULT_COLORS as h,DEFAULT_FONT_FAMILY as F,DEFAULT_VERTICAL_LEGEND_ITEM_GAP as w}from"../../../../theme/defaults.js";import{findLayer as fe}from"../../../../selectors/layers.js";import{isOpacityLegendLayer as pe}from"../../../../materialization/legends.js";import{findCanvasGraphic as ue}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as me}from"../sampling.js";import{editGraphicProperties as p}from"../../../primitives/graphicProperties.js";const ge=["clipHeight","target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","rowPadding","titlePadding","border","direction","columns","titlePosition"],Xe=Object.freeze(["clipHeight","target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","rowPadding","titlePadding","title","labels","titleStyle","border","overflow"]),he=["offset","color","fontSize","fontFamily","fontWeight","align","format"],Le=["color","lineWidth","padding","background","strokeDash"],D=["right","left","top","bottom",...J],x={offset:12,color:h.text,fontSize:12,fontFamily:F,fontWeight:"normal"},A={color:h.text,fontSize:13,fontFamily:F,fontWeight:600},be=Object.freeze(x),ye=Object.freeze({...A,color:h.strongText});function B(e,t){return e!==void 0&&e!==!1&&y(e,"Legend title"),{title:e===!1||e===void 0?t:e,inferredTitle:e===!1||e===void 0,titleVisible:e!==!1}}function Je(e,t,i){const n=["right","left"].includes(e.position??"right")||m(e),r=(e.direction??(n?"vertical":"horizontal"))==="vertical"?w:i;return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},...Ee({...e,itemGap:e.itemGap??r}),...B(e.title,t.field),labels:T(e.labels,"createLegend.labels",be),titleStyle:j(e.titleStyle,"createLegend.titleStyle",ye),border:k(e.border)}}const we={color:h.border,lineWidth:1,padding:12,background:"transparent"},E=oe,g=ne;function T(e,t,i){if(e===void 0)return{...i};if(!G(e))throw new TypeError(`${t} must be a plain object.`);z(e,he,t);const n={...i,...e};R(n,t),Object.hasOwn(n,"offset")&&g(n.offset,`${t} offset`),E(n.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])y(n[o],`${t} ${o}`);return ie(n.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(n,"format")&&(n.format=P(n.format,`${t} format`)),n}function j(e,t,i,n=!1){e!==void 0&&C(e,K,t);const o=X(i,e,{edit:n}),{align:r,...d}=o;return{...T(d,t,{}),...r===void 0?{}:{align:r}}}function k(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!G(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&z(e,Le,"createLegend.border");const t={...we,...e===!0?{}:e};for(const n of["color","background"])y(t[n],`Legend border ${n}`);g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding");const i=V(t.strokeDash);return{...t,...i===void 0?{}:{strokeDash:i}}}function Qe(e,t){C(e,ge,"createLegend");const i=e.position??"right";if(!D.includes(i))throw new Error(`Unsupported legend position "${i}".`);const n=e.align??"center";if(!["left","center","right"].includes(n))throw new Error(`Unsupported legend alignment "${n}".`);if(["left","right"].includes(i)&&n!=="center")throw new Error("Side continuous legends require center alignment.");const o=t==="opacity"||Object.hasOwn(e,"values")?me(e,{operation:"create",label:t==="gradient"?"Gradient legend":"Opacity legend"}):void 0,r=t==="gradient"?o?.count??e.count??5:void 0;if(t==="gradient"){if(!Number.isInteger(r)||r<2)throw new RangeError("Continuous legend count must be an integer of at least 2.");re(r,"Continuous legend count")}const d=e.offset??(m({position:i})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(I(e.titleStyle,l),!(t==="gradient"?["top","left","bottom"]:["top","left"]).includes(l))throw new Error(`Unsupported legend titlePosition "${l}".`);if(l==="left"&&(!["top","bottom"].includes(i)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const s=e.itemGap??(t==="opacity"&&(["right","left"].includes(i)||m({position:i}))?w:l==="left"?20:28);if(E(s,"Legend itemGap"),t==="gradient"){for(const a of["symbol","columns","itemGap"])if(Object.hasOwn(e,a))throw new Error(`Gradient legend does not accept ${a}.`)}else for(const a of["columns","direction","gradient"])if(Object.hasOwn(e,a))throw new Error(`Opacity legend does not accept ${a}.`);return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},target:e.target,...$(e),position:i,align:n,offset:d,...t==="gradient"?{count:r,...o?.mode==="values"?{sampling:o}:{},...e.direction===void 0?{}:{gradientDirection:M(e.direction)}}:{sampling:o},...B(e.title),labels:T(e.labels,"createLegend.labels",l==="left"?{...x,offset:8}:x),titleStyle:j(e.titleStyle,"createLegend.titleStyle",A),itemGap:s,titlePosition:l,border:k(e.border)}}function U(e,t,i){const n=e.semanticSpec.layers.filter(i);if(t===void 0)return n.length===1?n[0]:void 0;const o=fe(e,t);return n.includes(o)?o:void 0}function Ze(e,t,i){const n=U(e,t,i==="opacity"?pe:o=>o.mark?.type==="point"&&o.encoding?.[i]?.scale!==void 0);if(n===void 0)throw new Error(t===void 0?`${i} legend requires one eligible ${i==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${i} legend target "${t}".`);return n}function et(e,t,i="color"){const n=U(e,t,o=>(i==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","line","bar","rect","text","area","arc","tick"]).includes(o.mark?.type)&&o.encoding?.[i]?.scale!==void 0&&(i!=="color"||["quantitative","temporal"].includes(o.encoding[i].fieldType)));if(n===void 0)throw new Error(t===void 0?i==="color"?"color legend requires one eligible point, line, bar, rect, text, area, arc, or tick mark.":"stroke legend requires one eligible mark.":`Unknown ${i} legend target "${t}".`);return n}function tt(e,t,i){const n=e.resolvedScales[t];if(i==="sequential"?!Q(n?.type):n?.type!==i)throw new Error(`Legend requires resolved ${i} scale "${t}".`);return n}function it(e,t="Continuous legend layout requires Canvas bounds."){const i=ce(e),n=ue(e);if(i===void 0||![i.x,i.y,i.width,i.height].every(Number.isFinite)||n?.type!=="canvas"||!Number.isFinite(n.properties.width)||!Number.isFinite(n.properties.height))throw new Error(t);return{plot:i,canvas:n.properties}}function nt(e,t){return se(e[0],e[1],t,"Continuous legend domain")}function ot(e,t,i,n="auto"){const o=P(n,"Legend label format");return o==="auto"?i==="temporal"?e.map(r=>le(r,t)):ae(e):e.map(r=>de(r,{format:o,valueType:i==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function W(e,t,i,{align:n="left"}={}){const o=p(e,t,{fill:i.color,fontSize:i.fontSize,fontFamily:i.fontFamily,fontWeight:i.fontWeight,textAlign:t.endsWith("Labels")?i.align??n:n,textBaseline:"middle"});return t.endsWith("Title")?te(o,t,o.graphicSpec.objects[t].properties.text,i):q(o,t)}function rt(e,t,i,n,{hardBreaks:o=!0}={}){return Z({x:e.x,y:e.y,text:String(t),...i,...o?{lines:ee(t,i,n).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},n)}function lt(e,t,i,n){if(!(n?.groupIntrinsic||N(n)||["left","right"].includes(n?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw _(`${i} requires more Canvas margin space.`,e,t)}function dt(e,t,i,n,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,s=-1/0,a=-1/0;for(const f of e)d=Math.min(d,f.left),l=Math.min(l,f.top),s=Math.max(s,f.right),a=Math.max(a,f.bottom);if(d-=t.padding,l-=t.padding,s+=t.padding,a+=t.padding,!o?.groupIntrinsic&&!N(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||s+r>i.width||a+r>i.height))throw _(`${n} background requires more Canvas margin space.`,[{left:d-r,right:s+r,top:l-r,bottom:a+r}],i);return{x:d,y:l,width:s-d,height:a-l}}function xe(e,t,i,n){return i===void 0?e:Y(e,t,i,n)}function Ee(e,{multiColumnSide:t=!1}={}){const i=e.position??"right";if(!D.includes(i))throw new Error(`Unsupported legend position "${i}".`);const n=["left","right"].includes(i)||m({position:i}),o=e.layout===void 0?"edge":e.layout;if(o!=="edge")throw new Error('This legend requires layout "edge".');const r=e.align??"center";if(!["left","center","right"].includes(r))throw new Error(`Unsupported legend alignment "${r}".`);const d=e.direction??(n?"vertical":"horizontal");if(!["horizontal","vertical"].includes(d))throw new Error(`Unsupported legend direction "${d}".`);const l=e.columns;if(l!==void 0&&(!Number.isInteger(l)||l<1))throw new RangeError("Legend columns must be a positive integer.");const s=e.titlePosition??"top";if(I(e.titleStyle,s),!["top","left"].includes(s))throw new Error(`Unsupported legend titlePosition "${s}".`);if(n&&(r!=="center"||s!=="top"||!t&&(d!=="vertical"||l!==void 0&&l!==1)))throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");const a=e.offset??(m({position:i})?8:30),f=e.itemGap??(d==="vertical"?w:28);return g(a,"Legend offset"),E(f,"Legend itemGap"),{position:i,layout:o,align:r,direction:d,columns:l,titlePosition:s,offset:a,itemGap:f,...$(e)}}function at(e,t,i,n,{symbols:o,optional:r={},text:d=n.labels,labels:l=n.config.labels,titleStyle:s=n.config.titleStyle}){const a=`${t}Legend`,f=Object.values(o).find(Array.isArray)?.length??d.length;let c=e.editSemantic({property:`guide.legend.${t}.scale`,value:i.scale}).editSemantic({property:`guide.legend.${t}.title`,value:i.title})._withLegendConfig(t,i);c=p(c,`${a}Symbols`,{length:f,...o});const L=n.labelItemIndices,b=u=>L===void 0||!Array.isArray(u)?u:L.map(H=>u[H]);c=p(c,`${a}Labels`,{length:L?.length??d.length,x:b(n.labelX),y:b(n.itemY),text:b(d)}),c=xe(c,`${a}Background`,n.background,i.border),c=p(c,`${a}Symbols`,Object.fromEntries(Object.entries(r).filter(([,u])=>u!==void 0))),c=W(c,`${a}Labels`,l);const S=n.config;return S.titleVisible===!1?v(c):(c=p(c,`${a}Title`,{x:n.title.x,y:n.title.y,text:S.title}),v(W(c,`${a}Title`,s,{align:n.title.align})))}function st(e,t,i,n,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:i.scale}).editSemantic({property:`guide.legend.${t}.title`,value:i.title})._withLegendConfig(t,i);return i.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:n,...r}).createGraphics({id:`${d}Labels`,type:"text",length:n,...r}),i.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{be as ITEM_LEGEND_LABELS,Xe as ITEM_LEGEND_OPTIONS,ye as ITEM_LEGEND_TITLE_STYLE,lt as assertLegendBoundsInsideCanvas,st as createItemLegendGraphics,p as editGraphicProperties,xe as editLegendBackground,ot as formatContinuousValues,at as materializeItemLegend,Qe as normalizeContinuousLegend,B as normalizeInitialLegendTitle,Je as normalizeItemLegendConfig,Ee as normalizeItemLegendLayout,k as normalizeLegendBorder,T as normalizeLegendTextOptions,j as normalizeLegendTitleOptions,tt as requireResolvedLegendScale,it as resolveContinuousBounds,et as resolveContinuousColorLayer,Ze as resolveContinuousLegendLayer,dt as resolveLegendBackgroundFromBounds,rt as resolveLegendTextBounds,nt as sampleContinuousValues,U as selectLegendLayer,W as styleContinuousText,g as validateNonNegative,E as validatePositive};