ggaction 0.0.9 → 0.0.11
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 +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { isPlainObject } from "../../../../core/immutable.js";
|
|
2
2
|
import { noOptions } from "../../../../core/validation.js";
|
|
3
3
|
import { mapOrdinalValues } from "../../../../grammar/scales/index.js";
|
|
4
|
-
import { resolveGraphicBounds } from "../../../../layout/canvas.js";
|
|
5
4
|
import { unionConcreteGraphicBounds } from "../../../../grammar/schemas/graphicBounds.js";
|
|
6
5
|
import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
|
|
6
|
+
import { formatVisibleText } from "../../../../core/textMetrics.js";
|
|
7
|
+
import {
|
|
8
|
+
assertLegendBoundsInsideCanvas,
|
|
9
|
+
formatContinuousValues,
|
|
10
|
+
resolveContinuousBounds,
|
|
11
|
+
resolveLegendBackgroundFromBounds,
|
|
12
|
+
resolveLegendTextBounds,
|
|
13
|
+
sampleContinuousValues
|
|
14
|
+
} from "../continuous/common.js";
|
|
7
15
|
import {
|
|
8
16
|
alignLegendStart,
|
|
9
17
|
measureLegendSymbolHeight,
|
|
10
18
|
measureLegendTextWidth,
|
|
11
19
|
resolveLegendGrid
|
|
12
20
|
} from "../../../../layout/legend.js";
|
|
13
|
-
import { findCanvasGraphic } from
|
|
14
|
-
"../../../../materialization/graphicHierarchy.js";
|
|
15
21
|
|
|
16
22
|
export function activeConfig(program) {
|
|
17
|
-
const
|
|
18
|
-
.filter(kind => program.guideConfigs.legend?.[kind] !== undefined)
|
|
19
|
-
|
|
20
|
-
if (entries.length !== 1) {
|
|
23
|
+
const kinds = ["series", "color"]
|
|
24
|
+
.filter(kind => program.guideConfigs.legend?.[kind] !== undefined);
|
|
25
|
+
if (kinds.length !== 1) {
|
|
21
26
|
throw new Error("Legend component requires one categorical legend config.");
|
|
22
27
|
}
|
|
23
|
-
|
|
28
|
+
const kind = kinds[0];
|
|
29
|
+
return { kind, config: program.guideConfigs.legend[kind] };
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
function prefix(config) {
|
|
@@ -55,38 +61,43 @@ function leftGuideBoundary(program, bounds) {
|
|
|
55
61
|
return occupied === undefined ? bounds.x : Math.min(bounds.x, occupied.left);
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
function textBounds(x, y, value, style, textAlign = "left") {
|
|
65
|
+
return resolveLegendTextBounds(
|
|
66
|
+
{ x, y, align: textAlign },
|
|
67
|
+
formatVisibleText(value),
|
|
68
|
+
style
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
function resolveLeftSizeMetrics(program, sizeConfig) {
|
|
59
73
|
if (sizeConfig === undefined) return undefined;
|
|
60
74
|
const scale = program.resolvedScales[sizeConfig.scale];
|
|
61
75
|
if (scale?.type !== "linear") {
|
|
62
76
|
throw new Error(`Legend requires resolved linear scale "${sizeConfig.scale}".`);
|
|
63
77
|
}
|
|
64
|
-
const values =
|
|
65
|
-
|
|
66
|
-
(_, index) => scale.domain[0] +
|
|
67
|
-
index / (sizeConfig.count - 1) * (scale.domain[1] - scale.domain[0])
|
|
68
|
-
);
|
|
69
|
-
const labels = values.map(value => String(+value.toPrecision(3)));
|
|
78
|
+
const values = sampleContinuousValues(scale.domain, sizeConfig.count);
|
|
79
|
+
const labels = formatContinuousValues(values, scale.domain, "quantitative");
|
|
70
80
|
const maximumRadius = Math.sqrt(Math.max(...scale.range) / Math.PI);
|
|
71
|
-
return {
|
|
81
|
+
return { values, labels, maximumRadius };
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
function resolveLeftLayout(program, bounds, canvas, config, width, count) {
|
|
75
85
|
const sizeConfig = program.guideConfigs.legend?.size;
|
|
76
86
|
const size = resolveLeftSizeMetrics(program, sizeConfig);
|
|
77
87
|
const padding = config.border === false ? 0 : config.border.padding;
|
|
78
|
-
const categoricalWidth = Math.max(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
const categoricalWidth = config.domain.reduce((maximum, value) => Math.max(
|
|
89
|
+
maximum,
|
|
90
|
+
width + config.labels.offset +
|
|
91
|
+
measureLegendTextWidth(value, config.labels)
|
|
92
|
+
), config.titleVisible === false
|
|
93
|
+
? 0
|
|
94
|
+
: measureLegendTextWidth(config.title, config.titleStyle));
|
|
84
95
|
const sizeWidth = size === undefined
|
|
85
96
|
? 0
|
|
86
|
-
: Math.max(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
97
|
+
: size.labels.reduce((maximum, label) => Math.max(
|
|
98
|
+
maximum,
|
|
99
|
+
44 + measureLegendTextWidth(label, config.labels)
|
|
100
|
+
), measureLegendTextWidth(sizeConfig.title, config.titleStyle));
|
|
90
101
|
const contentWidth = Math.max(categoricalWidth, sizeWidth);
|
|
91
102
|
const occupiedRight = bounds.x - config.offset;
|
|
92
103
|
const contentRight = occupiedRight - padding;
|
|
@@ -122,7 +133,7 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
|
|
|
122
133
|
Math.max(config.labels.fontSize, measureLegendSymbolHeight(config)) / 2;
|
|
123
134
|
const blockTop = Math.min(categoricalTop, sizeTitleY - config.titleStyle.fontSize / 2);
|
|
124
135
|
const blockBottom = Math.max(categoricalBottom, sizeBottom);
|
|
125
|
-
if (blockTop < 0 || blockBottom > canvas.
|
|
136
|
+
if (blockTop < 0 || blockBottom > canvas.height) {
|
|
126
137
|
throw new Error("Legend layout requires more vertical Canvas space.");
|
|
127
138
|
}
|
|
128
139
|
let background;
|
|
@@ -139,8 +150,8 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
|
|
|
139
150
|
};
|
|
140
151
|
if (
|
|
141
152
|
background.x < 0 || background.y < 0 ||
|
|
142
|
-
background.x + background.width > canvas.
|
|
143
|
-
background.y + background.height > canvas.
|
|
153
|
+
background.x + background.width > canvas.width ||
|
|
154
|
+
background.y + background.height > canvas.height
|
|
144
155
|
) {
|
|
145
156
|
throw new Error("Legend background requires more left-margin space.");
|
|
146
157
|
}
|
|
@@ -155,7 +166,6 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
|
|
|
155
166
|
blockTop,
|
|
156
167
|
blockBottom,
|
|
157
168
|
size: size === undefined ? undefined : {
|
|
158
|
-
...size,
|
|
159
169
|
titleX: start,
|
|
160
170
|
titleY: sizeTitleY,
|
|
161
171
|
itemY: sizeItemY,
|
|
@@ -165,122 +175,65 @@ function resolveLeftLayout(program, bounds, canvas, config, width, count) {
|
|
|
165
175
|
};
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
function
|
|
178
|
+
function resolveGridLayout(program, bounds, canvas, config, width, count, top) {
|
|
169
179
|
const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
|
|
170
180
|
resolveLegendGrid(config, width, count);
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
181
|
+
const inlineTitle = config.titlePosition === "left";
|
|
182
|
+
const titleWidth = config.titleVisible === false
|
|
183
|
+
? 0
|
|
184
|
+
: measureLegendTextWidth(config.title, config.titleStyle);
|
|
185
|
+
const titleGap = inlineTitle ? 20 : 12;
|
|
186
|
+
const totalWidth = inlineTitle
|
|
174
187
|
? titleWidth + titleGap + gridWidth
|
|
175
188
|
: Math.max(titleWidth, gridWidth);
|
|
176
189
|
const start = alignLegendStart(bounds, totalWidth, config.align);
|
|
177
|
-
if (start < 0 || start + totalWidth > canvas.
|
|
190
|
+
if (start < 0 || start + totalWidth > canvas.width) {
|
|
178
191
|
throw new Error("Legend layout requires more horizontal Canvas space.");
|
|
179
192
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
193
|
+
const edge = top
|
|
194
|
+
? bounds.y - config.offset
|
|
195
|
+
: bounds.y + bounds.height + config.offset;
|
|
196
|
+
const gridStart = inlineTitle
|
|
182
197
|
? start + titleWidth + titleGap
|
|
183
198
|
: start + (totalWidth - gridWidth) / 2;
|
|
184
|
-
const gridTop =
|
|
185
|
-
|
|
186
|
-
?
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const backgroundWidth = totalWidth + config.border.padding * 2;
|
|
221
|
-
const height = blockBottom - blockTop + config.border.padding * 2;
|
|
222
|
-
if (x < 0 || y < 0 || x + backgroundWidth > canvas.properties.width) {
|
|
223
|
-
throw new Error("Legend background requires more top or horizontal space.");
|
|
199
|
+
const gridTop = top
|
|
200
|
+
? edge - gridHeight
|
|
201
|
+
: inlineTitle ? edge : edge + config.titleStyle.fontSize + titleGap;
|
|
202
|
+
const blockTop = top
|
|
203
|
+
? inlineTitle
|
|
204
|
+
? Math.min(
|
|
205
|
+
gridTop,
|
|
206
|
+
edge / 2 + gridTop / 2 - config.titleStyle.fontSize / 2
|
|
207
|
+
)
|
|
208
|
+
: gridTop - titleGap - config.titleStyle.fontSize
|
|
209
|
+
: edge;
|
|
210
|
+
const blockBottom = top ? edge : gridTop + gridHeight;
|
|
211
|
+
if (top) {
|
|
212
|
+
if (blockTop < 0) {
|
|
213
|
+
throw new Error("Legend layout requires more top-margin space.");
|
|
214
|
+
}
|
|
215
|
+
const titleGraphic = program.graphicSpec.objects.chartSubtitle ??
|
|
216
|
+
program.graphicSpec.objects.chartTitle;
|
|
217
|
+
if (titleGraphic?.type === "text" &&
|
|
218
|
+
titleGraphic.properties.y + titleGraphic.properties.fontSize / 2 >= blockTop) {
|
|
219
|
+
throw new Error("Top legend and chart title require more top-margin space.");
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const occupiedTop = config.border === false
|
|
223
|
+
? blockTop
|
|
224
|
+
: blockTop - config.border.padding;
|
|
225
|
+
const axisTitle = program.graphicSpec.objects.xAxisTitle;
|
|
226
|
+
if (axisTitle?.type === "text" &&
|
|
227
|
+
axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop) {
|
|
228
|
+
throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
|
|
229
|
+
}
|
|
230
|
+
const occupiedBottom = config.border === false
|
|
231
|
+
? blockBottom
|
|
232
|
+
: blockBottom + config.border.padding;
|
|
233
|
+
if (occupiedTop < 0 || occupiedBottom > canvas.height) {
|
|
234
|
+
throw new Error("Legend layout requires more bottom-margin space.");
|
|
224
235
|
}
|
|
225
|
-
background = { x, y, width: backgroundWidth, height };
|
|
226
|
-
}
|
|
227
|
-
return {
|
|
228
|
-
symbolX,
|
|
229
|
-
itemY,
|
|
230
|
-
labelX,
|
|
231
|
-
titleX,
|
|
232
|
-
titleY,
|
|
233
|
-
background,
|
|
234
|
-
blockTop,
|
|
235
|
-
blockBottom
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function resolveBottomLayout(program, bounds, canvas, config, width, count) {
|
|
240
|
-
const { cells, columnWidths, gridWidth, gridHeight, rowHeight } =
|
|
241
|
-
resolveLegendGrid(config, width, count);
|
|
242
|
-
const titleWidth = config.title.length * 7;
|
|
243
|
-
const titleGap = config.titlePosition === "left" ? 20 : 12;
|
|
244
|
-
const totalWidth = config.titlePosition === "left"
|
|
245
|
-
? titleWidth + titleGap + gridWidth
|
|
246
|
-
: Math.max(titleWidth, gridWidth);
|
|
247
|
-
const start = alignLegendStart(bounds, totalWidth, config.align);
|
|
248
|
-
if (start < 0 || start + totalWidth > canvas.properties.width) {
|
|
249
|
-
throw new Error("Legend layout requires more horizontal Canvas space.");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const blockTop = bounds.y + bounds.height + config.offset;
|
|
253
|
-
const gridStart = config.titlePosition === "left"
|
|
254
|
-
? start + titleWidth + titleGap
|
|
255
|
-
: start + (totalWidth - gridWidth) / 2;
|
|
256
|
-
const gridTop = config.titlePosition === "left"
|
|
257
|
-
? blockTop
|
|
258
|
-
: blockTop + config.titleStyle.fontSize + titleGap;
|
|
259
|
-
const blockBottom = gridTop + gridHeight;
|
|
260
|
-
const titleX = config.titlePosition === "left"
|
|
261
|
-
? start
|
|
262
|
-
: start + totalWidth / 2;
|
|
263
|
-
const titleY = config.titlePosition === "left"
|
|
264
|
-
? gridTop + gridHeight / 2
|
|
265
|
-
: blockTop + config.titleStyle.fontSize / 2;
|
|
266
|
-
|
|
267
|
-
const axisTitle = program.graphicSpec.objects.xAxisTitle;
|
|
268
|
-
const occupiedTop = config.border === false
|
|
269
|
-
? blockTop
|
|
270
|
-
: blockTop - config.border.padding;
|
|
271
|
-
if (
|
|
272
|
-
axisTitle?.type === "text" &&
|
|
273
|
-
axisTitle.properties.y + axisTitle.properties.fontSize / 2 >= occupiedTop
|
|
274
|
-
) {
|
|
275
|
-
throw new Error("Bottom legend and x-axis title require more bottom-margin space.");
|
|
276
|
-
}
|
|
277
|
-
const occupiedBottom = config.border === false
|
|
278
|
-
? blockBottom
|
|
279
|
-
: blockBottom + config.border.padding;
|
|
280
|
-
if (occupiedTop < 0 || occupiedBottom > canvas.properties.height) {
|
|
281
|
-
throw new Error("Legend layout requires more bottom-margin space.");
|
|
282
236
|
}
|
|
283
|
-
|
|
284
237
|
const columnX = [];
|
|
285
238
|
let cursor = gridStart;
|
|
286
239
|
for (const columnWidth of columnWidths) {
|
|
@@ -292,6 +245,12 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
|
|
|
292
245
|
const itemY = cells.map(cell =>
|
|
293
246
|
gridTop + rowHeight / 2 + cell.row * (rowHeight + config.itemGap)
|
|
294
247
|
);
|
|
248
|
+
const titleX = inlineTitle ? start : start + totalWidth / 2;
|
|
249
|
+
const titleY = inlineTitle
|
|
250
|
+
? gridTop + gridHeight / 2
|
|
251
|
+
: top
|
|
252
|
+
? gridTop - titleGap - config.titleStyle.fontSize / 2
|
|
253
|
+
: blockTop + config.titleStyle.fontSize / 2;
|
|
295
254
|
let background;
|
|
296
255
|
if (config.border !== false) {
|
|
297
256
|
background = {
|
|
@@ -300,13 +259,10 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
|
|
|
300
259
|
width: totalWidth + config.border.padding * 2,
|
|
301
260
|
height: blockBottom - blockTop + config.border.padding * 2
|
|
302
261
|
};
|
|
303
|
-
if (
|
|
304
|
-
background.x
|
|
305
|
-
background.y
|
|
306
|
-
|
|
307
|
-
background.y + background.height > canvas.properties.height
|
|
308
|
-
) {
|
|
309
|
-
throw new Error("Legend background requires more bottom or horizontal space.");
|
|
262
|
+
if (background.x < 0 || background.y < 0 ||
|
|
263
|
+
background.x + background.width > canvas.width ||
|
|
264
|
+
(!top && background.y + background.height > canvas.height)) {
|
|
265
|
+
throw new Error(`Legend background requires more ${top ? "top" : "bottom"} or horizontal space.`);
|
|
310
266
|
}
|
|
311
267
|
}
|
|
312
268
|
return {
|
|
@@ -322,16 +278,17 @@ function resolveBottomLayout(program, bounds, canvas, config, width, count) {
|
|
|
322
278
|
}
|
|
323
279
|
|
|
324
280
|
function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
|
|
325
|
-
const labels = config.domain.map(
|
|
281
|
+
const labels = config.domain.map(formatVisibleText);
|
|
326
282
|
const itemWidths = labels.map(
|
|
327
|
-
label => width + config.labels.offset +
|
|
283
|
+
label => width + config.labels.offset +
|
|
284
|
+
measureLegendTextWidth(label, config.labels)
|
|
328
285
|
);
|
|
329
286
|
const totalWidth = itemWidths.reduce((sum, value) => sum + value, 0) +
|
|
330
287
|
config.itemGap * Math.max(0, count - 1);
|
|
331
288
|
const start = config.align === "center"
|
|
332
|
-
? (canvas.
|
|
289
|
+
? (canvas.width - totalWidth) / 2
|
|
333
290
|
: alignLegendStart(bounds, totalWidth, config.align);
|
|
334
|
-
if (start < 0 || start + totalWidth > canvas.
|
|
291
|
+
if (start < 0 || start + totalWidth > canvas.width) {
|
|
335
292
|
throw new Error("Legend layout requires more horizontal Canvas space.");
|
|
336
293
|
}
|
|
337
294
|
const symbolX = [];
|
|
@@ -342,9 +299,9 @@ function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
|
|
|
342
299
|
labelX.push(cursor + width + config.labels.offset);
|
|
343
300
|
cursor += itemWidths[index] + config.itemGap;
|
|
344
301
|
}
|
|
345
|
-
const itemY = Array(count).fill(canvas.
|
|
302
|
+
const itemY = Array(count).fill(canvas.height - 28);
|
|
346
303
|
const titleX = start + totalWidth / 2;
|
|
347
|
-
const titleY = canvas.
|
|
304
|
+
const titleY = canvas.height - 52;
|
|
348
305
|
if (titleY <= bounds.y + bounds.height) {
|
|
349
306
|
throw new Error("Legend layout requires more bottom-margin space.");
|
|
350
307
|
}
|
|
@@ -362,22 +319,24 @@ function resolveCompactBottomLayout(bounds, canvas, config, width, count) {
|
|
|
362
319
|
width: totalWidth + config.border.padding * 2,
|
|
363
320
|
height: itemY[0] + maxHeight / 2 + config.border.padding - y
|
|
364
321
|
};
|
|
322
|
+
if (
|
|
323
|
+
background.x < 0 || background.y < 0 ||
|
|
324
|
+
background.x + background.width > canvas.width ||
|
|
325
|
+
background.y + background.height > canvas.height
|
|
326
|
+
) {
|
|
327
|
+
throw new Error("Legend background requires more bottom or horizontal space.");
|
|
328
|
+
}
|
|
365
329
|
}
|
|
366
330
|
return { symbolX, itemY, labelX, titleX, titleY, background };
|
|
367
331
|
}
|
|
368
332
|
|
|
369
333
|
export function resolveLayout(program, config) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
!Number.isFinite(canvas.properties.width) ||
|
|
377
|
-
!Number.isFinite(canvas.properties.height)
|
|
378
|
-
) {
|
|
379
|
-
throw new Error("Legend layout requires Canvas bounds, width, and height.");
|
|
380
|
-
}
|
|
334
|
+
const resolved = resolveContinuousBounds(
|
|
335
|
+
program,
|
|
336
|
+
"Legend layout requires Canvas bounds, width, and height."
|
|
337
|
+
);
|
|
338
|
+
const bounds = resolved.plot;
|
|
339
|
+
const canvas = resolved.canvas;
|
|
381
340
|
|
|
382
341
|
const count = config.domain.length;
|
|
383
342
|
const width = symbolWidth(config);
|
|
@@ -390,23 +349,37 @@ export function resolveLayout(program, config) {
|
|
|
390
349
|
(_, index) => bounds.y + 52 + index * config.itemGap
|
|
391
350
|
);
|
|
392
351
|
const labelX = symbolX.map(value => value + width + config.labels.offset);
|
|
393
|
-
if (labelX.some(value => value >= canvas.properties.width)) {
|
|
394
|
-
throw new Error("Legend layout requires more right-margin space.");
|
|
395
|
-
}
|
|
396
352
|
const titleX = symbolX[0];
|
|
397
353
|
const titleY = bounds.y + 20;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
354
|
+
const labelBounds = config.domain.map((value, index) => textBounds(
|
|
355
|
+
labelX[index], itemY[index], value, config.labels
|
|
356
|
+
));
|
|
357
|
+
const titleBounds = config.titleVisible === false
|
|
358
|
+
? undefined
|
|
359
|
+
: textBounds(titleX, titleY, config.title, config.titleStyle);
|
|
360
|
+
const symbolHeight = measureLegendSymbolHeight(config);
|
|
361
|
+
const symbolBounds = itemY.map(y => ({
|
|
362
|
+
left: symbolX[0],
|
|
363
|
+
right: symbolX[0] + width,
|
|
364
|
+
top: y - symbolHeight / 2,
|
|
365
|
+
bottom: y + symbolHeight / 2
|
|
366
|
+
}));
|
|
367
|
+
const contentBounds = [
|
|
368
|
+
...labelBounds,
|
|
369
|
+
...symbolBounds,
|
|
370
|
+
...(titleBounds === undefined ? [] : [titleBounds])
|
|
371
|
+
];
|
|
372
|
+
assertLegendBoundsInsideCanvas(
|
|
373
|
+
contentBounds,
|
|
374
|
+
canvas,
|
|
375
|
+
"Right legend layout"
|
|
376
|
+
);
|
|
377
|
+
const background = resolveLegendBackgroundFromBounds(
|
|
378
|
+
contentBounds,
|
|
379
|
+
config.border,
|
|
380
|
+
canvas,
|
|
381
|
+
"Right legend"
|
|
382
|
+
);
|
|
410
383
|
return { symbolX, itemY, labelX, titleX, titleY, background };
|
|
411
384
|
}
|
|
412
385
|
|
|
@@ -415,13 +388,13 @@ export function resolveLayout(program, config) {
|
|
|
415
388
|
}
|
|
416
389
|
|
|
417
390
|
if (config.position === "top") {
|
|
418
|
-
return
|
|
391
|
+
return resolveGridLayout(program, bounds, canvas, config, width, count, true);
|
|
419
392
|
}
|
|
420
393
|
|
|
421
394
|
if (config.bottomGrid !== true) {
|
|
422
395
|
return resolveCompactBottomLayout(bounds, canvas, config, width, count);
|
|
423
396
|
}
|
|
424
|
-
return
|
|
397
|
+
return resolveGridLayout(program, bounds, canvas, config, width, count, false);
|
|
425
398
|
}
|
|
426
399
|
|
|
427
400
|
export function resolveAppearance(program, config) {
|
|
@@ -99,6 +99,11 @@ export function normalizeRecipe(symbol, kind) {
|
|
|
99
99
|
if (!Array.isArray(symbol.layers) || symbol.layers.length === 0) {
|
|
100
100
|
throw new TypeError("Legend symbol layers must be a non-empty array.");
|
|
101
101
|
}
|
|
102
|
+
if (symbol.layers.length > 3) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Legend symbol recipe supports at most one layer per type."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
102
107
|
layers = symbol.layers.map(normalizeLayer);
|
|
103
108
|
} else if (kind === "series") {
|
|
104
109
|
validateKeys(symbol, ["length", "lineWidth"], "createLegend.symbol");
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { validateUserId } from "../../../../core/identifiers.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
sameOrderedValues,
|
|
4
|
+
validateGeneratedItemLimit
|
|
5
|
+
} from "../../../../core/validation.js";
|
|
3
6
|
import { CHANNELS } from "./options.js";
|
|
4
7
|
import { nonEmptyString } from "./validation.js";
|
|
5
8
|
import { findLayer } from "../../../../selectors/layers.js";
|
|
@@ -59,6 +62,10 @@ function resolveOrdinalScales(program, scaleIds) {
|
|
|
59
62
|
if (!Array.isArray(concrete.domain) || concrete.domain.length === 0) {
|
|
60
63
|
throw new Error(`Legend scale "${id}" requires a non-empty domain.`);
|
|
61
64
|
}
|
|
65
|
+
validateGeneratedItemLimit(
|
|
66
|
+
concrete.domain.length,
|
|
67
|
+
"Categorical legend item count"
|
|
68
|
+
);
|
|
62
69
|
return concrete;
|
|
63
70
|
});
|
|
64
71
|
if (scales.slice(1).some(scale => !sameValues(scale.domain, scales[0].domain))) {
|