ggaction 0.0.11 → 0.0.13
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 +80 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -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 +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +79 -24
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +153 -11
- package/src/actions/errorBars/edit.js +365 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +138 -14
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +60 -4
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +9 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +86 -23
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,28 +1,34 @@
|
|
|
1
|
+
import { withGuideLayoutValidation } from "../../../../materialization/guides/layout.js";
|
|
2
|
+
import { sameGuideValue } from "../../reuse.js";
|
|
3
|
+
import { normalizeLegendOrder } from "../../../../grammar/categoryOrder.js";
|
|
1
4
|
import { action } from "../../../../core/action.js";
|
|
2
5
|
import { validateOptionObject } from "../../../../core/validation.js";
|
|
3
6
|
import { noOptions, resolveLayout, activeConfig } from "./layout.js";
|
|
4
7
|
import { normalizeOptions } from "./options.js";
|
|
8
|
+
import { normalizeRecipe, resolveLegendSymbol } from "./recipes.js";
|
|
9
|
+
import { validateLegendChannels } from "../target.js";
|
|
10
|
+
import { createCategoricalLegendFromConfig, reconcileCategoricalSymbols } from "../lifecycle.js";
|
|
5
11
|
import { findLayer } from "../../../../selectors/layers.js";
|
|
6
12
|
import { findSemanticScale } from "../../../../selectors/scales.js";
|
|
7
|
-
import { isSizeLegendPoint } from "../size.js";
|
|
13
|
+
import { isSizeLegendPoint, resolveSizeLegendConfig } from "../size.js";
|
|
8
14
|
import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
|
|
15
|
+
import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
|
|
9
16
|
import { legendResourcePolicies } from
|
|
10
17
|
"../../../../materialization/guides/resources.js";
|
|
11
18
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
hasHorizontalLegendLane,
|
|
20
|
+
hasLegendLane,
|
|
14
21
|
hasMultiSideLegendLane
|
|
15
22
|
} from "../lane.js";
|
|
16
23
|
import {
|
|
17
24
|
resolveCurrentDefinition,
|
|
18
25
|
resolveDefinition,
|
|
19
|
-
resolveLegendKind,
|
|
20
26
|
resolveTarget,
|
|
21
27
|
sameValues
|
|
22
28
|
} from "./resolve.js";
|
|
23
29
|
|
|
24
30
|
function finishLegend(program) {
|
|
25
|
-
return
|
|
31
|
+
return hasLegendLane(program) ? program.rematerializeLegend() : program;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
function requestedCandidate(program, target, candidates) {
|
|
@@ -33,31 +39,15 @@ function requestedCandidate(program, target, candidates) {
|
|
|
33
39
|
return candidates.includes(layer) ? layer : undefined;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (unsupportedKeys.length > 0) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
`Standalone ${label} legend does not support option "${unsupportedKeys[0]}".`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (position !== undefined && position !== "right") {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Standalone ${label} legends currently require position "right".`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return finishLegend(program[kind === "size"
|
|
51
|
-
? "createSizeLegend"
|
|
52
|
-
: "createStrokeWidthLegend"]({
|
|
53
|
-
...(target === undefined ? {} : { target }),
|
|
54
|
-
...(count === undefined ? {} : { count })
|
|
55
|
-
}));
|
|
42
|
+
function resolveStandaloneLegendStep(args, kind) {
|
|
43
|
+
if (Object.hasOwn(args, "inheritAppearance")) throw new Error("createLegend does not accept inheritAppearance.");
|
|
44
|
+
const { channels: _channels, ...options } = args;
|
|
45
|
+
return { op: kind === "size" ? "createSizeLegend" : "createStrokeWidthLegend", args: options };
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
export const rematerializeLegend = action(
|
|
59
49
|
{ op: "rematerializeLegend", description: "Rematerialize every existing legend component." },
|
|
60
|
-
function (args = {}) {
|
|
50
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
61
51
|
noOptions(args, "rematerializeLegend");
|
|
62
52
|
let next = this;
|
|
63
53
|
const hasCategorical =
|
|
@@ -66,7 +56,11 @@ export const rematerializeLegend = action(
|
|
|
66
56
|
if (hasCategorical) {
|
|
67
57
|
const { kind, config } = activeConfig(this);
|
|
68
58
|
const definition = resolveCurrentDefinition(this, config);
|
|
69
|
-
const
|
|
59
|
+
const symbol = config.inferredSymbol
|
|
60
|
+
? normalizeRecipe(resolveLegendSymbol(this, findLayer(this, config.target), definition.channels), kind)
|
|
61
|
+
: config.symbol;
|
|
62
|
+
const symbolChanged = !sameGuideValue(symbol, config.symbol);
|
|
63
|
+
const changed = symbolChanged ||
|
|
70
64
|
!sameValues(config.channels, definition.channels) ||
|
|
71
65
|
!sameValues(config.domain, definition.domain) ||
|
|
72
66
|
!sameValues(config.scales, definition.scales) ||
|
|
@@ -75,6 +69,7 @@ export const rematerializeLegend = action(
|
|
|
75
69
|
next = changed
|
|
76
70
|
? this._withLegendConfig(kind, {
|
|
77
71
|
...config,
|
|
72
|
+
symbol,
|
|
78
73
|
channels: definition.channels,
|
|
79
74
|
scales: definition.scales,
|
|
80
75
|
field: definition.field,
|
|
@@ -112,6 +107,10 @@ export const rematerializeLegend = action(
|
|
|
112
107
|
});
|
|
113
108
|
}
|
|
114
109
|
}
|
|
110
|
+
if (symbolChanged) {
|
|
111
|
+
resolveLayout(next, next.guideConfigs.legend[kind]);
|
|
112
|
+
next = reconcileCategoricalSymbols(next, config, next.guideConfigs.legend[kind]);
|
|
113
|
+
}
|
|
115
114
|
if (config.border !== false) next = next.rematerializeLegendBackground();
|
|
116
115
|
next = next
|
|
117
116
|
.rematerializeLegendSymbols()
|
|
@@ -133,251 +132,196 @@ export const rematerializeLegend = action(
|
|
|
133
132
|
if (hasMultiSideLegendLane(next)) {
|
|
134
133
|
next = next.rematerializeSideLegendLane();
|
|
135
134
|
}
|
|
136
|
-
if (
|
|
135
|
+
if (hasHorizontalLegendLane(next)) {
|
|
137
136
|
next = next.rematerializeHorizontalLegendLane();
|
|
138
137
|
}
|
|
139
138
|
return next;
|
|
140
|
-
}
|
|
139
|
+
})
|
|
141
140
|
);
|
|
142
141
|
|
|
142
|
+
export function resolveCategoricalLegendConfig(program, args = {}) {
|
|
143
|
+
const layer = resolveTarget(program, args.target);
|
|
144
|
+
const definition = resolveDefinition(
|
|
145
|
+
program,
|
|
146
|
+
layer,
|
|
147
|
+
args.channels,
|
|
148
|
+
args.title,
|
|
149
|
+
args.order === undefined ? undefined : normalizeLegendOrder(args.order)
|
|
150
|
+
);
|
|
151
|
+
const options = normalizeOptions({ ...args,
|
|
152
|
+
symbol: resolveLegendSymbol(program, layer, definition.channels, args.symbol)
|
|
153
|
+
}, definition.kind);
|
|
154
|
+
const config = {
|
|
155
|
+
target: layer.id,
|
|
156
|
+
...definition,
|
|
157
|
+
inferredTitle: !Object.hasOwn(args, "title"),
|
|
158
|
+
inferredSymbol: args.symbol === undefined || args.symbol === "auto",
|
|
159
|
+
position: options.position,
|
|
160
|
+
align: options.align,
|
|
161
|
+
direction: options.direction,
|
|
162
|
+
columns: options.columns,
|
|
163
|
+
offset: options.offset,
|
|
164
|
+
titlePosition: options.titlePosition,
|
|
165
|
+
symbol: options.symbol,
|
|
166
|
+
labels: options.labels,
|
|
167
|
+
titleStyle: options.titleStyle,
|
|
168
|
+
itemGap: options.itemGap,
|
|
169
|
+
layout: options.layout,
|
|
170
|
+
border: options.border,
|
|
171
|
+
titleVisible: true
|
|
172
|
+
};
|
|
173
|
+
return config;
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
export const createCategoricalLegend = action(
|
|
144
177
|
{ op: "createCategoricalLegend", description: "Create one categorical legend block." },
|
|
145
|
-
function (args = {}) {
|
|
146
|
-
const
|
|
147
|
-
const kind = resolveLegendKind(layer, args.channels);
|
|
148
|
-
const options = normalizeOptions(args, kind);
|
|
178
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
179
|
+
const config = resolveCategoricalLegendConfig(this, args);
|
|
149
180
|
if (
|
|
150
181
|
this.semanticSpec.guides.legend?.series !== undefined ||
|
|
151
182
|
this.semanticSpec.guides.legend?.color !== undefined
|
|
152
183
|
) {
|
|
153
184
|
throw new Error("createCategoricalLegend requires a missing legend.");
|
|
154
185
|
}
|
|
155
|
-
const definition = resolveDefinition(
|
|
156
|
-
this,
|
|
157
|
-
layer,
|
|
158
|
-
options.channels,
|
|
159
|
-
options.title
|
|
160
|
-
);
|
|
161
|
-
const config = {
|
|
162
|
-
target: layer.id,
|
|
163
|
-
...definition,
|
|
164
|
-
inferredTitle: !Object.hasOwn(args, "title"),
|
|
165
|
-
position: options.position,
|
|
166
|
-
align: options.align,
|
|
167
|
-
direction: options.direction,
|
|
168
|
-
columns: options.columns,
|
|
169
|
-
offset: options.offset,
|
|
170
|
-
titlePosition: options.titlePosition,
|
|
171
|
-
symbol: options.symbol,
|
|
172
|
-
labels: options.labels,
|
|
173
|
-
titleStyle: options.titleStyle,
|
|
174
|
-
itemGap: options.itemGap,
|
|
175
|
-
bottomGrid: options.bottomGrid,
|
|
176
|
-
border: options.border,
|
|
177
|
-
titleVisible: true
|
|
178
|
-
};
|
|
179
186
|
resolveLayout(this, config);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.editSemantic({
|
|
184
|
-
property: "guide.legend.series.channels",
|
|
185
|
-
value: definition.channels
|
|
186
|
-
})
|
|
187
|
-
.editSemantic({
|
|
188
|
-
property: "guide.legend.series.scales",
|
|
189
|
-
value: definition.scales
|
|
190
|
-
})
|
|
191
|
-
.editSemantic({
|
|
192
|
-
property: "guide.legend.series.title",
|
|
193
|
-
value: definition.title
|
|
194
|
-
});
|
|
195
|
-
} else {
|
|
196
|
-
next = next
|
|
197
|
-
.editSemantic({
|
|
198
|
-
property: "guide.legend.color.scale",
|
|
199
|
-
value: definition.scales[0]
|
|
200
|
-
})
|
|
201
|
-
.editSemantic({
|
|
202
|
-
property: "guide.legend.color.title",
|
|
203
|
-
value: definition.title
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
next = next._withLegendConfig(kind, config);
|
|
207
|
-
if (config.border !== false) next = next.createLegendBackground();
|
|
208
|
-
return next
|
|
209
|
-
.createLegendSymbols()
|
|
210
|
-
.createLegendLabels()
|
|
211
|
-
.createLegendTitle();
|
|
212
|
-
}
|
|
187
|
+
return createCategoricalLegendFromConfig(this, config,
|
|
188
|
+
args.order === undefined ? undefined : normalizeLegendOrder(args.order));
|
|
189
|
+
})
|
|
213
190
|
);
|
|
214
191
|
|
|
215
|
-
export
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
192
|
+
export function resolveLegendCreationPlan(program, args = {}, layers = program.semanticSpec.layers) {
|
|
193
|
+
validateOptionObject(args, undefined, "createLegend");
|
|
194
|
+
const candidates = args.target === undefined ? layers : layers.filter(layer => layer.id === args.target);
|
|
195
|
+
const channels = args.channels;
|
|
196
|
+
if (channels !== undefined) validateLegendChannels(channels, "createLegend");
|
|
197
|
+
const standalone = [
|
|
198
|
+
["size", isSizeLegendPoint, ["color", "shape", "strokeDash", "opacity"]],
|
|
199
|
+
["strokeWidth", isStrokeWidthLegendLayer, args.target === undefined
|
|
200
|
+
? ["color", "shape", "strokeDash", "size", "opacity"]
|
|
201
|
+
: ["color", "shape", "strokeDash", "opacity"]],
|
|
202
|
+
["opacity", isOpacityLegendLayer,
|
|
203
|
+
["color", "shape", "strokeDash", "size"]]
|
|
204
|
+
];
|
|
205
|
+
for (const [kind, eligible, otherChannels] of standalone) {
|
|
206
|
+
const explicit = channels?.length === 1 && channels[0] === kind;
|
|
207
|
+
const inferred = channels === undefined && candidates.filter(eligible).length === 1 &&
|
|
208
|
+
!candidates.some(layer => otherChannels.some(channel => layer.encoding?.[channel]?.scale !== undefined));
|
|
209
|
+
if (explicit || inferred) {
|
|
210
|
+
const step = kind === "opacity" ? { op: "createOpacityLegend", args }
|
|
211
|
+
: resolveStandaloneLegendStep(args, kind);
|
|
212
|
+
return { steps: [step], finish: "auto" };
|
|
222
213
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
|
|
214
|
+
}
|
|
215
|
+
const continuousColorCandidates = candidates.filter(layer => {
|
|
216
|
+
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
|
|
217
|
+
? layer.encoding?.color
|
|
218
|
+
: undefined;
|
|
219
|
+
const scale = findSemanticScale(program, encoding?.scale);
|
|
220
|
+
return scale?.type === "sequential";
|
|
221
|
+
});
|
|
222
|
+
const continuousColor = requestedCandidate(
|
|
223
|
+
program, args.target, continuousColorCandidates
|
|
224
|
+
);
|
|
225
|
+
if (
|
|
226
|
+
(channels?.length === 1 && channels[0] === "color" && continuousColor) ||
|
|
227
|
+
(channels === undefined && continuousColor)
|
|
228
|
+
) {
|
|
229
|
+
return { steps: [{ op: "createGradientLegend", args }], finish: "auto" };
|
|
230
|
+
}
|
|
231
|
+
const intervalColorCandidates = candidates.filter(layer => {
|
|
232
|
+
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type) ? layer.encoding?.color : undefined;
|
|
233
|
+
const scale = findSemanticScale(program, encoding?.scale);
|
|
234
|
+
return ["quantize", "quantile", "threshold"].includes(scale?.type);
|
|
235
|
+
});
|
|
236
|
+
if (
|
|
237
|
+
(channels?.length === 1 && channels[0] === "color" &&
|
|
238
|
+
intervalColorCandidates.length > 0) ||
|
|
239
|
+
(channels === undefined && intervalColorCandidates.length > 0)
|
|
240
|
+
) {
|
|
241
|
+
return { steps: [{ op: "createIntervalLegend", args }], finish: "auto" };
|
|
242
|
+
}
|
|
243
|
+
const wantsShape = channels?.includes("shape") === true;
|
|
244
|
+
const wantsSize = channels?.includes("size") === true;
|
|
245
|
+
const pointCandidates = candidates.filter(layer =>
|
|
246
|
+
layer.mark?.type === "point" &&
|
|
247
|
+
(channels === undefined
|
|
248
|
+
? ["color", "shape"].some(channel => layer.encoding?.[channel]?.scale !== undefined)
|
|
249
|
+
: (wantsShape || wantsSize) && channels.every(channel =>
|
|
250
|
+
["color", "shape", "size"].includes(channel) && layer.encoding?.[channel]?.scale !== undefined))
|
|
251
|
+
);
|
|
252
|
+
const requestedPoint = requestedCandidate(program, args.target, pointCandidates);
|
|
253
|
+
const inferredSize = channels === undefined && pointCandidates.some(isSizeLegendPoint);
|
|
254
|
+
if ((wantsSize || inferredSize) && requestedPoint === undefined) {
|
|
255
|
+
throw new Error("Combined size legend requires one eligible point mark or an explicit target.");
|
|
256
|
+
}
|
|
257
|
+
if (requestedPoint !== undefined) {
|
|
258
|
+
const { count, ...categoricalArgs } = args;
|
|
259
|
+
const combined = requestedPoint.encoding?.size?.scale !== undefined &&
|
|
260
|
+
(channels === undefined || wantsSize);
|
|
261
|
+
if (count !== undefined && !combined) {
|
|
262
|
+
throw new Error("Legend count requires a selected size legend.");
|
|
245
263
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
);
|
|
249
|
-
const requestedStrokeWidthLayer = args.target === undefined
|
|
250
|
-
? undefined
|
|
251
|
-
: findLayer(this, args.target);
|
|
252
|
-
const hasOtherStrokeWidthLegendCandidate = this.semanticSpec.layers.some(
|
|
253
|
-
layer => ["color", "shape", "strokeDash", "size", "opacity"].some(
|
|
254
|
-
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
255
|
-
)
|
|
256
|
-
);
|
|
257
|
-
const inferredStrokeWidth = channels === undefined && (
|
|
258
|
-
(args.target !== undefined &&
|
|
259
|
-
isStrokeWidthLegendLayer(requestedStrokeWidthLayer) &&
|
|
260
|
-
!hasNonSizeLegendEncoding(requestedStrokeWidthLayer)) ||
|
|
261
|
-
(args.target === undefined && strokeWidthCandidates.length === 1 &&
|
|
262
|
-
!hasOtherStrokeWidthLegendCandidate)
|
|
263
|
-
);
|
|
264
|
-
if (explicitStrokeWidth || inferredStrokeWidth) {
|
|
265
|
-
return createStandaloneLegend(this, args, "strokeWidth");
|
|
264
|
+
if (combined && categoricalArgs.layout === "legacy-bottom") {
|
|
265
|
+
throw new Error('Combined size legends require layout "edge".');
|
|
266
266
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
);
|
|
270
|
-
const hasOtherLegendCandidate = this.semanticSpec.layers.some(layer =>
|
|
271
|
-
["color", "shape", "strokeDash", "size"].some(channel =>
|
|
272
|
-
layer.encoding?.[channel]?.scale !== undefined
|
|
273
|
-
)
|
|
267
|
+
const inferredChannels = ["color", "shape"].filter(
|
|
268
|
+
channel => requestedPoint.encoding?.[channel]?.scale !== undefined
|
|
274
269
|
);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
(
|
|
278
|
-
!hasOtherLegendCandidate)
|
|
279
|
-
) {
|
|
280
|
-
return finishLegend(this.createOpacityLegend(args));
|
|
281
|
-
}
|
|
282
|
-
const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
|
|
283
|
-
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
|
|
284
|
-
? layer.encoding?.color
|
|
285
|
-
: undefined;
|
|
286
|
-
const scale = findSemanticScale(this, encoding?.scale);
|
|
287
|
-
return scale?.type === "sequential";
|
|
288
|
-
});
|
|
289
|
-
const continuousColor = requestedCandidate(
|
|
290
|
-
this, args.target, continuousColorCandidates
|
|
291
|
-
);
|
|
292
|
-
if (
|
|
293
|
-
(channels?.length === 1 && channels[0] === "color" && continuousColor) ||
|
|
294
|
-
(channels === undefined && continuousColor)
|
|
295
|
-
) {
|
|
296
|
-
return finishLegend(this.createGradientLegend(args));
|
|
270
|
+
const { format: sizeFormat, ...categoricalLabels } = categoricalArgs.labels ?? {};
|
|
271
|
+
if (!combined && sizeFormat !== undefined && sizeFormat !== "auto") {
|
|
272
|
+
throw new Error("Categorical legend labels do not accept format.");
|
|
297
273
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
requestedPoint.encoding?.size?.scale !== undefined &&
|
|
321
|
-
categoricalArgs.position !== undefined &&
|
|
322
|
-
!["right", "left"].includes(categoricalArgs.position)
|
|
323
|
-
) {
|
|
324
|
-
throw new Error(
|
|
325
|
-
"Combined point series and size legends currently require a side position."
|
|
326
|
-
);
|
|
274
|
+
const steps = [{ op: "createCategoricalLegend", args: {
|
|
275
|
+
...categoricalArgs,
|
|
276
|
+
...(categoricalArgs.labels === undefined ? {} : { labels: categoricalLabels }),
|
|
277
|
+
target: requestedPoint.id,
|
|
278
|
+
channels: channels?.filter(channel => channel !== "size") ?? inferredChannels
|
|
279
|
+
} }];
|
|
280
|
+
if (combined) steps.push({ op: "createSizeLegend", args: {
|
|
281
|
+
target: requestedPoint.id,
|
|
282
|
+
...(count === undefined ? {} : { count }),
|
|
283
|
+
...(categoricalArgs.labels === undefined ? {} : { labels: {
|
|
284
|
+
...(categoricalArgs.labels.offset === undefined ? {} : { offset: categoricalArgs.labels.offset }),
|
|
285
|
+
...(sizeFormat === undefined ? {} : { format: sizeFormat })
|
|
286
|
+
} }),
|
|
287
|
+
inheritAppearance: true
|
|
288
|
+
} });
|
|
289
|
+
// Validate both content owners before any component action starts.
|
|
290
|
+
resolveCategoricalLegendConfig(program, steps[0].args);
|
|
291
|
+
if (combined) {
|
|
292
|
+
const size = resolveSizeLegendConfig(program, steps[1].args);
|
|
293
|
+
const existing = program.guideConfigs.legend?.size;
|
|
294
|
+
if (existing !== undefined && existing.target !== size.target) {
|
|
295
|
+
throw new Error("Combined point series legend requires the active size legend to share its target.");
|
|
327
296
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
candidate.encoding?.color?.field === requestedPoint.encoding.color.field &&
|
|
331
|
-
candidate.encoding.color.scale === requestedPoint.encoding.color.scale
|
|
332
|
-
);
|
|
333
|
-
const symbol = categoricalArgs.symbol ?? {
|
|
334
|
-
layers: [
|
|
335
|
-
...(hasMatchingLine
|
|
336
|
-
? [{ type: "line", length: 32, lineWidth: 3 }]
|
|
337
|
-
: []),
|
|
338
|
-
{
|
|
339
|
-
type: "point",
|
|
340
|
-
size: Math.sqrt(64 / Math.PI),
|
|
341
|
-
stroke: "white",
|
|
342
|
-
strokeWidth: 0
|
|
343
|
-
}
|
|
344
|
-
]
|
|
345
|
-
};
|
|
346
|
-
const inferredChannels = ["color", "shape"].filter(
|
|
347
|
-
channel => requestedPoint.encoding?.[channel]?.scale !== undefined
|
|
348
|
-
);
|
|
349
|
-
let next = this.createCategoricalLegend({
|
|
350
|
-
...categoricalArgs,
|
|
351
|
-
target: requestedPoint.id,
|
|
352
|
-
channels: categoricalArgs.channels ?? inferredChannels,
|
|
353
|
-
symbol
|
|
354
|
-
});
|
|
355
|
-
if (requestedPoint.encoding?.size?.scale !== undefined) {
|
|
356
|
-
const existingSize = next.guideConfigs.legend?.size;
|
|
357
|
-
if (existingSize === undefined) {
|
|
358
|
-
next = next.createSizeLegend({
|
|
359
|
-
target: requestedPoint.id,
|
|
360
|
-
...(count === undefined ? {} : { count }),
|
|
361
|
-
inheritAppearance:
|
|
362
|
-
categoricalArgs.position === "left" ||
|
|
363
|
-
categoricalArgs.labels !== undefined ||
|
|
364
|
-
categoricalArgs.titleStyle !== undefined
|
|
365
|
-
});
|
|
366
|
-
} else if (existingSize.target !== requestedPoint.id) {
|
|
367
|
-
throw new Error(
|
|
368
|
-
"Combined point series legend requires the active size legend to share its target."
|
|
369
|
-
);
|
|
370
|
-
} else if (count !== undefined && count !== existingSize.count) {
|
|
371
|
-
throw new Error(
|
|
372
|
-
"Existing size legend count must be edited before recreating the categorical block."
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
next = next.rematerializeLegend();
|
|
297
|
+
if (existing !== undefined && count !== undefined && existing.count !== count) {
|
|
298
|
+
throw new Error("Existing size legend count must be edited before recreating the categorical block.");
|
|
376
299
|
}
|
|
377
|
-
return next;
|
|
378
300
|
}
|
|
379
|
-
return
|
|
301
|
+
return { steps, combined, finish: combined ? "always" : "auto" };
|
|
302
|
+
}
|
|
303
|
+
return { steps: [{ op: "createCategoricalLegend", args }], finish: "auto" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function applyLegendCreationPlan(program, plan) {
|
|
307
|
+
if (plan.steps.length === 0) return program;
|
|
308
|
+
let next = program;
|
|
309
|
+
for (const step of plan.steps) {
|
|
310
|
+
const existingSize = next.guideConfigs.legend?.size;
|
|
311
|
+
// The plan already validated that a retained size block is compatible.
|
|
312
|
+
if (!(plan.combined && step.op === "createSizeLegend" && existingSize !== undefined)) {
|
|
313
|
+
next = next[step.op](step.args);
|
|
314
|
+
}
|
|
380
315
|
}
|
|
316
|
+
return plan.finish === "always" ? next.rematerializeLegend()
|
|
317
|
+
: plan.finish === "auto" ? finishLegend(next) : next;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const createLegend = action(
|
|
321
|
+
{ op: "createLegend", description: "Create an inferred legend for selected channels." },
|
|
322
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
323
|
+
return applyLegendCreationPlan(this, resolveLegendCreationPlan(this, args));
|
|
324
|
+
})
|
|
381
325
|
);
|
|
382
326
|
|
|
383
327
|
export const removeCategoricalLegend = action(
|
|
@@ -385,7 +329,7 @@ export const removeCategoricalLegend = action(
|
|
|
385
329
|
op: "removeCategoricalLegend",
|
|
386
330
|
description: "Remove the active categorical legend and its concrete components."
|
|
387
331
|
},
|
|
388
|
-
function (args = {}) {
|
|
332
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
389
333
|
noOptions(args, "removeCategoricalLegend");
|
|
390
334
|
const entries = ["series", "color"]
|
|
391
335
|
.filter(kind => this.guideConfigs.legend?.[kind] !== undefined);
|
|
@@ -405,5 +349,5 @@ export const removeCategoricalLegend = action(
|
|
|
405
349
|
next = next.editGraphics({ target, remove: true });
|
|
406
350
|
}
|
|
407
351
|
return next._withoutMaterializationConfig(["guides", "legend", kind]);
|
|
408
|
-
}
|
|
352
|
+
})
|
|
409
353
|
);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { action } from "../../../../core/action.js";
|
|
2
2
|
import { formatVisibleText } from "../../../../core/textMetrics.js";
|
|
3
3
|
import { activeConfig, graphic, noOptions, resolveLayout } from "./layout.js";
|
|
4
|
-
import {
|
|
5
|
-
"../../../../materialization/graphicHierarchy.js";
|
|
4
|
+
import { resolveCategoricalLegendPlacement } from "../lifecycle.js";
|
|
6
5
|
|
|
7
6
|
export const rematerializeLegendLabels = action(
|
|
8
7
|
{ op: "rematerializeLegendLabels", description: "Rematerialize categorical legend labels." },
|
|
@@ -46,7 +45,7 @@ export const createLegendLabels = action(
|
|
|
46
45
|
id,
|
|
47
46
|
type: "text",
|
|
48
47
|
length: config.domain.length,
|
|
49
|
-
...
|
|
48
|
+
...resolveCategoricalLegendPlacement(this)
|
|
50
49
|
})
|
|
51
50
|
.rematerializeLegendLabels();
|
|
52
51
|
}
|
|
@@ -95,7 +94,7 @@ export const createLegendTitle = action(
|
|
|
95
94
|
.createGraphics({
|
|
96
95
|
id,
|
|
97
96
|
type: "text",
|
|
98
|
-
...
|
|
97
|
+
...resolveCategoricalLegendPlacement(this)
|
|
99
98
|
})
|
|
100
99
|
.rematerializeLegendTitle();
|
|
101
100
|
}
|
|
@@ -138,7 +137,7 @@ export const createLegendBackground = action(
|
|
|
138
137
|
.createGraphics({
|
|
139
138
|
id,
|
|
140
139
|
type: "rect",
|
|
141
|
-
...
|
|
140
|
+
...resolveCategoricalLegendPlacement(this)
|
|
142
141
|
})
|
|
143
142
|
.rematerializeLegendBackground();
|
|
144
143
|
}
|