@bicharts/chart-host 0.6.84 → 0.6.86
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/README.md +64 -0
- package/dist/chunk-BQFG5HWZ.mjs +582 -0
- package/dist/{chunk-2HKO6GEK.mjs → chunk-MXL5SK7X.mjs} +11 -575
- package/dist/chunk-ZGQURBRJ.mjs +265 -0
- package/dist/index.mjs +37 -275
- package/dist/legend-fit.mjs +1645 -0
- package/dist/react.mjs +6 -3
- package/dist/types/host.d.ts +9 -0
- package/dist/types/legendFit/frameFit.d.ts +89 -0
- package/dist/types/legendFit/frameFitDom.d.ts +70 -0
- package/dist/types/legendFit/index.d.ts +5 -0
- package/dist/types/legendFit/legendReconcile.d.ts +295 -0
- package/dist/types/legendFit/legendReconcileDom.d.ts +109 -0
- package/dist/types/legendFit/pass.d.ts +29 -0
- package/dist/types/react.d.ts +7 -0
- package/package.json +6 -1
package/dist/react.mjs
CHANGED
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
loadGeo,
|
|
23
23
|
syncMemberSelection,
|
|
24
24
|
toSourceRows
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-MXL5SK7X.mjs";
|
|
26
26
|
import "./chunk-QIRLWJBI.mjs";
|
|
27
27
|
import "./chunk-2ZUUIZWL.mjs";
|
|
28
28
|
import "./chunk-7HKVV5T5.mjs";
|
|
29
29
|
import "./chunk-IURROJT7.mjs";
|
|
30
|
+
import "./chunk-BQFG5HWZ.mjs";
|
|
30
31
|
import "./chunk-A2GMXZP7.mjs";
|
|
31
32
|
import "./chunk-MR6TL7QZ.mjs";
|
|
32
33
|
import "./chunk-GHODWOAL.mjs";
|
|
@@ -68,6 +69,7 @@ function BicChart(props) {
|
|
|
68
69
|
viewState,
|
|
69
70
|
labelContrast,
|
|
70
71
|
onLabelContrast,
|
|
72
|
+
layoutPass,
|
|
71
73
|
onInvalidSentinel,
|
|
72
74
|
id,
|
|
73
75
|
filteredBy,
|
|
@@ -149,6 +151,7 @@ function BicChart(props) {
|
|
|
149
151
|
geoKind,
|
|
150
152
|
viewState,
|
|
151
153
|
labelContrast,
|
|
154
|
+
layoutPass,
|
|
152
155
|
onLabelContrast: (r) => onLabelContrastRef.current?.(r),
|
|
153
156
|
// A GETTER, not a wrapper: the host decides whether to
|
|
154
157
|
// swallow the sentinel throw by whether this is SET, so
|
|
@@ -180,7 +183,7 @@ function BicChart(props) {
|
|
|
180
183
|
host.destroy();
|
|
181
184
|
hostRef.current = null;
|
|
182
185
|
};
|
|
183
|
-
}, [code, renderFn, d3, geoKind, labelContrast, controls]);
|
|
186
|
+
}, [code, renderFn, d3, geoKind, labelContrast, layoutPass, controls]);
|
|
184
187
|
useLayoutEffect(() => {
|
|
185
188
|
const host = hostRef.current;
|
|
186
189
|
if (!host || !selects) return;
|
|
@@ -206,7 +209,7 @@ function BicChart(props) {
|
|
|
206
209
|
b.detach();
|
|
207
210
|
if (bindingRef.current === b) bindingRef.current = null;
|
|
208
211
|
};
|
|
209
|
-
}, [selects, code, renderFn, d3, geoKind, labelContrast]);
|
|
212
|
+
}, [selects, code, renderFn, d3, geoKind, labelContrast, layoutPass]);
|
|
210
213
|
useEffect(() => {
|
|
211
214
|
if (!hostRef.current || !options) return;
|
|
212
215
|
if (builtWithRef.current?.optKey === optKey) return;
|
package/dist/types/host.d.ts
CHANGED
|
@@ -190,6 +190,15 @@ export interface ChartHostConfig {
|
|
|
190
190
|
labelContrast?: boolean | LabelContrastOptions;
|
|
191
191
|
/** What the label-contrast pass did after each render - counts, so a host can log them. */
|
|
192
192
|
onLabelContrast?: (report: LabelContrastReport) => void;
|
|
193
|
+
/**
|
|
194
|
+
* A LAYOUT PASS the host brings, run after every render (both the synchronous and the promise lane, and every
|
|
195
|
+
* setOptions / setData re-render) AFTER the label-contrast pass and BEFORE the hit-target heal, the selection
|
|
196
|
+
* repaint, the censuses and the fit - the order a pass that moves the legend or grows the viewBox needs, because
|
|
197
|
+
* the fit must measure the final layout. Absent by default: the main entry carries no such pass. Build one with
|
|
198
|
+
* createLegendFitPass() from "@bicharts/chart-host/legend-fit". It must not throw (a throw is swallowed and the
|
|
199
|
+
* render stands); it reports through its own callback, never through this host.
|
|
200
|
+
*/
|
|
201
|
+
layoutPass?: (container: HTMLElement) => void;
|
|
193
202
|
/**
|
|
194
203
|
* What the id-scoping pass did after each render: how many of the chart's ids got this host's suffix, and which
|
|
195
204
|
* of them also existed elsewhere in the page (a clash it prevented - two same-size charts naming one clip path).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame fit — pure geometry.
|
|
3
|
+
*
|
|
4
|
+
* The third measured sibling of the label-contrast and legend-reconcile passes.
|
|
5
|
+
* Generated code routinely parks a footer/source caption against the
|
|
6
|
+
* RAW svg edge — `svg.append('text').attr('y', height - 2)` — instead of reserving
|
|
7
|
+
* a band for it inside `margin.bottom`. That looks right in the code and is wrong
|
|
8
|
+
* on screen: a baseline is not an ink box. A glyph descends roughly 0.27 * fontSize
|
|
9
|
+
* BELOW its baseline, so a 2px gap at font-size 7.5 puts the tails of "g", "p", "y"
|
|
10
|
+
* exactly on (or past) the frame, and `.lch-d3ctr` is `overflow:hidden` — the host
|
|
11
|
+
* silently slices them. Measured live on a Marimekko (690x340): the
|
|
12
|
+
* caption's ink box ran 331->340 in a 340-tall frame, i.e. flush, tails gone.
|
|
13
|
+
*
|
|
14
|
+
* Corpus: 41 D3 generations across ~20 chart types park text within 5 user units of
|
|
15
|
+
* the frame, so this is a RENDERER-level habit, not one chart's bug.
|
|
16
|
+
*
|
|
17
|
+
* WHY EXTEND THE VIEWBOX RATHER THAN MOVE THE TEXT: nudging a glyph back inside
|
|
18
|
+
* can shove it into whatever sits above it — in that Marimekko the axis title's ink
|
|
19
|
+
* ends 12 units above the caption, so a blind 6-unit lift would collide the two
|
|
20
|
+
* and trade a clipped line for an overlapping one. Growing the viewBox instead
|
|
21
|
+
* makes the SVG scale to fit its container: nothing moves relative to anything
|
|
22
|
+
* else, nothing is clipped, and a 2-unit grow on a 340-unit frame is a 0.6%
|
|
23
|
+
* rescale nobody can see. This is exactly the remedy the legend-reconcile pass already
|
|
24
|
+
* uses for a legend that runs off the right edge.
|
|
25
|
+
*
|
|
26
|
+
* Coordinates: EVERYTHING here is in the SVG's USER space (== viewBox units). The
|
|
27
|
+
* caller maps measured client-px boxes through `svg.getScreenCTM().inverse()`
|
|
28
|
+
* first, which captures viewBox, preserveAspectRatio, container scaling AND host
|
|
29
|
+
* page zoom in one matrix — so this module needs no px<->unit factor. Same
|
|
30
|
+
* contract as legendReconcile; the box and viewBox types are shared with it.
|
|
31
|
+
*
|
|
32
|
+
* DOM-free so it can be unit-tested on plain numbers; the DOM I/O lives in
|
|
33
|
+
* frameFitDom.fitTextToFrame.
|
|
34
|
+
*/
|
|
35
|
+
import type { Rect, ViewBox } from "./legendReconcile";
|
|
36
|
+
export type { Rect, ViewBox };
|
|
37
|
+
export interface FrameFitPlan {
|
|
38
|
+
action: "none" | "extend" | "defer";
|
|
39
|
+
top: number;
|
|
40
|
+
right: number;
|
|
41
|
+
bottom: number;
|
|
42
|
+
left: number;
|
|
43
|
+
newViewBox: ViewBox;
|
|
44
|
+
considered: number;
|
|
45
|
+
rescued: number;
|
|
46
|
+
deferred: number;
|
|
47
|
+
deferredIdx: number[];
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const FRAME_PAD = 1;
|
|
51
|
+
export declare const MAX_EXTEND_FRACTION = 0.08;
|
|
52
|
+
/**
|
|
53
|
+
* Work out how much the viewBox must grow so every measured ink box sits inside
|
|
54
|
+
* the frame with FRAME_PAD to spare.
|
|
55
|
+
*
|
|
56
|
+
* @param boxes ink boxes in USER space (see module note on the CTM mapping)
|
|
57
|
+
* @param vb the SVG's current viewBox in user units
|
|
58
|
+
* @param maxFraction per-axis extension ceiling, as a fraction of the frame
|
|
59
|
+
*/
|
|
60
|
+
export declare function planFrameFit(boxes: Rect[], vb: ViewBox, maxFraction?: number): FrameFitPlan;
|
|
61
|
+
/** Serialize a viewBox the way the SVG attribute wants it. */
|
|
62
|
+
export declare function viewBoxAttr(vb: ViewBox): string;
|
|
63
|
+
export type TextAnchor = "start" | "middle" | "end";
|
|
64
|
+
/** The minimum readable type, px. */
|
|
65
|
+
export declare const FIT_TYPE_FLOOR = 10;
|
|
66
|
+
/** A shrink never goes below this fraction of the chart's own size: past it, cut instead. */
|
|
67
|
+
export declare const FIT_SHRINK_FLOOR = 0.8;
|
|
68
|
+
/**
|
|
69
|
+
* The width, in user units, a label may keep so that its box sits inside `frame` (with
|
|
70
|
+
* FRAME_PAD), given the edge its anchor holds - or null when no shortening can fit it
|
|
71
|
+
* (it overflows vertically, or its held edge is itself outside the frame).
|
|
72
|
+
*/
|
|
73
|
+
export declare function labelRoom(box: Rect, frame: ViewBox, anchor: TextAnchor): number | null;
|
|
74
|
+
export interface LabelFitDecision {
|
|
75
|
+
action: "none" | "shrink" | "cut";
|
|
76
|
+
fontSize: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Shrink or cut: a label `width` units wide at `fontSize` px that must fit `room` units.
|
|
80
|
+
* Width scales with the font size, so the first guess is exact to within hinting; the caller
|
|
81
|
+
* re-measures after a shrink and falls through to the cut if hinting disagreed.
|
|
82
|
+
*/
|
|
83
|
+
export declare function labelFitDecision(width: number, room: number, fontSize: number): LabelFitDecision;
|
|
84
|
+
/**
|
|
85
|
+
* The widest prefix of `full` (plus the ellipsis) that `fits`, never cut inside a number:
|
|
86
|
+
* a cut between two digits moves back to before the number ("largest 12..." of
|
|
87
|
+
* "largest 128 of 400" states a different count). Empty when nothing fits.
|
|
88
|
+
*/
|
|
89
|
+
export declare function cutToFit(full: string, fits: (s: string) => boolean, ell?: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame fit - the labels the viewBox could not rescue.
|
|
3
|
+
*
|
|
4
|
+
* fitTextToFrame grows the viewBox for text that pokes a few units past the frame and
|
|
5
|
+
* DEFERS text that is further out, because swallowing it would shrink the whole chart. A
|
|
6
|
+
* deferred label used to stay exactly where the chart put it, sliced by the frame, and
|
|
7
|
+
* a log line only counted it: a Bullet of 21 KPIs at 1230x626 reported
|
|
8
|
+
* `{"rescued":8,"deferred":6,"grew":"left+68.06"}` while six KPI names still read
|
|
9
|
+
* "...f Service Effort - Reduce effort".
|
|
10
|
+
*
|
|
11
|
+
* A label that overflows on the side its anchor does NOT hold is fitted in place: the type
|
|
12
|
+
* shrinks first (see labelFitDecision - never below the 10px floor or 80% of the chart's size),
|
|
13
|
+
* then the string is cut to the widest prefix that fits, never inside a number, and the full
|
|
14
|
+
* text rides a <title>. A label that overflows vertically, or on the side its anchor holds, is
|
|
15
|
+
* counted as unfixable and left alone. The geometry is in frameFit.ts; this is the DOM I/O,
|
|
16
|
+
* with the measurement handed in so the caller's CTM mapping (viewBox, zoom) is the only one.
|
|
17
|
+
*/
|
|
18
|
+
import { Rect, ViewBox, FrameFitPlan } from "./frameFit";
|
|
19
|
+
export interface DeferredLabelFitReport {
|
|
20
|
+
/** Deferred text elements looked at. */
|
|
21
|
+
deferred: number;
|
|
22
|
+
/** Fitted by a smaller font size alone. */
|
|
23
|
+
shrunk: number;
|
|
24
|
+
/** Fitted by cutting the string (after any shrink). */
|
|
25
|
+
cut: number;
|
|
26
|
+
/** Could not be fitted by shortening: vertical overflow, the held edge outside, rotated, tspans. */
|
|
27
|
+
unfixable: number;
|
|
28
|
+
/** The first few full labels that were fitted, for the log line. */
|
|
29
|
+
labels: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fit ONE label, `width` wide, into `room` (both in whatever units `widthNow` measures): shrink the
|
|
33
|
+
* type first (labelFitDecision - never below the 10px floor or 80% of the label's size), then cut
|
|
34
|
+
* the string to the widest prefix that fits, never inside a number, with the full text riding a
|
|
35
|
+
* <title>. `minChars` keeps at least that many characters before the ellipsis even when they do not
|
|
36
|
+
* fit - a caller that must never REMOVE a label (the nominal axis) passes 1. Shared by the
|
|
37
|
+
* frame fit and the nominal-axis cut.
|
|
38
|
+
*/
|
|
39
|
+
export declare function fitLabelToRoom(el: Element, width: number, room: number, widthNow: () => number, minChars?: number): "none" | "shrunk" | "cut";
|
|
40
|
+
/**
|
|
41
|
+
* Fit each deferred label to `frame`. `measure` returns the element's box in the SAME user
|
|
42
|
+
* space as `frame` (null when it cannot). Never throws; a label it cannot judge is unfixable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function fitDeferredLabels(els: Element[], frame: ViewBox, measure: (el: Element) => Rect | null): DeferredLabelFitReport;
|
|
45
|
+
/** Perf ceiling: every text costs a getBoundingClientRect, and a Tabular can paint thousands of
|
|
46
|
+
* cells; above this the sweep costs more than the defect it fixes (and such charts scroll their
|
|
47
|
+
* own body anyway, so their overflow is intentional). */
|
|
48
|
+
export declare const FRAME_FIT_TEXT_CAP = 600;
|
|
49
|
+
export interface FrameFitOutcome {
|
|
50
|
+
/** Set when the pass declined: too many texts to measure. */
|
|
51
|
+
skipped?: {
|
|
52
|
+
texts: number;
|
|
53
|
+
cap: number;
|
|
54
|
+
};
|
|
55
|
+
/** The viewBox plan (planFrameFit). */
|
|
56
|
+
plan?: FrameFitPlan;
|
|
57
|
+
/** What happened to the labels the plan deferred, when there were any. */
|
|
58
|
+
labels?: DeferredLabelFitReport;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* THE WHOLE PASS, host-free: grow the viewBox for text a few units past the frame (planFrameFit),
|
|
62
|
+
* then fit the labels it deferred (fitDeferredLabels). Null when there is nothing to measure (no
|
|
63
|
+
* svg, no text, no CTM, an unreadable viewBox). A host adds its own logging around this call; it
|
|
64
|
+
* is kept DOM-only so a headless replay can run exactly what a host runs.
|
|
65
|
+
*/
|
|
66
|
+
export declare function fitTextToFrame(host: HTMLElement): FrameFitOutcome | null;
|
|
67
|
+
/** True when the element sits under a clip-path/mask, inside <defs>, or inside a scrollable
|
|
68
|
+
* subtree - all cases where being outside the frame is the chart's intent rather than a
|
|
69
|
+
* text-metric miss. */
|
|
70
|
+
export declare function isClippedOrScrolled(el: Element, stopAt: Element): boolean;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MIN_OVERLAP, MAX_GUTTER_FRACTION, COLORBAR_ON_PLOT_COVER, MAX_FIT_FRACTION, FIT_PAD, LEGIBILITY_GRACE_PX, MIN_LEGIBLE_PX, BOTTOM_TEXT_MIN_TICK_HITS, SWATCH_MAX_PX, SWATCH_LABEL_GAP_PX, SIGNATURE_MIN_PAIRS, AXIS_THIN_MIN_TICKS, AXIS_THIN_GAP, LABEL_ROW_MIN_TEXTS, LABEL_ROW_Y_TOLERANCE, LABEL_ROW_TRACK_COVER, LABEL_ROW_TRACK_REACH, rightmostOverlappingPlotEdge, bottommostOverlappingPlotEdge, planLegendReconcile, colorbarProbePoints, colorbarOnPlot, colorbarSitsOnMarks, colorbarReconcileSide, planContentFit, planBottomTextPush, swatchPairs, isLegendSignature, planAxisTickThin, isNumericLabelText, isDateLikeLabelText, isOrderedAxisLabelText, axisLabelsAreNominal, nominalLabelRooms, groupLabelRows, labelRowHasTrack, type Rect, type ViewBox, type ReconcileSide, type ReconcilePlan, type ReconcileOptions, type ContentFitPlan, type ContentFitOptions, type SwatchPairs, type AxisThinPlan, } from "./legendReconcile";
|
|
2
|
+
export { FRAME_PAD, MAX_EXTEND_FRACTION, FIT_TYPE_FLOOR, FIT_SHRINK_FLOOR, planFrameFit, viewBoxAttr, labelRoom, labelFitDecision, cutToFit, type FrameFitPlan, type TextAnchor, type LabelFitDecision, } from "./frameFit";
|
|
3
|
+
export { reconcileLegendInSvg, findSignatureLegendGroups, findColorbarFurniture, smallestRenderedTextPx, styleDeclaresScroll, type LegendReconcileResult, type AxisThinReport, } from "./legendReconcileDom";
|
|
4
|
+
export { FRAME_FIT_TEXT_CAP, fitTextToFrame, fitDeferredLabels, fitLabelToRoom, isClippedOrScrolled, type FrameFitOutcome, type DeferredLabelFitReport, } from "./frameFitDom";
|
|
5
|
+
export { createLegendFitPass, type LegendFitPassOptions, type LegendFitReport, type LegendFitStrictness } from "./pass";
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D3 legend / plot reconciliation — pure geometry.
|
|
3
|
+
*
|
|
4
|
+
* The measured sibling of the label-contrast pass. A generated D3 chart
|
|
5
|
+
* can draw its legend ON TOP of plot content, or with its text running off the
|
|
6
|
+
* right edge, because legend WIDTH is data-dependent — the generator can't reserve
|
|
7
|
+
* the right gutter at author time without measuring rendered text, which only
|
|
8
|
+
* exists after render. So we measure post-render and fix it.
|
|
9
|
+
*
|
|
10
|
+
* Coordinates: EVERYTHING here is in the SVG's USER space (== viewBox units).
|
|
11
|
+
* The caller maps the measured client-px bounding boxes into user space via
|
|
12
|
+
* svg.getScreenCTM().inverse() before calling — that one matrix captures the
|
|
13
|
+
* viewBox, preserveAspectRatio and any container scaling/letterboxing exactly,
|
|
14
|
+
* so this module needs no px↔unit scale factor.
|
|
15
|
+
*
|
|
16
|
+
* The fix is TWO independent quantities (the bug in v1 was conflating them):
|
|
17
|
+
* - SLIDE (legendDx): move the legend right just enough that its LEFT edge
|
|
18
|
+
* clears the rightmost plot content it overlaps, + pad.
|
|
19
|
+
* - EXTEND (ext): widen the viewBox just enough that the slid legend's RIGHT
|
|
20
|
+
* edge sits inside it, + pad. Widening the viewBox makes the SVG scale to
|
|
21
|
+
* fit its container, so nothing is clipped and the plot isn't reflowed.
|
|
22
|
+
* The first version set EXTEND = SLIDE, which left the legend's right edge exactly
|
|
23
|
+
* as clipped as before and made the gutter overlap-sized rather than need-sized
|
|
24
|
+
* (a Sankey's legend text stayed cut off at the viewport edge, with more gutter
|
|
25
|
+
* than necessary).
|
|
26
|
+
*
|
|
27
|
+
* DOM-free: the caller gathers user-space boxes + the viewBox and applies the
|
|
28
|
+
* plan (set newViewBox, translate the legend group by legendDx). Tests feed
|
|
29
|
+
* plain numbers and assert the plan.
|
|
30
|
+
*/
|
|
31
|
+
export interface Rect {
|
|
32
|
+
left: number;
|
|
33
|
+
top: number;
|
|
34
|
+
right: number;
|
|
35
|
+
bottom: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ViewBox {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
w: number;
|
|
41
|
+
h: number;
|
|
42
|
+
}
|
|
43
|
+
export type ReconcileSide = "right" | "bottom";
|
|
44
|
+
export interface ReconcilePlan {
|
|
45
|
+
action: "none" | "reserve";
|
|
46
|
+
legendDx: number;
|
|
47
|
+
legendDy: number;
|
|
48
|
+
ext: number;
|
|
49
|
+
newViewBox: ViewBox;
|
|
50
|
+
reason: string;
|
|
51
|
+
}
|
|
52
|
+
export declare const MIN_OVERLAP = 2;
|
|
53
|
+
export declare const MAX_GUTTER_FRACTION = 0.6;
|
|
54
|
+
export interface ReconcileOptions {
|
|
55
|
+
/** Ignore overlaps at or below this many user units (default MIN_OVERLAP). */
|
|
56
|
+
minOverlap?: number;
|
|
57
|
+
/** Defer if the viewBox extension would exceed this fraction of its width
|
|
58
|
+
* (default MAX_GUTTER_FRACTION). */
|
|
59
|
+
maxGutterFraction?: number;
|
|
60
|
+
/** Gap (user units) left between the legend's LEFT and the plot (default 8). */
|
|
61
|
+
pad?: number;
|
|
62
|
+
/** Gap (user units) left between the legend's RIGHT and the viewBox edge
|
|
63
|
+
* (default 4). Smaller than `pad` so the legend hugs the right edge — this
|
|
64
|
+
* is the lever for "too much gutter to the right of the legend". */
|
|
65
|
+
edgePad?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The rightmost right-edge among plot elements that vertically overlap the
|
|
69
|
+
* legend and do NOT horizontally span it. Per-element (not a union) so a title
|
|
70
|
+
* or axis label that isn't actually beside the legend doesn't count, and a
|
|
71
|
+
* full-width plot background (which spans the legend) is ignored — that's not a
|
|
72
|
+
* collision a right gutter fixes. Returns -Infinity when nothing sits beside it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function rightmostOverlappingPlotEdge(legendBox: Rect, plotRects: Rect[]): number;
|
|
75
|
+
/**
|
|
76
|
+
* The bottom-most bottom-edge among plot elements whose box INTERSECTS the legend
|
|
77
|
+
* (horizontal AND vertical overlap) without fully spanning it. The 90°-rotated
|
|
78
|
+
* analogue of rightmostOverlappingPlotEdge — for a horizontal bottom
|
|
79
|
+
* legend/colorbar overprinted by the x-axis ticks/title. Box-intersection (not a
|
|
80
|
+
* "starts above" test) so a colorbar whose own TITLE sits above its gradient —
|
|
81
|
+
* making the union box taller than the colliders — still detects the x-axis
|
|
82
|
+
* labels that overlap the gradient. Content that clears the legend entirely
|
|
83
|
+
* (a cell row above, a footer below) doesn't intersect and is ignored. Returns
|
|
84
|
+
* -Infinity when nothing intersects.
|
|
85
|
+
*/
|
|
86
|
+
export declare function bottommostOverlappingPlotEdge(legendBox: Rect, plotRects: Rect[]): number;
|
|
87
|
+
/**
|
|
88
|
+
* Decide whether/how to reconcile an overlapping/clipped D3 legend.
|
|
89
|
+
*
|
|
90
|
+
* @param legendBox legend group bbox in USER space
|
|
91
|
+
* @param plotRects per-element plot-content bboxes (marks + non-legend text) in USER space
|
|
92
|
+
* @param viewBox the SVG's current viewBox
|
|
93
|
+
* @param opts strictness thresholds (mode-dependent)
|
|
94
|
+
*/
|
|
95
|
+
export declare function planLegendReconcile(legendBox: Rect | null, plotRects: Rect[] | null, viewBox: ViewBox | null, opts?: ReconcileOptions, side?: ReconcileSide): ReconcilePlan;
|
|
96
|
+
/**
|
|
97
|
+
* WHICH MARGIN A STRUCTURALLY-FOUND COLORBAR BELONGS IN.
|
|
98
|
+
*
|
|
99
|
+
* The colorbar pass was first written for ONE case — a horizontal bar in the bottom margin
|
|
100
|
+
* overprinting the x-axis — so it asked `planLegendReconcile` for side "bottom" and nothing
|
|
101
|
+
* else. A VERTICAL colorbar inset into the plot was therefore detected, measured, refused
|
|
102
|
+
* ("not-horizontal-legend") and shipped covering the marks. A treemap with `margin.right = 4`,
|
|
103
|
+
* a colorbar at `plotW - barW - 52` and a `rgba(255,255,255,0.88)` plate under it is that case
|
|
104
|
+
* exactly — the generated code's own comment reads *"Background plate so legend is readable
|
|
105
|
+
* over treemap tiles"*, so the occlusion was deliberate, and two cells and their labels went
|
|
106
|
+
* behind it.
|
|
107
|
+
*
|
|
108
|
+
* The right-side remedy already existed for a legend that carries `.d3-legend-mark`; a colorbar
|
|
109
|
+
* carries no contract class, which is the only reason it was out of reach. So this picks the
|
|
110
|
+
* side from the bar's own geometry and the rest of the machinery is unchanged.
|
|
111
|
+
*
|
|
112
|
+
* THE TESTS HERE MIRROR THE PLANNER'S OWN GATES, deliberately — a strip wider than 2× its
|
|
113
|
+
* height can only go down, anything else can only go right, and each must already be anchored
|
|
114
|
+
* in the margin it is being pushed into. Returning null where the planner would refuse keeps
|
|
115
|
+
* the two from disagreeing: a caller that guesses gets a "not-…" reason instead of a move, and
|
|
116
|
+
* a decorative gradient panel in the middle of the canvas is nobody's furniture.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* IS THE COLORBAR DRAWN ON TOP OF THE CHART? Measured against the GRADIENT BAR itself,
|
|
120
|
+
* not the group box - and by plain intersection, not by the edge test the right slide uses.
|
|
121
|
+
*
|
|
122
|
+
* BOTH of those choices were forced by a corpus of rendered charts, and each was wrong the
|
|
123
|
+
* first time:
|
|
124
|
+
*
|
|
125
|
+
* - THE GROUP BOX IS AMBIGUOUS. It also holds the ticks, the title and a backing plate, so
|
|
126
|
+
* "marks reach past the group's left edge" reads the same for a colorbar sitting on the
|
|
127
|
+
* plot and for one parked correctly in the gutter with a label hanging left of its bar.
|
|
128
|
+
* Over 401 rendered charts that ambiguity was the whole problem: gating on the group box
|
|
129
|
+
* moved 89 charts, only 37 of which had a mark near the bar at all. The BAR's own box has
|
|
130
|
+
* no such ambiguity - a mark under it means the colorbar was drawn over the chart, full
|
|
131
|
+
* stop. That reading finds 15.
|
|
132
|
+
* - `rightmostOverlappingPlotEdge` SKIPS A MARK THAT SPANS THE BOX, which is right for a
|
|
133
|
+
* full-width backdrop and exactly wrong here: a treemap tile drawn behind the colorbar
|
|
134
|
+
* spans it, and that is the case where the occlusion is TOTAL. It answered "nothing
|
|
135
|
+
* overlaps" on a bar sitting entirely on a tile.
|
|
136
|
+
*
|
|
137
|
+
* A MAJORITY, because a colorbar beside the marks still gets its edge touched. The measured
|
|
138
|
+
* spread of `cover` over the 15 is
|
|
139
|
+
* 1.00 1.00 1.00 0.97 | 0.57 0.49 0.41 | 0.25 0.25 0.13 0.06 0.05 0.03 0.01 0.01
|
|
140
|
+
* - the top four are bars drawn wholly on a mark, and the tail from 0.25 down is the last
|
|
141
|
+
* bar of a chart grazing a bar parked in the gutter, which is the content-fit pass's business
|
|
142
|
+
* and not a reason to re-lay-out a cached chart. 0.5 is the default, not a discovery: it is
|
|
143
|
+
* the one number here that a reader should feel free to move.
|
|
144
|
+
*/
|
|
145
|
+
export declare const COLORBAR_ON_PLOT_COVER = 0.5;
|
|
146
|
+
/**
|
|
147
|
+
* WHERE TO ASK "IS ANYTHING PAINTED HERE?" - an evenly spaced grid strictly inside the
|
|
148
|
+
* bar, inset half a cell so no probe lands on its own edge.
|
|
149
|
+
*
|
|
150
|
+
* A BOX IS NOT INK, and this pass learned it the expensive way. Gating on box
|
|
151
|
+
* intersection alone fired on a density-contour plot whose OUTERMOST contour
|
|
152
|
+
* is a thin diagonal band from the top-left to the bottom-right: its bounding box is
|
|
153
|
+
* 705x664 of a 776x779 canvas and therefore covers the colorbar completely, while its
|
|
154
|
+
* ink comes nowhere near it. A box-only test shipped at 50% precision on the corpus.
|
|
155
|
+
* The remedy is the browser's own answer - isPointInFill / isPointInStroke at these
|
|
156
|
+
* points.
|
|
157
|
+
*/
|
|
158
|
+
export declare function colorbarProbePoints(bar: Rect | null, cols?: number, rows?: number): {
|
|
159
|
+
x: number;
|
|
160
|
+
y: number;
|
|
161
|
+
}[];
|
|
162
|
+
/** A majority of the bar's own area has a mark painted under it. */
|
|
163
|
+
export declare function colorbarOnPlot(inkHits: number, probes: number, minCover?: number): boolean;
|
|
164
|
+
export declare function colorbarSitsOnMarks(bar: Rect | null, marks: Rect[] | null, minCover?: number): boolean;
|
|
165
|
+
export declare function colorbarReconcileSide(bar: Rect | null, viewBox: ViewBox | null): ReconcileSide | null;
|
|
166
|
+
/**
|
|
167
|
+
* CONTENT FIT — the universal "nothing gets cut off" pass (a Sankey used
|
|
168
|
+
* margin.left=20 with text-anchor:end node labels at x0-6, so left labels hung ~80
|
|
169
|
+
* units into NEGATIVE x and were clipped — while the legend mechanism, which only
|
|
170
|
+
* ever measures the legend column and extends the RIGHT edge, applied cleanly.
|
|
171
|
+
* Whack-a-mole by design: every targeted fix watches one edge).
|
|
172
|
+
*
|
|
173
|
+
* This is edge- and chart-agnostic: take the union bbox of ALL rendered
|
|
174
|
+
* content and EXTEND the viewBox on whichever sides content hangs past, + pad.
|
|
175
|
+
* Extend-only — never shrink (intentional margins/whitespace are not ours to
|
|
176
|
+
* reclaim). Runs after any legend slide (it re-measures, so it covers the slid
|
|
177
|
+
* legend's final position too) and also when there is NO legend at all.
|
|
178
|
+
*
|
|
179
|
+
* Outlier defense: a generation may park junk far off-canvas (work elements at
|
|
180
|
+
* x=-9999). A rect is considered only if it intersects the viewBox expanded by
|
|
181
|
+
* maxFitFraction per side; the final extension is also clamped to that
|
|
182
|
+
* fraction. A real hanging label sits just past the edge and is unaffected;
|
|
183
|
+
* parked junk is ignored rather than blowing the canvas up.
|
|
184
|
+
*/
|
|
185
|
+
export interface ContentFitPlan {
|
|
186
|
+
action: "none" | "fit";
|
|
187
|
+
newViewBox: ViewBox;
|
|
188
|
+
reason: string;
|
|
189
|
+
/** user units added per side (0 when that side didn't move) */
|
|
190
|
+
cut: {
|
|
191
|
+
left: number;
|
|
192
|
+
top: number;
|
|
193
|
+
right: number;
|
|
194
|
+
bottom: number;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export declare const MAX_FIT_FRACTION = 0.35;
|
|
198
|
+
export declare const FIT_PAD = 4;
|
|
199
|
+
export declare const LEGIBILITY_GRACE_PX = 1;
|
|
200
|
+
export interface ContentFitOptions {
|
|
201
|
+
/** Ignore per-side overhangs at or below this many user units (default MIN_OVERLAP). */
|
|
202
|
+
minOverhang?: number;
|
|
203
|
+
/** Per-side extension cap as a fraction of the viewBox dimension (default MAX_FIT_FRACTION). */
|
|
204
|
+
maxFitFraction?: number;
|
|
205
|
+
/** Padding (user units) between content and the new viewBox edge (default FIT_PAD). */
|
|
206
|
+
pad?: number;
|
|
207
|
+
/**
|
|
208
|
+
* The smallest text the chart currently paints, in SCREEN px — declared user units
|
|
209
|
+
* already multiplied by the live CTM scale, because that is the number a reader's eye
|
|
210
|
+
* meets. Omitted (or non-finite) disables the legibility test entirely, which is the
|
|
211
|
+
* honest degrade: a chart whose text could not be measured must keep today's behaviour
|
|
212
|
+
* rather than be refused on a guess.
|
|
213
|
+
*/
|
|
214
|
+
smallestTextPx?: number;
|
|
215
|
+
/** The readable floor in px (default MIN_LEGIBLE_PX). */
|
|
216
|
+
minLegiblePx?: number;
|
|
217
|
+
/** How much of the smallest text the fit may spend (default LEGIBILITY_GRACE_PX). */
|
|
218
|
+
legibilityGracePx?: number;
|
|
219
|
+
}
|
|
220
|
+
export declare const MIN_LEGIBLE_PX = 10;
|
|
221
|
+
export declare function planContentFit(contentRects: Rect[] | null, viewBox: ViewBox | null, opts?: ContentFitOptions): ContentFitPlan;
|
|
222
|
+
export declare const BOTTOM_TEXT_MIN_TICK_HITS = 2;
|
|
223
|
+
export declare function planBottomTextPush(candidate: Rect, tickBoxes: Rect[], gap?: number, transform?: string): {
|
|
224
|
+
move: boolean;
|
|
225
|
+
dy: number;
|
|
226
|
+
};
|
|
227
|
+
export declare const SWATCH_MAX_PX = 28;
|
|
228
|
+
export declare const SWATCH_LABEL_GAP_PX = 30;
|
|
229
|
+
export declare const SIGNATURE_MIN_PAIRS = 2;
|
|
230
|
+
export interface SwatchPairs {
|
|
231
|
+
pairs: number;
|
|
232
|
+
sameSize: boolean;
|
|
233
|
+
}
|
|
234
|
+
export declare function swatchPairs(shapes: Rect[], texts: Rect[]): SwatchPairs;
|
|
235
|
+
export declare function isLegendSignature(p: SwatchPairs): boolean;
|
|
236
|
+
export declare const AXIS_THIN_MIN_TICKS = 4;
|
|
237
|
+
export declare const AXIS_THIN_GAP = 2;
|
|
238
|
+
export interface AxisThinPlan {
|
|
239
|
+
stride: number;
|
|
240
|
+
hide: number[];
|
|
241
|
+
reason: string;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Boxes are the tick labels of ONE axis, in any order (they are sorted here by
|
|
245
|
+
* left edge, and `hide` comes back indexed against the INPUT). Returns the
|
|
246
|
+
* smallest stride that clears every adjacent collision.
|
|
247
|
+
*
|
|
248
|
+
* Fail-open in both directions: too few ticks to be worth thinning, and an axis
|
|
249
|
+
* still colliding when only its two ends survive, both hide NOTHING. An ugly
|
|
250
|
+
* axis is recoverable by resizing the tile; an axis stripped to one label is not.
|
|
251
|
+
*/
|
|
252
|
+
export declare function planAxisTickThin(boxes: Rect[], gap?: number, minTicks?: number): AxisThinPlan;
|
|
253
|
+
export declare const LABEL_ROW_MIN_TEXTS = 3;
|
|
254
|
+
export declare const LABEL_ROW_Y_TOLERANCE = 2;
|
|
255
|
+
/**
|
|
256
|
+
* A tick-like label: at least one digit, and nothing but digits and the furniture a number
|
|
257
|
+
* format adds - separators, sign, decimal point, currency, percent, a K/M/B suffix, a
|
|
258
|
+
* time colon. "Jan 2026" and "1 region unmatched" are not numeric; "$1.2M" and "-3.5%" are.
|
|
259
|
+
*/
|
|
260
|
+
export declare function isNumericLabelText(s: string): boolean;
|
|
261
|
+
/**
|
|
262
|
+
* A date-like axis caption: a year (2026, FY2026, "Jan 2026"), a month or weekday name, a quarter,
|
|
263
|
+
* half or week ("Q3", "H1", "W12"), a slashed date ("1/15", "15.01.2026") or a clock time ("6 PM").
|
|
264
|
+
* Deliberately generous: a label read as ordered is thinned as before, so the cost of a wrong
|
|
265
|
+
* "date" is today's behaviour, never worse.
|
|
266
|
+
*/
|
|
267
|
+
export declare function isDateLikeLabelText(s: string): boolean;
|
|
268
|
+
/** Ordered = a number (isNumericLabelText) or a date-like caption (isDateLikeLabelText). */
|
|
269
|
+
export declare function isOrderedAxisLabelText(s: string): boolean;
|
|
270
|
+
/** A NOMINAL axis: at least one non-empty label that is neither a number nor date-like. One name
|
|
271
|
+
* among dates counts - that name has no neighbour to imply it. Empty labels say nothing. */
|
|
272
|
+
export declare function axisLabelsAreNominal(labels: string[]): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* The room each label of a nominal axis may take, in input order: the step to its NARROWER
|
|
275
|
+
* neighbour less `gap`. `anchors` are the labels' anchor x positions (centre for middle, left
|
|
276
|
+
* edge for start, right edge for end - one anchor kind per axis), so the step is the band pitch
|
|
277
|
+
* whatever the labels' widths. Two labels each within that room cannot collide, whichever anchor
|
|
278
|
+
* they share. A lone anchor has no band and no limit.
|
|
279
|
+
*/
|
|
280
|
+
export declare function nominalLabelRooms(anchors: number[], gap?: number): number[];
|
|
281
|
+
/**
|
|
282
|
+
* Group label boxes into horizontal ROWS - same vertical centre within `tol` px. Returns
|
|
283
|
+
* index lists into the input, each sorted by left edge; rows of one are dropped because a
|
|
284
|
+
* lone label has nothing to collide with.
|
|
285
|
+
*/
|
|
286
|
+
export declare function groupLabelRows(boxes: Rect[], tol?: number): number[][];
|
|
287
|
+
export declare const LABEL_ROW_TRACK_COVER = 0.8;
|
|
288
|
+
export declare const LABEL_ROW_TRACK_REACH = 3;
|
|
289
|
+
/**
|
|
290
|
+
* Does a horizontal track run under (or over) this row of labels? `row` is the row's label
|
|
291
|
+
* boxes; `candidates` are the non-mark rect / line / path boxes of the row's own group. The
|
|
292
|
+
* candidates within reach are unioned - a five-swatch bar is five rects - and the union has
|
|
293
|
+
* to cover `cover` of the row's span. Pure, so the geometry is testable without a DOM.
|
|
294
|
+
*/
|
|
295
|
+
export declare function labelRowHasTrack(row: Rect[], candidates: Rect[], cover?: number, reach?: number): boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D3 legend reconcile — DOM adapter.
|
|
3
|
+
*
|
|
4
|
+
* The PURE geometry lives in legendReconcile.ts (unit-tested). This is the
|
|
5
|
+
* browser-side glue that the geometry can't be: find the legend group in a
|
|
6
|
+
* rendered SVG, measure it + the plot content in the SVG's USER space (via
|
|
7
|
+
* getScreenCTM, which captures viewBox + scaling exactly), run the plan, and
|
|
8
|
+
* apply it (widen the viewBox, slide the legend). It needs browser layout
|
|
9
|
+
* (getScreenCTM / DOMPoint), so its tests run on a jsdom layout stand-in — same
|
|
10
|
+
* split as the label-contrast pass's applier and its pure decision. Idempotent within
|
|
11
|
+
* a render via a data attribute; the caller wraps it in try/catch and owns logging.
|
|
12
|
+
*/
|
|
13
|
+
import { ReconcileOptions } from "./legendReconcile";
|
|
14
|
+
export interface LegendReconcileResult {
|
|
15
|
+
applied: boolean;
|
|
16
|
+
reason: string;
|
|
17
|
+
dx?: number;
|
|
18
|
+
ext?: number;
|
|
19
|
+
newW?: number;
|
|
20
|
+
furniture?: number;
|
|
21
|
+
fit?: string;
|
|
22
|
+
cb?: string;
|
|
23
|
+
bt?: string;
|
|
24
|
+
fitDeclined?: string;
|
|
25
|
+
smallestTextPx?: number;
|
|
26
|
+
at?: string;
|
|
27
|
+
axisThin?: AxisThinReport;
|
|
28
|
+
lt?: string;
|
|
29
|
+
labelThin?: {
|
|
30
|
+
rows: number;
|
|
31
|
+
hidden: number;
|
|
32
|
+
stride: number;
|
|
33
|
+
};
|
|
34
|
+
dy?: number;
|
|
35
|
+
source?: "class" | "signature";
|
|
36
|
+
}
|
|
37
|
+
export declare function smallestRenderedTextPx(svg: SVGSVGElement, ctmScale: number): number | null;
|
|
38
|
+
/**
|
|
39
|
+
* SELF-MANAGED SCROLL LAYOUT. A chart that renders its
|
|
40
|
+
* content as HTML inside a <foreignObject> with its OWN inner scroll body — a
|
|
41
|
+
* table-with-embedded scorecard (30 users × 32px rows scrolling inside a ~525px body) —
|
|
42
|
+
* manages its own frame. Its below-the-fold rows are SCROLLED, not clipped. The
|
|
43
|
+
* content-fit pass, which exists to rescue clipped SVG content, reads those off-canvas
|
|
44
|
+
* row <text>/marks as clipped and grows the viewBox (+239 on the bottom in that case);
|
|
45
|
+
* preserveAspectRatio then draws the taller viewBox to fit, letterboxing the chart into
|
|
46
|
+
* huge left/right gutters with everything shrunk (~72%) and the text illegibly small.
|
|
47
|
+
* NONE of the reconcile passes (legend slide / colorbar push / bottom-text / content-fit)
|
|
48
|
+
* apply to a self-scrolling HTML table, so skip the whole thing. Detected STRUCTURALLY (a
|
|
49
|
+
* <foreignObject> containing an element that scrolls its own content), never by chart-type
|
|
50
|
+
* name — any future self-managed layout is covered automatically.
|
|
51
|
+
*/
|
|
52
|
+
export declare function styleDeclaresScroll(inlineStyle: string | null | undefined, overflow?: string, overflowY?: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* AXIS TICK THINNING. A horizontal axis whose own tick labels overlap each
|
|
55
|
+
* other - eleven "Jan 2026" into a 312px plot, or "50K | 60K | 70K" along a
|
|
56
|
+
* numeric axis. The decision (which stride clears it) is planAxisTickThin; this
|
|
57
|
+
* is the DOM half: find each axis, establish it IS horizontal, measure the real
|
|
58
|
+
* text, and hide the labels the plan drops.
|
|
59
|
+
*
|
|
60
|
+
* MEASURED, NOT ESTIMATED, and that is the whole reason this lives client-side.
|
|
61
|
+
* The server exec-gate breadcrumb has to approximate text width as
|
|
62
|
+
* chars * fontSize * 0.6 because jsdom has no getComputedTextLength - good
|
|
63
|
+
* enough to triage on and not good enough to spend a regeneration on. Here the
|
|
64
|
+
* box is the browser's own.
|
|
65
|
+
*
|
|
66
|
+
* Hides the text only. The tick LINE and the gridline stay, so the reader keeps
|
|
67
|
+
* every position they align against and loses only a redundant caption.
|
|
68
|
+
* Conservative and fail-open throughout: fewer than four ticks, any rotated
|
|
69
|
+
* label (rotation is the generation's own answer to density and pairs badly
|
|
70
|
+
* with thinning), a vertical axis, or an axis still colliding when only its two
|
|
71
|
+
* ends survive all leave the axis exactly as it was drawn.
|
|
72
|
+
*/
|
|
73
|
+
/** What the axis pass did, for the caller's log line. cut / shrunk / nominal appear only when a
|
|
74
|
+
* NOMINAL axis was fitted instead of thinned; colliding counts labels still touching a
|
|
75
|
+
* neighbour after the cut kept their first character (a band too narrow for even that). */
|
|
76
|
+
export interface AxisThinReport {
|
|
77
|
+
axes: number;
|
|
78
|
+
hidden: number;
|
|
79
|
+
stride: number;
|
|
80
|
+
cut?: number;
|
|
81
|
+
shrunk?: number;
|
|
82
|
+
nominal?: boolean;
|
|
83
|
+
colliding?: number;
|
|
84
|
+
}
|
|
85
|
+
export declare function reconcileLegendInSvg(svg: SVGSVGElement, opts?: ReconcileOptions): LegendReconcileResult;
|
|
86
|
+
/**
|
|
87
|
+
* LEGENDS THAT NEVER ANNOUNCED THEMSELVES. The
|
|
88
|
+
* maximal mark-free <g> subtrees of the chart, judged by the legend signature in
|
|
89
|
+
* legendReconcile.ts. Walk top-down and stop descending once a group qualifies as
|
|
90
|
+
* mark-free, so an axis is one block and a legend nested inside the plot group is
|
|
91
|
+
* found on the way down. Axes are skipped STRUCTURALLY - d3's axis generator always
|
|
92
|
+
* emits g.tick / path.domain - which is the one exclusion a signature is allowed.
|
|
93
|
+
*/
|
|
94
|
+
export declare function findSignatureLegendGroups(svg: SVGSVGElement): Element[];
|
|
95
|
+
/**
|
|
96
|
+
* THE COLORBAR'S OWN FURNITURE, FOUND STRUCTURALLY. The class-based legend finder
|
|
97
|
+
* above can't see a colorbar — the generator leaves the gradient rect UNCLASSED (and
|
|
98
|
+
* reuses .d3-legend-mark for the clickable AXIS labels, which are COLLIDERS here,
|
|
99
|
+
* not furniture). So: a <rect> filled url(#<gradient>) that isn't a plot mark; its
|
|
100
|
+
* parent <g> is the colorbar group, which is what carries the tick labels, the
|
|
101
|
+
* title and — the case that made this matter — a backing plate.
|
|
102
|
+
*
|
|
103
|
+
* Exported so the corpus replay measures with the SHIPPED detector instead of a
|
|
104
|
+
* copy of it; a replay that re-implements the predicate tests the translation.
|
|
105
|
+
*/
|
|
106
|
+
export declare function findColorbarFurniture(svg: SVGSVGElement): {
|
|
107
|
+
groups: Element[];
|
|
108
|
+
bars: SVGGraphicsElement[];
|
|
109
|
+
};
|