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
package/src/layout/legendLane.js
CHANGED
|
@@ -1,441 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const HORIZONTAL_LEGEND_BLOCK_GAP = 40;
|
|
3
|
-
export const SIDE_LEGEND_SYMBOL_CENTER = 16;
|
|
4
|
-
export const SIDE_LEGEND_LABEL_START = 44;
|
|
5
|
-
export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
|
|
6
|
-
|
|
7
|
-
export function isHorizontalEdgeLegend(config) {
|
|
8
|
-
return config !== undefined &&
|
|
9
|
-
["top", "bottom"].includes(config.position) &&
|
|
10
|
-
config.layout !== "legacy-bottom";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function resolveSingleHorizontalLegendPlacement({ plot, canvas, config, bounds }) {
|
|
14
|
-
if (!isHorizontalEdgeLegend(config)) {
|
|
15
|
-
throw new Error("Single legend placement requires a horizontal edge layout.");
|
|
16
|
-
}
|
|
17
|
-
const dx = config.align === "left" ? plot.x - bounds.left
|
|
18
|
-
: config.align === "right" ? plot.x + plot.width - bounds.right
|
|
19
|
-
: plot.x + plot.width / 2 - midpoint(bounds.left, bounds.right);
|
|
20
|
-
const dy = config.position === "top" ? plot.y - config.offset - bounds.bottom
|
|
21
|
-
: plot.y + plot.height + config.offset - bounds.top;
|
|
22
|
-
const occupied = translateBounds(bounds, dx, dy);
|
|
23
|
-
if (occupied.left < 0 || occupied.right > canvas.width || occupied.top < 0 || occupied.bottom > canvas.height) {
|
|
24
|
-
throw new Error(`Legend layout requires more ${config.position}-margin or Canvas space.`);
|
|
25
|
-
}
|
|
26
|
-
return { dx, dy, occupied };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function midpoint(start, end) {
|
|
30
|
-
const sum = start + end;
|
|
31
|
-
const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
|
|
32
|
-
return result === 0 ? 0 : result;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function unionBounds(bounds) {
|
|
36
|
-
return bounds.reduce((union, item) => ({
|
|
37
|
-
left: Math.min(union.left, item.left),
|
|
38
|
-
right: Math.max(union.right, item.right),
|
|
39
|
-
top: Math.min(union.top, item.top),
|
|
40
|
-
bottom: Math.max(union.bottom, item.bottom)
|
|
41
|
-
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function decoration(border) {
|
|
45
|
-
return border === false
|
|
46
|
-
? 0
|
|
47
|
-
: border.padding + border.lineWidth / 2;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function horizontalExtent(blocks, labelStart) {
|
|
51
|
-
let left = 0;
|
|
52
|
-
let right = 0;
|
|
53
|
-
for (const block of blocks) {
|
|
54
|
-
const inset = block.inset ?? 0;
|
|
55
|
-
if (block.title !== undefined) right = Math.max(right, block.title.width + inset);
|
|
56
|
-
left = Math.min(left,
|
|
57
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX - inset
|
|
58
|
-
);
|
|
59
|
-
right = Math.max(
|
|
60
|
-
right,
|
|
61
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX + inset,
|
|
62
|
-
labelStart + block.labels.width + inset
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return { left, right };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function placeBlock(block, cursor, labelStart) {
|
|
69
|
-
let dy;
|
|
70
|
-
if (block.title === undefined) {
|
|
71
|
-
dy = cursor - block.bounds.top;
|
|
72
|
-
} else {
|
|
73
|
-
const desiredTitleY = Math.ceil(cursor + block.title.fontSize / 2);
|
|
74
|
-
dy = desiredTitleY - block.title.y;
|
|
75
|
-
const futureTop = block.bounds.top + dy;
|
|
76
|
-
if (futureTop < cursor) dy += cursor - futureTop;
|
|
77
|
-
}
|
|
78
|
-
const bounds = {
|
|
79
|
-
left: block.bounds.left,
|
|
80
|
-
right: block.bounds.right,
|
|
81
|
-
top: block.bounds.top + dy,
|
|
82
|
-
bottom: block.bounds.bottom + dy
|
|
83
|
-
};
|
|
84
|
-
return {
|
|
85
|
-
id: block.id,
|
|
86
|
-
dy,
|
|
87
|
-
titleDx: block.title === undefined
|
|
88
|
-
? undefined
|
|
89
|
-
: -block.title.x,
|
|
90
|
-
symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
|
|
91
|
-
labelDx: labelStart - block.labels.x,
|
|
92
|
-
bounds
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function overlap(a, b) {
|
|
97
|
-
return a.left < b.right && a.right > b.left &&
|
|
98
|
-
a.top < b.bottom && a.bottom > b.top;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function resolveSideLegendLane({
|
|
102
|
-
side,
|
|
103
|
-
plot,
|
|
104
|
-
canvas,
|
|
105
|
-
groups,
|
|
106
|
-
axisBounds
|
|
107
|
-
}) {
|
|
108
|
-
if (!["right", "left"].includes(side)) {
|
|
109
|
-
throw new Error(`Unsupported side legend lane "${side}".`);
|
|
110
|
-
}
|
|
111
|
-
const blocks = groups.flatMap(group => group.blocks);
|
|
112
|
-
if (blocks.length < 2) return undefined;
|
|
113
|
-
const labelStart = Math.max(SIDE_LEGEND_LABEL_START, ...blocks.map(
|
|
114
|
-
block => SIDE_LEGEND_SYMBOL_CENTER + Math.abs(block.labels.x - block.symbol.centerX)
|
|
115
|
-
));
|
|
116
|
-
const laneExtent = horizontalExtent(blocks, labelStart);
|
|
117
|
-
const offset = blocks.reduce(
|
|
118
|
-
(maximum, block) => Math.max(maximum, block.offset),
|
|
119
|
-
-Infinity
|
|
120
|
-
);
|
|
121
|
-
const titleStartX = side === "right"
|
|
122
|
-
? plot.x + plot.width + offset
|
|
123
|
-
: plot.x - offset - laneExtent.right;
|
|
124
|
-
const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
|
|
125
|
-
const labelStartX = titleStartX + labelStart;
|
|
126
|
-
const first = blocks[0];
|
|
127
|
-
let cursor = first.title === undefined
|
|
128
|
-
? plot.y + 12
|
|
129
|
-
: plot.y + 20 - first.title.fontSize / 2;
|
|
130
|
-
const placements = [];
|
|
131
|
-
const backgrounds = [];
|
|
132
|
-
const occupiedGroups = [];
|
|
133
|
-
|
|
134
|
-
for (const group of groups) {
|
|
135
|
-
const inset = decoration(group.border);
|
|
136
|
-
if (occupiedGroups.length > 0) cursor += inset;
|
|
137
|
-
const groupPlacements = [];
|
|
138
|
-
for (const block of group.blocks) {
|
|
139
|
-
const placement = placeBlock(block, cursor, labelStart);
|
|
140
|
-
groupPlacements.push(placement);
|
|
141
|
-
placements.push({
|
|
142
|
-
...placement,
|
|
143
|
-
titleDx: placement.titleDx === undefined
|
|
144
|
-
? undefined
|
|
145
|
-
: placement.titleDx + titleStartX,
|
|
146
|
-
symbolDx: placement.symbolDx + titleStartX,
|
|
147
|
-
labelDx: placement.labelDx + titleStartX
|
|
148
|
-
});
|
|
149
|
-
cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
150
|
-
}
|
|
151
|
-
const vertical = unionBounds(groupPlacements.map(item => item.bounds));
|
|
152
|
-
const groupExtent = horizontalExtent(group.blocks, labelStart);
|
|
153
|
-
const foreground = {
|
|
154
|
-
left: titleStartX + groupExtent.left,
|
|
155
|
-
right: titleStartX + groupExtent.right,
|
|
156
|
-
top: vertical.top,
|
|
157
|
-
bottom: vertical.bottom
|
|
158
|
-
};
|
|
159
|
-
const occupied = {
|
|
160
|
-
left: foreground.left - inset,
|
|
161
|
-
right: foreground.right + inset,
|
|
162
|
-
top: foreground.top - inset,
|
|
163
|
-
bottom: foreground.bottom + inset
|
|
164
|
-
};
|
|
165
|
-
occupiedGroups.push(occupied);
|
|
166
|
-
if (group.backgroundId !== undefined) {
|
|
167
|
-
backgrounds.push({
|
|
168
|
-
id: group.backgroundId,
|
|
169
|
-
x: foreground.left - group.border.padding,
|
|
170
|
-
y: foreground.top - group.border.padding,
|
|
171
|
-
width: foreground.right - foreground.left + group.border.padding * 2,
|
|
172
|
-
height: foreground.bottom - foreground.top + group.border.padding * 2
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
cursor = occupied.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const occupied = unionBounds(occupiedGroups);
|
|
179
|
-
if (
|
|
180
|
-
occupied.left < 0 || occupied.right > canvas.width ||
|
|
181
|
-
occupied.top < 0 || occupied.bottom > canvas.height
|
|
182
|
-
) {
|
|
183
|
-
throw new Error(`Legend lane requires more ${side}-margin or vertical Canvas space.`);
|
|
184
|
-
}
|
|
185
|
-
if (axisBounds !== undefined && overlap(occupied, axisBounds)) {
|
|
186
|
-
throw new Error(`${side[0].toUpperCase()}${side.slice(1)} legend lane and y-axis guides require more margin space.`);
|
|
187
|
-
}
|
|
188
|
-
return {
|
|
189
|
-
side,
|
|
190
|
-
titleStartX,
|
|
191
|
-
symbolCenterX,
|
|
192
|
-
labelStartX,
|
|
193
|
-
placements,
|
|
194
|
-
backgrounds,
|
|
195
|
-
occupied
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function translateBounds(bounds, dx, dy) {
|
|
200
|
-
return {
|
|
201
|
-
left: bounds.left + dx,
|
|
202
|
-
right: bounds.right + dx,
|
|
203
|
-
top: bounds.top + dy,
|
|
204
|
-
bottom: bounds.bottom + dy
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function expandBounds(bounds, inset) {
|
|
209
|
-
return {
|
|
210
|
-
left: bounds.left - inset,
|
|
211
|
-
right: bounds.right + inset,
|
|
212
|
-
top: bounds.top - inset,
|
|
213
|
-
bottom: bounds.bottom + inset
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function packHorizontalRows(groups, plot) {
|
|
218
|
-
const rows = [];
|
|
219
|
-
let row = [];
|
|
220
|
-
let cursor = plot.x;
|
|
221
|
-
for (const group of groups) {
|
|
222
|
-
const interval = expandBounds(group.horizontal, group.inset);
|
|
223
|
-
const width = interval.right - interval.left;
|
|
224
|
-
if (width > plot.width) {
|
|
225
|
-
throw new Error("Horizontal legend block requires more plot width.");
|
|
226
|
-
}
|
|
227
|
-
let dx = cursor - interval.left;
|
|
228
|
-
let placed = translateBounds(interval, dx, 0);
|
|
229
|
-
if (row.length > 0 && placed.right > plot.x + plot.width) {
|
|
230
|
-
rows.push(row);
|
|
231
|
-
row = [];
|
|
232
|
-
cursor = plot.x;
|
|
233
|
-
dx = cursor - interval.left;
|
|
234
|
-
placed = translateBounds(interval, dx, 0);
|
|
235
|
-
}
|
|
236
|
-
row.push({ group, interval: placed, dx });
|
|
237
|
-
cursor = placed.right + HORIZONTAL_LEGEND_BLOCK_GAP;
|
|
238
|
-
}
|
|
239
|
-
if (row.length > 0) rows.push(row);
|
|
240
|
-
return rows;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function normalizeHorizontalRow(entries) {
|
|
244
|
-
const stacked = entries.filter(entry =>
|
|
245
|
-
entry.group.title !== undefined && entry.group.inline !== true
|
|
246
|
-
);
|
|
247
|
-
const commonTitleY = stacked[0]?.group.title.y;
|
|
248
|
-
const titleDescent = stacked.length === 0
|
|
249
|
-
? 0
|
|
250
|
-
: stacked.reduce((maximum, entry) => Math.max(
|
|
251
|
-
maximum,
|
|
252
|
-
entry.group.title.bounds.bottom - entry.group.title.y
|
|
253
|
-
), -Infinity);
|
|
254
|
-
const elementAnchor = commonTitleY === undefined
|
|
255
|
-
? midpoint(
|
|
256
|
-
entries[0].group.element.top,
|
|
257
|
-
entries[0].group.element.bottom
|
|
258
|
-
)
|
|
259
|
-
: commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
|
|
260
|
-
return entries.map(({ group, dx }) => {
|
|
261
|
-
if (group.atomic) {
|
|
262
|
-
const contentDy = elementAnchor - group.element.top;
|
|
263
|
-
const foreground = translateBounds(group.content, dx, contentDy);
|
|
264
|
-
return { id: group.id, dx, titleDy: contentDy, contentDy, foreground,
|
|
265
|
-
occupied: expandBounds(foreground, group.inset), padding: group.padding,
|
|
266
|
-
backgroundId: group.backgroundId };
|
|
267
|
-
}
|
|
268
|
-
const contentDy = commonTitleY === undefined
|
|
269
|
-
? elementAnchor - midpoint(
|
|
270
|
-
group.element.top,
|
|
271
|
-
group.element.bottom
|
|
272
|
-
)
|
|
273
|
-
: elementAnchor - group.element.top;
|
|
274
|
-
const titleDy = group.title === undefined
|
|
275
|
-
? 0
|
|
276
|
-
: group.inline === true
|
|
277
|
-
? contentDy
|
|
278
|
-
: commonTitleY - group.title.y;
|
|
279
|
-
const foreground = unionBounds([
|
|
280
|
-
...(group.title === undefined
|
|
281
|
-
? []
|
|
282
|
-
: [translateBounds(group.title.bounds, dx, titleDy)]),
|
|
283
|
-
translateBounds(group.content, dx, contentDy)
|
|
284
|
-
]);
|
|
285
|
-
return {
|
|
286
|
-
id: group.id,
|
|
287
|
-
dx,
|
|
288
|
-
titleDy,
|
|
289
|
-
contentDy,
|
|
290
|
-
foreground,
|
|
291
|
-
occupied: expandBounds(foreground, group.inset),
|
|
292
|
-
padding: group.padding,
|
|
293
|
-
backgroundId: group.backgroundId
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function translateHorizontalPlacement(placement, dy) {
|
|
299
|
-
return {
|
|
300
|
-
...placement,
|
|
301
|
-
titleDy: placement.titleDy + dy,
|
|
302
|
-
contentDy: placement.contentDy + dy,
|
|
303
|
-
foreground: translateBounds(placement.foreground, 0, dy),
|
|
304
|
-
occupied: translateBounds(placement.occupied, 0, dy)
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function resolveHorizontalLegendLane({
|
|
309
|
-
edge,
|
|
310
|
-
plot,
|
|
311
|
-
canvas,
|
|
312
|
-
groups,
|
|
313
|
-
collisionBounds = []
|
|
314
|
-
}) {
|
|
315
|
-
if (!["top", "bottom"].includes(edge)) {
|
|
316
|
-
throw new Error(`Unsupported horizontal legend lane "${edge}".`);
|
|
317
|
-
}
|
|
318
|
-
if (groups.length < 2) return undefined;
|
|
319
|
-
const packed = packHorizontalRows(groups, plot);
|
|
320
|
-
const normalized = packed.map(normalizeHorizontalRow);
|
|
321
|
-
const placements = [];
|
|
322
|
-
let previousRowBounds;
|
|
323
|
-
for (let index = 0; index < normalized.length; index += 1) {
|
|
324
|
-
const row = normalized[index];
|
|
325
|
-
const rowBounds = unionBounds(row.map(item => item.occupied));
|
|
326
|
-
let dy;
|
|
327
|
-
if (index === 0) {
|
|
328
|
-
const anchor = edge === "top"
|
|
329
|
-
? packed[index].reduce((maximum, item) => Math.max(
|
|
330
|
-
maximum,
|
|
331
|
-
item.group.horizontal.bottom
|
|
332
|
-
), -Infinity)
|
|
333
|
-
: packed[index].reduce((minimum, item) => Math.min(
|
|
334
|
-
minimum,
|
|
335
|
-
item.group.horizontal.top
|
|
336
|
-
), Infinity);
|
|
337
|
-
dy = edge === "top"
|
|
338
|
-
? anchor - rowBounds.bottom
|
|
339
|
-
: anchor - rowBounds.top;
|
|
340
|
-
} else {
|
|
341
|
-
dy = edge === "bottom"
|
|
342
|
-
? previousRowBounds.bottom + HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.top
|
|
343
|
-
: previousRowBounds.top - HORIZONTAL_LEGEND_BLOCK_GAP - rowBounds.bottom;
|
|
344
|
-
}
|
|
345
|
-
const translated = row.map(item => translateHorizontalPlacement(item, dy));
|
|
346
|
-
placements.push(...translated);
|
|
347
|
-
previousRowBounds = unionBounds(translated.map(item => item.occupied));
|
|
348
|
-
}
|
|
349
|
-
const occupied = unionBounds(placements.map(item => item.occupied));
|
|
350
|
-
if (placements.some(placement =>
|
|
351
|
-
placement.occupied.left < 0 || placement.occupied.right > canvas.width ||
|
|
352
|
-
placement.occupied.top < 0 || placement.occupied.bottom > canvas.height
|
|
353
|
-
)) {
|
|
354
|
-
throw new Error(`Legend lane requires more ${edge}-margin or Canvas space.`);
|
|
355
|
-
}
|
|
356
|
-
if (placements.some(placement =>
|
|
357
|
-
collisionBounds.some(bounds => overlap(placement.occupied, bounds))
|
|
358
|
-
)) {
|
|
359
|
-
const owner = edge === "top" ? "chart titles" : "x-axis guides";
|
|
360
|
-
throw new Error(
|
|
361
|
-
`${edge[0].toUpperCase()}${edge.slice(1)} legend lane and ${owner} require more margin space.`
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
edge,
|
|
366
|
-
placements: placements.map(placement => ({
|
|
367
|
-
...placement,
|
|
368
|
-
...(placement.backgroundId === undefined
|
|
369
|
-
? {}
|
|
370
|
-
: {
|
|
371
|
-
background: {
|
|
372
|
-
id: placement.backgroundId,
|
|
373
|
-
x: placement.foreground.left - placement.padding,
|
|
374
|
-
y: placement.foreground.top - placement.padding,
|
|
375
|
-
width: placement.foreground.right - placement.foreground.left +
|
|
376
|
-
placement.padding * 2,
|
|
377
|
-
height: placement.foreground.bottom - placement.foreground.top +
|
|
378
|
-
placement.padding * 2
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
})),
|
|
382
|
-
occupied,
|
|
383
|
-
rowCount: packed.length
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Compose independently measured content blocks before the outer edge lane
|
|
388
|
-
// treats the whole group as one indivisible item.
|
|
389
|
-
export function resolveHorizontalLegendGroup({ edge, plot, canvas, groups,
|
|
390
|
-
align, offset, border, backgroundId, collisionBounds = [] }) {
|
|
391
|
-
const inset = decoration(border);
|
|
392
|
-
const innerPlot = { ...plot, x: plot.x + inset, width: plot.width - inset * 2 };
|
|
393
|
-
const packed = packHorizontalRows(groups, innerPlot);
|
|
394
|
-
const rows = packed.map(normalizeHorizontalRow);
|
|
395
|
-
let cursor = 0;
|
|
396
|
-
const placements = [];
|
|
397
|
-
for (const row of rows) {
|
|
398
|
-
const bounds = unionBounds(row.map(item => item.occupied));
|
|
399
|
-
const dy = edge === "top" ? cursor - bounds.bottom : cursor - bounds.top;
|
|
400
|
-
const translated = row.map(item => translateHorizontalPlacement(item, dy));
|
|
401
|
-
placements.push(...translated);
|
|
402
|
-
const occupied = unionBounds(translated.map(item => item.occupied));
|
|
403
|
-
cursor = edge === "top" ? occupied.top - HORIZONTAL_LEGEND_BLOCK_GAP
|
|
404
|
-
: occupied.bottom + HORIZONTAL_LEGEND_BLOCK_GAP;
|
|
405
|
-
}
|
|
406
|
-
const foreground = unionBounds(placements.map(item => item.occupied));
|
|
407
|
-
const occupied = expandBounds(foreground, inset);
|
|
408
|
-
const width = occupied.right - occupied.left;
|
|
409
|
-
const x = align === "left" ? plot.x : align === "right" ? plot.x + plot.width - width
|
|
410
|
-
: plot.x + (plot.width - width) / 2;
|
|
411
|
-
const dx = x - occupied.left;
|
|
412
|
-
const dy = edge === "top" ? plot.y - offset - occupied.bottom
|
|
413
|
-
: plot.y + plot.height + offset - occupied.top;
|
|
414
|
-
const finalBounds = translateBounds(occupied, dx, dy);
|
|
415
|
-
if (finalBounds.left < 0 || finalBounds.right > canvas.width ||
|
|
416
|
-
finalBounds.top < 0 || finalBounds.bottom > canvas.height) {
|
|
417
|
-
throw new Error(`Combined legend requires more ${edge}-margin or Canvas space.`);
|
|
418
|
-
}
|
|
419
|
-
if (collisionBounds.some(bounds => overlap(finalBounds, bounds))) {
|
|
420
|
-
throw new Error(`Combined ${edge} legend and ${edge === "top" ? "chart titles" : "x-axis guides"} require more margin space.`);
|
|
421
|
-
}
|
|
422
|
-
return {
|
|
423
|
-
placements: placements.map(item => ({ ...item, dx: item.dx + dx,
|
|
424
|
-
titleDy: item.titleDy + dy, contentDy: item.contentDy + dy,
|
|
425
|
-
...(item.backgroundId === undefined ? {} : { background: {
|
|
426
|
-
id: item.backgroundId,
|
|
427
|
-
x: item.foreground.left + dx - item.padding,
|
|
428
|
-
y: item.foreground.top + dy - item.padding,
|
|
429
|
-
width: item.foreground.right - item.foreground.left + item.padding * 2,
|
|
430
|
-
height: item.foreground.bottom - item.foreground.top + item.padding * 2
|
|
431
|
-
} }) })),
|
|
432
|
-
occupied: finalBounds,
|
|
433
|
-
background: backgroundId === undefined ? undefined : {
|
|
434
|
-
id: backgroundId,
|
|
435
|
-
x: foreground.left + dx - border.padding,
|
|
436
|
-
y: foreground.top + dy - border.padding,
|
|
437
|
-
width: foreground.right - foreground.left + border.padding * 2,
|
|
438
|
-
height: foreground.bottom - foreground.top + border.padding * 2
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
}
|
|
1
|
+
import{canvasOverflowError as S}from"./canvas.js";import{unionBounds as _}from"../core/textMetrics.js";const N=24,$=40,v=16,X=44,U=12;function K(t){return t!==void 0&&["top","bottom"].includes(t.position)&&t.layout!=="legacy-bottom"}function W({plot:t,canvas:o,config:n,bounds:r}){if(!K(n))throw new Error("Single legend placement requires a horizontal edge layout.");const i=n.align==="left"?t.x-r.left:n.align==="right"?t.x+t.width-r.right:t.x+t.width/2-B(r.left,r.right),e=n.position==="top"?t.y-n.offset-r.bottom:t.y+t.height+n.offset-r.top,s=w(r,i,e);if(s.left<0||s.right>o.width||s.top<0||s.bottom>o.height)throw S(`Legend layout requires more ${n.position}-margin or Canvas space.`,[s],o);return{dx:i,dy:e,occupied:s}}function B(t,o){const n=t+o,r=Number.isFinite(n)?n/2:t/2+o/2;return r===0?0:r}function T(t){return t===!1?0:t.padding+t.lineWidth/2}function A(t,o){let n=0,r=0;for(const i of t){const e=i.inset??0;if(i.preserveGrid){n=Math.min(n,-e),r=Math.max(r,i.bounds.right-i.bounds.left+e);continue}i.title!==void 0&&(r=Math.max(r,i.title.width+e)),n=Math.min(n,v+i.symbol.left-i.symbol.centerX-e),r=Math.max(r,v+i.symbol.right-i.symbol.centerX+e,o+i.labels.width+e)}return{left:n,right:r}}function Y(t,o,n){const r=o-t.bounds.top,i={left:t.bounds.left,right:t.bounds.right,top:t.bounds.top+r,bottom:t.bounds.bottom+r};return{id:t.id,dy:r,titleDx:t.title===void 0?void 0:t.preserveGrid?-t.bounds.left:-t.title.x,symbolDx:t.preserveGrid?-t.bounds.left:v-t.symbol.centerX,labelDx:t.preserveGrid?-t.bounds.left:n-t.labels.x,bounds:i}}function H(t,o){return t.left<o.right&&t.right>o.left&&t.top<o.bottom&&t.bottom>o.top}function j({side:t,plot:o,canvas:n,groups:r,axisBounds:i}){if(!["right","left"].includes(t))throw new Error(`Unsupported side legend lane "${t}".`);const e=r.flatMap(c=>c.blocks);if(e.length<2)return;const s=Math.max(X,...e.filter(c=>!c.preserveGrid).map(c=>v+Math.abs(c.labels.x-c.symbol.centerX))),f=A(e,s),g=e.reduce((c,x)=>Math.max(c,x.offset),-1/0),l=t==="right"?o.x+o.width+g:o.x-g-f.right,h=l+v,d=l+s;let y=o.y;const z=[],E=[],D=[];for(const c of r){const x=T(c.border);D.length>0&&(y+=x);const G=[];for(const C of c.blocks){const I=Y(C,y,s);G.push(I),z.push({...I,titleDx:I.titleDx===void 0?void 0:I.titleDx+l,symbolDx:I.symbolDx+l,labelDx:I.labelDx+l}),y=I.bounds.bottom+N}const L=_(G.map(C=>C.bounds)),u=A(c.blocks,s),b={left:l+u.left,right:l+u.right,top:L.top,bottom:L.bottom},k={left:b.left-x,right:b.right+x,top:b.top-x,bottom:b.bottom+x};D.push(k),c.backgroundId!==void 0&&E.push({id:c.backgroundId,x:b.left-c.border.padding,y:b.top-c.border.padding,width:b.right-b.left+c.border.padding*2,height:b.bottom-b.top+c.border.padding*2}),y=k.bottom+N}const a=_(D),p=o.y-a.top,m=w(a,0,p);if(m.left<0||m.right>n.width||m.top<0||m.bottom>n.height)throw S(`Legend lane requires more ${t}-margin or vertical Canvas space.`,[m],n);if(i!==void 0&&H(m,i))throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and y-axis guides require more margin space.`);return{side:t,titleStartX:l,symbolCenterX:h,labelStartX:d,placements:z.map(c=>({...c,dy:c.dy+p,bounds:w(c.bounds,0,p)})),backgrounds:E.map(c=>({...c,y:c.y+p})),occupied:m}}function J({side:t,plot:o,canvas:n,bounds:r}){if(!["left","right"].includes(t))throw new Error("Single side legend placement requires a left or right edge.");const i=o.y-r.top,e=w(r,0,i);if(e.left<0||e.right>n.width||e.top<0||e.bottom>n.height)throw S(`Legend layout requires more ${t}-margin or Canvas space.`,[e],n);return{dy:i,occupied:e}}function w(t,o,n){return{left:t.left+o,right:t.right+o,top:t.top+n,bottom:t.bottom+n}}function q(t,o){return{left:t.left-o,right:t.right+o,top:t.top-o,bottom:t.bottom+o}}function O(t,o,n=!1){const r=[];let i=[],e=o.x;for(const s of t){const f=q(s.horizontal,s.inset);if(f.right-f.left>o.width&&!n)throw new Error("Horizontal legend block requires more plot width.");let l=e-f.left,h=w(f,l,0);i.length>0&&h.right>o.x+o.width&&(r.push(i),i=[],e=o.x,l=e-f.left,h=w(f,l,0)),i.push({group:s,interval:h,dx:l}),e=h.right+$}return i.length>0&&r.push(i),r}function P(t){const o=t.filter(e=>e.group.title!==void 0&&e.group.inline!==!0),n=o[0]?.group.title.y,r=o.length===0?0:o.reduce((e,s)=>Math.max(e,s.group.title.bounds.bottom-s.group.title.y),-1/0),i=n===void 0?B(t[0].group.element.top,t[0].group.element.bottom):n+r+U;return t.map(({group:e,dx:s})=>{if(e.atomic){const h=i-e.element.top,d=w(e.content,s,h);return{id:e.id,dx:s,titleDy:h,contentDy:h,foreground:d,occupied:q(d,e.inset),padding:e.padding,backgroundId:e.backgroundId}}const f=n===void 0?i-B(e.element.top,e.element.bottom):i-e.element.top,g=e.title===void 0?0:e.inline===!0?f:n-e.title.y,l=_([...e.title===void 0?[]:[w(e.title.bounds,s,g)],w(e.content,s,f)]);return{id:e.id,dx:s,titleDy:g,contentDy:f,foreground:l,occupied:q(l,e.inset),padding:e.padding,backgroundId:e.backgroundId}})}function R(t,o){return{...t,titleDy:t.titleDy+o,contentDy:t.contentDy+o,foreground:w(t.foreground,0,o),occupied:w(t.occupied,0,o)}}function Q({edge:t,plot:o,canvas:n,groups:r,collisionBounds:i=[],allowOverflow:e=!1}){if(!["top","bottom"].includes(t))throw new Error(`Unsupported horizontal legend lane "${t}".`);if(r.length<2)return;const s=O(r,o,e),f=s.map(P),g=[];let l;for(let d=0;d<f.length;d+=1){const y=f[d],z=_(y.map(a=>a.occupied));let E;if(d===0){const a=t==="top"?s[d].reduce((p,m)=>Math.max(p,m.group.horizontal.bottom),-1/0):s[d].reduce((p,m)=>Math.min(p,m.group.horizontal.top),1/0);E=t==="top"?a-z.bottom:a-z.top}else E=t==="bottom"?l.bottom+$-z.top:l.top-$-z.bottom;const D=y.map(a=>R(a,E));g.push(...D),l=_(D.map(a=>a.occupied))}const h=_(g.map(d=>d.occupied));if(g.some(d=>d.occupied.left<0||d.occupied.right>n.width||d.occupied.top<0||d.occupied.bottom>n.height))throw S(`Legend lane requires more ${t}-margin or Canvas space.`,[h],n);if(g.some(d=>i.some(y=>H(d.occupied,y)))){const d=t==="top"?"chart titles":"x-axis guides";throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and ${d} require more margin space.`)}return{edge:t,placements:g.map(d=>({...d,...d.backgroundId===void 0?{}:{background:{id:d.backgroundId,x:d.foreground.left-d.padding,y:d.foreground.top-d.padding,width:d.foreground.right-d.foreground.left+d.padding*2,height:d.foreground.bottom-d.foreground.top+d.padding*2}}})),occupied:h,rowCount:s.length}}function V({edge:t,plot:o,canvas:n,groups:r,align:i,offset:e,border:s,backgroundId:f,collisionBounds:g=[],allowOverflow:l=!1}){const h=T(s),d={...o,x:o.x+h,width:o.width-h*2},z=O(r,d,l).map(P);let E=0;const D=[];for(const u of z){const b=_(u.map(M=>M.occupied)),k=t==="top"?E-b.bottom:E-b.top,C=u.map(M=>R(M,k));D.push(...C);const I=_(C.map(M=>M.occupied));E=t==="top"?I.top-$:I.bottom+$}const a=_(D.map(u=>u.occupied)),p=q(a,h),m=p.right-p.left,x=(i==="left"?o.x:i==="right"?o.x+o.width-m:o.x+(o.width-m)/2)-p.left,G=t==="top"?o.y-e-p.bottom:o.y+o.height+e-p.top,L=w(p,x,G);if(L.left<0||L.right>n.width||L.top<0||L.bottom>n.height)throw S(`Combined legend requires more ${t}-margin or Canvas space.`,[L],n);if(g.some(u=>H(L,u)))throw new Error(`Combined ${t} legend and ${t==="top"?"chart titles":"x-axis guides"} require more margin space.`);return{placements:D.map(u=>({...u,dx:u.dx+x,titleDy:u.titleDy+G,contentDy:u.contentDy+G,...u.backgroundId===void 0?{}:{background:{id:u.backgroundId,x:u.foreground.left+x-u.padding,y:u.foreground.top+G-u.padding,width:u.foreground.right-u.foreground.left+u.padding*2,height:u.foreground.bottom-u.foreground.top+u.padding*2}}})),occupied:L,background:f===void 0?void 0:{id:f,x:a.left+x-s.padding,y:a.top+G-s.padding,width:a.right-a.left+s.padding*2,height:a.bottom-a.top+s.padding*2}}}export{$ as HORIZONTAL_LEGEND_BLOCK_GAP,U as HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP,N as SIDE_LEGEND_BLOCK_GAP,X as SIDE_LEGEND_LABEL_START,v as SIDE_LEGEND_SYMBOL_CENTER,K as isHorizontalEdgeLegend,V as resolveHorizontalLegendGroup,Q as resolveHorizontalLegendLane,j as resolveSideLegendLane,W as resolveSingleHorizontalLegendPlacement,J as resolveSingleSideLegendPlacement};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveTextBounds as f,resolveTextLines as m}from"../core/textMetrics.js";function s(e,t,n){const l=m(String(e),{fontSize:t.fontSize,align:"left",baseline:"middle",blockAlign:"middle"}),o=l.length>1?l:void 0,i=f({x:0,y:0,text:String(e),...t,lines:o,textAlign:"left",textBaseline:"middle"},n);return{lines:o,width:i.right-i.left,height:i.bottom-i.top,extraHeight:i.bottom-i.top-t.fontSize}}function h(e,t,n,l,o){if(n.align===void 0)return e;const i=s(t,n,o),r=f({x:e.x,y:e.y,text:t,...n,lines:i.lines,textAlign:e.align,textBaseline:"middle"},o),a=Math.min(r.left,...l.map(g=>g.left)),d=Math.max(r.right,...l.map(g=>g.right));return{...e,x:n.align==="left"?a:n.align==="right"?d:(a+d)/2,align:n.align}}export{h as alignLegendTitle,s as resolveLegendTitleText};
|
package/src/layout/title.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { unionBounds } from "../core/textMetrics.js";
|
|
1
2
|
import { measureTextWidth, resolveTextBounds, wrapText } from "./text.js";
|
|
2
3
|
|
|
3
4
|
function lineCenters(lines, style, lineHeight, start) {
|
|
4
5
|
return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
function wrapTitleLines(text, options) {
|
|
9
|
+
return text.split(/\r\n|\r|\n/u).flatMap(line => line === "" ? [""] : wrapText(line, options));
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
|
|
8
|
-
const titleLines =
|
|
13
|
+
const titleLines = wrapTitleLines(text, {
|
|
9
14
|
maxWidth: config.maxWidth,
|
|
10
15
|
mode: config.wrap,
|
|
11
16
|
style: config.titleStyle, profile
|
|
12
17
|
});
|
|
13
|
-
const subtitleLines = subtitle === undefined ? [] :
|
|
18
|
+
const subtitleLines = subtitle === undefined ? [] : wrapTitleLines(subtitle, {
|
|
14
19
|
maxWidth: config.maxWidth,
|
|
15
20
|
mode: config.wrap,
|
|
16
21
|
style: config.subtitleStyle, profile
|
|
@@ -73,14 +78,6 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile)
|
|
|
73
78
|
}, profile);
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
function unionBounds(bounds) {
|
|
77
|
-
return bounds.reduce((union, item) => ({
|
|
78
|
-
left: Math.min(union.left, item.left),
|
|
79
|
-
right: Math.max(union.right, item.right),
|
|
80
|
-
top: Math.min(union.top, item.top),
|
|
81
|
-
bottom: Math.max(union.bottom, item.bottom)
|
|
82
|
-
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
83
|
-
}
|
|
84
81
|
|
|
85
82
|
export function resolveTitleComponentBounds(component, style, profile) {
|
|
86
83
|
return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
|
|
@@ -97,7 +94,32 @@ export function unionTitleBounds(bounds) {
|
|
|
97
94
|
return unionBounds(bounds);
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
export function paintedTitleComponent(component) {
|
|
98
|
+
const indices = component.lines.flatMap((line, index) => line === "" ? [] : [index]);
|
|
99
|
+
const selected = value => Array.isArray(value) ? indices.map(index => value[index]) : value;
|
|
100
|
+
return {
|
|
101
|
+
...component,
|
|
102
|
+
lines: indices.map(index => component.lines[index]),
|
|
103
|
+
x: selected(component.x),
|
|
104
|
+
y: selected(component.y)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { textBoundsIntersect as layoutBoundsIntersect } from "../core/textMetrics.js";
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
export function titleTextProperties(component, style) {
|
|
112
|
+
const distributed = value => component.lines.length === 1 && Array.isArray(value) ? value[0] : value;
|
|
113
|
+
return {
|
|
114
|
+
x: distributed(component.x),
|
|
115
|
+
y: distributed(component.y),
|
|
116
|
+
text: distributed(component.lines),
|
|
117
|
+
fill: style.color,
|
|
118
|
+
fontSize: style.fontSize,
|
|
119
|
+
fontFamily: style.fontFamily,
|
|
120
|
+
fontWeight: style.fontWeight,
|
|
121
|
+
...(style.fontStyle === undefined ? {} : { fontStyle: style.fontStyle }),
|
|
122
|
+
textAlign: component.textAlign,
|
|
123
|
+
textBaseline: "middle"
|
|
124
|
+
};
|
|
103
125
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveBarCellOpacities } from "./resolve.js";
|
|
1
2
|
import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
|
|
2
3
|
import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
|
|
3
4
|
import {
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
} from "../../grammar/scales/index.js";
|
|
8
9
|
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
9
10
|
import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
|
|
10
|
-
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
11
|
+
import { resolveBarWidth, resolveBarInterval } from "../../grammar/bars/geometry.js";
|
|
11
12
|
import { finiteMidpoint } from "../../grammar/numeric.js";
|
|
12
13
|
|
|
13
14
|
export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
@@ -41,16 +42,20 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
|
41
42
|
resolved.resolvedScales[stroke.scale],
|
|
42
43
|
"stroke"
|
|
43
44
|
);
|
|
45
|
+
const opacities = resolveBarCellOpacities(resolved, layer, segments.map(segment => segment.cell.sourceIndices.map(index => dataset.values[index])));
|
|
44
46
|
const config = resolved.markConfigs[layer.id] ?? {};
|
|
45
47
|
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
46
|
-
|
|
48
|
+
if (widthConfig?.interval !== undefined && (grouped || !temporal)) throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
|
|
49
|
+
const defaultWidth = widthConfig?.interval === undefined ? resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step)) : undefined;
|
|
47
50
|
const indices = new Map(categoryScale.domain.map((value, index) => [value, index]));
|
|
48
51
|
return segments.map(({ cell, start, end, seriesIndex }, index) => {
|
|
49
52
|
const categoryValue = cell[channels.category];
|
|
53
|
+
const interval = widthConfig?.interval === undefined ? undefined : resolveBarInterval(widthConfig, categoryValue, categoryScale);
|
|
54
|
+
const width = interval?.width ?? defaultWidth;
|
|
50
55
|
const categoryCenter = (temporal ? mapContinuousScaleValues : mapOrdinalPositionValues)([categoryValue], categoryScale)[0];
|
|
51
|
-
let position = !grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
|
|
56
|
+
let position = interval?.position ?? (!grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
|
|
52
57
|
? (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step + categoryScale.bandwidth / 2 - width / 2
|
|
53
|
-
: categoryCenter - width / 2;
|
|
58
|
+
: categoryCenter - width / 2);
|
|
54
59
|
if (grouped) {
|
|
55
60
|
const direction = Math.sign(categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]) || 1;
|
|
56
61
|
const offsetCenter = offsetScale.start + seriesIndex * offsetScale.step + (Math.sign(offsetScale.step) || 1) * offsetScale.bandwidth / 2;
|
|
@@ -65,6 +70,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
|
65
70
|
width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
|
|
66
71
|
fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
|
|
67
72
|
...resolveBarAppearance(config, existing[index]?.properties),
|
|
68
|
-
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
73
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] }),
|
|
74
|
+
...(opacities === undefined ? {} : { opacity: opacities[index] }) };
|
|
69
75
|
});
|
|
70
76
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveBarCellOpacities } from "./resolve.js";
|
|
1
2
|
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
2
3
|
import {
|
|
3
4
|
findHistogramBinIndex,
|
|
@@ -172,6 +173,7 @@ export function deriveHistogramRectangles(required, resolved) {
|
|
|
172
173
|
...required,
|
|
173
174
|
resolvedScales: resolved.resolvedScales
|
|
174
175
|
});
|
|
176
|
+
const opacities = resolveBarCellOpacities(resolved, required.layer, segments.map(segment => segment.members));
|
|
175
177
|
const existing = resolved.graphicSpec.objects[required.layer.id].items;
|
|
176
178
|
const layout = resolveBarColorLayout(required.layer);
|
|
177
179
|
const config = resolved.markConfigs[required.layer.id] ?? {};
|
|
@@ -224,7 +226,8 @@ export function deriveHistogramRectangles(required, resolved) {
|
|
|
224
226
|
existing[index]?.properties.fill ??
|
|
225
227
|
DEFAULT_BAR_FILL,
|
|
226
228
|
...resolveBarAppearance(config, existing[index]?.properties),
|
|
227
|
-
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
229
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] }),
|
|
230
|
+
...(opacities === undefined ? {} : { opacity: opacities[index] })
|
|
228
231
|
};
|
|
229
232
|
});
|
|
230
233
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveBarCellOpacities } from "./resolve.js";
|
|
1
2
|
import {
|
|
2
3
|
mapContinuousScaleValues,
|
|
3
4
|
mapOrdinalPositionValues,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
} from "../../grammar/scales/index.js";
|
|
7
8
|
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
8
9
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
10
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
9
11
|
import {
|
|
10
12
|
DEFAULT_BAR_FILL,
|
|
11
13
|
DEFAULT_BAR_STROKE,
|
|
@@ -14,7 +16,9 @@ import {
|
|
|
14
16
|
|
|
15
17
|
export function deriveRangedRectangles(required, program, width) {
|
|
16
18
|
const { layer, dataset } = required;
|
|
17
|
-
|
|
19
|
+
if (width?.interval !== undefined) throw new Error("Bar interval width requires ungrouped aggregate temporal categories.");
|
|
20
|
+
const centered = resolveBarGrain(layer) === BAR_GRAINS.centered;
|
|
21
|
+
const vertical = centered ? resolveBarChannels(layer).orientation === "vertical" : layer.encoding?.y2 !== undefined;
|
|
18
22
|
const categoryChannel = vertical ? "x" : "y";
|
|
19
23
|
const measureChannel = vertical ? "y" : "x";
|
|
20
24
|
const secondaryChannel = `${measureChannel}2`;
|
|
@@ -23,16 +27,22 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
23
27
|
const secondary = layer.encoding[secondaryChannel];
|
|
24
28
|
const categoryScale = program.resolvedScales[category.scale];
|
|
25
29
|
const measureScale = program.resolvedScales[primary.scale];
|
|
26
|
-
const
|
|
30
|
+
const categoryValues = readScaleField(dataset.values, category.field, category.fieldType, {
|
|
31
|
+
temporalUnit: category.temporalUnit
|
|
32
|
+
});
|
|
33
|
+
const centers = (centered || categoryScale.type === "time" ? mapContinuousScaleValues : mapOrdinalPositionValues)(
|
|
34
|
+
categoryValues, categoryScale
|
|
35
|
+
);
|
|
27
36
|
const first = mapContinuousScaleValues(
|
|
28
37
|
dataset.values.map(row => row[primary.field]),
|
|
29
38
|
measureScale
|
|
30
39
|
);
|
|
31
40
|
const second = mapContinuousScaleValues(
|
|
32
|
-
dataset.values.map(row => row[secondary.field]),
|
|
41
|
+
dataset.values.map(row => secondary === undefined ? 0 : row[secondary.field]),
|
|
33
42
|
measureScale
|
|
34
43
|
);
|
|
35
|
-
|
|
44
|
+
if (centered && width?.band !== undefined) throw new Error("Numeric-center bars require pixel width.");
|
|
45
|
+
const band = resolveBarWidth(centered ? width ?? { pixels: 5 } : width, Math.abs(categoryScale.bandwidth ?? categoryScale.step));
|
|
36
46
|
const config = program.markConfigs[layer.id] ?? {};
|
|
37
47
|
const appearance = config.barAppearance ?? {};
|
|
38
48
|
const color = layer.encoding?.color;
|
|
@@ -50,6 +60,7 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
50
60
|
program.resolvedScales[stroke.scale],
|
|
51
61
|
"stroke"
|
|
52
62
|
);
|
|
63
|
+
const opacities = resolveBarCellOpacities(program, layer, dataset.values.map(row => [row]));
|
|
53
64
|
return dataset.values.map((_, index) => vertical ? {
|
|
54
65
|
x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
|
|
55
66
|
height: Math.abs(second[index] - first[index]), fill: fills[index],
|
|
@@ -59,7 +70,8 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
59
70
|
color === undefined ? DEFAULT_BAR_STROKE : fills[index],
|
|
60
71
|
1
|
|
61
72
|
),
|
|
62
|
-
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
73
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] }),
|
|
74
|
+
...(opacities === undefined ? {} : { opacity: opacities[index] })
|
|
63
75
|
} : {
|
|
64
76
|
x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
|
|
65
77
|
width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
|
|
@@ -69,6 +81,7 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
69
81
|
color === undefined ? DEFAULT_BAR_STROKE : fills[index],
|
|
70
82
|
1
|
|
71
83
|
),
|
|
72
|
-
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
84
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] }),
|
|
85
|
+
...(opacities === undefined ? {} : { opacity: opacities[index] })
|
|
73
86
|
});
|
|
74
87
|
}
|