@bicharts/chart-host 0.6.76 → 0.6.78
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/dist/adapters.mjs +75 -0
- package/dist/card.mjs +234 -0
- package/dist/chunk-BFN4NY22.mjs +3286 -0
- package/dist/{chunk-ZEXMBZHP.mjs → chunk-L554L6HF.mjs} +40 -3830
- package/dist/chunk-M7VM6F73.mjs +614 -0
- package/dist/chunk-MGAO2DR7.mjs +543 -0
- package/dist/index.mjs +72 -664
- package/dist/react.mjs +3 -1
- package/dist/types/adapters/d3.d.ts +3 -0
- package/dist/types/adapters/index.d.ts +3 -0
- package/dist/types/adapters/types.d.ts +108 -0
- package/dist/types/card.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/selectionCardChrome.d.ts +29 -0
- package/package.json +11 -1
|
@@ -0,0 +1,3286 @@
|
|
|
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
|
+
Fn,
|
|
4
|
+
J3,
|
|
5
|
+
Xn,
|
|
6
|
+
Yn,
|
|
7
|
+
ge
|
|
8
|
+
} from "./chunk-L554L6HF.mjs";
|
|
9
|
+
import {
|
|
10
|
+
censusHitBands,
|
|
11
|
+
createMarkResolver,
|
|
12
|
+
ensureCrossfilterHitTargets,
|
|
13
|
+
isInsideControl,
|
|
14
|
+
markKeyOf,
|
|
15
|
+
nextClickedMarks,
|
|
16
|
+
nextSelection,
|
|
17
|
+
planSelectionPaint,
|
|
18
|
+
selectionRuleOf
|
|
19
|
+
} from "./chunk-MGAO2DR7.mjs";
|
|
20
|
+
import {
|
|
21
|
+
NA_ISO3,
|
|
22
|
+
NA_LAT_MAX,
|
|
23
|
+
clipFeatureToLat
|
|
24
|
+
} from "./chunk-A2GMXZP7.mjs";
|
|
25
|
+
import {
|
|
26
|
+
ACTIVE_TICK_CLASS,
|
|
27
|
+
ANIM_LOOP_DELAY_DEFAULT,
|
|
28
|
+
ANIM_LOOP_DELAY_MIN,
|
|
29
|
+
ANIM_MAX_IDEAL_FRAMES_DEFAULT,
|
|
30
|
+
ANIM_MAX_IDEAL_FRAMES_MAX,
|
|
31
|
+
ANIM_MAX_IDEAL_FRAMES_MIN,
|
|
32
|
+
ANIM_PLAY_SPEED_DEFAULT,
|
|
33
|
+
ANIM_PLAY_SPEED_MAX,
|
|
34
|
+
ANIM_PLAY_SPEED_MIN,
|
|
35
|
+
APPROXIMATE_POSITIONS_DEFAULT,
|
|
36
|
+
AXIS_FILTER_CLASS,
|
|
37
|
+
COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT,
|
|
38
|
+
COLOR_SCALE_SELF_CLAMP_PCT_MAX,
|
|
39
|
+
COLOR_SCALE_SELF_CLAMP_PCT_MIN,
|
|
40
|
+
CONTAINER_SLOT_ANIM_STOP,
|
|
41
|
+
CONTAINER_SLOT_CLICKED_MARKS,
|
|
42
|
+
CONTAINER_SLOT_INITIAL_XF_MARK,
|
|
43
|
+
CONTAINER_SLOT_UI_STATE,
|
|
44
|
+
CONTAINER_SLOT_XF_CLEAR,
|
|
45
|
+
CONTROL_CLASS,
|
|
46
|
+
DIM_OPACITY_DEFAULT,
|
|
47
|
+
DIM_OPACITY_VAR,
|
|
48
|
+
FLIP_DIRECTION_DEFAULT,
|
|
49
|
+
FLIP_MODE_DEFAULT,
|
|
50
|
+
HOST_CONTAINER_CLASS,
|
|
51
|
+
HOST_CONTRACT_VERSION,
|
|
52
|
+
LEGEND_MARK_CLASS,
|
|
53
|
+
LIFT_SELECTED_CLASS,
|
|
54
|
+
MARK_CLASS,
|
|
55
|
+
MARK_SELECTED_CLASS,
|
|
56
|
+
MAX_MAP_POINTS_DEFAULT,
|
|
57
|
+
NO_ROW_MARKS_ATTR,
|
|
58
|
+
ROW_IDX_ATTR,
|
|
59
|
+
SEASONAL_MARKERS_DEFAULT,
|
|
60
|
+
SELECTION_ACTIVE_CLASS,
|
|
61
|
+
SNAPSHOT_SVG_ATTR,
|
|
62
|
+
VALUE_AXIS_BASELINE_DEFAULT,
|
|
63
|
+
XFILTER_REFRESH_EVENT,
|
|
64
|
+
chartOwnsTimeline,
|
|
65
|
+
periodTickSuppressesFeedback
|
|
66
|
+
} from "./chunk-7JWL6LYE.mjs";
|
|
67
|
+
|
|
68
|
+
// src/labelContrast.ts
|
|
69
|
+
function parseRGBA(c) {
|
|
70
|
+
if (typeof c !== "string") return null;
|
|
71
|
+
const s = c.trim();
|
|
72
|
+
const m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
|
|
73
|
+
if (m) return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), m[4] !== void 0 ? parseFloat(m[4]) : 1];
|
|
74
|
+
let h = s.replace(/^#/, "");
|
|
75
|
+
if (h.length === 3) h = h.split("").map((ch) => ch + ch).join("");
|
|
76
|
+
if (h.length === 6 && /^[0-9a-fA-F]{6}$/.test(h)) {
|
|
77
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
var DARK_TEXT = "#111111";
|
|
82
|
+
var LIGHT_TEXT = "#ffffff";
|
|
83
|
+
var MIN_CONTRAST = 3;
|
|
84
|
+
var WHITE_TEXT_BG_LUM = 0.5;
|
|
85
|
+
var PILL_MIN_ALPHA = 0.05;
|
|
86
|
+
var PILL_OPAQUE_ALPHA = 0.85;
|
|
87
|
+
var PAGE_MATCH_TOLERANCE = 2;
|
|
88
|
+
function isPillBackdropAlpha(a) {
|
|
89
|
+
return a >= PILL_MIN_ALPHA && a < PILL_OPAQUE_ALPHA;
|
|
90
|
+
}
|
|
91
|
+
var PILL_MIN_COVERAGE = 0.6;
|
|
92
|
+
function pillBacksGlyph(overlapArea, glyphArea) {
|
|
93
|
+
if (!(glyphArea > 0) || !isFinite(overlapArea) || overlapArea <= 0) return false;
|
|
94
|
+
return overlapArea / glyphArea >= PILL_MIN_COVERAGE;
|
|
95
|
+
}
|
|
96
|
+
var BACKING_MAJORITY = 0.5;
|
|
97
|
+
function backingHoldsGlyph(backedArea, glyphArea) {
|
|
98
|
+
if (!(glyphArea > 0) || !isFinite(backedArea) || backedArea <= 0) return false;
|
|
99
|
+
return backedArea / glyphArea >= BACKING_MAJORITY;
|
|
100
|
+
}
|
|
101
|
+
function cellSuppressesNormalize(cellClass) {
|
|
102
|
+
return typeof cellClass === "string" && /(^|\s)lch-deck-panel(\s|$)/.test(cellClass);
|
|
103
|
+
}
|
|
104
|
+
var NAMED = {
|
|
105
|
+
white: [255, 255, 255, 1],
|
|
106
|
+
black: [0, 0, 0, 1],
|
|
107
|
+
none: [0, 0, 0, 0],
|
|
108
|
+
transparent: [0, 0, 0, 0]
|
|
109
|
+
};
|
|
110
|
+
function toRGBA(c) {
|
|
111
|
+
if (typeof c !== "string") return null;
|
|
112
|
+
const k = c.trim().toLowerCase();
|
|
113
|
+
if (k in NAMED) return NAMED[k];
|
|
114
|
+
return parseRGBA(c);
|
|
115
|
+
}
|
|
116
|
+
function compositeOver(fg, bg) {
|
|
117
|
+
const a = Math.max(0, Math.min(1, fg[3]));
|
|
118
|
+
return [
|
|
119
|
+
Math.round(fg[0] * a + bg[0] * (1 - a)),
|
|
120
|
+
Math.round(fg[1] * a + bg[1] * (1 - a)),
|
|
121
|
+
Math.round(fg[2] * a + bg[2] * (1 - a))
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
function relativeLuminance(rgb) {
|
|
125
|
+
const lin = rgb.map((v) => {
|
|
126
|
+
const s = v / 255;
|
|
127
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
128
|
+
});
|
|
129
|
+
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
|
130
|
+
}
|
|
131
|
+
function contrastRatio(l1, l2) {
|
|
132
|
+
const hi = Math.max(l1, l2);
|
|
133
|
+
const lo = Math.min(l1, l2);
|
|
134
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
135
|
+
}
|
|
136
|
+
function decideLabelColor(textColor, fill, fillOpacity, pageBg, minContrast = MIN_CONTRAST, normalize = false) {
|
|
137
|
+
const fillRGBA = toRGBA(fill);
|
|
138
|
+
if (!fillRGBA) return { fix: false, color: "", reason: "fill-unparsed" };
|
|
139
|
+
const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
|
|
140
|
+
const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
|
|
141
|
+
const effFill = [fillRGBA[0], fillRGBA[1], fillRGBA[2], fillRGBA[3] * clamp01(fillOpacity)];
|
|
142
|
+
const bgRGB = compositeOver(effFill, pageRGB);
|
|
143
|
+
const bgLum = relativeLuminance(bgRGB);
|
|
144
|
+
const bgIsPage = Math.abs(bgRGB[0] - pageRGB[0]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[1] - pageRGB[1]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[2] - pageRGB[2]) <= PAGE_MATCH_TOLERANCE;
|
|
145
|
+
const effNormalize = normalize && !bgIsPage;
|
|
146
|
+
const textRGBA = toRGBA(textColor);
|
|
147
|
+
if (!effNormalize && textRGBA) {
|
|
148
|
+
const txtRGB = compositeOver(textRGBA, bgRGB);
|
|
149
|
+
const cur = contrastRatio(relativeLuminance(txtRGB), bgLum);
|
|
150
|
+
if (cur >= minContrast) return { fix: false, color: "", reason: "ok" };
|
|
151
|
+
}
|
|
152
|
+
const darkC = contrastRatio(relativeLuminance(hexToRGB(DARK_TEXT)), bgLum);
|
|
153
|
+
const lightC = contrastRatio(relativeLuminance(hexToRGB(LIGHT_TEXT)), bgLum);
|
|
154
|
+
let useLight = bgLum <= WHITE_TEXT_BG_LUM;
|
|
155
|
+
if (useLight && lightC < minContrast && darkC >= minContrast) useLight = false;
|
|
156
|
+
if (!useLight && darkC < minContrast && lightC >= minContrast) useLight = true;
|
|
157
|
+
return useLight ? { fix: true, color: LIGHT_TEXT, reason: "fixed-light" } : { fix: true, color: DARK_TEXT, reason: "fixed-dark" };
|
|
158
|
+
}
|
|
159
|
+
var GLYPH_SAMPLE_N = 3;
|
|
160
|
+
function glyphSampleGrid(box2, n = GLYPH_SAMPLE_N) {
|
|
161
|
+
const out = [];
|
|
162
|
+
if (!box2 || !(box2.width > 0) || !(box2.height > 0) || !(n >= 1)) return out;
|
|
163
|
+
const k = Math.floor(n);
|
|
164
|
+
for (let i = 0; i < k; i++) {
|
|
165
|
+
for (let j = 0; j < k; j++) {
|
|
166
|
+
out.push({
|
|
167
|
+
x: box2.left + box2.width * (i + 0.5) / k,
|
|
168
|
+
y: box2.top + box2.height * (j + 0.5) / k
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
var STRADDLE_COLS = 8;
|
|
175
|
+
var STRADDLE_ROWS = 2;
|
|
176
|
+
var STRADDLE_MIN_SHARE = 0.125;
|
|
177
|
+
var STRADDLE_TARGET_CONTRAST = 4.5;
|
|
178
|
+
var STRADDLE_PILL_ALPHA = 0.9;
|
|
179
|
+
function straddleSampleGrid(box2) {
|
|
180
|
+
const out = [];
|
|
181
|
+
if (!box2 || !(box2.width > 0) || !(box2.height > 0)) return out;
|
|
182
|
+
for (let i = 0; i < STRADDLE_COLS; i++) {
|
|
183
|
+
for (let j = 0; j < STRADDLE_ROWS; j++) {
|
|
184
|
+
out.push({
|
|
185
|
+
x: box2.left + box2.width * (i + 0.5) / STRADDLE_COLS,
|
|
186
|
+
y: box2.top + box2.height * (j + 0.5) / STRADDLE_ROWS
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
function backgroundGroups(bgs, tol = 8) {
|
|
193
|
+
const groups = [];
|
|
194
|
+
for (const b of bgs || []) {
|
|
195
|
+
const g = groups.find((x) => Math.abs(x.rgb[0] - b[0]) <= tol && Math.abs(x.rgb[1] - b[1]) <= tol && Math.abs(x.rgb[2] - b[2]) <= tol);
|
|
196
|
+
if (g) g.n++;
|
|
197
|
+
else groups.push({ rgb: [b[0], b[1], b[2]], n: 1 });
|
|
198
|
+
}
|
|
199
|
+
return groups.sort((a, b) => b.n - a.n);
|
|
200
|
+
}
|
|
201
|
+
function straddles(bgs) {
|
|
202
|
+
if (!bgs || bgs.length < 2) return false;
|
|
203
|
+
const g = backgroundGroups(bgs);
|
|
204
|
+
return g.length >= 2 && g[1].n / bgs.length >= STRADDLE_MIN_SHARE;
|
|
205
|
+
}
|
|
206
|
+
function minContrastOver(textColor, bgs) {
|
|
207
|
+
const t = toRGBA(textColor);
|
|
208
|
+
if (!t || !bgs || !bgs.length) return 0;
|
|
209
|
+
let worst = Infinity;
|
|
210
|
+
for (const b of bgs) {
|
|
211
|
+
const tx = compositeOver(t, b);
|
|
212
|
+
worst = Math.min(worst, contrastRatio(relativeLuminance(tx), relativeLuminance(b)));
|
|
213
|
+
}
|
|
214
|
+
return worst;
|
|
215
|
+
}
|
|
216
|
+
function pickColorOver(textColor, bgs, target = STRADDLE_TARGET_CONTRAST) {
|
|
217
|
+
const cur = minContrastOver(textColor, bgs);
|
|
218
|
+
if (cur >= target) return { color: String(textColor), contrast: cur, keep: true };
|
|
219
|
+
const d = minContrastOver(DARK_TEXT, bgs), l = minContrastOver(LIGHT_TEXT, bgs);
|
|
220
|
+
const best = d >= l ? { color: DARK_TEXT, contrast: d } : { color: LIGHT_TEXT, contrast: l };
|
|
221
|
+
return best.contrast > cur ? { ...best, keep: false } : { color: String(textColor), contrast: cur, keep: true };
|
|
222
|
+
}
|
|
223
|
+
function clamp01(v) {
|
|
224
|
+
if (typeof v !== "number" || !isFinite(v)) return 1;
|
|
225
|
+
return Math.max(0, Math.min(1, v));
|
|
226
|
+
}
|
|
227
|
+
function hexToRGB(hex2) {
|
|
228
|
+
const p = toRGBA(hex2);
|
|
229
|
+
return [p[0], p[1], p[2]];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/deltaE.ts
|
|
233
|
+
function linearise(c) {
|
|
234
|
+
const v = c / 255;
|
|
235
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
236
|
+
}
|
|
237
|
+
var Xn2 = 0.95047;
|
|
238
|
+
var Yn2 = 1;
|
|
239
|
+
var Zn = 1.08883;
|
|
240
|
+
var EPS = 216 / 24389;
|
|
241
|
+
var KAPPA = 24389 / 27;
|
|
242
|
+
function f(t) {
|
|
243
|
+
return t > EPS ? Math.cbrt(t) : (KAPPA * t + 16) / 116;
|
|
244
|
+
}
|
|
245
|
+
function rgbToLab(r, g, b) {
|
|
246
|
+
const R = linearise(r), G = linearise(g), B = linearise(b);
|
|
247
|
+
const X = 0.4124564 * R + 0.3575761 * G + 0.1804375 * B;
|
|
248
|
+
const Y = 0.2126729 * R + 0.7151522 * G + 0.072175 * B;
|
|
249
|
+
const Z = 0.0193339 * R + 0.119192 * G + 0.9503041 * B;
|
|
250
|
+
const fx = f(X / Xn2), fy = f(Y / Yn2), fz = f(Z / Zn);
|
|
251
|
+
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
252
|
+
}
|
|
253
|
+
function hexToLab(hex2) {
|
|
254
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex2.trim());
|
|
255
|
+
if (!m) return null;
|
|
256
|
+
const h = m[1].length === 3 ? m[1][0] + m[1][0] + m[1][1] + m[1][1] + m[1][2] + m[1][2] : m[1];
|
|
257
|
+
return rgbToLab(parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16));
|
|
258
|
+
}
|
|
259
|
+
var RAD = Math.PI / 180;
|
|
260
|
+
var DEG = 180 / Math.PI;
|
|
261
|
+
var NEUTRAL_CHROMA = 1e-4;
|
|
262
|
+
function deltaE2000(c1, c2) {
|
|
263
|
+
const C1 = Math.hypot(c1.a, c1.b), C2 = Math.hypot(c2.a, c2.b);
|
|
264
|
+
const Cbar = (C1 + C2) / 2;
|
|
265
|
+
const C7 = Math.pow(Cbar, 7);
|
|
266
|
+
const G = 0.5 * (1 - Math.sqrt(C7 / (C7 + Math.pow(25, 7))));
|
|
267
|
+
const a1p = (1 + G) * c1.a, a2p = (1 + G) * c2.a;
|
|
268
|
+
const C1p = Math.hypot(a1p, c1.b), C2p = Math.hypot(a2p, c2.b);
|
|
269
|
+
const neutral = C1p < NEUTRAL_CHROMA || C2p < NEUTRAL_CHROMA;
|
|
270
|
+
const h1p = C1p < NEUTRAL_CHROMA ? 0 : (Math.atan2(c1.b, a1p) * DEG + 360) % 360;
|
|
271
|
+
const h2p = C2p < NEUTRAL_CHROMA ? 0 : (Math.atan2(c2.b, a2p) * DEG + 360) % 360;
|
|
272
|
+
const dLp = c2.L - c1.L;
|
|
273
|
+
const dCp = C2p - C1p;
|
|
274
|
+
let dhp;
|
|
275
|
+
if (neutral) dhp = 0;
|
|
276
|
+
else if (Math.abs(h2p - h1p) <= 180) dhp = h2p - h1p;
|
|
277
|
+
else if (h2p - h1p > 180) dhp = h2p - h1p - 360;
|
|
278
|
+
else dhp = h2p - h1p + 360;
|
|
279
|
+
const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp / 2 * RAD);
|
|
280
|
+
const Lbarp = (c1.L + c2.L) / 2;
|
|
281
|
+
const Cbarp = (C1p + C2p) / 2;
|
|
282
|
+
let hbarp;
|
|
283
|
+
if (neutral) hbarp = h1p + h2p;
|
|
284
|
+
else if (Math.abs(h1p - h2p) <= 180) hbarp = (h1p + h2p) / 2;
|
|
285
|
+
else if (h1p + h2p < 360) hbarp = (h1p + h2p + 360) / 2;
|
|
286
|
+
else hbarp = (h1p + h2p - 360) / 2;
|
|
287
|
+
const T = 1 - 0.17 * Math.cos((hbarp - 30) * RAD) + 0.24 * Math.cos(2 * hbarp * RAD) + 0.32 * Math.cos((3 * hbarp + 6) * RAD) - 0.2 * Math.cos((4 * hbarp - 63) * RAD);
|
|
288
|
+
const dTheta = 30 * Math.exp(-Math.pow((hbarp - 275) / 25, 2));
|
|
289
|
+
const Cbarp7 = Math.pow(Cbarp, 7);
|
|
290
|
+
const RC = 2 * Math.sqrt(Cbarp7 / (Cbarp7 + Math.pow(25, 7)));
|
|
291
|
+
const RT = -RC * Math.sin(2 * dTheta * RAD);
|
|
292
|
+
const Lm50 = Math.pow(Lbarp - 50, 2);
|
|
293
|
+
const SL = 1 + 0.015 * Lm50 / Math.sqrt(20 + Lm50);
|
|
294
|
+
const SC = 1 + 0.045 * Cbarp;
|
|
295
|
+
const SH = 1 + 0.015 * Cbarp * T;
|
|
296
|
+
const tL = dLp / SL, tC = dCp / SC, tH = dHp / SH;
|
|
297
|
+
return Math.sqrt(tL * tL + tC * tC + tH * tH + RT * tC * tH);
|
|
298
|
+
}
|
|
299
|
+
function labToHex(lab) {
|
|
300
|
+
const finv = (t) => {
|
|
301
|
+
const t3 = t * t * t;
|
|
302
|
+
return t3 > EPS ? t3 : (116 * t - 16) / KAPPA;
|
|
303
|
+
};
|
|
304
|
+
const fy = (lab.L + 16) / 116, fx = fy + lab.a / 500, fz = fy - lab.b / 200;
|
|
305
|
+
const X = Xn2 * finv(fx);
|
|
306
|
+
const Y = Yn2 * (lab.L > KAPPA * EPS ? fy * fy * fy : lab.L / KAPPA);
|
|
307
|
+
const Z = Zn * finv(fz);
|
|
308
|
+
const lin = [
|
|
309
|
+
3.2404542 * X - 1.5371385 * Y - 0.4985314 * Z,
|
|
310
|
+
-0.969266 * X + 1.8760108 * Y + 0.041556 * Z,
|
|
311
|
+
0.0556434 * X - 0.2040259 * Y + 1.0572252 * Z
|
|
312
|
+
];
|
|
313
|
+
return "#" + lin.map((v) => {
|
|
314
|
+
const c = Math.max(0, Math.min(1, v));
|
|
315
|
+
const g = c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
316
|
+
return Math.round(g * 255).toString(16).padStart(2, "0");
|
|
317
|
+
}).join("");
|
|
318
|
+
}
|
|
319
|
+
function deltaEHex(a, b) {
|
|
320
|
+
const la = hexToLab(a), lb = hexToLab(b);
|
|
321
|
+
return la && lb ? deltaE2000(la, lb) : NaN;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/colourSpread.ts
|
|
325
|
+
var SAME_SHADE_DELTA_E = 5;
|
|
326
|
+
var MIN_RAMP_FILLS = 4;
|
|
327
|
+
var RAMP_SLACK_MAX = 0.15;
|
|
328
|
+
var MIN_MARKS = 24;
|
|
329
|
+
var ELEMENT_CAP = 5e3;
|
|
330
|
+
var MAX_CELLS = 48;
|
|
331
|
+
var CELL_STEP = 32;
|
|
332
|
+
var EMPTY = { marks: 0, distinctFills: 0, crowdShare: 0, spanDeltaE: 0 };
|
|
333
|
+
function styleOf(el, doc) {
|
|
334
|
+
try {
|
|
335
|
+
const view = doc?.defaultView;
|
|
336
|
+
return view?.getComputedStyle ? view.getComputedStyle(el) : null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function fillOf(el, cs) {
|
|
342
|
+
const a = el.getAttribute?.("fill");
|
|
343
|
+
if (a !== null && a !== void 0 && String(a).trim() !== "") return String(a).trim();
|
|
344
|
+
const c = cs?.fill;
|
|
345
|
+
return c === null || c === void 0 ? "" : String(c).trim();
|
|
346
|
+
}
|
|
347
|
+
function toRgb(raw) {
|
|
348
|
+
const s = raw.trim().toLowerCase();
|
|
349
|
+
if (!s || s === "none" || s === "transparent") return null;
|
|
350
|
+
const hex2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(s);
|
|
351
|
+
if (hex2) {
|
|
352
|
+
const h = hex2[1];
|
|
353
|
+
const full = h.length === 3 ? h[0] + h[0] + h[1] + h[1] + h[2] + h[2] : h;
|
|
354
|
+
return [
|
|
355
|
+
parseInt(full.slice(0, 2), 16),
|
|
356
|
+
parseInt(full.slice(2, 4), 16),
|
|
357
|
+
parseInt(full.slice(4, 6), 16)
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
const m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+)\s*)?\)$/.exec(s);
|
|
361
|
+
if (!m) return null;
|
|
362
|
+
if (m[4] !== void 0 && parseFloat(m[4]) === 0) return null;
|
|
363
|
+
return [Math.round(parseFloat(m[1])), Math.round(parseFloat(m[2])), Math.round(parseFloat(m[3]))];
|
|
364
|
+
}
|
|
365
|
+
function cellKey(rgb) {
|
|
366
|
+
return [
|
|
367
|
+
Math.round(rgb[0] / CELL_STEP),
|
|
368
|
+
Math.round(rgb[1] / CELL_STEP),
|
|
369
|
+
Math.round(rgb[2] / CELL_STEP)
|
|
370
|
+
].join(",");
|
|
371
|
+
}
|
|
372
|
+
function hex(rgb) {
|
|
373
|
+
return "#" + rgb.map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
374
|
+
}
|
|
375
|
+
function censusColourSpread(container, doc) {
|
|
376
|
+
if (!container || typeof container.querySelectorAll !== "function") return EMPTY;
|
|
377
|
+
try {
|
|
378
|
+
const els = Array.from(
|
|
379
|
+
container.querySelectorAll(`.${MARK_CLASS}, [${ROW_IDX_ATTR}]`)
|
|
380
|
+
);
|
|
381
|
+
if (els.length === 0 || els.length > ELEMENT_CAP) return EMPTY;
|
|
382
|
+
const ownerDoc = doc ?? container.ownerDocument;
|
|
383
|
+
const painted = [];
|
|
384
|
+
for (const el of els) {
|
|
385
|
+
const rgb = toRgb(fillOf(el, styleOf(el, ownerDoc)));
|
|
386
|
+
if (rgb) painted.push({ rgb, tag: String(el.tagName || "").toLowerCase() });
|
|
387
|
+
}
|
|
388
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const p of painted) {
|
|
390
|
+
const g = byTag.get(p.tag) ?? { fills: /* @__PURE__ */ new Set(), n: 0 };
|
|
391
|
+
g.fills.add(p.rgb.join(","));
|
|
392
|
+
g.n++;
|
|
393
|
+
byTag.set(p.tag, g);
|
|
394
|
+
}
|
|
395
|
+
const overlays = new Set([...byTag].filter(([, g]) => g.fills.size === 1 && g.n >= MIN_MARKS).map(([t]) => t));
|
|
396
|
+
const restFills = new Set([...byTag].filter(([t]) => !overlays.has(t)).flatMap(([, g]) => [...g.fills]));
|
|
397
|
+
const counted = restFills.size > 1 ? painted.filter((p) => !overlays.has(p.tag)) : painted;
|
|
398
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
399
|
+
const cells = /* @__PURE__ */ new Map();
|
|
400
|
+
let marks = 0;
|
|
401
|
+
for (const { rgb } of counted) {
|
|
402
|
+
marks++;
|
|
403
|
+
distinct.add(rgb.join(","));
|
|
404
|
+
const k = cellKey(rgb);
|
|
405
|
+
const cur = cells.get(k);
|
|
406
|
+
if (cur) cur.n++;
|
|
407
|
+
else cells.set(k, { rgb, n: 1 });
|
|
408
|
+
}
|
|
409
|
+
if (marks < MIN_MARKS || distinct.size < MIN_RAMP_FILLS) return EMPTY;
|
|
410
|
+
const top = [...cells.values()].sort((a, b) => b.n - a.n).slice(0, MAX_CELLS);
|
|
411
|
+
const cs = top.map((c) => hexToLab(hex(c.rgb))).filter((l) => l !== null);
|
|
412
|
+
if (cs.length < MIN_RAMP_FILLS) return EMPTY;
|
|
413
|
+
const d = cs.map(() => new Array(cs.length).fill(0));
|
|
414
|
+
let span = 0, lo = 0, hi = 1;
|
|
415
|
+
for (let i = 0; i < cs.length; i++) {
|
|
416
|
+
for (let j = i + 1; j < cs.length; j++) {
|
|
417
|
+
const v = deltaE2000(cs[i], cs[j]);
|
|
418
|
+
d[i][j] = v;
|
|
419
|
+
d[j][i] = v;
|
|
420
|
+
if (v > span) {
|
|
421
|
+
span = v;
|
|
422
|
+
lo = i;
|
|
423
|
+
hi = j;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (span <= 0) return EMPTY;
|
|
428
|
+
let slack = 0, interior = 0;
|
|
429
|
+
for (let i = 0; i < cs.length; i++) {
|
|
430
|
+
if (i === lo || i === hi) continue;
|
|
431
|
+
slack += (d[lo][i] + d[i][hi]) / span - 1;
|
|
432
|
+
interior++;
|
|
433
|
+
}
|
|
434
|
+
if (interior < 2 || slack / interior > RAMP_SLACK_MAX) return EMPTY;
|
|
435
|
+
let crowd = 0;
|
|
436
|
+
for (let i = 0; i < cs.length; i++) {
|
|
437
|
+
let n = 0;
|
|
438
|
+
for (let j = 0; j < cs.length; j++) {
|
|
439
|
+
if (i === j || d[i][j] <= SAME_SHADE_DELTA_E) n += top[j].n;
|
|
440
|
+
}
|
|
441
|
+
if (n > crowd) crowd = n;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
marks,
|
|
445
|
+
distinctFills: distinct.size,
|
|
446
|
+
crowdShare: Math.round(crowd / marks * 100),
|
|
447
|
+
spanDeltaE: Math.round(span)
|
|
448
|
+
};
|
|
449
|
+
} catch {
|
|
450
|
+
return EMPTY;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function colourSpreadFlag(c) {
|
|
454
|
+
if (c.marks === 0) return "";
|
|
455
|
+
return "ramp:d3:crowd" + Math.min(80, Math.floor(c.crowdShare / 20) * 20);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/autoRamp.ts
|
|
459
|
+
var AUTO_RAMP_CLASSES = 7;
|
|
460
|
+
var AUTO_RAMP_DRAWN_OPACITY = 0.5;
|
|
461
|
+
var AUTO_RAMP_MIN_STEP_DELTA_E = 2.3;
|
|
462
|
+
var FALLBACK_ACCENT = "#3182bd";
|
|
463
|
+
var MIN_ACCENT_VS_CANVAS = 20;
|
|
464
|
+
var HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
465
|
+
function normHex(raw) {
|
|
466
|
+
if (typeof raw !== "string") return null;
|
|
467
|
+
const m = HEX.exec(raw.trim());
|
|
468
|
+
if (!m) return null;
|
|
469
|
+
const h = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
|
|
470
|
+
return "#" + h.toLowerCase();
|
|
471
|
+
}
|
|
472
|
+
function mixLab(a, b, t) {
|
|
473
|
+
return { L: a.L + (b.L - a.L) * t, a: a.a + (b.a - a.a) * t, b: a.b + (b.b - a.b) * t };
|
|
474
|
+
}
|
|
475
|
+
function paintedOver(fg, canvas, alpha) {
|
|
476
|
+
const f2 = [1, 3, 5].map((i) => parseInt(fg.slice(i, i + 2), 16));
|
|
477
|
+
const c = [1, 3, 5].map((i) => parseInt(canvas.slice(i, i + 2), 16));
|
|
478
|
+
const mixed = f2.map((v, i) => Math.round(v * alpha + c[i] * (1 - alpha))).map((v) => v.toString(16).padStart(2, "0"));
|
|
479
|
+
return hexToLab("#" + mixed.join(""));
|
|
480
|
+
}
|
|
481
|
+
function autoRampMinStep(low, high, canvas, classes = AUTO_RAMP_CLASSES, opacity = AUTO_RAMP_DRAWN_OPACITY) {
|
|
482
|
+
const lo = hexToLab(low), hi = hexToLab(high);
|
|
483
|
+
const bg = normHex(canvas);
|
|
484
|
+
if (!lo || !hi || !bg || classes < 2) return NaN;
|
|
485
|
+
let prev = null, min = Infinity;
|
|
486
|
+
for (let k = 0; k < classes; k++) {
|
|
487
|
+
const painted = paintedOver(labToHex(mixLab(lo, hi, k / (classes - 1))), bg, opacity);
|
|
488
|
+
if (prev) min = Math.min(min, deltaE2000(prev, painted));
|
|
489
|
+
prev = painted;
|
|
490
|
+
}
|
|
491
|
+
return min;
|
|
492
|
+
}
|
|
493
|
+
function autoRampCanvas(p) {
|
|
494
|
+
return normHex(p.backgroundColor) ?? normHex(p.themeBg) ?? "#ffffff";
|
|
495
|
+
}
|
|
496
|
+
var memoKey = "";
|
|
497
|
+
var memoValue = { low: "", high: "" };
|
|
498
|
+
function resolveAutoColorScale(p) {
|
|
499
|
+
const canvas = autoRampCanvas(p);
|
|
500
|
+
const palette = Array.isArray(p.palette) ? p.palette : [];
|
|
501
|
+
const key = [canvas, p.isHighContrast ? 1 : 0, p.themeAccent ?? "", p.themeFg ?? "", palette.join(",")].join("|");
|
|
502
|
+
if (key === memoKey) return memoValue;
|
|
503
|
+
const bg = hexToLab(canvas);
|
|
504
|
+
const darkCanvas = bg.L < 50;
|
|
505
|
+
const usable = (hex2) => {
|
|
506
|
+
const lab = hex2 ? hexToLab(hex2) : null;
|
|
507
|
+
return !!lab && deltaE2000(lab, bg) >= MIN_ACCENT_VS_CANVAS;
|
|
508
|
+
};
|
|
509
|
+
const candidates = p.isHighContrast ? [normHex(p.themeAccent), normHex(p.themeFg)] : [...palette.map(normHex), normHex(p.themeAccent), normHex(p.themeFg)];
|
|
510
|
+
const accent = candidates.find(usable) ?? FALLBACK_ACCENT;
|
|
511
|
+
const A = hexToLab(accent);
|
|
512
|
+
let t = 0.1;
|
|
513
|
+
let low = mixLab(bg, A, t);
|
|
514
|
+
while (t < 0.6 && deltaE2000(paintedOver(labToHex(low), canvas, AUTO_RAMP_DRAWN_OPACITY), bg) < SAME_SHADE_DELTA_E) {
|
|
515
|
+
t += 0.02;
|
|
516
|
+
low = mixLab(bg, A, t);
|
|
517
|
+
}
|
|
518
|
+
const lowHex = labToHex(low);
|
|
519
|
+
let high = { ...A };
|
|
520
|
+
let highHex = labToHex(high);
|
|
521
|
+
for (let step = 0; step < 80 && autoRampMinStep(lowHex, highHex, canvas) < AUTO_RAMP_MIN_STEP_DELTA_E; step++) {
|
|
522
|
+
const L = Math.max(4, Math.min(98, high.L + (darkCanvas ? 1 : -1)));
|
|
523
|
+
if (L === high.L) break;
|
|
524
|
+
high = { ...high, L };
|
|
525
|
+
highHex = labToHex(high);
|
|
526
|
+
}
|
|
527
|
+
memoKey = key;
|
|
528
|
+
memoValue = { low: lowHex, high: highHex };
|
|
529
|
+
return memoValue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/defaults.ts
|
|
533
|
+
function themedLand(p) {
|
|
534
|
+
const bg = p.geoLandColor ? null : toRGBA(p.backgroundColor);
|
|
535
|
+
if (!bg || bg[3] < 1 || relativeLuminance([bg[0], bg[1], bg[2]]) > 0.2) return void 0;
|
|
536
|
+
const fg = toRGBA(p.themeFg) ?? [255, 255, 255, 1];
|
|
537
|
+
return "#" + compositeOver([fg[0], fg[1], fg[2], 0.18], [bg[0], bg[1], bg[2]]).map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
538
|
+
}
|
|
539
|
+
var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
540
|
+
var numberOr = (raw, dflt) => {
|
|
541
|
+
const n = raw == null || raw === "" ? NaN : Number(raw);
|
|
542
|
+
return Number.isFinite(n) ? n : dflt;
|
|
543
|
+
};
|
|
544
|
+
function resolveOptions(p) {
|
|
545
|
+
return {
|
|
546
|
+
// ---- host fields: pass through exactly as supplied ----
|
|
547
|
+
width: p.width,
|
|
548
|
+
height: p.height,
|
|
549
|
+
palette: p.palette,
|
|
550
|
+
geoUnmatched: p.geoUnmatched,
|
|
551
|
+
geo: p.geo,
|
|
552
|
+
geoPoint: p.geoPoint,
|
|
553
|
+
// THIRD AND FOURTH OCCURRENCES OF THE animLoopDelaySec OMISSION, found together by
|
|
554
|
+
// reading the contract against this list rather than by a bug report. geoPointDest is
|
|
555
|
+
// declared on RenderOptions, passed by the visual and read by the route archetypes, and
|
|
556
|
+
// was missing here - so a chart that claims "an arc is only as honest as its worse
|
|
557
|
+
// endpoint" never saw the destination end at all. The guard test at the bottom of
|
|
558
|
+
// tests/defaults.test.ts now compares the two lists, because four instances of one
|
|
559
|
+
// omission is a missing check, not four mistakes.
|
|
560
|
+
geoPointDest: p.geoPointDest,
|
|
561
|
+
// A host FACT about the data it serialised (contract 1.9.0), passed through untouched.
|
|
562
|
+
dateCellsAreUtcDays: p.dateCellsAreUtcDays,
|
|
563
|
+
// NO LONGER A PASS-THROUGH. A host without a Max Map Points setting used to leave this
|
|
564
|
+
// undefined, and every map chart then fell back to whatever number its own code carried -
|
|
565
|
+
// 1000 in every archetype written so far - while the offer gate used the server's. One
|
|
566
|
+
// default here makes the chart and the gate agree in every host. A positive value from
|
|
567
|
+
// the host wins; absent, blank, zero or junk means the shared default.
|
|
568
|
+
maxMapPoints: (() => {
|
|
569
|
+
const n = numberOr(p.maxMapPoints, 0);
|
|
570
|
+
return n > 0 ? Math.round(n) : MAX_MAP_POINTS_DEFAULT;
|
|
571
|
+
})(),
|
|
572
|
+
// FIFTH AND SIXTH OCCURRENCES, and the two that argue hardest for the guard test: both
|
|
573
|
+
// were passed by the visual AND read by the charts, and neither was ever declared here or
|
|
574
|
+
// in the contract - so the contract-vs-whitelist check could not see them either. What
|
|
575
|
+
// found them was scanning the OTHER direction, from the caller and the charts inward.
|
|
576
|
+
// 0 is MEANINGFUL for both ("let the chart choose", "no paging"), so numberOr rather than
|
|
577
|
+
// `|| DEFAULT`, which would turn an explicit 0 into something else. Clamping stays
|
|
578
|
+
// chart-side for the same reason it does for flipIntervalMs.
|
|
579
|
+
geoPointRadiusPx: Math.max(0, numberOr(p.geoPointRadiusPx, 0)),
|
|
580
|
+
pageSize: Math.max(0, numberOr(p.pageSize, 0)),
|
|
581
|
+
uiState: p.uiState,
|
|
582
|
+
setUiState: p.setUiState,
|
|
583
|
+
backgroundColor: p.backgroundColor,
|
|
584
|
+
themeBg: p.themeBg,
|
|
585
|
+
themeFg: p.themeFg,
|
|
586
|
+
isHighContrast: p.isHighContrast,
|
|
587
|
+
themeAccent: p.themeAccent,
|
|
588
|
+
cultureCode: p.cultureCode,
|
|
589
|
+
allowTooltips: p.allowTooltips,
|
|
590
|
+
noDataText: p.noDataText,
|
|
591
|
+
onSelect: p.onSelect,
|
|
592
|
+
// Control gestures (1.10.0). Passed straight through like onSelect: the host owns what a gesture MEANS,
|
|
593
|
+
// and a host that supplies nothing leaves every control behaving exactly as before.
|
|
594
|
+
onControlChange: p.onControlChange,
|
|
595
|
+
// ---- live-restyle knobs: `raw || undefined` / `.toString() || undefined` ----
|
|
596
|
+
colorScaleLow: p.colorScaleLow || void 0,
|
|
597
|
+
colorScaleHigh: p.colorScaleHigh || void 0,
|
|
598
|
+
// DERIVED, never passed: the automatic ends a blank picker stands for, resolved from the
|
|
599
|
+
// same palette and canvas the chart is handed on this render (autoRamp.ts).
|
|
600
|
+
colorScaleAutoLow: resolveAutoColorScale(p).low,
|
|
601
|
+
colorScaleAutoHigh: resolveAutoColorScale(p).high,
|
|
602
|
+
// "" (global, the default), "frame", or "self"; anything else fails open to global.
|
|
603
|
+
colorScaleScope: p.colorScaleScope === "frame" || p.colorScaleScope === "self" ? p.colorScaleScope : "",
|
|
604
|
+
// Math.max(50, Math.min(100, Number(raw) || 95)) — only meaningful under "self".
|
|
605
|
+
colorScaleSelfClampPct: clamp(Number(p.colorScaleSelfClampPct) || COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT, COLOR_SCALE_SELF_CLAMP_PCT_MIN, COLOR_SCALE_SELF_CLAMP_PCT_MAX),
|
|
606
|
+
// Map fills: `raw || undefined` like the colour-scale endpoints. The no-data ->
|
|
607
|
+
// land cascade is left to the chart (so the fallback chain is visible in the
|
|
608
|
+
// generated code); HC substitution has already happened in the caller.
|
|
609
|
+
geoLandColor: p.geoLandColor || themedLand(p) || void 0,
|
|
610
|
+
geoNoDataColor: p.geoNoDataColor || void 0,
|
|
611
|
+
aggregation: (p.aggregation == null ? "" : String(p.aggregation)) || void 0,
|
|
612
|
+
// A recognised value wins, anything else falls to the default: an unknown string must
|
|
613
|
+
// never reach a chart as a live mode. Both of these are read on EVERY render, so a
|
|
614
|
+
// reader can change their mind about a cached chart without regenerating it.
|
|
615
|
+
approximatePositions: (() => {
|
|
616
|
+
const v = (p.approximatePositions == null ? "" : String(p.approximatePositions)).toLowerCase();
|
|
617
|
+
return v === "mark" || v === "aggregate" || v === "skip" ? v : APPROXIMATE_POSITIONS_DEFAULT;
|
|
618
|
+
})(),
|
|
619
|
+
valueAxisBaseline: (() => {
|
|
620
|
+
const v = (p.valueAxisBaseline == null ? "" : String(p.valueAxisBaseline)).toLowerCase();
|
|
621
|
+
return v === "zero" || v === "fit" || v === "auto" ? v : VALUE_AXIS_BASELINE_DEFAULT;
|
|
622
|
+
})(),
|
|
623
|
+
// Same shape, and the same reason for it: an unrecognised string must never reach a chart
|
|
624
|
+
// as a live mode. Failing open to "auto" is the conservative direction here — "auto" is
|
|
625
|
+
// the setting that makes the chart PROVE the grid before it names a period, so a garbled
|
|
626
|
+
// value withdraws the claim rather than asserting one.
|
|
627
|
+
seasonalMarkers: (() => {
|
|
628
|
+
const v = (p.seasonalMarkers == null ? "" : String(p.seasonalMarkers)).toLowerCase();
|
|
629
|
+
return v === "auto" || v === "always" || v === "never" ? v : SEASONAL_MARKERS_DEFAULT;
|
|
630
|
+
})(),
|
|
631
|
+
// ---- animation: booleans, default+clamp numerics, "" default enum ----
|
|
632
|
+
animAutoPlay: !!p.animAutoPlay,
|
|
633
|
+
// Math.max(250, Math.min(5000, Number(raw) || 1000))
|
|
634
|
+
animPlaySpeedMs: clamp(Number(p.animPlaySpeedMs) || ANIM_PLAY_SPEED_DEFAULT, ANIM_PLAY_SPEED_MIN, ANIM_PLAY_SPEED_MAX),
|
|
635
|
+
// Loop-restart delay (seconds on the overview between auto-play passes).
|
|
636
|
+
// Floored at 0, deliberately NO upper clamp (huge = play once per viewing),
|
|
637
|
+
// and 0 must survive as 0 — hence numberOr, not `|| DEFAULT`. This field was
|
|
638
|
+
// MISSING from the resolve list when the assembly moved here, so callers
|
|
639
|
+
// that passed it saw it dropped: the scrubber read undefined -> 0 -> every
|
|
640
|
+
// pass restarted instantly and resumed-state auto-play lost its pacing.
|
|
641
|
+
animLoopDelaySec: Math.max(ANIM_LOOP_DELAY_MIN, numberOr(p.animLoopDelaySec, ANIM_LOOP_DELAY_DEFAULT)),
|
|
642
|
+
// Math.max(3, Math.min(500, Number(raw) || 60))
|
|
643
|
+
animMaxIdealFrames: clamp(Number(p.animMaxIdealFrames) || ANIM_MAX_IDEAL_FRAMES_DEFAULT, ANIM_MAX_IDEAL_FRAMES_MIN, ANIM_MAX_IDEAL_FRAMES_MAX),
|
|
644
|
+
animStopAtEnd: !!p.animStopAtEnd,
|
|
645
|
+
filtersDuringPlay: !!p.filtersDuringPlay,
|
|
646
|
+
animTimelineStyle: (p.animTimelineStyle || "").toString(),
|
|
647
|
+
// ---- card deck ----
|
|
648
|
+
// SECOND OCCURRENCE OF THE animLoopDelaySec BUG, six lines above (2026-08-13). The
|
|
649
|
+
// visual passed both of these, the generated code read both correctly, and this
|
|
650
|
+
// function — a whitelist that returns a NEW object — dropped them in between. Symptoms
|
|
651
|
+
// were exactly what the two defaults produce: flipSync undefined -> `!== false` -> true
|
|
652
|
+
// -> always synced, so "Flip Cards Together = Off" did nothing and the deck kept its
|
|
653
|
+
// shared strip; flipIntervalMs undefined -> `+(undefined || 0)` -> 0 -> the timer branch
|
|
654
|
+
// never ran, so a 1000 ms interval produced no flips. Two dead knobs, one omission.
|
|
655
|
+
//
|
|
656
|
+
// flipSync DEFAULTS TRUE, so it cannot use `!!p.flipSync` — that would read a missing
|
|
657
|
+
// value as "independent" and hand every deck twelve control strips. Absent means
|
|
658
|
+
// synced; only an explicit false switches it.
|
|
659
|
+
flipSync: p.flipSync === void 0 || p.flipSync === null ? true : !!p.flipSync,
|
|
660
|
+
// WHERE the controls live, which is a different question from whether the cards move
|
|
661
|
+
// together (2026-08-13: "the ability to flip individual cards shouldn't necessarily
|
|
662
|
+
// stop being able to flip all together too"). Resolution order matters and is the whole
|
|
663
|
+
// back-compat story:
|
|
664
|
+
// 1. an explicit, recognised flipMode wins;
|
|
665
|
+
// 2. otherwise DERIVE from flipSync, so a chart generated before this option existed
|
|
666
|
+
// behaves exactly as it did - true -> "all", false -> "single";
|
|
667
|
+
// 3. and a caller that sends neither gets the default.
|
|
668
|
+
// Deriving rather than defaulting is the point: a cached chart must not change behaviour
|
|
669
|
+
// because a newer client shipped, and "both" would have done exactly that to every deck
|
|
670
|
+
// already in a report.
|
|
671
|
+
flipMode: (() => {
|
|
672
|
+
const m = (p.flipMode == null ? "" : String(p.flipMode)).toLowerCase();
|
|
673
|
+
if (m === "single" || m === "all" || m === "both") return m;
|
|
674
|
+
if (p.flipSync === false) return "single";
|
|
675
|
+
if (p.flipSync === true) return "all";
|
|
676
|
+
return FLIP_MODE_DEFAULT;
|
|
677
|
+
})(),
|
|
678
|
+
// WHICH WAY A FACE TURNS. Anything unrecognised - including absent, which is every host
|
|
679
|
+
// that predates the option - is the default, side to side, which is what every chart that
|
|
680
|
+
// turns did before it existed.
|
|
681
|
+
flipDirection: (() => {
|
|
682
|
+
const d = (p.flipDirection == null ? "" : String(p.flipDirection)).toLowerCase();
|
|
683
|
+
return d === "horizontal" || d === "vertical" ? d : FLIP_DIRECTION_DEFAULT;
|
|
684
|
+
})(),
|
|
685
|
+
// 0 is MEANINGFUL here (manual only), so numberOr — not `|| DEFAULT` — for the same
|
|
686
|
+
// reason animLoopDelaySec uses it. UNSET STAYS UNSET (undefined), because the default is
|
|
687
|
+
// the CHART's, not this function's: the card deck reads `+(x || 0)` and is manual, a
|
|
688
|
+
// carousel reads `x == null ? 5000 : +x` and turns by itself. Resolving unset to 0 here
|
|
689
|
+
// made the carousel's default unreachable - every host handed it an explicit "manual".
|
|
690
|
+
// Clamping stays chart-side too: the archetypes clamp 500..120000, and two clamps in two
|
|
691
|
+
// repositories is how they end up disagreeing.
|
|
692
|
+
flipIntervalMs: (() => {
|
|
693
|
+
const n = numberOr(p.flipIntervalMs, NaN);
|
|
694
|
+
return Number.isFinite(n) ? Math.max(0, n) : void 0;
|
|
695
|
+
})(),
|
|
696
|
+
// `raw || undefined`, the same idiom as the colour-scale endpoints and the map fills:
|
|
697
|
+
// blank must arrive as ABSENT, not as an empty string, because the chart's fallback is
|
|
698
|
+
// `options.cardBackgroundColor || <its own choice>` and "" would satisfy a truthiness
|
|
699
|
+
// test in some hands while painting nothing in others.
|
|
700
|
+
cardBackgroundColor: p.cardBackgroundColor || void 0,
|
|
701
|
+
// A reader's on / off, or UNSET - never filled with false, for the flipIntervalMs reason: the
|
|
702
|
+
// default is the chart's (on only where its request asked for drop lines), and a host that
|
|
703
|
+
// turned unset into false would make that default unreachable. Pane choices arrive as text.
|
|
704
|
+
hoverDropLines: (() => {
|
|
705
|
+
const v = p.hoverDropLines;
|
|
706
|
+
if (typeof v === "boolean") return v;
|
|
707
|
+
const s = v == null ? "" : String(v).trim().toLowerCase();
|
|
708
|
+
return s === "on" || s === "true" || s === "1" ? true : s === "off" || s === "false" || s === "0" ? false : void 0;
|
|
709
|
+
})()
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/geoLazy.ts
|
|
714
|
+
var byKind = /* @__PURE__ */ new Map();
|
|
715
|
+
var byAsset = /* @__PURE__ */ new Map();
|
|
716
|
+
var _na;
|
|
717
|
+
function geoAssetFor(geoKind) {
|
|
718
|
+
if (!geoKind) return null;
|
|
719
|
+
const k = geoKind.toLowerCase();
|
|
720
|
+
if (/^country/.test(k)) return "world";
|
|
721
|
+
if (k === "north-america") return "world";
|
|
722
|
+
if (k === "world") return "world";
|
|
723
|
+
if (k === "us-state-code" || k === "us-state-name") return "us-states";
|
|
724
|
+
if (k === "us-zip5") return "us-zip3";
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
var _worldBase;
|
|
728
|
+
function worldBasemap(world) {
|
|
729
|
+
if (!_worldBase) {
|
|
730
|
+
_worldBase = {
|
|
731
|
+
type: "FeatureCollection",
|
|
732
|
+
features: world.features.filter((f2) => f2?.geometry && f2.geometry.type !== "Point")
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return _worldBase;
|
|
736
|
+
}
|
|
737
|
+
function northAmerica(world) {
|
|
738
|
+
if (!_na) {
|
|
739
|
+
_na = {
|
|
740
|
+
type: "FeatureCollection",
|
|
741
|
+
features: world.features.filter((f2) => NA_ISO3.has(f2?.id) && f2?.geometry && f2.geometry.type !== "Point").map((f2) => clipFeatureToLat(f2, NA_LAT_MAX)).filter((f2) => f2 !== null)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return _na;
|
|
745
|
+
}
|
|
746
|
+
function fromAssets(key) {
|
|
747
|
+
const asset = geoAssetFor(key);
|
|
748
|
+
if (!asset) return void 0;
|
|
749
|
+
const geo = byAsset.get(asset);
|
|
750
|
+
if (!geo) return void 0;
|
|
751
|
+
if (key === "north-america") return northAmerica(geo);
|
|
752
|
+
if (key === "world") return worldBasemap(geo);
|
|
753
|
+
return geo;
|
|
754
|
+
}
|
|
755
|
+
async function loadGeo(geoKind) {
|
|
756
|
+
if (!geoKind) return void 0;
|
|
757
|
+
const key = geoKind.toLowerCase();
|
|
758
|
+
const already = geoFromCache(key);
|
|
759
|
+
if (already) return already;
|
|
760
|
+
const asset = geoAssetFor(key);
|
|
761
|
+
if (!asset) return void 0;
|
|
762
|
+
if (asset === "us-states") {
|
|
763
|
+
byAsset.set(asset, (await import("./geoUsStates.generated-JEFM6OHO.mjs")).US_STATES);
|
|
764
|
+
} else if (asset === "us-zip3") {
|
|
765
|
+
byAsset.set(asset, (await import("./geoUsZip3.generated-VSWL7D6F.mjs")).US_ZIP3);
|
|
766
|
+
} else {
|
|
767
|
+
byAsset.set(asset, (await import("./geoWorld110m.generated-TD2TA7S7.mjs")).WORLD_110M);
|
|
768
|
+
}
|
|
769
|
+
return fromAssets(key);
|
|
770
|
+
}
|
|
771
|
+
function geoFromCache(geoKind) {
|
|
772
|
+
if (!geoKind) return void 0;
|
|
773
|
+
const key = geoKind.toLowerCase();
|
|
774
|
+
return byKind.get(key) ?? fromAssets(key);
|
|
775
|
+
}
|
|
776
|
+
function registerGeo(geoKind, geo) {
|
|
777
|
+
if (geoKind && geo) byKind.set(geoKind.toLowerCase(), geo);
|
|
778
|
+
}
|
|
779
|
+
function registerGeoAsset(asset, geo) {
|
|
780
|
+
if (!asset || !geo) return;
|
|
781
|
+
byAsset.set(asset, geo);
|
|
782
|
+
if (asset === "world") {
|
|
783
|
+
_na = void 0;
|
|
784
|
+
_worldBase = void 0;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function clearGeoCache() {
|
|
788
|
+
byKind.clear();
|
|
789
|
+
byAsset.clear();
|
|
790
|
+
_na = void 0;
|
|
791
|
+
_worldBase = void 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/payload.ts
|
|
795
|
+
var ROW_IDX_COLUMN = "__rowIdx__";
|
|
796
|
+
function resolvePointChannel(cols, rowObjs, point) {
|
|
797
|
+
let pLat = null;
|
|
798
|
+
let pLon = null;
|
|
799
|
+
let pPrec = null;
|
|
800
|
+
let geoPoint;
|
|
801
|
+
let bind = point ?? null;
|
|
802
|
+
let rolesBackfilled = [];
|
|
803
|
+
let rolesRefused = [];
|
|
804
|
+
let labelCol;
|
|
805
|
+
if (bind) {
|
|
806
|
+
const dims = cols.filter((c) => !c.isMeasure).map((c) => c.name);
|
|
807
|
+
const res = Xn(dims, rowObjs, bind, point?.mapKind, cols.map((c) => c.name));
|
|
808
|
+
bind = res.bind;
|
|
809
|
+
rolesBackfilled = res.backfilled;
|
|
810
|
+
rolesRefused = res.refused;
|
|
811
|
+
const lf = res.labelFallback;
|
|
812
|
+
labelCol = lf && (lf.country || lf.city || lf.state || lf.zip);
|
|
813
|
+
}
|
|
814
|
+
const hasPointBind = !!bind && !!(bind.city || bind.state || bind.zip || bind.country || bind.lat && bind.lon);
|
|
815
|
+
if (hasPointBind) {
|
|
816
|
+
const p = bind;
|
|
817
|
+
const built = J3(rowObjs.map((r) => ({
|
|
818
|
+
lat: p.lat ? r[p.lat] : null,
|
|
819
|
+
lon: p.lon ? r[p.lon] : null,
|
|
820
|
+
city: p.city ? r[p.city] : null,
|
|
821
|
+
state: p.state ? r[p.state] : null,
|
|
822
|
+
zip: p.zip ? r[p.zip] : null,
|
|
823
|
+
country: p.country ? r[p.country] : null,
|
|
824
|
+
label: labelCol ? r[labelCol] : null
|
|
825
|
+
})), point?.mapKind);
|
|
826
|
+
pLat = built.lat;
|
|
827
|
+
pLon = built.lon;
|
|
828
|
+
pPrec = built.precisions;
|
|
829
|
+
geoPoint = {
|
|
830
|
+
precision: built.precision,
|
|
831
|
+
precisionCounts: built.precisionCounts,
|
|
832
|
+
coarseExamples: built.coarseExamples,
|
|
833
|
+
// STRICTLY no-match. Ambiguous rows are deliberately NOT in here: new charts
|
|
834
|
+
// report them from ambiguousRows/ambiguousExamples, while OLD generated code
|
|
835
|
+
// still shows them — it computes its own off-map count from null coordinates
|
|
836
|
+
// and takes max(offMap, unplaced), so the not-shown total stays right without
|
|
837
|
+
// double-counting on either side of the upgrade.
|
|
838
|
+
unplaced: built.totalRows - built.matchedRows - built.ambiguousRows,
|
|
839
|
+
unplacedExamples: built.unmatched.slice(0, 8),
|
|
840
|
+
ambiguousRows: built.ambiguousRows,
|
|
841
|
+
...built.ambiguousExamples.length ? { ambiguousExamples: built.ambiguousExamples } : {},
|
|
842
|
+
// Omitted entirely when nothing was adjusted, so the common case adds no bytes
|
|
843
|
+
// and a chart can treat presence as "something about this binding is worth
|
|
844
|
+
// saying".
|
|
845
|
+
...rolesBackfilled.length ? { rolesBackfilled } : {},
|
|
846
|
+
...rolesRefused.length ? { rolesRefused } : {}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return { lat: pLat, lon: pLon, prec: pPrec, geoPoint, rolesRefused };
|
|
850
|
+
}
|
|
851
|
+
function buildRenderPayload(cols, rowObjs, geo, point, destination, opts) {
|
|
852
|
+
cols = cols.filter((c) => c.name !== ROW_IDX_COLUMN);
|
|
853
|
+
let utcDayColumns = null;
|
|
854
|
+
if (opts?.utcDays) {
|
|
855
|
+
const norm = ge(cols, rowObjs);
|
|
856
|
+
rowObjs = norm.rows;
|
|
857
|
+
utcDayColumns = norm.columns;
|
|
858
|
+
}
|
|
859
|
+
const o = resolvePointChannel(cols, rowObjs, point);
|
|
860
|
+
const d = resolvePointChannel(cols, rowObjs, destination);
|
|
861
|
+
const pLat = o.lat, pLon = o.lon, pPrec = o.prec;
|
|
862
|
+
const dLat = d.lat, dLon = d.lon, dPrec = d.prec;
|
|
863
|
+
const geoPoint = o.geoPoint;
|
|
864
|
+
const geoPointDest = d.geoPoint;
|
|
865
|
+
const rolesRefused = [...o.rolesRefused, ...d.rolesRefused.map((s) => `destination: ${s}`)];
|
|
866
|
+
let geoIso = null;
|
|
867
|
+
let geoUnmatched;
|
|
868
|
+
let geoUnmatchedDistinct;
|
|
869
|
+
if (geo && geo.column && geo.kind && Yn(geo.kind)) {
|
|
870
|
+
const built = Fn(rowObjs.map((r) => r[geo.column]), geo.kind);
|
|
871
|
+
geoIso = built.iso;
|
|
872
|
+
geoUnmatched = { count: built.totalRows - built.matchedRows, examples: built.unmatched.slice(0, 8) };
|
|
873
|
+
geoUnmatchedDistinct = built.unmatched.length;
|
|
874
|
+
}
|
|
875
|
+
const hostNames = [ROW_IDX_COLUMN];
|
|
876
|
+
if (geoIso) hostNames.push("__geoIso__");
|
|
877
|
+
if (pLat) hostNames.push("__geoLat__", "__geoLon__", "__geoPrecision__");
|
|
878
|
+
if (dLat) hostNames.push("__geoLatD__", "__geoLonD__", "__geoPrecisionD__");
|
|
879
|
+
const owned = new Set(hostNames);
|
|
880
|
+
if (cols.some((c) => owned.has(c.name))) cols = cols.filter((c) => !owned.has(c.name));
|
|
881
|
+
const colNames = cols.map((c) => c.name);
|
|
882
|
+
const aliasCols = [];
|
|
883
|
+
if (opts?.aliases && opts.aliases.length > 0) {
|
|
884
|
+
const claimed = /* @__PURE__ */ new Set([...colNames, ...hostNames]);
|
|
885
|
+
for (const a of opts.aliases) {
|
|
886
|
+
const from = a ? colNames.indexOf(a.name) : -1;
|
|
887
|
+
if (from < 0 || !a.alias || claimed.has(a.alias)) continue;
|
|
888
|
+
claimed.add(a.alias);
|
|
889
|
+
aliasCols.push({ from, name: a.alias });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const extra = hostNames.length + aliasCols.length;
|
|
893
|
+
const out = new Array(rowObjs.length);
|
|
894
|
+
for (let r = 0; r < rowObjs.length; r++) {
|
|
895
|
+
const row = rowObjs[r];
|
|
896
|
+
const a = new Array(cols.length + extra);
|
|
897
|
+
for (let c = 0; c < cols.length; c++) {
|
|
898
|
+
const v = row[colNames[c]];
|
|
899
|
+
if (v instanceof Date) {
|
|
900
|
+
a[c] = v.toISOString();
|
|
901
|
+
} else {
|
|
902
|
+
a[c] = v ?? null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
let k = cols.length;
|
|
906
|
+
a[k++] = r;
|
|
907
|
+
if (geoIso) a[k++] = geoIso[r];
|
|
908
|
+
if (pLat && pLon) {
|
|
909
|
+
a[k++] = pLat[r];
|
|
910
|
+
a[k++] = pLon[r];
|
|
911
|
+
a[k++] = pPrec ? pPrec[r] : null;
|
|
912
|
+
}
|
|
913
|
+
if (dLat && dLon) {
|
|
914
|
+
a[k++] = dLat[r];
|
|
915
|
+
a[k++] = dLon[r];
|
|
916
|
+
a[k++] = dPrec ? dPrec[r] : null;
|
|
917
|
+
}
|
|
918
|
+
for (const ac of aliasCols) a[k++] = a[ac.from];
|
|
919
|
+
out[r] = a;
|
|
920
|
+
}
|
|
921
|
+
const colsOut = [...cols, { name: ROW_IDX_COLUMN, dataType: "Integer", isMeasure: false, modelDesc: "" }];
|
|
922
|
+
if (geoIso) colsOut.push({ name: "__geoIso__", dataType: "String", isMeasure: false, modelDesc: "" });
|
|
923
|
+
if (pLat) colsOut.push(
|
|
924
|
+
{ name: "__geoLat__", dataType: "Double", isMeasure: false, modelDesc: "" },
|
|
925
|
+
{ name: "__geoLon__", dataType: "Double", isMeasure: false, modelDesc: "" },
|
|
926
|
+
// Host metadata like the two above: which TIER placed this row ("latlon" | "city" |
|
|
927
|
+
// "zip3" | "state" | "country"), null when it was not placed. Never a dimension,
|
|
928
|
+
// never a label, never a colour grouping.
|
|
929
|
+
{ name: "__geoPrecision__", dataType: "String", isMeasure: false, modelDesc: "" }
|
|
930
|
+
);
|
|
931
|
+
if (dLat) colsOut.push(
|
|
932
|
+
{ name: "__geoLatD__", dataType: "Double", isMeasure: false, modelDesc: "" },
|
|
933
|
+
{ name: "__geoLonD__", dataType: "Double", isMeasure: false, modelDesc: "" },
|
|
934
|
+
{ name: "__geoPrecisionD__", dataType: "String", isMeasure: false, modelDesc: "" }
|
|
935
|
+
);
|
|
936
|
+
for (const ac of aliasCols) colsOut.push({ ...cols[ac.from], name: ac.name, aliasOf: cols[ac.from].name });
|
|
937
|
+
return {
|
|
938
|
+
columns: colsOut,
|
|
939
|
+
rows: out,
|
|
940
|
+
geoUnmatched,
|
|
941
|
+
geoUnmatchedDistinct,
|
|
942
|
+
geoPoint,
|
|
943
|
+
// Omitted when there is no second endpoint, so every existing chart's options object
|
|
944
|
+
// is unchanged rather than gaining an undefined key.
|
|
945
|
+
...geoPointDest ? { geoPointDest } : {},
|
|
946
|
+
// Survives a refusal that placed nothing — see geoPointRefused. Omitted when empty so
|
|
947
|
+
// the ordinary case adds no bytes.
|
|
948
|
+
...rolesRefused.length ? { geoPointRefused: rolesRefused } : {},
|
|
949
|
+
...utcDayColumns ? { dateCellsAreUtcDays: true, utcDayColumns } : {}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/codeComments.ts
|
|
954
|
+
function stripJsComments(code) {
|
|
955
|
+
const src = String(code ?? "");
|
|
956
|
+
const n = src.length;
|
|
957
|
+
let out = "";
|
|
958
|
+
let i = 0;
|
|
959
|
+
const templates = [];
|
|
960
|
+
let prevSig = "";
|
|
961
|
+
let prevWord = "";
|
|
962
|
+
let wordOpen = false;
|
|
963
|
+
const regexMayFollow = () => {
|
|
964
|
+
if (prevSig === "") return true;
|
|
965
|
+
if ("(,=:[!&|?{};+-*%<>~^".includes(prevSig)) return true;
|
|
966
|
+
return /^(return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/.test(prevWord);
|
|
967
|
+
};
|
|
968
|
+
while (i < n) {
|
|
969
|
+
const ch = src[i];
|
|
970
|
+
const next = i + 1 < n ? src[i + 1] : "";
|
|
971
|
+
const top = templates.length > 0 ? templates[templates.length - 1] : null;
|
|
972
|
+
if (top === -1) {
|
|
973
|
+
if (ch === "\\") {
|
|
974
|
+
out += src.slice(i, i + 2);
|
|
975
|
+
i += 2;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (ch === "`") {
|
|
979
|
+
templates.pop();
|
|
980
|
+
out += ch;
|
|
981
|
+
i++;
|
|
982
|
+
prevSig = "`";
|
|
983
|
+
prevWord = "";
|
|
984
|
+
wordOpen = false;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (ch === "$" && next === "{") {
|
|
988
|
+
templates[templates.length - 1] = 0;
|
|
989
|
+
out += "${";
|
|
990
|
+
i += 2;
|
|
991
|
+
prevSig = "{";
|
|
992
|
+
prevWord = "";
|
|
993
|
+
wordOpen = false;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
out += ch;
|
|
997
|
+
i++;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (ch === "/" && next === "/") {
|
|
1001
|
+
let j = i + 2;
|
|
1002
|
+
while (j < n && src[j] !== "\n" && src[j] !== "\r") j++;
|
|
1003
|
+
out += " ";
|
|
1004
|
+
i = j;
|
|
1005
|
+
wordOpen = false;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (ch === "/" && next === "*") {
|
|
1009
|
+
const end = src.indexOf("*/", i + 2);
|
|
1010
|
+
const body = end < 0 ? src.slice(i + 2) : src.slice(i + 2, end);
|
|
1011
|
+
const newlines = body.match(/\r\n|\r|\n/g);
|
|
1012
|
+
out += newlines ? newlines.join("") : " ";
|
|
1013
|
+
i = end < 0 ? n : end + 2;
|
|
1014
|
+
wordOpen = false;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (ch === "'" || ch === '"') {
|
|
1018
|
+
let j = i + 1;
|
|
1019
|
+
while (j < n && src[j] !== ch && src[j] !== "\n") j += src[j] === "\\" ? 2 : 1;
|
|
1020
|
+
out += src.slice(i, Math.min(j + 1, n));
|
|
1021
|
+
i = j + 1;
|
|
1022
|
+
prevSig = ch;
|
|
1023
|
+
prevWord = "";
|
|
1024
|
+
wordOpen = false;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (ch === "`") {
|
|
1028
|
+
templates.push(-1);
|
|
1029
|
+
out += ch;
|
|
1030
|
+
i++;
|
|
1031
|
+
wordOpen = false;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (ch === "/" && regexMayFollow()) {
|
|
1035
|
+
let j = i + 1, inClass = false;
|
|
1036
|
+
while (j < n && src[j] !== "\n") {
|
|
1037
|
+
const c = src[j];
|
|
1038
|
+
if (c === "\\") {
|
|
1039
|
+
j += 2;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (c === "[") inClass = true;
|
|
1043
|
+
else if (c === "]") inClass = false;
|
|
1044
|
+
else if (c === "/" && !inClass) break;
|
|
1045
|
+
j++;
|
|
1046
|
+
}
|
|
1047
|
+
if (j < n && src[j] === "/") {
|
|
1048
|
+
j++;
|
|
1049
|
+
while (j < n && /[A-Za-z]/.test(src[j])) j++;
|
|
1050
|
+
out += src.slice(i, j);
|
|
1051
|
+
i = j;
|
|
1052
|
+
prevSig = "/";
|
|
1053
|
+
prevWord = "";
|
|
1054
|
+
wordOpen = false;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (top !== null && top >= 0) {
|
|
1059
|
+
if (ch === "{") templates[templates.length - 1] = top + 1;
|
|
1060
|
+
else if (ch === "}") {
|
|
1061
|
+
if (top === 0) {
|
|
1062
|
+
templates[templates.length - 1] = -1;
|
|
1063
|
+
out += ch;
|
|
1064
|
+
i++;
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
templates[templates.length - 1] = top - 1;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
out += ch;
|
|
1071
|
+
i++;
|
|
1072
|
+
if (/\s/.test(ch)) {
|
|
1073
|
+
wordOpen = false;
|
|
1074
|
+
} else {
|
|
1075
|
+
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
1076
|
+
prevWord = wordOpen ? prevWord + ch : ch;
|
|
1077
|
+
wordOpen = true;
|
|
1078
|
+
} else {
|
|
1079
|
+
prevWord = "";
|
|
1080
|
+
wordOpen = false;
|
|
1081
|
+
}
|
|
1082
|
+
prevSig = ch;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return out;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/blankRender.ts
|
|
1089
|
+
var EMPTY2 = { markCount: 0, legendMarkCount: 0, containerKind: "empty", declaresNoRowMarks: false };
|
|
1090
|
+
function censusMarks(container) {
|
|
1091
|
+
if (!container || typeof container.querySelectorAll !== "function") return EMPTY2;
|
|
1092
|
+
try {
|
|
1093
|
+
const marks = /* @__PURE__ */ new Set();
|
|
1094
|
+
for (const el of Array.from(container.querySelectorAll(`.${MARK_CLASS}`))) marks.add(el);
|
|
1095
|
+
for (const el of Array.from(container.querySelectorAll(`[${ROW_IDX_ATTR}]`))) marks.add(el);
|
|
1096
|
+
const legend = /* @__PURE__ */ new Set();
|
|
1097
|
+
for (const el of Array.from(container.querySelectorAll(`.${LEGEND_MARK_CLASS}`))) {
|
|
1098
|
+
legend.add(el);
|
|
1099
|
+
marks.delete(el);
|
|
1100
|
+
}
|
|
1101
|
+
const hasSvg = !!container.querySelector("svg");
|
|
1102
|
+
const hasAnything = hasSvg || (container.childElementCount ?? 0) > 0;
|
|
1103
|
+
return {
|
|
1104
|
+
markCount: marks.size,
|
|
1105
|
+
legendMarkCount: legend.size,
|
|
1106
|
+
containerKind: hasSvg ? "svg" : hasAnything ? "html" : "empty",
|
|
1107
|
+
declaresNoRowMarks: !!container.querySelector(`[${NO_ROW_MARKS_ATTR}]`)
|
|
1108
|
+
};
|
|
1109
|
+
} catch {
|
|
1110
|
+
return EMPTY2;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function isBlankRender(i) {
|
|
1114
|
+
if (i.contractUntagged) return false;
|
|
1115
|
+
if (i.declaresNoRowMarks) return false;
|
|
1116
|
+
if (i.animated) return false;
|
|
1117
|
+
if (i.authoredNoDataText) return false;
|
|
1118
|
+
if (!(i.rows > 0)) return false;
|
|
1119
|
+
return i.markCount === 0;
|
|
1120
|
+
}
|
|
1121
|
+
function blankRenderFlag(lang, cause) {
|
|
1122
|
+
const clean = (s) => String(s || "unknown").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
1123
|
+
return `blankrender:${clean(lang)}:${clean(cause)}`;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/clippedText.ts
|
|
1127
|
+
var CLIP_TOLERANCE_PX = 1;
|
|
1128
|
+
var MAX_REPORTED = 8;
|
|
1129
|
+
function censusClippedText(container) {
|
|
1130
|
+
const none = { measured: 0, clipped: [], clippedCount: 0, laidOut: false };
|
|
1131
|
+
try {
|
|
1132
|
+
const svg = container?.querySelector?.("svg");
|
|
1133
|
+
if (!svg) return none;
|
|
1134
|
+
const f2 = svg.getBoundingClientRect();
|
|
1135
|
+
if (!f2 || !(f2.width > 0) || !(f2.height > 0)) return none;
|
|
1136
|
+
const view = svg.ownerDocument?.defaultView;
|
|
1137
|
+
const clipped = [];
|
|
1138
|
+
let measured = 0;
|
|
1139
|
+
for (const t of Array.from(svg.querySelectorAll("text"))) {
|
|
1140
|
+
const r = t.getBoundingClientRect();
|
|
1141
|
+
if (!r || !(r.width > 0) || !(r.height > 0)) continue;
|
|
1142
|
+
const s = view?.getComputedStyle?.(t);
|
|
1143
|
+
if (s && (s.display === "none" || s.visibility === "hidden" || s.opacity === "0")) continue;
|
|
1144
|
+
measured++;
|
|
1145
|
+
const top = f2.top - r.top, left = f2.left - r.left, bottom = r.bottom - f2.bottom, right = r.right - f2.right;
|
|
1146
|
+
if (top > CLIP_TOLERANCE_PX || left > CLIP_TOLERANCE_PX || bottom > CLIP_TOLERANCE_PX || right > CLIP_TOLERANCE_PX) {
|
|
1147
|
+
clipped.push({
|
|
1148
|
+
text: String(t.textContent ?? "").slice(0, 40),
|
|
1149
|
+
top: Math.max(0, Math.round(top)),
|
|
1150
|
+
left: Math.max(0, Math.round(left)),
|
|
1151
|
+
bottom: Math.max(0, Math.round(bottom)),
|
|
1152
|
+
right: Math.max(0, Math.round(right))
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
clipped.sort((a, b) => Math.max(b.top, b.left, b.bottom, b.right) - Math.max(a.top, a.left, a.bottom, a.right));
|
|
1157
|
+
return { measured, clipped: clipped.slice(0, MAX_REPORTED), clippedCount: clipped.length, laidOut: true };
|
|
1158
|
+
} catch {
|
|
1159
|
+
return none;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function clippedTextFlag(c) {
|
|
1163
|
+
if (!c.laidOut || c.clippedCount === 0) return "";
|
|
1164
|
+
const worst = c.clipped[0];
|
|
1165
|
+
const side = ["top", "left", "bottom", "right"].reduce((a, k) => worst[k] > worst[a] ? k : a, "top");
|
|
1166
|
+
return `${c.clippedCount} of ${c.measured} text label(s) are cut off by the frame; the worst, "${worst.text}", by ${worst[side]}px at the ${side}.`;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/valuePlacement.ts
|
|
1170
|
+
var MIN_MARKS2 = 24;
|
|
1171
|
+
var ELEMENT_CAP2 = 5e3;
|
|
1172
|
+
var FIT_SAMPLE = 90;
|
|
1173
|
+
var VALUE_AXIS_MIN_FIT = 0.8;
|
|
1174
|
+
var MIN_AXIS_SPAN_PX = 40;
|
|
1175
|
+
var EMPTY3 = { marks: 0, axis: "", column: "", fit: 0, offShare: 0, medianOffRadii: 0 };
|
|
1176
|
+
function num(el, name) {
|
|
1177
|
+
const v = el.getAttribute?.(name);
|
|
1178
|
+
if (v === null || v === void 0 || String(v).trim() === "") return NaN;
|
|
1179
|
+
return parseFloat(String(v));
|
|
1180
|
+
}
|
|
1181
|
+
function median(xs) {
|
|
1182
|
+
if (!xs.length) return NaN;
|
|
1183
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
1184
|
+
const m = s.length >> 1;
|
|
1185
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
1186
|
+
}
|
|
1187
|
+
function robustLine(v, pos) {
|
|
1188
|
+
const n = v.length;
|
|
1189
|
+
const step = Math.max(1, Math.floor(n / FIT_SAMPLE));
|
|
1190
|
+
const idx = [];
|
|
1191
|
+
for (let i = 0; i < n; i += step) idx.push(i);
|
|
1192
|
+
const slopes = [];
|
|
1193
|
+
for (let p = 0; p < idx.length; p++) {
|
|
1194
|
+
for (let q = p + 1; q < idx.length; q++) {
|
|
1195
|
+
const dv = v[idx[q]] - v[idx[p]];
|
|
1196
|
+
if (Math.abs(dv) < 1e-12) continue;
|
|
1197
|
+
slopes.push((pos[idx[q]] - pos[idx[p]]) / dv);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (slopes.length < 3) return null;
|
|
1201
|
+
const b = median(slopes);
|
|
1202
|
+
if (!Number.isFinite(b) || Math.abs(b) < 1e-12) return null;
|
|
1203
|
+
const a = median(pos.map((y, i) => y - b * v[i]));
|
|
1204
|
+
return Number.isFinite(a) ? { a, b } : null;
|
|
1205
|
+
}
|
|
1206
|
+
function censusValuePlacement(container, data) {
|
|
1207
|
+
try {
|
|
1208
|
+
if (!container || typeof container.querySelectorAll !== "function") return EMPTY3;
|
|
1209
|
+
const rows = data?.rows;
|
|
1210
|
+
const columns = data?.columns ?? [];
|
|
1211
|
+
if (!Array.isArray(rows) || !rows.length || !columns.length) return EMPTY3;
|
|
1212
|
+
const els = Array.from(container.querySelectorAll(`circle[${ROW_IDX_ATTR}]`));
|
|
1213
|
+
if (els.length < MIN_MARKS2 || els.length > ELEMENT_CAP2) return EMPTY3;
|
|
1214
|
+
const dots = [];
|
|
1215
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1216
|
+
for (const el of els) {
|
|
1217
|
+
const raw = String(el.getAttribute(ROW_IDX_ATTR) ?? "").trim();
|
|
1218
|
+
if (!/^\d+$/.test(raw)) continue;
|
|
1219
|
+
const i = +raw;
|
|
1220
|
+
const row = rows[i];
|
|
1221
|
+
const x = num(el, "cx"), y = num(el, "cy"), r = num(el, "r");
|
|
1222
|
+
if (!row || !Number.isFinite(x) || !Number.isFinite(y) || !(r > 0)) continue;
|
|
1223
|
+
seen.add(i);
|
|
1224
|
+
dots.push({ row, x, y, r });
|
|
1225
|
+
}
|
|
1226
|
+
if (dots.length < MIN_MARKS2 || seen.size < dots.length * 0.9) return EMPTY3;
|
|
1227
|
+
let best = null;
|
|
1228
|
+
for (let c = 0; c < columns.length; c++) {
|
|
1229
|
+
if (/^__.*__$/.test(String(columns[c]?.name ?? ""))) continue;
|
|
1230
|
+
const vals = dots.map((d) => {
|
|
1231
|
+
const v = d.row[c];
|
|
1232
|
+
return v === null || v === void 0 || v === "" ? NaN : typeof v === "number" ? v : Number(v);
|
|
1233
|
+
});
|
|
1234
|
+
const finite = vals.filter(Number.isFinite);
|
|
1235
|
+
if (finite.length < dots.length * 0.9 || new Set(finite).size < 8) continue;
|
|
1236
|
+
const logs = finite.every((v) => v > 0) ? [false, true] : [false];
|
|
1237
|
+
for (const log of logs) {
|
|
1238
|
+
for (const axis of ["x", "y"]) {
|
|
1239
|
+
const keep = [];
|
|
1240
|
+
const v = [], pos = [];
|
|
1241
|
+
dots.forEach((d, k) => {
|
|
1242
|
+
if (!Number.isFinite(vals[k])) return;
|
|
1243
|
+
keep.push(k);
|
|
1244
|
+
v.push(log ? Math.log10(vals[k]) : vals[k]);
|
|
1245
|
+
pos.push(axis === "x" ? d.x : d.y);
|
|
1246
|
+
});
|
|
1247
|
+
const line = robustLine(v, pos);
|
|
1248
|
+
if (!line) continue;
|
|
1249
|
+
const lo = Math.min(...pos), hi = Math.max(...pos);
|
|
1250
|
+
if (hi - lo < MIN_AXIS_SPAN_PX) continue;
|
|
1251
|
+
const mean = pos.reduce((s, p) => s + p, 0) / pos.length;
|
|
1252
|
+
let ssRes = 0, ssTot = 0;
|
|
1253
|
+
const res = pos.map((p, k) => {
|
|
1254
|
+
const e = p - (line.a + line.b * v[k]);
|
|
1255
|
+
ssRes += e * e;
|
|
1256
|
+
ssTot += (p - mean) ** 2;
|
|
1257
|
+
return e;
|
|
1258
|
+
});
|
|
1259
|
+
if (ssTot <= 0) continue;
|
|
1260
|
+
const fit = 1 - ssRes / ssTot;
|
|
1261
|
+
if (fit >= VALUE_AXIS_MIN_FIT && (!best || fit > best.fit)) {
|
|
1262
|
+
const full = new Array(dots.length).fill(NaN);
|
|
1263
|
+
keep.forEach((k, j) => {
|
|
1264
|
+
full[k] = res[j];
|
|
1265
|
+
});
|
|
1266
|
+
best = { col: c, axis, fit, res: full, log };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (!best) return EMPTY3;
|
|
1272
|
+
let off = 0, measured = 0;
|
|
1273
|
+
const radii = [];
|
|
1274
|
+
dots.forEach((d, k) => {
|
|
1275
|
+
const e = best.res[k];
|
|
1276
|
+
if (!Number.isFinite(e)) return;
|
|
1277
|
+
measured++;
|
|
1278
|
+
const inRadii = Math.abs(e) / Math.max(d.r, 1);
|
|
1279
|
+
radii.push(inRadii);
|
|
1280
|
+
if (inRadii > 1) off++;
|
|
1281
|
+
});
|
|
1282
|
+
if (measured < MIN_MARKS2) return EMPTY3;
|
|
1283
|
+
return {
|
|
1284
|
+
marks: measured,
|
|
1285
|
+
axis: best.axis,
|
|
1286
|
+
column: String(columns[best.col]?.name ?? ""),
|
|
1287
|
+
fit: Math.round(best.fit * 1e3) / 1e3,
|
|
1288
|
+
offShare: Math.round(off / measured * 1e3) / 10,
|
|
1289
|
+
medianOffRadii: Math.round(median(radii) * 100) / 100
|
|
1290
|
+
};
|
|
1291
|
+
} catch {
|
|
1292
|
+
return EMPTY3;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function valuePlacementFlag(c) {
|
|
1296
|
+
if (c.marks === 0) return "";
|
|
1297
|
+
const s = c.offShare;
|
|
1298
|
+
const bucket = s <= 0 ? 0 : s < 5 ? 1 : s < 20 ? 5 : s < 50 ? 20 : 50;
|
|
1299
|
+
return "place:d3:off" + bucket;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/fit.ts
|
|
1303
|
+
var SCROLL_SLACK_PX = 8;
|
|
1304
|
+
function needsScroll(contentPx, viewportPx, slackPx = SCROLL_SLACK_PX) {
|
|
1305
|
+
if (!isFinite(contentPx) || !isFinite(viewportPx)) return false;
|
|
1306
|
+
if (contentPx <= 0 || viewportPx <= 0) return false;
|
|
1307
|
+
return contentPx - viewportPx > slackPx;
|
|
1308
|
+
}
|
|
1309
|
+
function contentExtentOf(boxes) {
|
|
1310
|
+
if (!boxes || boxes.length === 0) return { w: 0, h: 0, source: "none" };
|
|
1311
|
+
const usable = boxes.filter((b) => b && isFinite(b.right) && isFinite(b.bottom) && (b.right > 0 || b.bottom > 0));
|
|
1312
|
+
if (usable.length === 0) return { w: 0, h: 0, source: "none" };
|
|
1313
|
+
const inFlow = usable.filter((b) => !b.floating);
|
|
1314
|
+
const counted = inFlow.length > 0 ? inFlow : usable;
|
|
1315
|
+
let w = 0, h = 0;
|
|
1316
|
+
for (const b of counted) {
|
|
1317
|
+
if (b.right > w) w = b.right;
|
|
1318
|
+
if (b.bottom > h) h = b.bottom;
|
|
1319
|
+
}
|
|
1320
|
+
return { w, h, source: inFlow.length > 0 ? "in-flow" : "all-floating" };
|
|
1321
|
+
}
|
|
1322
|
+
function scrollFitFor(contentW, contentH, viewportW, viewportH, slackPx = SCROLL_SLACK_PX) {
|
|
1323
|
+
return {
|
|
1324
|
+
overflowX: needsScroll(contentW, viewportW, slackPx) ? "auto" : "hidden",
|
|
1325
|
+
overflowY: needsScroll(contentH, viewportH, slackPx) ? "auto" : "hidden"
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
var FIT_CONTENT_SELECTOR = "text, .d3-mark, .d3-legend-mark";
|
|
1329
|
+
var PHANTOM_FRACTION = 0.95;
|
|
1330
|
+
function isPhantomBox(w, h, refW, refH) {
|
|
1331
|
+
return w > refW * PHANTOM_FRACTION || h > refH * PHANTOM_FRACTION;
|
|
1332
|
+
}
|
|
1333
|
+
var MAX_FRAME_GROW_FACTOR = 20;
|
|
1334
|
+
function planFrameGrow(inkBottomPx, elHeightPx, viewBoxH, ctmScale, slackPx = SCROLL_SLACK_PX, maxFactor = MAX_FRAME_GROW_FACTOR) {
|
|
1335
|
+
const none = (reason) => ({ grow: false, heightPx: elHeightPx, viewBoxH, reason });
|
|
1336
|
+
if (!isFinite(inkBottomPx) || !isFinite(elHeightPx) || elHeightPx <= 0) return none("unmeasurable");
|
|
1337
|
+
if (!needsScroll(inkBottomPx, elHeightPx, slackPx)) return none("fits");
|
|
1338
|
+
if (inkBottomPx > elHeightPx * maxFactor) return none("beyond-ceiling");
|
|
1339
|
+
const heightPx = Math.ceil(inkBottomPx);
|
|
1340
|
+
const delta = heightPx - elHeightPx;
|
|
1341
|
+
if (!(viewBoxH > 0)) return { grow: true, heightPx, viewBoxH, reason: "grow-no-viewbox" };
|
|
1342
|
+
if (!(ctmScale > 0) || !isFinite(ctmScale)) return none("no-scale");
|
|
1343
|
+
return { grow: true, heightPx, viewBoxH: viewBoxH + delta / ctmScale, reason: "grow" };
|
|
1344
|
+
}
|
|
1345
|
+
var AXIS_PIN_MIN_LABELS = 2;
|
|
1346
|
+
var AXIS_PIN_BAND_PAD_PX = 2;
|
|
1347
|
+
var AXIS_PIN_MAX_BAND_FRACTION = 0.4;
|
|
1348
|
+
var AXIS_PIN_TRACK_REACH_PX = 12;
|
|
1349
|
+
function isHorizontalLabelRow(boxes) {
|
|
1350
|
+
if (!boxes || boxes.length < 2) return false;
|
|
1351
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1352
|
+
for (const b of boxes) {
|
|
1353
|
+
const cx = (b.left + b.right) / 2, cy = (b.top + b.bottom) / 2;
|
|
1354
|
+
if (!isFinite(cx) || !isFinite(cy)) return false;
|
|
1355
|
+
if (cx < minX) minX = cx;
|
|
1356
|
+
if (cx > maxX) maxX = cx;
|
|
1357
|
+
if (cy < minY) minY = cy;
|
|
1358
|
+
if (cy > maxY) maxY = cy;
|
|
1359
|
+
}
|
|
1360
|
+
return maxX - minX > maxY - minY;
|
|
1361
|
+
}
|
|
1362
|
+
function labelBand(boxes, tracks = [], pad = AXIS_PIN_BAND_PAD_PX, trackReach = AXIS_PIN_TRACK_REACH_PX) {
|
|
1363
|
+
if (!boxes || boxes.length === 0) return null;
|
|
1364
|
+
let top = Infinity, bottom = -Infinity;
|
|
1365
|
+
for (const b of boxes) {
|
|
1366
|
+
if (!isFinite(b.top) || !isFinite(b.bottom)) continue;
|
|
1367
|
+
if (b.top < top) top = b.top;
|
|
1368
|
+
if (b.bottom > bottom) bottom = b.bottom;
|
|
1369
|
+
}
|
|
1370
|
+
if (!isFinite(top) || !isFinite(bottom) || bottom <= top) return null;
|
|
1371
|
+
for (const t of tracks || []) {
|
|
1372
|
+
if (!isFinite(t.top) || !isFinite(t.bottom)) continue;
|
|
1373
|
+
const gap = t.bottom < top ? top - t.bottom : t.top > bottom ? t.top - bottom : 0;
|
|
1374
|
+
if (gap > trackReach) continue;
|
|
1375
|
+
if (t.top < top) top = t.top;
|
|
1376
|
+
if (t.bottom > bottom) bottom = t.bottom;
|
|
1377
|
+
}
|
|
1378
|
+
return { top: top - pad, bottom: bottom + pad };
|
|
1379
|
+
}
|
|
1380
|
+
function planAxisPin(candidates, viewportH, minLabels = AXIS_PIN_MIN_LABELS, maxFraction = AXIS_PIN_MAX_BAND_FRACTION) {
|
|
1381
|
+
const none = (reason) => ({ pin: false, index: -1, band: null, reason });
|
|
1382
|
+
if (!isFinite(viewportH) || viewportH <= 0) return none("unmeasurable");
|
|
1383
|
+
let sawAxis = false, best = null;
|
|
1384
|
+
for (const c of candidates || []) {
|
|
1385
|
+
if (!c || !c.labels || c.labels.length < minLabels) continue;
|
|
1386
|
+
sawAxis = true;
|
|
1387
|
+
if (!isHorizontalLabelRow(c.labels)) continue;
|
|
1388
|
+
if (!best || c.labels.length > best.labels.length) best = c;
|
|
1389
|
+
}
|
|
1390
|
+
if (!best) return none(sawAxis ? "vertical-only" : "no-axis");
|
|
1391
|
+
const band = labelBand(best.labels, best.tracks || []);
|
|
1392
|
+
if (!band) return none("unmeasurable");
|
|
1393
|
+
if (band.bottom - band.top > viewportH * maxFraction) return none("band-too-tall");
|
|
1394
|
+
return { pin: true, index: best.index, band, reason: "pinned" };
|
|
1395
|
+
}
|
|
1396
|
+
function axisPinPlacement(band, scrollTop, viewportH) {
|
|
1397
|
+
if (!band || !isFinite(band.top) || !isFinite(band.bottom)) return null;
|
|
1398
|
+
if (!isFinite(scrollTop) || !isFinite(viewportH) || viewportH <= 0) return null;
|
|
1399
|
+
if (band.top < scrollTop) return "top";
|
|
1400
|
+
if (band.bottom > scrollTop + viewportH) return "bottom";
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/fitDom.ts
|
|
1405
|
+
var FIT_WALK_CAP = 8e3;
|
|
1406
|
+
function ctmScaleOf(m) {
|
|
1407
|
+
if (!m) return 0;
|
|
1408
|
+
const s = Math.hypot(m.a, m.b);
|
|
1409
|
+
return isFinite(s) && s > 0 ? s : 0;
|
|
1410
|
+
}
|
|
1411
|
+
function svgInkReach(svg) {
|
|
1412
|
+
try {
|
|
1413
|
+
const box2 = svg.getBoundingClientRect();
|
|
1414
|
+
if (!(box2.width > 0) || !(box2.height > 0)) return null;
|
|
1415
|
+
const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
|
|
1416
|
+
const clipOf = clipWindowReader(svg);
|
|
1417
|
+
let right = -Infinity, bottom = -Infinity, seen = 0;
|
|
1418
|
+
for (let i = 0; i < els.length && seen < FIT_WALK_CAP; i++) {
|
|
1419
|
+
const r = els[i].getBoundingClientRect();
|
|
1420
|
+
if (r.width <= 0 && r.height <= 0) continue;
|
|
1421
|
+
if (isPhantomBox(r.width, r.height, box2.width, box2.height)) continue;
|
|
1422
|
+
let rr = r.right, rb = r.bottom;
|
|
1423
|
+
const clip = clipOf(els[i]);
|
|
1424
|
+
if (clip) {
|
|
1425
|
+
if (r.left >= clip.right || r.top >= clip.bottom) continue;
|
|
1426
|
+
rr = Math.min(rr, clip.right);
|
|
1427
|
+
rb = Math.min(rb, clip.bottom);
|
|
1428
|
+
}
|
|
1429
|
+
seen++;
|
|
1430
|
+
if (rr > right) right = rr;
|
|
1431
|
+
if (rb > bottom) bottom = rb;
|
|
1432
|
+
}
|
|
1433
|
+
if (seen === 0) return null;
|
|
1434
|
+
return { right: right - box2.left, bottom: bottom - box2.top };
|
|
1435
|
+
} catch {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function clipWindowOf(el) {
|
|
1440
|
+
try {
|
|
1441
|
+
const m = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(el.getAttribute("clip-path") || "");
|
|
1442
|
+
const cp = m ? (el.closest("svg") ?? el.ownerDocument).querySelector(`clipPath[id="${m[1].replace(/"/g, '\\"')}"]`) : null;
|
|
1443
|
+
const ctm = cp ? el.getScreenCTM?.() : null;
|
|
1444
|
+
if (!cp || !ctm || cp.getAttribute("clipPathUnits") === "objectBoundingBox") return null;
|
|
1445
|
+
let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
|
|
1446
|
+
for (let i = 0; i < cp.children.length; i++) {
|
|
1447
|
+
const bb = cp.children[i].getBBox?.();
|
|
1448
|
+
if (!bb || !(bb.width > 0) || !(bb.height > 0)) continue;
|
|
1449
|
+
for (const [x, y] of [[bb.x, bb.y], [bb.x + bb.width, bb.y], [bb.x, bb.y + bb.height], [bb.x + bb.width, bb.y + bb.height]]) {
|
|
1450
|
+
const sx = ctm.a * x + ctm.c * y + ctm.e, sy = ctm.b * x + ctm.d * y + ctm.f;
|
|
1451
|
+
l = Math.min(l, sx);
|
|
1452
|
+
t = Math.min(t, sy);
|
|
1453
|
+
r = Math.max(r, sx);
|
|
1454
|
+
b = Math.max(b, sy);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return isFinite(r) && isFinite(b) ? { left: l, top: t, right: r, bottom: b } : null;
|
|
1458
|
+
} catch {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function clipWindowReader(svg) {
|
|
1463
|
+
const own = /* @__PURE__ */ new Map();
|
|
1464
|
+
const windowOf = (el) => {
|
|
1465
|
+
if (!own.has(el)) own.set(el, clipWindowOf(el));
|
|
1466
|
+
return own.get(el);
|
|
1467
|
+
};
|
|
1468
|
+
return (el) => {
|
|
1469
|
+
let acc = null;
|
|
1470
|
+
for (let a = el; a && a !== svg; a = a.parentElement) {
|
|
1471
|
+
const w = windowOf(a);
|
|
1472
|
+
if (w) acc = acc ? { right: Math.min(acc.right, w.right), bottom: Math.min(acc.bottom, w.bottom) } : w;
|
|
1473
|
+
}
|
|
1474
|
+
return acc;
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function measureContainerBoxes(c, inkOf) {
|
|
1478
|
+
const boxes = [];
|
|
1479
|
+
if (!c || !c.children) return boxes;
|
|
1480
|
+
const view = c.ownerDocument && c.ownerDocument.defaultView || globalThis;
|
|
1481
|
+
const cRect = c.getBoundingClientRect();
|
|
1482
|
+
for (let i = 0; i < c.children.length; i++) {
|
|
1483
|
+
const el = c.children[i];
|
|
1484
|
+
let floating = false;
|
|
1485
|
+
try {
|
|
1486
|
+
const cs = view.getComputedStyle(el);
|
|
1487
|
+
if (cs.display === "none") continue;
|
|
1488
|
+
floating = cs.position === "absolute" || cs.position === "fixed";
|
|
1489
|
+
} catch {
|
|
1490
|
+
}
|
|
1491
|
+
const r = el.getBoundingClientRect();
|
|
1492
|
+
let right = r.right - cRect.left - c.clientLeft + c.scrollLeft;
|
|
1493
|
+
let bottom = r.bottom - cRect.top - c.clientTop + c.scrollTop;
|
|
1494
|
+
if (inkOf) {
|
|
1495
|
+
try {
|
|
1496
|
+
const ink = inkOf(el);
|
|
1497
|
+
if (ink) {
|
|
1498
|
+
const originX = r.left - cRect.left - c.clientLeft + c.scrollLeft;
|
|
1499
|
+
const originY = r.top - cRect.top - c.clientTop + c.scrollTop;
|
|
1500
|
+
if (isFinite(ink.right)) right = Math.max(right, originX + ink.right);
|
|
1501
|
+
if (isFinite(ink.bottom)) bottom = Math.max(bottom, originY + ink.bottom);
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
boxes.push({ right, bottom, floating });
|
|
1507
|
+
}
|
|
1508
|
+
return boxes;
|
|
1509
|
+
}
|
|
1510
|
+
function fitReadingFor(lane, c, slackPx = SCROLL_SLACK_PX, inkOf) {
|
|
1511
|
+
if (!c) return null;
|
|
1512
|
+
const viewW = c.clientWidth, viewH = c.clientHeight;
|
|
1513
|
+
if (!(viewW > 0) || !(viewH > 0)) return null;
|
|
1514
|
+
const boxes = measureContainerBoxes(c, inkOf);
|
|
1515
|
+
const extent = contentExtentOf(boxes);
|
|
1516
|
+
const contentW = extent.source === "none" ? c.scrollWidth : extent.w;
|
|
1517
|
+
const contentH = extent.source === "none" ? c.scrollHeight : extent.h;
|
|
1518
|
+
if (!isFinite(contentW) || !isFinite(contentH)) return null;
|
|
1519
|
+
return {
|
|
1520
|
+
lane,
|
|
1521
|
+
contentW: Math.round(contentW),
|
|
1522
|
+
contentH: Math.round(contentH),
|
|
1523
|
+
viewW,
|
|
1524
|
+
viewH,
|
|
1525
|
+
overflowsX: needsScroll(contentW, viewW, slackPx),
|
|
1526
|
+
overflowsY: needsScroll(contentH, viewH, slackPx),
|
|
1527
|
+
extentSource: extent.source,
|
|
1528
|
+
boxes: boxes.length,
|
|
1529
|
+
floating: boxes.filter((b) => b.floating).length,
|
|
1530
|
+
scrollW: c.scrollWidth,
|
|
1531
|
+
scrollH: c.scrollHeight
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
var noPin = (reason) => ({ pinned: false, reason, bandPx: 0, axisAt: "", labels: 0, carried: 0, edge: null });
|
|
1535
|
+
var CONTAINER_SLOT_AXIS_PIN = "__bicAxisPin";
|
|
1536
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
1537
|
+
var AXIS_PIN_MAX_CARRIED = 12;
|
|
1538
|
+
function unpinScrolledAxis(container) {
|
|
1539
|
+
if (!container) return;
|
|
1540
|
+
const st = container[CONTAINER_SLOT_AXIS_PIN];
|
|
1541
|
+
if (!st) return;
|
|
1542
|
+
try {
|
|
1543
|
+
container.removeEventListener("scroll", st.onScroll);
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
if (st.overlay.parentNode) st.overlay.parentNode.removeChild(st.overlay);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
delete container[CONTAINER_SLOT_AXIS_PIN];
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function textHidden(t, view) {
|
|
1556
|
+
if (t.getAttribute("display") === "none" || t.getAttribute("visibility") === "hidden") return true;
|
|
1557
|
+
try {
|
|
1558
|
+
const cs = view.getComputedStyle(t);
|
|
1559
|
+
if (cs && (cs.display === "none" || cs.visibility === "hidden")) return true;
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
function backdropFill(svg, container, view, svgBox) {
|
|
1565
|
+
try {
|
|
1566
|
+
const rects = svg.querySelectorAll("rect");
|
|
1567
|
+
for (let i = 0; i < rects.length && i < 6; i++) {
|
|
1568
|
+
const r = rects[i];
|
|
1569
|
+
if (r.closest(".d3-mark")) continue;
|
|
1570
|
+
const b = r.getBoundingClientRect();
|
|
1571
|
+
if (b.width >= svgBox.width * 0.9 && b.height >= svgBox.height * 0.9) {
|
|
1572
|
+
let fill = "";
|
|
1573
|
+
try {
|
|
1574
|
+
fill = view.getComputedStyle(r).fill || "";
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
if (!fill || fill === "none") fill = r.getAttribute("fill") || "";
|
|
1578
|
+
if (fill && fill !== "none" && fill !== "transparent") return fill;
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
let el = container;
|
|
1586
|
+
for (let hops = 0; el && hops < 12; hops++, el = el.parentElement) {
|
|
1587
|
+
const bg = view.getComputedStyle(el).backgroundColor;
|
|
1588
|
+
if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") return bg;
|
|
1589
|
+
}
|
|
1590
|
+
} catch {
|
|
1591
|
+
}
|
|
1592
|
+
return "#ffffff";
|
|
1593
|
+
}
|
|
1594
|
+
function cloneWithChain(el, svg, root) {
|
|
1595
|
+
const chain = [];
|
|
1596
|
+
let n = el.parentElement;
|
|
1597
|
+
while (n && n !== svg) {
|
|
1598
|
+
chain.unshift(n);
|
|
1599
|
+
n = n.parentElement;
|
|
1600
|
+
}
|
|
1601
|
+
let parent = root;
|
|
1602
|
+
for (const anc of chain) {
|
|
1603
|
+
const c = anc.cloneNode(false);
|
|
1604
|
+
parent.appendChild(c);
|
|
1605
|
+
parent = c;
|
|
1606
|
+
}
|
|
1607
|
+
parent.appendChild(el.cloneNode(true));
|
|
1608
|
+
const ids = root.querySelectorAll("[id]");
|
|
1609
|
+
for (let i = 0; i < ids.length; i++) ids[i].removeAttribute("id");
|
|
1610
|
+
}
|
|
1611
|
+
function pinScrolledAxis(container, svg) {
|
|
1612
|
+
unpinScrolledAxis(container);
|
|
1613
|
+
if (!container) return noPin("no-container");
|
|
1614
|
+
if (!svg) return noPin("no-svg");
|
|
1615
|
+
try {
|
|
1616
|
+
const doc = svg.ownerDocument;
|
|
1617
|
+
const view = doc && doc.defaultView || globalThis;
|
|
1618
|
+
const svgBox = svg.getBoundingClientRect();
|
|
1619
|
+
if (!(svgBox.width > 0) || !(svgBox.height > 0)) return noPin("unmeasurable");
|
|
1620
|
+
const m = typeof svg.getScreenCTM === "function" ? svg.getScreenCTM() : null;
|
|
1621
|
+
if (!m || !(m.a > 0) || !(m.d > 0) || !isFinite(m.e) || !isFinite(m.f)) return noPin("no-ctm");
|
|
1622
|
+
const viewportH = container.clientHeight;
|
|
1623
|
+
if (!(viewportH > 0)) return noPin("unmeasurable");
|
|
1624
|
+
const rel = (r) => ({ left: r.left - svgBox.left, top: r.top - svgBox.top, right: r.right - svgBox.left, bottom: r.bottom - svgBox.top });
|
|
1625
|
+
const ticks = svg.querySelectorAll(".tick");
|
|
1626
|
+
const byAxis = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (let i = 0; i < ticks.length; i++) {
|
|
1628
|
+
const t = ticks[i];
|
|
1629
|
+
if (t.closest("svg") !== svg) continue;
|
|
1630
|
+
const p = t.parentElement;
|
|
1631
|
+
if (!p) continue;
|
|
1632
|
+
const list = byAxis.get(p);
|
|
1633
|
+
if (list) list.push(t);
|
|
1634
|
+
else byAxis.set(p, [t]);
|
|
1635
|
+
}
|
|
1636
|
+
const axes = [];
|
|
1637
|
+
const cands = [];
|
|
1638
|
+
byAxis.forEach((group, axis2) => {
|
|
1639
|
+
const labels = [];
|
|
1640
|
+
for (const tick of group) {
|
|
1641
|
+
const txt = tick.querySelector("text");
|
|
1642
|
+
if (!txt || textHidden(txt, view)) continue;
|
|
1643
|
+
const r = txt.getBoundingClientRect();
|
|
1644
|
+
if (!(r.width > 0) || !(r.height > 0)) continue;
|
|
1645
|
+
labels.push(rel(r));
|
|
1646
|
+
}
|
|
1647
|
+
const tracks = [];
|
|
1648
|
+
for (let i = 0; i < axis2.children.length; i++) {
|
|
1649
|
+
const ch = axis2.children[i];
|
|
1650
|
+
if (ch.tagName.toLowerCase() !== "path" || !(ch.getAttribute("class") || "").split(/\s+/).includes("domain")) continue;
|
|
1651
|
+
const r = ch.getBoundingClientRect();
|
|
1652
|
+
if (r.width > 0 && r.height <= 4) tracks.push(rel(r));
|
|
1653
|
+
}
|
|
1654
|
+
cands.push({ index: axes.length, labels, tracks });
|
|
1655
|
+
axes.push(axis2);
|
|
1656
|
+
});
|
|
1657
|
+
const plan = planAxisPin(cands, viewportH);
|
|
1658
|
+
if (!plan.pin || !plan.band) return noPin(plan.reason);
|
|
1659
|
+
const axis = axes[plan.index];
|
|
1660
|
+
const band = { top: Math.max(0, plan.band.top), bottom: Math.min(svgBox.height, plan.band.bottom) };
|
|
1661
|
+
const bandPx = band.bottom - band.top;
|
|
1662
|
+
if (!(bandPx > 0)) return noPin("unmeasurable");
|
|
1663
|
+
const carried = [];
|
|
1664
|
+
const texts = svg.querySelectorAll("text");
|
|
1665
|
+
for (let i = 0; i < texts.length && carried.length < AXIS_PIN_MAX_CARRIED; i++) {
|
|
1666
|
+
const t = texts[i];
|
|
1667
|
+
if (t.closest("svg") !== svg) continue;
|
|
1668
|
+
if (t.closest(".tick") || t.closest(".d3-mark") || t.closest(".d3-legend-mark")) continue;
|
|
1669
|
+
if (axis.contains(t) || textHidden(t, view)) continue;
|
|
1670
|
+
const r = rel(t.getBoundingClientRect());
|
|
1671
|
+
if (!(r.right > r.left) || !(r.bottom > r.top)) continue;
|
|
1672
|
+
if (r.top >= band.top && r.bottom <= band.bottom) carried.push(t);
|
|
1673
|
+
}
|
|
1674
|
+
const pinSvg = doc.createElementNS(SVG_NS, "svg");
|
|
1675
|
+
for (const a of ["font-family", "font-size", "font-weight", "font", "fill", "color", "text-rendering"]) {
|
|
1676
|
+
const v = svg.getAttribute(a);
|
|
1677
|
+
if (v) pinSvg.setAttribute(a, v);
|
|
1678
|
+
}
|
|
1679
|
+
const styles = svg.querySelectorAll("style");
|
|
1680
|
+
for (let i = 0; i < styles.length; i++) pinSvg.appendChild(styles[i].cloneNode(true));
|
|
1681
|
+
const vbX = (svgBox.left - m.e) / m.a, vbY = (svgBox.top + band.top - m.f) / m.d;
|
|
1682
|
+
const vbW = svgBox.width / m.a, vbH = bandPx / m.d;
|
|
1683
|
+
pinSvg.setAttribute("viewBox", `${vbX} ${vbY} ${vbW} ${vbH}`);
|
|
1684
|
+
pinSvg.setAttribute("preserveAspectRatio", "none");
|
|
1685
|
+
pinSvg.setAttribute("width", String(svgBox.width));
|
|
1686
|
+
pinSvg.setAttribute("height", String(bandPx));
|
|
1687
|
+
pinSvg.setAttribute("aria-hidden", "true");
|
|
1688
|
+
pinSvg.style.display = "block";
|
|
1689
|
+
pinSvg.style.pointerEvents = "none";
|
|
1690
|
+
const back = doc.createElementNS(SVG_NS, "rect");
|
|
1691
|
+
back.setAttribute("x", String(vbX));
|
|
1692
|
+
back.setAttribute("y", String(vbY));
|
|
1693
|
+
back.setAttribute("width", String(vbW));
|
|
1694
|
+
back.setAttribute("height", String(vbH));
|
|
1695
|
+
back.setAttribute("fill", backdropFill(svg, container, view, svgBox));
|
|
1696
|
+
pinSvg.appendChild(back);
|
|
1697
|
+
cloneWithChain(axis, svg, pinSvg);
|
|
1698
|
+
for (const t of carried) cloneWithChain(t, svg, pinSvg);
|
|
1699
|
+
const overlay = doc.createElement("div");
|
|
1700
|
+
overlay.setAttribute("data-bic-axis-pin", "1");
|
|
1701
|
+
const o = overlay.style;
|
|
1702
|
+
o.position = "absolute";
|
|
1703
|
+
o.margin = "0";
|
|
1704
|
+
o.padding = "0";
|
|
1705
|
+
o.border = "0";
|
|
1706
|
+
o.width = `${svgBox.width}px`;
|
|
1707
|
+
o.height = `${bandPx}px`;
|
|
1708
|
+
o.overflow = "hidden";
|
|
1709
|
+
o.pointerEvents = "none";
|
|
1710
|
+
o.zIndex = "3";
|
|
1711
|
+
o.display = "none";
|
|
1712
|
+
overlay.appendChild(pinSvg);
|
|
1713
|
+
try {
|
|
1714
|
+
const cs = view.getComputedStyle(container);
|
|
1715
|
+
const pos = cs && cs.position || container.style.position;
|
|
1716
|
+
if (!pos || pos === "static") container.style.position = "relative";
|
|
1717
|
+
} catch {
|
|
1718
|
+
container.style.position = container.style.position || "relative";
|
|
1719
|
+
}
|
|
1720
|
+
const cRect = container.getBoundingClientRect();
|
|
1721
|
+
const svgLeft = svgBox.left - cRect.left - container.clientLeft + container.scrollLeft;
|
|
1722
|
+
const svgTop = svgBox.top - cRect.top - container.clientTop + container.scrollTop;
|
|
1723
|
+
const contentBand = { top: svgTop + band.top, bottom: svgTop + band.bottom };
|
|
1724
|
+
const place = () => {
|
|
1725
|
+
const st = container.scrollTop, vh = container.clientHeight;
|
|
1726
|
+
const edge2 = axisPinPlacement(contentBand, st, vh);
|
|
1727
|
+
if (!edge2) {
|
|
1728
|
+
o.display = "none";
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
o.display = "block";
|
|
1732
|
+
o.left = `${svgLeft}px`;
|
|
1733
|
+
o.top = `${edge2 === "top" ? st : st + vh - bandPx}px`;
|
|
1734
|
+
return edge2;
|
|
1735
|
+
};
|
|
1736
|
+
const onScroll = () => {
|
|
1737
|
+
try {
|
|
1738
|
+
if (!overlay.isConnected) {
|
|
1739
|
+
unpinScrolledAxis(container);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
place();
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
container.appendChild(overlay);
|
|
1747
|
+
container.addEventListener("scroll", onScroll, { passive: true });
|
|
1748
|
+
container[CONTAINER_SLOT_AXIS_PIN] = { overlay, onScroll };
|
|
1749
|
+
const edge = place();
|
|
1750
|
+
const mid = (band.top + band.bottom) / 2 / svgBox.height;
|
|
1751
|
+
return {
|
|
1752
|
+
pinned: true,
|
|
1753
|
+
reason: "pinned",
|
|
1754
|
+
bandPx: Math.round(bandPx),
|
|
1755
|
+
axisAt: mid < 1 / 3 ? "top" : mid > 2 / 3 ? "bottom" : "middle",
|
|
1756
|
+
labels: cands[plan.index].labels.length,
|
|
1757
|
+
carried: carried.length,
|
|
1758
|
+
edge
|
|
1759
|
+
};
|
|
1760
|
+
} catch {
|
|
1761
|
+
unpinScrolledAxis(container);
|
|
1762
|
+
return noPin("threw");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function fitRenderedChart(container, opts = {}) {
|
|
1766
|
+
const slackPx = opts.slackPx ?? SCROLL_SLACK_PX;
|
|
1767
|
+
const result = {
|
|
1768
|
+
grew: false,
|
|
1769
|
+
growFrom: 0,
|
|
1770
|
+
growTo: 0,
|
|
1771
|
+
growReason: "no-container",
|
|
1772
|
+
overflowX: "hidden",
|
|
1773
|
+
overflowY: "hidden",
|
|
1774
|
+
reading: null,
|
|
1775
|
+
axisPin: noPin("no-container")
|
|
1776
|
+
};
|
|
1777
|
+
if (!container) return result;
|
|
1778
|
+
try {
|
|
1779
|
+
let svg = container.querySelector(`svg[${SNAPSHOT_SVG_ATTR}]`);
|
|
1780
|
+
for (let i = 0; i < container.children.length && !svg; i++) {
|
|
1781
|
+
const el = container.children[i];
|
|
1782
|
+
if (el.tagName && el.tagName.toLowerCase() === "svg") svg = el;
|
|
1783
|
+
}
|
|
1784
|
+
if (!svg) svg = container.querySelector("svg");
|
|
1785
|
+
let inkCache;
|
|
1786
|
+
const svgInk = (el) => {
|
|
1787
|
+
if (inkCache === void 0) inkCache = svgInkReach(el);
|
|
1788
|
+
return inkCache;
|
|
1789
|
+
};
|
|
1790
|
+
const inkOf = (el) => el && el.tagName && el.tagName.toLowerCase() === "svg" ? svgInk(el) : null;
|
|
1791
|
+
if (svg && opts.grow !== false) {
|
|
1792
|
+
try {
|
|
1793
|
+
const ink = svgInk(svg);
|
|
1794
|
+
const elH = svg.getBoundingClientRect().height;
|
|
1795
|
+
const vb = svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal.height : 0;
|
|
1796
|
+
const ctm = typeof svg.getScreenCTM === "function" ? svg.getScreenCTM() : null;
|
|
1797
|
+
const plan = planFrameGrow(
|
|
1798
|
+
ink ? ink.bottom : NaN,
|
|
1799
|
+
elH,
|
|
1800
|
+
vb,
|
|
1801
|
+
ctmScaleOf(ctm),
|
|
1802
|
+
slackPx
|
|
1803
|
+
);
|
|
1804
|
+
result.growFrom = Math.round(elH);
|
|
1805
|
+
result.growTo = plan.heightPx;
|
|
1806
|
+
result.growReason = plan.reason;
|
|
1807
|
+
if (plan.grow) {
|
|
1808
|
+
svg.setAttribute("height", String(plan.heightPx));
|
|
1809
|
+
if (vb > 0) {
|
|
1810
|
+
const b = svg.viewBox.baseVal;
|
|
1811
|
+
svg.setAttribute("viewBox", `${b.x} ${b.y} ${b.width} ${plan.viewBoxH}`);
|
|
1812
|
+
}
|
|
1813
|
+
result.grew = true;
|
|
1814
|
+
}
|
|
1815
|
+
} catch {
|
|
1816
|
+
result.growReason = "grow-threw";
|
|
1817
|
+
}
|
|
1818
|
+
} else if (!svg) {
|
|
1819
|
+
result.growReason = "no-svg";
|
|
1820
|
+
} else {
|
|
1821
|
+
result.growReason = "grow-disabled";
|
|
1822
|
+
}
|
|
1823
|
+
const reading = fitReadingFor(opts.lane ?? "chart", container, slackPx, inkOf);
|
|
1824
|
+
result.reading = reading;
|
|
1825
|
+
const boxes = measureContainerBoxes(container, inkOf);
|
|
1826
|
+
const extent = contentExtentOf(boxes);
|
|
1827
|
+
const svgBox = svg ? svg.getBoundingClientRect() : null;
|
|
1828
|
+
const contentW = extent.source === "none" ? Math.max(container.scrollWidth, svgBox ? svgBox.width : 0) : extent.w;
|
|
1829
|
+
const contentH = extent.source === "none" ? Math.max(container.scrollHeight, svgBox ? svgBox.height : 0) : extent.h;
|
|
1830
|
+
const fit = scrollFitFor(contentW, contentH, container.clientWidth, container.clientHeight, slackPx);
|
|
1831
|
+
result.overflowX = fit.overflowX;
|
|
1832
|
+
result.overflowY = fit.overflowY;
|
|
1833
|
+
if (opts.applyOverflow !== false) {
|
|
1834
|
+
container.style.overflowX = fit.overflowX;
|
|
1835
|
+
container.style.overflowY = fit.overflowY;
|
|
1836
|
+
if (fit.overflowY !== "auto") {
|
|
1837
|
+
unpinScrolledAxis(container);
|
|
1838
|
+
result.axisPin = noPin("not-scrolling");
|
|
1839
|
+
} else if (opts.pinAxis === false) {
|
|
1840
|
+
unpinScrolledAxis(container);
|
|
1841
|
+
result.axisPin = noPin("disabled");
|
|
1842
|
+
} else result.axisPin = pinScrolledAxis(container, svg);
|
|
1843
|
+
} else {
|
|
1844
|
+
unpinScrolledAxis(container);
|
|
1845
|
+
result.axisPin = noPin("overflow-not-applied");
|
|
1846
|
+
}
|
|
1847
|
+
} catch {
|
|
1848
|
+
}
|
|
1849
|
+
return result;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/labelContrastDom.ts
|
|
1853
|
+
var LABEL_CONTRAST_DONE_ATTR = "data-lch-contrast";
|
|
1854
|
+
var LABEL_CONTRAST_CAP = 2e3;
|
|
1855
|
+
var EMPTY4 = {
|
|
1856
|
+
rects: 0,
|
|
1857
|
+
scanned: 0,
|
|
1858
|
+
fixed: 0,
|
|
1859
|
+
pillsBoosted: 0,
|
|
1860
|
+
offFill: 0,
|
|
1861
|
+
pageMajority: 0,
|
|
1862
|
+
paintedOver: 0,
|
|
1863
|
+
straddlePills: 0,
|
|
1864
|
+
pillsExtended: 0,
|
|
1865
|
+
raised: 0
|
|
1866
|
+
};
|
|
1867
|
+
function backedSamples(el, pts) {
|
|
1868
|
+
try {
|
|
1869
|
+
const ge2 = el;
|
|
1870
|
+
if (typeof ge2.isPointInFill !== "function" || typeof ge2.getScreenCTM !== "function") return null;
|
|
1871
|
+
const m = ge2.getScreenCTM();
|
|
1872
|
+
const svg = ge2.ownerSVGElement;
|
|
1873
|
+
if (!m || !svg || typeof svg.createSVGPoint !== "function") return null;
|
|
1874
|
+
if (pts.length === 0) return null;
|
|
1875
|
+
const inv = m.inverse();
|
|
1876
|
+
const hit = [];
|
|
1877
|
+
for (let i = 0; i < pts.length; i++) {
|
|
1878
|
+
const p = svg.createSVGPoint();
|
|
1879
|
+
p.x = pts[i].x;
|
|
1880
|
+
p.y = pts[i].y;
|
|
1881
|
+
hit.push(ge2.isPointInFill(p.matrixTransform(inv)));
|
|
1882
|
+
}
|
|
1883
|
+
return hit;
|
|
1884
|
+
} catch {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
var DOC_POSITION_FOLLOWING = 4;
|
|
1889
|
+
function paintRoot(el, container) {
|
|
1890
|
+
let root = null;
|
|
1891
|
+
for (let n = el.parentElement; n; n = n.parentElement) {
|
|
1892
|
+
if (String(n.tagName || "").toLowerCase() === "svg") root = n;
|
|
1893
|
+
if (n === container) break;
|
|
1894
|
+
}
|
|
1895
|
+
return root;
|
|
1896
|
+
}
|
|
1897
|
+
function drawnBeneath(shape, text2, textRoot) {
|
|
1898
|
+
if (!shape.root || shape.root !== textRoot) return true;
|
|
1899
|
+
if (typeof shape.el.compareDocumentPosition !== "function") return true;
|
|
1900
|
+
return (shape.el.compareDocumentPosition(text2) & DOC_POSITION_FOLLOWING) !== 0;
|
|
1901
|
+
}
|
|
1902
|
+
function sampleBackgrounds(shapes, pts, pageRGB, effAlpha) {
|
|
1903
|
+
if (pts.length === 0 || shapes.length === 0) return null;
|
|
1904
|
+
const ordered = shapes.slice().sort((a, b) => a.ord - b.ord);
|
|
1905
|
+
const masks = [];
|
|
1906
|
+
for (const mk of ordered) {
|
|
1907
|
+
const m = backedSamples(mk.el, pts);
|
|
1908
|
+
if (m === null) return null;
|
|
1909
|
+
masks.push(m);
|
|
1910
|
+
}
|
|
1911
|
+
return pts.map((_, i) => {
|
|
1912
|
+
let bg = [pageRGB[0], pageRGB[1], pageRGB[2]];
|
|
1913
|
+
for (let k = 0; k < ordered.length; k++) {
|
|
1914
|
+
if (!masks[k][i]) continue;
|
|
1915
|
+
const a = effAlpha(ordered[k]);
|
|
1916
|
+
if (a < PILL_MIN_ALPHA) continue;
|
|
1917
|
+
const c = toRGBA(ordered[k].fill);
|
|
1918
|
+
if (c) bg = compositeOver([c[0], c[1], c[2], a], bg);
|
|
1919
|
+
}
|
|
1920
|
+
return bg;
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
function extendPillOver(pill, tr) {
|
|
1924
|
+
try {
|
|
1925
|
+
if (String(pill.tagName || "").toLowerCase() !== "rect") return false;
|
|
1926
|
+
const ge2 = pill;
|
|
1927
|
+
if (typeof ge2.getScreenCTM !== "function") return false;
|
|
1928
|
+
const m = ge2.getScreenCTM();
|
|
1929
|
+
if (!m || Math.abs(m.b) > 1e-6 || Math.abs(m.c) > 1e-6 || !(m.a > 0) || !(m.d > 0)) return false;
|
|
1930
|
+
const inv = m.inverse();
|
|
1931
|
+
const toUser = (x2, y2) => ({ x: inv.a * x2 + inv.c * y2 + inv.e, y: inv.b * x2 + inv.d * y2 + inv.f });
|
|
1932
|
+
const a = toUser(tr.left, tr.top), b = toUser(tr.right, tr.bottom);
|
|
1933
|
+
const num2 = (k) => {
|
|
1934
|
+
const v = parseFloat(pill.getAttribute(k) || "");
|
|
1935
|
+
return isFinite(v) ? v : NaN;
|
|
1936
|
+
};
|
|
1937
|
+
const x = num2("x"), y = num2("y"), w = num2("width"), h = num2("height");
|
|
1938
|
+
if (![x, y, w, h].every((v) => isFinite(v)) || !(w > 0) || !(h > 0)) return false;
|
|
1939
|
+
const pad = 1 / m.a;
|
|
1940
|
+
const x0 = Math.min(x, Math.min(a.x, b.x) - pad), x1 = Math.max(x + w, Math.max(a.x, b.x) + pad);
|
|
1941
|
+
const y0 = Math.min(y, Math.min(a.y, b.y) - pad), y1 = Math.max(y + h, Math.max(a.y, b.y) + pad);
|
|
1942
|
+
if (x0 >= x - 0.01 && x1 <= x + w + 0.01 && y0 >= y - 0.01 && y1 <= y + h + 0.01) return false;
|
|
1943
|
+
pill.setAttribute("x", String(+x0.toFixed(2)));
|
|
1944
|
+
pill.setAttribute("y", String(+y0.toFixed(2)));
|
|
1945
|
+
pill.setAttribute("width", String(+(x1 - x0).toFixed(2)));
|
|
1946
|
+
pill.setAttribute("height", String(+(y1 - y0).toFixed(2)));
|
|
1947
|
+
return true;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function raiseOverOccluders(tx, occluders, tr, effAlpha) {
|
|
1953
|
+
try {
|
|
1954
|
+
const parent = tx.parentNode;
|
|
1955
|
+
if (!parent || typeof parent.contains !== "function") return false;
|
|
1956
|
+
if (!occluders.every((o) => parent.contains(o.el))) return false;
|
|
1957
|
+
const pts = straddleSampleGrid(tr);
|
|
1958
|
+
if (pts.length === 0) return false;
|
|
1959
|
+
const covered = pts.map(() => false);
|
|
1960
|
+
let opaque = 0;
|
|
1961
|
+
for (const o of occluders) {
|
|
1962
|
+
if (effAlpha(o) < PILL_OPAQUE_ALPHA) continue;
|
|
1963
|
+
const m = backedSamples(o.el, pts);
|
|
1964
|
+
if (m === null) return false;
|
|
1965
|
+
opaque++;
|
|
1966
|
+
for (let i = 0; i < m.length; i++) if (m[i]) covered[i] = true;
|
|
1967
|
+
}
|
|
1968
|
+
const n = covered.filter(Boolean).length;
|
|
1969
|
+
if (opaque === 0 || n === 0 || n === pts.length) return false;
|
|
1970
|
+
parent.appendChild(tx);
|
|
1971
|
+
return true;
|
|
1972
|
+
} catch {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
var LABEL_PILL_CLASS = "lch-label-pill";
|
|
1977
|
+
function addPill(tx, fill) {
|
|
1978
|
+
try {
|
|
1979
|
+
const parent = tx.parentNode;
|
|
1980
|
+
if (!parent || typeof tx.getBBox !== "function") return null;
|
|
1981
|
+
const bb = tx.getBBox();
|
|
1982
|
+
if (!bb || !(bb.width > 0) || !(bb.height > 0)) return null;
|
|
1983
|
+
const doc = tx.ownerDocument;
|
|
1984
|
+
if (!doc) return null;
|
|
1985
|
+
const r = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
1986
|
+
const px = Math.max(2, bb.height * 0.2), py = 1;
|
|
1987
|
+
r.setAttribute("x", String(+(bb.x - px).toFixed(2)));
|
|
1988
|
+
r.setAttribute("y", String(+(bb.y - py).toFixed(2)));
|
|
1989
|
+
r.setAttribute("width", String(+(bb.width + 2 * px).toFixed(2)));
|
|
1990
|
+
r.setAttribute("height", String(+(bb.height + 2 * py).toFixed(2)));
|
|
1991
|
+
r.setAttribute("rx", "2");
|
|
1992
|
+
r.setAttribute("fill", fill);
|
|
1993
|
+
r.setAttribute("fill-opacity", String(STRADDLE_PILL_ALPHA));
|
|
1994
|
+
r.setAttribute("pointer-events", "none");
|
|
1995
|
+
r.setAttribute("class", LABEL_PILL_CLASS);
|
|
1996
|
+
r.setAttribute(LABEL_CONTRAST_DONE_ATTR, "1");
|
|
1997
|
+
const tf = tx.getAttribute("transform");
|
|
1998
|
+
if (tf) r.setAttribute("transform", tf);
|
|
1999
|
+
parent.insertBefore(r, tx);
|
|
2000
|
+
return r;
|
|
2001
|
+
} catch {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function applyLabelContrast(container, opts = {}) {
|
|
2006
|
+
const report = { ...EMPTY4 };
|
|
2007
|
+
if (!container) {
|
|
2008
|
+
report.skipped = "no-container";
|
|
2009
|
+
return report;
|
|
2010
|
+
}
|
|
2011
|
+
const CAP = opts.cap ?? LABEL_CONTRAST_CAP;
|
|
2012
|
+
const pageBg = opts.pageBg || "#ffffff";
|
|
2013
|
+
try {
|
|
2014
|
+
const rects = [];
|
|
2015
|
+
let ord = 0;
|
|
2016
|
+
const pushShape = (el) => {
|
|
2017
|
+
const tag = el.tagName.toLowerCase();
|
|
2018
|
+
if (tag !== "rect" && tag !== "path") return;
|
|
2019
|
+
const r = el.getBoundingClientRect();
|
|
2020
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
2021
|
+
let fill = el.getAttribute("fill") || "";
|
|
2022
|
+
const win2 = container.ownerDocument?.defaultView;
|
|
2023
|
+
if ((!fill || fill === "none") && win2 && typeof win2.getComputedStyle === "function") {
|
|
2024
|
+
fill = win2.getComputedStyle(el).fill || "";
|
|
2025
|
+
}
|
|
2026
|
+
if (!fill || fill === "none") return;
|
|
2027
|
+
const opAttr = el.getAttribute("fill-opacity");
|
|
2028
|
+
const op = opAttr === null ? 1 : parseFloat(opAttr);
|
|
2029
|
+
rects.push({
|
|
2030
|
+
r,
|
|
2031
|
+
fill,
|
|
2032
|
+
op: isFinite(op) ? op : 1,
|
|
2033
|
+
area: r.width * r.height,
|
|
2034
|
+
el,
|
|
2035
|
+
ord: ord++,
|
|
2036
|
+
root: paintRoot(el, container)
|
|
2037
|
+
});
|
|
2038
|
+
};
|
|
2039
|
+
const shapeEls = container.querySelectorAll("rect, path");
|
|
2040
|
+
if (shapeEls.length === 0) {
|
|
2041
|
+
report.skipped = "no-shapes";
|
|
2042
|
+
return report;
|
|
2043
|
+
}
|
|
2044
|
+
if (shapeEls.length > CAP) {
|
|
2045
|
+
report.skipped = "too-many-shapes";
|
|
2046
|
+
return report;
|
|
2047
|
+
}
|
|
2048
|
+
for (let i = 0; i < shapeEls.length; i++) pushShape(shapeEls[i]);
|
|
2049
|
+
if (rects.length === 0) {
|
|
2050
|
+
report.skipped = "no-shapes";
|
|
2051
|
+
return report;
|
|
2052
|
+
}
|
|
2053
|
+
report.rects = rects.length;
|
|
2054
|
+
const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
|
|
2055
|
+
const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
|
|
2056
|
+
const effAlpha = (mk) => {
|
|
2057
|
+
const c = toRGBA(mk.fill);
|
|
2058
|
+
return (c ? c[3] : 1) * mk.op;
|
|
2059
|
+
};
|
|
2060
|
+
const texts = container.querySelectorAll("text");
|
|
2061
|
+
if (texts.length > CAP) {
|
|
2062
|
+
report.skipped = "too-many-texts";
|
|
2063
|
+
return report;
|
|
2064
|
+
}
|
|
2065
|
+
const win = container.ownerDocument?.defaultView;
|
|
2066
|
+
for (let t = 0; t < texts.length; t++) {
|
|
2067
|
+
const tx = texts[t];
|
|
2068
|
+
if (tx.getAttribute(LABEL_CONTRAST_DONE_ATTR) === "1") continue;
|
|
2069
|
+
const tr = tx.getBoundingClientRect();
|
|
2070
|
+
if (tr.width <= 0 || tr.height <= 0) continue;
|
|
2071
|
+
const ovArea = (r) => Math.max(0, Math.min(r.right, tr.right) - Math.max(r.left, tr.left)) * Math.max(0, Math.min(r.bottom, tr.bottom) - Math.max(r.top, tr.top));
|
|
2072
|
+
const glyphArea = tr.width * tr.height;
|
|
2073
|
+
const txRoot = paintRoot(tx, container);
|
|
2074
|
+
let over = 0;
|
|
2075
|
+
const occluders = [];
|
|
2076
|
+
const boxed = rects.map((mk) => ({ mk, ov: ovArea(mk.r) })).filter((x) => {
|
|
2077
|
+
if (!(x.ov > 0)) return false;
|
|
2078
|
+
if (drawnBeneath(x.mk, tx, txRoot)) return true;
|
|
2079
|
+
if (effAlpha(x.mk) >= PILL_MIN_ALPHA) {
|
|
2080
|
+
over++;
|
|
2081
|
+
occluders.push(x.mk);
|
|
2082
|
+
}
|
|
2083
|
+
return false;
|
|
2084
|
+
});
|
|
2085
|
+
const raised = occluders.length > 0 && raiseOverOccluders(tx, occluders, tr, effAlpha);
|
|
2086
|
+
if (raised) {
|
|
2087
|
+
report.raised++;
|
|
2088
|
+
tx.setAttribute(LABEL_CONTRAST_DONE_ATTR, "1");
|
|
2089
|
+
for (const mk of occluders) boxed.push({ mk, ov: ovArea(mk.r) });
|
|
2090
|
+
over = 0;
|
|
2091
|
+
}
|
|
2092
|
+
if (over > 0) report.paintedOver++;
|
|
2093
|
+
const pts = glyphSampleGrid({ left: tr.left, top: tr.top, width: tr.width, height: tr.height });
|
|
2094
|
+
const painted = pts.map(() => false);
|
|
2095
|
+
let unmeasured = 0;
|
|
2096
|
+
const under = boxed.map((x) => {
|
|
2097
|
+
const mask = backedSamples(x.mk.el, pts);
|
|
2098
|
+
if (mask === null) {
|
|
2099
|
+
unmeasured++;
|
|
2100
|
+
return x;
|
|
2101
|
+
}
|
|
2102
|
+
let inside = 0;
|
|
2103
|
+
const paints = effAlpha(x.mk) >= PILL_MIN_ALPHA;
|
|
2104
|
+
for (let i = 0; i < mask.length; i++) {
|
|
2105
|
+
if (!mask[i]) continue;
|
|
2106
|
+
inside++;
|
|
2107
|
+
if (paints) painted[i] = true;
|
|
2108
|
+
}
|
|
2109
|
+
return { mk: x.mk, ov: glyphArea * (inside / mask.length) };
|
|
2110
|
+
}).filter((x) => x.ov > 0);
|
|
2111
|
+
if (under.length === 0 && boxed.length > 0) report.offFill++;
|
|
2112
|
+
if (under.length === 0) continue;
|
|
2113
|
+
let raisedOnPage = false;
|
|
2114
|
+
if (unmeasured === 0 && pts.length > 0) {
|
|
2115
|
+
let held = 0;
|
|
2116
|
+
for (let i = 0; i < painted.length; i++) if (painted[i]) held++;
|
|
2117
|
+
if (!backingHoldsGlyph(glyphArea * (held / pts.length), glyphArea)) {
|
|
2118
|
+
if (!raised) {
|
|
2119
|
+
report.pageMajority++;
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
raisedOnPage = true;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
report.scanned++;
|
|
2126
|
+
const opaqueUnder = under.filter((x) => effAlpha(x.mk) >= PILL_OPAQUE_ALPHA);
|
|
2127
|
+
const cellEntry = (opaqueUnder.length ? opaqueUnder : under).reduce((a, b) => b.ov > a.ov || b.ov === a.ov && b.mk.ord > a.mk.ord ? b : a);
|
|
2128
|
+
const cell = cellEntry.mk;
|
|
2129
|
+
const pill = under.find((x) => x.mk !== cell && isPillBackdropAlpha(effAlpha(x.mk)) && x.mk.ord > cell.ord && pillBacksGlyph(x.ov, glyphArea))?.mk;
|
|
2130
|
+
const cellRGBA = toRGBA(cell.fill);
|
|
2131
|
+
let bgRGB = cellRGBA ? compositeOver([cellRGBA[0], cellRGBA[1], cellRGBA[2], cellRGBA[3] * cell.op], pageRGB) : pageRGB;
|
|
2132
|
+
if (pill) {
|
|
2133
|
+
const pr = toRGBA(pill.fill);
|
|
2134
|
+
if (pr) {
|
|
2135
|
+
bgRGB = compositeOver([pr[0], pr[1], pr[2], 0.9], bgRGB);
|
|
2136
|
+
pill.el.setAttribute("fill", `rgb(${pr[0]}, ${pr[1]}, ${pr[2]})`);
|
|
2137
|
+
pill.el.setAttribute("fill-opacity", "0.9");
|
|
2138
|
+
report.pillsBoosted++;
|
|
2139
|
+
if (extendPillOver(pill.el, tr)) report.pillsExtended++;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const cur = tx.getAttribute("fill") || (win && typeof win.getComputedStyle === "function" ? win.getComputedStyle(tx).fill : "");
|
|
2143
|
+
if (!pill && unmeasured === 0) {
|
|
2144
|
+
const bgs = sampleBackgrounds(boxed.map((x) => x.mk), straddleSampleGrid(tr), pageRGB, effAlpha);
|
|
2145
|
+
if (bgs && straddles(bgs)) {
|
|
2146
|
+
if (minContrastOver(cur, bgs) >= STRADDLE_TARGET_CONTRAST) continue;
|
|
2147
|
+
const pillEl = addPill(tx, `rgb(${pageRGB[0]}, ${pageRGB[1]}, ${pageRGB[2]})`);
|
|
2148
|
+
const behind = pillEl ? bgs.map((b) => compositeOver([pageRGB[0], pageRGB[1], pageRGB[2], STRADDLE_PILL_ALPHA], b)) : bgs;
|
|
2149
|
+
if (pillEl) report.straddlePills++;
|
|
2150
|
+
const pick = pickColorOver(cur, behind);
|
|
2151
|
+
if (!pick.keep) {
|
|
2152
|
+
tx.setAttribute("fill", pick.color);
|
|
2153
|
+
report.fixed++;
|
|
2154
|
+
}
|
|
2155
|
+
tx.setAttribute(LABEL_CONTRAST_DONE_ATTR, "1");
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (raisedOnPage) continue;
|
|
2160
|
+
const normalize = !cellSuppressesNormalize(cell.el.getAttribute("class"));
|
|
2161
|
+
const d = decideLabelColor(cur, `rgb(${bgRGB[0]}, ${bgRGB[1]}, ${bgRGB[2]})`, 1, pageBg, MIN_CONTRAST, normalize);
|
|
2162
|
+
if (d.fix) {
|
|
2163
|
+
tx.setAttribute("fill", d.color);
|
|
2164
|
+
report.fixed++;
|
|
2165
|
+
}
|
|
2166
|
+
if (pill || d.fix) tx.setAttribute(LABEL_CONTRAST_DONE_ATTR, "1");
|
|
2167
|
+
}
|
|
2168
|
+
return report;
|
|
2169
|
+
} catch {
|
|
2170
|
+
report.skipped = "error";
|
|
2171
|
+
return report;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/idScope.ts
|
|
2176
|
+
var seq = 0;
|
|
2177
|
+
function newIdScope() {
|
|
2178
|
+
seq += 1;
|
|
2179
|
+
return "h" + seq;
|
|
2180
|
+
}
|
|
2181
|
+
var URL_REF = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/g;
|
|
2182
|
+
function scopeChartIds(c, scope) {
|
|
2183
|
+
const r = { renamed: 0, collisions: [] };
|
|
2184
|
+
try {
|
|
2185
|
+
const tag = "--" + scope, doc = c.ownerDocument, map = /* @__PURE__ */ new Map();
|
|
2186
|
+
const refs = [], used = /* @__PURE__ */ new Set();
|
|
2187
|
+
c.querySelectorAll("*").forEach((el) => {
|
|
2188
|
+
for (const a of el.getAttributeNames()) {
|
|
2189
|
+
const v = el.getAttribute(a);
|
|
2190
|
+
const href = (a === "href" || a === "xlink:href") && v[0] === "#";
|
|
2191
|
+
if (!href && v.indexOf("url(") < 0) continue;
|
|
2192
|
+
refs.push([el, a, v]);
|
|
2193
|
+
if (href) used.add(v.slice(1));
|
|
2194
|
+
for (const m of v.matchAll(URL_REF)) used.add(m[2]);
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
c.querySelectorAll("[id]").forEach((el) => {
|
|
2198
|
+
const id = el.id;
|
|
2199
|
+
if (!used.has(id) || id.endsWith(tag)) return;
|
|
2200
|
+
const q = id.replace(/["\\]/g, "\\$&");
|
|
2201
|
+
if (doc && Array.from(doc.querySelectorAll(`[id="${q}"],[id^="${q}--h"]`)).some((o) => !c.contains(o))) {
|
|
2202
|
+
r.collisions.push(id);
|
|
2203
|
+
}
|
|
2204
|
+
map.set(id, el.id = id + tag);
|
|
2205
|
+
r.renamed++;
|
|
2206
|
+
});
|
|
2207
|
+
for (const [el, a, v] of refs) {
|
|
2208
|
+
const nv = v[0] === "#" && map.has(v.slice(1)) ? "#" + map.get(v.slice(1)) : v.replace(URL_REF, (m, q, id) => map.has(id) ? `url(${q}#${map.get(id)}${q})` : m);
|
|
2209
|
+
if (nv !== v) el.setAttribute(a, nv);
|
|
2210
|
+
}
|
|
2211
|
+
} catch {
|
|
2212
|
+
}
|
|
2213
|
+
return r;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/invalidSentinel.ts
|
|
2217
|
+
function isInvalidSentinelError(text2) {
|
|
2218
|
+
if (!text2) return false;
|
|
2219
|
+
return /\bINVALID\s*:/.test(text2);
|
|
2220
|
+
}
|
|
2221
|
+
function invalidSentinelReason(text2, max = 240) {
|
|
2222
|
+
if (!text2) return "";
|
|
2223
|
+
const m = /\bINVALID\s*:\s*([^\r\n]*)/.exec(text2);
|
|
2224
|
+
if (!m) return "";
|
|
2225
|
+
const reason = m[1].trim();
|
|
2226
|
+
const cap = Math.max(0, Math.floor(max));
|
|
2227
|
+
if (reason.length <= cap) return reason;
|
|
2228
|
+
return cap <= 3 ? reason.slice(0, cap) : reason.slice(0, cap - 3).trimEnd() + "...";
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// src/annotations.ts
|
|
2232
|
+
var ANNOTATION_LAYER_CLASS = "bic-annotations";
|
|
2233
|
+
var ANNOTATION_CLASS = "bic-annotation";
|
|
2234
|
+
var ZOOM_PAD_CLASS = "llm-zoom-pad";
|
|
2235
|
+
var text = (v) => v == null ? "" : String(v).trim();
|
|
2236
|
+
var FREE = { l: -Infinity, t: -Infinity, r: Infinity, b: Infinity };
|
|
2237
|
+
var box = (r) => ({ l: r.left, t: r.top, r: r.right, b: r.bottom });
|
|
2238
|
+
var area = (b) => (b.r - b.l) * (b.b - b.t);
|
|
2239
|
+
var cut = (a, b) => {
|
|
2240
|
+
if (!a) return null;
|
|
2241
|
+
const c = { l: Math.max(a.l, b.l), t: Math.max(a.t, b.t), r: Math.min(a.r, b.r), b: Math.min(a.b, b.b) };
|
|
2242
|
+
return c.r > c.l && c.b > c.t ? c : null;
|
|
2243
|
+
};
|
|
2244
|
+
function shapeTest(mark) {
|
|
2245
|
+
const g = mark;
|
|
2246
|
+
let inv = null;
|
|
2247
|
+
try {
|
|
2248
|
+
inv = g?.isPointInFill ? g.getScreenCTM?.()?.inverse() ?? null : null;
|
|
2249
|
+
} catch {
|
|
2250
|
+
inv = null;
|
|
2251
|
+
}
|
|
2252
|
+
if (!g || !inv) return () => true;
|
|
2253
|
+
const m = inv;
|
|
2254
|
+
return (x, y) => {
|
|
2255
|
+
try {
|
|
2256
|
+
return g.isPointInFill({ x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f });
|
|
2257
|
+
} catch {
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
var parseIdxs = (s) => (s || "").split(",").map((x) => parseInt(x, 10)).filter((n) => Number.isFinite(n));
|
|
2263
|
+
function createAnnotationLayer(container, onClick) {
|
|
2264
|
+
let list = [];
|
|
2265
|
+
let report = { shown: 0, notShown: 0 };
|
|
2266
|
+
let last = null;
|
|
2267
|
+
const doc = container.ownerDocument;
|
|
2268
|
+
const win = doc.defaultView;
|
|
2269
|
+
let observer = null;
|
|
2270
|
+
let queued = false;
|
|
2271
|
+
const redrawSoon = () => {
|
|
2272
|
+
if (queued || !last) return;
|
|
2273
|
+
queued = true;
|
|
2274
|
+
const run = () => {
|
|
2275
|
+
queued = false;
|
|
2276
|
+
if (last) paint(last.data, last.theme);
|
|
2277
|
+
};
|
|
2278
|
+
if (win?.requestAnimationFrame) win.requestAnimationFrame(run);
|
|
2279
|
+
else setTimeout(run, 16);
|
|
2280
|
+
};
|
|
2281
|
+
const watch = (on) => {
|
|
2282
|
+
const MO = win?.MutationObserver;
|
|
2283
|
+
if (on && !observer && MO) {
|
|
2284
|
+
observer = new MO((records) => {
|
|
2285
|
+
if (records.some((r) => !r.target.closest?.(`.${ANNOTATION_LAYER_CLASS}`))) redrawSoon();
|
|
2286
|
+
});
|
|
2287
|
+
observer.observe(container, { subtree: true, attributes: true, attributeFilter: ["transform", "d", "cx", "cy", "x", "y", "points"] });
|
|
2288
|
+
} else if (!on && observer) {
|
|
2289
|
+
observer.disconnect();
|
|
2290
|
+
observer = null;
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
function paint(data, theme) {
|
|
2294
|
+
let layer = container.querySelector(`:scope > .${ANNOTATION_LAYER_CLASS}`);
|
|
2295
|
+
if (!list.length) {
|
|
2296
|
+
layer?.remove();
|
|
2297
|
+
report = { shown: 0, notShown: 0 };
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!layer) {
|
|
2301
|
+
layer = doc.createElement("div");
|
|
2302
|
+
layer.className = ANNOTATION_LAYER_CLASS;
|
|
2303
|
+
layer.setAttribute("aria-hidden", "false");
|
|
2304
|
+
Object.assign(layer.style, { position: "absolute", left: "0", top: "0", width: "0", height: "0", overflow: "visible", zIndex: "2" });
|
|
2305
|
+
if (win?.getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
2306
|
+
container.appendChild(layer);
|
|
2307
|
+
}
|
|
2308
|
+
layer.textContent = "";
|
|
2309
|
+
const names = (data.columns || []).map((c) => typeof c === "string" ? c : c?.name);
|
|
2310
|
+
const marks = Array.from(container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}]`)).filter((m) => !m.closest(`.${ANNOTATION_LAYER_CLASS}`));
|
|
2311
|
+
const marksByRow = /* @__PURE__ */ new Map();
|
|
2312
|
+
for (const m of marks) for (const r of parseIdxs(m.getAttribute(ROW_IDX_ATTR))) {
|
|
2313
|
+
const at = marksByRow.get(r);
|
|
2314
|
+
if (at) at.push(m);
|
|
2315
|
+
else marksByRow.set(r, [m]);
|
|
2316
|
+
}
|
|
2317
|
+
const origin = container.getBoundingClientRect();
|
|
2318
|
+
const open = origin.width > 0 && origin.height > 0 ? box(origin) : FREE;
|
|
2319
|
+
const clipOf = /* @__PURE__ */ new Map();
|
|
2320
|
+
const viewOf = (m) => {
|
|
2321
|
+
let v = open;
|
|
2322
|
+
const svg = m.closest("svg");
|
|
2323
|
+
if (svg) {
|
|
2324
|
+
const s = svg.getBoundingClientRect();
|
|
2325
|
+
if (s.width > 0 && s.height > 0) v = cut(v, box(s));
|
|
2326
|
+
}
|
|
2327
|
+
for (let el = m.parentElement; v && el && el !== svg && el !== container; el = el.parentElement) {
|
|
2328
|
+
if (!clipOf.has(el)) {
|
|
2329
|
+
const w2 = clipWindowOf(el);
|
|
2330
|
+
clipOf.set(el, w2 && box(w2));
|
|
2331
|
+
}
|
|
2332
|
+
const w = clipOf.get(el);
|
|
2333
|
+
if (w) v = cut(v, w);
|
|
2334
|
+
}
|
|
2335
|
+
return v;
|
|
2336
|
+
};
|
|
2337
|
+
const controls = Array.from(container.querySelectorAll(`.${CONTROL_CLASS}, .${ZOOM_PAD_CLASS}`)).map((c) => c.getBoundingClientRect()).filter((r) => r.width > 0 && r.height > 0).map(box);
|
|
2338
|
+
const clear = (v, len, mark) => {
|
|
2339
|
+
const hw = Math.max(16, len * 6 + 8) / 2, hh = 8, g = 2;
|
|
2340
|
+
const cx = (v.l + v.r) / 2, cy = (v.t + v.b) / 2;
|
|
2341
|
+
const free = (x, y) => x >= v.l && x <= v.r && y >= v.t && y <= v.b && controls.every((c) => x + hw <= c.l || x - hw >= c.r || y + hh <= c.t || y - hh >= c.b);
|
|
2342
|
+
const onShape = shapeTest(mark);
|
|
2343
|
+
const tries = [[cx, cy]];
|
|
2344
|
+
for (let i = 0; i <= 8; i++) for (let j = 0; j <= 6; j++) tries.push([v.l + (v.r - v.l) * i / 8, v.t + (v.b - v.t) * j / 6]);
|
|
2345
|
+
tries.sort((p, q) => Math.hypot(p[0] - cx, p[1] - cy) - Math.hypot(q[0] - cx, q[1] - cy));
|
|
2346
|
+
for (const c of controls) tries.push([cx, c.b + hh + g], [cx, c.t - hh - g], [c.l - hw - g, cy], [c.r + hw + g, cy]);
|
|
2347
|
+
return tries.find(([x, y]) => free(x, y) && onShape(x, y)) ?? tries.find(([x, y]) => free(x, y)) ?? null;
|
|
2348
|
+
};
|
|
2349
|
+
let shown = 0;
|
|
2350
|
+
for (const a of list) {
|
|
2351
|
+
const pairs = (a.where ? Object.entries(a.where) : a.column != null ? [[a.column, a.value]] : []).map(([c, v]) => [names.indexOf(c), text(v)]);
|
|
2352
|
+
const want = pairs.map((p) => p[1]).join(" / ");
|
|
2353
|
+
const hits = /* @__PURE__ */ new Set();
|
|
2354
|
+
if (pairs.length && pairs.every((p) => p[0] >= 0)) (data.rows || []).forEach((row, r) => {
|
|
2355
|
+
if (pairs.every(([ci, v]) => text(row?.[ci]) === v)) for (const m of marksByRow.get(r) ?? []) hits.add(m);
|
|
2356
|
+
});
|
|
2357
|
+
if (!hits.size) continue;
|
|
2358
|
+
let best = null, bestMark = null, laidOut = false;
|
|
2359
|
+
for (const m of hits) {
|
|
2360
|
+
const b = m.getBoundingClientRect();
|
|
2361
|
+
if (!(b.width > 0 && b.height > 0)) continue;
|
|
2362
|
+
laidOut = true;
|
|
2363
|
+
const v = cut(box(b), viewOf(m));
|
|
2364
|
+
if (v && (!best || area(v) > area(best))) {
|
|
2365
|
+
best = v;
|
|
2366
|
+
bestMark = m;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const pt = best ? clear(best, (a.label ?? "\u2022").length, bestMark) : null;
|
|
2370
|
+
if (laidOut && !pt) continue;
|
|
2371
|
+
shown++;
|
|
2372
|
+
const badge = doc.createElement("div");
|
|
2373
|
+
badge.className = ANNOTATION_CLASS;
|
|
2374
|
+
badge.textContent = a.label ?? "\u2022";
|
|
2375
|
+
if (a.title) badge.setAttribute("title", a.title);
|
|
2376
|
+
badge.setAttribute("role", "button");
|
|
2377
|
+
badge.setAttribute("aria-label", a.title ? `Note: ${a.title}` : `Note on ${want}`);
|
|
2378
|
+
Object.assign(badge.style, {
|
|
2379
|
+
position: "absolute",
|
|
2380
|
+
left: `${Math.round(pt ? pt[0] - origin.left : 0)}px`,
|
|
2381
|
+
top: `${Math.round(pt ? pt[1] - origin.top : 0)}px`,
|
|
2382
|
+
transform: "translate(-50%, -50%)",
|
|
2383
|
+
minWidth: "16px",
|
|
2384
|
+
height: "16px",
|
|
2385
|
+
padding: "0 4px",
|
|
2386
|
+
boxSizing: "border-box",
|
|
2387
|
+
borderRadius: "8px",
|
|
2388
|
+
font: "600 10px/16px system-ui, sans-serif",
|
|
2389
|
+
textAlign: "center",
|
|
2390
|
+
color: theme.bg || "#ffffff",
|
|
2391
|
+
background: theme.accent || theme.fg || "#0f6cbd",
|
|
2392
|
+
boxShadow: `0 0 0 1.5px ${theme.bg || "#ffffff"}`,
|
|
2393
|
+
cursor: onClick ? "pointer" : "default",
|
|
2394
|
+
// Never pointer-events (a host failsafe turns it back on); visibility hides a badge whose mark
|
|
2395
|
+
// has no box yet.
|
|
2396
|
+
visibility: laidOut ? "visible" : "hidden"
|
|
2397
|
+
});
|
|
2398
|
+
badge.addEventListener("click", (e) => {
|
|
2399
|
+
e.stopPropagation();
|
|
2400
|
+
onClick?.(a);
|
|
2401
|
+
});
|
|
2402
|
+
layer.appendChild(badge);
|
|
2403
|
+
}
|
|
2404
|
+
report = { shown, notShown: list.length - shown };
|
|
2405
|
+
}
|
|
2406
|
+
return {
|
|
2407
|
+
set(next) {
|
|
2408
|
+
list = Array.isArray(next) ? next.slice() : [];
|
|
2409
|
+
watch(list.length > 0);
|
|
2410
|
+
if (last) paint(last.data, last.theme);
|
|
2411
|
+
},
|
|
2412
|
+
draw(data, theme) {
|
|
2413
|
+
last = { data, theme };
|
|
2414
|
+
paint(data, theme);
|
|
2415
|
+
},
|
|
2416
|
+
get report() {
|
|
2417
|
+
return report;
|
|
2418
|
+
},
|
|
2419
|
+
destroy() {
|
|
2420
|
+
watch(false);
|
|
2421
|
+
list = [];
|
|
2422
|
+
last = null;
|
|
2423
|
+
container.querySelector(`:scope > .${ANNOTATION_LAYER_CLASS}`)?.remove();
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/host.ts
|
|
2429
|
+
function sessionViewStateProvider(container) {
|
|
2430
|
+
const holder = container;
|
|
2431
|
+
const existing = holder[CONTAINER_SLOT_UI_STATE];
|
|
2432
|
+
const store = existing && typeof existing === "object" ? existing : holder[CONTAINER_SLOT_UI_STATE] = {};
|
|
2433
|
+
return {
|
|
2434
|
+
load: () => store,
|
|
2435
|
+
save: (next) => {
|
|
2436
|
+
if (!next || typeof next !== "object") return;
|
|
2437
|
+
for (const k of Object.keys(store)) delete store[k];
|
|
2438
|
+
Object.assign(store, next);
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function noopViewStateProvider() {
|
|
2443
|
+
return { load: () => ({}), save: () => {
|
|
2444
|
+
} };
|
|
2445
|
+
}
|
|
2446
|
+
var parseRowIdxs = (s) => (s || "").split(",").map((x) => parseInt(x, 10)).filter((n) => Number.isFinite(n));
|
|
2447
|
+
function assembleD3(base, ...plugins) {
|
|
2448
|
+
return Object.assign({}, base, ...plugins);
|
|
2449
|
+
}
|
|
2450
|
+
var D3_PLUGIN_PACKAGES = Object.freeze({
|
|
2451
|
+
sankey: "d3-sankey",
|
|
2452
|
+
sankeyLinkHorizontal: "d3-sankey",
|
|
2453
|
+
sankeyJustify: "d3-sankey",
|
|
2454
|
+
sankeyCenter: "d3-sankey",
|
|
2455
|
+
sankeyLeft: "d3-sankey",
|
|
2456
|
+
sankeyRight: "d3-sankey",
|
|
2457
|
+
hexbin: "d3-hexbin",
|
|
2458
|
+
voronoiTreemap: "d3-voronoi-treemap",
|
|
2459
|
+
voronoiMap: "d3-voronoi-map",
|
|
2460
|
+
weightedVoronoi: "d3-weighted-voronoi",
|
|
2461
|
+
// Attached onto d3 like the rest, but reached as d3.mermaid.render(...) — a NAMESPACE
|
|
2462
|
+
// OBJECT with methods, not a function on d3. The scan below accounts for that shape.
|
|
2463
|
+
mermaid: "mermaid",
|
|
2464
|
+
// AND THE NAME A REAL CHART ACTUALLY WRITES. This one is not a member of the mermaid
|
|
2465
|
+
// package at all: it is a helper the SERVER prepends into the generated code, and the
|
|
2466
|
+
// helper is what reaches for the library. So a genuine diagram chart contains
|
|
2467
|
+
// `d3.llmMermaid(` and never once writes `d3.mermaid` — measured on the first real
|
|
2468
|
+
// generation of that type, for which this scan returned an EMPTY list. It drew, because
|
|
2469
|
+
// the host it ran in happened to carry the library; a host relying on this answer to
|
|
2470
|
+
// decide what to install would have been told nothing and failed several frames deep.
|
|
2471
|
+
//
|
|
2472
|
+
// Mapping the helper is right rather than a hack, because this map answers "what must a
|
|
2473
|
+
// host INSTALL for this code to run" and not "which package exports this symbol". For
|
|
2474
|
+
// code that calls the helper, the answer is the library the helper reaches for.
|
|
2475
|
+
llmMermaid: "mermaid"
|
|
2476
|
+
});
|
|
2477
|
+
function requiredD3Plugins(code) {
|
|
2478
|
+
const out = /* @__PURE__ */ new Set();
|
|
2479
|
+
const body = stripJsComments(String(code || ""));
|
|
2480
|
+
const add = (name) => {
|
|
2481
|
+
const pkg = D3_PLUGIN_PACKAGES[name];
|
|
2482
|
+
if (pkg) out.add(pkg);
|
|
2483
|
+
};
|
|
2484
|
+
for (const m of body.matchAll(/\bd3\s*\.\s*(\w+)/g)) add(m[1]);
|
|
2485
|
+
for (const m of body.matchAll(/\bd3\s*\[\s*['"](\w+)['"]\s*\]/g)) add(m[1]);
|
|
2486
|
+
for (const m of body.matchAll(/\{([^{}]*)\}\s*=\s*d3\b/g)) {
|
|
2487
|
+
for (const part of m[1].split(",")) add(part.split(":")[0].trim());
|
|
2488
|
+
}
|
|
2489
|
+
return [...out].sort();
|
|
2490
|
+
}
|
|
2491
|
+
function explainRenderFailure(err, d3) {
|
|
2492
|
+
const msg = String(err?.message ?? err ?? "");
|
|
2493
|
+
if (!d3) {
|
|
2494
|
+
return new Error(
|
|
2495
|
+
"[@bicharts/chart-host] no d3 was provided, so the generated chart could not run. Pass it explicitly \u2014 createChartHost(el, { d3 }) / <BicChart d3={d3} /> \u2014 or set window.d3. The chart needs D3 v7 (npm install d3@7). Original error: " + msg
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
if (typeof d3?.mermaid?.render !== "function" && /\bmermaid\b/i.test(msg)) {
|
|
2499
|
+
return new Error(
|
|
2500
|
+
"[@bicharts/chart-host] this chart draws its diagram with Mermaid, which it reaches as d3.mermaid.render(...), and the d3 you passed has no mermaid on it \u2014 this host did not load the library. A host supplies it exactly the way the other plugins are supplied: hand createChartHost ONE d3 that carries it, the SAME d3 every chart gets \u2014 npm install mermaid then import * as d3base from 'd3'; import mermaid from 'mermaid'; import { assembleD3 } from '@bicharts/chart-host'; const d3 = assembleD3(d3base, { mermaid }); (an ES-module d3 namespace is frozen, so assigning onto it throws) Original error: " + msg
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
const m = /d3\.(\w+) is not a function|(\w+) is not a function/.exec(msg);
|
|
2504
|
+
const name = m?.[1] ?? m?.[2] ?? "";
|
|
2505
|
+
const pkg = D3_PLUGIN_PACKAGES[name];
|
|
2506
|
+
if (pkg) {
|
|
2507
|
+
return new Error(
|
|
2508
|
+
`[@bicharts/chart-host] this chart calls d3.${name}(), which comes from the separate "${pkg}" package and is not attached to the d3 you passed. Install it and hand the host ONE d3 that carries it, the SAME d3 every chart gets: npm install ${pkg} then import * as d3base from "d3"; import { ${name} } from "${pkg}"; import { assembleD3 } from "@bicharts/chart-host"; const d3 = assembleD3(d3base, { ${name} }); (an ES-module d3 namespace is frozen, so assigning onto it throws) Original error: ` + msg
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
return err;
|
|
2512
|
+
}
|
|
2513
|
+
var ESM_EXPORT_CLAUSE = /^[ \t]*export[ \t]+(?:\{[^}]*\}|default[ \t]+[A-Za-z_$][\w$]*)[ \t]*;?[ \t]*$/gm;
|
|
2514
|
+
var ESM_EXPORT_DECL = /^([ \t]*)export[ \t]+(?=(?:async[ \t]+)?function\b|const\b|let\b|var\b|class\b)/gm;
|
|
2515
|
+
function stripEsmExports(code) {
|
|
2516
|
+
return String(code || "").replace(ESM_EXPORT_CLAUSE, "").replace(ESM_EXPORT_DECL, "$1");
|
|
2517
|
+
}
|
|
2518
|
+
function writableD3(d3) {
|
|
2519
|
+
if (d3 == null || typeof d3 !== "object") return d3;
|
|
2520
|
+
if (Object.isExtensible(d3)) return d3;
|
|
2521
|
+
return { ...d3 };
|
|
2522
|
+
}
|
|
2523
|
+
function compileRenderFn(code, win, doc, d3) {
|
|
2524
|
+
const body = stripEsmExports(code) + "\n;return (typeof render === 'function') ? render : null;";
|
|
2525
|
+
let factory;
|
|
2526
|
+
try {
|
|
2527
|
+
factory = new Function("container", "data", "options", "window", "document", "d3", body);
|
|
2528
|
+
} catch (e) {
|
|
2529
|
+
const msg = String(e?.message ?? e ?? "");
|
|
2530
|
+
const modular = /\bimport\b|\bexport\b/.test(String(code || ""));
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
"[@bicharts/chart-host] the generated code could not be COMPILED, so nothing was drawn. This path evaluates the chart as a code string, which cannot contain ES-module syntax" + (modular ? " \u2014 and this code still has `import`/`export` in it after the export clause was stripped. Either request the plain form from the generator, or `import` the module yourself and pass the render function directly." : ".") + " Original error: " + msg
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const fn = factory.call(null, null, null, null, win, doc, writableD3(d3));
|
|
2536
|
+
if (typeof fn !== "function") throw new Error("generated code did not define render(container, data, options)");
|
|
2537
|
+
return fn;
|
|
2538
|
+
}
|
|
2539
|
+
var ASYNC_RENDER_TIMEOUT_MS = 15e3;
|
|
2540
|
+
var isThenable = (v) => !!v && (typeof v === "object" || typeof v === "function") && typeof v.then === "function";
|
|
2541
|
+
function createChartHost(container, config) {
|
|
2542
|
+
const doc = container.ownerDocument;
|
|
2543
|
+
const win = doc?.defaultView ?? globalThis;
|
|
2544
|
+
const d3 = config.d3 ?? globalThis.d3 ?? win?.d3;
|
|
2545
|
+
let data = config.data;
|
|
2546
|
+
{
|
|
2547
|
+
const stamped = config.data?.meta?.hostContract;
|
|
2548
|
+
const major = (v) => (v || "").split(".")[0];
|
|
2549
|
+
if (stamped && major(stamped) !== major(HOST_CONTRACT_VERSION)) {
|
|
2550
|
+
try {
|
|
2551
|
+
(win?.console ?? console)?.warn(
|
|
2552
|
+
`[@bicharts/chart-host] contract MAJOR mismatch: this chart was generated against v${stamped}, the host implements v${HOST_CONTRACT_VERSION}. Marks, container slots or the cross-filter event may not line up \u2014 regenerate the chart.`
|
|
2553
|
+
);
|
|
2554
|
+
} catch {
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
let raw = { ...config.options ?? {} };
|
|
2559
|
+
if (typeof raw.setUiState !== "function") {
|
|
2560
|
+
const provider = config.viewState ?? sessionViewStateProvider(container);
|
|
2561
|
+
let bag;
|
|
2562
|
+
try {
|
|
2563
|
+
bag = provider.load() ?? {};
|
|
2564
|
+
} catch {
|
|
2565
|
+
bag = {};
|
|
2566
|
+
}
|
|
2567
|
+
if (raw.uiState && typeof raw.uiState === "object" && Object.keys(bag).length === 0)
|
|
2568
|
+
Object.assign(bag, raw.uiState);
|
|
2569
|
+
raw.uiState = bag;
|
|
2570
|
+
raw.setUiState = (s) => {
|
|
2571
|
+
if (!s || typeof s !== "object") return;
|
|
2572
|
+
try {
|
|
2573
|
+
provider.save(s);
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const resolveForHost = (input) => {
|
|
2579
|
+
const out = resolveOptions(input);
|
|
2580
|
+
return out.allowTooltips == null ? { ...out, allowTooltips: true } : out;
|
|
2581
|
+
};
|
|
2582
|
+
let resolved = resolveForHost(raw);
|
|
2583
|
+
let renderFn = config.renderFn ?? null;
|
|
2584
|
+
let destroyed = false;
|
|
2585
|
+
let warnedGeo = false;
|
|
2586
|
+
let warnedBlank = false;
|
|
2587
|
+
let warnedIds = false;
|
|
2588
|
+
const idScope = newIdScope();
|
|
2589
|
+
let renderSeq = 0;
|
|
2590
|
+
const annotationLayer = createAnnotationLayer(container, (a) => config.onAnnotationClick?.(a));
|
|
2591
|
+
const drawAnnotations = () => annotationLayer.draw(
|
|
2592
|
+
{ columns: data.columns, rows: data.rows },
|
|
2593
|
+
{
|
|
2594
|
+
fg: resolved.themeFg,
|
|
2595
|
+
accent: resolved.themeAccent,
|
|
2596
|
+
bg: resolved.backgroundColor ?? resolved.themeBg
|
|
2597
|
+
}
|
|
2598
|
+
);
|
|
2599
|
+
annotationLayer.set(config.annotations ?? []);
|
|
2600
|
+
let renderedPromise = Promise.resolve();
|
|
2601
|
+
const settle = (p) => {
|
|
2602
|
+
renderedPromise = p;
|
|
2603
|
+
p.catch(() => {
|
|
2604
|
+
});
|
|
2605
|
+
};
|
|
2606
|
+
let current = null;
|
|
2607
|
+
let activeTickEl = null;
|
|
2608
|
+
let selectionIsPeriod = false;
|
|
2609
|
+
let selectionFromChart = false;
|
|
2610
|
+
const clickedMarks = () => container[CONTAINER_SLOT_CLICKED_MARKS] ?? null;
|
|
2611
|
+
const setClickedMarks = (c) => {
|
|
2612
|
+
try {
|
|
2613
|
+
container[CONTAINER_SLOT_CLICKED_MARKS] = c;
|
|
2614
|
+
} catch {
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
const subs = /* @__PURE__ */ new Set();
|
|
2618
|
+
const ensureAffordanceStyles = () => {
|
|
2619
|
+
try {
|
|
2620
|
+
if (!doc || doc.getElementById("bic-chart-host-affordances")) return;
|
|
2621
|
+
const st = doc.createElement("style");
|
|
2622
|
+
st.id = "bic-chart-host-affordances";
|
|
2623
|
+
st.textContent = `
|
|
2624
|
+
/* TYPOGRAPHY FIREWALL. Generated charts set font-size but almost never line-height or
|
|
2625
|
+
letter-spacing, so they INHERIT whatever the host page cascades in. A page with
|
|
2626
|
+
\`:root { font: 18px/145% }\` (the stock Vite template does exactly this) hands every
|
|
2627
|
+
chart an inherited 26.1px line box; an 11px label in a 14px band is then sliced through
|
|
2628
|
+
the middle. The chart is not wrong and the host is not wrong \u2014 the boundary between
|
|
2629
|
+
them is, so the boundary fixes it. Both properties are overridable: a chart that sets
|
|
2630
|
+
its own line-height still wins. Deliberately NOT resetting font-family or color, which
|
|
2631
|
+
a host SHOULD be able to theme. Found 2026-07-26 in the React demo, on a chart that
|
|
2632
|
+
renders perfectly inside Power BI. */
|
|
2633
|
+
.${HOST_CONTAINER_CLASS} { line-height: normal; letter-spacing: normal; }
|
|
2634
|
+
/* BACKGROUND MARKS ARE NOT DATA. A mark whose ${ROW_IDX_ATTR} is EMPTY has no rows in the
|
|
2635
|
+
CURRENT frame \u2014 an animated map's region before its country enters the series. Every
|
|
2636
|
+
affordance below therefore skips it: brightening or dimming it manufactures a shade
|
|
2637
|
+
that appears in no legend, and at 25% grey over white it comes out LIGHTER than plain
|
|
2638
|
+
land, which reads as a real bottom-of-scale value rather than as absence. The guard is
|
|
2639
|
+
exactly [${ROW_IDX_ATTR}=""] \u2014 an ABSENT attribute is a tagged companion layer (an
|
|
2640
|
+
anomaly badge, a highlight stroke) and must keep dimming with its datum. */
|
|
2641
|
+
.${HOST_CONTAINER_CLASS} .${MARK_CLASS} { cursor: pointer; transition: opacity 120ms ease-in-out, filter 100ms ease-in-out; }
|
|
2642
|
+
.${HOST_CONTAINER_CLASS} .${MARK_CLASS}:not([${ROW_IDX_ATTR}=""]):hover { filter: brightness(1.1) drop-shadow(0 0 1.5px rgba(0,0,0,0.5)) !important; }
|
|
2643
|
+
.${HOST_CONTAINER_CLASS}:has(.${MARK_CLASS}:not([${ROW_IDX_ATTR}=""]):hover) .${MARK_CLASS}:not(:hover):not([${ROW_IDX_ATTR}=""]) { opacity: 0.55; }
|
|
2644
|
+
.${HOST_CONTAINER_CLASS}.${SELECTION_ACTIVE_CLASS} .${MARK_CLASS}:not(.${MARK_SELECTED_CLASS}):not([${ROW_IDX_ATTR}=""]) {
|
|
2645
|
+
opacity: var(${DIM_OPACITY_VAR}, ${DIM_OPACITY_DEFAULT}) !important;
|
|
2646
|
+
}
|
|
2647
|
+
/* THE LIFT, BY DECLARATION ONLY. Dimming the others is all the rule above does; a selected
|
|
2648
|
+
mark keeps whatever paint its chart gave it. A chart that draws every mark pale on purpose
|
|
2649
|
+
(a parallel-coordinates plot at 0.38 so overplotting reads) therefore shows its SELECTED
|
|
2650
|
+
lines at 38% - the same eye-reading as dimmed. Inside a group the chart stamps
|
|
2651
|
+
${LIFT_SELECTED_CLASS} on, a selected mark is painted at full opacity. Opacity only, never
|
|
2652
|
+
stroke geometry. Undeclared charts are byte-identical, which is what keeps an alpha
|
|
2653
|
+
ENCODING (a depth cue, a hierarchy level) from being flattened by a fix meant for one. */
|
|
2654
|
+
.${HOST_CONTAINER_CLASS}.${SELECTION_ACTIVE_CLASS} .${LIFT_SELECTED_CLASS} .${MARK_CLASS}.${MARK_SELECTED_CLASS},
|
|
2655
|
+
.${HOST_CONTAINER_CLASS}.${SELECTION_ACTIVE_CLASS} .${MARK_CLASS}.${LIFT_SELECTED_CLASS}.${MARK_SELECTED_CLASS} {
|
|
2656
|
+
opacity: 1 !important; stroke-opacity: 1 !important; fill-opacity: 1 !important;
|
|
2657
|
+
}
|
|
2658
|
+
.${HOST_CONTAINER_CLASS} .${LEGEND_MARK_CLASS} { cursor: pointer; }
|
|
2659
|
+
/* An axis/group header that filters when clicked LOOKS like inert text, and until it is
|
|
2660
|
+
marked there is no way to tell a landed filter from a dead label \u2014 the report reads
|
|
2661
|
+
"the axis labels are not clickable" even when every click resolved. Weight + underline
|
|
2662
|
+
rather than a colour: these labels sit wherever the host page's theme puts them, and
|
|
2663
|
+
the host has no reliable contrast partner out in the axis gutter. This paints NO marks,
|
|
2664
|
+
so it is the one acknowledgement a PERIOD tick can have without breaking the rule that
|
|
2665
|
+
a timeline never dims its own chart. */
|
|
2666
|
+
.${HOST_CONTAINER_CLASS} .${AXIS_FILTER_CLASS} { cursor: pointer; }
|
|
2667
|
+
.${HOST_CONTAINER_CLASS} .${ACTIVE_TICK_CLASS},
|
|
2668
|
+
.${HOST_CONTAINER_CLASS} .${ACTIVE_TICK_CLASS} text {
|
|
2669
|
+
font-weight: 700 !important;
|
|
2670
|
+
text-decoration: underline !important;
|
|
2671
|
+
}`;
|
|
2672
|
+
(doc.head || doc.documentElement)?.appendChild(st);
|
|
2673
|
+
} catch {
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
const paintSelection = () => {
|
|
2677
|
+
try {
|
|
2678
|
+
const sel = current;
|
|
2679
|
+
const active = !!sel && sel.length > 0;
|
|
2680
|
+
const dim = active && !selectionIsPeriod;
|
|
2681
|
+
container.classList?.[dim ? "add" : "remove"](SELECTION_ACTIVE_CLASS);
|
|
2682
|
+
const lit = container.querySelectorAll(`.${ACTIVE_TICK_CLASS}`);
|
|
2683
|
+
for (const t of Array.from(lit)) t.classList?.remove(ACTIVE_TICK_CLASS);
|
|
2684
|
+
const tickStillDrawn = !!activeTickEl && (typeof container.contains === "function" ? container.contains(activeTickEl) : activeTickEl.isConnected !== false);
|
|
2685
|
+
if (active && tickStillDrawn) activeTickEl.classList?.add(ACTIVE_TICK_CLASS);
|
|
2686
|
+
const wanted = new Set(sel ?? []);
|
|
2687
|
+
const marks = container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}], .${LEGEND_MARK_CLASS}[${ROW_IDX_ATTR}]`);
|
|
2688
|
+
const rule = selectionRuleOf(container);
|
|
2689
|
+
if (rule !== "any-row") {
|
|
2690
|
+
const list = Array.from(marks);
|
|
2691
|
+
const plan = planSelectionPaint(
|
|
2692
|
+
list.map((m) => ({ key: markKeyOf(m), rows: parseRowIdxs(m.getAttribute(ROW_IDX_ATTR)) })),
|
|
2693
|
+
active ? wanted : [],
|
|
2694
|
+
rule,
|
|
2695
|
+
clickedMarks()
|
|
2696
|
+
);
|
|
2697
|
+
list.forEach((m, i) => m.classList?.[plan.on[i] ? "add" : "remove"](MARK_SELECTED_CLASS));
|
|
2698
|
+
setClickedMarks(plan.keys.length ? { keys: plan.keys, selection: Array.from(wanted) } : null);
|
|
2699
|
+
if (active) {
|
|
2700
|
+
try {
|
|
2701
|
+
config.onSelectionPaint?.({
|
|
2702
|
+
rule,
|
|
2703
|
+
mode: plan.mode,
|
|
2704
|
+
lit: plan.on.filter(Boolean).length,
|
|
2705
|
+
marks: list.length,
|
|
2706
|
+
selection: wanted.size
|
|
2707
|
+
});
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
for (const m of Array.from(marks)) {
|
|
2714
|
+
const rows = parseRowIdxs(m.getAttribute(ROW_IDX_ATTR));
|
|
2715
|
+
const on = active && rows.some((r) => wanted.has(r));
|
|
2716
|
+
m.classList?.[on ? "add" : "remove"](MARK_SELECTED_CLASS);
|
|
2717
|
+
}
|
|
2718
|
+
} catch {
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2721
|
+
const notify = (rowIdxs, source, tick, fromChart = false) => {
|
|
2722
|
+
current = rowIdxs;
|
|
2723
|
+
selectionFromChart = rowIdxs.length > 0 && fromChart;
|
|
2724
|
+
activeTickEl = rowIdxs.length ? tick ?? null : null;
|
|
2725
|
+
selectionIsPeriod = rowIdxs.length > 0 && periodTickSuppressesFeedback(!!tick, chartOwnsTimeline(container));
|
|
2726
|
+
paintSelection();
|
|
2727
|
+
for (const cb of subs) {
|
|
2728
|
+
try {
|
|
2729
|
+
cb(rowIdxs, source);
|
|
2730
|
+
} catch {
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
let xfAt = 0;
|
|
2735
|
+
const XF_ECHO_MS = 150;
|
|
2736
|
+
const onXf = (e) => {
|
|
2737
|
+
const d = e?.detail || {};
|
|
2738
|
+
xfAt = Date.now();
|
|
2739
|
+
if (d.clear) notify([], d.source || "chart");
|
|
2740
|
+
else if (d.mark) notify(parseRowIdxs(d.mark.getAttribute?.(ROW_IDX_ATTR)), d.source || "chart", d.mark, true);
|
|
2741
|
+
};
|
|
2742
|
+
const resolver = createMarkResolver({
|
|
2743
|
+
root: container,
|
|
2744
|
+
doc,
|
|
2745
|
+
log: () => {
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
const clickSel = `.${MARK_CLASS}[${ROW_IDX_ATTR}], .${LEGEND_MARK_CLASS}[${ROW_IDX_ATTR}], .${AXIS_FILTER_CLASS}[${ROW_IDX_ATTR}]`;
|
|
2749
|
+
const onClick = (e) => {
|
|
2750
|
+
if (Date.now() - xfAt < XF_ECHO_MS) return;
|
|
2751
|
+
if (isInsideControl(e?.target, container)) return;
|
|
2752
|
+
let el = resolver.findMark(e?.target, clickSel, e);
|
|
2753
|
+
if (!el) el = resolver.penetrateOverlayAt(e?.clientX, e?.clientY, clickSel);
|
|
2754
|
+
if (!el) el = resolver.resolveByGeometry(e?.clientX, e?.clientY, clickSel);
|
|
2755
|
+
if (!el) {
|
|
2756
|
+
if (current && current.length) {
|
|
2757
|
+
const slot = container[CONTAINER_SLOT_XF_CLEAR];
|
|
2758
|
+
if (selectionFromChart && typeof slot === "function") {
|
|
2759
|
+
try {
|
|
2760
|
+
slot();
|
|
2761
|
+
} catch {
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
if (current && current.length) notify([], "user");
|
|
2765
|
+
}
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
const rows = parseRowIdxs(el.getAttribute(ROW_IDX_ATTR));
|
|
2769
|
+
if (!rows.length) return;
|
|
2770
|
+
const tick = el.classList?.contains?.(AXIS_FILTER_CLASS) ? el : null;
|
|
2771
|
+
const multi = !!(e?.ctrlKey || e?.metaKey || e?.shiftKey);
|
|
2772
|
+
const next = nextSelection(current, rows, { ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey });
|
|
2773
|
+
const toggledOff = !multi && next.length === 0;
|
|
2774
|
+
setClickedMarks(nextClickedMarks(
|
|
2775
|
+
clickedMarks(),
|
|
2776
|
+
current,
|
|
2777
|
+
markKeyOf(el),
|
|
2778
|
+
next,
|
|
2779
|
+
{ ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey }
|
|
2780
|
+
));
|
|
2781
|
+
notify(next, "user", toggledOff ? null : tick);
|
|
2782
|
+
};
|
|
2783
|
+
container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
|
|
2784
|
+
container.addEventListener("click", onClick);
|
|
2785
|
+
container.classList?.add(HOST_CONTAINER_CLASS);
|
|
2786
|
+
ensureAffordanceStyles();
|
|
2787
|
+
const fitOpts = config.fit ?? true;
|
|
2788
|
+
const labelContrastOpts = config.labelContrast ?? true;
|
|
2789
|
+
const stopAnim = () => {
|
|
2790
|
+
const s = container[CONTAINER_SLOT_ANIM_STOP];
|
|
2791
|
+
if (typeof s === "function") {
|
|
2792
|
+
try {
|
|
2793
|
+
s();
|
|
2794
|
+
} catch {
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
const runPostRenderPasses = () => {
|
|
2799
|
+
try {
|
|
2800
|
+
const r = scopeChartIds(container, idScope);
|
|
2801
|
+
if (r.collisions.length && !warnedIds) {
|
|
2802
|
+
warnedIds = true;
|
|
2803
|
+
try {
|
|
2804
|
+
(win?.console ?? console)?.warn(
|
|
2805
|
+
`[@bicharts/chart-host] ids shared with another chart, scoped: ${r.collisions.slice(0, 5).join(", ")}`
|
|
2806
|
+
);
|
|
2807
|
+
} catch {
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
config.onIdScope?.(r);
|
|
2811
|
+
} catch {
|
|
2812
|
+
}
|
|
2813
|
+
if (labelContrastOpts !== false) {
|
|
2814
|
+
try {
|
|
2815
|
+
const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
|
|
2816
|
+
if (!lc.pageBg) {
|
|
2817
|
+
const bg = resolved.backgroundColor ?? resolved.themeBg;
|
|
2818
|
+
if (typeof bg === "string" && bg) lc.pageBg = bg;
|
|
2819
|
+
}
|
|
2820
|
+
const r = applyLabelContrast(container, lc);
|
|
2821
|
+
config.onLabelContrast?.(r);
|
|
2822
|
+
} catch {
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
ensureCrossfilterHitTargets(container, doc);
|
|
2826
|
+
paintSelection();
|
|
2827
|
+
try {
|
|
2828
|
+
const census = censusMarks(container);
|
|
2829
|
+
if (isBlankRender({
|
|
2830
|
+
markCount: census.markCount,
|
|
2831
|
+
rows: data.rows ? data.rows.length : 0,
|
|
2832
|
+
animated: config.animated,
|
|
2833
|
+
contractUntagged: config.contractUntagged,
|
|
2834
|
+
declaresNoRowMarks: census.declaresNoRowMarks
|
|
2835
|
+
})) {
|
|
2836
|
+
if (!warnedBlank) {
|
|
2837
|
+
warnedBlank = true;
|
|
2838
|
+
try {
|
|
2839
|
+
(win?.console ?? console)?.warn(
|
|
2840
|
+
`[@bicharts/chart-host] this chart rendered without error but painted no data marks, against ${data.rows.length} row(s) \u2014 so the container is empty. The usual cause is that the generated code binds a column BY NAME that is no longer in the data, sending it down its own "no data" branch. Check that the columns in your table still match the ones the chart was generated for.`
|
|
2841
|
+
);
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
|
|
2846
|
+
}
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
if (config.onHitBandCensus) {
|
|
2850
|
+
try {
|
|
2851
|
+
config.onHitBandCensus(censusHitBands(container, doc));
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
if (config.onColourSpreadCensus) {
|
|
2856
|
+
try {
|
|
2857
|
+
config.onColourSpreadCensus(censusColourSpread(container, doc));
|
|
2858
|
+
} catch {
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (config.onClippedTextCensus) {
|
|
2862
|
+
try {
|
|
2863
|
+
config.onClippedTextCensus(censusClippedText(container));
|
|
2864
|
+
} catch {
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (config.onValuePlacementCensus) {
|
|
2868
|
+
try {
|
|
2869
|
+
config.onValuePlacementCensus(censusValuePlacement(container, data));
|
|
2870
|
+
} catch {
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
if (fitOpts !== false) {
|
|
2874
|
+
try {
|
|
2875
|
+
const r = fitRenderedChart(container, fitOpts === true ? {} : fitOpts);
|
|
2876
|
+
config.onFit?.(r);
|
|
2877
|
+
} catch {
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
drawAnnotations();
|
|
2882
|
+
} catch {
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
const renderFailure = (err) => {
|
|
2886
|
+
const message = String(err?.message ?? err ?? "");
|
|
2887
|
+
if (config.onInvalidSentinel && isInvalidSentinelError(message)) {
|
|
2888
|
+
stopAnim();
|
|
2889
|
+
container.innerHTML = "";
|
|
2890
|
+
config.onInvalidSentinel({ reason: invalidSentinelReason(message), message });
|
|
2891
|
+
return null;
|
|
2892
|
+
}
|
|
2893
|
+
return explainRenderFailure(err, d3);
|
|
2894
|
+
};
|
|
2895
|
+
const host = {
|
|
2896
|
+
render() {
|
|
2897
|
+
if (destroyed) return;
|
|
2898
|
+
const seq2 = ++renderSeq;
|
|
2899
|
+
stopAnim();
|
|
2900
|
+
container.innerHTML = "";
|
|
2901
|
+
if (!renderFn) {
|
|
2902
|
+
if (!config.code) throw new Error("createChartHost: provide code or renderFn");
|
|
2903
|
+
renderFn = compileRenderFn(config.code, win, doc, d3);
|
|
2904
|
+
}
|
|
2905
|
+
if (resolved.geo === void 0 && config.geoKind) {
|
|
2906
|
+
const g = config.geoProvider?.(config.geoKind) ?? geoFromCache(config.geoKind);
|
|
2907
|
+
if (g) resolved = { ...resolved, geo: g };
|
|
2908
|
+
else if (!warnedGeo) {
|
|
2909
|
+
warnedGeo = true;
|
|
2910
|
+
try {
|
|
2911
|
+
(win?.console ?? console)?.warn(
|
|
2912
|
+
`[@bicharts/chart-host] geoKind "${config.geoKind}" was set but no geometry is available, so this chart will draw without a basemap. Either \`await loadGeo("${config.geoKind}")\` before rendering, pass a geoProvider, or import { geoForKind } from "@bicharts/chart-host/geo" and set options.geo.`
|
|
2913
|
+
);
|
|
2914
|
+
} catch {
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (resolved.geoUnmatched === void 0 && data.geoUnmatched) resolved = { ...resolved, geoUnmatched: data.geoUnmatched };
|
|
2919
|
+
if (resolved.geoPoint === void 0 && data.geoPoint) resolved = { ...resolved, geoPoint: data.geoPoint };
|
|
2920
|
+
if (resolved.geoPointDest === void 0 && data.geoPointDest) resolved = { ...resolved, geoPointDest: data.geoPointDest };
|
|
2921
|
+
if (resolved.dateCellsAreUtcDays === void 0 && data.dateCellsAreUtcDays) resolved = { ...resolved, dateCellsAreUtcDays: true };
|
|
2922
|
+
let outcome;
|
|
2923
|
+
try {
|
|
2924
|
+
outcome = renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
const fatal = renderFailure(err);
|
|
2927
|
+
if (fatal) {
|
|
2928
|
+
settle(Promise.reject(fatal));
|
|
2929
|
+
throw fatal;
|
|
2930
|
+
}
|
|
2931
|
+
settle(Promise.resolve());
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (!isThenable(outcome)) {
|
|
2935
|
+
runPostRenderPasses();
|
|
2936
|
+
settle(Promise.resolve());
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
settle(new Promise((resolve, reject) => {
|
|
2940
|
+
const timer = setTimeout(() => {
|
|
2941
|
+
reject(new Error(
|
|
2942
|
+
`[@bicharts/chart-host] the chart did not finish drawing within ${Math.round(ASYNC_RENDER_TIMEOUT_MS / 1e3)}s, so the host stopped waiting for it. Nothing past that point ran: no label-contrast pass, no hit-target heal, no census, no fit. A render() that returns a promise has to settle it.`
|
|
2943
|
+
));
|
|
2944
|
+
}, ASYNC_RENDER_TIMEOUT_MS);
|
|
2945
|
+
const done = (fn) => {
|
|
2946
|
+
clearTimeout(timer);
|
|
2947
|
+
fn();
|
|
2948
|
+
};
|
|
2949
|
+
Promise.resolve(outcome).then(
|
|
2950
|
+
() => done(() => {
|
|
2951
|
+
if (destroyed || seq2 !== renderSeq) {
|
|
2952
|
+
resolve();
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
runPostRenderPasses();
|
|
2956
|
+
resolve();
|
|
2957
|
+
}),
|
|
2958
|
+
(err) => done(() => {
|
|
2959
|
+
if (destroyed || seq2 !== renderSeq) {
|
|
2960
|
+
resolve();
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const fatal = renderFailure(err);
|
|
2964
|
+
if (fatal) reject(fatal);
|
|
2965
|
+
else resolve();
|
|
2966
|
+
})
|
|
2967
|
+
);
|
|
2968
|
+
}));
|
|
2969
|
+
},
|
|
2970
|
+
setOptions(partial) {
|
|
2971
|
+
raw = { ...raw, ...partial };
|
|
2972
|
+
resolved = resolveForHost(raw);
|
|
2973
|
+
host.render();
|
|
2974
|
+
},
|
|
2975
|
+
setData(next) {
|
|
2976
|
+
data = next;
|
|
2977
|
+
host.render();
|
|
2978
|
+
},
|
|
2979
|
+
get options() {
|
|
2980
|
+
return resolved;
|
|
2981
|
+
},
|
|
2982
|
+
setAnnotations(list) {
|
|
2983
|
+
annotationLayer.set(list);
|
|
2984
|
+
if (!destroyed) try {
|
|
2985
|
+
drawAnnotations();
|
|
2986
|
+
} catch {
|
|
2987
|
+
}
|
|
2988
|
+
},
|
|
2989
|
+
get annotationReport() {
|
|
2990
|
+
return annotationLayer.report;
|
|
2991
|
+
},
|
|
2992
|
+
// A GETTER, not a captured value: `rendered` names the LATEST render, so a host that
|
|
2993
|
+
// read it once would otherwise hold the promise of a draw long since replaced.
|
|
2994
|
+
get rendered() {
|
|
2995
|
+
return renderedPromise;
|
|
2996
|
+
},
|
|
2997
|
+
selection: {
|
|
2998
|
+
onChange(cb) {
|
|
2999
|
+
subs.add(cb);
|
|
3000
|
+
return () => subs.delete(cb);
|
|
3001
|
+
},
|
|
3002
|
+
clear() {
|
|
3003
|
+
const s = container[CONTAINER_SLOT_XF_CLEAR];
|
|
3004
|
+
if (typeof s === "function") {
|
|
3005
|
+
try {
|
|
3006
|
+
s();
|
|
3007
|
+
} catch {
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
setClickedMarks(null);
|
|
3011
|
+
notify([], "host");
|
|
3012
|
+
},
|
|
3013
|
+
highlight(rowIdxs) {
|
|
3014
|
+
notify(Array.isArray(rowIdxs) ? rowIdxs.slice() : [], "host");
|
|
3015
|
+
},
|
|
3016
|
+
get current() {
|
|
3017
|
+
return current;
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
animation: { stop: stopAnim },
|
|
3021
|
+
initialMark() {
|
|
3022
|
+
return container[CONTAINER_SLOT_INITIAL_XF_MARK] ?? null;
|
|
3023
|
+
},
|
|
3024
|
+
destroy() {
|
|
3025
|
+
if (destroyed) return;
|
|
3026
|
+
destroyed = true;
|
|
3027
|
+
stopAnim();
|
|
3028
|
+
container.removeEventListener(XFILTER_REFRESH_EVENT, onXf);
|
|
3029
|
+
container.removeEventListener("click", onClick);
|
|
3030
|
+
subs.clear();
|
|
3031
|
+
annotationLayer.destroy();
|
|
3032
|
+
unpinScrolledAxis(container);
|
|
3033
|
+
container.innerHTML = "";
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
return host;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/group.ts
|
|
3040
|
+
var EMPTY5 = Object.freeze({ sourceId: null, rows: Object.freeze([]) });
|
|
3041
|
+
function toSourceRows(rowMap, payloadRowIdxs) {
|
|
3042
|
+
if (!rowMap) return payloadRowIdxs.slice();
|
|
3043
|
+
return payloadRowIdxs.map((i) => rowMap[i]).filter((i) => i !== void 0);
|
|
3044
|
+
}
|
|
3045
|
+
function toPayloadRows(rowMap, sourceRowIdxs) {
|
|
3046
|
+
const at = /* @__PURE__ */ new Map();
|
|
3047
|
+
rowMap.forEach((src, k) => {
|
|
3048
|
+
if (!at.has(src)) at.set(src, k);
|
|
3049
|
+
});
|
|
3050
|
+
return sourceRowIdxs.map((s) => at.get(s)).filter((k) => k !== void 0);
|
|
3051
|
+
}
|
|
3052
|
+
var sameRows = (a, b) => {
|
|
3053
|
+
if (a.length !== b.length) return false;
|
|
3054
|
+
const x = a.slice().sort((p, q) => p - q), y = b.slice().sort((p, q) => p - q);
|
|
3055
|
+
return x.every((v, i) => v === y[i]);
|
|
3056
|
+
};
|
|
3057
|
+
function syncMemberSelection(host, selection, id, highlight, incoming, rowMap) {
|
|
3058
|
+
const mine = selection.sourceId === id && selection.rows.length > 0;
|
|
3059
|
+
if (mine) {
|
|
3060
|
+
if (rowMap) {
|
|
3061
|
+
const want = toPayloadRows(rowMap, selection.rows);
|
|
3062
|
+
if (!sameRows(want, host.selection.current ?? [])) host.selection.highlight(want);
|
|
3063
|
+
}
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
if (highlight && incoming && incoming.length) {
|
|
3067
|
+
host.selection.highlight(incoming.slice());
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
if (host.selection.current && host.selection.current.length) host.selection.clear();
|
|
3071
|
+
}
|
|
3072
|
+
function createChartGroup(columns, rows, opts = {}) {
|
|
3073
|
+
let cols = columns;
|
|
3074
|
+
let table = rows;
|
|
3075
|
+
let geo = opts.geo ?? null;
|
|
3076
|
+
let point = opts.point ?? null;
|
|
3077
|
+
let destination = opts.destination ?? null;
|
|
3078
|
+
let selection = EMPTY5;
|
|
3079
|
+
const subs = /* @__PURE__ */ new Set();
|
|
3080
|
+
const members = /* @__PURE__ */ new Set();
|
|
3081
|
+
const notify = (change) => {
|
|
3082
|
+
for (const cb of Array.from(subs)) cb(selection, change);
|
|
3083
|
+
};
|
|
3084
|
+
const incomingFor = (id, filteredBy) => {
|
|
3085
|
+
const s = selection;
|
|
3086
|
+
const forId = s.rows.length && s.sourceId && s.sourceId !== id ? s.rows.slice() : null;
|
|
3087
|
+
if (filteredBy) return s.sourceId === filteredBy ? forId : null;
|
|
3088
|
+
return forId;
|
|
3089
|
+
};
|
|
3090
|
+
const payloadFor = (sourceRowIdxs) => {
|
|
3091
|
+
const idxs = sourceRowIdxs ? sourceRowIdxs.slice() : table.map((_, i) => i);
|
|
3092
|
+
const subset = idxs.map((i) => table[i]);
|
|
3093
|
+
const payload = buildRenderPayload(
|
|
3094
|
+
cols,
|
|
3095
|
+
subset,
|
|
3096
|
+
geo ?? void 0,
|
|
3097
|
+
point ?? void 0,
|
|
3098
|
+
destination ?? void 0
|
|
3099
|
+
);
|
|
3100
|
+
return { payload, rowMap: idxs };
|
|
3101
|
+
};
|
|
3102
|
+
const memberPayload = (id, o = {}) => payloadFor(o.respondsWith === "highlight" ? null : incomingFor(id, o.filteredBy));
|
|
3103
|
+
const group = {
|
|
3104
|
+
get selection() {
|
|
3105
|
+
return selection;
|
|
3106
|
+
},
|
|
3107
|
+
get columns() {
|
|
3108
|
+
return cols;
|
|
3109
|
+
},
|
|
3110
|
+
get rows() {
|
|
3111
|
+
return table;
|
|
3112
|
+
},
|
|
3113
|
+
incomingFor,
|
|
3114
|
+
payloadFor,
|
|
3115
|
+
memberPayload,
|
|
3116
|
+
publish(id, sourceRowIdxs) {
|
|
3117
|
+
selection = sourceRowIdxs.length ? { sourceId: id, rows: sourceRowIdxs.slice() } : { sourceId: null, rows: [] };
|
|
3118
|
+
notify(sourceRowIdxs.length ? "select" : "clear");
|
|
3119
|
+
},
|
|
3120
|
+
clear() {
|
|
3121
|
+
selection = { sourceId: null, rows: [] };
|
|
3122
|
+
notify("clear");
|
|
3123
|
+
},
|
|
3124
|
+
setSource(nextColumns, nextRows, o = {}) {
|
|
3125
|
+
cols = nextColumns;
|
|
3126
|
+
table = nextRows;
|
|
3127
|
+
geo = o.geo ?? null;
|
|
3128
|
+
point = o.point ?? null;
|
|
3129
|
+
destination = o.destination ?? null;
|
|
3130
|
+
notify("source");
|
|
3131
|
+
},
|
|
3132
|
+
onChange(cb) {
|
|
3133
|
+
subs.add(cb);
|
|
3134
|
+
return () => {
|
|
3135
|
+
subs.delete(cb);
|
|
3136
|
+
};
|
|
3137
|
+
},
|
|
3138
|
+
attach(id, host, o = {}) {
|
|
3139
|
+
const highlight = o.respondsWith === "highlight";
|
|
3140
|
+
let rowMap = memberPayload(id, o).rowMap;
|
|
3141
|
+
const filterKey = (incoming) => highlight || !incoming ? "*" : incoming.join(",");
|
|
3142
|
+
let drawnUnder = filterKey(incomingFor(id, o.filteredBy));
|
|
3143
|
+
const offHost = host.selection.onChange((payloadIdxs, source) => {
|
|
3144
|
+
if (source === "host") return;
|
|
3145
|
+
const sourceIdxs = toSourceRows(rowMap, payloadIdxs);
|
|
3146
|
+
if (id) group.publish(id, sourceIdxs);
|
|
3147
|
+
o.onSelect?.(sourceIdxs);
|
|
3148
|
+
});
|
|
3149
|
+
const offGroup = group.onChange((sel, change) => {
|
|
3150
|
+
const incoming = incomingFor(id, o.filteredBy);
|
|
3151
|
+
const key = filterKey(incoming);
|
|
3152
|
+
if (change === "source" || key !== drawnUnder) {
|
|
3153
|
+
const next = payloadFor(highlight ? null : incoming);
|
|
3154
|
+
rowMap = next.rowMap;
|
|
3155
|
+
drawnUnder = key;
|
|
3156
|
+
host.setData(next.payload);
|
|
3157
|
+
}
|
|
3158
|
+
syncMemberSelection(host, sel, id, highlight, incoming, rowMap);
|
|
3159
|
+
});
|
|
3160
|
+
const member = {
|
|
3161
|
+
get rowMap() {
|
|
3162
|
+
return rowMap;
|
|
3163
|
+
},
|
|
3164
|
+
detach() {
|
|
3165
|
+
offHost();
|
|
3166
|
+
offGroup();
|
|
3167
|
+
members.delete(member);
|
|
3168
|
+
}
|
|
3169
|
+
};
|
|
3170
|
+
members.add(member);
|
|
3171
|
+
return member;
|
|
3172
|
+
},
|
|
3173
|
+
destroy() {
|
|
3174
|
+
for (const m of Array.from(members)) m.detach();
|
|
3175
|
+
subs.clear();
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
return group;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
export {
|
|
3182
|
+
DARK_TEXT,
|
|
3183
|
+
LIGHT_TEXT,
|
|
3184
|
+
MIN_CONTRAST,
|
|
3185
|
+
WHITE_TEXT_BG_LUM,
|
|
3186
|
+
PILL_MIN_ALPHA,
|
|
3187
|
+
PILL_OPAQUE_ALPHA,
|
|
3188
|
+
PAGE_MATCH_TOLERANCE,
|
|
3189
|
+
isPillBackdropAlpha,
|
|
3190
|
+
PILL_MIN_COVERAGE,
|
|
3191
|
+
pillBacksGlyph,
|
|
3192
|
+
BACKING_MAJORITY,
|
|
3193
|
+
backingHoldsGlyph,
|
|
3194
|
+
cellSuppressesNormalize,
|
|
3195
|
+
toRGBA,
|
|
3196
|
+
compositeOver,
|
|
3197
|
+
relativeLuminance,
|
|
3198
|
+
contrastRatio,
|
|
3199
|
+
decideLabelColor,
|
|
3200
|
+
GLYPH_SAMPLE_N,
|
|
3201
|
+
glyphSampleGrid,
|
|
3202
|
+
STRADDLE_COLS,
|
|
3203
|
+
STRADDLE_ROWS,
|
|
3204
|
+
STRADDLE_MIN_SHARE,
|
|
3205
|
+
STRADDLE_TARGET_CONTRAST,
|
|
3206
|
+
STRADDLE_PILL_ALPHA,
|
|
3207
|
+
straddleSampleGrid,
|
|
3208
|
+
straddles,
|
|
3209
|
+
minContrastOver,
|
|
3210
|
+
pickColorOver,
|
|
3211
|
+
deltaEHex,
|
|
3212
|
+
SAME_SHADE_DELTA_E,
|
|
3213
|
+
MIN_RAMP_FILLS,
|
|
3214
|
+
censusColourSpread,
|
|
3215
|
+
colourSpreadFlag,
|
|
3216
|
+
AUTO_RAMP_CLASSES,
|
|
3217
|
+
AUTO_RAMP_DRAWN_OPACITY,
|
|
3218
|
+
AUTO_RAMP_MIN_STEP_DELTA_E,
|
|
3219
|
+
autoRampMinStep,
|
|
3220
|
+
autoRampCanvas,
|
|
3221
|
+
resolveAutoColorScale,
|
|
3222
|
+
resolveOptions,
|
|
3223
|
+
geoAssetFor,
|
|
3224
|
+
loadGeo,
|
|
3225
|
+
geoFromCache,
|
|
3226
|
+
registerGeo,
|
|
3227
|
+
registerGeoAsset,
|
|
3228
|
+
clearGeoCache,
|
|
3229
|
+
buildRenderPayload,
|
|
3230
|
+
stripJsComments,
|
|
3231
|
+
censusMarks,
|
|
3232
|
+
isBlankRender,
|
|
3233
|
+
blankRenderFlag,
|
|
3234
|
+
CLIP_TOLERANCE_PX,
|
|
3235
|
+
censusClippedText,
|
|
3236
|
+
clippedTextFlag,
|
|
3237
|
+
VALUE_AXIS_MIN_FIT,
|
|
3238
|
+
censusValuePlacement,
|
|
3239
|
+
valuePlacementFlag,
|
|
3240
|
+
SCROLL_SLACK_PX,
|
|
3241
|
+
needsScroll,
|
|
3242
|
+
contentExtentOf,
|
|
3243
|
+
scrollFitFor,
|
|
3244
|
+
FIT_CONTENT_SELECTOR,
|
|
3245
|
+
PHANTOM_FRACTION,
|
|
3246
|
+
isPhantomBox,
|
|
3247
|
+
MAX_FRAME_GROW_FACTOR,
|
|
3248
|
+
planFrameGrow,
|
|
3249
|
+
AXIS_PIN_MIN_LABELS,
|
|
3250
|
+
AXIS_PIN_BAND_PAD_PX,
|
|
3251
|
+
AXIS_PIN_MAX_BAND_FRACTION,
|
|
3252
|
+
AXIS_PIN_TRACK_REACH_PX,
|
|
3253
|
+
isHorizontalLabelRow,
|
|
3254
|
+
labelBand,
|
|
3255
|
+
planAxisPin,
|
|
3256
|
+
axisPinPlacement,
|
|
3257
|
+
ctmScaleOf,
|
|
3258
|
+
svgInkReach,
|
|
3259
|
+
measureContainerBoxes,
|
|
3260
|
+
fitReadingFor,
|
|
3261
|
+
unpinScrolledAxis,
|
|
3262
|
+
pinScrolledAxis,
|
|
3263
|
+
fitRenderedChart,
|
|
3264
|
+
LABEL_CONTRAST_DONE_ATTR,
|
|
3265
|
+
LABEL_CONTRAST_CAP,
|
|
3266
|
+
LABEL_PILL_CLASS,
|
|
3267
|
+
applyLabelContrast,
|
|
3268
|
+
newIdScope,
|
|
3269
|
+
scopeChartIds,
|
|
3270
|
+
isInvalidSentinelError,
|
|
3271
|
+
invalidSentinelReason,
|
|
3272
|
+
ANNOTATION_LAYER_CLASS,
|
|
3273
|
+
ANNOTATION_CLASS,
|
|
3274
|
+
sessionViewStateProvider,
|
|
3275
|
+
noopViewStateProvider,
|
|
3276
|
+
assembleD3,
|
|
3277
|
+
D3_PLUGIN_PACKAGES,
|
|
3278
|
+
requiredD3Plugins,
|
|
3279
|
+
explainRenderFailure,
|
|
3280
|
+
stripEsmExports,
|
|
3281
|
+
compileRenderFn,
|
|
3282
|
+
createChartHost,
|
|
3283
|
+
toSourceRows,
|
|
3284
|
+
syncMemberSelection,
|
|
3285
|
+
createChartGroup
|
|
3286
|
+
};
|