ggaction 0.0.17 → 0.0.18
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 +46 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/regression.js +2 -1
- package/src/actions/data/shared.js +3 -2
- package/src/actions/data/stack.js +3 -1
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +8 -4
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +3 -3
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +8 -3
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +81 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +3 -7
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +7 -1
- package/src/grammar/regression/models.js +83 -55
- package/src/grammar/regression/parameters.js +14 -3
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +3 -26
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +5 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +18 -5
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +347 -85
|
@@ -1,447 +1 @@
|
|
|
1
|
-
import { isPlainObject } from "../../../../core/immutable.js";
|
|
2
|
-
import { resolveTextBounds } from "../../../../core/textMetrics.js";
|
|
3
|
-
import {
|
|
4
|
-
validateKeys,
|
|
5
|
-
validateOptionObject,
|
|
6
|
-
validateNonEmptyString,
|
|
7
|
-
validateNonNegativeFinite,
|
|
8
|
-
validatePositiveFinite,
|
|
9
|
-
validateGeneratedItemLimit
|
|
10
|
-
} from "../../../../core/validation.js";
|
|
11
|
-
import { formatTimeTick } from "../../../../grammar/ticks.js";
|
|
12
|
-
import {
|
|
13
|
-
formatValue,
|
|
14
|
-
validateValueFormat
|
|
15
|
-
} from "../../../../grammar/valueFormat.js";
|
|
16
|
-
import {
|
|
17
|
-
formatDistinctNumericSamples,
|
|
18
|
-
sampleNumericRange
|
|
19
|
-
} from "../../../../grammar/numeric.js";
|
|
20
|
-
import { resolveGraphicBounds } from "../../../../layout/canvas.js";
|
|
21
|
-
import { isHorizontalEdgeLegend } from "../../../../layout/legendLane.js";
|
|
22
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
23
|
-
"../../../../theme/defaults.js";
|
|
24
|
-
import { findLayer } from "../../../../selectors/layers.js";
|
|
25
|
-
import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
|
|
26
|
-
import { findCanvasGraphic } from
|
|
27
|
-
"../../../../materialization/graphicHierarchy.js";
|
|
28
|
-
import { normalizeLegendSampling } from "../sampling.js";
|
|
29
|
-
import { editGraphicProperties } from "../../../primitives/graphicProperties.js";
|
|
30
|
-
|
|
31
|
-
const OPTIONS = [
|
|
32
|
-
"target", "channels", "position", "align", "offset", "title", "count",
|
|
33
|
-
"values",
|
|
34
|
-
"gradient", "symbol", "labels", "titleStyle", "itemGap", "border",
|
|
35
|
-
"direction", "columns", "titlePosition"
|
|
36
|
-
];
|
|
37
|
-
const TEXT_OPTIONS = [
|
|
38
|
-
"offset", "color", "fontSize", "fontFamily", "fontWeight", "format"
|
|
39
|
-
];
|
|
40
|
-
const BORDER_OPTIONS = [
|
|
41
|
-
"color", "lineWidth", "padding", "background"
|
|
42
|
-
];
|
|
43
|
-
const POSITIONS = ["right", "left", "top", "bottom"];
|
|
44
|
-
const DEFAULT_LABELS = {
|
|
45
|
-
offset: 12,
|
|
46
|
-
color: DEFAULT_COLORS.text,
|
|
47
|
-
fontSize: 12,
|
|
48
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
49
|
-
fontWeight: "normal"
|
|
50
|
-
};
|
|
51
|
-
const DEFAULT_TITLE = {
|
|
52
|
-
color: DEFAULT_COLORS.text,
|
|
53
|
-
fontSize: 13,
|
|
54
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
55
|
-
fontWeight: 600
|
|
56
|
-
};
|
|
57
|
-
const DEFAULT_BORDER = {
|
|
58
|
-
color: DEFAULT_COLORS.border,
|
|
59
|
-
lineWidth: 1,
|
|
60
|
-
padding: 12,
|
|
61
|
-
background: "transparent"
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const validatePositive = validatePositiveFinite;
|
|
65
|
-
export const validateNonNegative = validateNonNegativeFinite;
|
|
66
|
-
|
|
67
|
-
export { editGraphicProperties };
|
|
68
|
-
|
|
69
|
-
export function normalizeLegendTextOptions(value, label, defaults) {
|
|
70
|
-
if (value === undefined) return { ...defaults };
|
|
71
|
-
if (!isPlainObject(value)) {
|
|
72
|
-
throw new TypeError(`${label} must be a plain object.`);
|
|
73
|
-
}
|
|
74
|
-
validateKeys(value, TEXT_OPTIONS, label);
|
|
75
|
-
const result = { ...defaults, ...value };
|
|
76
|
-
if (Object.hasOwn(result, "offset")) {
|
|
77
|
-
validateNonNegative(result.offset, `${label} offset`);
|
|
78
|
-
}
|
|
79
|
-
validatePositive(result.fontSize, `${label} fontSize`);
|
|
80
|
-
for (const key of ["color", "fontFamily"]) {
|
|
81
|
-
validateNonEmptyString(result[key], `${label} ${key}`);
|
|
82
|
-
}
|
|
83
|
-
if (
|
|
84
|
-
typeof result.fontWeight !== "string" &&
|
|
85
|
-
!Number.isFinite(result.fontWeight)
|
|
86
|
-
) {
|
|
87
|
-
throw new TypeError(`${label} fontWeight must be a string or finite number.`);
|
|
88
|
-
}
|
|
89
|
-
if (Object.hasOwn(result, "format")) {
|
|
90
|
-
result.format = validateValueFormat(result.format, `${label} format`);
|
|
91
|
-
}
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function normalizeLegendTitleOptions(value, label, defaults) {
|
|
96
|
-
if (value !== undefined) {
|
|
97
|
-
validateOptionObject(value, ["color", "fontSize", "fontFamily", "fontWeight"], label);
|
|
98
|
-
}
|
|
99
|
-
return normalizeLegendTextOptions(value, label, defaults);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function normalizeLegendBorder(value) {
|
|
103
|
-
if (value === undefined || value === false) return false;
|
|
104
|
-
if (value !== true && !isPlainObject(value)) {
|
|
105
|
-
throw new TypeError("createLegend.border must be a boolean or plain object.");
|
|
106
|
-
}
|
|
107
|
-
if (value !== true) validateKeys(value, BORDER_OPTIONS, "createLegend.border");
|
|
108
|
-
const border = { ...DEFAULT_BORDER, ...(value === true ? {} : value) };
|
|
109
|
-
for (const key of ["color", "background"]) {
|
|
110
|
-
validateNonEmptyString(border[key], `Legend border ${key}`);
|
|
111
|
-
}
|
|
112
|
-
validateNonNegative(border.lineWidth, "Legend border lineWidth");
|
|
113
|
-
validateNonNegative(border.padding, "Legend border padding");
|
|
114
|
-
return border;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function normalizeContinuousLegend(args, kind) {
|
|
118
|
-
validateOptionObject(args, OPTIONS, "createLegend");
|
|
119
|
-
const position = args.position ?? "right";
|
|
120
|
-
if (!POSITIONS.includes(position)) {
|
|
121
|
-
throw new Error(`Unsupported legend position "${position}".`);
|
|
122
|
-
}
|
|
123
|
-
const align = args.align ?? "center";
|
|
124
|
-
if (!["left", "center", "right"].includes(align)) {
|
|
125
|
-
throw new Error(`Unsupported legend alignment "${align}".`);
|
|
126
|
-
}
|
|
127
|
-
if (["left", "right"].includes(position) && align !== "center") {
|
|
128
|
-
throw new Error("Side continuous legends require center alignment.");
|
|
129
|
-
}
|
|
130
|
-
if (kind === "gradient" && Object.hasOwn(args, "values")) {
|
|
131
|
-
throw new Error("Gradient legends do not support exact sample values.");
|
|
132
|
-
}
|
|
133
|
-
const sampling = kind === "opacity"
|
|
134
|
-
? normalizeLegendSampling(args, {
|
|
135
|
-
operation: "create",
|
|
136
|
-
label: "Opacity legend"
|
|
137
|
-
})
|
|
138
|
-
: undefined;
|
|
139
|
-
const count = kind === "gradient" ? args.count ?? 5 : undefined;
|
|
140
|
-
if (kind === "gradient") {
|
|
141
|
-
if (!Number.isInteger(count) || count < 2) {
|
|
142
|
-
throw new RangeError(
|
|
143
|
-
"Continuous legend count must be an integer of at least 2."
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
validateGeneratedItemLimit(count, "Continuous legend count");
|
|
147
|
-
}
|
|
148
|
-
const offset = args.offset ?? 30;
|
|
149
|
-
validateNonNegative(offset, "Legend offset");
|
|
150
|
-
const titlePosition = args.titlePosition ?? "top";
|
|
151
|
-
if (!["top", "left"].includes(titlePosition)) {
|
|
152
|
-
throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
|
|
153
|
-
}
|
|
154
|
-
if (
|
|
155
|
-
titlePosition === "left" &&
|
|
156
|
-
(!["top", "bottom"].includes(position) || kind !== "opacity")
|
|
157
|
-
) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
"Only horizontal opacity legends support left titlePosition."
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
|
|
163
|
-
validatePositive(itemGap, "Legend itemGap");
|
|
164
|
-
if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
|
|
165
|
-
if (kind === "gradient") {
|
|
166
|
-
for (const key of ["symbol", "columns", "direction", "itemGap"]) {
|
|
167
|
-
if (Object.hasOwn(args, key)) {
|
|
168
|
-
throw new Error(`Gradient legend does not accept ${key}.`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
for (const key of ["columns", "direction", "gradient"]) {
|
|
173
|
-
if (Object.hasOwn(args, key)) {
|
|
174
|
-
throw new Error(`Opacity legend does not accept ${key}.`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
target: args.target,
|
|
180
|
-
position,
|
|
181
|
-
align,
|
|
182
|
-
offset,
|
|
183
|
-
...(kind === "gradient" ? { count } : { sampling }),
|
|
184
|
-
title: args.title,
|
|
185
|
-
inferredTitle: args.title === undefined,
|
|
186
|
-
labels: normalizeLegendTextOptions(
|
|
187
|
-
args.labels,
|
|
188
|
-
"createLegend.labels",
|
|
189
|
-
titlePosition === "left"
|
|
190
|
-
? { ...DEFAULT_LABELS, offset: 8 }
|
|
191
|
-
: DEFAULT_LABELS
|
|
192
|
-
),
|
|
193
|
-
titleStyle: normalizeLegendTitleOptions(
|
|
194
|
-
args.titleStyle,
|
|
195
|
-
"createLegend.titleStyle",
|
|
196
|
-
DEFAULT_TITLE
|
|
197
|
-
),
|
|
198
|
-
itemGap,
|
|
199
|
-
titlePosition,
|
|
200
|
-
border: normalizeLegendBorder(args.border)
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function selectLegendLayer(program, requested, predicate) {
|
|
205
|
-
const candidates = program.semanticSpec.layers.filter(predicate);
|
|
206
|
-
if (requested === undefined) return candidates.length === 1
|
|
207
|
-
? candidates[0]
|
|
208
|
-
: undefined;
|
|
209
|
-
const candidate = findLayer(program, requested);
|
|
210
|
-
return candidates.includes(candidate) ? candidate : undefined;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function resolveContinuousLegendLayer(program, requested, channel) {
|
|
214
|
-
const layer = selectLegendLayer(
|
|
215
|
-
program,
|
|
216
|
-
requested,
|
|
217
|
-
channel === "opacity" ? isOpacityLegendLayer : candidate =>
|
|
218
|
-
candidate.mark?.type === "point" && candidate.encoding?.[channel]?.scale !== undefined
|
|
219
|
-
);
|
|
220
|
-
if (layer === undefined) {
|
|
221
|
-
throw new Error(
|
|
222
|
-
requested === undefined
|
|
223
|
-
? `${channel} legend requires one eligible ${channel === "opacity" ? "point or line" : "point"} mark.`
|
|
224
|
-
: `Unknown ${channel} legend target "${requested}".`
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
return layer;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function resolveContinuousColorLayer(program, requested, channel = "color") {
|
|
231
|
-
const layer = selectLegendLayer(
|
|
232
|
-
program,
|
|
233
|
-
requested,
|
|
234
|
-
candidate => (channel === "stroke"
|
|
235
|
-
? ["point", "line", "area", "bar", "rect", "arc", "rule", "tick"]
|
|
236
|
-
: ["point", "bar", "rect"]
|
|
237
|
-
).includes(candidate.mark?.type) &&
|
|
238
|
-
candidate.encoding?.[channel]?.scale !== undefined
|
|
239
|
-
);
|
|
240
|
-
if (layer === undefined) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
requested === undefined
|
|
243
|
-
? channel === "color"
|
|
244
|
-
? "color legend requires one eligible point, bar, or rect mark."
|
|
245
|
-
: "stroke legend requires one eligible mark."
|
|
246
|
-
: `Unknown ${channel} legend target "${requested}".`
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
return layer;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function requireResolvedLegendScale(program, id, type) {
|
|
253
|
-
const scale = program.resolvedScales[id];
|
|
254
|
-
if (scale?.type !== type) {
|
|
255
|
-
throw new Error(`Legend requires resolved ${type} scale "${id}".`);
|
|
256
|
-
}
|
|
257
|
-
return scale;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function resolveContinuousBounds(
|
|
261
|
-
program,
|
|
262
|
-
message = "Continuous legend layout requires Canvas bounds."
|
|
263
|
-
) {
|
|
264
|
-
const plot = resolveGraphicBounds(program);
|
|
265
|
-
const canvas = findCanvasGraphic(program);
|
|
266
|
-
if (
|
|
267
|
-
plot === undefined ||
|
|
268
|
-
![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
|
|
269
|
-
canvas?.type !== "canvas" ||
|
|
270
|
-
!Number.isFinite(canvas.properties.width) ||
|
|
271
|
-
!Number.isFinite(canvas.properties.height)
|
|
272
|
-
) {
|
|
273
|
-
throw new Error(message);
|
|
274
|
-
}
|
|
275
|
-
return { plot, canvas: canvas.properties };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function sampleContinuousValues(domain, count) {
|
|
279
|
-
return sampleNumericRange(
|
|
280
|
-
domain[0],
|
|
281
|
-
domain[1],
|
|
282
|
-
count,
|
|
283
|
-
"Continuous legend domain"
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export function formatContinuousValues(values, domain, fieldType, format = "auto") {
|
|
288
|
-
const resolved = validateValueFormat(format, "Legend label format");
|
|
289
|
-
if (resolved === "auto") {
|
|
290
|
-
return fieldType === "temporal"
|
|
291
|
-
? values.map(value => formatTimeTick(value, domain))
|
|
292
|
-
: formatDistinctNumericSamples(values);
|
|
293
|
-
}
|
|
294
|
-
return values.map(value => formatValue(value, {
|
|
295
|
-
format: resolved,
|
|
296
|
-
valueType: fieldType === "temporal" ? "temporal" : "quantitative",
|
|
297
|
-
label: "Legend label format"
|
|
298
|
-
}));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export function styleContinuousText(
|
|
302
|
-
program,
|
|
303
|
-
id,
|
|
304
|
-
style,
|
|
305
|
-
{ align = "left" } = {}
|
|
306
|
-
) {
|
|
307
|
-
return editGraphicProperties(program, id, {
|
|
308
|
-
fill: style.color,
|
|
309
|
-
fontSize: style.fontSize,
|
|
310
|
-
fontFamily: style.fontFamily,
|
|
311
|
-
fontWeight: style.fontWeight,
|
|
312
|
-
textAlign: align,
|
|
313
|
-
textBaseline: "middle"
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function resolveLegendTextBounds(position, text, style, profile) {
|
|
318
|
-
return resolveTextBounds({
|
|
319
|
-
x: position.x,
|
|
320
|
-
y: position.y,
|
|
321
|
-
text: String(text),
|
|
322
|
-
...style,
|
|
323
|
-
textAlign: position.align ?? "left",
|
|
324
|
-
textBaseline: "middle"
|
|
325
|
-
}, profile);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function assertLegendBoundsInsideCanvas(bounds, canvas, label, config) {
|
|
329
|
-
// Horizontal content is intrinsic until the shared lane places its actual
|
|
330
|
-
// concrete bounds. Only that final placement can determine Canvas fit.
|
|
331
|
-
if (isHorizontalEdgeLegend(config)) return;
|
|
332
|
-
if (bounds.some(item =>
|
|
333
|
-
item.left < 0 || item.right > canvas.width ||
|
|
334
|
-
item.top < 0 || item.bottom > canvas.height
|
|
335
|
-
)) {
|
|
336
|
-
throw new Error(`${label} requires more Canvas margin space.`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function resolveLegendBackgroundFromBounds(
|
|
341
|
-
bounds,
|
|
342
|
-
border,
|
|
343
|
-
canvas,
|
|
344
|
-
label,
|
|
345
|
-
config
|
|
346
|
-
) {
|
|
347
|
-
if (border === false) return undefined;
|
|
348
|
-
const strokeExtent = border.lineWidth / 2;
|
|
349
|
-
let x = Infinity;
|
|
350
|
-
let y = Infinity;
|
|
351
|
-
let right = -Infinity;
|
|
352
|
-
let bottom = -Infinity;
|
|
353
|
-
for (const item of bounds) {
|
|
354
|
-
x = Math.min(x, item.left);
|
|
355
|
-
y = Math.min(y, item.top);
|
|
356
|
-
right = Math.max(right, item.right);
|
|
357
|
-
bottom = Math.max(bottom, item.bottom);
|
|
358
|
-
}
|
|
359
|
-
x -= border.padding;
|
|
360
|
-
y -= border.padding;
|
|
361
|
-
right += border.padding;
|
|
362
|
-
bottom += border.padding;
|
|
363
|
-
if (!isHorizontalEdgeLegend(config) && (
|
|
364
|
-
x - strokeExtent < 0 || y - strokeExtent < 0 ||
|
|
365
|
-
right + strokeExtent > canvas.width ||
|
|
366
|
-
bottom + strokeExtent > canvas.height
|
|
367
|
-
)) {
|
|
368
|
-
throw new Error(`${label} background requires more Canvas margin space.`);
|
|
369
|
-
}
|
|
370
|
-
return { x, y, width: right - x, height: bottom - y };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function editLegendBackground(program, id, bounds, border) {
|
|
374
|
-
if (bounds === undefined) return program;
|
|
375
|
-
return editGraphicProperties(program, id, {
|
|
376
|
-
...bounds,
|
|
377
|
-
fill: border.background,
|
|
378
|
-
stroke: border.color,
|
|
379
|
-
strokeWidth: border.lineWidth
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export function normalizeItemLegendLayout(args) {
|
|
384
|
-
const position = args.position ?? "right";
|
|
385
|
-
if (!["right", "left", "top", "bottom"].includes(position)) throw new Error(`Unsupported legend position "${position}".`);
|
|
386
|
-
const side = ["left", "right"].includes(position);
|
|
387
|
-
const layout = args.layout === undefined ? "edge" : args.layout;
|
|
388
|
-
if (layout !== "edge") throw new Error('This legend requires layout "edge".');
|
|
389
|
-
const align = args.align ?? "center";
|
|
390
|
-
if (!["left", "center", "right"].includes(align)) throw new Error(`Unsupported legend alignment "${align}".`);
|
|
391
|
-
const direction = args.direction ?? (side ? "vertical" : "horizontal");
|
|
392
|
-
if (!["horizontal", "vertical"].includes(direction)) throw new Error(`Unsupported legend direction "${direction}".`);
|
|
393
|
-
const columns = args.columns;
|
|
394
|
-
if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) throw new RangeError("Legend columns must be a positive integer.");
|
|
395
|
-
const titlePosition = args.titlePosition ?? "top";
|
|
396
|
-
if (!["top", "left"].includes(titlePosition)) throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
|
|
397
|
-
if (side && (direction !== "vertical" || align !== "center" || (columns !== undefined && columns !== 1) || titlePosition !== "top")) {
|
|
398
|
-
throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");
|
|
399
|
-
}
|
|
400
|
-
const offset = args.offset ?? 30;
|
|
401
|
-
const itemGap = args.itemGap ?? 28;
|
|
402
|
-
validateNonNegative(offset, "Legend offset");
|
|
403
|
-
validatePositive(itemGap, "Legend itemGap");
|
|
404
|
-
return { position, layout, align, direction, columns, titlePosition, offset, itemGap };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export function materializeItemLegend(program, kind, config, layout, {
|
|
408
|
-
symbols, optional = {}, text = layout.labels,
|
|
409
|
-
labels = layout.config.labels, titleStyle = layout.config.titleStyle
|
|
410
|
-
}) {
|
|
411
|
-
const prefix = `${kind}Legend`;
|
|
412
|
-
let next = program
|
|
413
|
-
.editSemantic({ property: `guide.legend.${kind}.scale`, value: config.scale })
|
|
414
|
-
.editSemantic({ property: `guide.legend.${kind}.title`, value: config.title })
|
|
415
|
-
._withLegendConfig(kind, config);
|
|
416
|
-
next = editGraphicProperties(next, `${prefix}Symbols`, {
|
|
417
|
-
length: text.length,
|
|
418
|
-
...symbols
|
|
419
|
-
});
|
|
420
|
-
next = editGraphicProperties(next, `${prefix}Labels`, {
|
|
421
|
-
length: text.length, x: layout.labelX, y: layout.itemY, text
|
|
422
|
-
});
|
|
423
|
-
next = editLegendBackground(next, `${prefix}Background`, layout.background, config.border);
|
|
424
|
-
next = editGraphicProperties(next, `${prefix}Symbols`, Object.fromEntries(
|
|
425
|
-
Object.entries(optional).filter(([, value]) => value !== undefined)
|
|
426
|
-
));
|
|
427
|
-
next = styleContinuousText(next, `${prefix}Labels`, labels);
|
|
428
|
-
const effective = layout.config;
|
|
429
|
-
if (effective.titleVisible === false) return next;
|
|
430
|
-
next = editGraphicProperties(next, `${prefix}Title`, {
|
|
431
|
-
x: layout.title.x, y: layout.title.y, text: effective.title
|
|
432
|
-
});
|
|
433
|
-
return styleContinuousText(next, `${prefix}Title`, titleStyle, { align: layout.title.align });
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function createItemLegendGraphics(program, kind, config, count, type, placement) {
|
|
437
|
-
const prefix = `${kind}Legend`;
|
|
438
|
-
let next = program
|
|
439
|
-
.editSemantic({ property: `guide.legend.${kind}.scale`, value: config.scale })
|
|
440
|
-
.editSemantic({ property: `guide.legend.${kind}.title`, value: config.title })
|
|
441
|
-
._withLegendConfig(kind, config);
|
|
442
|
-
if (config.border !== false) next = next.createGraphics({ id: `${prefix}Background`, type: "rect", ...placement });
|
|
443
|
-
next = next.createGraphics({ id: `${prefix}Symbols`, type, length: count, ...placement })
|
|
444
|
-
.createGraphics({ id: `${prefix}Labels`, type: "text", length: count, ...placement });
|
|
445
|
-
if (config.titleVisible !== false) next = next.createGraphics({ id: `${prefix}Title`, type: "text", ...placement });
|
|
446
|
-
return next;
|
|
447
|
-
}
|
|
1
|
+
import{LEGEND_TITLE_STYLE_KEYS as A,mergeLegendTitleStyle as W,validateLegendTitleAlignment as x}from"../../../../grammar/legendTitleStyle.js";import{PLOT_LEGEND_POSITIONS as k,isPlotLegend as p}from"../../../../layout/legend.js";import{isContinuousColorScaleType as D}from"../../../../grammar/scales/types.js";import{isPlainObject as T}from"../../../../core/immutable.js";import{resolveTextBounds as U}from"../../../../core/textMetrics.js";import{resolveLegendTitleText as R}from"../../../../layout/legendText.js";import{materializeLegendTitleLines as M}from"../../../../materialization/legendText.js";import{validateKeys as S,validateFontWeight as q,validateOptionObject as O,validateNonEmptyString as h,validateNonNegativeFinite as V,validatePositiveFinite as Y,validateGeneratedItemLimit as K}from"../../../../core/validation.js";import{formatTimeTick as X}from"../../../../grammar/ticks.js";import{formatValue as H,validateValueFormat as $}from"../../../../grammar/valueFormat.js";import{formatDistinctNumericSamples as J,sampleNumericRange as Q}from"../../../../grammar/numeric.js";import{resolveGraphicBounds as Z,canvasOverflowError as v}from"../../../../layout/canvas.js";import{isHorizontalEdgeLegend as I}from"../../../../layout/legendLane.js";import{DEFAULT_COLORS as m,DEFAULT_FONT_FAMILY as G,DEFAULT_VERTICAL_LEGEND_ITEM_GAP as L}from"../../../../theme/defaults.js";import{findLayer as ee}from"../../../../selectors/layers.js";import{isOpacityLegendLayer as te}from"../../../../materialization/legends.js";import{findCanvasGraphic as ne}from"../../../../materialization/graphicHierarchy.js";import{normalizeLegendSampling as ie}from"../sampling.js";import{editGraphicProperties as u}from"../../../primitives/graphicProperties.js";const oe=["target","channels","position","align","offset","title","count","values","gradient","symbol","labels","titleStyle","itemGap","border","direction","columns","titlePosition"],ze=Object.freeze(["target","count","values","position","layout","align","direction","columns","titlePosition","offset","itemGap","title","labels","titleStyle","border","overflow"]),re=["offset","color","fontSize","fontFamily","fontWeight","format"],le=["color","lineWidth","padding","background"],_=["right","left","top","bottom",...k],b={offset:12,color:m.text,fontSize:12,fontFamily:G,fontWeight:"normal"},z={color:m.text,fontSize:13,fontFamily:G,fontWeight:600},de=Object.freeze(b),se=Object.freeze({...z,color:m.strongText});function N(e,t){return e!==void 0&&e!==!1&&h(e,"Legend title"),{title:e===!1||e===void 0?t:e,inferredTitle:e===!1||e===void 0,titleVisible:e!==!1}}function Ne(e,t,n){const i=["right","left"].includes(e.position??"right")||p(e),r=(e.direction??(i?"vertical":"horizontal"))==="vertical"?L:n;return{...ce({...e,itemGap:e.itemGap??r}),...N(e.title,t.field),labels:w(e.labels,"createLegend.labels",de),titleStyle:C(e.titleStyle,"createLegend.titleStyle",se),border:F(e.border)}}const ae={color:m.border,lineWidth:1,padding:12,background:"transparent"},y=Y,g=V;function w(e,t,n){if(e===void 0)return{...n};if(!T(e))throw new TypeError(`${t} must be a plain object.`);S(e,re,t);const i={...n,...e};Object.hasOwn(i,"offset")&&g(i.offset,`${t} offset`),y(i.fontSize,`${t} fontSize`);for(const o of["color","fontFamily"])h(i[o],`${t} ${o}`);return q(i.fontWeight,`${t} fontWeight`,"finite number"),Object.hasOwn(i,"format")&&(i.format=$(i.format,`${t} format`)),i}function C(e,t,n,i=!1){e!==void 0&&O(e,A,t);const o=W(n,e,{edit:i}),{align:r,...d}=o;return{...w(d,t,{}),...r===void 0?{}:{align:r}}}function F(e){if(e===void 0||e===!1)return!1;if(e!==!0&&!T(e))throw new TypeError("createLegend.border must be a boolean or plain object.");e!==!0&&S(e,le,"createLegend.border");const t={...ae,...e===!0?{}:e};for(const n of["color","background"])h(t[n],`Legend border ${n}`);return g(t.lineWidth,"Legend border lineWidth"),g(t.padding,"Legend border padding"),t}function Ce(e,t){O(e,oe,"createLegend");const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=e.align??"center";if(!["left","center","right"].includes(i))throw new Error(`Unsupported legend alignment "${i}".`);if(["left","right"].includes(n)&&i!=="center")throw new Error("Side continuous legends require center alignment.");if(t==="gradient"&&Object.hasOwn(e,"values"))throw new Error("Gradient legends do not support exact sample values.");const o=t==="opacity"?ie(e,{operation:"create",label:"Opacity legend"}):void 0,r=t==="gradient"?e.count??5:void 0;if(t==="gradient"){if(!Number.isInteger(r)||r<2)throw new RangeError("Continuous legend count must be an integer of at least 2.");K(r,"Continuous legend count")}const d=e.offset??(p({position:n})?8:30);g(d,"Legend offset");const l=e.titlePosition??"top";if(x(e.titleStyle,l),!["top","left"].includes(l))throw new Error(`Unsupported legend titlePosition "${l}".`);if(l==="left"&&(!["top","bottom"].includes(n)||t!=="opacity"))throw new Error("Only horizontal opacity legends support left titlePosition.");const a=e.itemGap??(t==="opacity"&&(["right","left"].includes(n)||p({position:n}))?L:l==="left"?20:28);if(y(a,"Legend itemGap"),t==="gradient"){for(const s of["symbol","columns","direction","itemGap"])if(Object.hasOwn(e,s))throw new Error(`Gradient legend does not accept ${s}.`)}else for(const s of["columns","direction","gradient"])if(Object.hasOwn(e,s))throw new Error(`Opacity legend does not accept ${s}.`);return{target:e.target,position:n,align:i,offset:d,...t==="gradient"?{count:r}:{sampling:o},...N(e.title),labels:w(e.labels,"createLegend.labels",l==="left"?{...b,offset:8}:b),titleStyle:C(e.titleStyle,"createLegend.titleStyle",z),itemGap:a,titlePosition:l,border:F(e.border)}}function P(e,t,n){const i=e.semanticSpec.layers.filter(n);if(t===void 0)return i.length===1?i[0]:void 0;const o=ee(e,t);return i.includes(o)?o:void 0}function Fe(e,t,n){const i=P(e,t,n==="opacity"?te:o=>o.mark?.type==="point"&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?`${n} legend requires one eligible ${n==="opacity"?"point, line or bar":"point"} mark.`:`Unknown ${n} legend target "${t}".`);return i}function Pe(e,t,n="color"){const i=P(e,t,o=>(n==="stroke"?["point","line","area","bar","rect","arc","rule","tick"]:["point","bar","rect","text"]).includes(o.mark?.type)&&o.encoding?.[n]?.scale!==void 0);if(i===void 0)throw new Error(t===void 0?n==="color"?"color legend requires one eligible point, bar, or rect mark.":"stroke legend requires one eligible mark.":`Unknown ${n} legend target "${t}".`);return i}function Be(e,t,n){const i=e.resolvedScales[t];if(n==="sequential"?!D(i?.type):i?.type!==n)throw new Error(`Legend requires resolved ${n} scale "${t}".`);return i}function je(e,t="Continuous legend layout requires Canvas bounds."){const n=Z(e),i=ne(e);if(n===void 0||![n.x,n.y,n.width,n.height].every(Number.isFinite)||i?.type!=="canvas"||!Number.isFinite(i.properties.width)||!Number.isFinite(i.properties.height))throw new Error(t);return{plot:n,canvas:i.properties}}function Ae(e,t){return Q(e[0],e[1],t,"Continuous legend domain")}function We(e,t,n,i="auto"){const o=$(i,"Legend label format");return o==="auto"?n==="temporal"?e.map(r=>X(r,t)):J(e):e.map(r=>H(r,{format:o,valueType:n==="temporal"?"temporal":"quantitative",label:"Legend label format"}))}function B(e,t,n,{align:i="left"}={}){const o=u(e,t,{fill:n.color,fontSize:n.fontSize,fontFamily:n.fontFamily,fontWeight:n.fontWeight,textAlign:i,textBaseline:"middle"});return t.endsWith("Title")?M(o,t,o.graphicSpec.objects[t].properties.text,n):o}function ke(e,t,n,i,{hardBreaks:o=!1}={}){return U({x:e.x,y:e.y,text:String(t),...n,...o?{lines:R(t,n,i).lines}:{},textAlign:e.align??"left",textBaseline:"middle"},i)}function De(e,t,n,i){if(!(i?.groupIntrinsic||I(i)||["left","right"].includes(i?.position))&&e.some(o=>o.left<0||o.right>t.width||o.top<0||o.bottom>t.height))throw v(`${n} requires more Canvas margin space.`,e,t)}function Ue(e,t,n,i,o){if(t===!1)return;const r=t.lineWidth/2;let d=1/0,l=1/0,a=-1/0,s=-1/0;for(const c of e)d=Math.min(d,c.left),l=Math.min(l,c.top),a=Math.max(a,c.right),s=Math.max(s,c.bottom);if(d-=t.padding,l-=t.padding,a+=t.padding,s+=t.padding,!o?.groupIntrinsic&&!I(o)&&!["left","right"].includes(o?.position)&&(d-r<0||l-r<0||a+r>n.width||s+r>n.height))throw v(`${i} background requires more Canvas margin space.`,[{left:d-r,right:a+r,top:l-r,bottom:s+r}],n);return{x:d,y:l,width:a-d,height:s-l}}function fe(e,t,n,i){return n===void 0?e:u(e,t,{...n,fill:i.background,stroke:i.color,strokeWidth:i.lineWidth})}function ce(e,{multiColumnSide:t=!1}={}){const n=e.position??"right";if(!_.includes(n))throw new Error(`Unsupported legend position "${n}".`);const i=["left","right"].includes(n)||p({position:n}),o=e.layout===void 0?"edge":e.layout;if(o!=="edge")throw new Error('This legend requires layout "edge".');const r=e.align??"center";if(!["left","center","right"].includes(r))throw new Error(`Unsupported legend alignment "${r}".`);const d=e.direction??(i?"vertical":"horizontal");if(!["horizontal","vertical"].includes(d))throw new Error(`Unsupported legend direction "${d}".`);const l=e.columns;if(l!==void 0&&(!Number.isInteger(l)||l<1))throw new RangeError("Legend columns must be a positive integer.");const a=e.titlePosition??"top";if(x(e.titleStyle,a),!["top","left"].includes(a))throw new Error(`Unsupported legend titlePosition "${a}".`);if(i&&(r!=="center"||a!=="top"||!t&&(d!=="vertical"||l!==void 0&&l!==1)))throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");const s=e.offset??(p({position:n})?8:30),c=e.itemGap??(d==="vertical"?L:28);return g(s,"Legend offset"),y(c,"Legend itemGap"),{position:n,layout:o,align:r,direction:d,columns:l,titlePosition:a,offset:s,itemGap:c}}function Re(e,t,n,i,{symbols:o,optional:r={},text:d=i.labels,labels:l=i.config.labels,titleStyle:a=i.config.titleStyle}){const s=`${t}Legend`,c=Object.values(o).find(Array.isArray)?.length??d.length;let f=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);f=u(f,`${s}Symbols`,{length:c,...o}),f=u(f,`${s}Labels`,{length:d.length,x:i.labelX,y:i.itemY,text:d}),f=fe(f,`${s}Background`,i.background,n.border),f=u(f,`${s}Symbols`,Object.fromEntries(Object.entries(r).filter(([,j])=>j!==void 0))),f=B(f,`${s}Labels`,l);const E=i.config;return E.titleVisible===!1?f:(f=u(f,`${s}Title`,{x:i.title.x,y:i.title.y,text:E.title}),B(f,`${s}Title`,a,{align:i.title.align}))}function Me(e,t,n,i,o,r){const d=`${t}Legend`;let l=e.editSemantic({property:`guide.legend.${t}.scale`,value:n.scale}).editSemantic({property:`guide.legend.${t}.title`,value:n.title})._withLegendConfig(t,n);return n.border!==!1&&(l=l.createGraphics({id:`${d}Background`,type:"rect",...r})),l=l.createGraphics({id:`${d}Symbols`,type:o,length:i,...r}).createGraphics({id:`${d}Labels`,type:"text",length:i,...r}),n.titleVisible!==!1&&(l=l.createGraphics({id:`${d}Title`,type:"text",...r})),l}export{de as ITEM_LEGEND_LABELS,ze as ITEM_LEGEND_OPTIONS,se as ITEM_LEGEND_TITLE_STYLE,De as assertLegendBoundsInsideCanvas,Me as createItemLegendGraphics,u as editGraphicProperties,fe as editLegendBackground,We as formatContinuousValues,Re as materializeItemLegend,Ce as normalizeContinuousLegend,N as normalizeInitialLegendTitle,Ne as normalizeItemLegendConfig,ce as normalizeItemLegendLayout,F as normalizeLegendBorder,w as normalizeLegendTextOptions,C as normalizeLegendTitleOptions,Be as requireResolvedLegendScale,je as resolveContinuousBounds,Pe as resolveContinuousColorLayer,Fe as resolveContinuousLegendLayer,Ue as resolveLegendBackgroundFromBounds,ke as resolveLegendTextBounds,Ae as sampleContinuousValues,P as selectLegendLayer,B as styleContinuousText,g as validateNonNegative,y as validatePositive};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { normalizeColorSymbols, resolveColorSymbolsLayout, materializeColorSymbols } from "./colorSymbols.js";
|
|
2
|
+
import { alignLegendTitle, resolveLegendTitleText } from "../../../../layout/legendText.js";
|
|
3
|
+
import { intrinsicLegendConfig } from "../../../../materialization/legendGroups.js";
|
|
4
|
+
import { mapContinuousScaleValues, transformedTicks } from "../../../../grammar/scales/index.js";
|
|
1
5
|
import { action, closedAction } from "../../../../core/action.js";
|
|
2
6
|
import { isPlainObject } from "../../../../core/immutable.js";
|
|
3
|
-
import {
|
|
7
|
+
import { normalizeLegendGradient, DEFAULT_LEGEND_GRADIENT } from "../../../../grammar/legendGradient.js";
|
|
4
8
|
import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
|
|
5
9
|
import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
|
|
6
10
|
import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
|
|
@@ -16,31 +20,32 @@ import {
|
|
|
16
20
|
resolveLegendBackgroundFromBounds,
|
|
17
21
|
resolveLegendTextBounds,
|
|
18
22
|
sampleContinuousValues,
|
|
19
|
-
styleContinuousText
|
|
20
|
-
validatePositive
|
|
23
|
+
styleContinuousText
|
|
21
24
|
} from "./common.js";
|
|
22
25
|
import { resolveLegendGraphicPlacement } from
|
|
23
26
|
"../../../../materialization/graphicHierarchy.js";
|
|
24
27
|
import { resolveEffectiveLegendBlockConfig } from "../blocks.js";
|
|
25
28
|
|
|
26
|
-
export const DEFAULT_GRADIENT_SIZE =
|
|
29
|
+
export const DEFAULT_GRADIENT_SIZE = DEFAULT_LEGEND_GRADIENT;
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
export function resolveGradientLayout(program, config, scale, label = "Gradient legend", stripStrokeWidth = 0) {
|
|
32
|
+
if (config.representation === "symbols") return resolveColorSymbolsLayout(program, config, scale);
|
|
33
|
+
config = intrinsicLegendConfig(program, config);
|
|
31
34
|
const { plot, canvas } = resolveContinuousBounds(program);
|
|
32
35
|
const vertical = ["right", "left"].includes(config.position);
|
|
33
36
|
const length = config.gradient.length;
|
|
34
37
|
const thickness = config.gradient.thickness;
|
|
38
|
+
const titleText = config.titleVisible === false ? { height: 0, extraHeight: 0 }
|
|
39
|
+
: resolveLegendTitleText(config.title, config.titleStyle, program.materializationConfigs.textMetrics);
|
|
35
40
|
const titleGap = config.blockGap ?? 12;
|
|
36
41
|
let x;
|
|
37
42
|
let y;
|
|
38
43
|
if (config.position === "right") {
|
|
39
44
|
x = plot.x + plot.width + config.offset;
|
|
40
|
-
y = plot.y + 46 + titleGap - 12;
|
|
45
|
+
y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
|
|
41
46
|
} else if (config.position === "left") {
|
|
42
47
|
x = plot.x - config.offset - thickness;
|
|
43
|
-
y = plot.y + 46 + titleGap - 12;
|
|
48
|
+
y = plot.y + 46 + titleGap - 12 + titleText.extraHeight / 2;
|
|
44
49
|
} else {
|
|
45
50
|
x = config.align === "left" ? plot.x
|
|
46
51
|
: config.align === "right" ? plot.x + plot.width - length
|
|
@@ -49,16 +54,21 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
|
|
|
49
54
|
? plot.y - config.offset - thickness - config.labels.offset -
|
|
50
55
|
config.labels.fontSize
|
|
51
56
|
: plot.y + plot.height + config.offset +
|
|
52
|
-
(config.titleVisible === false ? 0 :
|
|
57
|
+
(config.titleVisible === false ? 0 : titleText.height + titleGap);
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
let title = vertical
|
|
55
60
|
? { x, y: plot.y + 20, align: "left" }
|
|
56
61
|
: {
|
|
57
62
|
x: x + length / 2,
|
|
58
|
-
y: y - titleGap -
|
|
63
|
+
y: y - titleGap - titleText.height / 2,
|
|
59
64
|
align: "center"
|
|
60
65
|
};
|
|
61
|
-
const
|
|
66
|
+
const transformed = ["log", "sqrt", "symlog"].includes(scale.type);
|
|
67
|
+
const values = transformed
|
|
68
|
+
? transformedTicks(scale.type, scale.domain, config.count,
|
|
69
|
+
scale.type === "log" ? {base: scale.base}
|
|
70
|
+
: scale.type === "symlog" ? {constant: scale.constant} : {})
|
|
71
|
+
: [...sampleContinuousValues(scale.domain, config.count)];
|
|
62
72
|
if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
|
|
63
73
|
values.push(scale.midpoint);
|
|
64
74
|
values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
|
|
@@ -69,7 +79,8 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
|
|
|
69
79
|
config.fieldType,
|
|
70
80
|
config.labels.format
|
|
71
81
|
);
|
|
72
|
-
const fractions = values
|
|
82
|
+
const fractions = transformed ? mapContinuousScaleValues(values, {...scale, range: [0, 1]})
|
|
83
|
+
: values.map((value, index) => scale.midpoint === undefined
|
|
73
84
|
? index / (values.length - 1)
|
|
74
85
|
: inverseLerp(value, ...scale.domain));
|
|
75
86
|
const labelOffset = config.labels.offset;
|
|
@@ -114,11 +125,15 @@ export function resolveGradientLayout(program, config, scale, label = "Gradient
|
|
|
114
125
|
const labelBounds = labels.map((label, index) =>
|
|
115
126
|
resolveLegendTextBounds(label, texts[index], config.labels, program.materializationConfigs.textMetrics)
|
|
116
127
|
);
|
|
128
|
+
if (config.titleVisible !== false) title = alignLegendTitle(title, config.title, config.titleStyle,
|
|
129
|
+
[{ left: stripBounds.left - stripStrokeWidth / 2, right: stripBounds.right + stripStrokeWidth / 2,
|
|
130
|
+
top: stripBounds.top - stripStrokeWidth / 2, bottom: stripBounds.bottom + stripStrokeWidth / 2 },
|
|
131
|
+
...tickBounds, ...labelBounds], program.materializationConfigs.textMetrics);
|
|
117
132
|
const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
|
|
118
133
|
title,
|
|
119
134
|
config.title,
|
|
120
135
|
config.titleStyle
|
|
121
|
-
, program.materializationConfigs.textMetrics);
|
|
136
|
+
, program.materializationConfigs.textMetrics, { hardBreaks: true });
|
|
122
137
|
const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
|
|
123
138
|
...(config.titleVisible === false ? [] : [titleBounds])];
|
|
124
139
|
assertLegendBoundsInsideCanvas(
|
|
@@ -173,14 +188,15 @@ export function materializeGradientLegend(program, resolved, appearance) {
|
|
|
173
188
|
const prefix = `${channel}Gradient`;
|
|
174
189
|
const { scale, encoding, config: currentConfig } = resolved;
|
|
175
190
|
const config = resolveEffectiveLegendBlockConfig(program, kind, currentConfig);
|
|
191
|
+
if (!stroke && config.representation === "symbols") return materializeColorSymbols(program, resolved, config);
|
|
176
192
|
const layout = resolveGradientLayout(program, config, scale,
|
|
177
|
-
stroke ? "Stroke gradient legend" : "Gradient legend");
|
|
193
|
+
stroke ? "Stroke gradient legend" : "Gradient legend", appearance?.strokeWidth ?? 0);
|
|
178
194
|
const stripCount = 60;
|
|
179
195
|
const stripSize = layout.length / stripCount;
|
|
180
196
|
const strips = Array.from({ length: stripCount }, (_, index) => {
|
|
181
197
|
const fraction = (index + 0.5) / stripCount;
|
|
182
198
|
const position = layout.vertical ? 1 - fraction : fraction;
|
|
183
|
-
const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
|
|
199
|
+
const samplingScale = scale.midpoint === undefined ? { ...scale, type: "sequential", domain: [0, 1] } : scale;
|
|
184
200
|
const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
|
|
185
201
|
const [color] = mapScaleConsumerValues([value], samplingScale, channel);
|
|
186
202
|
return {
|
|
@@ -204,6 +220,9 @@ export function materializeGradientLegend(program, resolved, appearance) {
|
|
|
204
220
|
for (const property of ["x", "y", "width", "height", "fill", "stroke", "strokeWidth"]) {
|
|
205
221
|
next = next.editGraphics({ target: `${prefix}Strips`, property, value: strips.map(strip => strip[property]) });
|
|
206
222
|
}
|
|
223
|
+
if (config.gradient.opacity !== undefined) {
|
|
224
|
+
next = next.editGraphics({ target: `${prefix}Strips`, property: "opacity", value: config.gradient.opacity });
|
|
225
|
+
}
|
|
207
226
|
next = next.editGraphics({ target: `${prefix}Ticks`, property: "length", value: layout.ticks.length });
|
|
208
227
|
for (const property of ["x1", "y1", "x2", "y2"]) {
|
|
209
228
|
next = next.editGraphics({ target: `${prefix}Ticks`, property, value: layout.ticks.map(tick => tick[property]) });
|
|
@@ -235,7 +254,10 @@ export const rematerializeGradientLegend = /* @__PURE__ */ closedAction(
|
|
|
235
254
|
|
|
236
255
|
|
|
237
256
|
export function resolveGradientLegendCreation(program, args = {}, channel = "color") {
|
|
238
|
-
const
|
|
257
|
+
const { representation, ...options } = args;
|
|
258
|
+
if (representation !== undefined && !["gradient", "symbols"].includes(representation)) throw new Error("Legend representation must be gradient or symbols.");
|
|
259
|
+
if (channel !== "color" && representation !== undefined) throw new Error("Legend representation requires continuous color.");
|
|
260
|
+
const config = representation === "symbols" ? normalizeColorSymbols(options) : normalizeContinuousLegend(options, "gradient");
|
|
239
261
|
if (args.channels !== undefined && (
|
|
240
262
|
!Array.isArray(args.channels) ||
|
|
241
263
|
args.channels.length !== 1 ||
|
|
@@ -246,18 +268,7 @@ export function resolveGradientLegendCreation(program, args = {}, channel = "col
|
|
|
246
268
|
if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
|
|
247
269
|
throw new TypeError("createLegend.gradient must be a plain object.");
|
|
248
270
|
}
|
|
249
|
-
|
|
250
|
-
args.gradient ?? {},
|
|
251
|
-
GRADIENT_OPTIONS,
|
|
252
|
-
"createLegend.gradient"
|
|
253
|
-
);
|
|
254
|
-
config.gradient = {
|
|
255
|
-
length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
|
|
256
|
-
thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
|
|
257
|
-
};
|
|
258
|
-
config.titleVisible = true;
|
|
259
|
-
validatePositive(config.gradient.length, "Gradient length");
|
|
260
|
-
validatePositive(config.gradient.thickness, "Gradient thickness");
|
|
271
|
+
if (config.representation !== "symbols") config.gradient = normalizeLegendGradient(args.gradient);
|
|
261
272
|
const resolved = resolveGradientConfig(program, config, channel);
|
|
262
273
|
return resolved;
|
|
263
274
|
}
|
|
@@ -284,7 +295,9 @@ export function createGradientLegendFromConfig(program, config, channel = "color
|
|
|
284
295
|
...resolveLegendGraphicPlacement(next)
|
|
285
296
|
});
|
|
286
297
|
}
|
|
287
|
-
|
|
298
|
+
if (resolved.config.representation === "symbols") {
|
|
299
|
+
next = next.createGraphics({ id: `${prefix}Symbols`, type: "circle", length: 0, ...resolveLegendGraphicPlacement(next) });
|
|
300
|
+
} else next = next
|
|
288
301
|
.createGraphics({
|
|
289
302
|
id: `${prefix}Strips`,
|
|
290
303
|
type: "rect",
|
|
@@ -298,8 +311,8 @@ export function createGradientLegendFromConfig(program, config, channel = "color
|
|
|
298
311
|
type: "line",
|
|
299
312
|
length: 0,
|
|
300
313
|
...resolveLegendGraphicPlacement(next)
|
|
301
|
-
})
|
|
302
|
-
|
|
314
|
+
});
|
|
315
|
+
next = next.createGraphics({
|
|
303
316
|
id: `${prefix}Labels`,
|
|
304
317
|
type: "text",
|
|
305
318
|
length: 0,
|
|
@@ -323,7 +336,7 @@ export const createGradientLegend = /* @__PURE__ */ action(
|
|
|
323
336
|
function (args = {}) {
|
|
324
337
|
const resolved = resolveGradientLegendCreation(this, args);
|
|
325
338
|
resolveGradientLayout(this, resolved.config, resolved.scale);
|
|
326
|
-
if (this.
|
|
339
|
+
if (this.guideConfigs.legend?.gradient !== undefined) {
|
|
327
340
|
throw new Error(
|
|
328
341
|
"createGradientLegend requires a missing gradient legend."
|
|
329
342
|
);
|