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