ggaction 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -2
- package/knowledge/action-card.schema.json +1 -1
- 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 +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +20 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +8 -7
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +7 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {resolveColorSizeConfig,createColorSizeLegendFromConfig,materializeColorSizeLegend} from "../colorSize.js";
|
|
2
|
+
import {resetLegendClipGraphics,applyLegendClipGraphics,restoreLegendClipContext}from"../../../../materialization/legendClip.js";
|
|
3
|
+
import {validateLegendCombine,resolveSizeShapeCreation} from "../combine.js";
|
|
4
|
+
import { recordLegendOffsetIntent } from "../group.js";
|
|
5
|
+
import { prepareLegendGroup, creationLegendTarget, validateLegendGroupOptions } from "../group.js";
|
|
6
|
+
import { PLOT_LEGEND_POSITIONS } from "../../../../layout/legend.js";
|
|
7
|
+
import { normalizeInitialLegendTitle } from "../continuous/common.js";
|
|
8
|
+
import { isContinuousColorScaleType } from "../../../../grammar/scales/types.js";
|
|
1
9
|
import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
|
|
2
10
|
import { sameGuideValue } from "../../reuse.js";
|
|
3
11
|
import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
|
|
@@ -17,8 +25,7 @@ import { legendResourcePolicies } from
|
|
|
17
25
|
"../../../../materialization/guides/resources.js";
|
|
18
26
|
import {
|
|
19
27
|
hasHorizontalLegendLane,
|
|
20
|
-
hasLegendLane
|
|
21
|
-
hasMultiSideLegendLane
|
|
28
|
+
hasLegendLane
|
|
22
29
|
} from "../lane.js";
|
|
23
30
|
import {
|
|
24
31
|
resolveCurrentDefinition,
|
|
@@ -33,7 +40,8 @@ import {
|
|
|
33
40
|
} from "../blocks.js";
|
|
34
41
|
|
|
35
42
|
function finishLegend(program) {
|
|
36
|
-
return hasLegendLane(program)
|
|
43
|
+
return Object.values(program.guideConfigs.legend??{}).some(config=>config.clipHeight!==undefined)||hasLegendLane(program) || Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0
|
|
44
|
+
? program.rematerializeLegend() : program;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
function requestedCandidate(program, target, candidates) {
|
|
@@ -54,7 +62,8 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
54
62
|
{ op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
|
|
55
63
|
withGuideLayoutValidation(function (args = {}) {
|
|
56
64
|
noOptions(args, "rematerializeLegend");
|
|
57
|
-
|
|
65
|
+
const oldClipContext=this.context.deferLegendClip;
|
|
66
|
+
let next = resetLegendClipGraphics(this)._withContext({deferLegendClip:true});
|
|
58
67
|
const categoricalKinds = ["series", "color", "stroke"].filter(
|
|
59
68
|
kind => this.guideConfigs.legend?.[kind] !== undefined
|
|
60
69
|
);
|
|
@@ -80,6 +89,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
80
89
|
!sameValues(stored.domain, definition.domain) ||
|
|
81
90
|
!sameValues(stored.scales, definition.scales) ||
|
|
82
91
|
stored.field !== definition.field ||
|
|
92
|
+
!sameValues(stored.sizeAreas ?? [], definition.sizeAreas ?? []) ||
|
|
83
93
|
stored.title !== definition.title;
|
|
84
94
|
next = changed
|
|
85
95
|
? next._withLegendConfig(kind, {
|
|
@@ -88,6 +98,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
88
98
|
channels: definition.channels,
|
|
89
99
|
scales: definition.scales,
|
|
90
100
|
field: definition.field,
|
|
101
|
+
...(definition.combine === undefined ? {} : {combine: definition.combine, sizeAreas: definition.sizeAreas}),
|
|
91
102
|
title: definition.title,
|
|
92
103
|
domain: definition.domain
|
|
93
104
|
})
|
|
@@ -147,6 +158,7 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
147
158
|
).some(highlight => highlight.target === config.target);
|
|
148
159
|
if (hasHighlight) next = next.rematerializeLegendHighlights({ kind });
|
|
149
160
|
}
|
|
161
|
+
if(next.guideConfigs.legend?.colorSize !== undefined) next=materializeColorSizeLegend(next,next.guideConfigs.legend.colorSize);
|
|
150
162
|
for (const policy of legendResourcePolicies()) {
|
|
151
163
|
if (
|
|
152
164
|
policy.rematerializeOp !== undefined &&
|
|
@@ -162,9 +174,10 @@ export const rematerializeLegend = /* @__PURE__ */ action(
|
|
|
162
174
|
next = next[policy.rematerializeOp]();
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
|
-
|
|
166
|
-
next
|
|
167
|
-
}
|
|
177
|
+
const clipAnchors = Object.fromEntries(categoricalKinds.filter(kind => next.guideConfigs.legend[kind].clipHeight !== undefined)
|
|
178
|
+
.map(kind => [kind, resolveLayout(next, resolveEffectiveLegendBlockConfig(next, kind, next.guideConfigs.legend[kind])).itemY]));
|
|
179
|
+
next=applyLegendClipGraphics(next,{force:true,anchors:clipAnchors});
|
|
180
|
+
next=restoreLegendClipContext(next,oldClipContext);
|
|
168
181
|
if (hasHorizontalLegendLane(next)) {
|
|
169
182
|
next = next.rematerializeHorizontalLegendLane();
|
|
170
183
|
}
|
|
@@ -178,7 +191,7 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
178
191
|
program,
|
|
179
192
|
layer,
|
|
180
193
|
args.channels,
|
|
181
|
-
args.title,
|
|
194
|
+
args.title === false ? undefined : args.title,
|
|
182
195
|
args.order === undefined ? undefined : normalizeLegendOrder(args.order)
|
|
183
196
|
);
|
|
184
197
|
const inferredSymbol = args.symbol === undefined || args.symbol === "auto";
|
|
@@ -194,7 +207,8 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
194
207
|
const config = {
|
|
195
208
|
target: layer.id,
|
|
196
209
|
...definition,
|
|
197
|
-
|
|
210
|
+
...normalizeInitialLegendTitle(args.title, definition.title),
|
|
211
|
+
inferredTitle: args.title === false || !Object.hasOwn(args, "title"),
|
|
198
212
|
inferredSymbol,
|
|
199
213
|
position: options.position,
|
|
200
214
|
align: options.align,
|
|
@@ -207,8 +221,9 @@ export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
|
207
221
|
titleStyle: options.titleStyle,
|
|
208
222
|
itemGap: options.itemGap,
|
|
209
223
|
layout: options.layout,
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
...(options.clipHeight === undefined ? {} : {clipHeight:options.clipHeight}),
|
|
225
|
+
...(options.overflow === undefined ? {} : { overflow: options.overflow }),
|
|
226
|
+
border: options.border
|
|
212
227
|
};
|
|
213
228
|
return config;
|
|
214
229
|
}
|
|
@@ -228,7 +243,18 @@ export const createCategoricalLegend = /* @__PURE__ */ action(
|
|
|
228
243
|
})
|
|
229
244
|
);
|
|
230
245
|
|
|
231
|
-
|
|
246
|
+
function resolveRawLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
|
|
247
|
+
if (Object.hasOwn(args, "combine")) {
|
|
248
|
+
validateLegendCombine(args.combine);
|
|
249
|
+
if (args.combine === "color-size") {
|
|
250
|
+
const config=resolveColorSizeConfig(program,args);
|
|
251
|
+
const prepared=prepareLegendGroup(program,{...args,blockLayout:args.blockLayout??(["left","right"].includes(args.position??"right")?"column":"row")},config.target,{creation:true,nativeOffset:30});
|
|
252
|
+
return {steps:[{op:"createColorSizeLegend",args}],finish:"always",legendGroup:{target:config.target,config:prepared.program.guideConfigs.legendGroups[config.target]}};
|
|
253
|
+
}
|
|
254
|
+
if (args.combine === "size-shape") return resolveSizeShapeCreation(program, args, layers, resolveRawLegendCreationPlan, internalChannels);
|
|
255
|
+
const {combine: _combine, ...separate} = args;
|
|
256
|
+
return resolveRawLegendCreationPlan(program, separate, layers);
|
|
257
|
+
}
|
|
232
258
|
validateOptionObject(args, undefined, "createLegend");
|
|
233
259
|
const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
|
|
234
260
|
const channels = args.channels;
|
|
@@ -252,15 +278,23 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
280
|
const continuousColorCandidates = candidates.filter(layer => {
|
|
255
|
-
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
|
|
281
|
+
const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type)
|
|
256
282
|
? layer.encoding?.color
|
|
257
283
|
: undefined;
|
|
258
284
|
const scale = findSemanticScale(program, encoding?.scale);
|
|
259
|
-
return scale?.type
|
|
285
|
+
return isContinuousColorScaleType(scale?.type);
|
|
260
286
|
});
|
|
261
287
|
const continuousColor = requestedCandidate(
|
|
262
288
|
program, args.target, continuousColorCandidates
|
|
263
289
|
);
|
|
290
|
+
if (args.representation !== undefined && channels !== undefined && channels.length > 1) {
|
|
291
|
+
if (!continuousColor || !channels.includes("color")) throw new Error("Legend representation requires selected continuous color.");
|
|
292
|
+
const { representation, values, count, symbol, gradient, ...common } = args;
|
|
293
|
+
const steps = channels.flatMap(channel => resolveRawLegendCreationPlan(program,
|
|
294
|
+
channel === "color" ? { ...args, channels: [channel] }
|
|
295
|
+
: { ...common, channels: [channel] }, layers).steps);
|
|
296
|
+
return { steps, finish: "always" };
|
|
297
|
+
}
|
|
264
298
|
if (
|
|
265
299
|
(channels?.length === 1 && channels[0] === "color" && continuousColor) ||
|
|
266
300
|
(channels === undefined && continuousColor)
|
|
@@ -268,7 +302,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
268
302
|
return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
|
|
269
303
|
}
|
|
270
304
|
const intervalColorCandidates = candidates.filter(layer => {
|
|
271
|
-
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
|
|
305
|
+
const encoding = ["point", "bar", "rect", "text"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
|
|
272
306
|
const scale = findSemanticScale(program, encoding?.scale);
|
|
273
307
|
return ["quantize", "quantile", "threshold"].includes(scale?.type);
|
|
274
308
|
});
|
|
@@ -281,7 +315,7 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
281
315
|
}
|
|
282
316
|
const continuousStrokeCandidates = candidates.filter(layer => {
|
|
283
317
|
const scale = findSemanticScale(program, layer.encoding?.stroke?.scale);
|
|
284
|
-
return scale?.type
|
|
318
|
+
return isContinuousColorScaleType(scale?.type);
|
|
285
319
|
});
|
|
286
320
|
const continuousStroke = requestedCandidate(
|
|
287
321
|
program,
|
|
@@ -365,6 +399,9 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
365
399
|
if (combined) steps.push({ op: "createSizeLegend", args: {
|
|
366
400
|
target: requestedPoint.id,
|
|
367
401
|
...(count === undefined ? {} : { count }),
|
|
402
|
+
...(categoricalArgs.overflow !== undefined &&
|
|
403
|
+
findSemanticScale(program, requestedPoint.encoding.size.scale)?.type === "ordinal"
|
|
404
|
+
? { overflow: categoricalArgs.overflow } : {}),
|
|
368
405
|
...(categoricalArgs.labels === undefined ? {} : { labels: {
|
|
369
406
|
...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
|
|
370
407
|
...(sizeFormat === undefined ? {} : { format: sizeFormat })
|
|
@@ -389,14 +426,53 @@ export function resolveLegendCreationPlan(program, args = {}, layers = program.s
|
|
|
389
426
|
return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
|
|
390
427
|
}
|
|
391
428
|
|
|
429
|
+
export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers, internalChannels) {
|
|
430
|
+
validateLegendGroupOptions(args);
|
|
431
|
+
const { blockLayout: _layout, at: _at, ...raw } = args;
|
|
432
|
+
const groups = program.guideConfigs.legendGroups ?? {};
|
|
433
|
+
if (Object.keys(groups).length > 0 && raw.position === undefined) {
|
|
434
|
+
const target = creationLegendTarget(program, args);
|
|
435
|
+
const placement = groups[target]?.placement;
|
|
436
|
+
if (placement?.position !== undefined) {
|
|
437
|
+
raw.position = placement.position;
|
|
438
|
+
raw.align = args.align ?? placement.align;
|
|
439
|
+
raw.offset = args.offset ?? placement.offset;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (raw.position === undefined && args.direction === "horizontal" &&
|
|
443
|
+
(args.at !== undefined || Object.keys(groups).length > 0)) raw.position = "top";
|
|
444
|
+
const initial = resolveRawLegendCreationPlan(program, raw, layers, internalChannels);
|
|
445
|
+
const inherited = Object.keys(program.guideConfigs.legendGroups ?? {}).length > 0;
|
|
446
|
+
const corner = PLOT_LEGEND_POSITIONS.includes(args.position) &&
|
|
447
|
+
(initial.steps.length > 1 || initial.steps[0]?.op !== "createCategoricalLegend" ||
|
|
448
|
+
Object.keys(program.guideConfigs.legend ?? {}).length > 0);
|
|
449
|
+
if (args.blockLayout === undefined && args.at === undefined && !inherited && !corner) return initial;
|
|
450
|
+
const target = creationLegendTarget(program, args);
|
|
451
|
+
const first = initial.steps[0];
|
|
452
|
+
const nativeOffset = program.guideConfigs.legendGroups?.[target] !== undefined ? 30 : first?.op === "createCategoricalLegend"
|
|
453
|
+
? resolveCategoricalLegendConfig(program, first.args).offset
|
|
454
|
+
: first?.op === "createSizeLegend" ? resolveSizeLegendConfig(program, first.args).offset : 30;
|
|
455
|
+
const prepared = prepareLegendGroup(program, args, target, { creation: true, corner, nativeOffset });
|
|
456
|
+
if (!prepared.grouped) return initial;
|
|
457
|
+
if (prepared.options.position === undefined && raw.position !== undefined) {
|
|
458
|
+
prepared.options.position = raw.position;
|
|
459
|
+
}
|
|
460
|
+
return { ...resolveRawLegendCreationPlan(prepared.program, prepared.options, layers, internalChannels),
|
|
461
|
+
legendGroup: { target, config: prepared.program.guideConfigs.legendGroups[target] } };
|
|
462
|
+
}
|
|
463
|
+
|
|
392
464
|
export function applyLegendCreationPlan(program, plan) {
|
|
393
465
|
if (plan.steps.length === 0) return program;
|
|
394
|
-
let next = program
|
|
466
|
+
let next = plan.legendGroup === undefined ? program : program._withMaterializationConfig(
|
|
467
|
+
["guides", "legendGroups", plan.legendGroup.target], plan.legendGroup.config);
|
|
395
468
|
for (const step of plan.steps) {
|
|
396
469
|
const existingSize = next.guideConfigs.legend?.size;
|
|
470
|
+
if (plan.combined && existingSize !== undefined && step.op === "createCategoricalLegend") {
|
|
471
|
+
next = next._withLegendConfig("size", { ...existingSize, inheritPosition: true });
|
|
472
|
+
}
|
|
397
473
|
// The plan already validated that a retained size block is compatible.
|
|
398
474
|
if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
|
|
399
|
-
next = next[step.op](step.args);
|
|
475
|
+
next = step.op === "createColorSizeLegend" ? createColorSizeLegendFromConfig(next,resolveColorSizeConfig(next,step.args)) : next[step.op](step.args);
|
|
400
476
|
}
|
|
401
477
|
}
|
|
402
478
|
return plan.finish === "always" ? next.rematerializeLegend()
|
|
@@ -406,7 +482,7 @@ export function applyLegendCreationPlan(program, plan) {
|
|
|
406
482
|
export const createLegend = /* @__PURE__ */ action(
|
|
407
483
|
{ op: "createLegend", description: "Create an inferred legend for selected channels." },
|
|
408
484
|
withGuideLayoutValidation(function (args = {}) {
|
|
409
|
-
return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
|
|
485
|
+
return recordLegendOffsetIntent(this, applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args)), args, { creation: true });
|
|
410
486
|
})
|
|
411
487
|
);
|
|
412
488
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { materializeLegendBorder } from "../../../../materialization/legendBorder.js";
|
|
1
2
|
import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
|
|
3
|
+
import { materializeLegendTitleLines } from "../../../../materialization/legendText.js";
|
|
2
4
|
import { action } from "../../../../core/action.js";
|
|
3
5
|
import {
|
|
4
6
|
activeConfig,
|
|
@@ -19,15 +21,17 @@ export const rematerializeLegendLabels = /* @__PURE__ */ action(
|
|
|
19
21
|
throw new Error("rematerializeLegendLabels requires existing legend labels.");
|
|
20
22
|
}
|
|
21
23
|
const layout = resolveLayout(this, config);
|
|
24
|
+
const labels = categoricalLegendLabels(config);
|
|
25
|
+
const visible = labels.flatMap((text, index) => text === "" ? [] : [index]);
|
|
22
26
|
return editGraphicProperties(editGraphicProperties(this, id, {
|
|
23
|
-
length:
|
|
24
|
-
x: layout.labelX,
|
|
25
|
-
y: layout.itemY
|
|
27
|
+
length: visible.length,
|
|
28
|
+
x: visible.map(index => layout.labelX[index]),
|
|
29
|
+
y: visible.map(index => layout.itemY[index])
|
|
26
30
|
})
|
|
27
31
|
.editGraphics({
|
|
28
32
|
target: id,
|
|
29
33
|
property: "text",
|
|
30
|
-
value:
|
|
34
|
+
value: visible.map(index => labels[index])
|
|
31
35
|
}), id, {
|
|
32
36
|
fill: config.labels.color,
|
|
33
37
|
fontSize: config.labels.fontSize,
|
|
@@ -52,7 +56,7 @@ export const createLegendLabels = /* @__PURE__ */ action(
|
|
|
52
56
|
.createGraphics({
|
|
53
57
|
id,
|
|
54
58
|
type: "text",
|
|
55
|
-
length: config.
|
|
59
|
+
length: categoricalLegendLabels(config).length,
|
|
56
60
|
...resolveCategoricalLegendPlacement(this)
|
|
57
61
|
})
|
|
58
62
|
.rematerializeLegendLabels({ kind: config.kind });
|
|
@@ -69,7 +73,7 @@ export const rematerializeLegendTitle = /* @__PURE__ */ action(
|
|
|
69
73
|
throw new Error("rematerializeLegendTitle requires an existing legend title.");
|
|
70
74
|
}
|
|
71
75
|
const layout = resolveLayout(this, config);
|
|
72
|
-
|
|
76
|
+
const next = editGraphicProperties(this, id, {
|
|
73
77
|
x: layout.titleX,
|
|
74
78
|
y: layout.titleY,
|
|
75
79
|
text: config.title,
|
|
@@ -81,12 +85,13 @@ export const rematerializeLegendTitle = /* @__PURE__ */ action(
|
|
|
81
85
|
.editGraphics({
|
|
82
86
|
target: id,
|
|
83
87
|
property: "textAlign",
|
|
84
|
-
value: ["top", "bottom"].includes(config.position) &&
|
|
88
|
+
value: config.titleStyle.align ?? (["top", "bottom"].includes(config.position) &&
|
|
85
89
|
config.titlePosition === "top"
|
|
86
90
|
? "center"
|
|
87
|
-
: "left"
|
|
91
|
+
: "left")
|
|
88
92
|
})
|
|
89
93
|
.editGraphics({ target: id, property: "textBaseline", value: "middle" });
|
|
94
|
+
return materializeLegendTitleLines(next, id, config.title, config.titleStyle);
|
|
90
95
|
}
|
|
91
96
|
);
|
|
92
97
|
|
|
@@ -118,19 +123,11 @@ export const rematerializeLegendBackground = /* @__PURE__ */ action(
|
|
|
118
123
|
throw new Error("rematerializeLegendBackground requires border configuration.");
|
|
119
124
|
}
|
|
120
125
|
const id = graphic(config, "Background");
|
|
121
|
-
if (this.graphicSpec.objects[id]?.type
|
|
126
|
+
if (!["rect", "path"].includes(this.graphicSpec.objects[id]?.type)) {
|
|
122
127
|
throw new Error("rematerializeLegendBackground requires an existing background.");
|
|
123
128
|
}
|
|
124
129
|
const layout = resolveLayout(this, config).background;
|
|
125
|
-
return
|
|
126
|
-
x: layout.x,
|
|
127
|
-
y: layout.y,
|
|
128
|
-
width: layout.width,
|
|
129
|
-
height: layout.height,
|
|
130
|
-
fill: config.border.background,
|
|
131
|
-
stroke: config.border.color,
|
|
132
|
-
strokeWidth: config.border.lineWidth
|
|
133
|
-
});
|
|
130
|
+
return materializeLegendBorder(this, id, layout, config.border);
|
|
134
131
|
}
|
|
135
132
|
);
|
|
136
133
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import{hasLegendClipGraphics}from"../../../../materialization/legendClip.js";
|
|
1
2
|
import { action } from "../../../../core/action.js";
|
|
2
3
|
import { editLegend } from "../edit.js";
|
|
3
4
|
import { resolveStoredSelection } from
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
} from "./symbols.js";
|
|
30
31
|
import { activeConfig, noOptions, symbolGraphic } from "./layout.js";
|
|
31
32
|
import {
|
|
33
|
+
rematerializeLegendGroups,
|
|
32
34
|
rematerializeHorizontalLegendLane,
|
|
33
35
|
rematerializeSideLegendLane
|
|
34
36
|
} from "../lane.js";
|
|
@@ -68,6 +70,11 @@ function createLegendHighlightAction() {
|
|
|
68
70
|
},
|
|
69
71
|
function (args = {}) {
|
|
70
72
|
noOptions(args, "rematerializeLegendHighlights");
|
|
73
|
+
if(hasLegendClipGraphics(this))return this.rematerializeLegend();
|
|
74
|
+
if (this.context.deferGuideLayoutValidation !== true &&
|
|
75
|
+
Object.values(this.guideConfigs.legend ?? {}).some(config => ["left", "right"].includes(config.position))) {
|
|
76
|
+
return this.rematerializeLegend();
|
|
77
|
+
}
|
|
71
78
|
const kinds = args.kind === undefined
|
|
72
79
|
? ["series", "color", "stroke"].filter(
|
|
73
80
|
kind => this.guideConfigs.legend?.[kind] !== undefined
|
|
@@ -134,6 +141,7 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
|
134
141
|
ProgramClass.prototype.createLegendTitle = createLegendTitle;
|
|
135
142
|
ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
|
|
136
143
|
ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
|
|
144
|
+
ProgramClass.prototype.rematerializeLegendGroups = rematerializeLegendGroups;
|
|
137
145
|
ProgramClass.prototype.rematerializeSideLegendLane =
|
|
138
146
|
rematerializeSideLegendLane;
|
|
139
147
|
ProgramClass.prototype.rematerializeHorizontalLegendLane =
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import{validateLegendClip,clippedSampleBounds}from"../../../../grammar/legendClip.js";
|
|
2
|
+
import {categoricalLegendSymbolId as symbolGraphic}from"../../../../grammar/legendBlocks.js";
|
|
3
|
+
export {symbolGraphic};
|
|
4
|
+
import { findLayer } from "../../../../selectors/layers.js";
|
|
5
|
+
import { isLinearGradientPaint } from "../../../../grammar/paint.js";
|
|
6
|
+
import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
|
|
1
7
|
import { validateOptionObject } from "../../../../core/validation.js";
|
|
2
8
|
import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
|
|
3
9
|
import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
|
|
@@ -57,14 +63,6 @@ function prefix(config) {
|
|
|
57
63
|
return `${config.kind}Legend`;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
export function symbolGraphic(config, type) {
|
|
61
|
-
const onlyDefault = config.symbol.layers.length === 1 &&
|
|
62
|
-
((config.kind === "series" && type === "line") ||
|
|
63
|
-
(["color", "stroke"].includes(config.kind) && type === "swatch"));
|
|
64
|
-
if (onlyDefault) return `${prefix(config)}Symbols`;
|
|
65
|
-
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
|
|
66
|
-
return `${prefix(config)}Symbol${suffix}`;
|
|
67
|
-
}
|
|
68
66
|
|
|
69
67
|
export function graphic(config, component) {
|
|
70
68
|
return `${prefix(config)}${component}`;
|
|
@@ -73,17 +71,25 @@ export function graphic(config, component) {
|
|
|
73
71
|
export function symbolWidth(config) {
|
|
74
72
|
return Math.max(...config.symbol.layers.map(layer => {
|
|
75
73
|
if (layer.type === "line") return layer.length;
|
|
76
|
-
if (layer.type === "point") return layer.size * 2;
|
|
74
|
+
if (layer.type === "point") return config.combine === "size-shape" ? 2 * Math.sqrt(Math.max(...config.sizeAreas) / Math.PI) : layer.size * 2;
|
|
77
75
|
return layer.width;
|
|
78
76
|
}));
|
|
79
77
|
}
|
|
80
78
|
|
|
79
|
+
export function categoricalLegendView(config) {
|
|
80
|
+
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
|
+
hiddenCount: config.domain.length - config.overflow.maxItems };
|
|
83
|
+
}
|
|
84
|
+
|
|
81
85
|
export function categoricalLegendLabels(config) {
|
|
82
|
-
|
|
86
|
+
config = categoricalLegendView(config);
|
|
87
|
+
const labels = config.domain.map(value => resolveDisplayLabel(
|
|
83
88
|
value,
|
|
84
89
|
config.labelMap,
|
|
85
90
|
formatVisibleText
|
|
86
91
|
));
|
|
92
|
+
return config.hiddenCount > 0 ? [...labels, `…${config.hiddenCount} entries`] : labels;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
function resolveSampleBounds(program, config, width) {
|
|
@@ -96,7 +102,7 @@ function resolveSampleBounds(program, config, width) {
|
|
|
96
102
|
stroke: layer.stroke ?? (config.channels.includes("stroke")
|
|
97
103
|
? appearance.strokes[index]
|
|
98
104
|
: appearance.colors[index]),
|
|
99
|
-
strokeWidth: layer.lineWidth, opacity: layer.opacity,
|
|
105
|
+
strokeWidth: layer.lineWidth, opacity: layer.opacity ?? appearance.opacity,
|
|
100
106
|
...requestedStrokeDetails(layer, "Legend line symbol") } };
|
|
101
107
|
}
|
|
102
108
|
if (layer.type === "swatch") {
|
|
@@ -106,24 +112,24 @@ function resolveSampleBounds(program, config, width) {
|
|
|
106
112
|
stroke: config.channels.includes("stroke")
|
|
107
113
|
? appearance.strokes[index]
|
|
108
114
|
: layer.stroke,
|
|
109
|
-
opacity: layer.opacity,
|
|
115
|
+
opacity: layer.opacity ?? appearance.opacity,
|
|
110
116
|
...requestedStrokeDetails(details, "Legend swatch symbol")
|
|
111
|
-
}, details
|
|
117
|
+
}, details);
|
|
112
118
|
}
|
|
113
119
|
if (config.channels.includes("shape")) {
|
|
114
120
|
return createPointShapeGraphic({ shape: appearance.shapes[index], x: width / 2, y: 0,
|
|
115
|
-
area: Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
|
|
116
|
-
stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity,
|
|
121
|
+
area: appearance.areas?.[index] ?? Math.PI * layer.size ** 2, fill: layer.fill ?? appearance.colors[index],
|
|
122
|
+
stroke: layer.stroke, strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
|
|
117
123
|
...requestedStrokeDetails(layer, "Legend point symbol") });
|
|
118
124
|
}
|
|
119
125
|
return { type: "circle", properties: { x: width / 2, y: 0, radius: layer.size,
|
|
120
126
|
stroke: config.channels.includes("stroke")
|
|
121
127
|
? appearance.strokes[index]
|
|
122
128
|
: layer.stroke,
|
|
123
|
-
strokeWidth: layer.strokeWidth, opacity: layer.opacity,
|
|
129
|
+
strokeWidth: layer.strokeWidth, opacity: layer.opacity ?? appearance.opacity,
|
|
124
130
|
...requestedStrokeDetails(layer, "Legend point symbol") } };
|
|
125
131
|
});
|
|
126
|
-
return items.map((item, itemIndex) => {
|
|
132
|
+
return clippedSampleBounds(items.map((item, itemIndex) => {
|
|
127
133
|
const bounds = resolveConcreteGraphicBounds({
|
|
128
134
|
objects: { sample: item },
|
|
129
135
|
order: ["sample"]
|
|
@@ -142,11 +148,14 @@ function resolveSampleBounds(program, config, width) {
|
|
|
142
148
|
right: Math.max(result.right, bounds.right),
|
|
143
149
|
top: Math.min(result.top, bounds.top),
|
|
144
150
|
bottom: Math.max(result.bottom, bounds.bottom)
|
|
145
|
-
}, undefined);
|
|
151
|
+
}, undefined),config.clipHeight);
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
export function resolveLayout(program, config) {
|
|
156
|
+
validateLegendClip(config.kind,config);
|
|
157
|
+
config = intrinsicLegendConfig(program, config);
|
|
158
|
+
config = categoricalLegendView(config);
|
|
150
159
|
const { plot, canvas } = resolveContinuousBounds(program,
|
|
151
160
|
"Legend layout requires Canvas bounds, width, and height.");
|
|
152
161
|
const width = symbolWidth(config);
|
|
@@ -160,7 +169,14 @@ export function resolveLayout(program, config) {
|
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
export function resolveAppearance(program, config) {
|
|
163
|
-
|
|
172
|
+
const target = findLayer(program, config.target);
|
|
173
|
+
const gradientFallback = target?.mark?.type === "area" &&
|
|
174
|
+
target.encoding.color === undefined &&
|
|
175
|
+
!config.channels.includes("color") &&
|
|
176
|
+
isLinearGradientPaint(program.markConfigs[target.id]?.fill);
|
|
177
|
+
const fallback = gradientFallback
|
|
178
|
+
? program.markConfigs[target.id].fill : DEFAULT_COLORS.mark;
|
|
179
|
+
let colors = config.domain.map(() => fallback);
|
|
164
180
|
let strokes = config.domain.map(() => "white");
|
|
165
181
|
let dashes = config.domain.map(() => []);
|
|
166
182
|
let shapes = config.domain.map(() => "circle");
|
|
@@ -172,7 +188,8 @@ export function resolveAppearance(program, config) {
|
|
|
172
188
|
if (config.channels[index] === "strokeDash") dashes = values;
|
|
173
189
|
if (config.channels[index] === "shape") shapes = values;
|
|
174
190
|
}
|
|
175
|
-
return { colors, strokes, dashes, shapes }
|
|
191
|
+
return { colors, strokes, dashes, shapes, ...(config.combine === "size-shape" ? {areas: config.sizeAreas} : {}),
|
|
192
|
+
...(gradientFallback ? { opacity: program.markConfigs[target.id].opacity } : {}) };
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
export function noOptions(args, operation) {
|
|
@@ -189,3 +206,11 @@ export function layerFor(config, type) {
|
|
|
189
206
|
}
|
|
190
207
|
return layer;
|
|
191
208
|
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
export function resolveSymbolLayout(program, config) {
|
|
212
|
+
const view = categoricalLegendView(config);
|
|
213
|
+
const layout = resolveLayout(program, view);
|
|
214
|
+
return { ...layout, symbolX: layout.symbolX.slice(0, view.domain.length),
|
|
215
|
+
itemY: layout.itemY.slice(0, view.domain.length) };
|
|
216
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import{normalizeLegendClipHeight}from"../../../../grammar/legendClip.js";
|
|
2
|
+
import { normalizeLegendBorderDash } from "../../../../grammar/legendBorder.js";
|
|
3
|
+
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../../../../grammar/legendTitleStyle.js";
|
|
4
|
+
import { PLOT_LEGEND_POSITIONS, isPlotLegend } from "../../../../layout/legend.js";
|
|
1
5
|
import { validateOptionObject } from "../../../../core/validation.js";
|
|
2
6
|
import { CATEGORICAL_LEGEND_CHANNELS } from "../../../../core/vocabulary.js";
|
|
3
7
|
import { normalizeRecipe } from "./recipes.js";
|
|
@@ -9,11 +13,12 @@ import {
|
|
|
9
13
|
validateKeys,
|
|
10
14
|
validateObject
|
|
11
15
|
} from "./validation.js";
|
|
12
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
16
|
+
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_LEGEND_ITEM_GAP } from
|
|
13
17
|
"../../../../theme/defaults.js";
|
|
14
18
|
|
|
15
19
|
export const CHANNELS = CATEGORICAL_LEGEND_CHANNELS;
|
|
16
20
|
const OPTIONS = Object.freeze([
|
|
21
|
+
"clipHeight",
|
|
17
22
|
"target",
|
|
18
23
|
"channels",
|
|
19
24
|
"order",
|
|
@@ -29,7 +34,8 @@ const OPTIONS = Object.freeze([
|
|
|
29
34
|
"labels",
|
|
30
35
|
"titleStyle",
|
|
31
36
|
"itemGap",
|
|
32
|
-
"border"
|
|
37
|
+
"border",
|
|
38
|
+
"overflow"
|
|
33
39
|
]);
|
|
34
40
|
const TEXT_OPTIONS = Object.freeze([
|
|
35
41
|
"offset",
|
|
@@ -39,17 +45,25 @@ const TEXT_OPTIONS = Object.freeze([
|
|
|
39
45
|
"fontWeight",
|
|
40
46
|
"format"
|
|
41
47
|
]);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
export function normalizeLegendOverflow(value) {
|
|
50
|
+
if (value === undefined || value === false) return value;
|
|
51
|
+
validateObject(value, ["maxItems", "summary"], "Legend overflow");
|
|
52
|
+
if (!Number.isInteger(value.maxItems) || value.maxItems < 1) {
|
|
53
|
+
throw new RangeError("Legend overflow.maxItems must be a positive integer.");
|
|
54
|
+
}
|
|
55
|
+
if (value.summary !== undefined && value.summary !== "ellipsis-count") {
|
|
56
|
+
throw new Error("Legend overflow.summary must be ellipsis-count.");
|
|
57
|
+
}
|
|
58
|
+
return { maxItems: value.maxItems, summary: "ellipsis-count" };
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
const BORDER_OPTIONS = Object.freeze([
|
|
49
62
|
"color",
|
|
50
63
|
"lineWidth",
|
|
51
64
|
"padding",
|
|
52
|
-
"background"
|
|
65
|
+
"background",
|
|
66
|
+
"strokeDash"
|
|
53
67
|
]);
|
|
54
68
|
const COMMON_DEFAULTS = Object.freeze({
|
|
55
69
|
labels: Object.freeze({
|
|
@@ -85,7 +99,8 @@ function normalizeBorder(border) {
|
|
|
85
99
|
nonEmptyString(normalized.background, "Legend border background");
|
|
86
100
|
nonNegative(normalized.lineWidth, "Legend border lineWidth");
|
|
87
101
|
nonNegative(normalized.padding, "Legend border padding");
|
|
88
|
-
|
|
102
|
+
const dash = normalizeLegendBorderDash(normalized.strokeDash);
|
|
103
|
+
return { ...normalized, ...(dash === undefined ? {} : { strokeDash: dash }) };
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
@@ -99,7 +114,7 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
99
114
|
if (Object.hasOwn(args, "titleStyle")) {
|
|
100
115
|
validateObject(
|
|
101
116
|
args.titleStyle,
|
|
102
|
-
|
|
117
|
+
LEGEND_TITLE_STYLE_KEYS,
|
|
103
118
|
"createLegend.titleStyle"
|
|
104
119
|
);
|
|
105
120
|
}
|
|
@@ -121,13 +136,15 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
121
136
|
};
|
|
122
137
|
const position = args.position ?? defaults.position;
|
|
123
138
|
const align = args.align ?? defaults.align;
|
|
139
|
+
const inside = isPlotLegend({ position });
|
|
124
140
|
const side = ["left", "right"].includes(position);
|
|
125
|
-
const direction = args.direction ?? (side ? "vertical" : "horizontal");
|
|
141
|
+
const direction = args.direction ?? (side || inside ? "vertical" : "horizontal");
|
|
126
142
|
const columns = args.columns;
|
|
127
143
|
const offset = args.offset ?? 8;
|
|
128
144
|
const titlePosition = args.titlePosition ?? "top";
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
validateLegendTitleAlignment(titleStyle, titlePosition);
|
|
146
|
+
const itemGap = args.itemGap ?? (direction === "vertical"
|
|
147
|
+
? DEFAULT_VERTICAL_LEGEND_ITEM_GAP
|
|
131
148
|
: position === "top" ? 24 : 20);
|
|
132
149
|
const layout = args.layout === undefined ? "edge" : args.layout;
|
|
133
150
|
if (!["edge", "legacy-bottom"].includes(layout)) {
|
|
@@ -142,14 +159,14 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
142
159
|
throw new Error('Legend columns, vertical direction, left title and custom offset require layout "edge".');
|
|
143
160
|
}
|
|
144
161
|
|
|
145
|
-
if (!["right", "left", "bottom", "top"].includes(position)) {
|
|
162
|
+
if (!["right", "left", "bottom", "top", ...PLOT_LEGEND_POSITIONS].includes(position)) {
|
|
146
163
|
throw new Error(`Unsupported legend position "${position}".`);
|
|
147
164
|
}
|
|
148
165
|
if (!["left", "center", "right"].includes(align)) {
|
|
149
166
|
throw new Error(`Unsupported legend alignment "${align}".`);
|
|
150
167
|
}
|
|
151
|
-
if (
|
|
152
|
-
throw new Error("Side legends currently require center alignment.");
|
|
168
|
+
if ((side || inside) && align !== "center") {
|
|
169
|
+
throw new Error(inside ? "In-plot legends require center alignment." : "Side legends currently require center alignment.");
|
|
153
170
|
}
|
|
154
171
|
if (!["horizontal", "vertical"].includes(direction)) {
|
|
155
172
|
throw new Error(`Unsupported legend direction "${direction}".`);
|
|
@@ -157,11 +174,8 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
157
174
|
if (side && direction !== "vertical") {
|
|
158
175
|
throw new Error("Side legends require vertical direction.");
|
|
159
176
|
}
|
|
160
|
-
if (side
|
|
161
|
-
throw new Error("Side legends require
|
|
162
|
-
}
|
|
163
|
-
if (side && titlePosition !== "top") {
|
|
164
|
-
throw new Error("Side legends require a top title.");
|
|
177
|
+
if ((side || inside) && titlePosition !== "top") {
|
|
178
|
+
throw new Error(inside ? "In-plot legends require a top title." : "Side legends require a top title.");
|
|
165
179
|
}
|
|
166
180
|
if (columns !== undefined && (!Number.isInteger(columns) || columns <= 0)) {
|
|
167
181
|
throw new RangeError("Legend columns must be a positive integer.");
|
|
@@ -181,7 +195,10 @@ export function normalizeOptions(args, kind, { internalSymbol = false } = {}) {
|
|
|
181
195
|
validateFontWeight(titleStyle.fontWeight, "Legend title fontWeight");
|
|
182
196
|
positive(itemGap, "Legend itemGap");
|
|
183
197
|
|
|
198
|
+
const overflow = normalizeLegendOverflow(args.overflow);
|
|
184
199
|
return {
|
|
200
|
+
...(args.clipHeight===undefined?{}:{clipHeight:normalizeLegendClipHeight(args.clipHeight)}),
|
|
201
|
+
...(overflow === undefined ? {} : { overflow }),
|
|
185
202
|
target: args.target,
|
|
186
203
|
channels: args.channels,
|
|
187
204
|
position,
|