@bicharts/chart-host 0.5.44 → 0.5.46

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.
@@ -433,20 +433,20 @@ function A3(n) {
433
433
  if (r && i && c3(r) !== i && (r = null), n.city !== void 0 && n.city !== null) {
434
434
  let a2 = u(String(n.city)), s = O(a2), f = s ? F(s, n.mapKind) : null;
435
435
  if (f && f.length) {
436
- 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;
436
+ 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;
437
437
  for (let c of f) {
438
438
  let l = c.row, A = 0;
439
439
  if (d && l.a1 !== "") if (g && l.a1 === g || u(l.a1) === R) A++;
440
440
  else continue;
441
441
  if (i) if (T(l.cc) === i) A++;
442
442
  else continue;
443
- A > m2 ? (m2 = A, S2 = [c]) : A === m2 && S2.push(c);
443
+ A > m2 ? (m2 = A, S = [c]) : A === m2 && S.push(c);
444
444
  }
445
- S2.some((c) => c.primary) && (S2 = S2.filter((c) => c.primary));
445
+ S.some((c) => c.primary) && (S = S.filter((c) => c.primary));
446
446
  let p2 = /* @__PURE__ */ new Map();
447
- for (let c of S2) p2.set(c.row.lon.toFixed(2) + "," + c.row.lat.toFixed(2), c.row);
447
+ for (let c of S) p2.set(c.row.lon.toFixed(2) + "," + c.row.lat.toFixed(2), c.row);
448
448
  if (p2.size === 1) {
449
- let c = S2[0].row;
449
+ let c = S[0].row;
450
450
  return { lat: c.lat, lon: c.lon, precision: "city" };
451
451
  }
452
452
  if (p2.size > 1) return { ambiguous: true, matches: p2.size };
@@ -469,26 +469,26 @@ function A3(n) {
469
469
  var G = { latlon: 0, city: 1, zip3: 2, state: 3, country: 4 };
470
470
  var x = 8;
471
471
  function y3(n, e) {
472
- 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;
472
+ 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;
473
473
  for (let l = 0; l < n.length; ++l) {
474
- let A = n[l], C2 = A3({ ...A, mapKind: e });
475
- if (S3(C2)) {
474
+ let A = n[l], C = A3({ ...A, mapKind: e });
475
+ if (S3(C)) {
476
476
  t[l] = null, o[l] = null, i[l] = null, m2++;
477
- let M = K(A);
478
- M && !d.has(M.toLowerCase()) && (d.add(M.toLowerCase()), R.length < x && R.push(M));
479
- } else if (C2) {
480
- 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) {
481
- let M = K(A);
482
- M && !s.has(M.toLowerCase()) && (s.add(M.toLowerCase()), f.length < x && f.push(M));
477
+ let M2 = K(A);
478
+ M2 && !d.has(M2.toLowerCase()) && (d.add(M2.toLowerCase()), R.length < x && R.push(M2));
479
+ } else if (C) {
480
+ if (t[l] = C.lat, o[l] = C.lon, i[l] = C.precision, S++, g[C.precision]++, (p2 === null || G[C.precision] > G[p2]) && (p2 = C.precision), G[C.precision] > G.city) {
481
+ let M2 = K(A);
482
+ M2 && !s.has(M2.toLowerCase()) && (s.add(M2.toLowerCase()), f.length < x && f.push(M2));
483
483
  }
484
484
  } else {
485
485
  t[l] = null, o[l] = null, i[l] = null;
486
- let M = K(A);
487
- M && !r.has(M.toLowerCase()) && (r.add(M.toLowerCase()), a2.push(M));
486
+ let M2 = K(A);
487
+ M2 && !r.has(M2.toLowerCase()) && (r.add(M2.toLowerCase()), a2.push(M2));
488
488
  }
489
489
  }
490
490
  let c = !n3() && n.some((l) => l.city !== null && l.city !== void 0 && String(l.city).trim() !== "");
491
- 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 } : {} };
491
+ 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 } : {} };
492
492
  }
493
493
  function K(n) {
494
494
  let e = [n.city, n.state, n.zip, n.country].filter((t) => t != null && String(t).trim() !== "").map((t) => String(t).trim()).join(", ");
@@ -569,13 +569,13 @@ function wn(n) {
569
569
  }
570
570
  function Kn(n, s) {
571
571
  let i = new Array(n.length), r = /* @__PURE__ */ new Set(), A = [], y2 = 0;
572
- for (let S2 = 0; S2 < n.length; ++S2) {
573
- let O3 = hn(n[S2], s);
574
- if (i[S2] = O3, O3 !== null) y2++;
572
+ for (let S = 0; S < n.length; ++S) {
573
+ let O2 = hn(n[S], s);
574
+ if (i[S] = O2, O2 !== null) y2++;
575
575
  else {
576
- let C2 = n[S2];
577
- if (C2 != null) {
578
- let d = String(C2).trim();
576
+ let C = n[S];
577
+ if (C != null) {
578
+ let d = String(C).trim();
579
579
  if (d.length > 0) {
580
580
  let g = d.toLowerCase();
581
581
  r.has(g) || (r.add(g), A.push(d));
@@ -588,142 +588,211 @@ function Kn(n, s) {
588
588
 
589
589
  // ../shape-core/dist/index.mjs
590
590
  /*! @bicharts/shape-core — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; US Census/TIGER (public domain). Full text: NOTICE in this package. */
591
- var w = 2;
592
- var j2 = 400;
593
- function N2(r) {
594
- return !!E(r);
595
- }
596
- function P2(r) {
597
- let n = /* @__PURE__ */ new Set();
598
- for (let e of r) {
599
- if (e == null) continue;
600
- let s = u(String(e));
601
- if (!V(s) && !n.has(s) && (n.add(s), n.size >= j2)) break;
591
+ var W2 = ["rate", "ratio", "pct", "percent", "percentage", "rating", "share", "score", "index", "nps", "csat", "margin", "yield", "coverage", "utilization", "z[_-]?score", "average", "avg", "mean", "median", "stddev", "variance", "gpa", "percentile", "quantile", "efficiency", "accuracy", "probability", "multiplier", "coefficient", "correlation", "occupancy", "age", "bmi", "temperature", "density", "pressure", "humidity", "ph", "velocity", "speed", "elevation", "altitude", "latency", "throughput", "bandwidth", "iops"];
592
+ var X2 = ["pct", "percent", "percentage", "rate", "ratio", "rating", "share", "score", "index", "nps", "csat", "margin", "yield", "coverage", "utilization", "gpa", "percentile", "quantile", "bmi", "density", "pressure", "humidity", "velocity", "efficiency", "occupancy", "latency", "throughput", "bandwidth", "iops"];
593
+ var Y2 = ["latitude", "longitude"];
594
+ var J2 = ["latitude", "longitude"];
595
+ var Q2 = /^\s*(sum|count|count\s+distinct|distinct\s+count)\s+of\s+/i;
596
+ var Ut = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])/g;
597
+ var Zt = new RegExp("\\b(" + W2.join("|") + ")\\b|(" + X2.join("|") + ")$", "i");
598
+ var qt = new RegExp("\\b(" + Y2.join("|") + ")\\b|(" + J2.join("|") + ")$", "i");
599
+ function M(e) {
600
+ return e ? String(e).replace(Q2, "").trim() : "";
601
+ }
602
+ function tt(e) {
603
+ return !!e && Q2.test(String(e));
604
+ }
605
+ function jt(e) {
606
+ let t = M(e);
607
+ return t.length === 0 ? "" : t.replace(Ut, " ");
608
+ }
609
+ function et(e, t) {
610
+ return t ? e.test(jt(t)) || e.test(M(t)) : false;
611
+ }
612
+ function L2(e) {
613
+ return et(qt, e);
614
+ }
615
+ function nt(e) {
616
+ return et(Zt, e) || L2(e);
617
+ }
618
+ var it = /int|double|decimal|single|float|number|currency|money/i;
619
+ function Wt(e) {
620
+ let t = String(e?.valueNature ?? "").trim().toLowerCase();
621
+ return t === "categorical" ? "Categorical" : t === "ordinal" ? "Ordinal" : t === "continuous" || e?.isMeasure === true || it.test(String(e?.dataType ?? "")) ? "Continuous" : "Categorical";
622
+ }
623
+ function Xt(e) {
624
+ return e?.isMeasure === true || it.test(String(e?.dataType ?? ""));
625
+ }
626
+ function z2(e) {
627
+ let t = e ?? {}, i = Wt(t);
628
+ if (i !== "Continuous") return { nature: i, additivity: "unknown", basis: "role", suppressed: false };
629
+ if (L2(t.name)) return { nature: i, additivity: "positional", basis: "name", suppressed: true };
630
+ let r = String(t.additivity ?? "").trim().toLowerCase();
631
+ if (!(r === "additive" && tt(t.name))) {
632
+ if (r === "additive") return { nature: i, additivity: "additive", basis: "measured", suppressed: false };
633
+ if (r === "part_of_whole") return { nature: i, additivity: "part_of_whole", basis: "measured", suppressed: false };
634
+ if (r === "intensive_rate") return { nature: i, additivity: "intensive_rate", basis: "measured", suppressed: false };
602
635
  }
603
- return Array.from(n);
604
- }
605
- function C(r) {
606
- let n = P2(r);
607
- if (n.length === 0 || E2(r) < 95) return false;
608
- let e = n.filter((l) => N2(l) && !Y(l)).length, s = n.filter((l) => !N2(l) && !!Y(l)).length;
609
- return e >= 1 && e >= s;
610
- }
611
- function q2(r, n) {
612
- if (!Y(r)) return false;
613
- let e = E(r);
614
- return e ? n === "world" ? true : s3.has(e) : false;
615
- }
616
- function E2(r) {
617
- let n = P2(r);
618
- if (n.length === 0) return 0;
619
- let e = 0;
620
- for (let s of n) N2(s) && e++;
621
- return Math.round(e / n.length * 1e3) / 10;
622
- }
623
- function S(r) {
624
- let n = P2(r);
625
- if (n.length === 0) return 0;
626
- let e = 0;
627
- for (let s of n) Y(s) && e++;
628
- return Math.round(e / n.length * 1e3) / 10;
629
- }
630
- function D2(r) {
631
- let n = /* @__PURE__ */ new Set();
632
- for (let e of r) {
633
- if (e == null) continue;
634
- let s = String(e).trim();
635
- if (!V(u(s)) && !n.has(s) && (n.add(s), n.size >= j2)) break;
636
+ return t.discourageAggregationAcrossGroups === true ? { nature: i, additivity: "intensive_rate", basis: "host-flag", suppressed: false } : nt(t.name) ? { nature: i, additivity: "intensive_rate", basis: "name", suppressed: false } : { nature: i, additivity: "additive", basis: "default", suppressed: false };
637
+ }
638
+ function G2(e) {
639
+ let t = z2(e), i = e ?? {};
640
+ if (t.nature === "Categorical") return ["first", "distinctcount", "count"];
641
+ if (t.nature === "Ordinal") return Xt(i) ? ["median", "min", "max", "first", "distinctcount", "count"] : ["first", "distinctcount", "count"];
642
+ switch (t.additivity) {
643
+ case "positional":
644
+ return ["min", "max", "count", "distinctcount", "first"];
645
+ case "intensive_rate":
646
+ return ["average", "median", "min", "max", "count", "distinctcount", "first"];
647
+ default:
648
+ return ["sum", "average", "median", "min", "max", "count", "distinctcount", "first"];
636
649
  }
637
- return Array.from(n);
638
- }
639
- function O2(r) {
640
- let n = D2(r);
641
- if (n.length === 0) return 0;
642
- let e = 0;
643
- for (let s of n) Z(s).length > 0 && e++;
644
- return Math.round(e / n.length * 1e3) / 10;
645
- }
646
- function Z2(r, n) {
647
- return p(n, en) ? true : D2(r).some((e) => /^\d{5}(-\d{4})?$/.test(e.replace(/\.0+$/, "")));
648
- }
649
- function Gt(r, n) {
650
- let e = P2(r);
651
- if (e.length === 0) return 0;
652
- let s = 0;
653
- for (let l of e) p3(l, n) && s++;
654
- return Math.round(s / e.length * 1e3) / 10;
655
- }
656
- function $2(r, n) {
657
- let e = 0;
658
- for (let s of P2(r)) n(s) && e++;
659
- return e;
660
- }
661
- function _t(r, n) {
662
- let e = 0;
663
- for (let s of D2(r)) n(s) && e++;
664
- return e;
665
650
  }
666
- function wt(r, n, e, s) {
667
- let l = [], c = [], o = {}, v2 = {}, k2 = new Set(r), u2 = (t) => n.map((i) => i[t]);
668
- for (let t of ["city", "lat", "lon", "country"]) {
669
- let i = e?.[t];
670
- i && k2.has(i) && (o[t] = i);
651
+ function Yt(e) {
652
+ return G2(e)[0];
653
+ }
654
+ function Qt(e, t) {
655
+ if (t !== "sum" && t !== "count") return false;
656
+ if (t === "count") return true;
657
+ let i = z2(e);
658
+ return i.additivity === "additive" || i.additivity === "part_of_whole" || i.additivity === "unknown";
659
+ }
660
+ var F2 = 2;
661
+ var st = 400;
662
+ function D2(e) {
663
+ return !!E(e);
664
+ }
665
+ function v2(e) {
666
+ let t = /* @__PURE__ */ new Set();
667
+ for (let i of e) {
668
+ if (i == null) continue;
669
+ let r = u(String(i));
670
+ if (!V(r) && !t.has(r) && (t.add(r), t.size >= st)) break;
671
+ }
672
+ return Array.from(t);
673
+ }
674
+ function h2(e) {
675
+ let t = v2(e);
676
+ if (t.length === 0 || K2(e) < 95) return false;
677
+ let i = t.filter((u2) => D2(u2) && !Y(u2)).length, r = t.filter((u2) => !D2(u2) && !!Y(u2)).length;
678
+ return i >= 1 && i >= r;
679
+ }
680
+ function ot(e, t) {
681
+ if (!Y(e)) return false;
682
+ let i = E(e);
683
+ return i ? t === "world" ? true : s3.has(i) : false;
684
+ }
685
+ function K2(e) {
686
+ let t = v2(e);
687
+ if (t.length === 0) return 0;
688
+ let i = 0;
689
+ for (let r of t) D2(r) && i++;
690
+ return Math.round(i / t.length * 1e3) / 10;
691
+ }
692
+ function P2(e) {
693
+ let t = v2(e);
694
+ if (t.length === 0) return 0;
695
+ let i = 0;
696
+ for (let r of t) Y(r) && i++;
697
+ return Math.round(i / t.length * 1e3) / 10;
698
+ }
699
+ function H2(e) {
700
+ let t = /* @__PURE__ */ new Set();
701
+ for (let i of e) {
702
+ if (i == null) continue;
703
+ let r = String(i).trim();
704
+ if (!V(u(r)) && !t.has(r) && (t.add(r), t.size >= st)) break;
705
+ }
706
+ return Array.from(t);
707
+ }
708
+ function B2(e) {
709
+ let t = H2(e);
710
+ if (t.length === 0) return 0;
711
+ let i = 0;
712
+ for (let r of t) Z(r).length > 0 && i++;
713
+ return Math.round(i / t.length * 1e3) / 10;
714
+ }
715
+ function rt(e, t) {
716
+ return p(t, en) ? true : H2(e).some((i) => /^\d{5}(-\d{4})?$/.test(i.replace(/\.0+$/, "")));
717
+ }
718
+ function te(e, t) {
719
+ let i = v2(e);
720
+ if (i.length === 0) return 0;
721
+ let r = 0;
722
+ for (let u2 of i) p3(u2, t) && r++;
723
+ return Math.round(r / i.length * 1e3) / 10;
724
+ }
725
+ function $2(e, t) {
726
+ let i = 0;
727
+ for (let r of v2(e)) t(r) && i++;
728
+ return i;
729
+ }
730
+ function ee(e, t) {
731
+ let i = 0;
732
+ for (let r of H2(e)) t(r) && i++;
733
+ return i;
734
+ }
735
+ function ne(e, t, i, r) {
736
+ let u2 = [], c = [], o = {}, I2 = {}, R = new Set(e), l = (n) => t.map((s) => s[n]);
737
+ for (let n of ["city", "lat", "lon", "country"]) {
738
+ let s = i?.[n];
739
+ s && R.has(s) && (o[n] = s);
671
740
  }
672
- let y2 = e?.zip;
673
- if (y2 && k2.has(y2)) {
674
- let t = u2(y2), i = O2(t);
675
- i < 95 ? c.push(`zip=${y2} (only ${i}% of values read as ZIPs)`) : Z2(t, y2) ? o.zip = y2 : c.push(`zip=${y2} (all values are 3-4 digits with nothing to say they are postal)`);
741
+ let p2 = i?.zip;
742
+ if (p2 && R.has(p2)) {
743
+ let n = l(p2), s = B2(n);
744
+ s < 95 ? c.push(`zip=${p2} (only ${s}% of values read as ZIPs)`) : rt(n, p2) ? o.zip = p2 : c.push(`zip=${p2} (all values are 3-4 digits with nothing to say they are postal)`);
676
745
  }
677
- let m2 = e?.state;
678
- if (m2 && k2.has(m2)) {
679
- let t = u2(m2), i = P2(t);
680
- 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;
746
+ let f = i?.state;
747
+ if (f && R.has(f)) {
748
+ let n = l(f), s = v2(n);
749
+ s.length > 0 && s.every((a2) => ot(a2, r)) ? c.push(`state=${f} (every value is "CA", which is both Canada and California)`) : h2(n) ? (c.push(`state=${f} (every value is a country, not a state)`), o.country || (o.country = f)) : P2(n) < 95 ? c.push(`state=${f} (only ${P2(n)}% of values are states/provinces)`) : o.state = f;
681
750
  }
682
- let d = new Set(Object.values(o).filter(Boolean)), x2 = r.filter((t) => !d.has(t) && !t.startsWith("__"));
751
+ let m2 = new Set(Object.values(o).filter(Boolean)), x2 = e.filter((n) => !m2.has(n) && !n.startsWith("__"));
683
752
  if (!o.state) {
684
- let t = null;
685
- for (let i of x2) {
686
- let a2 = u2(i);
687
- if (C(a2)) continue;
688
- let f = S(a2);
689
- if (f < 95) continue;
690
- let z2 = $2(a2, (b2) => !!Y(b2)), W2 = $2(a2, (b2) => !!Y(b2) && !q2(b2, s));
691
- z2 < w && W2 < 1 || (!t || f > t.pct) && (t = { name: i, pct: f });
753
+ let n = null;
754
+ for (let s of x2) {
755
+ let a2 = l(s);
756
+ if (h2(a2)) continue;
757
+ let d = P2(a2);
758
+ if (d < 95) continue;
759
+ let E2 = $2(a2, (C) => !!Y(C)), ut = $2(a2, (C) => !!Y(C) && !ot(C, r));
760
+ E2 < F2 && ut < 1 || (!n || d > n.pct) && (n = { name: s, pct: d });
692
761
  }
693
- t && (o.state = t.name, d.add(t.name), l.push(`state=${t.name} (${t.pct}% states/provinces)`));
762
+ n && (o.state = n.name, m2.add(n.name), u2.push(`state=${n.name} (${n.pct}% states/provinces)`));
694
763
  }
695
- if (!o.zip) for (let t of x2) {
696
- if (d.has(t)) continue;
697
- let i = u2(t), a2 = O2(i);
698
- if (!(a2 < 95) && Z2(i, t) && !(_t(i, (f) => Z(f).length > 0) < w)) {
699
- o.zip = t, d.add(t), l.push(`zip=${t} (${a2}% ZIP codes)`);
764
+ if (!o.zip) for (let n of x2) {
765
+ if (m2.has(n)) continue;
766
+ let s = l(n), a2 = B2(s);
767
+ if (!(a2 < 95) && rt(s, n) && !(ee(s, (d) => Z(d).length > 0) < F2)) {
768
+ o.zip = n, m2.add(n), u2.push(`zip=${n} (${a2}% ZIP codes)`);
700
769
  break;
701
770
  }
702
771
  }
703
- let h2 = o.country;
704
- if (!h2 || !C(u2(h2))) {
705
- let t = null;
706
- for (let i of x2) {
707
- if (d.has(i) || !C(u2(i))) continue;
708
- let a2 = E2(u2(i));
709
- (!t || a2 > t.pct) && (t = { name: i, pct: a2 });
772
+ let A = o.country;
773
+ if (!A || !h2(l(A))) {
774
+ let n = null;
775
+ for (let s of x2) {
776
+ if (m2.has(s) || !h2(l(s))) continue;
777
+ let a2 = K2(l(s));
778
+ (!n || a2 > n.pct) && (n = { name: s, pct: a2 });
710
779
  }
711
- 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}`));
780
+ n && (A && (c.push(`country=${A} (only ${K2(l(A))}% of values are country identifiers; using ${n.name} instead)`), I2.country = A), o.country = n.name, m2.add(n.name), u2.push(`country=${n.name}`));
712
781
  }
713
782
  if (!o.city) {
714
- let t = null;
715
- for (let i of x2) {
716
- if (d.has(i)) continue;
717
- let a2 = u2(i), f = Gt(a2, s);
718
- f < m3 || $2(a2, (z2) => p3(z2, s)) < w || (!t || f > t.pct) && (t = { name: i, pct: f });
783
+ let n = null;
784
+ for (let s of x2) {
785
+ if (m2.has(s)) continue;
786
+ let a2 = l(s), d = te(a2, r);
787
+ d < m3 || $2(a2, (E2) => p3(E2, r)) < F2 || (!n || d > n.pct) && (n = { name: s, pct: d });
719
788
  }
720
- t && (o.city = t.name, l.push(`city=${t.name} (${t.pct}% known cities)`));
789
+ n && (o.city = n.name, u2.push(`city=${n.name} (${n.pct}% known cities)`));
721
790
  }
722
- 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) => {
723
- let i = +String(t[o.lat] ?? "").trim(), a2 = +String(t[o.lon] ?? "").trim();
724
- return isFinite(i) && isFinite(a2) && i >= -90 && i <= 90 && a2 >= -180 && a2 <= 180 && String(t[o.lat] ?? "").trim() !== "" && String(t[o.lon] ?? "").trim() !== "";
791
+ let V2 = o.country ? l(o.country) : [], N2 = !!o.country && h2(V2), at = N2 ? new Set(V2.map((n) => E(n == null ? null : String(n))).filter(Boolean)).size : 0, U2 = N2 && at >= 2, O2 = !!(o.lat && o.lon) && t.some((n) => {
792
+ let s = +String(n[o.lat] ?? "").trim(), a2 = +String(n[o.lon] ?? "").trim();
793
+ return isFinite(s) && isFinite(a2) && s >= -90 && s <= 90 && a2 >= -180 && a2 <= 180 && String(n[o.lat] ?? "").trim() !== "" && String(n[o.lon] ?? "").trim() !== "";
725
794
  });
726
- 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 } : {} };
795
+ return o.lat && o.lon && !O2 && c.push(`lat/lon=${o.lat}/${o.lon} (no row holds a usable coordinate pair)`), o.country && !U2 && !(o.city || o.state || o.zip || O2) && c.push(N2 ? `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 || U2 || O2) ? o : null, backfilled: u2, refused: c, ...Object.keys(I2).length ? { labelFallback: I2 } : {} };
727
796
  }
728
797
 
729
798
  // src/payload.ts
@@ -738,7 +807,7 @@ function resolvePointChannel(cols, rowObjs, point) {
738
807
  let labelCol;
739
808
  if (bind) {
740
809
  const dims = cols.filter((c) => !c.isMeasure).map((c) => c.name);
741
- const res = wt(dims, rowObjs, bind, point?.mapKind);
810
+ const res = ne(dims, rowObjs, bind, point?.mapKind);
742
811
  bind = res.bind;
743
812
  rolesBackfilled = res.backfilled;
744
813
  rolesRefused = res.refused;
@@ -806,11 +875,11 @@ function buildRenderPayload(cols, rowObjs, geo, point, destination) {
806
875
  const row = rowObjs[r];
807
876
  const a2 = new Array(cols.length + extra);
808
877
  for (let c = 0; c < cols.length; c++) {
809
- const v2 = row[colNames[c]];
810
- if (v2 instanceof Date) {
811
- a2[c] = v2.toISOString();
878
+ const v3 = row[colNames[c]];
879
+ if (v3 instanceof Date) {
880
+ a2[c] = v3.toISOString();
812
881
  } else {
813
- a2[c] = v2 ?? null;
882
+ a2[c] = v3 ?? null;
814
883
  }
815
884
  }
816
885
  let k2 = cols.length;
@@ -862,8 +931,8 @@ function buildRenderPayload(cols, rowObjs, geo, point, destination) {
862
931
  function createMarkResolver(env) {
863
932
  const { root, doc, log } = env;
864
933
  const isElement = (t) => {
865
- const E4 = doc?.defaultView?.Element ?? globalThis.Element;
866
- if (E4 && t instanceof E4) return true;
934
+ const E2 = doc?.defaultView?.Element ?? globalThis.Element;
935
+ if (E2 && t instanceof E2) return true;
867
936
  return !!t && typeof t.closest === "function" && typeof t.getAttribute === "function";
868
937
  };
869
938
  const viewStyle = (n) => {
@@ -883,14 +952,14 @@ function createMarkResolver(env) {
883
952
  return s === "transparent" || s === "none" || so <= 0.01;
884
953
  };
885
954
  const distToPathCenterline = (el, x2, y2) => {
886
- const L2 = el.getTotalLength();
887
- if (!(L2 > 0)) return Number.MAX_VALUE;
955
+ const L4 = el.getTotalLength();
956
+ if (!(L4 > 0)) return Number.MAX_VALUE;
888
957
  const m2 = el.getScreenCTM();
889
958
  if (!m2) return Number.MAX_VALUE;
890
959
  let best = Number.MAX_VALUE;
891
- const N3 = 64;
892
- for (let i = 0; i <= N3; i++) {
893
- const pt = el.getPointAtLength(L2 * i / N3);
960
+ const N2 = 64;
961
+ for (let i = 0; i <= N2; i++) {
962
+ const pt = el.getPointAtLength(L4 * i / N2);
894
963
  const dx = m2.a * pt.x + m2.c * pt.y + m2.e - x2;
895
964
  const dy = m2.b * pt.x + m2.d * pt.y + m2.f - y2;
896
965
  const d = dx * dx + dy * dy;
@@ -1086,6 +1155,42 @@ function ensureCrossfilterHitTargets(container, doc) {
1086
1155
  return report;
1087
1156
  }
1088
1157
 
1158
+ // src/blankRender.ts
1159
+ var EMPTY = { markCount: 0, legendMarkCount: 0, containerKind: "empty" };
1160
+ function censusMarks(container) {
1161
+ if (!container || typeof container.querySelectorAll !== "function") return EMPTY;
1162
+ try {
1163
+ const marks = /* @__PURE__ */ new Set();
1164
+ for (const el of Array.from(container.querySelectorAll(`.${MARK_CLASS}`))) marks.add(el);
1165
+ for (const el of Array.from(container.querySelectorAll(`[${ROW_IDX_ATTR}]`))) marks.add(el);
1166
+ const legend = /* @__PURE__ */ new Set();
1167
+ for (const el of Array.from(container.querySelectorAll(`.${LEGEND_MARK_CLASS}`))) {
1168
+ legend.add(el);
1169
+ marks.delete(el);
1170
+ }
1171
+ const hasSvg = !!container.querySelector("svg");
1172
+ const hasAnything = hasSvg || (container.childElementCount ?? 0) > 0;
1173
+ return {
1174
+ markCount: marks.size,
1175
+ legendMarkCount: legend.size,
1176
+ containerKind: hasSvg ? "svg" : hasAnything ? "html" : "empty"
1177
+ };
1178
+ } catch {
1179
+ return EMPTY;
1180
+ }
1181
+ }
1182
+ function isBlankRender(i) {
1183
+ if (i.contractUntagged) return false;
1184
+ if (i.animated) return false;
1185
+ if (i.authoredNoDataText) return false;
1186
+ if (!(i.rows > 0)) return false;
1187
+ return i.markCount === 0;
1188
+ }
1189
+ function blankRenderFlag(lang, cause) {
1190
+ const clean = (s) => String(s || "unknown").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
1191
+ return `blankrender:${clean(lang)}:${clean(cause)}`;
1192
+ }
1193
+
1089
1194
  // src/host.ts
1090
1195
  var parseRowIdxs = (s) => (s || "").split(",").map((x2) => parseInt(x2, 10)).filter((n) => Number.isFinite(n));
1091
1196
  var D3_PLUGIN_PACKAGES = {
@@ -1158,7 +1263,7 @@ function createChartHost(container, config) {
1158
1263
  let data = config.data;
1159
1264
  {
1160
1265
  const stamped = config.data?.meta?.hostContract;
1161
- const major = (v2) => (v2 || "").split(".")[0];
1266
+ const major = (v3) => (v3 || "").split(".")[0];
1162
1267
  if (stamped && major(stamped) !== major(HOST_CONTRACT_VERSION)) {
1163
1268
  try {
1164
1269
  (win?.console ?? console)?.warn(
@@ -1186,6 +1291,7 @@ function createChartHost(container, config) {
1186
1291
  let renderFn = config.renderFn ?? null;
1187
1292
  let destroyed = false;
1188
1293
  let warnedGeo = false;
1294
+ let warnedBlank = false;
1189
1295
  let current = null;
1190
1296
  let activeTickEl = null;
1191
1297
  let selectionIsPeriod = false;
@@ -1193,9 +1299,9 @@ function createChartHost(container, config) {
1193
1299
  const ensureAffordanceStyles = () => {
1194
1300
  try {
1195
1301
  if (!doc || doc.getElementById("bic-chart-host-affordances")) return;
1196
- const st = doc.createElement("style");
1197
- st.id = "bic-chart-host-affordances";
1198
- st.textContent = `
1302
+ const st2 = doc.createElement("style");
1303
+ st2.id = "bic-chart-host-affordances";
1304
+ st2.textContent = `
1199
1305
  /* TYPOGRAPHY FIREWALL. Generated charts set font-size but almost never line-height or
1200
1306
  letter-spacing, so they INHERIT whatever the host page cascades in. A page with
1201
1307
  \`:root { font: 18px/145% }\` (the stock Vite template does exactly this) hands every
@@ -1233,7 +1339,7 @@ function createChartHost(container, config) {
1233
1339
  font-weight: 700 !important;
1234
1340
  text-decoration: underline !important;
1235
1341
  }`;
1236
- (doc.head || doc.documentElement)?.appendChild(st);
1342
+ (doc.head || doc.documentElement)?.appendChild(st2);
1237
1343
  } catch {
1238
1344
  }
1239
1345
  };
@@ -1353,6 +1459,27 @@ function createChartHost(container, config) {
1353
1459
  }
1354
1460
  ensureCrossfilterHitTargets(container, doc);
1355
1461
  paintSelection();
1462
+ try {
1463
+ const census = censusMarks(container);
1464
+ if (isBlankRender({
1465
+ markCount: census.markCount,
1466
+ rows: data.rows ? data.rows.length : 0,
1467
+ animated: config.animated,
1468
+ contractUntagged: config.contractUntagged
1469
+ })) {
1470
+ if (!warnedBlank) {
1471
+ warnedBlank = true;
1472
+ try {
1473
+ (win?.console ?? console)?.warn(
1474
+ `[@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.`
1475
+ );
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
1480
+ }
1481
+ } catch {
1482
+ }
1356
1483
  },
1357
1484
  setOptions(partial) {
1358
1485
  raw = { ...raw, ...partial };
@@ -1445,9 +1572,16 @@ export {
1445
1572
  registerGeoAsset,
1446
1573
  clearGeoCache,
1447
1574
  L3,
1575
+ z2 as z,
1576
+ G2 as G,
1577
+ Yt,
1578
+ Qt,
1448
1579
  buildRenderPayload,
1449
1580
  createMarkResolver,
1450
1581
  ensureCrossfilterHitTargets,
1582
+ censusMarks,
1583
+ isBlankRender,
1584
+ blankRenderFlag,
1451
1585
  requiredD3Plugins,
1452
1586
  explainRenderFailure,
1453
1587
  stripEsmExports,
package/dist/index.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  DIM_OPACITY_DEFAULT,
21
21
  DIM_OPACITY_VAR,
22
22
  FLIP_MODE_DEFAULT,
23
+ G,
23
24
  GEO_POINT_PRECISIONS,
24
25
  HOST_CONTAINER_CLASS,
25
26
  HOST_CONTRACT_VERSION,
@@ -27,10 +28,14 @@ import {
27
28
  LEGEND_MARK_CLASS,
28
29
  MARK_CLASS,
29
30
  MARK_SELECTED_CLASS,
31
+ Qt,
30
32
  ROW_IDX_ATTR,
31
33
  SELECTION_ACTIVE_CLASS,
32
34
  XFILTER_REFRESH_EVENT,
35
+ Yt,
36
+ blankRenderFlag,
33
37
  buildRenderPayload,
38
+ censusMarks,
34
39
  chartOwnsTimeline,
35
40
  clearGeoCache,
36
41
  compileRenderFn,
@@ -40,14 +45,16 @@ import {
40
45
  explainRenderFailure,
41
46
  geoAssetFor,
42
47
  geoFromCache,
48
+ isBlankRender,
43
49
  loadGeo,
44
50
  periodTickSuppressesFeedback,
45
51
  registerGeo,
46
52
  registerGeoAsset,
47
53
  requiredD3Plugins,
48
54
  resolveOptions,
49
- stripEsmExports
50
- } from "./chunk-BMYEYUEN.mjs";
55
+ stripEsmExports,
56
+ z
57
+ } from "./chunk-FC3XJPVX.mjs";
51
58
  import "./chunk-A2GMXZP7.mjs";
52
59
 
53
60
  // src/trivial.ts
@@ -622,9 +629,6 @@ function normaliseAggregation(raw) {
622
629
  if (/^(distinctcount|dcount|countdistinct|uniques?)$/.test(s)) return "distinctcount";
623
630
  return "sum";
624
631
  }
625
- function isAdditive(agg) {
626
- return agg === "sum" || agg === "count";
627
- }
628
632
  function aggLabel(agg) {
629
633
  switch (agg) {
630
634
  case "average":
@@ -651,6 +655,20 @@ function isMeasureColumn(col) {
651
655
  if (col.isMeasure === true) return true;
652
656
  return NUMERIC_DATATYPE.test(String(col.dataType ?? ""));
653
657
  }
658
+ function isRealColumn(col) {
659
+ return !!col && !isSynthetic(col.name);
660
+ }
661
+ function isSuppressed(col) {
662
+ return z(col).suppressed;
663
+ }
664
+ function resolveAgg(col, requested, auto) {
665
+ const fallback = () => {
666
+ const d = Yt(col);
667
+ return d === "first" ? "count" : d;
668
+ };
669
+ if (auto) return fallback();
670
+ return G(col).indexOf(requested) >= 0 ? requested : fallback();
671
+ }
654
672
  function toNumber(v) {
655
673
  if (v == null || v === "") return null;
656
674
  const n = typeof v === "number" ? v : Number(v);
@@ -706,9 +724,11 @@ function computeSelectionCard(payload, selectedRowIdxs, opts = {}) {
706
724
  if (pos != null && pos >= 0 && pos < rows.length) positions.push(pos);
707
725
  }
708
726
  if (!positions.length) return null;
727
+ const auto = String(opts.aggregation ?? "").trim().length === 0;
709
728
  const agg = normaliseAggregation(opts.aggregation);
710
729
  const maxMeasures = opts.maxMeasures ?? 4;
711
730
  const maxHeaderValues = opts.maxHeaderValues ?? 3;
731
+ const maxDimensions = opts.maxDimensions ?? 2;
712
732
  const dimIdx = columns.findIndex((c) => c && !isSynthetic(c.name) && !isMeasureColumn(c));
713
733
  let header;
714
734
  if (dimIdx >= 0) {
@@ -728,10 +748,20 @@ function computeSelectionCard(payload, selectedRowIdxs, opts = {}) {
728
748
  header = `${positions.length} mark${positions.length === 1 ? "" : "s"}`;
729
749
  }
730
750
  const measureCols = [];
731
- for (let c = 0; c < columns.length; c++) if (isMeasureColumn(columns[c])) measureCols.push(c);
751
+ const suppressedColumns = [];
752
+ for (let c = 0; c < columns.length; c++) {
753
+ if (!isMeasureColumn(columns[c])) continue;
754
+ if (isSuppressed(columns[c])) {
755
+ suppressedColumns.push(String(columns[c]?.name ?? ""));
756
+ continue;
757
+ }
758
+ measureCols.push(c);
759
+ }
732
760
  const shown = measureCols.slice(0, maxMeasures);
733
761
  const lines = [];
734
762
  for (const c of shown) {
763
+ const col = columns[c];
764
+ const colAgg = resolveAgg(col, agg, auto);
735
765
  const selVals = [];
736
766
  const selRaw = [];
737
767
  for (const p of positions) {
@@ -739,9 +769,9 @@ function computeSelectionCard(payload, selectedRowIdxs, opts = {}) {
739
769
  selRaw.push(rows[p]?.[c]);
740
770
  if (n != null) selVals.push(n);
741
771
  }
742
- const value = reduce(selVals, agg, selRaw);
772
+ const value = reduce(selVals, colAgg, selRaw);
743
773
  let sharePct = null;
744
- if (isAdditive(agg) && value != null) {
774
+ if (Qt(col, colAgg) && value != null) {
745
775
  const allVals = [];
746
776
  const allRaw = [];
747
777
  for (let r = 0; r < rows.length; r++) {
@@ -749,21 +779,49 @@ function computeSelectionCard(payload, selectedRowIdxs, opts = {}) {
749
779
  allRaw.push(rows[r]?.[c]);
750
780
  if (n != null) allVals.push(n);
751
781
  }
752
- const total = reduce(allVals, agg, allRaw);
782
+ const total = reduce(allVals, colAgg, allRaw);
753
783
  if (total != null && total > 0) sharePct = value / total * 100;
754
784
  }
755
785
  lines.push({
756
- column: String(columns[c]?.name ?? ""),
757
- label: `${aggLabel(agg)} of ${columns[c]?.name ?? ""}`,
786
+ column: String(col?.name ?? ""),
787
+ label: `${aggLabel(colAgg)} of ${col?.name ?? ""}`,
758
788
  value,
759
789
  valueText: value == null ? "\u2014" : fmtNumber(value, opts.cultureCode),
760
790
  sharePct,
761
- shareText: sharePct == null ? "" : `${sharePct < 0.1 ? "<0.1" : sharePct.toFixed(1)}% of total`
791
+ shareText: sharePct == null ? "" : `${sharePct < 0.1 ? "<0.1" : sharePct.toFixed(1)}% of total`,
792
+ aggregation: colAgg
793
+ });
794
+ }
795
+ const dimensionLines = [];
796
+ for (let c = 0; c < columns.length && dimensionLines.length < maxDimensions; c++) {
797
+ if (c === dimIdx) continue;
798
+ if (!isRealColumn(columns[c]) || isMeasureColumn(columns[c])) continue;
799
+ const name = String(columns[c]?.name ?? "");
800
+ const distinct = /* @__PURE__ */ new Set();
801
+ let firstText = "";
802
+ for (const p of positions) {
803
+ const raw = rows[p]?.[c];
804
+ const s = raw == null || raw === "" ? "(blank)" : String(raw);
805
+ if (!distinct.size) firstText = s;
806
+ distinct.add(s);
807
+ }
808
+ if (!distinct.size) continue;
809
+ const single = distinct.size === 1;
810
+ dimensionLines.push({
811
+ column: name,
812
+ label: single ? name : `Distinct ${name}`,
813
+ value: single ? null : distinct.size,
814
+ valueText: single ? firstText : fmtNumber(distinct.size, opts.cultureCode),
815
+ sharePct: null,
816
+ shareText: "",
817
+ aggregation: single ? "first" : "distinctcount"
762
818
  });
763
819
  }
764
820
  return {
765
821
  header,
766
822
  lines,
823
+ dimensionLines,
824
+ suppressedColumns,
767
825
  rowsText: `${positions.length} of ${rows.length} rows`,
768
826
  selectedRows: positions.length,
769
827
  totalRows: rows.length,
@@ -811,10 +869,12 @@ export {
811
869
  actionFor,
812
870
  askApplyImprovements,
813
871
  bareBase64,
872
+ blankRenderFlag,
814
873
  buildRenderPayload,
815
874
  buildReviewWire,
816
875
  canConfirmLaunch,
817
876
  captureSvgSnapshot,
877
+ censusMarks,
818
878
  chartOwnsTimeline,
819
879
  chooserFitsViewport,
820
880
  clearGeoCache,
@@ -828,6 +888,7 @@ export {
828
888
  explainRenderFailure,
829
889
  geoAssetFor,
830
890
  geoFromCache,
891
+ isBlankRender,
831
892
  launchFavorStyle,
832
893
  launchGenerates,
833
894
  loadGeo,
package/dist/react.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createChartHost,
5
5
  geoFromCache,
6
6
  loadGeo
7
- } from "./chunk-BMYEYUEN.mjs";
7
+ } from "./chunk-FC3XJPVX.mjs";
8
8
  import "./chunk-A2GMXZP7.mjs";
9
9
 
10
10
  // src/react.tsx
@@ -0,0 +1,60 @@
1
+ export interface MarkCensus {
2
+ /** Data marks found anywhere in the container. */
3
+ markCount: number;
4
+ /** Legend swatches / axis furniture — clickable, but NOT evidence the data drew. */
5
+ legendMarkCount: number;
6
+ /** What the chart built. `html` is a first-class answer, not a failure — see below. */
7
+ containerKind: "svg" | "html" | "empty";
8
+ }
9
+ /**
10
+ * Count what the render actually left behind.
11
+ *
12
+ * NOT SCOPED TO `<svg>`, and that is what makes this work at all. Plenty of chart types are pure
13
+ * HTML - a table with embedded bars, a KPI card - and never create an SVG, so a census that
14
+ * starts from `container.querySelector("svg")` finds nothing for exactly the charts it most needs
15
+ * to measure. Those charts still honour the contract: their working path tags each row
16
+ * `class="d3-mark"` + `data-row-idx`, and their no-data branch appends a bare element with
17
+ * neither. The class and the attribute ARE the signal, in both DOM worlds; the element name never
18
+ * was.
19
+ *
20
+ * Counts by contract, never by tag: a chart marking `<rect>`, `<text>`, `<path>`, `<circle>` or
21
+ * `<div>` is equally visible here. Never throws — telemetry must not be why a delivered render
22
+ * fails.
23
+ */
24
+ export declare function censusMarks(container: Element | null | undefined): MarkCensus;
25
+ export interface BlankVerdictInput {
26
+ /** From censusMarks. */
27
+ markCount: number;
28
+ /** Rows the chart was HANDED. Zero rows is a different, already-handled story. */
29
+ rows: number;
30
+ /**
31
+ * This chart declares time keyframes. Its first frame is legitimately allowed to be empty
32
+ * (an animated map before any country enters the series), so a verdict on frame one would be
33
+ * a lie about the commonest animated opening.
34
+ */
35
+ animated?: boolean;
36
+ /**
37
+ * The author configured their own "No Data Text". They asked for this rectangle to say
38
+ * something when there is nothing to draw, and overruling them would be telling someone their
39
+ * deliberate empty state is a defect.
40
+ */
41
+ authoredNoDataText?: boolean;
42
+ /**
43
+ * The chart's marks are not tagged by our contract at all — a Vega lane, whose marks the
44
+ * generated spec names itself. Absence of `.d3-mark` there proves nothing, so the census is
45
+ * evidence-free and must not return a verdict.
46
+ */
47
+ contractUntagged?: boolean;
48
+ }
49
+ /**
50
+ * Did this render paint nothing, against data that had something to paint?
51
+ *
52
+ * EVERY CLAUSE IS A FALSE POSITIVE WE WOULD OTHERWISE SHIP. The value of this verdict is that a
53
+ * human believes it; one wrong accusation on a chart that is fine costs more than the case it
54
+ * catches, which is the same argument `columnsGoneFromDiff` makes about type narrowings.
55
+ */
56
+ export declare function isBlankRender(i: BlankVerdictInput): boolean;
57
+ /** A short, stable token a host can attach to its own per-render telemetry. Deliberately
58
+ * formatted for an always-on channel rather than a debug log: a blank render is most
59
+ * interesting in aggregate, and the charts worth counting are the ones nobody is watching. */
60
+ export declare function blankRenderFlag(lang: string, cause: string): string;
@@ -1,5 +1,6 @@
1
1
  import { type RenderOptions } from "./contract";
2
2
  import { type ResolveOptionsInput } from "./defaults";
3
+ import { type MarkCensus } from "./blankRender";
3
4
  export type RenderFn = (container: HTMLElement, data: any, options: RenderOptions) => void;
4
5
  export interface ChartHostConfig {
5
6
  data: {
@@ -36,6 +37,26 @@ export interface ChartHostConfig {
36
37
  * already holds its assets (e.g. `geoForKind` from "@bicharts/chart-host/geo", or the
37
38
  * data.geo.json the MCP wrote next to the chart). */
38
39
  geoProvider?: (geoKind: string) => any | undefined;
40
+ /**
41
+ * Called when a render completes cleanly having painted NO data marks against a non-empty
42
+ * table (2026-09-01). The chart did not throw, so nothing else will tell you — this is
43
+ * the only notification that the rectangle your reader is looking at is empty.
44
+ *
45
+ * The commonest cause by far is schema drift: generated code binds columns BY NAME, so a
46
+ * renamed or removed field sends its own `findIndex(...) === -1` guard down the no-data
47
+ * branch. A host that knows the binding can say WHICH column; one that does not should still
48
+ * surface it, because "empty and silent" is the state this exists to abolish.
49
+ */
50
+ onBlankRender?: (info: {
51
+ census: MarkCensus;
52
+ rows: number;
53
+ }) => void;
54
+ /** This chart declares time keyframes: frame one is allowed to be empty, so no blank verdict
55
+ * is issued for it. */
56
+ animated?: boolean;
57
+ /** The chart's marks are not tagged with the shared contract (a Vega lane names its own
58
+ * marks), so a mark count proves nothing and the blank verdict is suppressed. */
59
+ contractUntagged?: boolean;
39
60
  }
40
61
  export interface ChartHost {
41
62
  render(): void;
@@ -15,3 +15,4 @@ export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, typ
15
15
  export { qualifyPick, qualifyAuto, qualifyCancel, launchGenerates, launchFavorStyle, chooserFitsViewport, shouldOpenChooserOnGenerate, shouldOpenInlineChooserOnGenerate, canConfirmLaunch, confirmLaunch, qualifyFailureFallsOpen, CHOOSER_MIN_WIDTH_PX, CHOOSER_MIN_HEIGHT_PX, type QualifyLaunchOutcome, type ChooserGateInput, } from "./qualifyLaunch";
16
16
  export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
17
17
  export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
18
+ export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
@@ -1,7 +1,16 @@
1
1
  import type { RenderPayload } from "./payload";
2
2
  export interface SelectionCardOptions {
3
3
  /** The chart's own aggregation, so the card AGREES with the picture it sits on. A card
4
- * reading "Sum" beside a chart drawn from averages is two answers to one question. */
4
+ * reading "Sum" beside a chart drawn from averages is two answers to one question.
5
+ *
6
+ * BLANK MEANS AUTO, and Auto is now per COLUMN. The Excel settings panel has offered
7
+ * "Auto — sum amounts, average rates" since the control shipped; `normaliseAggregation("")`
8
+ * answered `sum` for every column alike, so the label described a rule the code did not
9
+ * implement. A blank now resolves through `defaultAggregation` per column: amounts sum,
10
+ * rates average, dimensions report their value. A NAMED aggregation is still honoured
11
+ * wherever it is honest, and substituted per column where it is not — the label carries
12
+ * the substitution, so "Average of ChurnRate" beside "Sum of Revenue" reads as the answer
13
+ * it is rather than a silent override. */
5
14
  aggregation?: string;
6
15
  cultureCode?: string;
7
16
  /** Cards are capped at roughly a third of the tile, so the measure list is capped too.
@@ -10,29 +19,55 @@ export interface SelectionCardOptions {
10
19
  /** Past this many distinct dimension values the header becomes "N marks" — naming twelve
11
20
  * cities in a header is not a header. */
12
21
  maxHeaderValues?: number;
22
+ /** How many DIMENSION lines to add below the measures. Capped separately and small on
23
+ * purpose: dimensions must never crowd out the amounts, which is the failure the measure
24
+ * cap already had (on the sample table the two coordinate columns took two of the four
25
+ * measure slots and pushed the real measures under "+2 more"). Default 2. */
26
+ maxDimensions?: number;
13
27
  }
14
28
  export interface SelectionCardLine {
15
29
  column: string;
16
- /** "Sum of Revenue" — the aggregation is named because it is not always sum. */
30
+ /** "Sum of Revenue" — the aggregation is named because it is not always sum. For a
31
+ * dimension resolving to ONE value it is just the column name, because "First of Segment:
32
+ * East" says nothing "Segment: East" does not. */
17
33
  label: string;
34
+ /** Null for a dimension line, whose answer is text. `valueText` is what a host renders. */
18
35
  value: number | null;
19
36
  valueText: string;
20
- /** Share of the same aggregate over ALL rows. Null whenever a share would be a lie — see
21
- * isAdditive: the average of a subset is not a percentage of the average of the whole. */
37
+ /** Share of the same aggregate over ALL rows. Null whenever a share would be a lie — it
38
+ * needs BOTH an additive aggregation and an additive COLUMN. The average of a subset is
39
+ * not a percentage of the average of the whole; neither is a total of latitudes. */
22
40
  sharePct: number | null;
23
41
  shareText: string;
42
+ /** Which aggregation this line actually used, after any per-column substitution. */
43
+ aggregation: string;
24
44
  }
25
45
  export interface SelectionCardModel {
26
46
  header: string;
47
+ /** MEASURE lines. Kept measures-only so `maxMeasures` and `hiddenMeasures` keep meaning
48
+ * exactly what they meant; dimensions have their own list and their own cap. */
27
49
  lines: SelectionCardLine[];
50
+ /** DIMENSION lines — the "First, Count, Distinct" half. Before 2026-09-01 the FIRST dimension
51
+ * supplied the header and every other one was discarded, so on the sample table `Segment`
52
+ * appeared nowhere at all. The header column is still excluded here: it is already on
53
+ * screen. */
54
+ dimensionLines: SelectionCardLine[];
28
55
  /** "6 of 24 rows" — the one line that is true regardless of what the columns contain. */
29
56
  rowsText: string;
30
57
  selectedRows: number;
31
58
  totalRows: number;
32
59
  /** Measures the cap left out, so a host can say "+2 more" instead of implying there were none. */
33
60
  hiddenMeasures: number;
61
+ /** Columns the CLASSIFIER withheld rather than the cap — coordinates today. Not for display:
62
+ * a reader does not need telling that a latitude was not totalled. It is here so a host can
63
+ * log it, because a column vanishing from a card with no trace is the kind of thing that
64
+ * costs an afternoon later. */
65
+ suppressedColumns: string[];
34
66
  aggregation: string;
35
67
  }
68
+ /** The set this file reduces over. `first` is shape-core's eighth kind and is handled on the
69
+ * dimension path only, which is why it is absent here — a `reduce` over numbers has nothing to
70
+ * do with it. */
36
71
  type Agg = "sum" | "average" | "min" | "max" | "count" | "median" | "distinctcount";
37
72
  /** Normalise the many spellings the hosts use into the set this file reduces over. Unknown
38
73
  * spellings fall back to sum rather than throwing: a card is a courtesy, and refusing to draw
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
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",