ggaction 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +1 -1
  7. package/src/actions/canvas/fitting.js +6 -2
  8. package/src/actions/charts/heatmap.js +4 -3
  9. package/src/actions/data/derived.js +11 -1
  10. package/src/actions/data/edit.js +2 -0
  11. package/src/actions/data/index.js +4 -0
  12. package/src/actions/data/regression.js +24 -16
  13. package/src/actions/data/regressionStatistics.js +73 -0
  14. package/src/actions/data/shared.js +5 -2
  15. package/src/actions/distributions/revision.js +24 -18
  16. package/src/actions/encodings/remove.js +11 -6
  17. package/src/actions/encodings/shared.js +26 -24
  18. package/src/actions/facets/actions.js +18 -7
  19. package/src/actions/facets/guides.js +2 -1
  20. package/src/actions/guides/axes/axes.js +30 -11
  21. package/src/actions/guides/axes/axis.js +58 -8
  22. package/src/actions/guides/axes/components.js +13 -13
  23. package/src/actions/guides/axes/edit.js +36 -24
  24. package/src/actions/guides/axes/labels.js +37 -27
  25. package/src/actions/guides/axes/lines.js +22 -20
  26. package/src/actions/guides/axes/remove.js +10 -6
  27. package/src/actions/guides/axes/tickGroups.js +8 -7
  28. package/src/actions/guides/axes/ticks.js +31 -22
  29. package/src/actions/guides/axes/titles.js +53 -60
  30. package/src/actions/guides/facadeAxes.js +33 -24
  31. package/src/actions/guides/facadeLegend.js +3 -3
  32. package/src/actions/guides/grids/grid.js +1 -1
  33. package/src/actions/guides/grids/resolve.js +48 -51
  34. package/src/actions/guides/legends/blocks.js +43 -19
  35. package/src/actions/guides/legends/categorical/actions.js +30 -7
  36. package/src/actions/guides/legends/categorical/components.js +3 -10
  37. package/src/actions/guides/legends/categorical/index.js +2 -0
  38. package/src/actions/guides/legends/categorical/layout.js +9 -13
  39. package/src/actions/guides/legends/categorical/options.js +8 -2
  40. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  41. package/src/actions/guides/legends/categorical/resolve.js +9 -2
  42. package/src/actions/guides/legends/categorical/symbols.js +22 -8
  43. package/src/actions/guides/legends/colorSize.js +110 -0
  44. package/src/actions/guides/legends/combine.js +37 -0
  45. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
  46. package/src/actions/guides/legends/continuous/common.js +1 -1
  47. package/src/actions/guides/legends/continuous/gradient.js +27 -13
  48. package/src/actions/guides/legends/continuous/opacity.js +16 -11
  49. package/src/actions/guides/legends/creation.js +2 -0
  50. package/src/actions/guides/legends/edit.js +1 -1
  51. package/src/actions/guides/legends/focused.js +6 -5
  52. package/src/actions/guides/legends/lane.js +1 -1
  53. package/src/actions/guides/legends/lifecycle.js +2 -1
  54. package/src/actions/guides/legends/remove.js +6 -3
  55. package/src/actions/guides/legends/size.js +16 -6
  56. package/src/actions/guides/legends/strokeWidth.js +2 -0
  57. package/src/actions/guides/legends/target.js +2 -79
  58. package/src/actions/guides/legends/transition.js +24 -5
  59. package/src/actions/guides/tickValues.js +8 -5
  60. package/src/actions/marks/bar/edit.js +23 -12
  61. package/src/actions/marks/bar/materialize.js +6 -1
  62. package/src/actions/marks/point/materialize.js +5 -1
  63. package/src/actions/marks/remove.js +10 -10
  64. package/src/actions/primitives/createGraphics.js +3 -2
  65. package/src/actions/primitives/editGraphics.js +15 -11
  66. package/src/actions/primitives/graphicProperties.js +1 -6
  67. package/src/actions/primitives/semanticAction.js +27 -1
  68. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  69. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  70. package/src/actions/regression/edit.js +15 -3
  71. package/src/actions/scales/channels.js +2 -2
  72. package/src/actions/scales/create.js +3 -3
  73. package/src/actions/scales/definitions.js +1 -1
  74. package/src/actions/scales/edit.js +1 -1
  75. package/src/actions/scales/editPolicy.js +3 -0
  76. package/src/actions/scales/patch.js +1 -1
  77. package/src/actions/scales/quantitativeColor.js +8 -3
  78. package/src/actions/theme/reconcile.js +50 -23
  79. package/src/core/compositionState.js +2 -2
  80. package/src/core/graphicProperties.js +1 -0
  81. package/src/core/programState.js +4 -0
  82. package/src/core/resourceReferences.js +2 -2
  83. package/src/core/validation.js +9 -0
  84. package/src/core/vocabulary.js +3 -0
  85. package/src/grammar/axisTextPlacement.js +1 -0
  86. package/src/grammar/colorSizeLegend.js +39 -0
  87. package/src/grammar/computed.js +23 -1
  88. package/src/grammar/datasetSchema.js +10 -2
  89. package/src/grammar/displayLabels.js +8 -0
  90. package/src/grammar/facets/guides.js +54 -31
  91. package/src/grammar/facets/legendIntent.js +65 -0
  92. package/src/grammar/guideDash.js +1 -0
  93. package/src/grammar/legendBlocks.js +1 -0
  94. package/src/grammar/legendBorder.js +9 -0
  95. package/src/grammar/legendClip.js +19 -0
  96. package/src/grammar/legendCombinations.js +45 -0
  97. package/src/grammar/legendGradient.js +9 -0
  98. package/src/grammar/legendGroups.js +1 -1
  99. package/src/grammar/legendOpacity.js +6 -0
  100. package/src/grammar/pointShapes.js +16 -2
  101. package/src/grammar/regression/derive.js +9 -1
  102. package/src/grammar/regression/models.js +60 -0
  103. package/src/grammar/regression/parameters.js +9 -1
  104. package/src/grammar/regression/statistics.js +64 -0
  105. package/src/grammar/scales/color.js +1 -1
  106. package/src/grammar/scales/colorConsumers.js +1 -1
  107. package/src/grammar/scales/definition.js +1 -1
  108. package/src/grammar/scales/types.js +1 -1
  109. package/src/grammar/schemas/concreteGraphic.js +3 -0
  110. package/src/grammar/schemas/graphic.js +3 -0
  111. package/src/grammar/schemas/graphicBounds.js +31 -7
  112. package/src/grammar/schemas/semanticPath.js +17 -2
  113. package/src/grammar/summary.js +1 -1
  114. package/src/grammar/timeUnit.js +6 -4
  115. package/src/grammar/transformTopology.js +1 -0
  116. package/src/grammar/transforms.js +11 -1
  117. package/src/inspectionDescriptors.js +1 -1
  118. package/src/layout/guideCollisions.js +9 -1
  119. package/src/layout/legendGroup.js +1 -1
  120. package/src/materialization/compositionSnapshot.js +2 -7
  121. package/src/materialization/coordinateBounds.js +17 -4
  122. package/src/materialization/facetGuides/edit.js +30 -0
  123. package/src/materialization/facetGuides/layout.js +1 -1
  124. package/src/materialization/facetGuides/owner.js +125 -0
  125. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  126. package/src/materialization/facetGuides/placement.js +52 -6
  127. package/src/materialization/facetGuides/preparation.js +7 -0
  128. package/src/materialization/facets.js +1 -1
  129. package/src/materialization/graphicHierarchy.js +5 -1
  130. package/src/materialization/graphicTranslation.js +73 -0
  131. package/src/materialization/guides/layout.js +1 -1
  132. package/src/materialization/guides/resources.js +1 -1
  133. package/src/materialization/legendBorder.js +28 -0
  134. package/src/materialization/legendClip.js +76 -0
  135. package/src/materialization/legendGroups.js +1 -1
  136. package/src/materialization/legends.js +1 -1
  137. package/src/materialization/scaleGuideDependencies.js +13 -14
  138. package/src/materialization/scales/map.js +3 -0
  139. package/src/materialization/scales/resolve.js +2 -0
  140. package/src/materialization/typography.js +10 -2
  141. package/src/persistence/validation.js +174 -1
  142. package/src/persistence.js +1 -1
  143. package/src/renderers/canvas/index.js +20 -10
  144. package/src/renderers/canvas/isolated.js +166 -0
  145. package/src/renderers/canvas/limits.js +3 -0
  146. package/src/renderers/canvas/rect.js +1 -1
  147. package/src/renderers/pdf.js +2 -2
  148. package/src/renderers/png.js +2 -2
  149. package/src/renderers/svg.js +10 -3
  150. package/src/selectors/axes.js +1 -0
  151. package/src/selectors/graphicCapacity.js +38 -0
  152. package/src/version.js +1 -1
  153. package/types/basic.d.ts +2 -0
  154. package/types/index.d.ts +5 -0
  155. package/types/program.d.ts +206 -61
@@ -1,3 +1,4 @@
1
+ import {validateLegendLayerOpacity}from"../../../../grammar/legendOpacity.js";
1
2
  import { isPlainObject } from "../../../../core/immutable.js";
2
3
  import {
3
4
  nonEmptyString,
@@ -16,21 +17,23 @@ import {
16
17
  import { findLayerMatching } from "../../../../selectors/layers.js";
17
18
 
18
19
  const LAYER_OPTIONS = Object.freeze({
19
- line: Object.freeze(["type", "length", "lineWidth"]),
20
+ line: Object.freeze(["type", "length", "lineWidth", "opacity"]),
20
21
  point: Object.freeze([
21
22
  "type",
22
23
  "shape",
23
24
  "size",
24
25
  "fill",
25
26
  "stroke",
26
- "strokeWidth"
27
+ "strokeWidth",
28
+ "opacity"
27
29
  ]),
28
30
  swatch: Object.freeze([
29
31
  "type",
30
32
  "width",
31
33
  "height",
32
34
  "stroke",
33
- "strokeWidth"
35
+ "strokeWidth",
36
+ "opacity"
34
37
  ])
35
38
  });
36
39
  const INTERNAL_LAYER_OPTIONS = Object.freeze({
@@ -145,6 +148,7 @@ function defaultRecipe(kind) {
145
148
  }
146
149
 
147
150
  function normalizeLayer(layer, internal) {
151
+ validateLegendLayerOpacity(layer?.opacity);
148
152
  if (!isPlainObject(layer) || !Object.hasOwn(LAYER_OPTIONS, layer.type)) {
149
153
  throw new Error("Legend symbol layer type must be line, point, or swatch.");
150
154
  }
@@ -1,3 +1,4 @@
1
+ import { resolveSizeShapeMapping } from "../../../../grammar/legendCombinations.js";
1
2
  import { requireDataset } from "../../../../selectors/datasets.js";
2
3
  import { readNominalField } from "../../../../grammar/scales/fields.js";
3
4
  import { resolveLegendOrderDomain } from "../../../../grammar/categoryOrder.js";
@@ -85,11 +86,15 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
85
86
  const channels = requestedChannels ?? CHANNELS.filter(
86
87
  channel => layer.encoding?.[channel]?.scale !== undefined
87
88
  );
89
+ const sizeShape = channels?.length === 2 && channels.includes("size") && channels.includes("shape");
90
+ if (sizeShape && (layer.mark.type !== "point" || !["nominal", "ordinal"].includes(layer.encoding.size?.fieldType))) {
91
+ throw new Error("Combined size-shape requires one categorical point owner.");
92
+ }
88
93
  const kind = resolveLegendKind(layer, channels);
89
94
  if (
90
95
  !Array.isArray(channels) ||
91
96
  channels.length === 0 ||
92
- !channels.every(channel => CHANNELS.includes(channel)) ||
97
+ !channels.every(channel => CHANNELS.includes(channel) || sizeShape && channel === "size") ||
93
98
  new Set(channels).size !== channels.length
94
99
  ) {
95
100
  throw new Error(
@@ -129,7 +134,9 @@ export function resolveDefinition(program, layer, requestedChannels, requestedTi
129
134
  field,
130
135
  title: nonEmptyString(requestedTitle ?? field, "Legend title"),
131
136
  domain: resolveLegendOrderDomain(resolved.domain, order, linked,
132
- order?.values === undefined ? undefined : readNominalField(requireDataset(program, layer.data).values, field))
137
+ order?.values === undefined ? undefined : readNominalField(requireDataset(program, layer.data).values, field)),
138
+ ...(sizeShape ? {combine:"size-shape",...resolveSizeShapeMapping({layer,
139
+ scales:program.resolvedScales,order,rows:requireDataset(program,layer.data).values})} : {})
133
140
  };
134
141
  }
135
142
 
@@ -1,3 +1,4 @@
1
+ import{hasLegendClipGraphics,applyLegendClipGraphics}from"../../../../materialization/legendClip.js";
1
2
  import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
2
3
  import { action } from "../../../../core/action.js";
3
4
  import {
@@ -21,10 +22,17 @@ import {
21
22
  import { requestedStrokeDetails } from "../../../../grammar/strokeStyle.js";
22
23
  import { replaceGraphicItems } from "../../../primitives/editGraphics.js";
23
24
 
24
- function applyLayerOpacity(program, id, opacity) {
25
- return opacity === undefined
26
- ? program
27
- : program.editGraphics({ target: id, property: "opacity", value: opacity });
25
+ function applyLayerOpacity(program,id,opacity,normal=false) {
26
+ const owner=program.graphicSpec.objects[id];
27
+ if(!normal&&!owner.items.some(item=>item.properties.blend!==undefined)){
28
+ if(opacity!==undefined)return program.editGraphics({target:id,property:"opacity",value:opacity});
29
+ if(!owner.items.some(item=>item.properties.opacity!==undefined))return program;
30
+ }
31
+ return program.editGraphics({target:id,property:"items",value:owner.items.map(item=>{
32
+ const {opacity:_old,blend:_blend,...properties}=item.properties;
33
+ return {type:item.type??owner.type,properties:{...properties,
34
+ ...(opacity===undefined?{}:{opacity}),...(normal?{blend:"normal"}:{})}};
35
+ })});
28
36
  }
29
37
 
30
38
  function makeEditSymbol(type) {
@@ -34,9 +42,12 @@ function makeEditSymbol(type) {
34
42
  { op, description: `Rematerialize categorical legend ${type} symbols.` },
35
43
  function (args = {}) {
36
44
  noOptions(args, op);
37
- const { config: stored } = activeConfig(this, args.kind);
45
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
46
+ const { kind, config: stored } = activeConfig(this, args.kind);
38
47
  const config = categoricalLegendView(stored);
39
48
  const layer = layerFor(config, type);
49
+ const explicitRecipeOpacity = this.guideConfigs.legend[kind].symbol.layers
50
+ .some(sample=>sample.type===type&&sample.opacity!==undefined);
40
51
  const id = symbolGraphic(config, type);
41
52
  const dynamicPoint = type === "point" && config.channels.includes("shape");
42
53
  const styledSwatch = type === "swatch" && hasRectRadius(layer);
@@ -91,11 +102,11 @@ function makeEditSymbol(type) {
91
102
  if (dynamicPoint) {
92
103
  const items = config.domain.map((_, index) => {
93
104
  const fill = layer.fill ?? appearance.colors[index];
94
- return createPointShapeGraphic({
105
+ const graphic = createPointShapeGraphic({
95
106
  shape: appearance.shapes[index],
96
107
  x: x[index],
97
108
  y: layout.itemY[index],
98
- area: Math.PI * layer.size ** 2,
109
+ area: appearance.areas?.[index] ?? Math.PI * layer.size ** 2,
99
110
  fill,
100
111
  stroke: config.channels.includes("stroke")
101
112
  ? appearance.strokes[index]
@@ -104,6 +115,7 @@ function makeEditSymbol(type) {
104
115
  opacity: layer.opacity ?? appearance.opacity,
105
116
  ...requestedStrokeDetails(layer, "Legend point symbol")
106
117
  });
118
+ return !explicitRecipeOpacity?graphic:{...graphic,properties:{...graphic.properties,blend:"normal"}};
107
119
  });
108
120
  return next.editGraphics({
109
121
  target: id,
@@ -130,7 +142,7 @@ function makeEditSymbol(type) {
130
142
  )) {
131
143
  next = next.editGraphics({ target: id, property, value });
132
144
  }
133
- return applyLayerOpacity(next, id, layer.opacity ?? appearance.opacity);
145
+ return applyLayerOpacity(next, id, layer.opacity ?? appearance.opacity, explicitRecipeOpacity);
134
146
  }
135
147
  const x = layout.symbolX.map(
136
148
  value => value + (symbolWidth(config) - layer.width) / 2
@@ -155,6 +167,7 @@ function makeEditSymbol(type) {
155
167
  stroke: strokes[index],
156
168
  strokeWidth: layer.strokeWidth,
157
169
  ...((layer.opacity ?? appearance.opacity) === undefined ? {} : { opacity: layer.opacity ?? appearance.opacity }),
170
+ ...(!explicitRecipeOpacity?{}:{blend:"normal"}),
158
171
  ...requestedStrokeDetails(details, "Legend swatch symbol")
159
172
  }, details))
160
173
  );
@@ -236,6 +249,7 @@ export const rematerializeLegendSymbols = /* @__PURE__ */ action(
236
249
  { op: "rematerializeLegendSymbols", description: "Rematerialize layered legend symbols." },
237
250
  function (args = {}) {
238
251
  noOptions(args, "rematerializeLegendSymbols");
252
+ if (hasLegendClipGraphics(this)) return this.rematerializeLegend();
239
253
  const { config: stored } = activeConfig(this, args.kind);
240
254
  const config = categoricalLegendView(stored);
241
255
  let next = this;
@@ -0,0 +1,110 @@
1
+ import {findLayer} from '../../../selectors/layers.js';
2
+ import {findCoordinate} from '../../../selectors/coordinates.js';
3
+ import {legendBlockChannels} from '../../../grammar/legendBlocks.js';
4
+ import {mergeLegendTitleStyle} from '../../../grammar/legendTitleStyle.js';
5
+ import {validateOptionObject} from '../../../core/validation.js';
6
+ import {resolveColorSizeMapping,normalizeColorSizeSymbol,isColorSizeChannels} from '../../../grammar/colorSizeLegend.js';
7
+ import {mapSizeValues} from '../../../grammar/scales/size.js';
8
+ import {mapScaleConsumerValues} from '../../../materialization/scales/map.js';
9
+ import {intrinsicLegendConfig} from '../../../materialization/legendGroups.js';
10
+ import {resolveDisplayLabel} from '../../../grammar/displayLabels.js';
11
+ import {clippedSampleBounds,validateLegendClip} from '../../../grammar/legendClip.js';
12
+ import {resolveLegendItemLayout} from '../../../layout/legendItems.js';
13
+ import {resolveLegendGraphicPlacement} from '../../../materialization/graphicHierarchy.js';
14
+ import {normalizeItemLegendConfig,ITEM_LEGEND_OPTIONS,resolveContinuousBounds,formatContinuousValues,assertLegendBoundsInsideCanvas,resolveLegendBackgroundFromBounds,editLegendBackground,styleContinuousText,editGraphicProperties} from './continuous/common.js';
15
+ import {normalizeLegendSampling,resolveLegendSampleValues,validateSamplingAgainstScale,readLegendSampling} from './sampling.js';
16
+ import {resolveEffectiveLegendBlockConfig} from './blocks.js';
17
+ import {applyLegendClipGraphics} from '../../../materialization/legendClip.js';
18
+
19
+ export function resolveColorSizeOwner(program,target) {
20
+ const candidates=(target===undefined?program.semanticSpec.layers:[findLayer(program,target)]).filter(layer=>layer?.mark?.type==='point'&&layer.encoding?.color&&layer.encoding?.size);
21
+ const mapping=layer=>resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer.id],
22
+ coordinate:findCoordinate(program,layer.coordinate)});
23
+ 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]);}
24
+ const eligible=[];for(const layer of candidates){try{eligible.push(mapping(layer));}catch{/* Ineligible owners do not participate in inference. */}}
25
+ 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.');}
26
+ return eligible[0];
27
+ }
28
+
29
+ export function resolveColorSizeConfig(program,args={}) {
30
+ validateOptionObject(args,[...ITEM_LEGEND_OPTIONS.filter(key=>key!=='overflow'),'channels','combine','symbol','blockLayout','at'],'Color-size legend');
31
+ if(args.channels!==undefined&&!isColorSizeChannels(args.channels))throw new Error('Color-size combination requires exactly color and size channels.');
32
+ const mapping=resolveColorSizeOwner(program,args.target);
33
+ return {...mapping,...normalizeItemLegendConfig(args,{field:mapping.field},40),
34
+ sampling:normalizeLegendSampling(args,{label:'Color-size legend'}),symbol:normalizeColorSizeSymbol(args.symbol),combine:'color-size'};
35
+ }
36
+
37
+ export function refreshColorSizeConfig(program,stored) {
38
+ const mapping=resolveColorSizeOwner(program,stored.target);
39
+ if(mapping.field!==stored.field)throw new Error('Color-size combination field changed; explicitly recreate the block.');
40
+ return {...stored,...mapping,title:stored.inferredTitle?mapping.field:stored.title};
41
+ }
42
+
43
+ export function resolveColorSizeLayout(program,stored) {
44
+ validateLegendClip('colorSize',stored);
45
+ const config=intrinsicLegendConfig(program,resolveEffectiveLegendBlockConfig(program,'colorSize',stored));
46
+ const color=program.resolvedScales[stored.colorScale],size=program.resolvedScales[stored.sizeScale];
47
+ const sampling=readLegendSampling(config);validateSamplingAgainstScale(sampling,color,'Color-size samples');validateSamplingAgainstScale(sampling,size,'Color-size samples');
48
+ const values=resolveLegendSampleValues(config,size,'Color-size legend');
49
+ const colors=mapScaleConsumerValues(values,color,'color'),areas=mapSizeValues(values,size),radii=areas.map(area=>Math.sqrt(area/Math.PI));
50
+ const labels=formatContinuousValues(values,stored.domain,'quantitative',config.labels.format)
51
+ .map((text,index)=>resolveDisplayLabel(values[index],config.labelMap,()=>text));
52
+ const radius=Math.max(...radii),stroke=config.symbol.strokeWidth/2,width=2*(radius+stroke),height=Math.min(width,config.clipHeight??Infinity);
53
+ const {plot,canvas}=resolveContinuousBounds(program);
54
+ const layout=resolveLegendItemLayout(plot,config,labels,{width,height,itemBounds:radii.map(r=>clippedSampleBounds({left:width/2-r-stroke,right:width/2+r+stroke,top:-r-stroke,bottom:r+stroke},config.clipHeight))},canvas,program.materializationConfigs.textMetrics);
55
+ assertLegendBoundsInsideCanvas(layout.bounds,canvas,'Color-size legend',config);
56
+ const background=resolveLegendBackgroundFromBounds(layout.bounds,config.border,canvas,'Color-size legend',config);
57
+ return {...layout,background,values,colors,areas,radii,text:labels,config,symbolX:layout.symbolX.map(x=>x+width/2)};
58
+ }
59
+
60
+ function writeSemantic(program,config) {
61
+ return program.editSemantic({property:'guide.legend.colorSize.colorScale',value:config.colorScale})
62
+ .editSemantic({property:'guide.legend.colorSize.sizeScale',value:config.sizeScale})
63
+ .editSemantic({property:'guide.legend.colorSize.title',value:config.title})._withLegendConfig('colorSize',config);
64
+ }
65
+
66
+ export function materializeColorSizeLegend(program,stored) {
67
+ const config=refreshColorSizeConfig(program,stored),layout=resolveColorSizeLayout(program,config),effective=layout.config;
68
+ let next=writeSemantic(program,config);
69
+ next=editGraphicProperties(next,'colorSizeLegendSymbols',{length:layout.values.length,x:layout.symbolX,y:layout.itemY,radius:layout.radii,
70
+ fill:layout.colors,stroke:effective.symbol.stroke??layout.colors,strokeWidth:effective.symbol.strokeWidth,opacity:effective.symbol.opacity});
71
+ const visible=layout.text.flatMap((text,index)=>text===''?[]:[index]);
72
+ 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])});
73
+ next=editLegendBackground(next,'colorSizeLegendBackground',layout.background,effective.border);
74
+ next=styleContinuousText(next,'colorSizeLegendLabels',effective.labels);
75
+ if(effective.titleVisible!==false){
76
+ next=editGraphicProperties(next,'colorSizeLegendTitle',{x:layout.title.x,y:layout.title.y,text:effective.title});
77
+ next=styleContinuousText(next,'colorSizeLegendTitle',effective.titleStyle,{align:layout.title.align});
78
+ }
79
+ return applyLegendClipGraphics(next);
80
+ }
81
+
82
+ export function createColorSizeLegendFromConfig(program,config) {
83
+ 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.');
84
+ const layout=resolveColorSizeLayout(program,config),placement=resolveLegendGraphicPlacement(program);
85
+ let next=writeSemantic(program,config);
86
+ if(config.border!==false)next=next.createGraphics({id:'colorSizeLegendBackground',type:'rect',...placement});
87
+ next=next.createGraphics({id:'colorSizeLegendSymbols',type:'circle',length:layout.values.length,...placement})
88
+ .createGraphics({id:'colorSizeLegendLabels',type:'text',length:layout.values.length,...placement});
89
+ if(config.titleVisible!==false)next=next.createGraphics({id:'colorSizeLegendTitle',type:'text',...placement});
90
+ return materializeColorSizeLegend(next,config);
91
+ }
92
+
93
+ export function resolveColorSizeEdit(program,previous,args) {
94
+ validateOptionObject(args,[...ITEM_LEGEND_OPTIONS.filter(key=>key!=='overflow'),'symbol'],'Color-size legend edit');
95
+ const {count:_count,values:_values,title:_title,...layoutArgs}=args;
96
+ const title=args.title==='auto'?previous.field:typeof args.title==='string'?args.title:previous.title;
97
+ const border=args.border===undefined?previous.border:args.border===false||args.border===true?args.border:
98
+ {...(previous.border===false?{}:previous.border),...args.border};
99
+ const normalized=normalizeItemLegendConfig({...previous,...layoutArgs,title,
100
+ ...(args.position!==undefined&&args.position!==previous.position&&args.direction===undefined?{direction:undefined}:{}),
101
+ labels:{...previous.labels,...args.labels},titleStyle:mergeLegendTitleStyle(previous.titleStyle,args.titleStyle,{edit:true}),border},
102
+ {field:previous.field},40);
103
+ const config={...previous,...normalized,title,
104
+ inferredTitle:args.title==='auto'?true:typeof args.title==='string'?false:previous.inferredTitle,
105
+ titleVisible:args.title===false?false:args.title===undefined?previous.titleVisible:true,
106
+ symbol:normalizeColorSizeSymbol(args.symbol,previous.symbol),
107
+ sampling:normalizeLegendSampling(args,{previous,operation:'edit',label:'Color-size legend'})};
108
+ resolveColorSizeLayout(program,config);
109
+ return config;
110
+ }
@@ -0,0 +1,37 @@
1
+ import {prepareLegendGroup} from "./group.js";
2
+ import {sameOrderedValues} from '../../../core/validation.js';
3
+
4
+ export {validateLegendCombine} from "../../../grammar/legendCombinations.js";
5
+ import {validateMappedSizeShapeSymbol} from "../../../grammar/legendCombinations.js";
6
+
7
+ export function resolveSizeShapeCreation(program,args,layers,standard,internalChannels) {
8
+ const explicit=args.channels;
9
+ if (internalChannels===undefined && explicit!==undefined &&
10
+ !(explicit.length===2&&explicit.includes('shape')&&explicit.includes('size'))) {
11
+ throw new Error('size-shape combine requires exactly shape and size channels.');
12
+ }
13
+ const candidates=layers.filter(layer=>layer.mark?.type==='point'&&layer.encoding?.shape&&layer.encoding?.size&&
14
+ (args.target===undefined||layer.id===args.target));
15
+ if(candidates.length!==1)throw new Error('size-shape combine requires one point owner or an explicit target.');
16
+ const layer=candidates[0],shape=layer.encoding.shape,size=layer.encoding.size;
17
+ const shapeScale=program.resolvedScales[shape.scale],sizeScale=program.resolvedScales[size.scale];
18
+ if(shape.field!==size.field||!['nominal','ordinal'].includes(size.fieldType)||
19
+ shapeScale?.type!=='ordinal'||sizeScale?.type!=='ordinal'||
20
+ !sameOrderedValues(shapeScale.domain,sizeScale.domain)) {
21
+ throw new Error('size-shape combine requires one categorical field and identical ordered ordinal domains.');
22
+ }
23
+ if(sizeScale.range.some(area=>!Number.isFinite(area)||area<0))throw new Error('Combined size areas must be finite and nonnegative.');
24
+ validateMappedSizeShapeSymbol(args.symbol);
25
+ const {combine:_combine,channels:_channels,...common}=args;
26
+ const steps=[{op:'createCategoricalLegend',args:{...common,target:layer.id,channels:['shape','size']}}];
27
+ const others=internalChannels??explicit??(layer.encoding.color?['color']:[]);
28
+ for(const channel of others.filter(channel=>!['shape','size'].includes(channel))){
29
+ const color={target:layer.id,channels:[channel]};
30
+ for(const key of ['position','align','offset','title','labels','titleStyle','border'])if(Object.hasOwn(common,key))color[key]=common[key];
31
+ steps.push(...standard(program,color,layers).steps);
32
+ }
33
+ const position=common.position??'right';
34
+ const blockLayout=program.guideConfigs.legendGroups?.[layer.id]?.blockLayout??(['left','right'].includes(position)?'column':'row');
35
+ const prepared=prepareLegendGroup(program,{...common,blockLayout},layer.id,{creation:true,nativeOffset:8});
36
+ return {steps,finish:'always',legendGroup:{target:layer.id,config:prepared.program.guideConfigs.legendGroups[layer.id]}};
37
+ }
@@ -1 +1 @@
1
- import{validateOptionObject as h,validatePositiveFinite as k,validateNonNegativeFinite as x,validateNonEmptyString as v}from"../../../../core/validation.js";import{intrinsicLegendConfig as L}from"../../../../materialization/legendGroups.js";import{mapScaleConsumerValues as C}from"../../../../materialization/scales/map.js";import{transformedTicks as G}from"../../../../grammar/scales/index.js";import{resolveLegendItemLayout as _}from"../../../../layout/legendItems.js";import{editGraphicProperties as u}from"../../../primitives/graphicProperties.js";import{normalizeLegendSampling as z,readLegendSampling as W,validateSamplingAgainstScale as w,resolveLegendSampleValues as B}from"../sampling.js";import{normalizeContinuousLegend as T,normalizeItemLegendLayout as P,resolveContinuousBounds as V,formatContinuousValues as E,assertLegendBoundsInsideCanvas as F,resolveLegendBackgroundFromBounds as I,editLegendBackground as N,styleContinuousText as y}from"./common.js";function q(t){if(t===void 0||t==="auto")return{type:"point",radius:7,strokeWidth:1};if(h(t,["type","radius","stroke","strokeWidth"],"Sampled color symbol"),t.type!==void 0&&t.type!=="point")throw new Error("Sampled color symbol requires type point.");const e={type:"point",radius:7,strokeWidth:1,...t};return k(e.radius,"Sampled color symbol radius"),x(e.strokeWidth,"Sampled color symbol strokeWidth"),e.stroke!==void 0&&v(e.stroke,"Sampled color symbol stroke"),e}function Q(t,e,i="create"){if(t.gradient!==void 0)throw new Error("Symbol color legends do not accept gradient dimensions.");const{representation:l,values:r,count:s,symbol:o,columns:d,direction:n,itemGap:a,layout:p,titlePosition:c,...m}=t,b=T(m,"gradient"),g=["left","right","top-left","top-right","bottom-left","bottom-right"].includes(t.position??"right"),S=P({...t,itemGap:t.itemGap??(g?18:20)},{multiColumnSide:!0}),{count:X,...f}=b;return{...f,...S,representation:"symbols",sampling:z(t,{previous:e,operation:i,label:"Sampled color legend"}),symbol:q(t.symbol)}}function O(t,e,i){e=L(t,e);const{plot:l,canvas:r}=V(t),s=w(W(e),i,"Sampled color legend");let o=s.mode==="auto"&&["log","sqrt","symlog"].includes(i.type)?G(i.type,i.domain,s.count,i.type==="log"?{base:i.base}:i.type==="symlog"?{constant:i.constant}:{}):B(e,i,"Sampled color legend");s.mode==="auto"&&i.midpoint!==void 0&&!o.includes(i.midpoint)&&(o=[...o,i.midpoint].sort((c,m)=>c-m));let d=E(o,i.domain,e.fieldType,e.labels.format);const n=e.symbol.radius+e.symbol.strokeWidth/2,a=_(l,e,d,{width:n*2,height:n*2},r,t.materializationConfigs.textMetrics);F(a.bounds,r,"Sampled color legend",e);const p=I(a.bounds,e.border,r,"Sampled color legend",e);return{...a,background:p,values:o,texts:d,extent:n,config:e}}function R(t,e,i){const l=i,{scale:r,encoding:s}=e,o=O(t,l,r),d=C(o.values,r,"color");let n=t.editSemantic({property:"guide.legend.color.scale",value:s.scale}).editSemantic({property:"guide.legend.color.title",value:l.title})._withLegendConfig("gradient",e.config);return n=u(n,"colorGradientSymbols",{length:o.values.length,x:o.symbolX.map(a=>a+o.extent),y:o.itemY,radius:l.symbol.radius,fill:d,stroke:l.symbol.stroke??d,strokeWidth:l.symbol.strokeWidth,opacity:l.blockSymbol?.opacity??1}),n=u(n,"colorGradientLabels",{length:o.values.length,x:o.labelX,y:o.itemY,text:o.texts}),n=N(n,"colorGradientBackground",o.background,l.border),n=y(n,"colorGradientLabels",l.labels),l.titleVisible===!1?n:(n=u(n,"colorGradientTitle",{x:o.title.x,y:o.title.y,text:l.title}),y(n,"colorGradientTitle",l.titleStyle,{align:o.title.align}))}export{R as materializeColorSymbols,q as normalizeColorSymbol,Q as normalizeColorSymbols,O as resolveColorSymbolsLayout};
1
+ import{validateLegendClip as k}from"../../../../grammar/legendClip.js";import{applyLegendClipGraphics as c}from"../../../../materialization/legendClip.js";import{validateOptionObject as x,validatePositiveFinite as L,validateNonNegativeFinite as v,validateNonEmptyString as C}from"../../../../core/validation.js";import{intrinsicLegendConfig as G}from"../../../../materialization/legendGroups.js";import{mapScaleConsumerValues as _}from"../../../../materialization/scales/map.js";import{transformedTicks as w}from"../../../../grammar/scales/index.js";import{resolveLegendItemLayout as z}from"../../../../layout/legendItems.js";import{editGraphicProperties as p}from"../../../primitives/graphicProperties.js";import{normalizeLegendSampling as W,readLegendSampling as B,validateSamplingAgainstScale as T,resolveLegendSampleValues as I}from"../sampling.js";import{normalizeContinuousLegend as P,normalizeItemLegendLayout as V,resolveContinuousBounds as E,formatContinuousValues as F,assertLegendBoundsInsideCanvas as N,resolveLegendBackgroundFromBounds as q,editLegendBackground as M,styleContinuousText as b}from"./common.js";function O(t){if(t===void 0||t==="auto")return{type:"point",radius:7,strokeWidth:1};if(x(t,["type","radius","stroke","strokeWidth"],"Sampled color symbol"),t.type!==void 0&&t.type!=="point")throw new Error("Sampled color symbol requires type point.");const e={type:"point",radius:7,strokeWidth:1,...t};return L(e.radius,"Sampled color symbol radius"),v(e.strokeWidth,"Sampled color symbol strokeWidth"),e.stroke!==void 0&&C(e.stroke,"Sampled color symbol stroke"),e}function $(t,e,i="create"){if(t.gradient!==void 0)throw new Error("Symbol color legends do not accept gradient dimensions.");const{representation:l,values:r,count:s,symbol:o,columns:d,direction:n,itemGap:a,layout:u,titlePosition:y,...m}=t,g=P(m,"gradient"),S=["left","right","top-left","top-right","bottom-left","bottom-right"].includes(t.position??"right"),f=V({...t,itemGap:t.itemGap??(S?18:20)},{multiColumnSide:!0}),{count:Y,...h}=g;return{...h,...f,representation:"symbols",sampling:W(t,{previous:e,operation:i,label:"Sampled color legend"}),symbol:O(t.symbol)}}function X(t,e,i){k("gradient",e),e=G(t,e);const{plot:l,canvas:r}=E(t),s=T(B(e),i,"Sampled color legend");let o=s.mode==="auto"&&["log","pow","sqrt","symlog"].includes(i.type)?w(i.type,i.domain,s.count,i.type==="log"?{base:i.base}:i.type==="pow"?{exponent:i.exponent}:i.type==="symlog"?{constant:i.constant}:{}):I(e,i,"Sampled color legend");s.mode==="auto"&&i.midpoint!==void 0&&!o.includes(i.midpoint)&&(o=[...o,i.midpoint].sort((y,m)=>y-m));let d=F(o,i.domain,e.fieldType,e.labels.format);const n=e.symbol.radius+e.symbol.strokeWidth/2,a=z(l,e,d,{width:n*2,height:Math.min(n*2,e.clipHeight??1/0)},r,t.materializationConfigs.textMetrics);N(a.bounds,r,"Sampled color legend",e);const u=q(a.bounds,e.border,r,"Sampled color legend",e);return{...a,background:u,values:o,texts:d,extent:n,config:e}}function ee(t,e,i){const l=i,{scale:r,encoding:s}=e,o=X(t,l,r),d=_(o.values,r,"color");let n=t.editSemantic({property:"guide.legend.color.scale",value:s.scale}).editSemantic({property:"guide.legend.color.title",value:l.title})._withLegendConfig("gradient",e.config);return n=p(n,"colorGradientSymbols",{length:o.values.length,x:o.symbolX.map(a=>a+o.extent),y:o.itemY,radius:l.symbol.radius,fill:d,stroke:l.symbol.stroke??d,strokeWidth:l.symbol.strokeWidth,opacity:l.blockSymbol?.opacity??1}),n=p(n,"colorGradientLabels",{length:o.values.length,x:o.labelX,y:o.itemY,text:o.texts}),n=M(n,"colorGradientBackground",o.background,l.border),n=b(n,"colorGradientLabels",l.labels),l.titleVisible===!1?c(n):(n=p(n,"colorGradientTitle",{x:o.title.x,y:o.title.y,text:l.title}),c(b(n,"colorGradientTitle",l.titleStyle,{align:o.title.align})))}export{ee as materializeColorSymbols,O as normalizeColorSymbol,$ as normalizeColorSymbols,X as resolveColorSymbolsLayout};
@@ -1 +1 @@
1
- import{LEGEND_TITLE_STYLE_KEYS as A,mergeLegendTitleStyle as W,validateLegendTitleAlignment as x}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as k,isPlotLegend as p}from"../../../../layout/legend.js";import{isContinuousColorScaleType as D}from"../../../../grammar/scales/types.js";import{isPlainObject as T}from"../../../../core/immutable.js";import{resolveTextBounds as U}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as R}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as M}from"../../../../materialization/legendText.js";import{validateKeys as S,validateFontWeight as q,validateOptionObject as O,validateNonEmptyString as h,validateNonNegativeFinite as V,validatePositiveFinite as Y,validateGeneratedItemLimit as K}from"../../../../core/validation.js";import{formatTimeTick as X}from"../../../../grammar/ticks.js";import{formatValue as H,validateValueFormat as $}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as J,sampleNumericRange as Q}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as Z,canvasOverflowError as v}from"../../../../layout/canvas.js";import{isHorizontalEdgeLegend as I}from"../../../../layout/legendLane.js";import{DEFAULT_COLORS as m,DEFAULT_FONT_FAMILY as G,DEFAULT_VERTICAL_LEGEND_ITEM_GAP as L}from"../../../../theme/defaults.js";import{findLayer as ee}from"../../../../selectors/layers.js";import{isOpacityLegendLayer as te}from"../../../../materialization/legends.js";import{findCanvasGraphic as ne}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as ie}from"../sampling.js";import{editGraphicProperties as u}from"../../../primitives/graphicProperties.js";const oe=["target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","border","direction","columns","titlePosition"],ze=Object.freeze(["target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","title","labels","titleStyle","border","overflow"]),re=["offset","color","fontSize","fontFamily","fontWeight","format"],le=["color","lineWidth","padding","background"],_=["right","left","top","bottom",...k],b={offset:12,color:m.text,fontSize:12,fontFamily:G,fontWeight:"normal"},z={color:m.text,fontSize:13,fontFamily:G,fontWeight:600},de=Object.freeze(b),se=Object.freeze({...z,color:m.strongText});function N(e,t){return e!==void 0&&e!==!1&&h(e,"Legend title"),{title:e===!1||e===void 0?t:e,inferredTitle:e===!1||e===void 0,titleVisible:e!==!1}}function Ne(e,t,n){const i=["right","left"].includes(e.position??"right")||p(e),r=(e.direction??(i?"vertical":"horizontal"))==="vertical"?L:n;return{...ce({...e,itemGap:e.itemGap??r}),...N(e.title,t.field),labels:w(e.labels,"createLegend.labels",de),titleStyle:C(e.titleStyle,"createLegend.titleStyle",se),border:F(e.border)}}const ae={color:m.border,lineWidth:1,padding:12,background:"transparent"},y=Y,g=V;function w(e,t,n){if(e===void 0)return{...n};if(!T(e))throw new TypeError(`${t} must be a plain object.`);S(e,re,t);const i={...n,...e};Object.hasOwn(i,"offset")&&g(i.offset,`${t} offset`),y(i.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])h(i[o],`${t} ${o}`);return q(i.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(i,"format")&&(i.format=$(i.format,`${t} format`)),i}function C(e,t,n,i=!1){e!==void 0&&O(e,A,t);const o=W(n,e,{edit:i}),{align:r,...d}=o;return{...w(d,t,{}),...r===void 0?{}:{align:r}}}function F(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!T(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&S(e,le,"createLegend.border");const t={...ae,...e===!0?{}:e};for(const n of["color","background"])h(t[n],`Legend border ${n}`);return g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding"),t}function Ce(e,t){O(e,oe,"createLegend");const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=e.align??"center";if(!["left","center","right"].includes(i))throw new Error(`Unsupported legend alignment "${i}".`);if(["left","right"].includes(n)&&i!=="center")throw new Error("Side continuous legends require center alignment.");if(t==="gradient"&&Object.hasOwn(e,"values"))throw new Error("Gradient legends do not support exact sample values.");const o=t==="opacity"?ie(e,{operation:"create",label:"Opacity legend"}):void 0,r=t==="gradient"?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.");K(r,"Continuous legend count")}const d=e.offset??(p({position:n})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(x(e.titleStyle,l),!["top","left"].includes(l))throw new Error(`Unsupported legend titlePosition "${l}".`);if(l==="left"&&(!["top","bottom"].includes(n)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const a=e.itemGap??(t==="opacity"&&(["right","left"].includes(n)||p({position:n}))?L:l==="left"?20:28);if(y(a,"Legend itemGap"),t==="gradient"){for(const s of["symbol","columns","direction","itemGap"])if(Object.hasOwn(e,s))throw new Error(`Gradient legend does not accept ${s}.`)}else for(const s of["columns","direction","gradient"])if(Object.hasOwn(e,s))throw new Error(`Opacity legend does not accept ${s}.`);return{target:e.target,position:n,align:i,offset:d,...t==="gradient"?{count:r}:{sampling:o},...N(e.title),labels:w(e.labels,"createLegend.labels",l==="left"?{...b,offset:8}:b),titleStyle:C(e.titleStyle,"createLegend.titleStyle",z),itemGap:a,titlePosition:l,border:F(e.border)}}function P(e,t,n){const i=e.semanticSpec.layers.filter(n);if(t===void 0)return i.length===1?i[0]:void 0;const o=ee(e,t);return i.includes(o)?o:void 0}function Fe(e,t,n){const i=P(e,t,n==="opacity"?te:o=>o.mark?.type==="point"&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?`${n} legend requires one eligible ${n==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${n} legend target "${t}".`);return i}function Pe(e,t,n="color"){const i=P(e,t,o=>(n==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","bar","rect","text"]).includes(o.mark?.type)&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?n==="color"?"color legend requires one eligible point, bar, or rect mark.":"stroke legend requires one eligible mark.":`Unknown ${n} legend target "${t}".`);return i}function Be(e,t,n){const i=e.resolvedScales[t];if(n==="sequential"?!D(i?.type):i?.type!==n)throw new Error(`Legend requires resolved ${n} scale "${t}".`);return i}function je(e,t="Continuous legend layout requires Canvas bounds."){const n=Z(e),i=ne(e);if(n===void 0||![n.x,n.y,n.width,n.height].every(Number.isFinite)||i?.type!=="canvas"||!Number.isFinite(i.properties.width)||!Number.isFinite(i.properties.height))throw new Error(t);return{plot:n,canvas:i.properties}}function Ae(e,t){return Q(e[0],e[1],t,"Continuous legend domain")}function We(e,t,n,i="auto"){const o=$(i,"Legend label format");return o==="auto"?n==="temporal"?e.map(r=>X(r,t)):J(e):e.map(r=>H(r,{format:o,valueType:n==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function B(e,t,n,{align:i="left"}={}){const o=u(e,t,{fill:n.color,fontSize:n.fontSize,fontFamily:n.fontFamily,fontWeight:n.fontWeight,textAlign:i,textBaseline:"middle"});return t.endsWith("Title")?M(o,t,o.graphicSpec.objects[t].properties.text,n):o}function ke(e,t,n,i,{hardBreaks:o=!1}={}){return U({x:e.x,y:e.y,text:String(t),...n,...o?{lines:R(t,n,i).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},i)}function De(e,t,n,i){if(!(i?.groupIntrinsic||I(i)||["left","right"].includes(i?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw v(`${n} requires more Canvas margin space.`,e,t)}function Ue(e,t,n,i,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,a=-1/0,s=-1/0;for(const c of e)d=Math.min(d,c.left),l=Math.min(l,c.top),a=Math.max(a,c.right),s=Math.max(s,c.bottom);if(d-=t.padding,l-=t.padding,a+=t.padding,s+=t.padding,!o?.groupIntrinsic&&!I(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||a+r>n.width||s+r>n.height))throw v(`${i} background requires more Canvas margin space.`,[{left:d-r,right:a+r,top:l-r,bottom:s+r}],n);return{x:d,y:l,width:a-d,height:s-l}}function fe(e,t,n,i){return n===void 0?e:u(e,t,{...n,fill:i.background,stroke:i.color,strokeWidth:i.lineWidth})}function ce(e,{multiColumnSide:t=!1}={}){const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=["left","right"].includes(n)||p({position:n}),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??(i?"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 a=e.titlePosition??"top";if(x(e.titleStyle,a),!["top","left"].includes(a))throw new Error(`Unsupported legend titlePosition "${a}".`);if(i&&(r!=="center"||a!=="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 s=e.offset??(p({position:n})?8:30),c=e.itemGap??(d==="vertical"?L:28);return g(s,"Legend offset"),y(c,"Legend itemGap"),{position:n,layout:o,align:r,direction:d,columns:l,titlePosition:a,offset:s,itemGap:c}}function Re(e,t,n,i,{symbols:o,optional:r={},text:d=i.labels,labels:l=i.config.labels,titleStyle:a=i.config.titleStyle}){const s=`${t}Legend`,c=Object.values(o).find(Array.isArray)?.length??d.length;let f=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);f=u(f,`${s}Symbols`,{length:c,...o}),f=u(f,`${s}Labels`,{length:d.length,x:i.labelX,y:i.itemY,text:d}),f=fe(f,`${s}Background`,i.background,n.border),f=u(f,`${s}Symbols`,Object.fromEntries(Object.entries(r).filter(([,j])=>j!==void 0))),f=B(f,`${s}Labels`,l);const E=i.config;return E.titleVisible===!1?f:(f=u(f,`${s}Title`,{x:i.title.x,y:i.title.y,text:E.title}),B(f,`${s}Title`,a,{align:i.title.align}))}function Me(e,t,n,i,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);return n.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:i,...r}).createGraphics({id:`${d}Labels`,type:"text",length:i,...r}),n.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{de as ITEM_LEGEND_LABELS,ze as ITEM_LEGEND_OPTIONS,se as ITEM_LEGEND_TITLE_STYLE,De as assertLegendBoundsInsideCanvas,Me as createItemLegendGraphics,u as editGraphicProperties,fe as editLegendBackground,We as formatContinuousValues,Re as materializeItemLegend,Ce as normalizeContinuousLegend,N as normalizeInitialLegendTitle,Ne as normalizeItemLegendConfig,ce as normalizeItemLegendLayout,F as normalizeLegendBorder,w as normalizeLegendTextOptions,C as normalizeLegendTitleOptions,Be as requireResolvedLegendScale,je as resolveContinuousBounds,Pe as resolveContinuousColorLayer,Fe as resolveContinuousLegendLayer,Ue as resolveLegendBackgroundFromBounds,ke as resolveLegendTextBounds,Ae as sampleContinuousValues,P as selectLegendLayer,B as styleContinuousText,g as validateNonNegative,y as validatePositive};
1
+ import{normalizeLegendClipHeight as O}from"../../../../grammar/legendClip.js";import{applyLegendClipGraphics as $}from"../../../../materialization/legendClip.js";import{normalizeGradientDirection as H}from"../../../../grammar/legendGradient.js";import{normalizeLegendBorderDash as R}from"../../../../grammar/legendBorder.js";import{materializeLegendBorder as M}from"../../../../materialization/legendBorder.js";import{LEGEND_TITLE_STYLE_KEYS as q,mergeLegendTitleStyle as V,validateLegendTitleAlignment as v}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as Y,isPlotLegend as m}from"../../../../layout/legend.js";import{isContinuousColorScaleType as K}from"../../../../grammar/scales/types.js";import{isPlainObject as I}from"../../../../core/immutable.js";import{resolveTextBounds as X}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as J}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as Q}from"../../../../materialization/legendText.js";import{validateKeys as G,validateFontWeight as Z,validateOptionObject as z,validateNonEmptyString as y,validateNonNegativeFinite as ee,validatePositiveFinite as te,validateGeneratedItemLimit as ne}from"../../../../core/validation.js";import{formatTimeTick as ie}from"../../../../grammar/ticks.js";import{formatValue as oe,validateValueFormat as C}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as re,sampleNumericRange as le}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as de,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 ae}from"../../../../materialization/legends.js";import{findCanvasGraphic as ce}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as fe}from"../sampling.js";import{editGraphicProperties as p}from"../../../primitives/graphicProperties.js";const pe=["clipHeight","target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","border","direction","columns","titlePosition"],Re=Object.freeze(["clipHeight","target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","title","labels","titleStyle","border","overflow"]),ue=["offset","color","fontSize","fontFamily","fontWeight","format"],me=["color","lineWidth","padding","background","strokeDash"],P=["right","left","top","bottom",...Y],E={offset:12,color:h.text,fontSize:12,fontFamily:F,fontWeight:"normal"},D={color:h.text,fontSize:13,fontFamily:F,fontWeight:600},ge=Object.freeze(E),he=Object.freeze({...D,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 Me(e,t,n){const i=["right","left"].includes(e.position??"right")||m(e),r=(e.direction??(i?"vertical":"horizontal"))==="vertical"?w:n;return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},...ye({...e,itemGap:e.itemGap??r}),...B(e.title,t.field),labels:T(e.labels,"createLegend.labels",ge),titleStyle:A(e.titleStyle,"createLegend.titleStyle",he),border:j(e.border)}}const Le={color:h.border,lineWidth:1,padding:12,background:"transparent"},x=te,g=ee;function T(e,t,n){if(e===void 0)return{...n};if(!I(e))throw new TypeError(`${t} must be a plain object.`);G(e,ue,t);const i={...n,...e};Object.hasOwn(i,"offset")&&g(i.offset,`${t} offset`),x(i.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])y(i[o],`${t} ${o}`);return Z(i.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(i,"format")&&(i.format=C(i.format,`${t} format`)),i}function A(e,t,n,i=!1){e!==void 0&&z(e,q,t);const o=V(n,e,{edit:i}),{align:r,...d}=o;return{...T(d,t,{}),...r===void 0?{}:{align:r}}}function j(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!I(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&G(e,me,"createLegend.border");const t={...Le,...e===!0?{}:e};for(const i of["color","background"])y(t[i],`Legend border ${i}`);g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding");const n=R(t.strokeDash);return{...t,...n===void 0?{}:{strokeDash:n}}}function qe(e,t){z(e,pe,"createLegend");const n=e.position??"right";if(!P.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=e.align??"center";if(!["left","center","right"].includes(i))throw new Error(`Unsupported legend alignment "${i}".`);if(["left","right"].includes(n)&&i!=="center")throw new Error("Side continuous legends require center alignment.");if(t==="gradient"&&Object.hasOwn(e,"values"))throw new Error("Gradient legends do not support exact sample values.");const o=t==="opacity"?fe(e,{operation:"create",label:"Opacity legend"}):void 0,r=t==="gradient"?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:n})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(v(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(n)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const a=e.itemGap??(t==="opacity"&&(["right","left"].includes(n)||m({position:n}))?w:l==="left"?20:28);if(x(a,"Legend itemGap"),t==="gradient"){for(const s of["symbol","columns","itemGap"])if(Object.hasOwn(e,s))throw new Error(`Gradient legend does not accept ${s}.`)}else for(const s of["columns","direction","gradient"])if(Object.hasOwn(e,s))throw new Error(`Opacity legend does not accept ${s}.`);return{...e.clipHeight===void 0?{}:{clipHeight:O(e.clipHeight)},target:e.target,position:n,align:i,offset:d,...t==="gradient"?{count:r,...e.direction===void 0?{}:{gradientDirection:H(e.direction)}}:{sampling:o},...B(e.title),labels:T(e.labels,"createLegend.labels",l==="left"?{...E,offset:8}:E),titleStyle:A(e.titleStyle,"createLegend.titleStyle",D),itemGap:a,titlePosition:l,border:j(e.border)}}function U(e,t,n){const i=e.semanticSpec.layers.filter(n);if(t===void 0)return i.length===1?i[0]:void 0;const o=se(e,t);return i.includes(o)?o:void 0}function Ve(e,t,n){const i=U(e,t,n==="opacity"?ae:o=>o.mark?.type==="point"&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?`${n} legend requires one eligible ${n==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${n} legend target "${t}".`);return i}function Ye(e,t,n="color"){const i=U(e,t,o=>(n==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","bar","rect","text"]).includes(o.mark?.type)&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?n==="color"?"color legend requires one eligible point, bar, or rect mark.":"stroke legend requires one eligible mark.":`Unknown ${n} legend target "${t}".`);return i}function Ke(e,t,n){const i=e.resolvedScales[t];if(n==="sequential"?!K(i?.type):i?.type!==n)throw new Error(`Legend requires resolved ${n} scale "${t}".`);return i}function Xe(e,t="Continuous legend layout requires Canvas bounds."){const n=de(e),i=ce(e);if(n===void 0||![n.x,n.y,n.width,n.height].every(Number.isFinite)||i?.type!=="canvas"||!Number.isFinite(i.properties.width)||!Number.isFinite(i.properties.height))throw new Error(t);return{plot:n,canvas:i.properties}}function Je(e,t){return le(e[0],e[1],t,"Continuous legend domain")}function Qe(e,t,n,i="auto"){const o=C(i,"Legend label format");return o==="auto"?n==="temporal"?e.map(r=>ie(r,t)):re(e):e.map(r=>oe(r,{format:o,valueType:n==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function k(e,t,n,{align:i="left"}={}){const o=p(e,t,{fill:n.color,fontSize:n.fontSize,fontFamily:n.fontFamily,fontWeight:n.fontWeight,textAlign:i,textBaseline:"middle"});return t.endsWith("Title")?Q(o,t,o.graphicSpec.objects[t].properties.text,n):o}function Ze(e,t,n,i,{hardBreaks:o=!1}={}){return X({x:e.x,y:e.y,text:String(t),...n,...o?{lines:J(t,n,i).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},i)}function et(e,t,n,i){if(!(i?.groupIntrinsic||N(i)||["left","right"].includes(i?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw _(`${n} requires more Canvas margin space.`,e,t)}function tt(e,t,n,i,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,a=-1/0,s=-1/0;for(const f of e)d=Math.min(d,f.left),l=Math.min(l,f.top),a=Math.max(a,f.right),s=Math.max(s,f.bottom);if(d-=t.padding,l-=t.padding,a+=t.padding,s+=t.padding,!o?.groupIntrinsic&&!N(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||a+r>n.width||s+r>n.height))throw _(`${i} background requires more Canvas margin space.`,[{left:d-r,right:a+r,top:l-r,bottom:s+r}],n);return{x:d,y:l,width:a-d,height:s-l}}function be(e,t,n,i){return n===void 0?e:M(e,t,n,i)}function ye(e,{multiColumnSide:t=!1}={}){const n=e.position??"right";if(!P.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=["left","right"].includes(n)||m({position:n}),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??(i?"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 a=e.titlePosition??"top";if(v(e.titleStyle,a),!["top","left"].includes(a))throw new Error(`Unsupported legend titlePosition "${a}".`);if(i&&(r!=="center"||a!=="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 s=e.offset??(m({position:n})?8:30),f=e.itemGap??(d==="vertical"?w:28);return g(s,"Legend offset"),x(f,"Legend itemGap"),{position:n,layout:o,align:r,direction:d,columns:l,titlePosition:a,offset:s,itemGap:f}}function nt(e,t,n,i,{symbols:o,optional:r={},text:d=i.labels,labels:l=i.config.labels,titleStyle:a=i.config.titleStyle}){const s=`${t}Legend`,f=Object.values(o).find(Array.isArray)?.length??d.length;let c=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);c=p(c,`${s}Symbols`,{length:f,...o});const L=i.labelItemIndices,b=u=>L===void 0||!Array.isArray(u)?u:L.map(W=>u[W]);c=p(c,`${s}Labels`,{length:L?.length??d.length,x:b(i.labelX),y:b(i.itemY),text:b(d)}),c=be(c,`${s}Background`,i.background,n.border),c=p(c,`${s}Symbols`,Object.fromEntries(Object.entries(r).filter(([,u])=>u!==void 0))),c=k(c,`${s}Labels`,l);const S=i.config;return S.titleVisible===!1?$(c):(c=p(c,`${s}Title`,{x:i.title.x,y:i.title.y,text:S.title}),$(k(c,`${s}Title`,a,{align:i.title.align})))}function it(e,t,n,i,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);return n.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:i,...r}).createGraphics({id:`${d}Labels`,type:"text",length:i,...r}),n.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{ge as ITEM_LEGEND_LABELS,Re as ITEM_LEGEND_OPTIONS,he as ITEM_LEGEND_TITLE_STYLE,et as assertLegendBoundsInsideCanvas,it as createItemLegendGraphics,p as editGraphicProperties,be as editLegendBackground,Qe as formatContinuousValues,nt as materializeItemLegend,qe as normalizeContinuousLegend,B as normalizeInitialLegendTitle,Me as normalizeItemLegendConfig,ye as normalizeItemLegendLayout,j as normalizeLegendBorder,T as normalizeLegendTextOptions,A as normalizeLegendTitleOptions,Ke as requireResolvedLegendScale,Xe as resolveContinuousBounds,Ye as resolveContinuousColorLayer,Ve as resolveContinuousLegendLayer,tt as resolveLegendBackgroundFromBounds,Ze as resolveLegendTextBounds,Je as sampleContinuousValues,U as selectLegendLayer,k as styleContinuousText,g as validateNonNegative,x as validatePositive};
@@ -1,3 +1,5 @@
1
+ import{validateLegendClip}from"../../../../grammar/legendClip.js";
2
+ import{hasLegendClipGraphics}from"../../../../materialization/legendClip.js";
1
3
  import { normalizeColorSymbols, resolveColorSymbolsLayout, materializeColorSymbols } from "./colorSymbols.js";
2
4
  import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
3
5
  import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
@@ -29,10 +31,14 @@ import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
29
31
  export const DEFAULT_GRADIENT_SIZE = DEFAULT_LEGEND_GRADIENT;
30
32
 
31
33
  export function resolveGradientLayout(program, config, scale, label = "Gradient legend", stripStrokeWidth = 0) {
34
+ validateLegendClip("gradient",config);
32
35
  if (config.representation === "symbols") return resolveColorSymbolsLayout(program, config, scale);
33
36
  config = intrinsicLegendConfig(program, config);
34
37
  const { plot, canvas } = resolveContinuousBounds(program);
35
- const vertical = ["right", "left"].includes(config.position);
38
+ const vertical = config.gradientDirection === undefined
39
+ ? ["right", "left"].includes(config.position) : config.gradientDirection === "vertical";
40
+ const stripWidth = vertical ? config.gradient.thickness : config.gradient.length;
41
+ const stripHeight = vertical ? config.gradient.length : config.gradient.thickness;
36
42
  const length = config.gradient.length;
37
43
  const thickness = config.gradient.thickness;
38
44
  const titleText = config.titleVisible === false ? { height: 0, extraHeight: 0 }
@@ -44,29 +50,31 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
44
50
  x = plot.x + plot.width + config.offset;
45
51
  y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
46
52
  } else if (config.position === "left") {
47
- x = plot.x - config.offset - thickness;
53
+ x = plot.x - config.offset - stripWidth;
48
54
  y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
49
55
  } else {
50
56
  x = config.align === "left" ? plot.x
51
- : config.align === "right" ? plot.x + plot.width - length
52
- : plot.x + (plot.width - length) / 2;
57
+ : config.align === "right" ? plot.x + plot.width - stripWidth
58
+ : plot.x + (plot.width - stripWidth) / 2;
53
59
  y = config.position === "top"
54
- ? plot.y - config.offset - thickness - config.labels.offset -
60
+ ? plot.y - config.offset - stripHeight - config.labels.offset -
55
61
  config.labels.fontSize
56
62
  : plot.y + plot.height + config.offset +
57
63
  (config.titleVisible === false ? 0 : titleText.height + titleGap);
58
64
  }
59
65
  let title = vertical
60
- ? { x, y: plot.y + 20, align: "left" }
66
+ ? { x, y: ["right", "left"].includes(config.position) ? plot.y + 20
67
+ : y - titleGap - titleText.height / 2, align: "left" }
61
68
  : {
62
69
  x: x + length / 2,
63
70
  y: y - titleGap - titleText.height / 2,
64
71
  align: "center"
65
72
  };
66
- const transformed = ["log", "sqrt", "symlog"].includes(scale.type);
73
+ const transformed = ["log", "pow", "sqrt", "symlog"].includes(scale.type);
67
74
  const values = transformed
68
75
  ? transformedTicks(scale.type, scale.domain, config.count,
69
76
  scale.type === "log" ? {base: scale.base}
77
+ : scale.type === "pow" ? {exponent: scale.exponent}
70
78
  : scale.type === "symlog" ? {constant: scale.constant} : {})
71
79
  : [...sampleContinuousValues(scale.domain, config.count)];
72
80
  if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
@@ -86,11 +94,10 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
86
94
  const labelOffset = config.labels.offset;
87
95
  const labels = vertical
88
96
  ? fractions.map(fraction => ({
89
- x: config.position === "right"
90
- ? x + thickness + labelOffset
91
- : x - labelOffset,
97
+ x: config.position === "left"
98
+ ? x - labelOffset : x + thickness + labelOffset,
92
99
  y: y + length * (1 - fraction),
93
- align: config.position === "right" ? "left" : "right"
100
+ align: config.position === "left" ? "right" : "left"
94
101
  }))
95
102
  : fractions.map(fraction => ({
96
103
  x: x + length * fraction,
@@ -99,9 +106,9 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
99
106
  }));
100
107
  const ticks = vertical
101
108
  ? labels.map(label => ({
102
- x1: config.position === "right" ? x + thickness : x,
109
+ x1: config.position === "left" ? x : x + thickness,
103
110
  y1: label.y,
104
- x2: config.position === "right" ? x + thickness + 6 : x - 6,
111
+ x2: config.position === "left" ? x - 6 : x + thickness + 6,
105
112
  y2: label.y
106
113
  }))
107
114
  : labels.map(label => ({
@@ -125,6 +132,12 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
125
132
  const labelBounds = labels.map((label, index) =>
126
133
  resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
127
134
  );
135
+ if (config.titlePosition === "bottom" && config.titleVisible !== false) {
136
+ const bottom = Math.max(stripBounds.bottom + stripStrokeWidth / 2,
137
+ ...tickBounds.map(bounds => bounds.bottom), ...labelBounds.map(bounds => bounds.bottom));
138
+ title = { x: vertical ? x : x + length / 2,
139
+ y: bottom + titleGap + titleText.height / 2, align: vertical ? "left" : "center" };
140
+ }
128
141
  if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
129
142
  [{ left: stripBounds.left - stripStrokeWidth / 2, right: stripBounds.right + stripStrokeWidth / 2,
130
143
  top: stripBounds.top - stripStrokeWidth / 2, bottom: stripBounds.bottom + stripStrokeWidth / 2 },
@@ -244,6 +257,7 @@ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
244
257
  description: "Rematerialize a continuous color gradient legend."
245
258
  }, [],
246
259
  function (args = {}) {
260
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
247
261
  const stored = this.guideConfigs.legend?.gradient;
248
262
  if (stored === undefined) {
249
263
  throw new Error("Gradient legend requires stored configuration.");
@@ -1,3 +1,5 @@
1
+ import{validateLegendClip}from"../../../../grammar/legendClip.js";
2
+ import{hasLegendClipGraphics,applyLegendClipGraphics}from"../../../../materialization/legendClip.js";
1
3
  import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
2
4
  import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
3
5
  import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
@@ -88,6 +90,7 @@ function resolveOpacityConfig(program, config) {
88
90
  }
89
91
 
90
92
  function resolveOpacityLayout(program, config, scale) {
93
+ validateLegendClip("opacity",config);
91
94
  config = intrinsicLegendConfig(program, config);
92
95
  const { plot, canvas } = resolveContinuousBounds(program);
93
96
  const vertical = ["right", "left"].includes(config.position);
@@ -99,6 +102,7 @@ function resolveOpacityLayout(program, config, scale) {
99
102
  config.labels.format
100
103
  );
101
104
  const symbolExtent = config.symbol.radius + (config.symbol.strokeWidth ?? 0) / 2;
105
+ const visibleExtent=Math.min(symbolExtent,config.clipHeight===undefined?Infinity:config.clipHeight/2);
102
106
  const titleText = config.titleVisible === false ? { width: 0, height: 0 }
103
107
  : resolveLegendTitleText(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
104
108
  const labelWidths = texts.map(text => measureTextWidth(text, config.labels, program.materializationConfigs.textMetrics));
@@ -109,7 +113,7 @@ function resolveOpacityLayout(program, config, scale) {
109
113
  const baseX = config.position === "right"
110
114
  ? plot.x + plot.width + config.offset
111
115
  : plot.x - config.offset;
112
- const itemHeight = Math.max(symbolExtent * 2, legendLabelLineHeight(config.labels.fontSize));
116
+ const itemHeight = Math.max(visibleExtent * 2, legendLabelLineHeight(config.labels.fontSize));
113
117
  const pitch = Math.max(config.itemGap, itemHeight);
114
118
  const firstY = Math.max(plot.y + 46, config.titleVisible === false ? plot.y + 46
115
119
  : plot.y + 20 + titleText.height / 2 + 12 + itemHeight / 2);
@@ -142,7 +146,7 @@ function resolveOpacityLayout(program, config, scale) {
142
146
  : config.align === "right" ? plot.x + plot.width - width
143
147
  : plot.x + (plot.width - width) / 2;
144
148
  const height = Math.max(
145
- symbolExtent * 2,
149
+ visibleExtent * 2,
146
150
  config.labels.fontSize,
147
151
  titleText.height
148
152
  );
@@ -171,9 +175,9 @@ function resolveOpacityLayout(program, config, scale) {
171
175
  const labelHalf = config.labels.fontSize / 2;
172
176
  const y = config.position === "top"
173
177
  ? plot.y - config.offset - config.labels.fontSize -
174
- config.labels.offset - symbolExtent
178
+ config.labels.offset - visibleExtent
175
179
  : plot.y + plot.height + config.offset +
176
- (config.titleVisible === false ? 0 : titleText.height + 12) + symbolExtent;
180
+ (config.titleVisible === false ? 0 : titleText.height + 12) + visibleExtent;
177
181
  const samplePitch = values.length === 1 ? 0 : width / (values.length - 1);
178
182
  symbols = values.map((_, index) => ({
179
183
  x: startX + index * samplePitch,
@@ -181,12 +185,12 @@ function resolveOpacityLayout(program, config, scale) {
181
185
  }));
182
186
  labels = symbols.map(symbol => ({
183
187
  x: symbol.x,
184
- y: symbol.y + symbolExtent + config.labels.offset + labelHalf,
188
+ y: symbol.y + visibleExtent + config.labels.offset + labelHalf,
185
189
  align: "center"
186
190
  }));
187
191
  title = {
188
192
  x: startX + width / 2,
189
- y: y - symbolExtent - 12 - titleHalf,
193
+ y: y - visibleExtent - 12 - titleHalf,
190
194
  align: "center"
191
195
  };
192
196
  }
@@ -196,8 +200,8 @@ function resolveOpacityLayout(program, config, scale) {
196
200
  const symbolBounds = symbols.map(symbol => ({
197
201
  left: symbol.x - symbolExtent,
198
202
  right: symbol.x + symbolExtent,
199
- top: symbol.y - symbolExtent,
200
- bottom: symbol.y + symbolExtent
203
+ top: symbol.y - visibleExtent,
204
+ bottom: symbol.y + visibleExtent
201
205
  }));
202
206
  if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
203
207
  [...labelBounds, ...symbolBounds], program.materializationConfigs.textMetrics);
@@ -227,6 +231,7 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ closedAction(
227
231
  description: "Rematerialize a field-opacity sample legend."
228
232
  }, [],
229
233
  function (args = {}) {
234
+ if(hasLegendClipGraphics(this))return this.rematerializeLegend();
230
235
  const stored = this.guideConfigs.legend?.opacity;
231
236
  if (stored === undefined) {
232
237
  throw new Error("Opacity legend requires stored configuration.");
@@ -286,18 +291,18 @@ export const rematerializeOpacityLegend = /* @__PURE__ */ closedAction(
286
291
  config.labels,
287
292
  { align: layout.labels[0].align }
288
293
  );
289
- if (config.titleVisible === false) return next;
294
+ if (config.titleVisible === false) return applyLegendClipGraphics(next);
290
295
  next = editGraphicProperties(next, "opacityLegendTitle", {
291
296
  x: layout.title.x,
292
297
  y: layout.title.y,
293
298
  text: config.title
294
299
  });
295
- return styleContinuousText(
300
+ return applyLegendClipGraphics(styleContinuousText(
296
301
  next,
297
302
  "opacityLegendTitle",
298
303
  config.titleStyle,
299
304
  { align: layout.title.align }
300
- );
305
+ ));
301
306
  }
302
307
  );
303
308