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.
- package/CHANGELOG.md +50 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- 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,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 -
|
|
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 -
|
|
52
|
-
: plot.x + (plot.width -
|
|
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 -
|
|
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
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
109
|
+
x1: config.position === "left" ? x : x + thickness,
|
|
103
110
|
y1: label.y,
|
|
104
|
-
x2: config.position === "
|
|
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(
|
|
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
|
-
|
|
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 -
|
|
178
|
+
config.labels.offset - visibleExtent
|
|
175
179
|
: plot.y + plot.height + config.offset +
|
|
176
|
-
(config.titleVisible === false ? 0 : titleText.height + 12) +
|
|
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 +
|
|
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 -
|
|
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 -
|
|
200
|
-
bottom: symbol.y +
|
|
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
|
|