@bicharts/chart-host 0.5.57 → 0.5.59

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.
@@ -69,6 +69,15 @@ function resolveOptions(p2) {
69
69
  // omission is a missing check, not four mistakes.
70
70
  geoPointDest: p2.geoPointDest,
71
71
  maxMapPoints: p2.maxMapPoints,
72
+ // FIFTH AND SIXTH OCCURRENCES, and the two that argue hardest for the guard test: both
73
+ // were passed by the visual AND read by the charts, and neither was ever declared here or
74
+ // in the contract - so the contract-vs-whitelist check could not see them either. What
75
+ // found them was scanning the OTHER direction, from the caller and the charts inward.
76
+ // 0 is MEANINGFUL for both ("let the chart choose", "no paging"), so numberOr rather than
77
+ // `|| DEFAULT`, which would turn an explicit 0 into something else. Clamping stays
78
+ // chart-side for the same reason it does for flipIntervalMs.
79
+ geoPointRadiusPx: Math.max(0, numberOr(p2.geoPointRadiusPx, 0)),
80
+ pageSize: Math.max(0, numberOr(p2.pageSize, 0)),
72
81
  uiState: p2.uiState,
73
82
  setUiState: p2.setUiState,
74
83
  backgroundColor: p2.backgroundColor,
@@ -1218,6 +1227,87 @@ function blankRenderFlag(lang, cause) {
1218
1227
  return `blankrender:${clean(lang)}:${clean(cause)}`;
1219
1228
  }
1220
1229
 
1230
+ // src/hitBands.ts
1231
+ var MIN_HIT_BAND_PX = 8;
1232
+ var ELEMENT_CAP2 = 5e3;
1233
+ var EMPTY2 = {
1234
+ openStrokes: 0,
1235
+ interactive: 0,
1236
+ hairline: 0,
1237
+ uncovered: 0,
1238
+ widestPx: 0
1239
+ };
1240
+ var OPEN_TAGS = /* @__PURE__ */ new Set(["path", "line", "polyline"]);
1241
+ function styleOf(el, doc) {
1242
+ try {
1243
+ const view = doc?.defaultView;
1244
+ return view?.getComputedStyle ? view.getComputedStyle(el) : null;
1245
+ } catch {
1246
+ return null;
1247
+ }
1248
+ }
1249
+ function attrOrComputed(el, name, cs) {
1250
+ const a2 = el.getAttribute?.(name);
1251
+ if (a2 !== null && a2 !== void 0 && String(a2).trim() !== "") return String(a2).trim();
1252
+ const c = cs?.[name === "stroke-width" ? "strokeWidth" : name];
1253
+ return c === null || c === void 0 ? "" : String(c).trim();
1254
+ }
1255
+ function isUnfilled(fill) {
1256
+ const f = fill.toLowerCase();
1257
+ return f === "none" || f === "transparent" || f === "rgba(0, 0, 0, 0)" || f === "rgba(0,0,0,0)";
1258
+ }
1259
+ function widthPx(raw) {
1260
+ const m2 = /^(-?\d+(?:\.\d+)?)\s*(px)?$/i.exec(raw);
1261
+ if (!m2) return NaN;
1262
+ const n = parseFloat(m2[1]);
1263
+ return isFinite(n) ? n : NaN;
1264
+ }
1265
+ function censusHitBands(container, doc) {
1266
+ if (!container || typeof container.querySelectorAll !== "function") return EMPTY2;
1267
+ try {
1268
+ const els = Array.from(
1269
+ container.querySelectorAll(`.${MARK_CLASS}, [${ROW_IDX_ATTR}]`)
1270
+ );
1271
+ if (els.length === 0 || els.length > ELEMENT_CAP2) return EMPTY2;
1272
+ const ownerDoc = doc ?? container.ownerDocument;
1273
+ const widestByGeom = /* @__PURE__ */ new Map();
1274
+ const rows = [];
1275
+ for (const el of els) {
1276
+ const tag = String(el.tagName || "").toLowerCase();
1277
+ if (!OPEN_TAGS.has(tag)) continue;
1278
+ const cs = styleOf(el, ownerDoc);
1279
+ const open = isUnfilled(attrOrComputed(el, "fill", cs));
1280
+ const w = widthPx(attrOrComputed(el, "stroke-width", cs) || "1");
1281
+ const geom = String(el.getAttribute?.("d") ?? "") || `${tag}#${String(el.getAttribute?.(ROW_IDX_ATTR) ?? "")}`;
1282
+ const inert = String(cs?.pointerEvents ?? el.style?.pointerEvents ?? "") === "none";
1283
+ rows.push({ el, geom, w, open, inert });
1284
+ if (isFinite(w)) {
1285
+ const prev = widestByGeom.get(geom);
1286
+ if (prev === void 0 || w > prev) widestByGeom.set(geom, w);
1287
+ }
1288
+ }
1289
+ const out = { ...EMPTY2 };
1290
+ for (const r of rows) {
1291
+ if (!r.open) continue;
1292
+ out.openStrokes++;
1293
+ if (r.inert) continue;
1294
+ out.interactive++;
1295
+ if (isFinite(r.w) && r.w > out.widestPx) out.widestPx = Math.round(r.w);
1296
+ if (!(r.w < MIN_HIT_BAND_PX)) continue;
1297
+ out.hairline++;
1298
+ const best = widestByGeom.get(r.geom);
1299
+ if (best === void 0 || best < MIN_HIT_BAND_PX) out.uncovered++;
1300
+ }
1301
+ return out;
1302
+ } catch {
1303
+ return EMPTY2;
1304
+ }
1305
+ }
1306
+ function hitBandFlag(c) {
1307
+ if (c.interactive === 0) return "";
1308
+ return c.uncovered > 0 ? "hitband:d3:thin" : "hitband:d3:ok";
1309
+ }
1310
+
1221
1311
  // src/host.ts
1222
1312
  function sessionViewStateProvider(container) {
1223
1313
  const holder = container;
@@ -1541,6 +1631,12 @@ function createChartHost(container, config) {
1541
1631
  }
1542
1632
  } catch {
1543
1633
  }
1634
+ if (config.onHitBandCensus) {
1635
+ try {
1636
+ config.onHitBandCensus(censusHitBands(container, doc));
1637
+ } catch {
1638
+ }
1639
+ }
1544
1640
  },
1545
1641
  setOptions(partial) {
1546
1642
  raw = { ...raw, ...partial };
@@ -1646,6 +1742,9 @@ export {
1646
1742
  censusMarks,
1647
1743
  isBlankRender,
1648
1744
  blankRenderFlag,
1745
+ MIN_HIT_BAND_PX,
1746
+ censusHitBands,
1747
+ hitBandFlag,
1649
1748
  sessionViewStateProvider,
1650
1749
  noopViewStateProvider,
1651
1750
  requiredD3Plugins,
package/dist/index.mjs CHANGED
@@ -31,12 +31,14 @@ import {
31
31
  LIFT_SELECTED_CLASS,
32
32
  MARK_CLASS,
33
33
  MARK_SELECTED_CLASS,
34
+ MIN_HIT_BAND_PX,
34
35
  ROW_IDX_ATTR,
35
36
  SELECTION_ACTIVE_CLASS,
36
37
  VALUE_AXIS_BASELINE_DEFAULT,
37
38
  XFILTER_REFRESH_EVENT,
38
39
  blankRenderFlag,
39
40
  buildRenderPayload,
41
+ censusHitBands,
40
42
  censusMarks,
41
43
  chartOwnsTimeline,
42
44
  clearGeoCache,
@@ -47,6 +49,7 @@ import {
47
49
  explainRenderFailure,
48
50
  geoAssetFor,
49
51
  geoFromCache,
52
+ hitBandFlag,
50
53
  isBlankRender,
51
54
  loadGeo,
52
55
  ne,
@@ -59,7 +62,7 @@ import {
59
62
  sessionViewStateProvider,
60
63
  stripEsmExports,
61
64
  te
62
- } from "./chunk-5W35EISP.mjs";
65
+ } from "./chunk-WG4NTR5H.mjs";
63
66
  import "./chunk-A2GMXZP7.mjs";
64
67
 
65
68
  // src/trivial.ts
@@ -898,6 +901,7 @@ export {
898
901
  LIFT_SELECTED_CLASS,
899
902
  MARK_CLASS,
900
903
  MARK_SELECTED_CLASS,
904
+ MIN_HIT_BAND_PX,
901
905
  ROW_IDX_ATTR,
902
906
  SELECTION_ACTIVE_CLASS,
903
907
  VALUE_AXIS_BASELINE_DEFAULT,
@@ -910,6 +914,7 @@ export {
910
914
  buildReviewWire,
911
915
  canConfirmLaunch,
912
916
  captureSvgSnapshot,
917
+ censusHitBands,
913
918
  censusMarks,
914
919
  chartOwnsTimeline,
915
920
  chooserFitsViewport,
@@ -925,6 +930,7 @@ export {
925
930
  geoAssetFor,
926
931
  geoFromCache,
927
932
  hasRefusalsToShow,
933
+ hitBandFlag,
928
934
  isBlankRender,
929
935
  launchFavorStyle,
930
936
  launchGenerates,
package/dist/react.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createChartHost,
5
5
  geoFromCache,
6
6
  loadGeo
7
- } from "./chunk-5W35EISP.mjs";
7
+ } from "./chunk-WG4NTR5H.mjs";
8
8
  import "./chunk-A2GMXZP7.mjs";
9
9
 
10
10
  // src/react.tsx
@@ -100,6 +100,8 @@ export interface RenderOptions {
100
100
  rolesRefused?: string[];
101
101
  } | null;
102
102
  maxMapPoints?: number;
103
+ geoPointRadiusPx?: number;
104
+ pageSize?: number;
103
105
  approximatePositions?: ApproximatePositions;
104
106
  valueAxisBaseline?: ValueAxisBaseline;
105
107
  colorScaleLow?: string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Narrowest stroke, in CSS pixels, that counts as a real click target.
3
+ *
4
+ * The generated-code recipe asks for 10-14px. Eight is the floor rather than the target: it is
5
+ * generous enough that a chart which made a deliberate effort is not counted as a defect, and it
6
+ * is the same number the server-side rule uses, so the two halves cannot drift into disagreeing
7
+ * about which strokes are thin.
8
+ */
9
+ export declare const MIN_HIT_BAND_PX = 8;
10
+ export interface HitBandCensus {
11
+ /** Tagged marks that are OPEN strokes (`fill` none/transparent with a painted stroke). */
12
+ openStrokes: number;
13
+ /** Of those, the ones the chart has not opted out of with `pointer-events:none`. */
14
+ interactive: number;
15
+ /** Of the interactive ones, those whose own band is under `MIN_HIT_BAND_PX`. */
16
+ hairline: number;
17
+ /** Of the hairlines, those with no wider companion sharing their geometry. THE DEFECT. */
18
+ uncovered: number;
19
+ /** Widest band, rounded, seen on any interactive open stroke — for tuning the floor. */
20
+ widestPx: number;
21
+ }
22
+ /**
23
+ * Count the declared marks that are open strokes, and how many of them are too thin to hit.
24
+ *
25
+ * A companion is any OTHER tagged element sharing this one's geometry (the same `d`, or the same
26
+ * row index when there is no `d`) whose band clears the floor. That is exactly the recipe's
27
+ * shape, and it means a chart which drew its hit band correctly reports `uncovered: 0` however it
28
+ * spelled the transparency — the width is what makes a band a band.
29
+ *
30
+ * A mark whose own visible stroke is already wide is its OWN hit band and is never counted as a
31
+ * hairline. A Sankey link is the case that matters: its thickness is its value, routinely 10-40px,
32
+ * and asking it for a companion would be asking for a second hit area on top of a good one.
33
+ *
34
+ * Never throws — telemetry must not be the reason a delivered render fails. An unreadable width
35
+ * counts as WIDE (`NaN` fails the `< MIN_HIT_BAND_PX` test), because a census that guesses
36
+ * "defect" when it cannot measure would report the environment rather than the chart.
37
+ */
38
+ export declare function censusHitBands(container: any, doc?: any): HitBandCensus;
39
+ /**
40
+ * The always-on behaviour tag for this census, or "" when there is nothing to say.
41
+ *
42
+ * One tag per render in the `hitband:` namespace, so the rate is a `LIKE` away and the three
43
+ * states sum to every render that drew an open stroke — a rate needs a denominator, and
44
+ * `hitband:d3:ok` is it. Charts with no open-stroke marks at all emit nothing rather than a
45
+ * third value nobody will remember to exclude.
46
+ */
47
+ export declare function hitBandFlag(c: HitBandCensus): string;
@@ -1,6 +1,7 @@
1
1
  import { type RenderOptions, type ViewStateProvider } from "./contract";
2
2
  import { type ResolveOptionsInput } from "./defaults";
3
3
  import { type MarkCensus } from "./blankRender";
4
+ import { type HitBandCensus } from "./hitBands";
4
5
  export type RenderFn = (container: HTMLElement, data: any, options: RenderOptions) => void;
5
6
  /**
6
7
  * THE SESSION PROVIDER — what every host got hard-coded before contract 1.6.0, now named.
@@ -77,6 +78,18 @@ export interface ChartHostConfig {
77
78
  census: MarkCensus;
78
79
  rows: number;
79
80
  }) => void;
81
+ /**
82
+ * Called after every render with the hit-band census — how many of this chart's marks are
83
+ * thin open strokes, and how many of those have no wider companion to catch a click
84
+ * (2026-09-03). Counts only; the host decides what, if anything, to record. It fires even
85
+ * when everything is fine, because a rate needs a denominator and `interactive` is it: a
86
+ * chart with no open-stroke marks reports zeroes and should be excluded, not counted as good.
87
+ *
88
+ * This is the half a server-side code check structurally cannot do. A regex reads
89
+ * `.attr('stroke-width', 2)` and nothing else; `.attr('stroke-width', d => scale(d.v))` is a
90
+ * number only the browser knows, and this reads the COMPUTED width off the rendered element.
91
+ */
92
+ onHitBandCensus?: (census: HitBandCensus) => void;
80
93
  /** This chart declares time keyframes: frame one is allowed to be empty, so no blank verdict
81
94
  * is issued for it. */
82
95
  animated?: boolean;
@@ -16,3 +16,4 @@ export { qualifyPick, qualifyAuto, qualifyCancel, launchGenerates, launchFavorSt
16
16
  export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
17
17
  export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
18
18
  export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
19
+ export { censusHitBands, hitBandFlag, MIN_HIT_BAND_PX, type HitBandCensus } from "./hitBands";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.57",
3
+ "version": "0.5.59",
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",