@bicharts/chart-host 0.5.44 → 0.5.45
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.
|
@@ -1086,6 +1086,42 @@ function ensureCrossfilterHitTargets(container, doc) {
|
|
|
1086
1086
|
return report;
|
|
1087
1087
|
}
|
|
1088
1088
|
|
|
1089
|
+
// src/blankRender.ts
|
|
1090
|
+
var EMPTY = { markCount: 0, legendMarkCount: 0, containerKind: "empty" };
|
|
1091
|
+
function censusMarks(container) {
|
|
1092
|
+
if (!container || typeof container.querySelectorAll !== "function") return EMPTY;
|
|
1093
|
+
try {
|
|
1094
|
+
const marks = /* @__PURE__ */ new Set();
|
|
1095
|
+
for (const el of Array.from(container.querySelectorAll(`.${MARK_CLASS}`))) marks.add(el);
|
|
1096
|
+
for (const el of Array.from(container.querySelectorAll(`[${ROW_IDX_ATTR}]`))) marks.add(el);
|
|
1097
|
+
const legend = /* @__PURE__ */ new Set();
|
|
1098
|
+
for (const el of Array.from(container.querySelectorAll(`.${LEGEND_MARK_CLASS}`))) {
|
|
1099
|
+
legend.add(el);
|
|
1100
|
+
marks.delete(el);
|
|
1101
|
+
}
|
|
1102
|
+
const hasSvg = !!container.querySelector("svg");
|
|
1103
|
+
const hasAnything = hasSvg || (container.childElementCount ?? 0) > 0;
|
|
1104
|
+
return {
|
|
1105
|
+
markCount: marks.size,
|
|
1106
|
+
legendMarkCount: legend.size,
|
|
1107
|
+
containerKind: hasSvg ? "svg" : hasAnything ? "html" : "empty"
|
|
1108
|
+
};
|
|
1109
|
+
} catch {
|
|
1110
|
+
return EMPTY;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function isBlankRender(i) {
|
|
1114
|
+
if (i.contractUntagged) return false;
|
|
1115
|
+
if (i.animated) return false;
|
|
1116
|
+
if (i.authoredNoDataText) return false;
|
|
1117
|
+
if (!(i.rows > 0)) return false;
|
|
1118
|
+
return i.markCount === 0;
|
|
1119
|
+
}
|
|
1120
|
+
function blankRenderFlag(lang, cause) {
|
|
1121
|
+
const clean = (s) => String(s || "unknown").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
1122
|
+
return `blankrender:${clean(lang)}:${clean(cause)}`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1089
1125
|
// src/host.ts
|
|
1090
1126
|
var parseRowIdxs = (s) => (s || "").split(",").map((x2) => parseInt(x2, 10)).filter((n) => Number.isFinite(n));
|
|
1091
1127
|
var D3_PLUGIN_PACKAGES = {
|
|
@@ -1186,6 +1222,7 @@ function createChartHost(container, config) {
|
|
|
1186
1222
|
let renderFn = config.renderFn ?? null;
|
|
1187
1223
|
let destroyed = false;
|
|
1188
1224
|
let warnedGeo = false;
|
|
1225
|
+
let warnedBlank = false;
|
|
1189
1226
|
let current = null;
|
|
1190
1227
|
let activeTickEl = null;
|
|
1191
1228
|
let selectionIsPeriod = false;
|
|
@@ -1353,6 +1390,27 @@ function createChartHost(container, config) {
|
|
|
1353
1390
|
}
|
|
1354
1391
|
ensureCrossfilterHitTargets(container, doc);
|
|
1355
1392
|
paintSelection();
|
|
1393
|
+
try {
|
|
1394
|
+
const census = censusMarks(container);
|
|
1395
|
+
if (isBlankRender({
|
|
1396
|
+
markCount: census.markCount,
|
|
1397
|
+
rows: data.rows ? data.rows.length : 0,
|
|
1398
|
+
animated: config.animated,
|
|
1399
|
+
contractUntagged: config.contractUntagged
|
|
1400
|
+
})) {
|
|
1401
|
+
if (!warnedBlank) {
|
|
1402
|
+
warnedBlank = true;
|
|
1403
|
+
try {
|
|
1404
|
+
(win?.console ?? console)?.warn(
|
|
1405
|
+
`[@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.`
|
|
1406
|
+
);
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
|
|
1411
|
+
}
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1356
1414
|
},
|
|
1357
1415
|
setOptions(partial) {
|
|
1358
1416
|
raw = { ...raw, ...partial };
|
|
@@ -1448,6 +1506,9 @@ export {
|
|
|
1448
1506
|
buildRenderPayload,
|
|
1449
1507
|
createMarkResolver,
|
|
1450
1508
|
ensureCrossfilterHitTargets,
|
|
1509
|
+
censusMarks,
|
|
1510
|
+
isBlankRender,
|
|
1511
|
+
blankRenderFlag,
|
|
1451
1512
|
requiredD3Plugins,
|
|
1452
1513
|
explainRenderFailure,
|
|
1453
1514
|
stripEsmExports,
|
package/dist/index.mjs
CHANGED
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
ROW_IDX_ATTR,
|
|
31
31
|
SELECTION_ACTIVE_CLASS,
|
|
32
32
|
XFILTER_REFRESH_EVENT,
|
|
33
|
+
blankRenderFlag,
|
|
33
34
|
buildRenderPayload,
|
|
35
|
+
censusMarks,
|
|
34
36
|
chartOwnsTimeline,
|
|
35
37
|
clearGeoCache,
|
|
36
38
|
compileRenderFn,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
explainRenderFailure,
|
|
41
43
|
geoAssetFor,
|
|
42
44
|
geoFromCache,
|
|
45
|
+
isBlankRender,
|
|
43
46
|
loadGeo,
|
|
44
47
|
periodTickSuppressesFeedback,
|
|
45
48
|
registerGeo,
|
|
@@ -47,7 +50,7 @@ import {
|
|
|
47
50
|
requiredD3Plugins,
|
|
48
51
|
resolveOptions,
|
|
49
52
|
stripEsmExports
|
|
50
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-DZ5FNFID.mjs";
|
|
51
54
|
import "./chunk-A2GMXZP7.mjs";
|
|
52
55
|
|
|
53
56
|
// src/trivial.ts
|
|
@@ -811,10 +814,12 @@ export {
|
|
|
811
814
|
actionFor,
|
|
812
815
|
askApplyImprovements,
|
|
813
816
|
bareBase64,
|
|
817
|
+
blankRenderFlag,
|
|
814
818
|
buildRenderPayload,
|
|
815
819
|
buildReviewWire,
|
|
816
820
|
canConfirmLaunch,
|
|
817
821
|
captureSvgSnapshot,
|
|
822
|
+
censusMarks,
|
|
818
823
|
chartOwnsTimeline,
|
|
819
824
|
chooserFitsViewport,
|
|
820
825
|
clearGeoCache,
|
|
@@ -828,6 +833,7 @@ export {
|
|
|
828
833
|
explainRenderFailure,
|
|
829
834
|
geoAssetFor,
|
|
830
835
|
geoFromCache,
|
|
836
|
+
isBlankRender,
|
|
831
837
|
launchFavorStyle,
|
|
832
838
|
launchGenerates,
|
|
833
839
|
loadGeo,
|
package/dist/react.mjs
CHANGED
|
@@ -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;
|
package/dist/types/host.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.45",
|
|
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",
|