ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { isPlainObject } from "../../../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
nonEmptyString,
|
|
4
|
+
nonNegative,
|
|
5
|
+
positive,
|
|
6
|
+
validateKeys
|
|
7
|
+
} from "./validation.js";
|
|
8
|
+
|
|
9
|
+
const LAYER_OPTIONS = Object.freeze({
|
|
10
|
+
line: Object.freeze(["type", "length", "lineWidth"]),
|
|
11
|
+
point: Object.freeze([
|
|
12
|
+
"type",
|
|
13
|
+
"shape",
|
|
14
|
+
"size",
|
|
15
|
+
"fill",
|
|
16
|
+
"stroke",
|
|
17
|
+
"strokeWidth"
|
|
18
|
+
]),
|
|
19
|
+
swatch: Object.freeze([
|
|
20
|
+
"type",
|
|
21
|
+
"width",
|
|
22
|
+
"height",
|
|
23
|
+
"stroke",
|
|
24
|
+
"strokeWidth"
|
|
25
|
+
])
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function defaultRecipe(kind) {
|
|
29
|
+
return kind === "series"
|
|
30
|
+
? { layers: [{ type: "line", length: 32, lineWidth: 2 }] }
|
|
31
|
+
: {
|
|
32
|
+
layers: [{
|
|
33
|
+
type: "swatch",
|
|
34
|
+
width: 14,
|
|
35
|
+
height: 12,
|
|
36
|
+
stroke: "white",
|
|
37
|
+
strokeWidth: 0.5
|
|
38
|
+
}]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeLayer(layer) {
|
|
43
|
+
if (!isPlainObject(layer) || !Object.hasOwn(LAYER_OPTIONS, layer.type)) {
|
|
44
|
+
throw new Error("Legend symbol layer type must be line, point, or swatch.");
|
|
45
|
+
}
|
|
46
|
+
validateKeys(
|
|
47
|
+
layer,
|
|
48
|
+
LAYER_OPTIONS[layer.type],
|
|
49
|
+
`createLegend.symbol.${layer.type}`
|
|
50
|
+
);
|
|
51
|
+
if (layer.type === "line") {
|
|
52
|
+
const normalized = { length: 32, lineWidth: 2, ...layer };
|
|
53
|
+
positive(normalized.length, "Legend line symbol length");
|
|
54
|
+
nonNegative(normalized.lineWidth, "Legend line symbol lineWidth");
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
if (layer.type === "point") {
|
|
58
|
+
const normalized = {
|
|
59
|
+
shape: "circle",
|
|
60
|
+
size: 5,
|
|
61
|
+
stroke: "white",
|
|
62
|
+
strokeWidth: 0,
|
|
63
|
+
...layer
|
|
64
|
+
};
|
|
65
|
+
if (normalized.shape !== "circle") {
|
|
66
|
+
throw new Error(`Unsupported legend point shape "${normalized.shape}".`);
|
|
67
|
+
}
|
|
68
|
+
positive(normalized.size, "Legend point symbol size");
|
|
69
|
+
nonEmptyString(normalized.stroke, "Legend point symbol stroke");
|
|
70
|
+
nonNegative(normalized.strokeWidth, "Legend point symbol strokeWidth");
|
|
71
|
+
if (normalized.fill !== undefined) {
|
|
72
|
+
nonEmptyString(normalized.fill, "Legend point symbol fill");
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
const normalized = {
|
|
77
|
+
width: 14,
|
|
78
|
+
height: 12,
|
|
79
|
+
stroke: "white",
|
|
80
|
+
strokeWidth: 0.5,
|
|
81
|
+
...layer
|
|
82
|
+
};
|
|
83
|
+
positive(normalized.width, "Legend swatch width");
|
|
84
|
+
positive(normalized.height, "Legend swatch height");
|
|
85
|
+
nonEmptyString(normalized.stroke, "Legend swatch stroke");
|
|
86
|
+
nonNegative(normalized.strokeWidth, "Legend swatch strokeWidth");
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeRecipe(symbol, kind) {
|
|
91
|
+
if (symbol === undefined || symbol === "auto") return defaultRecipe(kind);
|
|
92
|
+
if (!isPlainObject(symbol)) {
|
|
93
|
+
throw new TypeError('createLegend.symbol must be "auto" or a plain object.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let layers;
|
|
97
|
+
if (Object.hasOwn(symbol, "layers")) {
|
|
98
|
+
validateKeys(symbol, ["layers"], "createLegend.symbol");
|
|
99
|
+
if (!Array.isArray(symbol.layers) || symbol.layers.length === 0) {
|
|
100
|
+
throw new TypeError("Legend symbol layers must be a non-empty array.");
|
|
101
|
+
}
|
|
102
|
+
layers = symbol.layers.map(normalizeLayer);
|
|
103
|
+
} else if (kind === "series") {
|
|
104
|
+
validateKeys(symbol, ["length", "lineWidth"], "createLegend.symbol");
|
|
105
|
+
layers = [normalizeLayer({ type: "line", ...symbol })];
|
|
106
|
+
} else {
|
|
107
|
+
validateKeys(
|
|
108
|
+
symbol,
|
|
109
|
+
["width", "height", "stroke", "strokeWidth"],
|
|
110
|
+
"createLegend.symbol"
|
|
111
|
+
);
|
|
112
|
+
layers = [normalizeLayer({ type: "swatch", ...symbol })];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const types = layers.map(layer => layer.type);
|
|
116
|
+
if (new Set(types).size !== types.length) {
|
|
117
|
+
throw new Error("Legend symbol recipe supports at most one layer per type.");
|
|
118
|
+
}
|
|
119
|
+
return { layers };
|
|
120
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { validateUserId } from "../../../../core/identifiers.js";
|
|
2
|
+
import { sameOrderedValues } from "../../../../core/validation.js";
|
|
3
|
+
import { CHANNELS } from "./options.js";
|
|
4
|
+
import { nonEmptyString } from "./validation.js";
|
|
5
|
+
import { findLayer } from "../../../../selectors/layers.js";
|
|
6
|
+
import { findSemanticScale } from "../../../../selectors/scales.js";
|
|
7
|
+
|
|
8
|
+
function isCategoricalTarget(layer) {
|
|
9
|
+
if (["line", "point"].includes(layer?.mark?.type)) {
|
|
10
|
+
return CHANNELS.some(
|
|
11
|
+
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return ["bar", "area"].includes(layer?.mark?.type) &&
|
|
15
|
+
layer.encoding?.color?.scale !== undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveTarget(program, requested) {
|
|
19
|
+
if (requested !== undefined) {
|
|
20
|
+
const id = validateUserId(requested, "Legend target id");
|
|
21
|
+
const layer = findLayer(program, id);
|
|
22
|
+
if (!isCategoricalTarget(layer)) {
|
|
23
|
+
throw new Error(`Unknown categorical legend target "${id}".`);
|
|
24
|
+
}
|
|
25
|
+
return layer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const current = findLayer(program, program.context.currentMark);
|
|
29
|
+
if (isCategoricalTarget(current)) return current;
|
|
30
|
+
const candidates = program.semanticSpec.layers.filter(isCategoricalTarget);
|
|
31
|
+
if (candidates.length !== 1) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"createLegend requires target when the categorical mark is ambiguous."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return candidates[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const sameValues = sameOrderedValues;
|
|
40
|
+
|
|
41
|
+
export function resolveLegendKind(layer, requestedChannels) {
|
|
42
|
+
if (["bar", "area"].includes(layer.mark.type)) return "color";
|
|
43
|
+
if (
|
|
44
|
+
layer.mark.type === "point" &&
|
|
45
|
+
sameValues(requestedChannels, ["color"])
|
|
46
|
+
) {
|
|
47
|
+
return "color";
|
|
48
|
+
}
|
|
49
|
+
return "series";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveOrdinalScales(program, scaleIds) {
|
|
53
|
+
const scales = scaleIds.map(id => {
|
|
54
|
+
const semantic = findSemanticScale(program, id);
|
|
55
|
+
const concrete = program.resolvedScales[id];
|
|
56
|
+
if (semantic?.type !== "ordinal" || concrete?.type !== "ordinal") {
|
|
57
|
+
throw new Error(`Legend requires resolved ordinal scale "${id}".`);
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(concrete.domain) || concrete.domain.length === 0) {
|
|
60
|
+
throw new Error(`Legend scale "${id}" requires a non-empty domain.`);
|
|
61
|
+
}
|
|
62
|
+
return concrete;
|
|
63
|
+
});
|
|
64
|
+
if (scales.slice(1).some(scale => !sameValues(scale.domain, scales[0].domain))) {
|
|
65
|
+
throw new Error("Combined legend scales must have identical ordered domains.");
|
|
66
|
+
}
|
|
67
|
+
return { scales, domain: scales[0].domain };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveDefinition(program, layer, requestedChannels, requestedTitle) {
|
|
71
|
+
const kind = resolveLegendKind(layer, requestedChannels);
|
|
72
|
+
const channels = requestedChannels ?? (kind === "color"
|
|
73
|
+
? ["color"]
|
|
74
|
+
: CHANNELS.filter(
|
|
75
|
+
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
76
|
+
));
|
|
77
|
+
if (
|
|
78
|
+
!Array.isArray(channels) ||
|
|
79
|
+
channels.length === 0 ||
|
|
80
|
+
!channels.every(channel => CHANNELS.includes(channel)) ||
|
|
81
|
+
new Set(channels).size !== channels.length
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Legend channels must be a non-empty unique color/strokeDash/shape array."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (kind === "color" && !sameValues(channels, ["color"])) {
|
|
88
|
+
throw new Error("Color legends currently support only the color channel.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const encodings = channels.map(channel => {
|
|
92
|
+
const encoding = layer.encoding?.[channel];
|
|
93
|
+
if (encoding?.scale === undefined) {
|
|
94
|
+
throw new Error(`Legend target does not encode channel "${channel}".`);
|
|
95
|
+
}
|
|
96
|
+
return { channel, encoding };
|
|
97
|
+
});
|
|
98
|
+
const fields = new Set(encodings.map(item => item.encoding.field));
|
|
99
|
+
if (fields.size !== 1) {
|
|
100
|
+
throw new Error("Combined legend channels must encode the same field.");
|
|
101
|
+
}
|
|
102
|
+
const scales = encodings.map(item => item.encoding.scale);
|
|
103
|
+
const resolved = resolveOrdinalScales(program, scales);
|
|
104
|
+
const field = [...fields][0];
|
|
105
|
+
return {
|
|
106
|
+
kind,
|
|
107
|
+
channels,
|
|
108
|
+
scales,
|
|
109
|
+
field,
|
|
110
|
+
title: nonEmptyString(requestedTitle ?? field, "Legend title"),
|
|
111
|
+
domain: resolved.domain
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveCurrentDefinition(program, config) {
|
|
116
|
+
const layer = findLayer(program, config.target);
|
|
117
|
+
if (layer === undefined) {
|
|
118
|
+
throw new Error(`Unknown categorical legend target "${config.target}".`);
|
|
119
|
+
}
|
|
120
|
+
const guide = program.semanticSpec.guides.legend?.[config.kind];
|
|
121
|
+
if (guide === undefined) {
|
|
122
|
+
throw new Error("Legend rematerialization requires semantic guide state.");
|
|
123
|
+
}
|
|
124
|
+
const channels = config.kind === "series" ? guide.channels : ["color"];
|
|
125
|
+
return resolveDefinition(
|
|
126
|
+
program,
|
|
127
|
+
layer,
|
|
128
|
+
channels,
|
|
129
|
+
config.inferredTitle === true ? undefined : guide.title
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { action } from "../../../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
activeConfig,
|
|
4
|
+
graphic,
|
|
5
|
+
layerFor,
|
|
6
|
+
noOptions,
|
|
7
|
+
resolveAppearance,
|
|
8
|
+
resolveLayout,
|
|
9
|
+
symbolGraphic,
|
|
10
|
+
symbolWidth
|
|
11
|
+
} from "./layout.js";
|
|
12
|
+
import { resolveLegendGraphicPlacement } from
|
|
13
|
+
"../../../../materialization/graphicHierarchy.js";
|
|
14
|
+
import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
|
|
15
|
+
import { resolveStoredSelection } from "../../../../materialization/selection/state.js";
|
|
16
|
+
|
|
17
|
+
function exactLegendSelection(program, config, highlight) {
|
|
18
|
+
if (highlight.target !== config.target) return undefined;
|
|
19
|
+
const resolved = resolveStoredSelection(program, highlight.selection);
|
|
20
|
+
const selected = new Set(resolved.keys);
|
|
21
|
+
const groups = config.domain.map(value =>
|
|
22
|
+
resolved.items.filter(item => item.fields?.[config.field] === value)
|
|
23
|
+
);
|
|
24
|
+
if (
|
|
25
|
+
groups.some(group => group.length === 0) ||
|
|
26
|
+
resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
|
|
27
|
+
) return undefined;
|
|
28
|
+
const states = groups.map(group => {
|
|
29
|
+
const count = group.filter(item => selected.has(item.key)).length;
|
|
30
|
+
return count === 0 ? false : count === group.length ? true : undefined;
|
|
31
|
+
});
|
|
32
|
+
return states.includes(undefined) ? undefined : states;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function legendLayerStyle(layer, style) {
|
|
36
|
+
if (layer.type === "line") {
|
|
37
|
+
return Object.fromEntries(
|
|
38
|
+
["stroke", "strokeWidth", "strokeDash", "opacity"]
|
|
39
|
+
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (layer.type === "swatch") {
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
["fill", "stroke", "strokeWidth", "opacity"]
|
|
45
|
+
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
["fill", "stroke", "strokeWidth", "opacity"]
|
|
50
|
+
.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeEditSymbol(type) {
|
|
55
|
+
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
|
|
56
|
+
const op = `editLegendSymbol${suffix}`;
|
|
57
|
+
return action(
|
|
58
|
+
{ op, description: `Rematerialize categorical legend ${type} symbols.` },
|
|
59
|
+
function (args = {}) {
|
|
60
|
+
noOptions(args, op);
|
|
61
|
+
const { config } = activeConfig(this);
|
|
62
|
+
const layer = layerFor(config, type);
|
|
63
|
+
const id = symbolGraphic(config, type);
|
|
64
|
+
const dynamicPoint = type === "point" && config.channels.includes("shape");
|
|
65
|
+
const expected = dynamicPoint
|
|
66
|
+
? "collection"
|
|
67
|
+
: { line: "line", point: "circle", swatch: "rect" }[type];
|
|
68
|
+
if (this.graphicSpec.objects[id]?.type !== expected) {
|
|
69
|
+
throw new Error(`${op} requires existing ${type} symbols.`);
|
|
70
|
+
}
|
|
71
|
+
const layout = resolveLayout(this, config);
|
|
72
|
+
const appearance = resolveAppearance(this, config);
|
|
73
|
+
let next = this;
|
|
74
|
+
if (!dynamicPoint) {
|
|
75
|
+
next = next.editGraphics({
|
|
76
|
+
target: id,
|
|
77
|
+
property: "length",
|
|
78
|
+
value: config.domain.length
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (type === "line") {
|
|
82
|
+
const x1 = layout.symbolX.map(
|
|
83
|
+
value => value + (symbolWidth(config) - layer.length) / 2
|
|
84
|
+
);
|
|
85
|
+
return next
|
|
86
|
+
.editGraphics({ target: id, property: "x1", value: x1 })
|
|
87
|
+
.editGraphics({ target: id, property: "y1", value: layout.itemY })
|
|
88
|
+
.editGraphics({
|
|
89
|
+
target: id,
|
|
90
|
+
property: "x2",
|
|
91
|
+
value: x1.map(value => value + layer.length)
|
|
92
|
+
})
|
|
93
|
+
.editGraphics({ target: id, property: "y2", value: layout.itemY })
|
|
94
|
+
.editGraphics({ target: id, property: "stroke", value: appearance.colors })
|
|
95
|
+
.editGraphics({ target: id, property: "strokeWidth", value: layer.lineWidth })
|
|
96
|
+
.editGraphics({ target: id, property: "strokeDash", value: appearance.dashes });
|
|
97
|
+
}
|
|
98
|
+
if (type === "point") {
|
|
99
|
+
const x = layout.symbolX.map(value => value + symbolWidth(config) / 2);
|
|
100
|
+
if (dynamicPoint) {
|
|
101
|
+
const items = config.domain.map((_, index) => {
|
|
102
|
+
const fill = layer.fill ?? appearance.colors[index];
|
|
103
|
+
return createPointShapeGraphic({
|
|
104
|
+
shape: appearance.shapes[index],
|
|
105
|
+
x: x[index],
|
|
106
|
+
y: layout.itemY[index],
|
|
107
|
+
area: Math.PI * layer.size ** 2,
|
|
108
|
+
fill,
|
|
109
|
+
stroke: layer.stroke,
|
|
110
|
+
strokeWidth: layer.strokeWidth
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return next.editGraphics({
|
|
114
|
+
target: id,
|
|
115
|
+
property: "items",
|
|
116
|
+
value: items
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return next
|
|
120
|
+
.editGraphics({ target: id, property: "x", value: x })
|
|
121
|
+
.editGraphics({ target: id, property: "y", value: layout.itemY })
|
|
122
|
+
.editGraphics({ target: id, property: "radius", value: layer.size })
|
|
123
|
+
.editGraphics({
|
|
124
|
+
target: id,
|
|
125
|
+
property: "fill",
|
|
126
|
+
value: layer.fill ?? appearance.colors
|
|
127
|
+
})
|
|
128
|
+
.editGraphics({ target: id, property: "stroke", value: layer.stroke })
|
|
129
|
+
.editGraphics({ target: id, property: "strokeWidth", value: layer.strokeWidth });
|
|
130
|
+
}
|
|
131
|
+
const x = layout.symbolX.map(
|
|
132
|
+
value => value + (symbolWidth(config) - layer.width) / 2
|
|
133
|
+
);
|
|
134
|
+
return next
|
|
135
|
+
.editGraphics({ target: id, property: "x", value: x })
|
|
136
|
+
.editGraphics({
|
|
137
|
+
target: id,
|
|
138
|
+
property: "y",
|
|
139
|
+
value: layout.itemY.map(value => value - layer.height / 2)
|
|
140
|
+
})
|
|
141
|
+
.editGraphics({ target: id, property: "width", value: layer.width })
|
|
142
|
+
.editGraphics({ target: id, property: "height", value: layer.height })
|
|
143
|
+
.editGraphics({ target: id, property: "fill", value: appearance.colors })
|
|
144
|
+
.editGraphics({ target: id, property: "stroke", value: layer.stroke })
|
|
145
|
+
.editGraphics({ target: id, property: "strokeWidth", value: layer.strokeWidth });
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function makeCreateSymbol(type, edit) {
|
|
151
|
+
const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
|
|
152
|
+
const op = `createLegendSymbol${suffix}`;
|
|
153
|
+
return action(
|
|
154
|
+
{ op, description: `Create categorical legend ${type} symbols.` },
|
|
155
|
+
function (args = {}) {
|
|
156
|
+
noOptions(args, op);
|
|
157
|
+
const { config } = activeConfig(this);
|
|
158
|
+
layerFor(config, type);
|
|
159
|
+
const id = symbolGraphic(config, type);
|
|
160
|
+
if (this.graphicSpec.objects[id] !== undefined) {
|
|
161
|
+
throw new Error(`${op} requires missing ${type} symbols.`);
|
|
162
|
+
}
|
|
163
|
+
const graphicType = type === "point" && config.channels.includes("shape")
|
|
164
|
+
? "collection"
|
|
165
|
+
: { line: "line", point: "circle", swatch: "rect" }[type];
|
|
166
|
+
return this
|
|
167
|
+
.createGraphics({
|
|
168
|
+
id,
|
|
169
|
+
type: graphicType,
|
|
170
|
+
...resolveLegendGraphicPlacement(this),
|
|
171
|
+
...(this.graphicSpec.objects[graphic(config, "Labels")] === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { before: graphic(config, "Labels") }),
|
|
174
|
+
...(graphicType === "collection"
|
|
175
|
+
? {}
|
|
176
|
+
: { length: config.domain.length })
|
|
177
|
+
})
|
|
178
|
+
[edit]();
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const editLegendSymbolLines = makeEditSymbol("line");
|
|
184
|
+
export const editLegendSymbolPoints = makeEditSymbol("point");
|
|
185
|
+
export const editLegendSymbolSwatches = makeEditSymbol("swatch");
|
|
186
|
+
export const createLegendSymbolLines = makeCreateSymbol(
|
|
187
|
+
"line",
|
|
188
|
+
"editLegendSymbolLines"
|
|
189
|
+
);
|
|
190
|
+
export const createLegendSymbolPoints = makeCreateSymbol(
|
|
191
|
+
"point",
|
|
192
|
+
"editLegendSymbolPoints"
|
|
193
|
+
);
|
|
194
|
+
export const createLegendSymbolSwatches = makeCreateSymbol(
|
|
195
|
+
"swatch",
|
|
196
|
+
"editLegendSymbolSwatches"
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
export const createLegendSymbols = action(
|
|
200
|
+
{ op: "createLegendSymbols", description: "Create layered legend symbols." },
|
|
201
|
+
function (args = {}) {
|
|
202
|
+
noOptions(args, "createLegendSymbols");
|
|
203
|
+
const { config } = activeConfig(this);
|
|
204
|
+
let next = this;
|
|
205
|
+
for (const layer of config.symbol.layers) {
|
|
206
|
+
const operation = {
|
|
207
|
+
line: "createLegendSymbolLines",
|
|
208
|
+
point: "createLegendSymbolPoints",
|
|
209
|
+
swatch: "createLegendSymbolSwatches"
|
|
210
|
+
}[layer.type];
|
|
211
|
+
next = next[operation]();
|
|
212
|
+
}
|
|
213
|
+
return next;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
export const editLegendSymbols = action(
|
|
218
|
+
{ op: "editLegendSymbols", description: "Rematerialize layered legend symbols." },
|
|
219
|
+
function (args = {}) {
|
|
220
|
+
noOptions(args, "editLegendSymbols");
|
|
221
|
+
const { config } = activeConfig(this);
|
|
222
|
+
let next = this;
|
|
223
|
+
for (const layer of config.symbol.layers) {
|
|
224
|
+
const operation = {
|
|
225
|
+
line: "editLegendSymbolLines",
|
|
226
|
+
point: "editLegendSymbolPoints",
|
|
227
|
+
swatch: "editLegendSymbolSwatches"
|
|
228
|
+
}[layer.type];
|
|
229
|
+
next = next[operation]();
|
|
230
|
+
}
|
|
231
|
+
return next;
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
export const rematerializeLegendHighlights = action(
|
|
236
|
+
{
|
|
237
|
+
op: "rematerializeLegendHighlights",
|
|
238
|
+
description: "Reflect exact categorical mark highlights in legend symbols."
|
|
239
|
+
},
|
|
240
|
+
function (args = {}) {
|
|
241
|
+
noOptions(args, "rematerializeLegendHighlights");
|
|
242
|
+
const hasCategorical =
|
|
243
|
+
this.guideConfigs.legend?.series !== undefined ||
|
|
244
|
+
this.guideConfigs.legend?.color !== undefined;
|
|
245
|
+
if (!hasCategorical) return this;
|
|
246
|
+
const { config } = activeConfig(this);
|
|
247
|
+
const highlights = Object.values(
|
|
248
|
+
this.materializationConfigs.highlights ?? {}
|
|
249
|
+
).map(highlight => ({
|
|
250
|
+
highlight,
|
|
251
|
+
states: exactLegendSelection(this, config, highlight)
|
|
252
|
+
})).filter(entry => entry.states !== undefined);
|
|
253
|
+
if (highlights.length === 0) return this;
|
|
254
|
+
|
|
255
|
+
let next = this.editLegendSymbols();
|
|
256
|
+
for (const { highlight, states } of highlights) {
|
|
257
|
+
for (const layer of config.symbol.layers) {
|
|
258
|
+
const id = symbolGraphic(config, layer.type);
|
|
259
|
+
const graphic = next.graphicSpec.objects[id];
|
|
260
|
+
const selectedStyle = legendLayerStyle(layer, highlight.style);
|
|
261
|
+
const dimOpacity = highlight.dimOthers === false
|
|
262
|
+
? undefined
|
|
263
|
+
: highlight.dimOthers.opacity;
|
|
264
|
+
next = next.editGraphics({
|
|
265
|
+
target: id,
|
|
266
|
+
property: "items",
|
|
267
|
+
value: graphic.items.map((child, index) => ({
|
|
268
|
+
type: child.type ?? graphic.type,
|
|
269
|
+
properties: states[index]
|
|
270
|
+
? { ...child.properties, ...selectedStyle }
|
|
271
|
+
: dimOpacity === undefined
|
|
272
|
+
? child.properties
|
|
273
|
+
: { ...child.properties, opacity: dimOpacity }
|
|
274
|
+
}))
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isPlainObject } from "../../../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite
|
|
7
|
+
} from "../../../../core/validation.js";
|
|
8
|
+
|
|
9
|
+
export { validateKeys };
|
|
10
|
+
|
|
11
|
+
export function validateObject(value, supported, label) {
|
|
12
|
+
if (!isPlainObject(value)) {
|
|
13
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
14
|
+
}
|
|
15
|
+
validateKeys(value, supported, label);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const nonEmptyString = validateNonEmptyString;
|
|
19
|
+
export const nonNegative = validateNonNegativeFinite;
|
|
20
|
+
export const positive = validatePositiveFinite;
|
|
21
|
+
|
|
22
|
+
export function validateFontWeight(value, label) {
|
|
23
|
+
if (typeof value !== "string" && !Number.isFinite(value)) {
|
|
24
|
+
throw new TypeError(`${label} must be a string or number.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|