@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.
@@ -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, S = [], m2 = -1;
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, S = [c]) : A === m2 && S.push(c);
435
+ A > m2 ? (m2 = A, S2 = [c]) : A === m2 && S2.push(c);
436
436
  }
437
- S.some((c) => c.primary) && (S = S.filter((c) => c.primary));
437
+ S2.some((c) => c.primary) && (S2 = S2.filter((c) => c.primary));
438
438
  let p2 = /* @__PURE__ */ new Map();
439
- for (let c of S) p2.set(c.row.lon.toFixed(2) + "," + c.row.lat.toFixed(2), c.row);
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 = S[0].row;
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 }, S = 0, m2 = 0, p2 = null;
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, S++, g[C2.precision]++, (p2 === null || G[C2.precision] > G[p2]) && (p2 = C2.precision), G[C2.precision] > G.city) {
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: S, totalRows: n.length, ...c ? { cityTableMissing: true } : {} };
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 S = 0; S < n.length; ++S) {
565
- let O3 = hn(n[S], s);
566
- if (i[S] = O3, O3 !== null) y2++;
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[S];
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 v2(r) {
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 B2(r) {
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 = B2(r);
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 : B2(r).some((e) => /^\d{5}(-\d{4})?$/.test(e.replace(/\.0+$/, "")));
639
+ return p(n, en) ? true : D2(r).some((e) => /^\d{5}(-\d{4})?$/.test(e.replace(/\.0+$/, "")));
640
640
  }
641
- function zt(r, n) {
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 Gt(r, n) {
653
+ function _t(r, n) {
654
654
  let e = 0;
655
- for (let s of B2(r)) n(s) && e++;
655
+ for (let s of D2(r)) n(s) && e++;
656
656
  return e;
657
657
  }
658
- function _t(r, n, e, s) {
659
- let l = [], c = [], o = {}, T2 = {}, k2 = new Set(r), u2 = (t) => n.map((i) => i[t]);
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)) : v2(t) < 95 ? c.push(`state=${m2} (only ${v2(t)}% of values are states/provinces)`) : o.state = 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)), b2 = r.filter((t) => !d.has(t) && !t.startsWith("__"));
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 b2) {
677
+ for (let i of x2) {
678
678
  let a2 = u2(i);
679
679
  if (C(a2)) continue;
680
- let f = v2(a2);
680
+ let f = S(a2);
681
681
  if (f < 95) continue;
682
- let z2 = $2(a2, (R) => !!Y(R)), W2 = $2(a2, (R) => !!Y(R) && !q2(R, s));
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 b2) {
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) && !(Gt(i, (f) => Z(f).length > 0) < w)) {
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 b2) {
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)`), T2.country = h2), o.country = t.name, d.add(t.name), l.push(`country=${t.name}`));
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 b2) {
707
+ for (let i of x2) {
708
708
  if (d.has(i)) continue;
709
- let a2 = u2(i), f = zt(a2, s);
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 D2 = o.country ? u2(o.country) : [], I2 = !!o.country && C(D2), U2 = I2 ? new Set(D2.map((t) => E(t == null ? null : String(t))).filter(Boolean)).size : 0, F2 = I2 && U2 >= 2, L2 = !!(o.lat && o.lon) && n.some((t) => {
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(T2).length ? { labelFallback: T2 } : {} };
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 = _t(dims, rowObjs, bind, point?.mapKind);
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 v3 = row[colNames[c]];
802
- if (v3 instanceof Date) {
803
- a2[c] = v3.toISOString();
801
+ const v2 = row[colNames[c]];
802
+ if (v2 instanceof Date) {
803
+ a2[c] = v2.toISOString();
804
804
  } else {
805
- a2[c] = v3 ?? null;
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 = (v3) => (v3 || "").split(".")[0];
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-PAX7FM4P.mjs";
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
- stripEsmExports
502
+ shouldReview,
503
+ stripEsmExports,
504
+ svgNaturalSize,
505
+ svgToDataUrl
369
506
  };
package/dist/react.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createChartHost,
5
5
  geoFromCache,
6
6
  loadGeo
7
- } from "./chunk-PAX7FM4P.mjs";
7
+ } from "./chunk-5DV7OOBW.mjs";
8
8
  import "./chunk-A2GMXZP7.mjs";
9
9
 
10
10
  // src/react.tsx
@@ -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.26",
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",