ggaction 0.0.24 → 0.0.26
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 +47 -0
- package/README.md +2 -2
- 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 +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +5 -3
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +2 -0
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -1
- package/src/actions/guides/axes/edit.js +1 -1
- package/src/actions/guides/axes/labels.js +2 -1
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +12 -9
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +16 -2
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/bin2d.js +7 -0
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +30 -4
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +97 -24
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -2
- package/types/index.d.ts +11 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +553 -365
|
@@ -82,7 +82,7 @@ export function normalizeCompositionChildren(children) {
|
|
|
82
82
|
throw new TypeError(`Composition child ${index} must be a plain object.`);
|
|
83
83
|
}
|
|
84
84
|
const unknown = Object.keys(child).find(
|
|
85
|
-
key => !["id", "width", "height", "widthMode", "heightMode"].includes(key)
|
|
85
|
+
key => !["id", "width", "height", "widthMode", "heightMode", "plot"].includes(key)
|
|
86
86
|
);
|
|
87
87
|
if (unknown !== undefined) {
|
|
88
88
|
throw new Error(`Unknown composition child property "${unknown}".`);
|
|
@@ -112,7 +112,8 @@ export function normalizeCompositionChildren(children) {
|
|
|
112
112
|
width: child.width,
|
|
113
113
|
height: child.height,
|
|
114
114
|
widthMode: child.widthMode ?? "explicit",
|
|
115
|
-
heightMode: child.heightMode ?? "explicit"
|
|
115
|
+
heightMode: child.heightMode ?? "explicit",
|
|
116
|
+
...(child.plot === undefined ? {} : { plot: validatePlacedBounds(child.plot, `Composition plot ${child.id}`) })
|
|
116
117
|
});
|
|
117
118
|
});
|
|
118
119
|
}
|
|
@@ -146,10 +147,11 @@ export function resolveCompositionLayout({
|
|
|
146
147
|
const resolvedDirection = normalizeDirection(direction);
|
|
147
148
|
const resolvedChildren = normalizeCompositionChildren(children);
|
|
148
149
|
const resolvedGap = validateCompositionSpacing(gap, "Composition gap");
|
|
149
|
-
const resolvedAlign = normalizeCompositionAlign(align);
|
|
150
|
+
const resolvedAlign = align === "plot" ? "plot" : normalizeCompositionAlign(align);
|
|
150
151
|
const resolvedPadding = normalizeCompositionPadding(padding);
|
|
151
152
|
const horizontal = resolvedDirection === "horizontal";
|
|
152
|
-
const sizedChildren = normalizeAutoCrossSize(resolvedChildren, horizontal);
|
|
153
|
+
const sizedChildren = resolvedAlign === "plot" ? resolvedChildren : normalizeAutoCrossSize(resolvedChildren, horizontal);
|
|
154
|
+
if (resolvedAlign === "plot") return resolvePlotAlignedComposition({ direction: resolvedDirection, children: sizedChildren, gap: resolvedGap, padding: resolvedPadding });
|
|
153
155
|
const contentWidth = horizontal
|
|
154
156
|
? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
|
|
155
157
|
resolvedGap * (sizedChildren.length - 1)
|
|
@@ -303,3 +305,27 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
|
|
|
303
305
|
height: bottom - top
|
|
304
306
|
});
|
|
305
307
|
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
function resolvePlotAlignedComposition({ direction, children, gap, padding }) {
|
|
311
|
+
const horizontal = direction === "horizontal";
|
|
312
|
+
const axis = horizontal ? "y" : "x", dimension = horizontal ? "height" : "width";
|
|
313
|
+
if (children.some(child => !child.plot)) throw new Error("Plot alignment requires a plot in every child.");
|
|
314
|
+
const crossSize = children[0].plot[dimension];
|
|
315
|
+
if (children.some(child => Math.abs(child.plot[dimension] - crossSize) > 1e-6)) {
|
|
316
|
+
throw new Error("Plot alignment requires equal cross-axis plot sizes after normalization.");
|
|
317
|
+
}
|
|
318
|
+
const leading = Math.max(...children.map(child => child.plot[axis]));
|
|
319
|
+
const trailing = Math.max(...children.map(child => child[dimension] - child.plot[axis] - crossSize));
|
|
320
|
+
let cursor = horizontal ? padding.left : padding.top;
|
|
321
|
+
const placements = children.map(child => {
|
|
322
|
+
const result = { id: child.id, width: child.width, height: child.height,
|
|
323
|
+
x: horizontal ? cursor : padding.left + leading - child.plot.x,
|
|
324
|
+
y: horizontal ? padding.top + leading - child.plot.y : cursor };
|
|
325
|
+
cursor += (horizontal ? child.width : child.height) + gap;
|
|
326
|
+
return result;
|
|
327
|
+
});
|
|
328
|
+
return cloneAndFreeze({ direction, gap, align: "plot", padding, children: placements,
|
|
329
|
+
width: horizontal ? cursor - gap + padding.right : padding.left + leading + crossSize + trailing + padding.right,
|
|
330
|
+
height: horizontal ? padding.top + leading + crossSize + trailing + padding.bottom : cursor - gap + padding.bottom });
|
|
331
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { textBoundsIntersect } from "../core/textMetrics.js";
|
|
2
|
+
import { SIDE_LEGEND_BLOCK_GAP, HORIZONTAL_LEGEND_BLOCK_GAP } from "./legendLane.js";
|
|
3
|
+
import { resolveLegendGroupPlacement } from "./legendGroup.js";
|
|
4
|
+
|
|
5
|
+
export function compositionLegendEdgeClearance(plot, retained, position, occupied) {
|
|
6
|
+
const side = position === 'left' || position === 'right';
|
|
7
|
+
const relevant = occupied ? retained.filter(value => side
|
|
8
|
+
? value.top < occupied.bottom && value.bottom > occupied.top
|
|
9
|
+
: value.left < occupied.right && value.right > occupied.left) : retained;
|
|
10
|
+
return Math.max(0, ...relevant.map(value => position === 'right' ? value.right - plot.x - plot.width
|
|
11
|
+
: position === 'left' ? plot.x - value.left : position === 'top' ? plot.y - value.top
|
|
12
|
+
: value.bottom - plot.y - plot.height));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Collected legends retain the native plot-relative anchor. Reuse available
|
|
16
|
+
// child margins; only occupied graphics can require an additional guide lane.
|
|
17
|
+
export function layoutCompositionLegend(base, plot, legend, title = {}, retained = []) {
|
|
18
|
+
if (!legend) return { layout: base };
|
|
19
|
+
const { bounds, placement } = legend;
|
|
20
|
+
const anchorBounds = legend.anchorBounds ?? bounds;
|
|
21
|
+
const width = bounds.right - bounds.left, height = bounds.bottom - bounds.top;
|
|
22
|
+
const { position = 'right', align = 'center', offset = 8, alongOffset = 0, at } = placement;
|
|
23
|
+
const inferred = placement.inferredOffset ?? legend.config.inferredOffset;
|
|
24
|
+
const outside = !at && ['left', 'right', 'top', 'bottom'].includes(position);
|
|
25
|
+
const group = { blockLayout: 'column', inferredOffset: inferred,
|
|
26
|
+
placement: at ? { at } : { position, align, offset, alongOffset } };
|
|
27
|
+
const place = (group, measured = bounds) => {
|
|
28
|
+
const [value] = resolveLegendGroupPlacement({ blocks: [{ id: 'shared', bounds: anchorBounds }],
|
|
29
|
+
group, plot, canvas: base, allowOverflow: true, deferCanvasFit: true });
|
|
30
|
+
return { left: measured.left + value.dx, right: measured.right + value.dx,
|
|
31
|
+
top: measured.top + value.dy, bottom: measured.bottom + value.dy };
|
|
32
|
+
};
|
|
33
|
+
let occupied = place(group);
|
|
34
|
+
if (outside && inferred) {
|
|
35
|
+
const clearance = compositionLegendEdgeClearance(plot, retained, position, place(group, anchorBounds));
|
|
36
|
+
occupied = place({ ...group, placement: { ...group.placement, offset: offset + clearance } });
|
|
37
|
+
} else if (outside && retained.some(value => textBoundsIntersect(occupied, value))) {
|
|
38
|
+
throw new Error('Shared composition legend overlaps retained chart content. Increase its offset.');
|
|
39
|
+
}
|
|
40
|
+
const titleSpace = side => title.config?.position === side ? title.reservation + 8 : 0;
|
|
41
|
+
const dx = Math.max(0, 8 + titleSpace('left') - occupied.left);
|
|
42
|
+
const dy = Math.max(0, 8 + titleSpace('top') - occupied.top);
|
|
43
|
+
const x = occupied.left + dx, y = occupied.top + dy;
|
|
44
|
+
const layout = { ...base,
|
|
45
|
+
width: Math.max(base.width + dx, x + width + 8 + titleSpace('right')),
|
|
46
|
+
height: Math.max(base.height + dy, y + height + 8 + titleSpace('bottom')),
|
|
47
|
+
children: base.children.map(child => ({ ...child, x: child.x + dx, y: child.y + dy })) };
|
|
48
|
+
return { layout, frame: { x, y, width, height } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Every displayed block uses the same parent frame. Compatible shared color
|
|
52
|
+
// explanations are deduplicated upstream; independent blocks remain distinct.
|
|
53
|
+
// Pack overlapping outside blocks along their edge, preserving authored gaps.
|
|
54
|
+
export function layoutCompositionLegends(base, plot, legends, title = {}, retained = []) {
|
|
55
|
+
let layout = base, currentPlot = plot, occupiedContent = retained;
|
|
56
|
+
const frames = new Map(), placed = [];
|
|
57
|
+
for (const legend of legends) {
|
|
58
|
+
let candidate = legend;
|
|
59
|
+
let result = layoutCompositionLegend(layout, currentPlot, candidate, title, occupiedContent);
|
|
60
|
+
const position = legend.placement.position ?? 'right';
|
|
61
|
+
const outside = !legend.placement.at && ['left', 'right', 'top', 'bottom'].includes(position);
|
|
62
|
+
if (outside) {
|
|
63
|
+
const side = ['left', 'right'].includes(position);
|
|
64
|
+
for (let attempt = 0; attempt < placed.length; attempt++) {
|
|
65
|
+
const dx = result.layout.children[0].x - layout.children[0].x;
|
|
66
|
+
const dy = result.layout.children[0].y - layout.children[0].y;
|
|
67
|
+
const f = result.frame;
|
|
68
|
+
const overlaps = placed.filter(previous => previous.position === position).map(previous => frames.get(previous.id))
|
|
69
|
+
.filter(other => textBoundsIntersect({ left: f.x, right: f.x + f.width, top: f.y, bottom: f.y + f.height },
|
|
70
|
+
{ left: other.x + dx, right: other.x + dx + other.width, top: other.y + dy, bottom: other.y + dy + other.height }));
|
|
71
|
+
if (!overlaps.length) break;
|
|
72
|
+
const shift = side ? Math.max(...overlaps.map(other => other.y + dy + other.height)) + SIDE_LEGEND_BLOCK_GAP - f.y
|
|
73
|
+
: Math.max(...overlaps.map(other => other.x + dx + other.width)) + HORIZONTAL_LEGEND_BLOCK_GAP - f.x;
|
|
74
|
+
candidate = { ...candidate, placement: { ...candidate.placement,
|
|
75
|
+
alongOffset: (candidate.placement.alongOffset ?? 0) + shift } };
|
|
76
|
+
result = layoutCompositionLegend(layout, currentPlot, candidate, title, occupiedContent);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const dx = result.layout.children[0].x - layout.children[0].x;
|
|
80
|
+
const dy = result.layout.children[0].y - layout.children[0].y;
|
|
81
|
+
if (dx || dy) {
|
|
82
|
+
for (const [id, frame] of frames) frames.set(id, { ...frame, x: frame.x + dx, y: frame.y + dy });
|
|
83
|
+
occupiedContent = occupiedContent.map(b => ({ left: b.left + dx, right: b.right + dx, top: b.top + dy, bottom: b.bottom + dy }));
|
|
84
|
+
currentPlot = { ...currentPlot, x: currentPlot.x + dx, y: currentPlot.y + dy };
|
|
85
|
+
}
|
|
86
|
+
frames.set(legend.id, result.frame);
|
|
87
|
+
placed.push({ id: legend.id, position });
|
|
88
|
+
layout = result.layout;
|
|
89
|
+
}
|
|
90
|
+
return { layout, frames };
|
|
91
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveTextBounds as m}from"../core/textMetrics.js";const p=Object.freeze(["fontSize","fontFamily","fontWeight","color"]),
|
|
1
|
+
import{resolveTextBounds as m,resolveTextLines as b}from"../core/textMetrics.js";const p=Object.freeze(["fontSize","fontFamily","fontWeight","color"]),S=Object.freeze({left:-Math.PI/2,right:Math.PI/2,top:0,bottom:0});function d(i,n,o,c){const s=b(i,{fontSize:n.fontSize,align:"center",baseline:"middle",blockAlign:"middle"}),l=m({...n,text:i,x:0,y:0,...s.length>1?{lines:s}:{},textAlign:"center",textBaseline:"middle"},c);return["left","right"].includes(o)?l.right-l.left:l.bottom-l.top}function T(i,n){const o=["left","right"].includes(n)?i.cellColumns?.[n]:i.cellRows[n];return o===void 0?0:["left","top"].includes(n)?o[0]:o.at(-1)}function M({roles:i,descriptors:n,space:o,headerLayout:c,profile:s,ids:l}){const u=[];for(const[g,f]of i){if(f.title===void 0)continue;const{side:t}=f,r=f.title,a={...Object.fromEntries(p.map(e=>[e,f[e]])),...r,side:t,align:r.align??"center",rotation:r.rotation??S[t]},x=Math.max(0,...n.filter(e=>e.config.side===t&&e.text!=="").map(e=>d(e.text,e.config,t,s)+e.config.offset+(e.clearance?.[t]??0)));a.offset=r.offset??Math.max(o[t],x)+8;const h=a.offset+d(r.text,a,t,s);c.outer[t]=Math.max(c.outer[t],h-o[t]-T(c,t),0),u.push({topology:"role-title",role:g,ids:l,text:r.text,config:a})}return u}export{M as prepareFacetHeaderTitles};
|
package/src/layout/labels.js
CHANGED
|
@@ -517,11 +517,45 @@ function textBounds(item, offset = { x: 0, y: 0 }, profile) {
|
|
|
517
517
|
}, profile);
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
|
|
520
|
+
// Query only nearby rectangles. Oversized bounds use the ordered fallback so
|
|
521
|
+
// extreme dimensions cannot allocate an unbounded number of grid cells.
|
|
522
|
+
function labelBoundsIndex() {
|
|
523
|
+
const cells = new Map(), rectangles = [], wide = [];
|
|
524
|
+
const keys = bounds => {
|
|
525
|
+
const x0 = Math.floor(bounds.left / 32), x1 = Math.floor(bounds.right / 32);
|
|
526
|
+
const y0 = Math.floor(bounds.top / 32), y1 = Math.floor(bounds.bottom / 32);
|
|
527
|
+
if (![x0, x1, y0, y1].every(Number.isSafeInteger) || (x1 - x0 + 1) * (y1 - y0 + 1) > 256) return undefined;
|
|
528
|
+
const result = [];
|
|
529
|
+
for (let x = x0; x <= x1; x++) for (let y = y0; y <= y1; y++) result.push(`${x},${y}`);
|
|
530
|
+
return result;
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
add(bounds) {
|
|
534
|
+
const id = rectangles.push(bounds) - 1, slots = keys(bounds);
|
|
535
|
+
if (!slots) { wide.push(id); return; }
|
|
536
|
+
for (const key of slots) {
|
|
537
|
+
if (!cells.has(key)) cells.set(key, []);
|
|
538
|
+
cells.get(key).push(id);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
near(bounds) {
|
|
542
|
+
const slots = keys(bounds);
|
|
543
|
+
if (!slots) return rectangles.map((bounds, id) => ({ bounds, id }));
|
|
544
|
+
const ids = new Set(wide);
|
|
545
|
+
for (const key of slots) for (const id of cells.get(key) ?? []) ids.add(id);
|
|
546
|
+
// Preserve stable summation and tie breaking from the ordered scan.
|
|
547
|
+
return [...ids].sort((a, b) => a - b).map(id => ({ bounds: rectangles[id], id }));
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function candidateScore(bounds, boundary, placed, candidate, order, account) {
|
|
553
|
+
const nearby = placed.near(bounds);
|
|
554
|
+
account(nearby.length + 1);
|
|
521
555
|
return {
|
|
522
556
|
outside: overflow(bounds, boundary),
|
|
523
|
-
overlap:
|
|
524
|
-
(area, prior) => area + intersectionArea(bounds, prior),
|
|
557
|
+
overlap: nearby.reduce(
|
|
558
|
+
(area, prior) => area + intersectionArea(bounds, prior.bounds),
|
|
525
559
|
0
|
|
526
560
|
),
|
|
527
561
|
distanceSquared: candidate.distanceSquared,
|
|
@@ -537,21 +571,15 @@ function compareScore(first, second) {
|
|
|
537
571
|
first.order - second.order;
|
|
538
572
|
}
|
|
539
573
|
|
|
540
|
-
function overlapPairs(items) {
|
|
541
|
-
const pairs = [];
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
) {
|
|
550
|
-
pairs.push([items[first].id, items[second].id]);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return pairs;
|
|
574
|
+
function overlapPairs(items, account) {
|
|
575
|
+
const pairs = [], index = labelBoundsIndex();
|
|
576
|
+
items.forEach((item, second) => {
|
|
577
|
+
const nearby = index.near(item.collisionBounds);
|
|
578
|
+
account(nearby.length + 1);
|
|
579
|
+
for (const first of nearby) if (intersectionArea(first.bounds, item.collisionBounds) > 0) pairs.push([first.id, second]);
|
|
580
|
+
index.add(item.collisionBounds);
|
|
581
|
+
});
|
|
582
|
+
return pairs.sort((a, b) => a[0] - b[0] || a[1] - b[1]).map(pair => pair.map(id => items[id].id));
|
|
555
583
|
}
|
|
556
584
|
|
|
557
585
|
export function resolveLabelLayout({ items, bounds, ...options } = {}, profile) {
|
|
@@ -563,15 +591,14 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
|
|
|
563
591
|
const ids = new Set();
|
|
564
592
|
for (const item of items) validateItem(item, ids);
|
|
565
593
|
const candidates = enumerateLabelOffsets(policy);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
);
|
|
594
|
+
let work = 0;
|
|
595
|
+
const account = amount => validateWorkLimit(work += amount, "Label layout work");
|
|
596
|
+
account(items.length);
|
|
570
597
|
const base = items.map(item => ({
|
|
571
598
|
id: item.id,
|
|
572
599
|
collisionBounds: expanded(textBounds(item, undefined, profile), policy.padding)
|
|
573
600
|
}));
|
|
574
|
-
const placed =
|
|
601
|
+
const placed = labelBoundsIndex();
|
|
575
602
|
const resolved = [];
|
|
576
603
|
for (const item of items) {
|
|
577
604
|
let best;
|
|
@@ -583,7 +610,8 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
|
|
|
583
610
|
bounds,
|
|
584
611
|
placed,
|
|
585
612
|
candidate,
|
|
586
|
-
order
|
|
613
|
+
order,
|
|
614
|
+
account
|
|
587
615
|
);
|
|
588
616
|
if (best === undefined || compareScore(score, best.score) < 0) {
|
|
589
617
|
best = { candidate, bounds: boundsAtCandidate, collisionBounds, score };
|
|
@@ -602,11 +630,11 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}, profile)
|
|
|
602
630
|
bounds: best.bounds,
|
|
603
631
|
collisionBounds: best.collisionBounds
|
|
604
632
|
};
|
|
605
|
-
placed.
|
|
633
|
+
placed.add(resolvedItem.collisionBounds);
|
|
606
634
|
resolved.push(resolvedItem);
|
|
607
635
|
}
|
|
608
|
-
const beforePairs = overlapPairs(base);
|
|
609
|
-
const afterPairs = overlapPairs(resolved);
|
|
636
|
+
const beforePairs = overlapPairs(base, account);
|
|
637
|
+
const afterPairs = overlapPairs(resolved, account);
|
|
610
638
|
const outside = resolved
|
|
611
639
|
.filter(item => overflow(item.collisionBounds, bounds) > 0)
|
|
612
640
|
.map(item => item.id);
|
package/src/layout/legend.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveLegendTitleText } from "./legendText.js";
|
|
1
2
|
import { PLOT_LEGEND_POSITIONS } from "../core/vocabulary.js";
|
|
2
3
|
import {
|
|
3
4
|
formatVisibleText,
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
} from "../core/textMetrics.js";
|
|
6
7
|
|
|
7
8
|
export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile) {
|
|
8
|
-
return
|
|
9
|
+
return resolveLegendTitleText(String(value), style, profile).width;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
// A conservative line-height reserve, not a font-specific glyph measurement.
|
|
@@ -37,7 +38,7 @@ export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
|
|
|
37
38
|
config.itemGap * Math.max(0, actualColumns - 1);
|
|
38
39
|
const rowHeight = Math.max(
|
|
39
40
|
config.labels.fontSize,
|
|
40
|
-
symbolHeight
|
|
41
|
+
symbolHeight, ...labels.map(label => resolveLegendTitleText(label, config.labels, profile).height)
|
|
41
42
|
);
|
|
42
43
|
const gridHeight = rowHeight * rowCount +
|
|
43
44
|
config.itemGap * Math.max(0, rowCount - 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{canvasOverflowError as
|
|
1
|
+
import{canvasOverflowError as N}from"./canvas.js";import{annotateError as C}from"../core/diagnostics.js";import{PLOT_LEGEND_POSITIONS as D}from"./legend.js";import{SIDE_LEGEND_BLOCK_GAP as H,HORIZONTAL_LEGEND_BLOCK_GAP as P}from"./legendLane.js";function S({blocks:m,group:l,plot:t,canvas:x,allowOverflow:G=!1,deferCanvasFit:I=!1,deferPlotFit:L=!1}){if(m.length===0)return[];const{placement:e,blockLayout:h}=l,d=D.includes(e.position),w=["left","right"].includes(e.position),y=h==="column"||h==="wrap"&&w,p=d||w||e.at!==void 0?H:P,_=t.width-(d?e.offset*2:0);let a=0,E=0,c=0;const g=m.map(i=>{const o=i.bounds.right-i.bounds.left,O=i.bounds.bottom-i.bounds.top;if(h==="wrap"&&!y&&!d&&o>_&&!G)throw new Error("Horizontal legend block requires more plot width.");!y&&h==="wrap"&&a>0&&a+o>_&&(a=0,E+=c+p,c=0);const M={id:i.id,x:a,y:E,width:o,height:O};return y?E+=O+p:(a+=o+p,c=Math.max(c,O)),M}),s=Math.max(...g.map(i=>i.x+i.width)),u=Math.max(...g.map(i=>i.y+i.height));if(h==="wrap"&&e.position==="top"){const i=new Map;for(const o of g)i.set(o.y,Math.max(i.get(o.y)??0,o.height));for(const o of g)o.y=u-o.y-i.get(o.y)}let r,f;if(e.at!==void 0)r=t.x+e.at.x,f=t.y+e.at.y;else if(d){if(!L&&(s+2*e.offset>t.width||u+2*e.offset>t.height))throw new Error("In-plot legend group does not fit the plot inset.");r=e.position.endsWith("left")?t.x+e.offset:t.x+t.width-e.offset-s,f=e.position.startsWith("top")?t.y+e.offset:t.y+t.height-e.offset-u}else w?(r=e.position==="right"?t.x+t.width+e.offset:t.x-e.offset-s,f=t.y):(r=e.align==="left"?t.x:e.align==="right"?t.x+t.width-s:t.x+(t.width-s)/2,f=e.position==="top"?t.y-e.offset-u:t.y+t.height+e.offset);e.at===void 0&&(w?f+=e.alongOffset??0:r+=e.alongOffset??0);const n={left:r,top:f,right:r+s,bottom:f+u};if(d&&!L&&(n.left<t.x||n.top<t.y||n.right>t.x+t.width||n.bottom>t.y+t.height))throw new Error("In-plot legend group does not fit the plot inset.");if(!I&&(n.left<0||n.top<0||n.right>x.width||n.bottom>x.height)){const i=N("Legend group requires more Canvas space.",[n],x);throw e.at!==void 0||l.inferredOffset!==!0?C(i,{requiresExplicitCanvas:!0}):i}return g.map((i,o)=>({id:i.id,dx:r+i.x-m[o].bounds.left,dy:f+i.y-m[o].bounds.top}))}export{S as resolveLegendGroupPlacement};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
|
|
2
2
|
import { alignLegendStart, resolveLegendGrid, isPlotLegend, legendLabelLineHeight } from "./legend.js";
|
|
3
|
-
import { alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
|
|
3
|
+
import { alignLegendLabels, alignLegendTitle, resolveLegendTitleText } from "./legendText.js";
|
|
4
4
|
import {resolveLegendContentSpacing} from "./legendSpacing.js";
|
|
5
5
|
|
|
6
6
|
// Measure item content before choosing its edge. Family owners supply the
|
|
@@ -32,7 +32,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
32
32
|
let itemRows;
|
|
33
33
|
let title;
|
|
34
34
|
if (config.layout === "legacy-bottom") {
|
|
35
|
-
const widths = labels.map(text => sampleWidth + config.labels.offset +
|
|
35
|
+
const widths = labels.map(text => sampleWidth + config.labels.offset + resolveLegendTitleText(text, config.labels, profile).width);
|
|
36
36
|
const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
|
|
37
37
|
const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
|
|
38
38
|
let cursor = x;
|
|
@@ -52,7 +52,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
52
52
|
const width = Math.max(grid.gridWidth, titleWidth);
|
|
53
53
|
const x = config.position === "right" ? plot.x + plot.width + config.offset
|
|
54
54
|
: plot.x - config.offset - width;
|
|
55
|
-
const itemHeight = Math.max(sampleHeight, legendLabelLineHeight(config.labels.fontSize));
|
|
55
|
+
const itemHeight = Math.max(sampleHeight, legendLabelLineHeight(config.labels.fontSize), ...labels.map(text => resolveLegendTitleText(text, config.labels, profile).height));
|
|
56
56
|
const pitch = Math.max(config.itemGap, itemHeight);
|
|
57
57
|
let cursor = x;
|
|
58
58
|
const columnStarts = grid.columnWidths.map(columnWidth => {
|
|
@@ -69,7 +69,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
69
69
|
text: config.title, ...config.titleStyle, lines: titleText.lines,
|
|
70
70
|
textAlign: "left", textBaseline: "middle" }, profile);
|
|
71
71
|
const labelTops = labels.map(text => resolveTextBounds({
|
|
72
|
-
x: 0, y: 0, text, ...config.labels,
|
|
72
|
+
x: 0, y: 0, text, ...config.labels, lines: resolveLegendTitleText(text, config.labels, profile).lines,
|
|
73
73
|
textAlign: "left", textBaseline: "middle"
|
|
74
74
|
}, profile).top);
|
|
75
75
|
const itemTop = Math.min(...sampleBounds.map(bounds => bounds.top), ...labelTops);
|
|
@@ -104,9 +104,10 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, pr
|
|
|
104
104
|
: { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
|
|
105
105
|
}
|
|
106
106
|
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
107
|
-
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
107
|
+
x, y, text, ...style, lines: resolveLegendTitleText(text, style, profile).lines, textAlign: align, textBaseline: "middle"
|
|
108
108
|
}, profile);
|
|
109
|
-
|
|
109
|
+
if (config.labels.align !== undefined) labelX = alignLegendLabels(labelX.map((x, index) => ({ x, y: itemY[index], align: "left" })), labels, config.labels, profile).map(position => position.x);
|
|
110
|
+
const labelBounds = labels.map((label,index) => textBounds(labelX[index],itemY[index],label,config.labels,config.labels.align ?? "left"));
|
|
110
111
|
const symbolItemBounds = symbolX.map((x, index) => {
|
|
111
112
|
const local = sampleBounds[index];
|
|
112
113
|
return { left: x + local.left, right: x + local.right,
|
package/src/layout/legendLane.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{canvasOverflowError as
|
|
1
|
+
import{canvasOverflowError as $}from"./canvas.js";import{unionBounds as z}from"../core/textMetrics.js";const N=24,v=40,k=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 $(`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,k+i.symbol.left-i.symbol.centerX-e),r=Math.max(r,k+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:k-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,allowOverflow:e=!1}){if(!["right","left"].includes(t))throw new Error(`Unsupported side legend lane "${t}".`);const s=r.flatMap(c=>c.blocks);if(s.length<2)return;const a=Math.max(X,...s.filter(c=>!c.preserveGrid).map(c=>k+Math.abs(c.labels.x-c.symbol.centerX))),p=A(s,a),h=s.reduce((c,m)=>Math.max(c,m.offset),-1/0),u=t==="right"?o.x+o.width+h:o.x-h-p.right,d=u+k,G=u+a;let y=o.y;const D=[],I=[],f=[];for(const c of r){const m=T(c.border);f.length>0&&(y+=m);const L=[];for(const C of c.blocks){const b=Y(C,y,a);L.push(b),D.push({...b,titleDx:b.titleDx===void 0?void 0:b.titleDx+u,symbolDx:b.symbolDx+u,labelDx:b.labelDx+u}),y=b.bounds.bottom+N}const l=z(L.map(C=>C.bounds)),M=A(c.blocks,a),x={left:u+M.left,right:u+M.right,top:l.top,bottom:l.bottom},S={left:x.left-m,right:x.right+m,top:x.top-m,bottom:x.bottom+m};f.push(S),c.backgroundId!==void 0&&I.push({id:c.backgroundId,x:x.left-c.border.padding,y:x.top-c.border.padding,width:x.right-x.left+c.border.padding*2,height:x.bottom-x.top+c.border.padding*2}),y=S.bottom+N}const g=z(f),E=o.y-g.top,_=w(g,0,E);if(!e&&(_.left<0||_.right>n.width||_.top<0||_.bottom>n.height))throw $(`Legend lane requires more ${t}-margin or vertical Canvas space.`,[_],n);if(i!==void 0&&H(_,i))throw new Error(`${t[0].toUpperCase()}${t.slice(1)} legend lane and y-axis guides require more margin space.`);return{side:t,titleStartX:u,symbolCenterX:d,labelStartX:G,placements:D.map(c=>({...c,dy:c.dy+E,bounds:w(c.bounds,0,E)})),backgrounds:I.map(c=>({...c,y:c.y+E})),occupied:_}}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 $(`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 a=q(s.horizontal,s.inset);if(a.right-a.left>o.width&&!n)throw new Error("Horizontal legend block requires more plot width.");let h=e-a.left,u=w(a,h,0);i.length>0&&u.right>o.x+o.width&&(r.push(i),i=[],e=o.x,h=e-a.left,u=w(a,h,0)),i.push({group:s,interval:u,dx:h}),e=u.right+v}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 u=i-e.element.top,d=w(e.content,s,u);return{id:e.id,dx:s,titleDy:u,contentDy:u,foreground:d,occupied:q(d,e.inset),padding:e.padding,backgroundId:e.backgroundId}}const a=n===void 0?i-B(e.element.top,e.element.bottom):i-e.element.top,p=e.title===void 0?0:e.inline===!0?a:n-e.title.y,h=z([...e.title===void 0?[]:[w(e.title.bounds,s,p)],w(e.content,s,a)]);return{id:e.id,dx:s,titleDy:p,contentDy:a,foreground:h,occupied:q(h,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),a=s.map(P),p=[];let h;for(let d=0;d<a.length;d+=1){const G=a[d],y=z(G.map(f=>f.occupied));let D;if(d===0){const f=t==="top"?s[d].reduce((g,E)=>Math.max(g,E.group.horizontal.bottom),-1/0):s[d].reduce((g,E)=>Math.min(g,E.group.horizontal.top),1/0);D=t==="top"?f-y.bottom:f-y.top}else D=t==="bottom"?h.bottom+v-y.top:h.top-v-y.bottom;const I=G.map(f=>R(f,D));p.push(...I),h=z(I.map(f=>f.occupied))}const u=z(p.map(d=>d.occupied));if(p.some(d=>d.occupied.left<0||d.occupied.right>n.width||d.occupied.top<0||d.occupied.bottom>n.height))throw $(`Legend lane requires more ${t}-margin or Canvas space.`,[u],n);if(p.some(d=>i.some(G=>H(d.occupied,G)))){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:p.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:u,rowCount:s.length}}function V({edge:t,plot:o,canvas:n,groups:r,align:i,offset:e,border:s,backgroundId:a,collisionBounds:p=[],allowOverflow:h=!1}){const u=T(s),d={...o,x:o.x+u,width:o.width-u*2},y=O(r,d,h).map(P);let D=0;const I=[];for(const l of y){const M=z(l.map(b=>b.occupied)),x=t==="top"?D-M.bottom:D-M.top,S=l.map(b=>R(b,x));I.push(...S);const C=z(S.map(b=>b.occupied));D=t==="top"?C.top-v:C.bottom+v}const f=z(I.map(l=>l.occupied)),g=q(f,u),E=g.right-g.left,c=(i==="left"?o.x:i==="right"?o.x+o.width-E:o.x+(o.width-E)/2)-g.left,m=t==="top"?o.y-e-g.bottom:o.y+o.height+e-g.top,L=w(g,c,m);if(L.left<0||L.right>n.width||L.top<0||L.bottom>n.height)throw $(`Combined legend requires more ${t}-margin or Canvas space.`,[L],n);if(p.some(l=>H(L,l)))throw new Error(`Combined ${t} legend and ${t==="top"?"chart titles":"x-axis guides"} require more margin space.`);return{placements:I.map(l=>({...l,dx:l.dx+c,titleDy:l.titleDy+m,contentDy:l.contentDy+m,...l.backgroundId===void 0?{}:{background:{id:l.backgroundId,x:l.foreground.left+c-l.padding,y:l.foreground.top+m-l.padding,width:l.foreground.right-l.foreground.left+l.padding*2,height:l.foreground.bottom-l.foreground.top+l.padding*2}}})),occupied:L,background:a===void 0?void 0:{id:a,x:f.left+c-s.padding,y:f.top+m-s.padding,width:f.right-f.left+s.padding*2,height:f.bottom-f.top+s.padding*2}}}export{v as HORIZONTAL_LEGEND_BLOCK_GAP,U as HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP,N as SIDE_LEGEND_BLOCK_GAP,X as SIDE_LEGEND_LABEL_START,k as SIDE_LEGEND_SYMBOL_CENTER,K as isHorizontalEdgeLegend,V as resolveHorizontalLegendGroup,Q as resolveHorizontalLegendLane,j as resolveSideLegendLane,W as resolveSingleHorizontalLegendPlacement,J as resolveSingleSideLegendPlacement};
|
package/src/layout/legendText.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveTextBounds as
|
|
1
|
+
import{resolveTextBounds as m,resolveTextLines as x}from"../core/textMetrics.js";function u(n,r,t){const f=x(String(n),{fontSize:r.fontSize,align:"left",baseline:"middle",blockAlign:"middle"}),g=f.length>1?f:void 0,e=m({x:0,y:0,text:String(n),...r,lines:g,textAlign:"left",textBaseline:"middle"},t);return{lines:g,width:e.right-e.left,height:e.bottom-e.top,extraHeight:e.bottom-e.top-r.fontSize}}function p(n,r,t,f,g){if(t.align===void 0)return n;const e=u(r,t,g),i=m({x:n.x,y:n.y,text:r,...t,lines:e.lines,textAlign:n.align,textBaseline:"middle"},g),a=Math.min(i.left,...f.map(o=>o.left)),l=Math.max(i.right,...f.map(o=>o.right));return{...n,x:t.align==="left"?a:t.align==="right"?l:(a+l)/2,align:t.align}}function b(n,r,t,f){if(t.align===void 0)return n;const g=new Map,e=n.map(i=>`${i.x}:${i.align??"left"}`);return n.forEach((i,a)=>{const l=u(r[a],t,f),o=i.align==="right"?l.width:i.align==="center"?l.width/2:0,h=i.x-o,c=h+l.width,d=g.get(e[a]);g.set(e[a],{left:Math.min(d?.left??h,h),right:Math.max(d?.right??c,c)})}),n.map((i,a)=>{const l=g.get(e[a]);return{...i,x:t.align==="left"?l.left:t.align==="right"?l.right:(l.left+l.right)/2,align:t.align}})}export{b as alignLegendLabels,p as alignLegendTitle,u as resolveLegendTitleText};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { normalizeActionTargets } from "../grammar/actionTargets.js";
|
|
3
|
+
import { resolveEligibleLayer } from "../selectors/layers.js";
|
|
4
|
+
import { ownership } from "../selectors/markOwners.js";
|
|
5
|
+
|
|
6
|
+
// Creation and inspection share the same explicit source ownership contract.
|
|
7
|
+
export function resolveAdjacentSource(program, source) {
|
|
8
|
+
const grouped = program.compositionSpec && program.compositionSpec.type !== "facet";
|
|
9
|
+
const scoped = isPlainObject(source);
|
|
10
|
+
if (grouped && !scoped) throw new Error("Adding beside a concat requires source: { childPath, id }.");
|
|
11
|
+
let view = program, id = source;
|
|
12
|
+
if (scoped) {
|
|
13
|
+
const selector = normalizeActionTargets(source)[0];
|
|
14
|
+
if (selector.id === undefined) throw new Error("Adjacent source requires an explicit mark id.");
|
|
15
|
+
id = selector.id;
|
|
16
|
+
for (const child of selector.childPath) {
|
|
17
|
+
if (view.compositionSpec?.type === "facet") throw new Error("Select the facet source view, not a generated facet cell.");
|
|
18
|
+
if (!Object.hasOwn(view.children, child)) throw new Error(`Unknown adjacent source child "${child}".`);
|
|
19
|
+
view = view.children[child];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (view.compositionSpec && view.compositionSpec.type !== "facet") throw new Error("Adjacent source must identify a unit or facet view.");
|
|
23
|
+
const owners = ownership(view);
|
|
24
|
+
const layer = resolveEligibleLayer(view, { target: id, targetOption: "source", label: "adjacent chart source",
|
|
25
|
+
predicate: layer => !owners.has(layer.id) && !layer.derivedBindings?.overlay && layer.data !== undefined });
|
|
26
|
+
return { view, layer };
|
|
27
|
+
}
|