@bicharts/chart-host 0.6.84 → 0.6.85

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.
@@ -0,0 +1,1613 @@
1
+ /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
+ import {
3
+ chartSvgOf
4
+ } from "./chunk-ZGQURBRJ.mjs";
5
+ import {
6
+ FIT_CONTENT_SELECTOR,
7
+ ctmScaleOf,
8
+ isPhantomBox
9
+ } from "./chunk-BQFG5HWZ.mjs";
10
+ import "./chunk-MR6TL7QZ.mjs";
11
+ import "./chunk-GHODWOAL.mjs";
12
+
13
+ // src/legendFit/legendReconcile.ts
14
+ var MIN_OVERLAP = 2;
15
+ var MAX_GUTTER_FRACTION = 0.6;
16
+ function overlap1D(aLo, aHi, bLo, bHi) {
17
+ return Math.min(aHi, bHi) - Math.max(aLo, bLo);
18
+ }
19
+ function rightmostOverlappingPlotEdge(legendBox, plotRects) {
20
+ let edge = -Infinity;
21
+ for (const r of plotRects) {
22
+ if (!r) continue;
23
+ if (overlap1D(legendBox.top, legendBox.bottom, r.top, r.bottom) <= 0) continue;
24
+ if (r.left <= legendBox.left && r.right >= legendBox.right) continue;
25
+ if (r.right > edge) edge = r.right;
26
+ }
27
+ return edge;
28
+ }
29
+ function bottommostOverlappingPlotEdge(legendBox, plotRects) {
30
+ let edge = -Infinity;
31
+ for (const r of plotRects) {
32
+ if (!r) continue;
33
+ if (overlap1D(legendBox.left, legendBox.right, r.left, r.right) <= 0) continue;
34
+ if (overlap1D(legendBox.top, legendBox.bottom, r.top, r.bottom) <= 0) continue;
35
+ if (r.left <= legendBox.left && r.right >= legendBox.right && r.top <= legendBox.top && r.bottom >= legendBox.bottom) continue;
36
+ if (r.bottom > edge) edge = r.bottom;
37
+ }
38
+ return edge;
39
+ }
40
+ function planLegendReconcile(legendBox, plotRects, viewBox, opts = {}, side = "right") {
41
+ const minOverlap = opts.minOverlap ?? MIN_OVERLAP;
42
+ const maxGutterFraction = opts.maxGutterFraction ?? MAX_GUTTER_FRACTION;
43
+ const pad = opts.pad ?? 8;
44
+ const edgePad = opts.edgePad ?? 4;
45
+ const none = (reason, vb) => ({
46
+ action: "none",
47
+ legendDx: 0,
48
+ legendDy: 0,
49
+ ext: 0,
50
+ newViewBox: vb || { x: 0, y: 0, w: 0, h: 0 },
51
+ reason
52
+ });
53
+ if (!legendBox || !plotRects || plotRects.length === 0) return none("missing-box", viewBox);
54
+ if (!viewBox || viewBox.w <= 0 || viewBox.h <= 0) return none("no-viewbox", viewBox);
55
+ const legW = legendBox.right - legendBox.left;
56
+ const legH = legendBox.bottom - legendBox.top;
57
+ if (side === "bottom") {
58
+ const plotBottom = bottommostOverlappingPlotEdge(legendBox, plotRects);
59
+ if (plotBottom === -Infinity) return none("no-overlap", viewBox);
60
+ const overlap2 = plotBottom - legendBox.top;
61
+ if (overlap2 <= minOverlap) return none("no-overlap", viewBox);
62
+ if (legW <= legH * 2) return none("not-horizontal-legend", viewBox);
63
+ if (legendBox.top < viewBox.y + 0.55 * viewBox.h) return none("not-bottom-legend", viewBox);
64
+ const legendDy = plotBottom + pad - legendBox.top;
65
+ const viewBoxBottom = viewBox.y + viewBox.h;
66
+ const extB = Math.max(0, Math.ceil(legendBox.bottom + legendDy + edgePad - viewBoxBottom));
67
+ if (extB > viewBox.h * maxGutterFraction) return none("none-too-big", viewBox);
68
+ return {
69
+ action: "reserve",
70
+ legendDx: 0,
71
+ legendDy: Math.ceil(legendDy),
72
+ ext: extB,
73
+ newViewBox: { x: viewBox.x, y: viewBox.y, w: viewBox.w, h: viewBox.h + extB },
74
+ reason: "reserve"
75
+ };
76
+ }
77
+ const plotRight = rightmostOverlappingPlotEdge(legendBox, plotRects);
78
+ if (plotRight === -Infinity) return none("no-overlap", viewBox);
79
+ const overlap = plotRight - legendBox.left;
80
+ if (overlap <= minOverlap) return none("no-overlap", viewBox);
81
+ if (legW > legH * 2) return none("not-vertical-legend", viewBox);
82
+ if (legendBox.left < viewBox.x + 0.55 * viewBox.w) return none("not-right-legend", viewBox);
83
+ const legendDx = plotRight + pad - legendBox.left;
84
+ const viewBoxRight = viewBox.x + viewBox.w;
85
+ const ext = Math.max(0, Math.ceil(legendBox.right + legendDx + edgePad - viewBoxRight));
86
+ if (ext > viewBox.w * maxGutterFraction) return none("none-too-big", viewBox);
87
+ return {
88
+ action: "reserve",
89
+ legendDx: Math.ceil(legendDx),
90
+ legendDy: 0,
91
+ ext,
92
+ newViewBox: { x: viewBox.x, y: viewBox.y, w: viewBox.w + ext, h: viewBox.h },
93
+ reason: "reserve"
94
+ };
95
+ }
96
+ var COLORBAR_ON_PLOT_COVER = 0.5;
97
+ function colorbarProbePoints(bar, cols = 3, rows = 5) {
98
+ if (!bar || !(cols > 0) || !(rows > 0)) return [];
99
+ const w = bar.right - bar.left, h = bar.bottom - bar.top;
100
+ if (!(w > 0) || !(h > 0)) return [];
101
+ const pts = [];
102
+ for (let i = 0; i < cols; i++) {
103
+ for (let j = 0; j < rows; j++) {
104
+ pts.push({ x: bar.left + w * (i + 0.5) / cols, y: bar.top + h * (j + 0.5) / rows });
105
+ }
106
+ }
107
+ return pts;
108
+ }
109
+ function colorbarOnPlot(inkHits, probes, minCover = COLORBAR_ON_PLOT_COVER) {
110
+ return probes > 0 && inkHits / probes >= minCover;
111
+ }
112
+ function colorbarSitsOnMarks(bar, marks, minCover = COLORBAR_ON_PLOT_COVER) {
113
+ if (!bar || !marks || marks.length === 0) return false;
114
+ const barArea = (bar.right - bar.left) * (bar.bottom - bar.top);
115
+ if (!(barArea > 0)) return false;
116
+ for (const m of marks) {
117
+ if (!m) continue;
118
+ const ox = Math.min(bar.right, m.right) - Math.max(bar.left, m.left);
119
+ const oy = Math.min(bar.bottom, m.bottom) - Math.max(bar.top, m.top);
120
+ if (ox <= MIN_OVERLAP || oy <= MIN_OVERLAP) continue;
121
+ if (ox * oy / barArea >= minCover) return true;
122
+ }
123
+ return false;
124
+ }
125
+ function colorbarReconcileSide(bar, viewBox) {
126
+ if (!bar || !viewBox || viewBox.w <= 0 || viewBox.h <= 0) return null;
127
+ const w = bar.right - bar.left;
128
+ const h = bar.bottom - bar.top;
129
+ if (!(w > 0) || !(h > 0)) return null;
130
+ if (w > h * 2) {
131
+ return bar.top >= viewBox.y + 0.55 * viewBox.h ? "bottom" : null;
132
+ }
133
+ return bar.left >= viewBox.x + 0.55 * viewBox.w ? "right" : null;
134
+ }
135
+ var MAX_FIT_FRACTION = 0.35;
136
+ var FIT_PAD = 4;
137
+ var LEGIBILITY_GRACE_PX = 1;
138
+ var MIN_LEGIBLE_PX = 10;
139
+ function planContentFit(contentRects, viewBox, opts = {}) {
140
+ const zero = { left: 0, top: 0, right: 0, bottom: 0 };
141
+ const none = (reason) => ({
142
+ action: "none",
143
+ newViewBox: viewBox || { x: 0, y: 0, w: 0, h: 0 },
144
+ reason,
145
+ cut: zero
146
+ });
147
+ if (!viewBox || viewBox.w <= 0 || viewBox.h <= 0) return none("no-viewbox");
148
+ if (!contentRects || contentRects.length === 0) return none("no-content");
149
+ const minOverhang = opts.minOverhang ?? MIN_OVERLAP;
150
+ const maxFit = opts.maxFitFraction ?? MAX_FIT_FRACTION;
151
+ const pad = opts.pad ?? FIT_PAD;
152
+ const capX = maxFit * viewBox.w;
153
+ const capY = maxFit * viewBox.h;
154
+ const vbR = viewBox.x + viewBox.w;
155
+ const vbB = viewBox.y + viewBox.h;
156
+ let uL = Infinity, uT = Infinity, uR = -Infinity, uB = -Infinity;
157
+ for (const r of contentRects) {
158
+ if (!r || r.right <= r.left || r.bottom <= r.top) continue;
159
+ if (r.right < viewBox.x - capX || r.left > vbR + capX) continue;
160
+ if (r.bottom < viewBox.y - capY || r.top > vbB + capY) continue;
161
+ if (r.left < uL) uL = r.left;
162
+ if (r.top < uT) uT = r.top;
163
+ if (r.right > uR) uR = r.right;
164
+ if (r.bottom > uB) uB = r.bottom;
165
+ }
166
+ if (uL === Infinity) return none("no-content");
167
+ const overL = Math.max(0, viewBox.x - uL);
168
+ const overT = Math.max(0, viewBox.y - uT);
169
+ const overR = Math.max(0, uR - vbR);
170
+ const overB = Math.max(0, uB - vbB);
171
+ let clamped = 0;
172
+ const ext = (over, cap) => {
173
+ if (over <= minOverhang) return 0;
174
+ const need = Math.ceil(over + pad);
175
+ if (need > Math.ceil(cap)) {
176
+ clamped++;
177
+ return 0;
178
+ }
179
+ return need;
180
+ };
181
+ const cut = {
182
+ left: ext(overL, capX),
183
+ top: ext(overT, capY),
184
+ right: ext(overR, capX),
185
+ bottom: ext(overB, capY)
186
+ };
187
+ if (cut.left === 0 && cut.top === 0 && cut.right === 0 && cut.bottom === 0) {
188
+ return none(clamped > 0 ? "clamped" : "fits");
189
+ }
190
+ const shrink = Math.min(
191
+ viewBox.w / (viewBox.w + cut.left + cut.right),
192
+ viewBox.h / (viewBox.h + cut.top + cut.bottom)
193
+ );
194
+ const smallest = opts.smallestTextPx;
195
+ if (typeof smallest === "number" && isFinite(smallest) && smallest > 0) {
196
+ const floorPx = opts.minLegiblePx ?? MIN_LEGIBLE_PX;
197
+ const grace = opts.legibilityGracePx ?? LEGIBILITY_GRACE_PX;
198
+ const allowed = Math.min(floorPx, smallest) - grace;
199
+ if (smallest * shrink < allowed) return none("legibility");
200
+ }
201
+ return {
202
+ action: "fit",
203
+ newViewBox: {
204
+ x: viewBox.x - cut.left,
205
+ y: viewBox.y - cut.top,
206
+ w: viewBox.w + cut.left + cut.right,
207
+ h: viewBox.h + cut.top + cut.bottom
208
+ },
209
+ reason: "fit",
210
+ cut
211
+ };
212
+ }
213
+ var BOTTOM_TEXT_MIN_TICK_HITS = 2;
214
+ function planBottomTextPush(candidate, tickBoxes, gap = 6, transform = "") {
215
+ if (tickBoxes.length === 0) return { move: false, dy: 0 };
216
+ if (!(candidate.right - candidate.left > candidate.bottom - candidate.top)) return { move: false, dy: 0 };
217
+ if (/rotate\s*\(/i.test(transform)) return { move: false, dy: 0 };
218
+ let hits = 0;
219
+ let floor = -Infinity;
220
+ for (const t of tickBoxes) {
221
+ if (candidate.left < t.right && candidate.right > t.left && candidate.top < t.bottom && candidate.bottom > t.top) {
222
+ hits++;
223
+ if (t.bottom > floor) floor = t.bottom;
224
+ }
225
+ }
226
+ if (hits < BOTTOM_TEXT_MIN_TICK_HITS) return { move: false, dy: 0 };
227
+ const dy = floor + gap - candidate.top;
228
+ return dy > 0.5 ? { move: true, dy } : { move: false, dy: 0 };
229
+ }
230
+ var SWATCH_MAX_PX = 28;
231
+ var SWATCH_LABEL_GAP_PX = 30;
232
+ var SIGNATURE_MIN_PAIRS = 2;
233
+ function swatchPairs(shapes, texts) {
234
+ let pairs = 0;
235
+ const sizes = /* @__PURE__ */ new Set();
236
+ for (const s of shapes) {
237
+ const w = s.right - s.left, h = s.bottom - s.top;
238
+ if (!(w > 0 || h > 0)) continue;
239
+ if (w > SWATCH_MAX_PX || h > SWATCH_MAX_PX) continue;
240
+ const beside = texts.some((t) => t.left >= s.right - 2 && t.left - s.right <= SWATCH_LABEL_GAP_PX && Math.min(t.bottom, s.bottom) - Math.max(t.top, s.top) > 0);
241
+ if (!beside) continue;
242
+ pairs++;
243
+ sizes.add(Math.round(w) + "x" + Math.round(h));
244
+ }
245
+ return { pairs, sameSize: pairs > 1 && sizes.size === 1 };
246
+ }
247
+ function isLegendSignature(p) {
248
+ return p.pairs >= SIGNATURE_MIN_PAIRS && p.sameSize;
249
+ }
250
+ var AXIS_THIN_MIN_TICKS = 4;
251
+ var AXIS_THIN_GAP = 2;
252
+ function planAxisTickThin(boxes, gap = AXIS_THIN_GAP, minTicks = AXIS_THIN_MIN_TICKS) {
253
+ const n = boxes.length;
254
+ if (n < minTicks) return { stride: 1, hide: [], reason: "too-few" };
255
+ const order = boxes.map((b, i) => ({ b, i })).sort((p, q) => p.b.left - q.b.left);
256
+ const collides = (a, b) => b.left < a.right + gap;
257
+ let any = false;
258
+ for (let i = 1; i < n; i++) {
259
+ if (collides(order[i - 1].b, order[i].b)) {
260
+ any = true;
261
+ break;
262
+ }
263
+ }
264
+ if (!any) return { stride: 1, hide: [], reason: "no-overlap" };
265
+ const cleanAt = (k) => {
266
+ let prev = -1;
267
+ for (let i = 0; i < n; i += k) {
268
+ if (prev >= 0 && collides(order[prev].b, order[i].b)) return false;
269
+ prev = i;
270
+ }
271
+ return true;
272
+ };
273
+ for (let k = 2; k <= Math.floor(n / 2); k++) {
274
+ if (!cleanAt(k)) continue;
275
+ const hide2 = [];
276
+ for (let i = 0; i < n; i++) if (i % k !== 0) hide2.push(order[i].i);
277
+ return { stride: k, hide: hide2.sort((a, b) => a - b), reason: "stride" };
278
+ }
279
+ if (collides(order[0].b, order[n - 1].b)) return { stride: 1, hide: [], reason: "unfixable" };
280
+ const hide = [];
281
+ for (let i = 1; i < n - 1; i++) hide.push(order[i].i);
282
+ return { stride: 0, hide: hide.sort((a, b) => a - b), reason: "ends-only" };
283
+ }
284
+ var LABEL_ROW_MIN_TEXTS = 3;
285
+ var LABEL_ROW_Y_TOLERANCE = 2;
286
+ function isNumericLabelText(s) {
287
+ const t = (s || "").trim();
288
+ if (!t || !/\d/.test(t)) return false;
289
+ return /^[\s$\u20ac\u00a3\u00a5%+\-\u2013\u2212.,0-9kKmMbB:]+$/.test(t);
290
+ }
291
+ var EN_MONTHS = [
292
+ "jan",
293
+ "feb",
294
+ "mar",
295
+ "apr",
296
+ "may",
297
+ "jun",
298
+ "jul",
299
+ "aug",
300
+ "sep",
301
+ "sept",
302
+ "oct",
303
+ "nov",
304
+ "dec",
305
+ "january",
306
+ "february",
307
+ "march",
308
+ "april",
309
+ "june",
310
+ "july",
311
+ "august",
312
+ "september",
313
+ "october",
314
+ "november",
315
+ "december"
316
+ ];
317
+ var EN_DAYS = [
318
+ "mon",
319
+ "tue",
320
+ "tues",
321
+ "wed",
322
+ "thu",
323
+ "thur",
324
+ "thurs",
325
+ "fri",
326
+ "sat",
327
+ "sun",
328
+ "monday",
329
+ "tuesday",
330
+ "wednesday",
331
+ "thursday",
332
+ "friday",
333
+ "saturday",
334
+ "sunday"
335
+ ];
336
+ var calendarWords = null;
337
+ function calendarWordSet() {
338
+ if (calendarWords) return calendarWords;
339
+ const set = /* @__PURE__ */ new Set([...EN_MONTHS, ...EN_DAYS]);
340
+ try {
341
+ const add = (s) => {
342
+ const w = s.toLowerCase().replace(/\.$/, "").trim();
343
+ if (w) set.add(w);
344
+ };
345
+ for (const month of ["short", "long"]) {
346
+ const f = new Intl.DateTimeFormat(void 0, { month });
347
+ for (let m = 0; m < 12; m++) add(f.format(new Date(2026, m, 15)));
348
+ }
349
+ for (const weekday of ["short", "long"]) {
350
+ const f = new Intl.DateTimeFormat(void 0, { weekday });
351
+ for (let d = 0; d < 7; d++) add(f.format(new Date(2026, 0, 4 + d)));
352
+ }
353
+ } catch {
354
+ }
355
+ calendarWords = set;
356
+ return set;
357
+ }
358
+ function isDateLikeLabelText(s) {
359
+ const t = (s || "").trim();
360
+ if (!t) return false;
361
+ if (/(^|\D)(1[89]|20)\d{2}(\D|$)/.test(t)) return true;
362
+ if (/^(Q|H|T|W|FY|CW|KW)\s?\d{1,2}$/i.test(t) || /\bQ[1-4]\b/.test(t)) return true;
363
+ if (/^\d{1,4}[\/.\-]\d{1,2}([\/.\-]\d{1,4})?$/.test(t)) return true;
364
+ if (/^\d{1,2}(:\d{2})?\s*(am|pm|a\.m\.|p\.m\.)$/i.test(t)) return true;
365
+ const words = t.toLowerCase().split(/[\s,.'\-\/]+/).filter((w) => w !== "");
366
+ const cal = calendarWordSet();
367
+ return words.length > 0 && words.length <= 3 && words.some((w) => cal.has(w)) && words.every((w) => cal.has(w) || /^\d{1,4}(st|nd|rd|th)?$/.test(w));
368
+ }
369
+ function isOrderedAxisLabelText(s) {
370
+ return isNumericLabelText(s) || isDateLikeLabelText(s);
371
+ }
372
+ function axisLabelsAreNominal(labels) {
373
+ return labels.some((s) => (s || "").trim() !== "" && !isOrderedAxisLabelText(s));
374
+ }
375
+ function nominalLabelRooms(anchors, gap = AXIS_THIN_GAP) {
376
+ const n = anchors.length;
377
+ if (n < 2) return anchors.map(() => Infinity);
378
+ const order = anchors.map((x, i) => ({ x, i })).sort((p, q) => p.x - q.x);
379
+ const out = new Array(n);
380
+ for (let k = 0; k < n; k++) {
381
+ const left = k > 0 ? order[k].x - order[k - 1].x : Infinity;
382
+ const right = k < n - 1 ? order[k + 1].x - order[k].x : Infinity;
383
+ out[order[k].i] = Math.min(left, right) - gap;
384
+ }
385
+ return out;
386
+ }
387
+ function groupLabelRows(boxes, tol = LABEL_ROW_Y_TOLERANCE) {
388
+ const used = new Array(boxes.length).fill(false);
389
+ const rows = [];
390
+ for (let i = 0; i < boxes.length; i++) {
391
+ if (used[i]) continue;
392
+ const cy = (boxes[i].top + boxes[i].bottom) / 2;
393
+ const row = [i];
394
+ used[i] = true;
395
+ for (let j = i + 1; j < boxes.length; j++) {
396
+ if (used[j]) continue;
397
+ const cj = (boxes[j].top + boxes[j].bottom) / 2;
398
+ if (Math.abs(cj - cy) <= tol) {
399
+ row.push(j);
400
+ used[j] = true;
401
+ }
402
+ }
403
+ if (row.length >= 2) rows.push(row.sort((a, b) => boxes[a].left - boxes[b].left));
404
+ }
405
+ return rows;
406
+ }
407
+ var LABEL_ROW_TRACK_COVER = 0.8;
408
+ var LABEL_ROW_TRACK_REACH = 3;
409
+ function labelRowHasTrack(row, candidates, cover = LABEL_ROW_TRACK_COVER, reach = LABEL_ROW_TRACK_REACH) {
410
+ if (row.length === 0 || candidates.length === 0) return false;
411
+ let l = Infinity, r = -Infinity, cy = 0, fs = 0;
412
+ for (const b of row) {
413
+ if (b.left < l) l = b.left;
414
+ if (b.right > r) r = b.right;
415
+ cy += (b.top + b.bottom) / 2;
416
+ fs += b.bottom - b.top;
417
+ }
418
+ cy /= row.length;
419
+ fs /= row.length;
420
+ if (!(r > l) || !(fs > 0)) return false;
421
+ let tl = Infinity, tr = -Infinity, any = false;
422
+ for (const c of candidates) {
423
+ if (!(c.right > c.left)) continue;
424
+ if (Math.abs((c.top + c.bottom) / 2 - cy) > reach * fs) continue;
425
+ any = true;
426
+ if (c.left < tl) tl = c.left;
427
+ if (c.right > tr) tr = c.right;
428
+ }
429
+ if (!any) return false;
430
+ const overlap = Math.min(r, tr) - Math.max(l, tl);
431
+ return overlap >= cover * (r - l);
432
+ }
433
+
434
+ // src/legendFit/frameFit.ts
435
+ var FRAME_PAD = 1;
436
+ var MAX_EXTEND_FRACTION = 0.08;
437
+ function planFrameFit(boxes, vb, maxFraction = MAX_EXTEND_FRACTION) {
438
+ const deferredIdx = [];
439
+ const rescuedIdx = [];
440
+ const none = (reason, considered, deferred2, idx = deferredIdx) => ({
441
+ action: deferred2 > 0 && considered > 0 ? "defer" : "none",
442
+ top: 0,
443
+ right: 0,
444
+ bottom: 0,
445
+ left: 0,
446
+ newViewBox: vb,
447
+ considered,
448
+ rescued: 0,
449
+ deferred: deferred2,
450
+ deferredIdx: idx.slice().sort((a, b) => a - b),
451
+ reason
452
+ });
453
+ if (!boxes || boxes.length === 0) return none("no-text", 0, 0);
454
+ if (!vb || !(vb.w > 0) || !(vb.h > 0)) return none("bad-viewbox", boxes.length, 0);
455
+ const maxH = vb.w * maxFraction;
456
+ const maxV = vb.h * maxFraction;
457
+ const vbRight = vb.x + vb.w;
458
+ const vbBottom = vb.y + vb.h;
459
+ let top = 0, right = 0, bottom = 0, left = 0;
460
+ let rescued = 0, deferred = 0;
461
+ for (let bi = 0; bi < boxes.length; bi++) {
462
+ const b = boxes[bi];
463
+ if (!b || !isFinite(b.left) || !isFinite(b.top) || !isFinite(b.right) || !isFinite(b.bottom)) {
464
+ continue;
465
+ }
466
+ const needTop = vb.y - b.top + FRAME_PAD;
467
+ const needLeft = vb.x - b.left + FRAME_PAD;
468
+ const needRight = b.right - vbRight + FRAME_PAD;
469
+ const needBottom = b.bottom - vbBottom + FRAME_PAD;
470
+ const worstV = Math.max(needTop, needBottom);
471
+ const worstH = Math.max(needLeft, needRight);
472
+ if (worstV <= 0 && worstH <= 0) continue;
473
+ if (worstV > maxV || worstH > maxH) {
474
+ deferred++;
475
+ deferredIdx.push(bi);
476
+ continue;
477
+ }
478
+ let drove = false;
479
+ if (needTop > 0) {
480
+ top = Math.max(top, needTop);
481
+ drove = true;
482
+ }
483
+ if (needLeft > 0) {
484
+ left = Math.max(left, needLeft);
485
+ drove = true;
486
+ }
487
+ if (needRight > 0) {
488
+ right = Math.max(right, needRight);
489
+ drove = true;
490
+ }
491
+ if (needBottom > 0) {
492
+ bottom = Math.max(bottom, needBottom);
493
+ drove = true;
494
+ }
495
+ if (drove) {
496
+ rescued++;
497
+ rescuedIdx.push(bi);
498
+ }
499
+ }
500
+ if (rescued === 0) {
501
+ return none(deferred > 0 ? "all-overflow-structural" : "all-inside", boxes.length, deferred);
502
+ }
503
+ if (top + bottom > maxV || left + right > maxH) {
504
+ return none("accumulated-overflow-structural", boxes.length, deferred + rescued, deferredIdx.concat(rescuedIdx));
505
+ }
506
+ const parts = [];
507
+ if (top > 0) parts.push("top+" + round2(top));
508
+ if (right > 0) parts.push("right+" + round2(right));
509
+ if (bottom > 0) parts.push("bottom+" + round2(bottom));
510
+ if (left > 0) parts.push("left+" + round2(left));
511
+ return {
512
+ action: "extend",
513
+ top: round2(top),
514
+ right: round2(right),
515
+ bottom: round2(bottom),
516
+ left: round2(left),
517
+ newViewBox: {
518
+ x: round2(vb.x - left),
519
+ y: round2(vb.y - top),
520
+ w: round2(vb.w + left + right),
521
+ h: round2(vb.h + top + bottom)
522
+ },
523
+ considered: boxes.length,
524
+ rescued,
525
+ deferred,
526
+ deferredIdx: deferredIdx.slice(),
527
+ reason: parts.join(",")
528
+ };
529
+ }
530
+ function round2(n) {
531
+ return Math.round(n * 100) / 100;
532
+ }
533
+ function viewBoxAttr(vb) {
534
+ return `${vb.x} ${vb.y} ${vb.w} ${vb.h}`;
535
+ }
536
+ var FIT_TYPE_FLOOR = 10;
537
+ var FIT_SHRINK_FLOOR = 0.8;
538
+ function labelRoom(box, frame, anchor) {
539
+ if (!box || !frame || ![box.left, box.right, box.top, box.bottom].every(isFinite)) return null;
540
+ const fl = frame.x + FRAME_PAD, fr = frame.x + frame.w - FRAME_PAD;
541
+ const ft = frame.y, fb = frame.y + frame.h;
542
+ if (box.top < ft - FRAME_PAD || box.bottom > fb + FRAME_PAD) return null;
543
+ let room;
544
+ if (anchor === "end") {
545
+ if (box.right > fr) return null;
546
+ room = box.right - fl;
547
+ } else if (anchor === "start") {
548
+ if (box.left < fl) return null;
549
+ room = fr - box.left;
550
+ } else {
551
+ const c = (box.left + box.right) / 2;
552
+ if (c < fl || c > fr) return null;
553
+ room = 2 * Math.min(c - fl, fr - c);
554
+ }
555
+ return room > 0 ? round2(room) : null;
556
+ }
557
+ function labelFitDecision(width, room, fontSize) {
558
+ if (!(width > 0) || !(room > 0) || !(fontSize > 0) || width <= room) return { action: "none", fontSize };
559
+ const floor = Math.min(fontSize, Math.max(FIT_TYPE_FLOOR, fontSize * FIT_SHRINK_FLOOR));
560
+ const want = Math.floor(fontSize * room / width * 2) / 2;
561
+ if (want >= floor && want < fontSize) return { action: "shrink", fontSize: want };
562
+ return { action: "cut", fontSize };
563
+ }
564
+ function cutToFit(full, fits, ell = "\u2026") {
565
+ if (fits(full)) return full;
566
+ let lo = 0, hi = full.length;
567
+ while (lo < hi) {
568
+ const mid = lo + hi + 1 >> 1;
569
+ if (fits(full.slice(0, mid) + ell)) lo = mid;
570
+ else hi = mid - 1;
571
+ }
572
+ const digit = (i) => i >= 0 && i < full.length && /[0-9]/.test(full.charAt(i));
573
+ const sep = (i) => i >= 0 && i < full.length && /[.,]/.test(full.charAt(i));
574
+ const splits = digit(lo - 1) ? digit(lo) || sep(lo) && digit(lo + 1) : sep(lo - 1) && digit(lo - 2) && digit(lo);
575
+ if (lo > 0 && splits) {
576
+ while (lo > 0 && (digit(lo - 1) || sep(lo - 1) && digit(lo - 2))) lo--;
577
+ while (lo > 0 && /[\s+\-$]/.test(full.charAt(lo - 1))) lo--;
578
+ }
579
+ while (lo > 0 && /\s/.test(full.charAt(lo - 1))) lo--;
580
+ return lo > 0 ? full.slice(0, lo) + ell : "";
581
+ }
582
+
583
+ // src/legendFit/frameFitDom.ts
584
+ function presentation(el, attr, cssProp, computed) {
585
+ const own = (e) => (e.getAttribute && e.getAttribute(attr) || e.style && e.style[cssProp] || "").trim();
586
+ let v = own(el);
587
+ if (!v) {
588
+ try {
589
+ if (typeof getComputedStyle === "function") v = (computed(getComputedStyle(el)) || "").trim();
590
+ } catch {
591
+ }
592
+ }
593
+ for (let n = el.parentElement; !v && n; n = n.parentElement) v = own(n);
594
+ return v;
595
+ }
596
+ function anchorOf(el) {
597
+ const v = presentation(el, "text-anchor", "textAnchor", (cs) => cs.getPropertyValue("text-anchor"));
598
+ return v === "end" ? "end" : v === "middle" ? "middle" : "start";
599
+ }
600
+ function fontSizeOf(el) {
601
+ let raw = presentation(el, "font-size", "fontSize", (cs) => cs.fontSize);
602
+ if (/(em|%)\s*$/.test(raw)) {
603
+ try {
604
+ if (typeof getComputedStyle === "function") raw = getComputedStyle(el).fontSize || raw;
605
+ } catch {
606
+ }
607
+ }
608
+ const v = parseFloat(raw);
609
+ return v > 0 && !/(em|%)\s*$/.test(raw) ? v : 10;
610
+ }
611
+ function ownText(el) {
612
+ let s = "";
613
+ el.childNodes.forEach((n) => {
614
+ if (n.nodeType === 3) s += n.nodeValue || "";
615
+ });
616
+ return s;
617
+ }
618
+ function setOwnText(el, s) {
619
+ const texts = [];
620
+ el.childNodes.forEach((n) => {
621
+ if (n.nodeType === 3) texts.push(n);
622
+ });
623
+ if (texts.length === 0) {
624
+ el.insertBefore(el.ownerDocument.createTextNode(s), el.firstChild);
625
+ return;
626
+ }
627
+ texts[0].nodeValue = s;
628
+ for (let i = 1; i < texts.length; i++) texts[i].nodeValue = "";
629
+ }
630
+ function rotated(el) {
631
+ try {
632
+ const m = el.getCTM ? el.getCTM() : null;
633
+ return !!m && (Math.abs(m.b) > 1e-6 || Math.abs(m.c) > 1e-6);
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ function fitLabelToRoom(el, width, room, widthNow, minChars = 0) {
639
+ const full = ownText(el);
640
+ const d = labelFitDecision(width, room, fontSizeOf(el));
641
+ if (d.action === "none") return "none";
642
+ if (d.action === "shrink") {
643
+ el.setAttribute("font-size", String(d.fontSize));
644
+ if (widthNow() <= room + 0.5) return "shrunk";
645
+ }
646
+ let fitted = cutToFit(full, (s) => {
647
+ setOwnText(el, s);
648
+ return widthNow() <= room + 0.5;
649
+ });
650
+ const keep = full.trim();
651
+ if (minChars > 0 && fitted.length - 1 < minChars && keep.length > minChars) {
652
+ fitted = Array.from(keep).slice(0, minChars).join("") + "\u2026";
653
+ }
654
+ setOwnText(el, fitted);
655
+ if (!Array.from(el.children).some((c) => c.tagName.toLowerCase() === "title")) {
656
+ const t = el.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "title");
657
+ t.textContent = full;
658
+ el.appendChild(t);
659
+ }
660
+ return "cut";
661
+ }
662
+ function fitDeferredLabels(els, frame, measure) {
663
+ const rep = { deferred: els.length, shrunk: 0, cut: 0, unfixable: 0, labels: [] };
664
+ for (const el of els) {
665
+ try {
666
+ const kids = Array.from(el.children).filter((c) => c.tagName.toLowerCase() !== "title");
667
+ if (kids.length > 0 || rotated(el)) {
668
+ rep.unfixable++;
669
+ continue;
670
+ }
671
+ const box = measure(el);
672
+ if (!box) {
673
+ rep.unfixable++;
674
+ continue;
675
+ }
676
+ const room = labelRoom(box, frame, anchorOf(el));
677
+ if (room == null) {
678
+ rep.unfixable++;
679
+ continue;
680
+ }
681
+ const full = ownText(el);
682
+ if (!full.trim()) continue;
683
+ const done = fitLabelToRoom(el, box.right - box.left, room, () => {
684
+ const b = measure(el);
685
+ return b ? b.right - b.left : Infinity;
686
+ });
687
+ if (done === "none") continue;
688
+ if (done === "shrunk") rep.shrunk++;
689
+ else rep.cut++;
690
+ if (rep.labels.length < 6) rep.labels.push(full.slice(0, 40));
691
+ } catch {
692
+ rep.unfixable++;
693
+ }
694
+ }
695
+ return rep;
696
+ }
697
+ var FRAME_FIT_TEXT_CAP = 600;
698
+ function fitTextToFrame(host) {
699
+ const svg = chartSvgOf(host);
700
+ if (!svg) return null;
701
+ const texts = svg.querySelectorAll("text");
702
+ if (texts.length === 0) return null;
703
+ if (texts.length > FRAME_FIT_TEXT_CAP) return { skipped: { texts: texts.length, cap: FRAME_FIT_TEXT_CAP } };
704
+ const userMapper = () => {
705
+ const ctm = svg.getScreenCTM();
706
+ if (!ctm) return null;
707
+ const inv = ctm.inverse();
708
+ return (r) => {
709
+ const map = (x, y) => {
710
+ const p = svg.createSVGPoint();
711
+ p.x = x;
712
+ p.y = y;
713
+ return p.matrixTransform(inv);
714
+ };
715
+ const tl = map(r.left, r.top), br = map(r.right, r.bottom);
716
+ return { left: Math.min(tl.x, br.x), top: Math.min(tl.y, br.y), right: Math.max(tl.x, br.x), bottom: Math.max(tl.y, br.y) };
717
+ };
718
+ };
719
+ const toUser = userMapper();
720
+ if (!toUser) return null;
721
+ const vbAttr = svg.getAttribute("viewBox");
722
+ let vb;
723
+ if (vbAttr) {
724
+ const n = vbAttr.trim().split(/[\s,]+/).map(parseFloat);
725
+ if (n.length !== 4 || n.some((v) => !isFinite(v))) return null;
726
+ vb = { x: n[0], y: n[1], w: n[2], h: n[3] };
727
+ } else {
728
+ const w = parseFloat(svg.getAttribute("width") || "") || host.offsetWidth;
729
+ const h = parseFloat(svg.getAttribute("height") || "") || host.offsetHeight;
730
+ if (!(w > 0) || !(h > 0)) return null;
731
+ vb = { x: 0, y: 0, w, h };
732
+ }
733
+ const boxes = [];
734
+ const els = [];
735
+ for (let i = 0; i < texts.length; i++) {
736
+ const el = texts[i];
737
+ if (isClippedOrScrolled(el, svg)) continue;
738
+ const r = el.getBoundingClientRect();
739
+ if (r.width <= 0 && r.height <= 0) continue;
740
+ boxes.push(toUser(r));
741
+ els.push(el);
742
+ }
743
+ const plan = planFrameFit(boxes, vb);
744
+ const out = { plan };
745
+ let frame = vb;
746
+ if (plan.action === "extend") {
747
+ svg.setAttribute("viewBox", viewBoxAttr(plan.newViewBox));
748
+ if (!svg.getAttribute("preserveAspectRatio")) svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
749
+ frame = plan.newViewBox;
750
+ }
751
+ if (plan.deferredIdx.length > 0) {
752
+ const after = userMapper();
753
+ if (after) {
754
+ out.labels = fitDeferredLabels(plan.deferredIdx.map((i) => els[i]), frame, (el) => {
755
+ const r = el.getBoundingClientRect();
756
+ return r.width > 0 || r.height > 0 ? after(r) : null;
757
+ });
758
+ }
759
+ }
760
+ return out;
761
+ }
762
+ function isClippedOrScrolled(el, stopAt) {
763
+ let n = el;
764
+ while (n && n !== stopAt.parentElement) {
765
+ const tag = n.tagName ? n.tagName.toLowerCase() : "";
766
+ if (tag === "defs" || tag === "clippath" || tag === "mask" || tag === "symbol") return true;
767
+ if (n.getAttribute && (n.getAttribute("clip-path") || n.getAttribute("mask"))) return true;
768
+ if (typeof getComputedStyle === "function") {
769
+ try {
770
+ const cs = getComputedStyle(n);
771
+ if (cs.clipPath && cs.clipPath !== "none") return true;
772
+ if (cs.overflow === "auto" || cs.overflow === "scroll" || cs.overflowY === "auto" || cs.overflowY === "scroll") return true;
773
+ } catch {
774
+ }
775
+ }
776
+ n = n.parentElement;
777
+ }
778
+ return false;
779
+ }
780
+
781
+ // src/legendFit/legendReconcileDom.ts
782
+ function readViewBox(svg) {
783
+ const vbAttr = svg.getAttribute("viewBox");
784
+ if (vbAttr) {
785
+ const p = vbAttr.split(/[\s,]+/).map(Number).filter((n) => isFinite(n));
786
+ if (p.length === 4) return { x: p[0], y: p[1], w: p[2], h: p[3] };
787
+ }
788
+ const svgRect = svg.getBoundingClientRect();
789
+ const w = parseFloat(svg.getAttribute("width") || "") || svgRect.width;
790
+ const h = parseFloat(svg.getAttribute("height") || "") || svgRect.height;
791
+ if (w > 0 && h > 0) return { x: 0, y: 0, w, h };
792
+ return null;
793
+ }
794
+ var FIT_WALK_CAP = 8e3;
795
+ var FONT_SCAN_CAP = 400;
796
+ function smallestRenderedTextPx(svg, ctmScale) {
797
+ if (!(ctmScale > 0) || !isFinite(ctmScale)) return null;
798
+ const texts = svg.querySelectorAll("text");
799
+ let smallest = Infinity;
800
+ const n = Math.min(texts.length, FONT_SCAN_CAP);
801
+ for (let i = 0; i < n; i++) {
802
+ try {
803
+ const el = texts[i];
804
+ const r = el.getBoundingClientRect();
805
+ if (r.width <= 0 && r.height <= 0) continue;
806
+ const px = parseFloat(getComputedStyle(el).fontSize);
807
+ if (isFinite(px) && px > 0 && px < smallest) smallest = px;
808
+ } catch {
809
+ }
810
+ }
811
+ return isFinite(smallest) ? smallest * ctmScale : null;
812
+ }
813
+ function fitSvgToContent(svg) {
814
+ const vbox = readViewBox(svg);
815
+ if (!vbox) return { applied: false };
816
+ const ctm = svg.getScreenCTM();
817
+ if (!ctm) return { applied: false };
818
+ const inv = ctm.inverse();
819
+ const toUser = (r) => {
820
+ const a = new DOMPoint(r.left, r.top).matrixTransform(inv);
821
+ const b = new DOMPoint(r.right, r.bottom).matrixTransform(inv);
822
+ return { left: Math.min(a.x, b.x), right: Math.max(a.x, b.x), top: Math.min(a.y, b.y), bottom: Math.max(a.y, b.y) };
823
+ };
824
+ const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
825
+ const rects = [];
826
+ for (let i = 0; i < els.length && rects.length < FIT_WALK_CAP; i++) {
827
+ const r = els[i].getBoundingClientRect();
828
+ if (r.width <= 0 && r.height <= 0) continue;
829
+ const ur = toUser(r);
830
+ if (isPhantomBox(ur.right - ur.left, ur.bottom - ur.top, vbox.w, vbox.h)) continue;
831
+ rects.push(ur);
832
+ }
833
+ const smallestTextPx = smallestRenderedTextPx(svg, ctmScaleOf(ctm)) ?? void 0;
834
+ const plan = planContentFit(rects, vbox, { smallestTextPx });
835
+ if (plan.action !== "fit") {
836
+ return { applied: false, declined: plan.reason, smallestTextPx };
837
+ }
838
+ svg.setAttribute("viewBox", `${plan.newViewBox.x} ${plan.newViewBox.y} ${plan.newViewBox.w} ${plan.newViewBox.h}`);
839
+ if (!svg.getAttribute("preserveAspectRatio")) svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
840
+ const c = plan.cut;
841
+ return { applied: true, fit: `L${c.left} R${c.right} T${c.top} B${c.bottom}` };
842
+ }
843
+ function styleDeclaresScroll(inlineStyle, overflow, overflowY) {
844
+ if (inlineStyle && /overflow(-[xy])?\s*:\s*(auto|scroll)/i.test(inlineStyle)) return true;
845
+ const v = (s) => s === "auto" || s === "scroll";
846
+ return v(overflow) || v(overflowY);
847
+ }
848
+ function hasSelfManagedScrollLayout(svg) {
849
+ const fos = svg.querySelectorAll("foreignObject");
850
+ if (fos.length === 0) return false;
851
+ const win = svg.ownerDocument && svg.ownerDocument.defaultView;
852
+ const CAP = 2e3;
853
+ let seen = 0;
854
+ for (let i = 0; i < fos.length; i++) {
855
+ const kids = fos[i].querySelectorAll("*");
856
+ for (let j = 0; j < kids.length && seen < CAP; j++, seen++) {
857
+ const el = kids[j];
858
+ const inline = el.getAttribute && el.getAttribute("style") || "";
859
+ let cs = null;
860
+ if (win) {
861
+ try {
862
+ cs = win.getComputedStyle(el);
863
+ } catch {
864
+ cs = null;
865
+ }
866
+ }
867
+ if (styleDeclaresScroll(inline, cs?.overflow, cs?.overflowY)) return true;
868
+ }
869
+ if (seen >= CAP) break;
870
+ }
871
+ return false;
872
+ }
873
+ function thinAxisTicksPhase(svg) {
874
+ const none = { applied: false, axes: 0, hidden: 0, stride: 1 };
875
+ const ticks = svg.querySelectorAll(".tick");
876
+ if (ticks.length < AXIS_THIN_MIN_TICKS) return none;
877
+ const byAxis = /* @__PURE__ */ new Map();
878
+ for (let i = 0; i < ticks.length; i++) {
879
+ const parent = ticks[i].parentElement;
880
+ if (!parent) continue;
881
+ const list = byAxis.get(parent);
882
+ if (list) list.push(ticks[i]);
883
+ else byAxis.set(parent, [ticks[i]]);
884
+ }
885
+ const isRotated = (el) => {
886
+ let n = el;
887
+ while (n && n !== svg) {
888
+ const t = n.getAttribute ? n.getAttribute("transform") : null;
889
+ if (t && t.indexOf("rotate") >= 0) return true;
890
+ n = n.parentElement;
891
+ }
892
+ return false;
893
+ };
894
+ let axesTouched = 0, hiddenTotal = 0, strideUsed = 1, cutTotal = 0, shrunkTotal = 0, nominalAxes = 0, collidingTotal = 0;
895
+ byAxis.forEach((group, axis) => {
896
+ if (group.length < AXIS_THIN_MIN_TICKS) return;
897
+ if (axis.getAttribute("data-lch-ticks-thinned") === "1") return;
898
+ const texts = [];
899
+ const boxes = [];
900
+ let rotated2 = false;
901
+ for (const tick of group) {
902
+ const t = tick.querySelector("text");
903
+ if (!t) continue;
904
+ if (isRotated(t)) {
905
+ rotated2 = true;
906
+ break;
907
+ }
908
+ const r = t.getBoundingClientRect();
909
+ if (r.width <= 0 || r.height <= 0) continue;
910
+ texts.push(t);
911
+ boxes.push({ left: r.left, right: r.right, top: r.top, bottom: r.bottom });
912
+ }
913
+ if (rotated2 || boxes.length < AXIS_THIN_MIN_TICKS) return;
914
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
915
+ for (const b of boxes) {
916
+ const cx = (b.left + b.right) / 2, cy = (b.top + b.bottom) / 2;
917
+ if (cx < minX) minX = cx;
918
+ if (cx > maxX) maxX = cx;
919
+ if (cy < minY) minY = cy;
920
+ if (cy > maxY) maxY = cy;
921
+ }
922
+ if (maxX - minX <= maxY - minY) return;
923
+ const plan = planAxisTickThin(boxes, AXIS_THIN_GAP);
924
+ if (plan.hide.length === 0) return;
925
+ if (axisLabelsAreNominal(texts.map((t) => ownTextOf(t)))) {
926
+ const anchor = anchorKindOf(texts[0]);
927
+ const anchors = boxes.map((b) => anchor === "start" ? b.left : anchor === "end" ? b.right : (b.left + b.right) / 2);
928
+ const rooms = nominalLabelRooms(anchors, AXIS_THIN_GAP);
929
+ let cut = 0, shrunk = 0;
930
+ texts.forEach((t, i) => {
931
+ const width = boxes[i].right - boxes[i].left;
932
+ if (!(rooms[i] < width)) return;
933
+ const done = fitLabelToRoom(t, width, Math.max(1, rooms[i]), () => t.getBoundingClientRect().width, 1);
934
+ if (done === "cut") cut++;
935
+ else if (done === "shrunk") shrunk++;
936
+ });
937
+ if (cut + shrunk === 0) return;
938
+ const after = texts.map((t) => {
939
+ const r = t.getBoundingClientRect();
940
+ return { left: r.left, right: r.right, top: r.top, bottom: r.bottom };
941
+ }).sort((p, q) => p.left - q.left);
942
+ let colliding = 0;
943
+ for (let i = 1; i < after.length; i++) if (after[i].left < after[i - 1].right) colliding++;
944
+ axis.setAttribute("data-lch-ticks-thinned", "1");
945
+ axesTouched++;
946
+ nominalAxes++;
947
+ cutTotal += cut;
948
+ shrunkTotal += shrunk;
949
+ collidingTotal += colliding;
950
+ return;
951
+ }
952
+ for (const idx of plan.hide) texts[idx].setAttribute("display", "none");
953
+ axis.setAttribute("data-lch-ticks-thinned", "1");
954
+ axesTouched++;
955
+ hiddenTotal += plan.hide.length;
956
+ if (plan.stride > strideUsed) strideUsed = plan.stride;
957
+ });
958
+ if (axesTouched === 0) return none;
959
+ const out = {
960
+ applied: true,
961
+ at: "axes" + axesTouched + " hid" + hiddenTotal + " k" + strideUsed + (nominalAxes > 0 ? " cut" + cutTotal + " shr" + shrunkTotal : ""),
962
+ axes: axesTouched,
963
+ hidden: hiddenTotal,
964
+ stride: strideUsed
965
+ };
966
+ if (nominalAxes > 0) {
967
+ out.nominal = true;
968
+ out.cut = cutTotal;
969
+ out.shrunk = shrunkTotal;
970
+ if (collidingTotal > 0) out.colliding = collidingTotal;
971
+ }
972
+ return out;
973
+ }
974
+ function ownTextOf(el) {
975
+ let s = "";
976
+ el.childNodes.forEach((n) => {
977
+ if (n.nodeType === 3) s += n.nodeValue || "";
978
+ });
979
+ return s.trim() !== "" ? s : el.textContent || "";
980
+ }
981
+ function anchorKindOf(el) {
982
+ for (let n = el; n; n = n.parentElement) {
983
+ const v = n.getAttribute && n.getAttribute("text-anchor") || n.style && n.style.textAnchor || "";
984
+ if (v === "start" || v === "end" || v === "middle") return v;
985
+ }
986
+ try {
987
+ const v = typeof getComputedStyle === "function" ? getComputedStyle(el).getPropertyValue("text-anchor") : "";
988
+ if (v === "start" || v === "end") return v;
989
+ } catch {
990
+ }
991
+ return "middle";
992
+ }
993
+ function hasRotateAbove(el, stop) {
994
+ let n = el;
995
+ while (n && n !== stop) {
996
+ const t = n.getAttribute ? n.getAttribute("transform") : null;
997
+ if (t && t.indexOf("rotate") >= 0) return true;
998
+ n = n.parentElement;
999
+ }
1000
+ return false;
1001
+ }
1002
+ function rowSitsOnTrack(els, rowBoxes, svg) {
1003
+ let lca = els[0].parentElement;
1004
+ while (lca && lca !== svg && !els.every((e) => lca.contains(e))) lca = lca.parentElement;
1005
+ if (!lca || lca === svg) return false;
1006
+ if (lca.querySelector(".d3-mark")) return false;
1007
+ const cands = [];
1008
+ const shapes = lca.querySelectorAll("rect, line, path, polyline");
1009
+ for (let i = 0; i < shapes.length; i++) {
1010
+ const el = shapes[i];
1011
+ if (el.closest(".d3-mark")) continue;
1012
+ const r = el.getBoundingClientRect();
1013
+ if (r.width <= 0) continue;
1014
+ cands.push({ left: r.left, right: r.right, top: r.top, bottom: r.bottom });
1015
+ }
1016
+ return labelRowHasTrack(rowBoxes, cands);
1017
+ }
1018
+ function thinLabelRowsPhase(svg) {
1019
+ const none = { applied: false, rows: 0, hidden: 0, stride: 0 };
1020
+ const all = svg.querySelectorAll("text");
1021
+ if (all.length < LABEL_ROW_MIN_TEXTS) return none;
1022
+ const texts = [];
1023
+ const boxes = [];
1024
+ for (let i = 0; i < all.length; i++) {
1025
+ const t = all[i];
1026
+ if (t.closest("svg") !== svg) continue;
1027
+ if (t.closest(".tick, g.llm-zoom-pad, g.llm-zoom-btn, g.llm-map-labels, foreignObject")) continue;
1028
+ if (t.getAttribute("display") === "none") continue;
1029
+ if (!isNumericLabelText(t.textContent || "")) continue;
1030
+ if (hasRotateAbove(t, svg)) continue;
1031
+ const r = t.getBoundingClientRect();
1032
+ if (r.width <= 0 || r.height <= 0) continue;
1033
+ texts.push(t);
1034
+ boxes.push({ left: r.left, right: r.right, top: r.top, bottom: r.bottom });
1035
+ }
1036
+ if (boxes.length < LABEL_ROW_MIN_TEXTS) return none;
1037
+ let rowsTouched = 0, hiddenTotal = 0, strideUsed = 0;
1038
+ for (const row of groupLabelRows(boxes)) {
1039
+ if (row.length < LABEL_ROW_MIN_TEXTS) continue;
1040
+ const rowBoxes = row.map((i) => boxes[i]);
1041
+ if (!rowSitsOnTrack(row.map((i) => texts[i]), rowBoxes, svg)) continue;
1042
+ const plan = planAxisTickThin(rowBoxes, AXIS_THIN_GAP, LABEL_ROW_MIN_TEXTS);
1043
+ if (plan.hide.length === 0) continue;
1044
+ for (const k of plan.hide) {
1045
+ texts[row[k]].setAttribute("display", "none");
1046
+ texts[row[k]].setAttribute("data-lch-label-thinned", "1");
1047
+ }
1048
+ rowsTouched++;
1049
+ hiddenTotal += plan.hide.length;
1050
+ if (plan.stride > strideUsed) strideUsed = plan.stride;
1051
+ }
1052
+ if (rowsTouched === 0) return none;
1053
+ return {
1054
+ applied: true,
1055
+ lt: "rows" + rowsTouched + " hid" + hiddenTotal + " k" + strideUsed,
1056
+ rows: rowsTouched,
1057
+ hidden: hiddenTotal,
1058
+ stride: strideUsed
1059
+ };
1060
+ }
1061
+ function axisThinReportOf(r) {
1062
+ const out = { axes: r.axes, hidden: r.hidden, stride: r.stride };
1063
+ if (r.nominal) {
1064
+ out.nominal = true;
1065
+ out.cut = r.cut;
1066
+ out.shrunk = r.shrunk;
1067
+ if (r.colliding) out.colliding = r.colliding;
1068
+ }
1069
+ return out;
1070
+ }
1071
+ function reconcileLegendInSvg(svg, opts = {}) {
1072
+ if (svg.getAttribute("data-lch-legend-fixed") === "1") return { applied: false, reason: "already-fixed" };
1073
+ if (hasSelfManagedScrollLayout(svg)) {
1074
+ svg.setAttribute("data-lch-legend-fixed", "1");
1075
+ return { applied: false, reason: "self-managed-layout" };
1076
+ }
1077
+ const legendRes = reconcileLegendPhase(svg, opts);
1078
+ let cbRes = { applied: false };
1079
+ try {
1080
+ cbRes = reconcileColorbarPhase(svg, opts);
1081
+ } catch {
1082
+ }
1083
+ let atRes = { applied: false, axes: 0, hidden: 0, stride: 1 };
1084
+ try {
1085
+ atRes = thinAxisTicksPhase(svg);
1086
+ } catch {
1087
+ }
1088
+ let ltRes = { applied: false, rows: 0, hidden: 0, stride: 0 };
1089
+ try {
1090
+ ltRes = thinLabelRowsPhase(svg);
1091
+ } catch {
1092
+ }
1093
+ let btRes = { applied: false };
1094
+ try {
1095
+ btRes = reconcileBottomTextPhase(svg);
1096
+ } catch {
1097
+ }
1098
+ let fitRes = { applied: false };
1099
+ try {
1100
+ fitRes = fitSvgToContent(svg);
1101
+ } catch {
1102
+ }
1103
+ return {
1104
+ ...legendRes,
1105
+ applied: legendRes.applied || cbRes.applied || atRes.applied || ltRes.applied || btRes.applied || fitRes.applied,
1106
+ reason: legendRes.applied ? legendRes.reason : cbRes.applied ? "colorbar" : atRes.applied ? "axis-thin" : ltRes.applied ? "label-thin" : btRes.applied ? "bottom-text" : fitRes.applied ? "fit" : legendRes.reason,
1107
+ cb: cbRes.cb,
1108
+ bt: btRes.bt,
1109
+ at: atRes.at,
1110
+ axisThin: atRes.applied ? axisThinReportOf(atRes) : void 0,
1111
+ lt: ltRes.lt,
1112
+ labelThin: ltRes.applied ? { rows: ltRes.rows, hidden: ltRes.hidden, stride: ltRes.stride } : void 0,
1113
+ fit: fitRes.fit,
1114
+ // Only when the fit DECLINED for a reason of its own ("clamped" / "legibility") —
1115
+ // not for "fits" or "no-content", which are silence for the right reasons.
1116
+ fitDeclined: fitRes.declined === "clamped" || fitRes.declined === "legibility" ? fitRes.declined : void 0,
1117
+ smallestTextPx: fitRes.smallestTextPx !== void 0 ? Math.round(fitRes.smallestTextPx * 10) / 10 : void 0
1118
+ };
1119
+ }
1120
+ function findSignatureLegendGroups(svg) {
1121
+ const SHAPES = "rect,circle,ellipse,line,path,polygon,polyline";
1122
+ const PLOT_MARK = ".d3-mark:not(.d3-legend-mark)";
1123
+ const out = [];
1124
+ const px = (el) => {
1125
+ const r = el.getBoundingClientRect();
1126
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
1127
+ };
1128
+ const walk = (g) => {
1129
+ for (let i = 0; i < g.children.length; i++) {
1130
+ const ch = g.children[i];
1131
+ const tag = ch.tagName.toLowerCase();
1132
+ if (tag !== "g") continue;
1133
+ if (ch.matches("g.llm-zoom-pad, g.llm-zoom-btn")) continue;
1134
+ const markFree = !ch.querySelector(PLOT_MARK) && !!ch.querySelector("text") && !!ch.querySelector(SHAPES);
1135
+ if (!markFree) {
1136
+ walk(ch);
1137
+ continue;
1138
+ }
1139
+ if (ch.querySelector("g.tick, path.domain") || /axis/i.test(ch.getAttribute("class") || "")) continue;
1140
+ const shapes = Array.from(ch.querySelectorAll(SHAPES)).map(px);
1141
+ const texts = Array.from(ch.querySelectorAll("text")).map(px).filter((t) => t.right > t.left);
1142
+ if (isLegendSignature(swatchPairs(shapes, texts))) out.push(ch);
1143
+ }
1144
+ };
1145
+ walk(svg);
1146
+ return out;
1147
+ }
1148
+ function reconcileLegendPhase(svg, opts = {}) {
1149
+ const legendMarks = svg.querySelectorAll(".d3-legend-mark");
1150
+ let source = "class";
1151
+ let signatureGroups = [];
1152
+ if (legendMarks.length === 0) {
1153
+ signatureGroups = findSignatureLegendGroups(svg);
1154
+ if (signatureGroups.length === 0) {
1155
+ svg.setAttribute("data-lch-legend-fixed", "1");
1156
+ return { applied: false, reason: "no-legend" };
1157
+ }
1158
+ source = "signature";
1159
+ }
1160
+ const plotMarks = svg.querySelectorAll(".d3-mark");
1161
+ if (plotMarks.length === 0) {
1162
+ svg.setAttribute("data-lch-legend-fixed", "1");
1163
+ return { applied: false, reason: "no-marks", source };
1164
+ }
1165
+ const groupHasPlotMark = (el) => {
1166
+ const dm = el.querySelectorAll(".d3-mark");
1167
+ for (let j = 0; j < dm.length; j++) if (!dm[j].classList.contains("d3-legend-mark")) return true;
1168
+ return false;
1169
+ };
1170
+ let legendGroup = source === "signature" ? signatureGroups[0] : legendMarks[0].parentElement;
1171
+ while (source === "class" && legendGroup && legendGroup !== svg) {
1172
+ let containsAll = true;
1173
+ for (let i = 0; i < legendMarks.length; i++) {
1174
+ if (!legendGroup.contains(legendMarks[i])) {
1175
+ containsAll = false;
1176
+ break;
1177
+ }
1178
+ }
1179
+ if (containsAll) break;
1180
+ legendGroup = legendGroup.parentElement;
1181
+ }
1182
+ let legendContainers;
1183
+ let mixedGroup = false;
1184
+ if (source === "signature") {
1185
+ legendContainers = signatureGroups;
1186
+ } else if (!legendGroup || legendGroup === svg) {
1187
+ const set = /* @__PURE__ */ new Set();
1188
+ for (let i = 0; i < legendMarks.length; i++) {
1189
+ let el = legendMarks[i];
1190
+ while (el && el.parentElement !== svg) el = el.parentElement;
1191
+ if (!el) return { applied: false, reason: "no-clean-group" };
1192
+ set.add(el);
1193
+ }
1194
+ legendContainers = Array.from(set);
1195
+ for (const c of legendContainers) {
1196
+ if (groupHasPlotMark(c)) return { applied: false, reason: "impure-group" };
1197
+ }
1198
+ legendGroup = legendContainers[0];
1199
+ } else {
1200
+ if (groupHasPlotMark(legendGroup)) {
1201
+ mixedGroup = true;
1202
+ legendContainers = Array.from(legendMarks);
1203
+ } else {
1204
+ legendContainers = [legendGroup];
1205
+ }
1206
+ }
1207
+ const ctm = svg.getScreenCTM();
1208
+ if (!ctm) return { applied: false, reason: "no-ctm" };
1209
+ const inv = ctm.inverse();
1210
+ const toUserRect = (r) => {
1211
+ const a = new DOMPoint(r.left, r.top).matrixTransform(inv);
1212
+ const b = new DOMPoint(r.right, r.bottom).matrixTransform(inv);
1213
+ return { left: Math.min(a.x, b.x), right: Math.max(a.x, b.x), top: Math.min(a.y, b.y), bottom: Math.max(a.y, b.y) };
1214
+ };
1215
+ const userRectOf = (el) => toUserRect(el.getBoundingClientRect());
1216
+ let vbox = null;
1217
+ const vbAttr = svg.getAttribute("viewBox");
1218
+ if (vbAttr) {
1219
+ const p = vbAttr.split(/[\s,]+/).map(Number).filter((n) => isFinite(n));
1220
+ if (p.length === 4) vbox = { x: p[0], y: p[1], w: p[2], h: p[3] };
1221
+ }
1222
+ if (!vbox) {
1223
+ const svgRect = svg.getBoundingClientRect();
1224
+ const w = parseFloat(svg.getAttribute("width") || "") || svgRect.width;
1225
+ const h = parseFloat(svg.getAttribute("height") || "") || svgRect.height;
1226
+ if (w > 0 && h > 0) vbox = { x: 0, y: 0, w, h };
1227
+ }
1228
+ if (!vbox) return { applied: false, reason: "no-viewbox" };
1229
+ let lbL = Infinity, lbT = Infinity, lbR = -Infinity, lbB = -Infinity;
1230
+ for (const c of legendContainers) {
1231
+ const cb = userRectOf(c);
1232
+ lbL = Math.min(lbL, cb.left);
1233
+ lbT = Math.min(lbT, cb.top);
1234
+ lbR = Math.max(lbR, cb.right);
1235
+ lbB = Math.max(lbB, cb.bottom);
1236
+ }
1237
+ const legendBox = { left: lbL, top: lbT, right: lbR, bottom: lbB };
1238
+ const tol = Math.max(8, vbox.w * 0.015);
1239
+ const isOrHasPlotMark = (el) => {
1240
+ if (el.matches && el.matches(".d3-mark") && !(el.classList && el.classList.contains("d3-legend-mark"))) return true;
1241
+ return groupHasPlotMark(el);
1242
+ };
1243
+ const furniture = legendContainers.slice();
1244
+ if (mixedGroup) {
1245
+ const xTol = Math.min(4, tol);
1246
+ const yPad = Math.max(28, legendBox.bottom - legendBox.top);
1247
+ const cand = legendGroup.querySelectorAll("text, rect");
1248
+ for (let i = 0; i < cand.length; i++) {
1249
+ const el = cand[i];
1250
+ if (furniture.indexOf(el) >= 0) continue;
1251
+ if (isOrHasPlotMark(el)) continue;
1252
+ const ub = userRectOf(el);
1253
+ if (ub.right <= ub.left || ub.bottom <= ub.top) continue;
1254
+ if (ub.left < legendBox.left - xTol) continue;
1255
+ if (ub.bottom < legendBox.top - yPad || ub.top > legendBox.bottom + yPad) continue;
1256
+ furniture.push(el);
1257
+ }
1258
+ } else {
1259
+ const parent = legendGroup.parentElement;
1260
+ if (parent) {
1261
+ const sibs = parent.children;
1262
+ for (let i = 0; i < sibs.length; i++) {
1263
+ const el = sibs[i];
1264
+ if (furniture.indexOf(el) >= 0) continue;
1265
+ const tag = el.tagName.toLowerCase();
1266
+ if (tag === "defs" || tag === "style" || tag === "title") continue;
1267
+ if (isOrHasPlotMark(el)) continue;
1268
+ const ub = userRectOf(el);
1269
+ if (ub.right <= ub.left || ub.bottom <= ub.top) continue;
1270
+ if (ub.left >= legendBox.left - tol) furniture.push(el);
1271
+ }
1272
+ }
1273
+ }
1274
+ const CAP = 4e3;
1275
+ const inFurniture = (node) => furniture.some((f) => f.contains(node));
1276
+ let totalDx = 0, totalDy = 0, totalExt = 0, passes = 0;
1277
+ let lastReason = "no-overlap";
1278
+ let lastW = vbox.w;
1279
+ for (let pass = 0; pass < 3; pass++) {
1280
+ const ctmP = svg.getScreenCTM();
1281
+ if (!ctmP) break;
1282
+ const invP = ctmP.inverse();
1283
+ const toUserP = (r) => {
1284
+ const a = new DOMPoint(r.left, r.top).matrixTransform(invP);
1285
+ const b = new DOMPoint(r.right, r.bottom).matrixTransform(invP);
1286
+ return { left: Math.min(a.x, b.x), right: Math.max(a.x, b.x), top: Math.min(a.y, b.y), bottom: Math.max(a.y, b.y) };
1287
+ };
1288
+ let vbP = null;
1289
+ const vbA = svg.getAttribute("viewBox");
1290
+ if (vbA) {
1291
+ const p = vbA.split(/[\s,]+/).map(Number).filter((n) => isFinite(n));
1292
+ if (p.length === 4) vbP = { x: p[0], y: p[1], w: p[2], h: p[3] };
1293
+ }
1294
+ if (!vbP) vbP = vbox;
1295
+ let uL = Infinity, uT = Infinity, uR = -Infinity, uB = -Infinity;
1296
+ for (const f of furniture) {
1297
+ const ub = toUserP(f.getBoundingClientRect());
1298
+ uL = Math.min(uL, ub.left);
1299
+ uT = Math.min(uT, ub.top);
1300
+ uR = Math.max(uR, ub.right);
1301
+ uB = Math.max(uB, ub.bottom);
1302
+ }
1303
+ const unionBox = { left: uL, top: uT, right: uR, bottom: uB };
1304
+ const plotRects = [];
1305
+ for (let i = 0; i < plotMarks.length && plotRects.length < CAP; i++) {
1306
+ if (inFurniture(plotMarks[i])) continue;
1307
+ const r = plotMarks[i].getBoundingClientRect();
1308
+ if (r.width > 0 && r.height > 0) plotRects.push(toUserP(r));
1309
+ }
1310
+ const texts = svg.querySelectorAll("text");
1311
+ for (let i = 0; i < texts.length && plotRects.length < CAP; i++) {
1312
+ if (inFurniture(texts[i])) continue;
1313
+ const r = texts[i].getBoundingClientRect();
1314
+ if (r.width > 0 && r.height > 0) plotRects.push(toUserP(r));
1315
+ }
1316
+ const side = source === "class" ? "right" : colorbarReconcileSide(unionBox, vbP);
1317
+ if (!side) {
1318
+ lastReason = "not-a-margin-legend";
1319
+ break;
1320
+ }
1321
+ const plan = planLegendReconcile(unionBox, plotRects, vbP, opts, side);
1322
+ lastReason = plan.reason;
1323
+ if (plan.action !== "reserve") break;
1324
+ svg.setAttribute("viewBox", `${plan.newViewBox.x} ${plan.newViewBox.y} ${plan.newViewBox.w} ${plan.newViewBox.h}`);
1325
+ if (!svg.getAttribute("preserveAspectRatio")) svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1326
+ for (const f of furniture) {
1327
+ const prior = f.getAttribute("transform") || "";
1328
+ f.setAttribute("transform", `translate(${plan.legendDx},${plan.legendDy}) ${prior}`.trim());
1329
+ }
1330
+ totalDx += plan.legendDx;
1331
+ totalDy += plan.legendDy;
1332
+ totalExt += plan.ext;
1333
+ lastW = plan.newViewBox.w;
1334
+ passes++;
1335
+ }
1336
+ svg.setAttribute("data-lch-legend-fixed", "1");
1337
+ if (passes === 0) return { applied: false, reason: lastReason, furniture: furniture.length, source };
1338
+ return {
1339
+ applied: true,
1340
+ reason: passes > 1 ? `reserve-x${passes}` : "reserve",
1341
+ dx: totalDx,
1342
+ dy: totalDy || void 0,
1343
+ ext: totalExt,
1344
+ newW: lastW,
1345
+ furniture: furniture.length,
1346
+ source
1347
+ };
1348
+ }
1349
+ function findColorbarFurniture(svg) {
1350
+ const none = { groups: [], bars: [] };
1351
+ const gradIds = /* @__PURE__ */ new Set();
1352
+ const grads = svg.querySelectorAll("linearGradient[id], radialGradient[id]");
1353
+ for (let i = 0; i < grads.length; i++) {
1354
+ const id = grads[i].getAttribute("id");
1355
+ if (id) gradIds.add("#" + id);
1356
+ }
1357
+ if (gradIds.size === 0) return none;
1358
+ const bars = [];
1359
+ const rects = svg.querySelectorAll("rect");
1360
+ for (let i = 0; i < rects.length; i++) {
1361
+ const r = rects[i];
1362
+ if (r.classList && r.classList.contains("d3-mark") && !r.classList.contains("d3-legend-mark")) continue;
1363
+ const fill = r.getAttribute("fill") || r.style && r.style.fill || "";
1364
+ const m = /url\((#[^)\s]+)\)/.exec(fill);
1365
+ if (m && gradIds.has(m[1])) bars.push(r);
1366
+ }
1367
+ if (bars.length === 0) return none;
1368
+ const furnSet = /* @__PURE__ */ new Set();
1369
+ for (const bar of bars) {
1370
+ const p = bar.parentElement;
1371
+ furnSet.add(p && p !== svg && p.tagName.toLowerCase() === "g" ? p : bar);
1372
+ }
1373
+ return { groups: Array.from(furnSet), bars };
1374
+ }
1375
+ function barHasMarkInkUnderIt(svg, bar, ctm, inFurniture) {
1376
+ const probes = colorbarProbePoints(bar);
1377
+ if (probes.length === 0) return false;
1378
+ const screen = probes.map((p) => new DOMPoint(p.x, p.y).matrixTransform(ctm));
1379
+ const marks = svg.querySelectorAll(".d3-mark");
1380
+ const hit = new Array(probes.length).fill(false);
1381
+ let hits = 0;
1382
+ for (let i = 0; i < marks.length && hits < probes.length; i++) {
1383
+ const el = marks[i];
1384
+ if (inFurniture(el)) continue;
1385
+ const r = el.getBoundingClientRect();
1386
+ if (r.width <= 0 || r.height <= 0) continue;
1387
+ const geo = el;
1388
+ let inv = null;
1389
+ const canProbe = typeof geo.isPointInFill === "function";
1390
+ if (canProbe) {
1391
+ const m = el.getScreenCTM();
1392
+ inv = m ? m.inverse() : null;
1393
+ }
1394
+ for (let p = 0; p < screen.length; p++) {
1395
+ if (hit[p]) continue;
1396
+ const s = screen[p];
1397
+ if (s.x < r.left || s.x > r.right || s.y < r.top || s.y > r.bottom) continue;
1398
+ let painted = true;
1399
+ if (canProbe && inv) {
1400
+ const q = new DOMPoint(s.x, s.y).matrixTransform(inv);
1401
+ try {
1402
+ painted = geo.isPointInFill(q) || typeof geo.isPointInStroke === "function" && geo.isPointInStroke(q);
1403
+ } catch {
1404
+ painted = true;
1405
+ }
1406
+ }
1407
+ if (painted) {
1408
+ hit[p] = true;
1409
+ hits++;
1410
+ }
1411
+ }
1412
+ }
1413
+ return colorbarOnPlot(hits, probes.length);
1414
+ }
1415
+ function reconcileColorbarPhase(svg, opts = {}) {
1416
+ const found = findColorbarFurniture(svg);
1417
+ const furniture = found.groups;
1418
+ if (furniture.length === 0) return { applied: false, reason: "no-colorbar" };
1419
+ const vbox0 = readViewBox(svg);
1420
+ if (!vbox0) return { applied: false, reason: "no-viewbox" };
1421
+ const inFurniture = (node) => furniture.some((f) => f === node || f.contains(node));
1422
+ const CAP = 4e3;
1423
+ let totalDx = 0, totalDy = 0, totalExt = 0, passes = 0;
1424
+ let lastReason = "no-overlap";
1425
+ let sideUsed = "bottom";
1426
+ for (let pass = 0; pass < 3; pass++) {
1427
+ const ctm = svg.getScreenCTM();
1428
+ if (!ctm) break;
1429
+ const inv = ctm.inverse();
1430
+ const toUser = (r) => {
1431
+ const a = new DOMPoint(r.left, r.top).matrixTransform(inv);
1432
+ const b = new DOMPoint(r.right, r.bottom).matrixTransform(inv);
1433
+ return { left: Math.min(a.x, b.x), right: Math.max(a.x, b.x), top: Math.min(a.y, b.y), bottom: Math.max(a.y, b.y) };
1434
+ };
1435
+ const vbP = readViewBox(svg) || vbox0;
1436
+ let uL = Infinity, uT = Infinity, uR = -Infinity, uB = -Infinity;
1437
+ for (const f of furniture) {
1438
+ const ub = toUser(f.getBoundingClientRect());
1439
+ uL = Math.min(uL, ub.left);
1440
+ uT = Math.min(uT, ub.top);
1441
+ uR = Math.max(uR, ub.right);
1442
+ uB = Math.max(uB, ub.bottom);
1443
+ }
1444
+ const unionBox = { left: uL, top: uT, right: uR, bottom: uB };
1445
+ const plotRects = [];
1446
+ const marks = svg.querySelectorAll(".d3-mark");
1447
+ for (let i = 0; i < marks.length && plotRects.length < CAP; i++) {
1448
+ if (inFurniture(marks[i])) continue;
1449
+ const r = marks[i].getBoundingClientRect();
1450
+ if (r.width > 0 && r.height > 0) plotRects.push(toUser(r));
1451
+ }
1452
+ const markCount = plotRects.length;
1453
+ const texts = svg.querySelectorAll("text");
1454
+ for (let i = 0; i < texts.length && plotRects.length < CAP; i++) {
1455
+ if (inFurniture(texts[i])) continue;
1456
+ const r = texts[i].getBoundingClientRect();
1457
+ if (r.width > 0 && r.height > 0) plotRects.push(toUser(r));
1458
+ }
1459
+ const side = colorbarReconcileSide(unionBox, vbP);
1460
+ if (!side) {
1461
+ lastReason = "not-a-margin-colorbar";
1462
+ break;
1463
+ }
1464
+ if (side === "right") {
1465
+ let bL = Infinity, bT = Infinity, bR = -Infinity, bB = -Infinity;
1466
+ for (const bar of found.bars) {
1467
+ const bb = toUser(bar.getBoundingClientRect());
1468
+ bL = Math.min(bL, bb.left);
1469
+ bT = Math.min(bT, bb.top);
1470
+ bR = Math.max(bR, bb.right);
1471
+ bB = Math.max(bB, bb.bottom);
1472
+ }
1473
+ const barBox = { left: bL, top: bT, right: bR, bottom: bB };
1474
+ const markRects = plotRects.slice(0, markCount);
1475
+ if (!(bR > -Infinity) || !colorbarSitsOnMarks(barBox, markRects) || !barHasMarkInkUnderIt(svg, barBox, ctm, inFurniture)) {
1476
+ lastReason = "colorbar-clear-of-marks";
1477
+ break;
1478
+ }
1479
+ }
1480
+ sideUsed = side;
1481
+ const plan = planLegendReconcile(unionBox, plotRects, vbP, opts, side);
1482
+ lastReason = plan.reason;
1483
+ if (plan.action !== "reserve") break;
1484
+ svg.setAttribute("viewBox", `${plan.newViewBox.x} ${plan.newViewBox.y} ${plan.newViewBox.w} ${plan.newViewBox.h}`);
1485
+ if (!svg.getAttribute("preserveAspectRatio")) svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1486
+ for (const f of furniture) {
1487
+ const prior = f.getAttribute("transform") || "";
1488
+ f.setAttribute("transform", `translate(${plan.legendDx},${plan.legendDy}) ${prior}`.trim());
1489
+ }
1490
+ totalDx += plan.legendDx;
1491
+ totalDy += plan.legendDy;
1492
+ totalExt += plan.ext;
1493
+ passes++;
1494
+ }
1495
+ if (passes === 0) return { applied: false, reason: lastReason };
1496
+ return {
1497
+ applied: true,
1498
+ reason: "reserve",
1499
+ cb: sideUsed === "right" ? `dx${totalDx} w+${totalExt}` : `dy${totalDy} h+${totalExt}`
1500
+ };
1501
+ }
1502
+ function reconcileBottomTextPhase(svg) {
1503
+ const vbox = readViewBox(svg);
1504
+ if (!vbox) return { applied: false };
1505
+ const ctm = svg.getScreenCTM();
1506
+ if (!ctm) return { applied: false };
1507
+ const inv = ctm.inverse();
1508
+ const toUser = (r) => {
1509
+ const a = new DOMPoint(r.left, r.top).matrixTransform(inv);
1510
+ const b = new DOMPoint(r.right, r.bottom).matrixTransform(inv);
1511
+ return { left: Math.min(a.x, b.x), right: Math.max(a.x, b.x), top: Math.min(a.y, b.y), bottom: Math.max(a.y, b.y) };
1512
+ };
1513
+ const tickEls = svg.querySelectorAll(".tick text");
1514
+ if (tickEls.length === 0) return { applied: false };
1515
+ const tickBoxes = [];
1516
+ for (let i = 0; i < tickEls.length; i++) {
1517
+ const r = tickEls[i].getBoundingClientRect();
1518
+ if (r.width > 0 && r.height > 0) tickBoxes.push(toUser(r));
1519
+ }
1520
+ if (tickBoxes.length === 0) return { applied: false };
1521
+ const inTick = (n) => {
1522
+ let p = n;
1523
+ while (p) {
1524
+ if (p.classList && p.classList.contains("tick")) return true;
1525
+ p = p.parentElement;
1526
+ }
1527
+ return false;
1528
+ };
1529
+ const GAP = 6;
1530
+ let moved = 0, maxDy = 0, maxBottom = -Infinity;
1531
+ const texts = svg.querySelectorAll("text");
1532
+ const CAP = 4e3;
1533
+ for (let i = 0; i < texts.length && i < CAP; i++) {
1534
+ const el = texts[i];
1535
+ if (inTick(el)) continue;
1536
+ if (el.classList && (el.classList.contains("d3-mark") || el.classList.contains("d3-legend-mark"))) continue;
1537
+ const r = el.getBoundingClientRect();
1538
+ if (r.width <= 0 || r.height <= 0) continue;
1539
+ const box = toUser(r);
1540
+ const prior = el.getAttribute("transform") || "";
1541
+ const plan = planBottomTextPush(box, tickBoxes, GAP, prior);
1542
+ if (!plan.move) continue;
1543
+ el.setAttribute("transform", `translate(0,${plan.dy}) ${prior}`.trim());
1544
+ moved++;
1545
+ if (plan.dy > maxDy) maxDy = plan.dy;
1546
+ const nb = box.bottom + plan.dy;
1547
+ if (nb > maxBottom) maxBottom = nb;
1548
+ }
1549
+ if (moved === 0) return { applied: false };
1550
+ if (maxBottom > vbox.y + vbox.h) {
1551
+ const newH = maxBottom - vbox.y + GAP;
1552
+ svg.setAttribute("viewBox", `${vbox.x} ${vbox.y} ${vbox.w} ${newH}`);
1553
+ if (!svg.getAttribute("preserveAspectRatio")) svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1554
+ }
1555
+ return { applied: true, bt: `moved${moved} dy${Math.round(maxDy)}` };
1556
+ }
1557
+ export {
1558
+ AXIS_THIN_GAP,
1559
+ AXIS_THIN_MIN_TICKS,
1560
+ BOTTOM_TEXT_MIN_TICK_HITS,
1561
+ COLORBAR_ON_PLOT_COVER,
1562
+ FIT_PAD,
1563
+ FIT_SHRINK_FLOOR,
1564
+ FIT_TYPE_FLOOR,
1565
+ FRAME_FIT_TEXT_CAP,
1566
+ FRAME_PAD,
1567
+ LABEL_ROW_MIN_TEXTS,
1568
+ LABEL_ROW_TRACK_COVER,
1569
+ LABEL_ROW_TRACK_REACH,
1570
+ LABEL_ROW_Y_TOLERANCE,
1571
+ LEGIBILITY_GRACE_PX,
1572
+ MAX_EXTEND_FRACTION,
1573
+ MAX_FIT_FRACTION,
1574
+ MAX_GUTTER_FRACTION,
1575
+ MIN_LEGIBLE_PX,
1576
+ MIN_OVERLAP,
1577
+ SIGNATURE_MIN_PAIRS,
1578
+ SWATCH_LABEL_GAP_PX,
1579
+ SWATCH_MAX_PX,
1580
+ axisLabelsAreNominal,
1581
+ bottommostOverlappingPlotEdge,
1582
+ colorbarOnPlot,
1583
+ colorbarProbePoints,
1584
+ colorbarReconcileSide,
1585
+ colorbarSitsOnMarks,
1586
+ cutToFit,
1587
+ findColorbarFurniture,
1588
+ findSignatureLegendGroups,
1589
+ fitDeferredLabels,
1590
+ fitLabelToRoom,
1591
+ fitTextToFrame,
1592
+ groupLabelRows,
1593
+ isClippedOrScrolled,
1594
+ isDateLikeLabelText,
1595
+ isLegendSignature,
1596
+ isNumericLabelText,
1597
+ isOrderedAxisLabelText,
1598
+ labelFitDecision,
1599
+ labelRoom,
1600
+ labelRowHasTrack,
1601
+ nominalLabelRooms,
1602
+ planAxisTickThin,
1603
+ planBottomTextPush,
1604
+ planContentFit,
1605
+ planFrameFit,
1606
+ planLegendReconcile,
1607
+ reconcileLegendInSvg,
1608
+ rightmostOverlappingPlotEdge,
1609
+ smallestRenderedTextPx,
1610
+ styleDeclaresScroll,
1611
+ swatchPairs,
1612
+ viewBoxAttr
1613
+ };