@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-
|
|
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
package/dist/types/contract.d.ts
CHANGED
|
@@ -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;
|
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",
|