ggaction 0.0.13 → 0.0.15
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 +78 -0
- package/README.md +20 -11
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +8 -3
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +11 -3
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/categoryOrder/index.js +15 -4
- package/src/actions/charts/area.js +5 -1
- package/src/actions/charts/bar.js +2 -1
- package/src/actions/charts/density.js +6 -2
- package/src/actions/charts/ecdf.js +4 -1
- package/src/actions/charts/endpoints.js +25 -3
- package/src/actions/charts/heatmap.js +2 -1
- package/src/actions/charts/histogram.js +4 -3
- package/src/actions/charts/horizon.js +3 -1
- package/src/actions/charts/interval-regression.js +5 -2
- package/src/actions/charts/line.js +3 -1
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +10 -2
- package/src/actions/charts/polar.js +15 -4
- package/src/actions/charts/radar.js +8 -2
- package/src/actions/charts/radial.js +10 -2
- package/src/actions/charts/rug-strip.js +6 -2
- package/src/actions/charts/scatter.js +3 -1
- package/src/actions/composition/actions.js +13 -4
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +47 -17
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +14 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +14 -0
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +16 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/summary.js +1 -1
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +2 -1
- package/src/actions/encodings/density.js +5 -2
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +20 -13
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +10 -2
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/remove.js +9 -4
- package/src/actions/encodings/ruleAppearance.js +1 -23
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/errorBands/create.js +23 -3
- package/src/actions/errorBands/edit.js +21 -6
- package/src/actions/errorBars/create.js +35 -7
- package/src/actions/errorBars/edit.js +19 -4
- package/src/actions/facets/actions.js +237 -57
- package/src/actions/facets/derive.js +91 -26
- package/src/actions/gradientPlots/components.js +10 -2
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +3 -1
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/labels.js +58 -12
- package/src/actions/guides/axes/lines.js +6 -2
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/tickGroups.js +2 -1
- package/src/actions/guides/axes/ticks.js +6 -2
- package/src/actions/guides/axes/titles.js +11 -3
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +19 -3
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +134 -48
- package/src/actions/guides/legends/categorical/components.js +17 -12
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +97 -15
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +85 -30
- package/src/actions/guides/legends/continuous/common.js +30 -12
- package/src/actions/guides/legends/continuous/gradient.js +10 -7
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +8 -3
- package/src/actions/guides/legends/continuous/opacity.js +8 -5
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +158 -59
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +54 -19
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +13 -3
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +93 -40
- package/src/actions/guides/legends/strokeWidth.js +34 -28
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +24 -5
- package/src/actions/guides/polar/axes/lines.js +10 -4
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +1 -1
- package/src/actions/guides/polar/axes/titles.js +1 -1
- package/src/actions/guides/polar/grids.js +1 -1
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +14 -3
- package/src/actions/marks/arc/actions.js +77 -19
- package/src/actions/marks/area/actions.js +34 -9
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +22 -2
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +37 -14
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +18 -6
- package/src/actions/marks/point/materialize.js +49 -15
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +601 -30
- package/src/actions/marks/remove.js +71 -1
- package/src/actions/marks/rule/actions.js +47 -10
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +74 -9
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +154 -6
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +2 -1
- package/src/actions/primitives/semanticAction.js +83 -10
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +10 -2
- package/src/actions/regression/edit.js +21 -7
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +54 -22
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +66 -11
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +4 -3
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +68 -13
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +315 -67
- package/src/actions/theme/state.js +160 -0
- package/src/actions/violinPlots/create.js +5 -2
- package/src/actions/violinPlots/edit.js +1 -0
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +6 -1
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +340 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +343 -0
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +19 -4
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/svg.js +19 -0
- package/src/selectors/datasets.js +32 -1
- package/src/selectors/layers.js +4 -0
- package/src/theme/defaults.js +101 -0
- package/types/basic.d.ts +6 -0
- package/types/index.d.ts +85 -0
- package/types/program.d.ts +910 -117
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isTransformedScaleType,
|
|
11
11
|
mapContinuousScaleValues
|
|
12
12
|
} from "../../../grammar/scales/index.js";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
14
14
|
import { resolveGridLineGeometry } from "../../../layout/grid.js";
|
|
15
15
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
16
16
|
import { findCoordinate } from "../../../selectors/coordinates.js";
|
|
@@ -248,7 +248,12 @@ export function editGridConfig(previous, args) {
|
|
|
248
248
|
|
|
249
249
|
export function resolveGridGeometry(program, config) {
|
|
250
250
|
const scale = program.resolvedScales[config.scale];
|
|
251
|
-
const
|
|
251
|
+
const channel = config.direction === "horizontal" ? "y" : "x";
|
|
252
|
+
const bounds = resolveGuideCoordinateBounds(program, {
|
|
253
|
+
coordinate: config.coordinate,
|
|
254
|
+
scale: config.scale,
|
|
255
|
+
channel
|
|
256
|
+
});
|
|
252
257
|
if ((
|
|
253
258
|
!["linear", "time"].includes(scale?.type) &&
|
|
254
259
|
!isTransformedScaleType(scale?.type)
|
|
@@ -89,6 +89,13 @@ function mergeInferredOptions(inferred, explicit) {
|
|
|
89
89
|
return merged;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function hasStoredGuideCollection(program, collection) {
|
|
93
|
+
return [
|
|
94
|
+
program.semanticSpec.guides?.[collection],
|
|
95
|
+
program.guideConfigs?.[collection]
|
|
96
|
+
].some(value => value !== undefined && Object.keys(value).length > 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
export function resolveGuideOptions(program, args = {}, layers = program.semanticSpec.layers) {
|
|
93
100
|
validateOptions(args);
|
|
94
101
|
const applicability = resolveGuideApplicability(program, layers);
|
|
@@ -128,9 +135,18 @@ const createGuides = action(
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
let next = this;
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
if (
|
|
139
|
+
axes !== undefined &&
|
|
140
|
+
!(args.axes === undefined && hasStoredGuideCollection(this, "axis"))
|
|
141
|
+
) next = next.createAxes(axes);
|
|
142
|
+
if (
|
|
143
|
+
grid !== undefined &&
|
|
144
|
+
!(args.grid === undefined && hasStoredGuideCollection(this, "grid"))
|
|
145
|
+
) next = next.createGrid(grid);
|
|
146
|
+
if (
|
|
147
|
+
legend !== undefined &&
|
|
148
|
+
!(args.legend === undefined && hasStoredGuideCollection(this, "legend"))
|
|
149
|
+
) next = next.createLegend(legend);
|
|
134
150
|
return next;
|
|
135
151
|
}
|
|
136
152
|
);
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validateOptionObject,
|
|
7
|
+
validatePositiveFinite,
|
|
8
|
+
validateUnitInterval
|
|
9
|
+
} from "../../../core/validation.js";
|
|
10
|
+
import { isNominalValue } from "../../../grammar/scales/fields.js";
|
|
11
|
+
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
12
|
+
import { legendResourcePolicy } from "../../../materialization/guides/resources.js";
|
|
13
|
+
import {
|
|
14
|
+
preserveGraphicPlacement,
|
|
15
|
+
resolveLegendGraphicPlacement
|
|
16
|
+
} from "../../../materialization/graphicHierarchy.js";
|
|
17
|
+
import {
|
|
18
|
+
normalizeLegendSampling,
|
|
19
|
+
validateSamplingAgainstScale
|
|
20
|
+
} from "./sampling.js";
|
|
21
|
+
import {
|
|
22
|
+
legendBlockKey,
|
|
23
|
+
resolveLegendBlock
|
|
24
|
+
} from "./target.js";
|
|
25
|
+
import { validateFontWeight } from "./categorical/validation.js";
|
|
26
|
+
import { normalizeDisplayLabelMap } from "../../../grammar/displayLabels.js";
|
|
27
|
+
|
|
28
|
+
const OPTIONS = Object.freeze([
|
|
29
|
+
"target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
|
|
30
|
+
"labelMap"
|
|
31
|
+
]);
|
|
32
|
+
const TEXT_OPTIONS = Object.freeze(["fontSize", "fontFamily", "fontWeight", "color"]);
|
|
33
|
+
const SYMBOL_OPTIONS = Object.freeze(["size", "fill", "stroke", "strokeWidth", "opacity"]);
|
|
34
|
+
|
|
35
|
+
function validatePatchObject(value, keys, label) {
|
|
36
|
+
if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object.`);
|
|
37
|
+
validateOptionObject(value, keys, label);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeTextPatch(value) {
|
|
41
|
+
validatePatchObject(value, TEXT_OPTIONS, "editLegendBlock text");
|
|
42
|
+
if (value.fontSize !== undefined) {
|
|
43
|
+
validatePositiveFinite(value.fontSize, "editLegendBlock text.fontSize");
|
|
44
|
+
}
|
|
45
|
+
if (value.fontFamily !== undefined) {
|
|
46
|
+
validateNonEmptyString(value.fontFamily, "editLegendBlock text.fontFamily");
|
|
47
|
+
}
|
|
48
|
+
if (value.fontWeight !== undefined) {
|
|
49
|
+
validateFontWeight(value.fontWeight, "editLegendBlock text.fontWeight");
|
|
50
|
+
}
|
|
51
|
+
if (value.color !== undefined) {
|
|
52
|
+
validateNonEmptyString(value.color, "editLegendBlock text.color");
|
|
53
|
+
}
|
|
54
|
+
return { ...value };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function categoricalSupports(config, property) {
|
|
58
|
+
const types = new Set(config.symbol.layers.map(layer => layer.type));
|
|
59
|
+
if (property === "size") return types.has("point");
|
|
60
|
+
if (property === "fill") return types.has("point") || types.has("swatch");
|
|
61
|
+
return types.size > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateSymbolSupport(descriptor, property) {
|
|
65
|
+
const { channels, family, kind, config } = descriptor;
|
|
66
|
+
if (family === "gradient") {
|
|
67
|
+
throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
|
|
68
|
+
}
|
|
69
|
+
if (family === "categorical") {
|
|
70
|
+
if (property === "fill" && channels.includes("color")) {
|
|
71
|
+
throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
72
|
+
}
|
|
73
|
+
if (property === "stroke" && channels.includes("stroke")) {
|
|
74
|
+
throw new Error("editLegendBlock symbol.stroke conflicts with the stroke mapping.");
|
|
75
|
+
}
|
|
76
|
+
if (!categoricalSupports(config, property)) {
|
|
77
|
+
throw new Error(`editLegendBlock symbol.${property} is unsupported by this categorical recipe.`);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (kind === "size" && property === "size") {
|
|
82
|
+
throw new Error("editLegendBlock symbol.size conflicts with the size mapping.");
|
|
83
|
+
}
|
|
84
|
+
if (kind === "opacity" && property === "opacity") {
|
|
85
|
+
throw new Error("editLegendBlock symbol.opacity conflicts with the opacity mapping.");
|
|
86
|
+
}
|
|
87
|
+
if (kind === "strokeWidth" && property === "strokeWidth") {
|
|
88
|
+
throw new Error("editLegendBlock symbol.strokeWidth conflicts with the strokeWidth mapping.");
|
|
89
|
+
}
|
|
90
|
+
if (kind === "strokeWidth" && ["size", "fill"].includes(property)) {
|
|
91
|
+
throw new Error(`editLegendBlock symbol.${property} is unsupported for strokeWidth blocks.`);
|
|
92
|
+
}
|
|
93
|
+
if (kind === "interval" && property === "fill") {
|
|
94
|
+
throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
95
|
+
}
|
|
96
|
+
if (kind === "strokeInterval" && property === "stroke") {
|
|
97
|
+
throw new Error("editLegendBlock symbol.stroke conflicts with the stroke mapping.");
|
|
98
|
+
}
|
|
99
|
+
if (["interval", "strokeInterval"].includes(kind) && property === "size") {
|
|
100
|
+
throw new Error(`editLegendBlock symbol.size is unsupported for ${kind} blocks.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeSymbolPatch(value, descriptor) {
|
|
105
|
+
validatePatchObject(value, SYMBOL_OPTIONS, "editLegendBlock symbol");
|
|
106
|
+
if (value.size !== undefined) {
|
|
107
|
+
validateNonNegativeFinite(value.size, "editLegendBlock symbol.size");
|
|
108
|
+
}
|
|
109
|
+
if (value.fill !== undefined) {
|
|
110
|
+
validateNonEmptyString(value.fill, "editLegendBlock symbol.fill");
|
|
111
|
+
}
|
|
112
|
+
if (value.stroke !== undefined) {
|
|
113
|
+
validateNonEmptyString(value.stroke, "editLegendBlock symbol.stroke");
|
|
114
|
+
}
|
|
115
|
+
if (value.strokeWidth !== undefined) {
|
|
116
|
+
validateNonNegativeFinite(value.strokeWidth, "editLegendBlock symbol.strokeWidth");
|
|
117
|
+
}
|
|
118
|
+
if (value.opacity !== undefined) {
|
|
119
|
+
validateUnitInterval(value.opacity, "editLegendBlock symbol.opacity");
|
|
120
|
+
}
|
|
121
|
+
for (const property of Object.keys(value)) {
|
|
122
|
+
validateSymbolSupport(descriptor, property);
|
|
123
|
+
}
|
|
124
|
+
return { ...value };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function validateLegendBlockOverride(descriptor, override) {
|
|
128
|
+
if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
|
|
129
|
+
if (override.labelMap !== undefined) {
|
|
130
|
+
if (descriptor.family !== "categorical") {
|
|
131
|
+
throw new Error("editLegendBlock labelMap requires a categorical block.");
|
|
132
|
+
}
|
|
133
|
+
normalizeDisplayLabelMap(override.labelMap, "editLegendBlock labelMap");
|
|
134
|
+
}
|
|
135
|
+
return override;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function exactCategoricalOrder(program, descriptor, value) {
|
|
139
|
+
if (descriptor.family !== "categorical") {
|
|
140
|
+
throw new Error("editLegendBlock order requires a categorical block.");
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
143
|
+
throw new TypeError("editLegendBlock order must be a non-empty array.");
|
|
144
|
+
}
|
|
145
|
+
if (!value.every(isNominalValue)) {
|
|
146
|
+
throw new TypeError("editLegendBlock order must contain categorical values.");
|
|
147
|
+
}
|
|
148
|
+
if (value.some((item, index) =>
|
|
149
|
+
value.slice(0, index).some(previous => Object.is(previous, item)))) {
|
|
150
|
+
throw new Error("editLegendBlock order values must be unique.");
|
|
151
|
+
}
|
|
152
|
+
const domain = program.resolvedScales[descriptor.scaleIds[0]].domain;
|
|
153
|
+
if (domain.length !== value.length ||
|
|
154
|
+
domain.some(item => !value.some(candidate => Object.is(candidate, item)))) {
|
|
155
|
+
throw new Error("editLegendBlock order must be an exact permutation of the current domain.");
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze({ values: Object.freeze([...value]) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function withoutEmptyNested(entry, key, value) {
|
|
161
|
+
if (Object.keys(value).length > 0) return { ...entry, [key]: value };
|
|
162
|
+
const { [key]: _removed, ...rest } = entry;
|
|
163
|
+
return rest;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function patchLegendBlockOverride(config, key, patch) {
|
|
167
|
+
const previous = config.blockOverrides?.[key] ?? {};
|
|
168
|
+
let entry = { ...previous };
|
|
169
|
+
if (Object.hasOwn(patch, "title")) entry.title = patch.title;
|
|
170
|
+
if (Object.hasOwn(patch, "gap")) entry.gap = patch.gap;
|
|
171
|
+
if (Object.hasOwn(patch, "text")) {
|
|
172
|
+
entry = withoutEmptyNested(entry, "text", patch.text);
|
|
173
|
+
}
|
|
174
|
+
if (Object.hasOwn(patch, "symbol")) {
|
|
175
|
+
entry = withoutEmptyNested(entry, "symbol", patch.symbol);
|
|
176
|
+
}
|
|
177
|
+
if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
|
|
178
|
+
if (patch.removeLabelMap === true) delete entry.labelMap;
|
|
179
|
+
const blockOverrides = { ...(config.blockOverrides ?? {}) };
|
|
180
|
+
if (Object.keys(entry).length === 0) delete blockOverrides[key];
|
|
181
|
+
else blockOverrides[key] = entry;
|
|
182
|
+
return Object.keys(blockOverrides).length === 0
|
|
183
|
+
? undefined
|
|
184
|
+
: blockOverrides;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyCategoricalSymbol(symbol, patch) {
|
|
188
|
+
return {
|
|
189
|
+
...symbol,
|
|
190
|
+
layers: symbol.layers.map(layer => ({
|
|
191
|
+
...layer,
|
|
192
|
+
...(patch.size !== undefined && layer.type === "point"
|
|
193
|
+
? { size: Math.sqrt(patch.size / Math.PI) }
|
|
194
|
+
: {}),
|
|
195
|
+
...(patch.fill !== undefined && ["point", "swatch"].includes(layer.type)
|
|
196
|
+
? { fill: patch.fill }
|
|
197
|
+
: {}),
|
|
198
|
+
...(patch.stroke !== undefined ? { stroke: patch.stroke } : {}),
|
|
199
|
+
...(patch.strokeWidth !== undefined
|
|
200
|
+
? layer.type === "line"
|
|
201
|
+
? { lineWidth: patch.strokeWidth }
|
|
202
|
+
: { strokeWidth: patch.strokeWidth }
|
|
203
|
+
: {}),
|
|
204
|
+
...(patch.opacity !== undefined ? { opacity: patch.opacity } : {})
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
210
|
+
const channels = kind === "series" || kind === "color" || kind === "stroke"
|
|
211
|
+
? config.channels
|
|
212
|
+
: [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
|
|
213
|
+
gradient: "color", interval: "color", strokeGradient: "stroke",
|
|
214
|
+
strokeInterval: "stroke" }[kind]];
|
|
215
|
+
const override = config.blockOverrides?.[legendBlockKey(channels)] ?? {};
|
|
216
|
+
const effective = {
|
|
217
|
+
...config,
|
|
218
|
+
...(override.title === undefined || override.title === ""
|
|
219
|
+
? {}
|
|
220
|
+
: { title: override.title }),
|
|
221
|
+
...(override.title === ""
|
|
222
|
+
? { titleVisible: false }
|
|
223
|
+
: override.title === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { titleVisible: true }),
|
|
226
|
+
...(override.gap === undefined ? {} : { itemGap: override.gap, blockGap: override.gap }),
|
|
227
|
+
...(override.text === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { labels: { ...config.labels, ...override.text } }),
|
|
230
|
+
...(Object.hasOwn(override, "labelMap")
|
|
231
|
+
? { labelMap: override.labelMap }
|
|
232
|
+
: {})
|
|
233
|
+
};
|
|
234
|
+
if (override.symbol === undefined) return effective;
|
|
235
|
+
if (["series", "color", "stroke"].includes(kind)) {
|
|
236
|
+
return {
|
|
237
|
+
...effective,
|
|
238
|
+
symbol: applyCategoricalSymbol(config.symbol, override.symbol),
|
|
239
|
+
blockSymbol: override.symbol
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (kind === "opacity") {
|
|
243
|
+
return {
|
|
244
|
+
...effective,
|
|
245
|
+
symbol: {
|
|
246
|
+
...config.symbol,
|
|
247
|
+
...(override.symbol.size === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { radius: Math.sqrt(override.symbol.size / Math.PI) }),
|
|
250
|
+
...Object.fromEntries(Object.entries(override.symbol)
|
|
251
|
+
.filter(([key]) => ["fill", "stroke", "strokeWidth"].includes(key)))
|
|
252
|
+
},
|
|
253
|
+
blockSymbol: override.symbol
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (["interval", "strokeInterval"].includes(kind)) {
|
|
257
|
+
return {
|
|
258
|
+
...effective,
|
|
259
|
+
symbol: { ...config.symbol, ...override.symbol },
|
|
260
|
+
blockSymbol: override.symbol
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return { ...effective, blockSymbol: override.symbol };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function titleGraphicId(kind) {
|
|
267
|
+
return legendResourcePolicy(kind).graphicIds.find(id => id.endsWith("Title"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
|
|
271
|
+
const id = titleGraphicId(kind);
|
|
272
|
+
const exists = program.graphicSpec.objects[id] !== undefined;
|
|
273
|
+
if (!visible && exists) return program.editGraphics({ target: id, remove: true });
|
|
274
|
+
if (visible && !exists) {
|
|
275
|
+
const owned = new Set(legendResourcePolicy(kind).graphicIds);
|
|
276
|
+
const anchor = program.graphicSpec.objects.canvas.children
|
|
277
|
+
.filter(childId => childId !== id && owned.has(childId))
|
|
278
|
+
.at(-1);
|
|
279
|
+
if (anchor === undefined) {
|
|
280
|
+
throw new Error(`editLegendBlock cannot place the ${kind} legend title.`);
|
|
281
|
+
}
|
|
282
|
+
return program.createGraphics({
|
|
283
|
+
id,
|
|
284
|
+
type: "text",
|
|
285
|
+
...resolveLegendGraphicPlacement(program, { after: anchor })
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return program;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const SPECIALIZED_SYMBOLS = Object.freeze({
|
|
292
|
+
size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
|
|
293
|
+
opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
|
|
294
|
+
strokeWidth: Object.freeze({ id: "strokeWidthLegendSymbols", type: "line" }),
|
|
295
|
+
interval: Object.freeze({ id: "colorLegendSymbols", type: "rect" }),
|
|
296
|
+
strokeInterval: Object.freeze({ id: "strokeIntervalSymbols", type: "rect" })
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Symbol patches replace the complete block-local patch. Recreating the concrete
|
|
300
|
+
// symbol owner removes properties that existed only in the previous patch before
|
|
301
|
+
// the ordinary rematerializer writes the new effective appearance.
|
|
302
|
+
function recreateSymbolGraphics(program, descriptor) {
|
|
303
|
+
if (descriptor.family === "gradient") return program;
|
|
304
|
+
if (descriptor.family === "categorical") {
|
|
305
|
+
const symbolIds = legendResourcePolicy(descriptor.kind).graphicIds
|
|
306
|
+
.filter(id => id.includes("Symbol"));
|
|
307
|
+
let next = program;
|
|
308
|
+
for (const id of symbolIds) {
|
|
309
|
+
if (next.graphicSpec.objects[id] !== undefined) {
|
|
310
|
+
next = next.editGraphics({ target: id, remove: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return next.createLegendSymbols({ kind: descriptor.kind });
|
|
314
|
+
}
|
|
315
|
+
const symbol = SPECIALIZED_SYMBOLS[descriptor.kind];
|
|
316
|
+
if (symbol === undefined || program.graphicSpec.objects[symbol.id] === undefined) {
|
|
317
|
+
throw new Error(`editLegendBlock cannot recreate ${descriptor.kind} legend symbols.`);
|
|
318
|
+
}
|
|
319
|
+
const placement = preserveGraphicPlacement(program, symbol.id);
|
|
320
|
+
return program
|
|
321
|
+
.editGraphics({ target: symbol.id, remove: true })
|
|
322
|
+
.createGraphics({ id: symbol.id, type: symbol.type, length: 0, ...placement });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeBlockPatch(args, descriptor) {
|
|
326
|
+
const patch = {};
|
|
327
|
+
if (Object.hasOwn(args, "title")) {
|
|
328
|
+
if (typeof args.title !== "string") {
|
|
329
|
+
throw new TypeError("editLegendBlock title must be a string.");
|
|
330
|
+
}
|
|
331
|
+
patch.title = args.title;
|
|
332
|
+
}
|
|
333
|
+
if (Object.hasOwn(args, "gap")) {
|
|
334
|
+
validateNonNegativeFinite(args.gap, "editLegendBlock gap");
|
|
335
|
+
patch.gap = args.gap;
|
|
336
|
+
}
|
|
337
|
+
if (Object.hasOwn(args, "text")) patch.text = normalizeTextPatch(args.text);
|
|
338
|
+
if (Object.hasOwn(args, "symbol")) {
|
|
339
|
+
patch.symbol = normalizeSymbolPatch(args.symbol, descriptor);
|
|
340
|
+
}
|
|
341
|
+
if (Object.hasOwn(args, "labelMap")) {
|
|
342
|
+
if (descriptor.family !== "categorical") {
|
|
343
|
+
throw new Error("editLegendBlock labelMap requires a categorical block.");
|
|
344
|
+
}
|
|
345
|
+
if (args.labelMap === "auto") patch.removeLabelMap = true;
|
|
346
|
+
else {
|
|
347
|
+
patch.labelMap = normalizeDisplayLabelMap(
|
|
348
|
+
args.labelMap,
|
|
349
|
+
"editLegendBlock labelMap"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return patch;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const editLegendBlock = action(
|
|
357
|
+
{
|
|
358
|
+
op: "editLegendBlock",
|
|
359
|
+
description: "Edit one logical legend block selected by channel."
|
|
360
|
+
},
|
|
361
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
362
|
+
validateOptionObject(args, OPTIONS, "editLegendBlock");
|
|
363
|
+
if (!Object.hasOwn(args, "target") || typeof args.target !== "string") {
|
|
364
|
+
throw new TypeError("editLegendBlock target must be an explicit legend target id.");
|
|
365
|
+
}
|
|
366
|
+
if (!Object.hasOwn(args, "channel") || typeof args.channel !== "string") {
|
|
367
|
+
throw new TypeError("editLegendBlock channel must be explicit.");
|
|
368
|
+
}
|
|
369
|
+
if (!Object.keys(args).some(key => !["target", "channel"].includes(key))) {
|
|
370
|
+
throw new Error("editLegendBlock requires at least one change.");
|
|
371
|
+
}
|
|
372
|
+
const descriptor = resolveLegendBlock(this, args, "editLegendBlock");
|
|
373
|
+
if ((Object.hasOwn(args, "values") || Object.hasOwn(args, "count")) &&
|
|
374
|
+
descriptor.family !== "sampled") {
|
|
375
|
+
throw new Error("editLegendBlock values and count require a sampled legend block.");
|
|
376
|
+
}
|
|
377
|
+
const patch = normalizeBlockPatch(args, descriptor);
|
|
378
|
+
let config = descriptor.config;
|
|
379
|
+
const blockOverrides = patchLegendBlockOverride(config, descriptor.key, patch);
|
|
380
|
+
if (blockOverrides === undefined) {
|
|
381
|
+
const { blockOverrides: _removed, ...rest } = config;
|
|
382
|
+
config = rest;
|
|
383
|
+
} else {
|
|
384
|
+
config = { ...config, blockOverrides };
|
|
385
|
+
}
|
|
386
|
+
if (Object.hasOwn(args, "values") || Object.hasOwn(args, "count")) {
|
|
387
|
+
const sampling = normalizeLegendSampling(args, {
|
|
388
|
+
previous: config,
|
|
389
|
+
operation: "edit",
|
|
390
|
+
label: `editLegendBlock ${descriptor.channel ?? args.channel} legend`
|
|
391
|
+
});
|
|
392
|
+
validateSamplingAgainstScale(sampling, this.resolvedScales[descriptor.scaleIds[0]],
|
|
393
|
+
"editLegendBlock values");
|
|
394
|
+
const { count: _legacyCount, ...withoutCount } = config;
|
|
395
|
+
config = { ...withoutCount, sampling };
|
|
396
|
+
}
|
|
397
|
+
const order = Object.hasOwn(args, "order")
|
|
398
|
+
? exactCategoricalOrder(this, descriptor, args.order)
|
|
399
|
+
: undefined;
|
|
400
|
+
if (args.title !== undefined && args.title !== "") {
|
|
401
|
+
config = { ...config, title: args.title, inferredTitle: false };
|
|
402
|
+
}
|
|
403
|
+
let next = this._withLegendConfig(descriptor.kind, config);
|
|
404
|
+
const semanticKind = legendResourcePolicy(descriptor.kind).semanticKind;
|
|
405
|
+
if (order !== undefined) {
|
|
406
|
+
next = next.editSemantic({
|
|
407
|
+
property: `guide.legend.${semanticKind}.order`,
|
|
408
|
+
value: order
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (args.title !== undefined && args.title !== "") {
|
|
412
|
+
next = next.editSemantic({
|
|
413
|
+
property: `guide.legend.${semanticKind}.title`,
|
|
414
|
+
value: args.title
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const effective = resolveEffectiveLegendBlockConfig(next, descriptor.kind,
|
|
418
|
+
next.guideConfigs.legend[descriptor.kind]);
|
|
419
|
+
if (Object.hasOwn(args, "symbol")) {
|
|
420
|
+
next = recreateSymbolGraphics(next, descriptor);
|
|
421
|
+
}
|
|
422
|
+
next = reconcileLegendBlockTitleGraphic(next, descriptor.kind,
|
|
423
|
+
effective.titleVisible !== false);
|
|
424
|
+
return next.rematerializeLegend();
|
|
425
|
+
})
|
|
426
|
+
);
|