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,4 +1,7 @@
|
|
|
1
|
+
import { cartesianAxisOwners } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
1
3
|
import { isNominalValue } from "../../../grammar/scales/fields.js";
|
|
4
|
+
import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
|
|
2
5
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
3
6
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
4
7
|
import {
|
|
@@ -27,10 +30,10 @@ import {
|
|
|
27
30
|
} from "../../../grammar/schemas/graphicTree.js";
|
|
28
31
|
|
|
29
32
|
const GRID_OPTIONS = Object.freeze([
|
|
30
|
-
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "zIndex", "bandPosition", "alongOffset"
|
|
33
|
+
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
31
34
|
]);
|
|
32
35
|
const GRID_EDIT_OPTIONS = Object.freeze([
|
|
33
|
-
"count", "values", "color", "lineWidth", "strokeDash", "zIndex", "bandPosition", "alongOffset"
|
|
36
|
+
"count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
34
37
|
]);
|
|
35
38
|
const DEFAULT_STYLE = Object.freeze({
|
|
36
39
|
color: DEFAULT_COLORS.grid,
|
|
@@ -49,19 +52,11 @@ export function gridNames(direction) {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function validateStrokeDash(value) {
|
|
52
|
-
|
|
53
|
-
!Array.isArray(value) ||
|
|
54
|
-
value.length % 2 !== 0 ||
|
|
55
|
-
!value.every(item => Number.isFinite(item) && item >= 0)
|
|
56
|
-
) {
|
|
57
|
-
throw new TypeError(
|
|
58
|
-
"Grid strokeDash must be an even-length array of non-negative finite numbers."
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return value;
|
|
55
|
+
return validateGuideDash(value, "Grid strokeDash");
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
export function validateGridCreateArgs(args, operation) {
|
|
59
|
+
if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
|
|
65
60
|
validateOptionObject(args, GRID_OPTIONS, operation);
|
|
66
61
|
if (Array.isArray(args.values)) {
|
|
67
62
|
validateGeneratedItemLimit(args.values.length, "Grid value count");
|
|
@@ -95,6 +90,7 @@ export function validateGridCreateArgs(args, operation) {
|
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
export function validateGridEditArgs(args, operation) {
|
|
93
|
+
if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
|
|
98
94
|
validateOptionObject(args, GRID_EDIT_OPTIONS, operation, {
|
|
99
95
|
allowEmpty: false
|
|
100
96
|
});
|
|
@@ -138,39 +134,25 @@ export function resolveGridResources(program, direction, args) {
|
|
|
138
134
|
let layers = program.semanticSpec.layers.filter(
|
|
139
135
|
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
|
|
140
136
|
);
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
if (layers.length === 0) throw new Error(`${direction} grid found no matching encoded layers.`);
|
|
155
|
-
const coordinateIds = [...new Set(layers.map(layer => layer.coordinate).filter(Boolean))];
|
|
156
|
-
if (layers.some(layer => layer.coordinate === undefined)) {
|
|
157
|
-
throw new Error(`${direction} grid requires a stored coordinate.`);
|
|
158
|
-
}
|
|
159
|
-
if (coordinateIds.length === 0) throw new Error(`Unknown grid coordinate "${requestedCoordinate}".`);
|
|
160
|
-
if (coordinateIds.length > 1) {
|
|
161
|
-
throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
|
|
162
|
-
}
|
|
137
|
+
const requestedCoordinate = args.coordinate === undefined ? undefined : validateUserId(args.coordinate, "Grid coordinate id");
|
|
138
|
+
const requestedScale = args.scale === undefined ? undefined : validateUserId(args.scale, "Grid scale id");
|
|
139
|
+
const axes = cartesianAxisOwners(program).filter(owner => owner.channel === channel && owner.scale !== undefined && owner.coordinate !== undefined);
|
|
140
|
+
const bindings = [...layers.map(layer => ({ coordinate: layer.coordinate, scale: layer.encoding[channel].scale })),
|
|
141
|
+
...axes.map(owner => ({ coordinate: owner.coordinate, scale: owner.scale }))]
|
|
142
|
+
.filter(binding => (requestedCoordinate === undefined || binding.coordinate === requestedCoordinate) &&
|
|
143
|
+
(requestedScale === undefined || binding.scale === requestedScale));
|
|
144
|
+
if (bindings.length === 0) throw new Error(layers.length === 0
|
|
145
|
+
? `${direction} grid requires a ${channel} encoding or bound Cartesian axis.`
|
|
146
|
+
: `${direction} grid found no matching encoded layers.`);
|
|
147
|
+
if (bindings.some(binding => binding.coordinate === undefined)) throw new Error(`${direction} grid requires a stored coordinate.`);
|
|
148
|
+
const coordinateIds = [...new Set(bindings.map(binding => binding.coordinate))];
|
|
149
|
+
if (coordinateIds.length > 1) throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
|
|
163
150
|
const coordinate = requestedCoordinate ?? coordinateIds[0];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
const scaleIds = [...new Set(layers.map(layer => layer.encoding[channel].scale))];
|
|
169
|
-
if (scaleIds.length === 0) throw new Error(`Unknown grid scale "${requestedScale}".`);
|
|
170
|
-
if (scaleIds.length > 1) {
|
|
171
|
-
throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
|
|
172
|
-
}
|
|
151
|
+
if (findCoordinate(program, coordinate)?.type !== "cartesian") throw new Error(`${direction} grid requires a Cartesian coordinate.`);
|
|
152
|
+
const scaleIds = [...new Set(bindings.map(binding => binding.scale))];
|
|
153
|
+
if (scaleIds.length > 1) throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
|
|
173
154
|
const scale = requestedScale ?? scaleIds[0];
|
|
155
|
+
layers = layers.filter(layer => layer.coordinate === coordinate && layer.encoding[channel].scale === scale);
|
|
174
156
|
if (
|
|
175
157
|
!["linear", "time", "band", "point"].includes(program.resolvedScales[scale]?.type) &&
|
|
176
158
|
!isTransformedScaleType(program.resolvedScales[scale]?.type)
|
|
@@ -185,6 +167,11 @@ export function resolveGridResources(program, direction, args) {
|
|
|
185
167
|
const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
|
|
186
168
|
return leader === undefined ? [layer.id] : [leader, layer.id];
|
|
187
169
|
}));
|
|
170
|
+
if (related.size === 0) {
|
|
171
|
+
for (const owner of axes.filter(owner => owner.scale === scale && owner.coordinate === coordinate)) {
|
|
172
|
+
for (const id of axisGraphicIds(channel, owner.id)) if (program.graphicSpec.objects[id] !== undefined) related.add(id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
188
175
|
let before;
|
|
189
176
|
let after;
|
|
190
177
|
walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
|
|
@@ -210,7 +197,7 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
210
197
|
: Object.hasOwn(args, "count")
|
|
211
198
|
? { mode: "count", count: args.count }
|
|
212
199
|
: inferGridTickConfig(program, resources.channel, resources.scale);
|
|
213
|
-
return {
|
|
200
|
+
return patchGuideDash({
|
|
214
201
|
direction,
|
|
215
202
|
...(args.bandPosition === undefined ? {} : { bandPosition: args.bandPosition }),
|
|
216
203
|
...(args.zIndex === undefined ? {} : { zIndex: args.zIndex }),
|
|
@@ -222,7 +209,7 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
222
209
|
color: args.color ?? DEFAULT_STYLE.color,
|
|
223
210
|
lineWidth: args.lineWidth ?? DEFAULT_STYLE.lineWidth,
|
|
224
211
|
strokeDash: args.strokeDash ?? DEFAULT_STYLE.strokeDash
|
|
225
|
-
};
|
|
212
|
+
}, args);
|
|
226
213
|
}
|
|
227
214
|
|
|
228
215
|
export function refreshGridConfig(program, config) {
|
|
@@ -257,7 +244,7 @@ export function editGridConfig(previous, args) {
|
|
|
257
244
|
} else if (args.values === "auto") {
|
|
258
245
|
config.inferredValues = true;
|
|
259
246
|
}
|
|
260
|
-
return config;
|
|
247
|
+
return patchGuideDash(config, args);
|
|
261
248
|
}
|
|
262
249
|
|
|
263
250
|
export function resolveGridGeometry(program, config) {
|
|
@@ -281,11 +268,21 @@ export function resolveGridGeometry(program, config) {
|
|
|
281
268
|
if (!values.every(value => scale.domain.includes(value))) throw new RangeError("Grid values must be inside the scale domain.");
|
|
282
269
|
const centers = mapOrdinalPositionValues(values, scale);
|
|
283
270
|
const direction = scale.range[1] >= scale.range[0] ? 1 : -1;
|
|
271
|
+
const owners = new Map();
|
|
272
|
+
if (config.bandPosition === "boundary") {
|
|
273
|
+
centers.forEach((center, index) => {
|
|
274
|
+
for (const edge of [center - direction * scale.bandwidth / 2, center + direction * scale.bandwidth / 2]) {
|
|
275
|
+
if (!owners.has(edge)) owners.set(edge, values[index]);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
284
279
|
const positions = config.bandPosition === "boundary"
|
|
285
|
-
? [...
|
|
286
|
-
: centers;
|
|
280
|
+
? [...owners.keys()].sort((a, b) => direction * (a - b)) : centers;
|
|
287
281
|
validateGeneratedItemLimit(positions.length, "Grid value count");
|
|
288
|
-
return
|
|
282
|
+
return {
|
|
283
|
+
...resolveGridLineGeometry({ direction: config.direction, values: positions, positions: positions.map(value => value + (config.alongOffset ?? 0)), bounds }),
|
|
284
|
+
strokeDash: resolveGuideDashes(config.bandPosition === "boundary" ? positions.map(value => owners.get(value)) : values, config)
|
|
285
|
+
};
|
|
289
286
|
}
|
|
290
287
|
const values = valuesFromTickConfig(program, config);
|
|
291
288
|
validateGeneratedItemLimit(values.length, "Grid value count");
|
|
@@ -295,12 +292,12 @@ export function resolveGridGeometry(program, config) {
|
|
|
295
292
|
throw new RangeError("Grid values must be inside the scale domain.");
|
|
296
293
|
}
|
|
297
294
|
const positions = mapContinuousScaleValues(values, scale);
|
|
298
|
-
return resolveGridLineGeometry({
|
|
295
|
+
return { ...resolveGridLineGeometry({
|
|
299
296
|
direction: config.direction,
|
|
300
297
|
values,
|
|
301
298
|
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
302
299
|
bounds
|
|
303
|
-
});
|
|
300
|
+
}), strokeDash: resolveGuideDashes(values, config) };
|
|
304
301
|
}
|
|
305
302
|
|
|
306
303
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { editFacetParentLegend } from "../../../materialization/facetGuides/edit.js";
|
|
2
|
+
|
|
3
|
+
import {patchLegendClipHeight, validateLegendClip} from "../../../grammar/legendClip.js";
|
|
4
|
+
import { LEGEND_POSITIONS } from "../../../core/vocabulary.js";
|
|
1
5
|
import { action } from "../../../core/action.js";
|
|
2
6
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
7
|
import {
|
|
@@ -23,11 +27,11 @@ import {
|
|
|
23
27
|
resolveLegendBlock
|
|
24
28
|
} from "./target.js";
|
|
25
29
|
import { validateFontWeight } from "./categorical/validation.js";
|
|
26
|
-
import { normalizeDisplayLabelMap } from "../../../grammar/displayLabels.js";
|
|
30
|
+
import { normalizeDisplayLabelMap, normalizeNumericDisplayLabelMap } from "../../../grammar/displayLabels.js";
|
|
27
31
|
|
|
28
32
|
const OPTIONS = Object.freeze([
|
|
29
33
|
"target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
|
|
30
|
-
"labelMap"
|
|
34
|
+
"labelMap", "position", "clipHeight"
|
|
31
35
|
]);
|
|
32
36
|
const TEXT_OPTIONS = Object.freeze(["fontSize", "fontFamily", "fontWeight", "color"]);
|
|
33
37
|
const SYMBOL_OPTIONS = Object.freeze(["size", "fill", "stroke", "strokeWidth", "opacity"]);
|
|
@@ -63,10 +67,12 @@ function categoricalSupports(config, property) {
|
|
|
63
67
|
|
|
64
68
|
function validateSymbolSupport(descriptor, property) {
|
|
65
69
|
const { channels, family, kind, config } = descriptor;
|
|
70
|
+
if(kind === "colorSize" && ["fill","size"].includes(property)) throw new Error("Color-size symbols cannot override mapped fill or size.");
|
|
66
71
|
if (family === "gradient") {
|
|
67
72
|
throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
|
|
68
73
|
}
|
|
69
74
|
if (family === "categorical") {
|
|
75
|
+
if (property === "size" && channels.includes("size")) throw new Error("Combined size-shape block cannot override mapped size.");
|
|
70
76
|
if (property === "fill" && channels.includes("color")) {
|
|
71
77
|
throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
72
78
|
}
|
|
@@ -125,13 +131,21 @@ function normalizeSymbolPatch(value, descriptor) {
|
|
|
125
131
|
return { ...value };
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
function normalizeBlockLabelMap(descriptor, map) {
|
|
135
|
+
if (["size","colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
|
|
136
|
+
return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
137
|
+
}
|
|
138
|
+
if (descriptor.family !== "categorical") {
|
|
139
|
+
throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
|
|
140
|
+
}
|
|
141
|
+
return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
142
|
+
}
|
|
143
|
+
|
|
128
144
|
export function validateLegendBlockOverride(descriptor, override) {
|
|
145
|
+
if (override.position !== undefined && !LEGEND_POSITIONS.includes(override.position)) throw new Error("Invalid local legend position.");
|
|
129
146
|
if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
|
|
130
147
|
if (override.labelMap !== undefined) {
|
|
131
|
-
|
|
132
|
-
throw new Error("editLegendBlock labelMap requires a categorical block.");
|
|
133
|
-
}
|
|
134
|
-
normalizeDisplayLabelMap(override.labelMap, "editLegendBlock labelMap");
|
|
148
|
+
normalizeBlockLabelMap(descriptor, override.labelMap);
|
|
135
149
|
}
|
|
136
150
|
return override;
|
|
137
151
|
}
|
|
@@ -176,6 +190,8 @@ export function patchLegendBlockOverride(config, key, patch) {
|
|
|
176
190
|
entry = withoutEmptyNested(entry, "symbol", patch.symbol);
|
|
177
191
|
}
|
|
178
192
|
if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
|
|
193
|
+
if (Object.hasOwn(patch, "position")) entry.position = patch.position;
|
|
194
|
+
if (patch.removePosition === true) delete entry.position;
|
|
179
195
|
if (patch.removeLabelMap === true) delete entry.labelMap;
|
|
180
196
|
const blockOverrides = { ...(config.blockOverrides ?? {}) };
|
|
181
197
|
if (Object.keys(entry).length === 0) delete blockOverrides[key];
|
|
@@ -208,7 +224,7 @@ function applyCategoricalSymbol(symbol, patch) {
|
|
|
208
224
|
}
|
|
209
225
|
|
|
210
226
|
export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
211
|
-
const channels = kind === "series" || kind === "color" || kind === "stroke"
|
|
227
|
+
const channels = kind === "colorSize" ? ["color","size"] : kind === "series" || kind === "color" || kind === "stroke"
|
|
212
228
|
? config.channels
|
|
213
229
|
: [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
|
|
214
230
|
gradient: "color", interval: "color", strokeGradient: "stroke",
|
|
@@ -233,6 +249,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
|
233
249
|
: {})
|
|
234
250
|
};
|
|
235
251
|
if (override.symbol === undefined) return effective;
|
|
252
|
+
if(kind === "colorSize") return {...effective,symbol:{...config.symbol,...override.symbol}};
|
|
236
253
|
if (["series", "color", "stroke"].includes(kind)) {
|
|
237
254
|
return {
|
|
238
255
|
...effective,
|
|
@@ -290,6 +307,7 @@ export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
|
|
|
290
307
|
}
|
|
291
308
|
|
|
292
309
|
const SPECIALIZED_SYMBOLS = Object.freeze({
|
|
310
|
+
colorSize: Object.freeze({id:"colorSizeLegendSymbols",type:"circle"}),
|
|
293
311
|
gradient: Object.freeze({ id: "colorGradientSymbols", type: "circle" }),
|
|
294
312
|
size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
|
|
295
313
|
opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
|
|
@@ -326,6 +344,13 @@ function recreateSymbolGraphics(program, descriptor) {
|
|
|
326
344
|
|
|
327
345
|
function normalizeBlockPatch(args, descriptor) {
|
|
328
346
|
const patch = {};
|
|
347
|
+
if (Object.hasOwn(args, "position")) {
|
|
348
|
+
if (args.position === "inherit") patch.removePosition = true;
|
|
349
|
+
else {
|
|
350
|
+
if (!LEGEND_POSITIONS.includes(args.position)) throw new Error("Legend block position requires an anchor or inherit.");
|
|
351
|
+
patch.position = args.position;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
329
354
|
if (Object.hasOwn(args, "title")) {
|
|
330
355
|
if (typeof args.title !== "string") {
|
|
331
356
|
throw new TypeError("editLegendBlock title must be a string.");
|
|
@@ -341,26 +366,21 @@ function normalizeBlockPatch(args, descriptor) {
|
|
|
341
366
|
patch.symbol = normalizeSymbolPatch(args.symbol, descriptor);
|
|
342
367
|
}
|
|
343
368
|
if (Object.hasOwn(args, "labelMap")) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
369
|
+
// Validate eligibility even for a reset, without introducing an empty map owner.
|
|
370
|
+
const map = normalizeBlockLabelMap(descriptor, args.labelMap === "auto" ? [] : args.labelMap);
|
|
347
371
|
if (args.labelMap === "auto") patch.removeLabelMap = true;
|
|
348
|
-
else
|
|
349
|
-
patch.labelMap = normalizeDisplayLabelMap(
|
|
350
|
-
args.labelMap,
|
|
351
|
-
"editLegendBlock labelMap"
|
|
352
|
-
);
|
|
353
|
-
}
|
|
372
|
+
else patch.labelMap = map;
|
|
354
373
|
}
|
|
355
374
|
return patch;
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
export const editLegendBlock = /* @__PURE__ */ action(
|
|
359
378
|
{
|
|
360
|
-
op: "editLegendBlock",
|
|
379
|
+
op: "editLegendBlock", scope: "any",
|
|
361
380
|
description: "Edit one logical legend block selected by channel."
|
|
362
381
|
},
|
|
363
382
|
withGuideLayoutValidation(function (args = {}) {
|
|
383
|
+
if (this.compositionSpec !== undefined) return editFacetParentLegend(this, "editLegendBlock", args);
|
|
364
384
|
validateOptionObject(args, OPTIONS, "editLegendBlock");
|
|
365
385
|
if (!Object.hasOwn(args, "target") || typeof args.target !== "string") {
|
|
366
386
|
throw new TypeError("editLegendBlock target must be an explicit legend target id.");
|
|
@@ -377,7 +397,9 @@ export const editLegendBlock = /* @__PURE__ */ action(
|
|
|
377
397
|
throw new Error("editLegendBlock values and count require a sampled legend block.");
|
|
378
398
|
}
|
|
379
399
|
const patch = normalizeBlockPatch(args, descriptor);
|
|
380
|
-
let config =
|
|
400
|
+
let config = Object.hasOwn(args, "clipHeight")
|
|
401
|
+
? patchLegendClipHeight(descriptor.config, args.clipHeight) : descriptor.config;
|
|
402
|
+
validateLegendClip(descriptor.kind, config);
|
|
381
403
|
const blockOverrides = patchLegendBlockOverride(config, descriptor.key, patch);
|
|
382
404
|
if (blockOverrides === undefined) {
|
|
383
405
|
const { blockOverrides: _removed, ...rest } = config;
|
|
@@ -423,6 +445,8 @@ export const editLegendBlock = /* @__PURE__ */ action(
|
|
|
423
445
|
}
|
|
424
446
|
next = reconcileLegendBlockTitleGraphic(next, descriptor.kind,
|
|
425
447
|
effective.titleVisible !== false);
|
|
426
|
-
return
|
|
448
|
+
return Object.hasOwn(args, "position")
|
|
449
|
+
? next.editLegend({target: args.target, channel: args.channel, position: args.position})
|
|
450
|
+
: next.rematerializeLegend();
|
|
427
451
|
})
|
|
428
452
|
);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {resolveColorSizeConfig,createColorSizeLegendFromConfig,materializeColorSizeLegend} from "../colorSize.js";
|
|
2
|
+
import {resetLegendClipGraphics,applyLegendClipGraphics,restoreLegendClipContext}from"../../../../materialization/legendClip.js";
|
|
3
|
+
import {validateLegendCombine,resolveSizeShapeCreation} from "../combine.js";
|
|
1
4
|
import { recordLegendOffsetIntent } from "../group.js";
|
|
2
5
|
import { prepareLegendGroup, creationLegendTarget, validateLegendGroupOptions } from "../group.js";
|
|
3
6
|
import { PLOT_LEGEND_POSITIONS } from "../../../../layout/legend.js";
|
|
@@ -37,7 +40,7 @@ import {
|
|
|
37
40
|
} from "../blocks.js";
|
|
38
41
|
|
|
39
42
|
function finishLegend(program) {
|
|
40
|
-
return hasLegendLane(program) || Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0
|
|
43
|
+
return Object.values(program.guideConfigs.legend??{}).some(config=>config.clipHeight!==undefined)||hasLegendLane(program) || Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0
|
|
41
44
|
? program.rematerializeLegend() : program;
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -59,7 +62,8 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
59
62
|
{ op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
|
|
60
63
|
withGuideLayoutValidation(function (args = {}) {
|
|
61
64
|
noOptions(args, "rematerializeLegend");
|
|
62
|
-
|
|
65
|
+
const oldClipContext=this.context.deferLegendClip;
|
|
66
|
+
let next = resetLegendClipGraphics(this)._withContext({deferLegendClip:true});
|
|
63
67
|
const categoricalKinds = ["series", "color", "stroke"].filter(
|
|
64
68
|
kind => this.guideConfigs.legend?.[kind] !== undefined
|
|
65
69
|
);
|
|
@@ -85,6 +89,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
85
89
|
!sameValues(stored.domain, definition.domain) ||
|
|
86
90
|
!sameValues(stored.scales, definition.scales) ||
|
|
87
91
|
stored.field !== definition.field ||
|
|
92
|
+
!sameValues(stored.sizeAreas ?? [], definition.sizeAreas ?? []) ||
|
|
88
93
|
stored.title !== definition.title;
|
|
89
94
|
next = changed
|
|
90
95
|
? next._withLegendConfig(kind, {
|
|
@@ -93,6 +98,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
93
98
|
channels: definition.channels,
|
|
94
99
|
scales: definition.scales,
|
|
95
100
|
field: definition.field,
|
|
101
|
+
...(definition.combine === undefined ? {} : {combine: definition.combine, sizeAreas: definition.sizeAreas}),
|
|
96
102
|
title: definition.title,
|
|
97
103
|
domain: definition.domain
|
|
98
104
|
})
|
|
@@ -152,6 +158,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
152
158
|
).some(highlight => highlight.target === config.target);
|
|
153
159
|
if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
|
|
154
160
|
}
|
|
161
|
+
if(next.guideConfigs.legend?.colorSize !== undefined) next=materializeColorSizeLegend(next,next.guideConfigs.legend.colorSize);
|
|
155
162
|
for (const policy of legendResourcePolicies()) {
|
|
156
163
|
if (
|
|
157
164
|
policy.rematerializeOp !== undefined &&
|
|
@@ -167,6 +174,10 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
167
174
|
next = next[policy.rematerializeOp]();
|
|
168
175
|
}
|
|
169
176
|
}
|
|
177
|
+
const clipAnchors = Object.fromEntries(categoricalKinds.filter(kind => next.guideConfigs.legend[kind].clipHeight !== undefined)
|
|
178
|
+
.map(kind => [kind, resolveLayout(next, resolveEffectiveLegendBlockConfig(next, kind, next.guideConfigs.legend[kind])).itemY]));
|
|
179
|
+
next=applyLegendClipGraphics(next,{force:true,anchors:clipAnchors});
|
|
180
|
+
next=restoreLegendClipContext(next,oldClipContext);
|
|
170
181
|
if (hasHorizontalLegendLane(next)) {
|
|
171
182
|
next = next.rematerializeHorizontalLegendLane();
|
|
172
183
|
}
|
|
@@ -210,6 +221,7 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
210
221
|
titleStyle: options.titleStyle,
|
|
211
222
|
itemGap: options.itemGap,
|
|
212
223
|
layout: options.layout,
|
|
224
|
+
...(options.clipHeight === undefined ? {} : {clipHeight:options.clipHeight}),
|
|
213
225
|
...(options.overflow === undefined ? {} : { overflow: options.overflow }),
|
|
214
226
|
border: options.border
|
|
215
227
|
};
|
|
@@ -231,7 +243,18 @@ export const createCategoricalLegend = /* @__PURE__ */ action(
|
|
|
231
243
|
})
|
|
232
244
|
);
|
|
233
245
|
|
|
234
|
-
function resolveRawLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
|
|
246
|
+
function resolveRawLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
|
|
247
|
+
if (Object.hasOwn(args, "combine")) {
|
|
248
|
+
validateLegendCombine(args.combine);
|
|
249
|
+
if (args.combine === "color-size") {
|
|
250
|
+
const config=resolveColorSizeConfig(program,args);
|
|
251
|
+
const prepared=prepareLegendGroup(program,{...args,blockLayout:args.blockLayout??(["left","right"].includes(args.position??"right")?"column":"row")},config.target,{creation:true,nativeOffset:30});
|
|
252
|
+
return {steps:[{op:"createColorSizeLegend",args}],finish:"always",legendGroup:{target:config.target,config:prepared.program.guideConfigs.legendGroups[config.target]}};
|
|
253
|
+
}
|
|
254
|
+
if (args.combine === "size-shape") return resolveSizeShapeCreation(program, args, layers, resolveRawLegendCreationPlan, internalChannels);
|
|
255
|
+
const {combine: _combine, ...separate} = args;
|
|
256
|
+
return resolveRawLegendCreationPlan(program, separate, layers);
|
|
257
|
+
}
|
|
235
258
|
validateOptionObject(args, undefined, "createLegend");
|
|
236
259
|
const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
|
|
237
260
|
const channels = args.channels;
|
|
@@ -403,7 +426,7 @@ function resolveRawLegendCreationPlan(program, args = {}, layers = program.seman
|
|
|
403
426
|
return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
|
|
404
427
|
}
|
|
405
428
|
|
|
406
|
-
export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
|
|
429
|
+
export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
|
|
407
430
|
validateLegendGroupOptions(args);
|
|
408
431
|
const { blockLayout: _layout, at: _at, ...raw } = args;
|
|
409
432
|
const groups = program.guideConfigs.legendGroups ?? {};
|
|
@@ -418,7 +441,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
418
441
|
}
|
|
419
442
|
if (raw.position === undefined && args.direction === "horizontal" &&
|
|
420
443
|
(args.at !== undefined || Object.keys(groups).length > 0)) raw.position = "top";
|
|
421
|
-
const initial = resolveRawLegendCreationPlan(program, raw, layers);
|
|
444
|
+
const initial = resolveRawLegendCreationPlan(program, raw, layers, internalChannels);
|
|
422
445
|
const inherited = Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0;
|
|
423
446
|
const corner = PLOT_LEGEND_POSITIONS.includes(args.position) &&
|
|
424
447
|
(initial.steps.length > 1 || initial.steps[0]?.op !== "createCategoricalLegend" ||
|
|
@@ -434,7 +457,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
434
457
|
if (prepared.options.position === undefined && raw.position !== undefined) {
|
|
435
458
|
prepared.options.position = raw.position;
|
|
436
459
|
}
|
|
437
|
-
return { ...resolveRawLegendCreationPlan(prepared.program, prepared.options, layers),
|
|
460
|
+
return { ...resolveRawLegendCreationPlan(prepared.program, prepared.options, layers, internalChannels),
|
|
438
461
|
legendGroup: { target, config: prepared.program.guideConfigs.legendGroups[target] } };
|
|
439
462
|
}
|
|
440
463
|
|
|
@@ -449,7 +472,7 @@ export function applyLegendCreationPlan(program, plan) {
|
|
|
449
472
|
}
|
|
450
473
|
// The plan already validated that a retained size block is compatible.
|
|
451
474
|
if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
|
|
452
|
-
next = next[step.op](step.args);
|
|
475
|
+
next = step.op === "createColorSizeLegend" ? createColorSizeLegendFromConfig(next,resolveColorSizeConfig(next,step.args)) : next[step.op](step.args);
|
|
453
476
|
}
|
|
454
477
|
}
|
|
455
478
|
return plan.finish === "always" ? next.rematerializeLegend()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { materializeLegendBorder } from "../../../../materialization/legendBorder.js";
|
|
1
2
|
import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
|
|
2
3
|
import { materializeLegendTitleLines } from "../../../../materialization/legendText.js";
|
|
3
4
|
import { action } from "../../../../core/action.js";
|
|
@@ -122,19 +123,11 @@ export const rematerializeLegendBackground = /* @__PURE__ */ action(
|
|
|
122
123
|
throw new Error("rematerializeLegendBackground requires border configuration.");
|
|
123
124
|
}
|
|
124
125
|
const id = graphic(config, "Background");
|
|
125
|
-
if (this.graphicSpec.objects[id]?.type
|
|
126
|
+
if (!["rect", "path"].includes(this.graphicSpec.objects[id]?.type)) {
|
|
126
127
|
throw new Error("rematerializeLegendBackground requires an existing background.");
|
|
127
128
|
}
|
|
128
129
|
const layout = resolveLayout(this, config).background;
|
|
129
|
-
return
|
|
130
|
-
x: layout.x,
|
|
131
|
-
y: layout.y,
|
|
132
|
-
width: layout.width,
|
|
133
|
-
height: layout.height,
|
|
134
|
-
fill: config.border.background,
|
|
135
|
-
stroke: config.border.color,
|
|
136
|
-
strokeWidth: config.border.lineWidth
|
|
137
|
-
});
|
|
130
|
+
return materializeLegendBorder(this, id, layout, config.border);
|
|
138
131
|
}
|
|
139
132
|
);
|
|
140
133
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import{hasLegendClipGraphics}from"../../../../materialization/legendClip.js";
|
|
1
2
|
import { action } from "../../../../core/action.js";
|
|
2
3
|
import { editLegend } from "../edit.js";
|
|
3
4
|
import { resolveStoredSelection } from
|
|
@@ -69,6 +70,7 @@ function createLegendHighlightAction() {
|
|
|
69
70
|
},
|
|
70
71
|
function (args = {}) {
|
|
71
72
|
noOptions(args, "rematerializeLegendHighlights");
|
|
73
|
+
if(hasLegendClipGraphics(this))return this.rematerializeLegend();
|
|
72
74
|
if (this.context.deferGuideLayoutValidation !== true &&
|
|
73
75
|
Object.values(this.guideConfigs.legend ?? {}).some(config => ["left", "right"].includes(config.position))) {
|
|
74
76
|
return this.rematerializeLegend();
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import{validateLegendClip,clippedSampleBounds}from"../../../../grammar/legendClip.js";
|
|
2
|
+
import {categoricalLegendSymbolId as symbolGraphic}from"../../../../grammar/legendBlocks.js";
|
|
3
|
+
export {symbolGraphic};
|
|
1
4
|
import { findLayer } from "../../../../selectors/layers.js";
|
|
2
5
|
import { isLinearGradientPaint } from "../../../../grammar/paint.js";
|
|
3
6
|
import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
|
|
@@ -60,14 +63,6 @@ function prefix(config) {
|
|
|
60
63
|
return `${config.kind}Legend`;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
export function symbolGraphic(config, type) {
|
|
64
|
-
const onlyDefault = config.symbol.layers.length === 1 &&
|
|
65
|
-
((config.kind === "series" && type === "line") ||
|
|
66
|
-
(["color", "stroke"].includes(config.kind) && type === "swatch"));
|
|
67
|
-
if (onlyDefault) return `${prefix(config)}Symbols`;
|
|
68
|
-
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
|
|
69
|
-
return `${prefix(config)}Symbol${suffix}`;
|
|
70
|
-
}
|
|
71
66
|
|
|
72
67
|
export function graphic(config, component) {
|
|
73
68
|
return `${prefix(config)}${component}`;
|
|
@@ -76,7 +71,7 @@ export function graphic(config, component) {
|
|
|
76
71
|
export function symbolWidth(config) {
|
|
77
72
|
return Math.max(...config.symbol.layers.map(layer => {
|
|
78
73
|
if (layer.type === "line") return layer.length;
|
|
79
|
-
if (layer.type === "point") return layer.size * 2;
|
|
74
|
+
if (layer.type === "point") return config.combine === "size-shape" ? 2 * Math.sqrt(Math.max(...config.sizeAreas) / Math.PI) : layer.size * 2;
|
|
80
75
|
return layer.width;
|
|
81
76
|
}));
|
|
82
77
|
}
|
|
@@ -123,7 +118,7 @@ function resolveSampleBounds(program, config, width) {
|
|
|
123
118
|
}
|
|
124
119
|
if (config.channels.includes("shape")) {
|
|
125
120
|
return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
|
|
126
|
-
area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
|
|
121
|
+
area: appearance.areas?.[index] ?? Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
|
|
127
122
|
stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
|
|
128
123
|
...requestedStrokeDetails(layer, "Legend point symbol") });
|
|
129
124
|
}
|
|
@@ -134,7 +129,7 @@ function resolveSampleBounds(program, config, width) {
|
|
|
134
129
|
strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
|
|
135
130
|
...requestedStrokeDetails(layer, "Legend point symbol") } };
|
|
136
131
|
});
|
|
137
|
-
return items.map((item, itemIndex) => {
|
|
132
|
+
return clippedSampleBounds(items.map((item, itemIndex) => {
|
|
138
133
|
const bounds = resolveConcreteGraphicBounds({
|
|
139
134
|
objects: { sample: item },
|
|
140
135
|
order: ["sample"]
|
|
@@ -153,11 +148,12 @@ function resolveSampleBounds(program, config, width) {
|
|
|
153
148
|
right: Math.max(result.right, bounds.right),
|
|
154
149
|
top: Math.min(result.top, bounds.top),
|
|
155
150
|
bottom: Math.max(result.bottom, bounds.bottom)
|
|
156
|
-
}, undefined);
|
|
151
|
+
}, undefined),config.clipHeight);
|
|
157
152
|
});
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
export function resolveLayout(program, config) {
|
|
156
|
+
validateLegendClip(config.kind,config);
|
|
161
157
|
config = intrinsicLegendConfig(program, config);
|
|
162
158
|
config = categoricalLegendView(config);
|
|
163
159
|
const { plot, canvas } = resolveContinuousBounds(program,
|
|
@@ -192,7 +188,7 @@ export function resolveAppearance(program, config) {
|
|
|
192
188
|
if (config.channels[index] === "strokeDash") dashes = values;
|
|
193
189
|
if (config.channels[index] === "shape") shapes = values;
|
|
194
190
|
}
|
|
195
|
-
return { colors, strokes, dashes, shapes,
|
|
191
|
+
return { colors, strokes, dashes, shapes, ...(config.combine === "size-shape" ? {areas: config.sizeAreas} : {}),
|
|
196
192
|
...(gradientFallback ? { opacity: program.markConfigs[target.id].opacity } : {}) };
|
|
197
193
|
}
|
|
198
194
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import{normalizeLegendClipHeight}from"../../../../grammar/legendClip.js";
|
|
2
|
+
import { normalizeLegendBorderDash } from "../../../../grammar/legendBorder.js";
|
|
1
3
|
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../../../../grammar/legendTitleStyle.js";
|
|
2
4
|
import { PLOT_LEGEND_POSITIONS, isPlotLegend } from "../../../../layout/legend.js";
|
|
3
5
|
import { validateOptionObject } from "../../../../core/validation.js";
|
|
@@ -16,6 +18,7 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_LEGEND_ITEM_GAP }
|
|
|
16
18
|
|
|
17
19
|
export const CHANNELS = CATEGORICAL_LEGEND_CHANNELS;
|
|
18
20
|
const OPTIONS = Object.freeze([
|
|
21
|
+
"clipHeight",
|
|
19
22
|
"target",
|
|
20
23
|
"channels",
|
|
21
24
|
"order",
|
|
@@ -59,7 +62,8 @@ const BORDER_OPTIONS = Object.freeze([
|
|
|
59
62
|
"color",
|
|
60
63
|
"lineWidth",
|
|
61
64
|
"padding",
|
|
62
|
-
"background"
|
|
65
|
+
"background",
|
|
66
|
+
"strokeDash"
|
|
63
67
|
]);
|
|
64
68
|
const COMMON_DEFAULTS = Object.freeze({
|
|
65
69
|
labels: Object.freeze({
|
|
@@ -95,7 +99,8 @@ function normalizeBorder(border) {
|
|
|
95
99
|
nonEmptyString(normalized.background, "Legend border background");
|
|
96
100
|
nonNegative(normalized.lineWidth, "Legend border lineWidth");
|
|
97
101
|
nonNegative(normalized.padding, "Legend border padding");
|
|
98
|
-
|
|
102
|
+
const dash = normalizeLegendBorderDash(normalized.strokeDash);
|
|
103
|
+
return { ...normalized, ...(dash === undefined ? {} : { strokeDash: dash }) };
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
@@ -192,6 +197,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
192
197
|
|
|
193
198
|
const overflow = normalizeLegendOverflow(args.overflow);
|
|
194
199
|
return {
|
|
200
|
+
...(args.clipHeight===undefined?{}:{clipHeight:normalizeLegendClipHeight(args.clipHeight)}),
|
|
195
201
|
...(overflow === undefined ? {} : { overflow }),
|
|
196
202
|
target: args.target,
|
|
197
203
|
channels: args.channels,
|