@bicharts/chart-host 0.5.58 → 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.
|
@@ -1227,6 +1227,87 @@ function blankRenderFlag(lang, cause) {
|
|
|
1227
1227
|
return `blankrender:${clean(lang)}:${clean(cause)}`;
|
|
1228
1228
|
}
|
|
1229
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
|
+
|
|
1230
1311
|
// src/host.ts
|
|
1231
1312
|
function sessionViewStateProvider(container) {
|
|
1232
1313
|
const holder = container;
|
|
@@ -1550,6 +1631,12 @@ function createChartHost(container, config) {
|
|
|
1550
1631
|
}
|
|
1551
1632
|
} catch {
|
|
1552
1633
|
}
|
|
1634
|
+
if (config.onHitBandCensus) {
|
|
1635
|
+
try {
|
|
1636
|
+
config.onHitBandCensus(censusHitBands(container, doc));
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1553
1640
|
},
|
|
1554
1641
|
setOptions(partial) {
|
|
1555
1642
|
raw = { ...raw, ...partial };
|
|
@@ -1655,6 +1742,9 @@ export {
|
|
|
1655
1742
|
censusMarks,
|
|
1656
1743
|
isBlankRender,
|
|
1657
1744
|
blankRenderFlag,
|
|
1745
|
+
MIN_HIT_BAND_PX,
|
|
1746
|
+
censusHitBands,
|
|
1747
|
+
hitBandFlag,
|
|
1658
1748
|
sessionViewStateProvider,
|
|
1659
1749
|
noopViewStateProvider,
|
|
1660
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-
|
|
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
|
@@ -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;
|
package/dist/types/host.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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",
|