@bicharts/chart-host 0.5.26 → 0.5.28
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/{chunk-PAX7FM4P.mjs → chunk-5DV7OOBW.mjs} +40 -40
- package/dist/index.mjs +139 -2
- package/dist/react.mjs +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/review.d.ts +89 -0
- package/dist/types/snapshot.d.ts +32 -0
- package/package.json +1 -1
|
@@ -425,20 +425,20 @@ function A3(n) {
|
|
|
425
425
|
if (r && i && c3(r) !== i && (r = null), n.city !== void 0 && n.city !== null) {
|
|
426
426
|
let a2 = u(String(n.city)), s = O(a2), f = s ? F(s, n.mapKind) : null;
|
|
427
427
|
if (f && f.length) {
|
|
428
|
-
let d = n.state === void 0 || n.state === null ? "" : String(n.state).trim(), R = d ? u(d) : "", g = d ? Y(d) : null,
|
|
428
|
+
let d = n.state === void 0 || n.state === null ? "" : String(n.state).trim(), R = d ? u(d) : "", g = d ? Y(d) : null, S2 = [], m2 = -1;
|
|
429
429
|
for (let c of f) {
|
|
430
430
|
let l = c.row, A = 0;
|
|
431
431
|
if (d && l.a1 !== "") if (g && l.a1 === g || u(l.a1) === R) A++;
|
|
432
432
|
else continue;
|
|
433
433
|
if (i) if (T(l.cc) === i) A++;
|
|
434
434
|
else continue;
|
|
435
|
-
A > m2 ? (m2 = A,
|
|
435
|
+
A > m2 ? (m2 = A, S2 = [c]) : A === m2 && S2.push(c);
|
|
436
436
|
}
|
|
437
|
-
|
|
437
|
+
S2.some((c) => c.primary) && (S2 = S2.filter((c) => c.primary));
|
|
438
438
|
let p2 = /* @__PURE__ */ new Map();
|
|
439
|
-
for (let c of
|
|
439
|
+
for (let c of S2) p2.set(c.row.lon.toFixed(2) + "," + c.row.lat.toFixed(2), c.row);
|
|
440
440
|
if (p2.size === 1) {
|
|
441
|
-
let c =
|
|
441
|
+
let c = S2[0].row;
|
|
442
442
|
return { lat: c.lat, lon: c.lon, precision: "city" };
|
|
443
443
|
}
|
|
444
444
|
if (p2.size > 1) return { ambiguous: true, matches: p2.size };
|
|
@@ -461,7 +461,7 @@ function A3(n) {
|
|
|
461
461
|
var G = { latlon: 0, city: 1, zip3: 2, state: 3, country: 4 };
|
|
462
462
|
var x = 8;
|
|
463
463
|
function y3(n, e) {
|
|
464
|
-
let t = new Array(n.length), o = new Array(n.length), i = new Array(n.length), r = /* @__PURE__ */ new Set(), a2 = [], s = /* @__PURE__ */ new Set(), f = [], d = /* @__PURE__ */ new Set(), R = [], g = { latlon: 0, city: 0, zip3: 0, state: 0, country: 0 },
|
|
464
|
+
let t = new Array(n.length), o = new Array(n.length), i = new Array(n.length), r = /* @__PURE__ */ new Set(), a2 = [], s = /* @__PURE__ */ new Set(), f = [], d = /* @__PURE__ */ new Set(), R = [], g = { latlon: 0, city: 0, zip3: 0, state: 0, country: 0 }, S2 = 0, m2 = 0, p2 = null;
|
|
465
465
|
for (let l = 0; l < n.length; ++l) {
|
|
466
466
|
let A = n[l], C2 = A3({ ...A, mapKind: e });
|
|
467
467
|
if (S3(C2)) {
|
|
@@ -469,7 +469,7 @@ function y3(n, e) {
|
|
|
469
469
|
let M = K(A);
|
|
470
470
|
M && !d.has(M.toLowerCase()) && (d.add(M.toLowerCase()), R.length < x && R.push(M));
|
|
471
471
|
} else if (C2) {
|
|
472
|
-
if (t[l] = C2.lat, o[l] = C2.lon, i[l] = C2.precision,
|
|
472
|
+
if (t[l] = C2.lat, o[l] = C2.lon, i[l] = C2.precision, S2++, g[C2.precision]++, (p2 === null || G[C2.precision] > G[p2]) && (p2 = C2.precision), G[C2.precision] > G.city) {
|
|
473
473
|
let M = K(A);
|
|
474
474
|
M && !s.has(M.toLowerCase()) && (s.add(M.toLowerCase()), f.length < x && f.push(M));
|
|
475
475
|
}
|
|
@@ -480,7 +480,7 @@ function y3(n, e) {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
let c = !n3() && n.some((l) => l.city !== null && l.city !== void 0 && String(l.city).trim() !== "");
|
|
483
|
-
return { lat: t, lon: o, precisions: i, precision: p2, precisionCounts: g, coarseExamples: f, unmatched: a2, ambiguousRows: m2, ambiguousExamples: R, matchedRows:
|
|
483
|
+
return { lat: t, lon: o, precisions: i, precision: p2, precisionCounts: g, coarseExamples: f, unmatched: a2, ambiguousRows: m2, ambiguousExamples: R, matchedRows: S2, totalRows: n.length, ...c ? { cityTableMissing: true } : {} };
|
|
484
484
|
}
|
|
485
485
|
function K(n) {
|
|
486
486
|
let e = [n.city, n.state, n.zip, n.country].filter((t) => t != null && String(t).trim() !== "").map((t) => String(t).trim()).join(", ");
|
|
@@ -561,11 +561,11 @@ function wn(n) {
|
|
|
561
561
|
}
|
|
562
562
|
function Kn(n, s) {
|
|
563
563
|
let i = new Array(n.length), r = /* @__PURE__ */ new Set(), A = [], y2 = 0;
|
|
564
|
-
for (let
|
|
565
|
-
let O3 = hn(n[
|
|
566
|
-
if (i[
|
|
564
|
+
for (let S2 = 0; S2 < n.length; ++S2) {
|
|
565
|
+
let O3 = hn(n[S2], s);
|
|
566
|
+
if (i[S2] = O3, O3 !== null) y2++;
|
|
567
567
|
else {
|
|
568
|
-
let C2 = n[
|
|
568
|
+
let C2 = n[S2];
|
|
569
569
|
if (C2 != null) {
|
|
570
570
|
let d = String(C2).trim();
|
|
571
571
|
if (d.length > 0) {
|
|
@@ -612,14 +612,14 @@ function E2(r) {
|
|
|
612
612
|
for (let s of n) N2(s) && e++;
|
|
613
613
|
return Math.round(e / n.length * 1e3) / 10;
|
|
614
614
|
}
|
|
615
|
-
function
|
|
615
|
+
function S(r) {
|
|
616
616
|
let n = P2(r);
|
|
617
617
|
if (n.length === 0) return 0;
|
|
618
618
|
let e = 0;
|
|
619
619
|
for (let s of n) Y(s) && e++;
|
|
620
620
|
return Math.round(e / n.length * 1e3) / 10;
|
|
621
621
|
}
|
|
622
|
-
function
|
|
622
|
+
function D2(r) {
|
|
623
623
|
let n = /* @__PURE__ */ new Set();
|
|
624
624
|
for (let e of r) {
|
|
625
625
|
if (e == null) continue;
|
|
@@ -629,16 +629,16 @@ function B2(r) {
|
|
|
629
629
|
return Array.from(n);
|
|
630
630
|
}
|
|
631
631
|
function O2(r) {
|
|
632
|
-
let n =
|
|
632
|
+
let n = D2(r);
|
|
633
633
|
if (n.length === 0) return 0;
|
|
634
634
|
let e = 0;
|
|
635
635
|
for (let s of n) Z(s).length > 0 && e++;
|
|
636
636
|
return Math.round(e / n.length * 1e3) / 10;
|
|
637
637
|
}
|
|
638
638
|
function Z2(r, n) {
|
|
639
|
-
return p(n, en) ? true :
|
|
639
|
+
return p(n, en) ? true : D2(r).some((e) => /^\d{5}(-\d{4})?$/.test(e.replace(/\.0+$/, "")));
|
|
640
640
|
}
|
|
641
|
-
function
|
|
641
|
+
function Gt(r, n) {
|
|
642
642
|
let e = P2(r);
|
|
643
643
|
if (e.length === 0) return 0;
|
|
644
644
|
let s = 0;
|
|
@@ -650,13 +650,13 @@ function $2(r, n) {
|
|
|
650
650
|
for (let s of P2(r)) n(s) && e++;
|
|
651
651
|
return e;
|
|
652
652
|
}
|
|
653
|
-
function
|
|
653
|
+
function _t(r, n) {
|
|
654
654
|
let e = 0;
|
|
655
|
-
for (let s of
|
|
655
|
+
for (let s of D2(r)) n(s) && e++;
|
|
656
656
|
return e;
|
|
657
657
|
}
|
|
658
|
-
function
|
|
659
|
-
let l = [], c = [], o = {},
|
|
658
|
+
function wt(r, n, e, s) {
|
|
659
|
+
let l = [], c = [], o = {}, v2 = {}, k2 = new Set(r), u2 = (t) => n.map((i) => i[t]);
|
|
660
660
|
for (let t of ["city", "lat", "lon", "country"]) {
|
|
661
661
|
let i = e?.[t];
|
|
662
662
|
i && k2.has(i) && (o[t] = i);
|
|
@@ -669,25 +669,25 @@ function _t(r, n, e, s) {
|
|
|
669
669
|
let m2 = e?.state;
|
|
670
670
|
if (m2 && k2.has(m2)) {
|
|
671
671
|
let t = u2(m2), i = P2(t);
|
|
672
|
-
i.length > 0 && i.every((a2) => q2(a2, s)) ? c.push(`state=${m2} (every value is "CA", which is both Canada and California)`) : C(t) ? (c.push(`state=${m2} (every value is a country, not a state)`), o.country || (o.country = m2)) :
|
|
672
|
+
i.length > 0 && i.every((a2) => q2(a2, s)) ? c.push(`state=${m2} (every value is "CA", which is both Canada and California)`) : C(t) ? (c.push(`state=${m2} (every value is a country, not a state)`), o.country || (o.country = m2)) : S(t) < 95 ? c.push(`state=${m2} (only ${S(t)}% of values are states/provinces)`) : o.state = m2;
|
|
673
673
|
}
|
|
674
|
-
let d = new Set(Object.values(o).filter(Boolean)),
|
|
674
|
+
let d = new Set(Object.values(o).filter(Boolean)), x2 = r.filter((t) => !d.has(t) && !t.startsWith("__"));
|
|
675
675
|
if (!o.state) {
|
|
676
676
|
let t = null;
|
|
677
|
-
for (let i of
|
|
677
|
+
for (let i of x2) {
|
|
678
678
|
let a2 = u2(i);
|
|
679
679
|
if (C(a2)) continue;
|
|
680
|
-
let f =
|
|
680
|
+
let f = S(a2);
|
|
681
681
|
if (f < 95) continue;
|
|
682
|
-
let z2 = $2(a2, (
|
|
682
|
+
let z2 = $2(a2, (b2) => !!Y(b2)), W2 = $2(a2, (b2) => !!Y(b2) && !q2(b2, s));
|
|
683
683
|
z2 < w && W2 < 1 || (!t || f > t.pct) && (t = { name: i, pct: f });
|
|
684
684
|
}
|
|
685
685
|
t && (o.state = t.name, d.add(t.name), l.push(`state=${t.name} (${t.pct}% states/provinces)`));
|
|
686
686
|
}
|
|
687
|
-
if (!o.zip) for (let t of
|
|
687
|
+
if (!o.zip) for (let t of x2) {
|
|
688
688
|
if (d.has(t)) continue;
|
|
689
689
|
let i = u2(t), a2 = O2(i);
|
|
690
|
-
if (!(a2 < 95) && Z2(i, t) && !(
|
|
690
|
+
if (!(a2 < 95) && Z2(i, t) && !(_t(i, (f) => Z(f).length > 0) < w)) {
|
|
691
691
|
o.zip = t, d.add(t), l.push(`zip=${t} (${a2}% ZIP codes)`);
|
|
692
692
|
break;
|
|
693
693
|
}
|
|
@@ -695,27 +695,27 @@ function _t(r, n, e, s) {
|
|
|
695
695
|
let h2 = o.country;
|
|
696
696
|
if (!h2 || !C(u2(h2))) {
|
|
697
697
|
let t = null;
|
|
698
|
-
for (let i of
|
|
698
|
+
for (let i of x2) {
|
|
699
699
|
if (d.has(i) || !C(u2(i))) continue;
|
|
700
700
|
let a2 = E2(u2(i));
|
|
701
701
|
(!t || a2 > t.pct) && (t = { name: i, pct: a2 });
|
|
702
702
|
}
|
|
703
|
-
t && (h2 && (c.push(`country=${h2} (only ${E2(u2(h2))}% of values are country identifiers; using ${t.name} instead)`),
|
|
703
|
+
t && (h2 && (c.push(`country=${h2} (only ${E2(u2(h2))}% of values are country identifiers; using ${t.name} instead)`), v2.country = h2), o.country = t.name, d.add(t.name), l.push(`country=${t.name}`));
|
|
704
704
|
}
|
|
705
705
|
if (!o.city) {
|
|
706
706
|
let t = null;
|
|
707
|
-
for (let i of
|
|
707
|
+
for (let i of x2) {
|
|
708
708
|
if (d.has(i)) continue;
|
|
709
|
-
let a2 = u2(i), f =
|
|
709
|
+
let a2 = u2(i), f = Gt(a2, s);
|
|
710
710
|
f < m3 || $2(a2, (z2) => p3(z2, s)) < w || (!t || f > t.pct) && (t = { name: i, pct: f });
|
|
711
711
|
}
|
|
712
712
|
t && (o.city = t.name, l.push(`city=${t.name} (${t.pct}% known cities)`));
|
|
713
713
|
}
|
|
714
|
-
let
|
|
714
|
+
let B2 = o.country ? u2(o.country) : [], I2 = !!o.country && C(B2), U2 = I2 ? new Set(B2.map((t) => E(t == null ? null : String(t))).filter(Boolean)).size : 0, F2 = I2 && U2 >= 2, L2 = !!(o.lat && o.lon) && n.some((t) => {
|
|
715
715
|
let i = +String(t[o.lat] ?? "").trim(), a2 = +String(t[o.lon] ?? "").trim();
|
|
716
716
|
return isFinite(i) && isFinite(a2) && i >= -90 && i <= 90 && a2 >= -180 && a2 <= 180 && String(t[o.lat] ?? "").trim() !== "" && String(t[o.lon] ?? "").trim() !== "";
|
|
717
717
|
});
|
|
718
|
-
return o.lat && o.lon && !L2 && c.push(`lat/lon=${o.lat}/${o.lon} (no row holds a usable coordinate pair)`), o.country && !F2 && !(o.city || o.state || o.zip || L2) && c.push(I2 ? `country=${o.country} (every row resolves to the same country - nothing to place apart)` : `country=${o.country} (values are not country identifiers)`), { bind: !!(o.city || o.state || o.zip || F2 || L2) ? o : null, backfilled: l, refused: c, ...Object.keys(
|
|
718
|
+
return o.lat && o.lon && !L2 && c.push(`lat/lon=${o.lat}/${o.lon} (no row holds a usable coordinate pair)`), o.country && !F2 && !(o.city || o.state || o.zip || L2) && c.push(I2 ? `country=${o.country} (every row resolves to the same country - nothing to place apart)` : `country=${o.country} (values are not country identifiers)`), { bind: !!(o.city || o.state || o.zip || F2 || L2) ? o : null, backfilled: l, refused: c, ...Object.keys(v2).length ? { labelFallback: v2 } : {} };
|
|
719
719
|
}
|
|
720
720
|
|
|
721
721
|
// src/payload.ts
|
|
@@ -730,7 +730,7 @@ function resolvePointChannel(cols, rowObjs, point) {
|
|
|
730
730
|
let labelCol;
|
|
731
731
|
if (bind) {
|
|
732
732
|
const dims = cols.filter((c) => !c.isMeasure).map((c) => c.name);
|
|
733
|
-
const res =
|
|
733
|
+
const res = wt(dims, rowObjs, bind, point?.mapKind);
|
|
734
734
|
bind = res.bind;
|
|
735
735
|
rolesBackfilled = res.backfilled;
|
|
736
736
|
rolesRefused = res.refused;
|
|
@@ -798,11 +798,11 @@ function buildRenderPayload(cols, rowObjs, geo, point, destination) {
|
|
|
798
798
|
const row = rowObjs[r];
|
|
799
799
|
const a2 = new Array(cols.length + extra);
|
|
800
800
|
for (let c = 0; c < cols.length; c++) {
|
|
801
|
-
const
|
|
802
|
-
if (
|
|
803
|
-
a2[c] =
|
|
801
|
+
const v2 = row[colNames[c]];
|
|
802
|
+
if (v2 instanceof Date) {
|
|
803
|
+
a2[c] = v2.toISOString();
|
|
804
804
|
} else {
|
|
805
|
-
a2[c] =
|
|
805
|
+
a2[c] = v2 ?? null;
|
|
806
806
|
}
|
|
807
807
|
}
|
|
808
808
|
let k2 = cols.length;
|
|
@@ -1071,7 +1071,7 @@ function createChartHost(container, config) {
|
|
|
1071
1071
|
let data = config.data;
|
|
1072
1072
|
{
|
|
1073
1073
|
const stamped = config.data?.meta?.hostContract;
|
|
1074
|
-
const major = (
|
|
1074
|
+
const major = (v2) => (v2 || "").split(".")[0];
|
|
1075
1075
|
if (stamped && major(stamped) !== major(HOST_CONTRACT_VERSION)) {
|
|
1076
1076
|
try {
|
|
1077
1077
|
(win?.console ?? console)?.warn(
|
package/dist/index.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
requiredD3Plugins,
|
|
43
43
|
resolveOptions,
|
|
44
44
|
stripEsmExports
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-5DV7OOBW.mjs";
|
|
46
46
|
import "./chunk-A2GMXZP7.mjs";
|
|
47
47
|
|
|
48
48
|
// src/trivial.ts
|
|
@@ -317,6 +317,135 @@ function histogramSource(idx, name) {
|
|
|
317
317
|
}`;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// src/snapshot.ts
|
|
321
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
322
|
+
function svgToDataUrl(svg) {
|
|
323
|
+
const cloned = svg.cloneNode(true);
|
|
324
|
+
const w = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
325
|
+
const h = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
326
|
+
cloned.setAttribute("width", String(Math.round(w)));
|
|
327
|
+
cloned.setAttribute("height", String(Math.round(h)));
|
|
328
|
+
if (!cloned.getAttribute("xmlns")) cloned.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
329
|
+
const svgStr = new XMLSerializer().serializeToString(cloned);
|
|
330
|
+
const bytes = new TextEncoder().encode(svgStr);
|
|
331
|
+
let binary = "";
|
|
332
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
333
|
+
return "data:image/svg+xml;base64," + btoa(binary);
|
|
334
|
+
}
|
|
335
|
+
function svgNaturalSize(svg) {
|
|
336
|
+
const w = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
337
|
+
const h = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
338
|
+
return { width: Math.round(w), height: Math.round(h) };
|
|
339
|
+
}
|
|
340
|
+
function rasterizeSvgToPngDataUrl(svgDataUrl, width, height, opts = {}) {
|
|
341
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
let img;
|
|
344
|
+
try {
|
|
345
|
+
img = new Image();
|
|
346
|
+
} catch (e) {
|
|
347
|
+
opts.onWarn?.("snapshot-no-image", e instanceof Error ? e.message : String(e));
|
|
348
|
+
resolve(null);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const timer = setTimeout(() => {
|
|
352
|
+
opts.onWarn?.("snapshot-raster-timeout", { width, height, timeoutMs });
|
|
353
|
+
resolve(null);
|
|
354
|
+
}, timeoutMs);
|
|
355
|
+
img.onload = () => {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
try {
|
|
358
|
+
const cap = opts.maxSide && opts.maxSide > 0 ? opts.maxSide : Math.max(width, height);
|
|
359
|
+
const scale = Math.min(1, cap / Math.max(width, height));
|
|
360
|
+
const cw = Math.max(1, Math.round(width * scale));
|
|
361
|
+
const ch = Math.max(1, Math.round(height * scale));
|
|
362
|
+
const canvas = document.createElement("canvas");
|
|
363
|
+
canvas.width = cw;
|
|
364
|
+
canvas.height = ch;
|
|
365
|
+
const c2d = canvas.getContext("2d");
|
|
366
|
+
if (!c2d) {
|
|
367
|
+
resolve(null);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
c2d.fillStyle = "white";
|
|
371
|
+
c2d.fillRect(0, 0, cw, ch);
|
|
372
|
+
c2d.drawImage(img, 0, 0, cw, ch);
|
|
373
|
+
resolve(canvas.toDataURL("image/png"));
|
|
374
|
+
} catch (e) {
|
|
375
|
+
opts.onWarn?.("snapshot-raster-draw", e instanceof Error ? e.message : String(e));
|
|
376
|
+
resolve(null);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
img.onerror = () => {
|
|
380
|
+
clearTimeout(timer);
|
|
381
|
+
opts.onWarn?.("snapshot-raster-decode", { width, height });
|
|
382
|
+
resolve(null);
|
|
383
|
+
};
|
|
384
|
+
img.src = svgDataUrl;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function captureSvgSnapshot(root, opts = {}) {
|
|
388
|
+
if (!root) return null;
|
|
389
|
+
const svg = root instanceof SVGSVGElement ? root : root.querySelector("svg");
|
|
390
|
+
if (!svg) return null;
|
|
391
|
+
let svgUrl;
|
|
392
|
+
try {
|
|
393
|
+
svgUrl = svgToDataUrl(svg);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
opts.onWarn?.("snapshot-serialize", e instanceof Error ? e.message : String(e));
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const { width, height } = svgNaturalSize(svg);
|
|
399
|
+
try {
|
|
400
|
+
const png = await rasterizeSvgToPngDataUrl(svgUrl, width, height, opts);
|
|
401
|
+
return png || svgUrl;
|
|
402
|
+
} catch (e) {
|
|
403
|
+
opts.onWarn?.("snapshot-raster-unexpected", e instanceof Error ? e.message : String(e));
|
|
404
|
+
return svgUrl;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/review.ts
|
|
409
|
+
function shouldReview(g) {
|
|
410
|
+
if (!g.enabled) return false;
|
|
411
|
+
if (!g.modeLicensed) return false;
|
|
412
|
+
if (!g.firstRenderAfterGenerate) return false;
|
|
413
|
+
if (!g.codeVersion || g.codeVersion <= 0) return false;
|
|
414
|
+
if (g.inFlight) return false;
|
|
415
|
+
if (g.reviewedVersions.has(g.codeVersion)) return false;
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
function bareBase64(dataUrlOrB64) {
|
|
419
|
+
const m = /^data:([^;,]+);base64,(.*)$/s.exec(dataUrlOrB64 || "");
|
|
420
|
+
if (m) return { b64: m[2], mediaType: m[1] };
|
|
421
|
+
return { b64: dataUrlOrB64 || "", mediaType: "image/png" };
|
|
422
|
+
}
|
|
423
|
+
function buildReviewWire(input) {
|
|
424
|
+
const { b64, mediaType } = bareBase64(input.capture);
|
|
425
|
+
return {
|
|
426
|
+
request: input.request,
|
|
427
|
+
imageBase64: b64,
|
|
428
|
+
imageMediaType: mediaType,
|
|
429
|
+
version: input.version,
|
|
430
|
+
originalAsk: (input.originalAsk ?? "").toString(),
|
|
431
|
+
codeSummary: (input.codeSummary ?? "").toString(),
|
|
432
|
+
chartType: (input.chartType ?? "").toString(),
|
|
433
|
+
correlationId: (input.correlationId ?? "").toString()
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function actionFor(v) {
|
|
437
|
+
if (!v) return { kind: "keep", reason: "no verdict returned", userVisible: false };
|
|
438
|
+
if (v.status === "PROPOSED") {
|
|
439
|
+
const instr = (v.instruction ?? "").toString().trim();
|
|
440
|
+
if (instr === "") return { kind: "keep", reason: "reviewer proposed no actionable change", userVisible: false };
|
|
441
|
+
return { kind: "propose", instruction: instr, reason: (v.verdictReason ?? "").toString().trim() };
|
|
442
|
+
}
|
|
443
|
+
if (v.status === "OK" && v.fix) return { kind: "apply-fix", fix: v.fix, reason: v.verdictReason ?? "" };
|
|
444
|
+
if (v.status === "NOCHANGE") return { kind: "keep", reason: v.verdictReason ?? "the chart renders as asked", userVisible: false };
|
|
445
|
+
if (v.status === "REFUSED") return { kind: "keep", reason: v.errorMessage ?? "review not available", userVisible: false };
|
|
446
|
+
return { kind: "keep", reason: v.errorMessage ?? v.verdictReason ?? "review failed", userVisible: false };
|
|
447
|
+
}
|
|
448
|
+
|
|
320
449
|
// src/index.ts
|
|
321
450
|
function registerCityTable(packed) {
|
|
322
451
|
L3(packed);
|
|
@@ -349,7 +478,11 @@ export {
|
|
|
349
478
|
ROW_IDX_ATTR,
|
|
350
479
|
SELECTION_ACTIVE_CLASS,
|
|
351
480
|
XFILTER_REFRESH_EVENT,
|
|
481
|
+
actionFor,
|
|
482
|
+
bareBase64,
|
|
352
483
|
buildRenderPayload,
|
|
484
|
+
buildReviewWire,
|
|
485
|
+
captureSvgSnapshot,
|
|
353
486
|
clearGeoCache,
|
|
354
487
|
compileRenderFn,
|
|
355
488
|
compileTrivialSource,
|
|
@@ -360,10 +493,14 @@ export {
|
|
|
360
493
|
geoFromCache,
|
|
361
494
|
loadGeo,
|
|
362
495
|
planTrivialChart,
|
|
496
|
+
rasterizeSvgToPngDataUrl,
|
|
363
497
|
registerCityTable,
|
|
364
498
|
registerGeo,
|
|
365
499
|
registerGeoAsset,
|
|
366
500
|
requiredD3Plugins,
|
|
367
501
|
resolveOptions,
|
|
368
|
-
|
|
502
|
+
shouldReview,
|
|
503
|
+
stripEsmExports,
|
|
504
|
+
svgNaturalSize,
|
|
505
|
+
svgToDataUrl
|
|
369
506
|
};
|
package/dist/react.mjs
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./
|
|
|
8
8
|
export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
|
|
9
9
|
export { createMarkResolver, type MarkResolver, type MarkResolverEnv } from "./selection";
|
|
10
10
|
export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
|
|
11
|
+
export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, type SnapshotOptions } from "./snapshot";
|
|
12
|
+
export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** The facts a "should we review this render?" decision needs, named so policy reads in one place. */
|
|
2
|
+
export interface ReviewGate {
|
|
3
|
+
/** The user's settings toggle. Reviews are strictly opt-in. */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Licensed accounts only — the host checks its own credential shape and says so here. */
|
|
6
|
+
modeLicensed: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* True when THIS render is the first paint after a generate this session. A cached
|
|
9
|
+
* restore, a resize, a cross-filter repaint — none of these are "the first render from a
|
|
10
|
+
* generate", and reviewing them would judge the same pixels twice.
|
|
11
|
+
*/
|
|
12
|
+
firstRenderAfterGenerate: boolean;
|
|
13
|
+
/** The version on screen. 0/undefined = nothing to review. */
|
|
14
|
+
codeVersion: number | null | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Versions this session already submitted. Guards re-entrancy: an applied fix's own first
|
|
17
|
+
* render must not trigger a second review of the fix — a fix of a fix of a fix is a
|
|
18
|
+
* billing loop.
|
|
19
|
+
*/
|
|
20
|
+
reviewedVersions: ReadonlySet<number>;
|
|
21
|
+
/** A review already in flight for this instance. */
|
|
22
|
+
inFlight: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Should a review fire for this render? Every clause is a reason NOT to, because the free
|
|
25
|
+
* direction is "no review". */
|
|
26
|
+
export declare function shouldReview(g: ReviewGate): boolean;
|
|
27
|
+
/** The review endpoint's wire body. `request` is the SAME generate request the host would send
|
|
28
|
+
* for a modify of the reviewed version — the service writes the fix intent into it, so an
|
|
29
|
+
* applied fix is provably an ordinary modify of exactly this request. */
|
|
30
|
+
export interface ReviewWire {
|
|
31
|
+
request: unknown;
|
|
32
|
+
imageBase64: string;
|
|
33
|
+
imageMediaType: string;
|
|
34
|
+
version: number;
|
|
35
|
+
originalAsk: string;
|
|
36
|
+
codeSummary: string;
|
|
37
|
+
chartType: string;
|
|
38
|
+
correlationId: string;
|
|
39
|
+
/**
|
|
40
|
+
* THE SECOND HALF of the propose/accept round-trip. Absent on the judging call; set to the
|
|
41
|
+
* proposal's instruction — verbatim — when the user accepted. Its presence is the whole
|
|
42
|
+
* difference between a free judgement and a billed modify.
|
|
43
|
+
*/
|
|
44
|
+
applyInstruction?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Strip a data-URL prefix if the capture produced one; the wire carries bare base64. */
|
|
47
|
+
export declare function bareBase64(dataUrlOrB64: string): {
|
|
48
|
+
b64: string;
|
|
49
|
+
mediaType: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function buildReviewWire(input: {
|
|
52
|
+
request: unknown;
|
|
53
|
+
capture: string;
|
|
54
|
+
version: number;
|
|
55
|
+
originalAsk: string | null | undefined;
|
|
56
|
+
codeSummary: string | null | undefined;
|
|
57
|
+
chartType: string | null | undefined;
|
|
58
|
+
correlationId: string | null | undefined;
|
|
59
|
+
}): ReviewWire;
|
|
60
|
+
/** What the service answers, camelCase on the wire. */
|
|
61
|
+
export interface ReviewVerdict {
|
|
62
|
+
status: "PROPOSED" | "OK" | "NOCHANGE" | "REFUSED" | "ERROR" | string;
|
|
63
|
+
verdictReason?: string | null;
|
|
64
|
+
/** Set only on PROPOSED: the judge's instruction, sent back verbatim if the user accepts. */
|
|
65
|
+
instruction?: string | null;
|
|
66
|
+
/** A full generate result when status === "OK": the fix IS a modify. */
|
|
67
|
+
fix?: unknown;
|
|
68
|
+
errorMessage?: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** How the host should react to a verdict:
|
|
71
|
+
* - propose → ASK THE USER; nothing has been charged and declining stays free;
|
|
72
|
+
* - apply-fix → the accepted round-trip answered with a finished result — apply and re-render;
|
|
73
|
+
* - keep → chart untouched; log the reason. A REFUSED never surfaces as a user error:
|
|
74
|
+
* the toggle was on for an account that cannot use the feature, and the chart
|
|
75
|
+
* is fine. */
|
|
76
|
+
export type ReviewAction = {
|
|
77
|
+
kind: "propose";
|
|
78
|
+
instruction: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "apply-fix";
|
|
82
|
+
fix: unknown;
|
|
83
|
+
reason: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "keep";
|
|
86
|
+
reason: string;
|
|
87
|
+
userVisible: boolean;
|
|
88
|
+
};
|
|
89
|
+
export declare function actionFor(v: ReviewVerdict | null | undefined): ReviewAction;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface SnapshotOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Cap the raster's longest side, preserving aspect. Keeps a giant viewport from producing
|
|
4
|
+
* a multi-megabyte PNG. 0 = no cap (raster at the SVG's natural size).
|
|
5
|
+
*/
|
|
6
|
+
maxSide?: number;
|
|
7
|
+
/** Deadline for the SVG decode, in ms. Past it the raster resolves null. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** Diagnostic sink for the silent-failure paths. */
|
|
10
|
+
onWarn?: (event: string, detail?: unknown) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Serialize an SVG element to a `data:image/svg+xml;base64,` URL, with intrinsic
|
|
14
|
+
* width/height inlined on a clone so a later Image() load knows its size.
|
|
15
|
+
*/
|
|
16
|
+
export declare function svgToDataUrl(svg: SVGSVGElement): string;
|
|
17
|
+
/** The measured drawing size of an SVG, with the same fallbacks svgToDataUrl inlines. */
|
|
18
|
+
export declare function svgNaturalSize(svg: SVGSVGElement): {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Rasterize an SVG data URL to a PNG data URL. Resolves null on any failure — decode error,
|
|
24
|
+
* deadline, tainted canvas — and never rejects.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rasterizeSvgToPngDataUrl(svgDataUrl: string, width: number, height: number, opts?: SnapshotOptions): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Capture the first <svg> under `root` (or the element itself) as a PNG data URL, falling
|
|
29
|
+
* back to the SVG data URL when rasterizing is unavailable, and to null only when there is
|
|
30
|
+
* genuinely nothing on the canvas to capture.
|
|
31
|
+
*/
|
|
32
|
+
export declare function captureSvgSnapshot(root: Element | null | undefined, opts?: SnapshotOptions): Promise<string | null>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.28",
|
|
4
4
|
"description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|