@bicharts/chart-host 0.5.116 → 0.5.117
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.
- package/dist/{chunk-W5XDW6OK.mjs → chunk-VRFOZUAT.mjs} +138 -68
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/types/host.d.ts +14 -0
- package/package.json +1 -1
|
@@ -3165,11 +3165,14 @@ var D3_PLUGIN_PACKAGES = {
|
|
|
3165
3165
|
hexbin: "d3-hexbin",
|
|
3166
3166
|
voronoiTreemap: "d3-voronoi-treemap",
|
|
3167
3167
|
voronoiMap: "d3-voronoi-map",
|
|
3168
|
-
weightedVoronoi: "d3-weighted-voronoi"
|
|
3168
|
+
weightedVoronoi: "d3-weighted-voronoi",
|
|
3169
|
+
// Attached onto d3 like the rest, but reached as d3.mermaid.render(...) — a NAMESPACE
|
|
3170
|
+
// OBJECT with methods, not a function on d3. The scan below accounts for that shape.
|
|
3171
|
+
mermaid: "mermaid"
|
|
3169
3172
|
};
|
|
3170
3173
|
function requiredD3Plugins(code) {
|
|
3171
3174
|
const out = /* @__PURE__ */ new Set();
|
|
3172
|
-
for (const m2 of String(code || "").matchAll(/\bd3\s*\.\s*(\w+)\s
|
|
3175
|
+
for (const m2 of String(code || "").matchAll(/\bd3\s*\.\s*(\w+)\s*[.(]/g)) {
|
|
3173
3176
|
const pkg = D3_PLUGIN_PACKAGES[m2[1]];
|
|
3174
3177
|
if (pkg) out.add(pkg);
|
|
3175
3178
|
}
|
|
@@ -3182,6 +3185,11 @@ function explainRenderFailure(err, d3) {
|
|
|
3182
3185
|
"[@bicharts/chart-host] no d3 was provided, so the generated chart could not run. Pass it explicitly \u2014 createChartHost(el, { d3 }) / <BicChart d3={d3} /> \u2014 or set window.d3. The chart needs D3 v7 (npm install d3@7). Original error: " + msg
|
|
3183
3186
|
);
|
|
3184
3187
|
}
|
|
3188
|
+
if (typeof d3?.mermaid?.render !== "function" && /\bmermaid\b/i.test(msg)) {
|
|
3189
|
+
return new Error(
|
|
3190
|
+
"[@bicharts/chart-host] this chart draws its diagram with Mermaid, which it reaches as d3.mermaid.render(...), and the d3 you passed has no mermaid on it \u2014 this host did not load the library. A host supplies it exactly the way the other plugins are supplied: attach it onto the SAME d3 instance you hand to createChartHost \u2014 npm install mermaid then import mermaid from 'mermaid'; Object.assign(d3, { mermaid }); Original error: " + msg
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3185
3193
|
const m2 = /d3\.(\w+) is not a function|(\w+) is not a function/.exec(msg);
|
|
3186
3194
|
const name = m2?.[1] ?? m2?.[2] ?? "";
|
|
3187
3195
|
const pkg = D3_PLUGIN_PACKAGES[name];
|
|
@@ -3218,6 +3226,8 @@ function compileRenderFn(code, win, doc, d3) {
|
|
|
3218
3226
|
if (typeof fn !== "function") throw new Error("generated code did not define render(container, data, options)");
|
|
3219
3227
|
return fn;
|
|
3220
3228
|
}
|
|
3229
|
+
var ASYNC_RENDER_TIMEOUT_MS = 15e3;
|
|
3230
|
+
var isThenable = (v3) => !!v3 && (typeof v3 === "object" || typeof v3 === "function") && typeof v3.then === "function";
|
|
3221
3231
|
function createChartHost(container, config) {
|
|
3222
3232
|
const doc = container.ownerDocument;
|
|
3223
3233
|
const win = doc?.defaultView ?? globalThis;
|
|
@@ -3264,6 +3274,13 @@ function createChartHost(container, config) {
|
|
|
3264
3274
|
let destroyed = false;
|
|
3265
3275
|
let warnedGeo = false;
|
|
3266
3276
|
let warnedBlank = false;
|
|
3277
|
+
let renderSeq = 0;
|
|
3278
|
+
let renderedPromise = Promise.resolve();
|
|
3279
|
+
const settle = (p2) => {
|
|
3280
|
+
renderedPromise = p2;
|
|
3281
|
+
p2.catch(() => {
|
|
3282
|
+
});
|
|
3283
|
+
};
|
|
3267
3284
|
let current = null;
|
|
3268
3285
|
let activeTickEl = null;
|
|
3269
3286
|
let selectionIsPeriod = false;
|
|
@@ -3423,9 +3440,82 @@ function createChartHost(container, config) {
|
|
|
3423
3440
|
}
|
|
3424
3441
|
}
|
|
3425
3442
|
};
|
|
3443
|
+
const runPostRenderPasses = () => {
|
|
3444
|
+
if (labelContrastOpts !== false) {
|
|
3445
|
+
try {
|
|
3446
|
+
const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
|
|
3447
|
+
if (!lc.pageBg) {
|
|
3448
|
+
const bg = resolved.backgroundColor ?? resolved.themeBg;
|
|
3449
|
+
if (typeof bg === "string" && bg) lc.pageBg = bg;
|
|
3450
|
+
}
|
|
3451
|
+
const r = applyLabelContrast(container, lc);
|
|
3452
|
+
config.onLabelContrast?.(r);
|
|
3453
|
+
} catch {
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
ensureCrossfilterHitTargets(container, doc);
|
|
3457
|
+
paintSelection();
|
|
3458
|
+
try {
|
|
3459
|
+
const census = censusMarks(container);
|
|
3460
|
+
if (isBlankRender({
|
|
3461
|
+
markCount: census.markCount,
|
|
3462
|
+
rows: data.rows ? data.rows.length : 0,
|
|
3463
|
+
animated: config.animated,
|
|
3464
|
+
contractUntagged: config.contractUntagged
|
|
3465
|
+
})) {
|
|
3466
|
+
if (!warnedBlank) {
|
|
3467
|
+
warnedBlank = true;
|
|
3468
|
+
try {
|
|
3469
|
+
(win?.console ?? console)?.warn(
|
|
3470
|
+
`[@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.`
|
|
3471
|
+
);
|
|
3472
|
+
} catch {
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
|
|
3476
|
+
}
|
|
3477
|
+
} catch {
|
|
3478
|
+
}
|
|
3479
|
+
if (config.onHitBandCensus) {
|
|
3480
|
+
try {
|
|
3481
|
+
config.onHitBandCensus(censusHitBands(container, doc));
|
|
3482
|
+
} catch {
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
if (config.onColourSpreadCensus) {
|
|
3486
|
+
try {
|
|
3487
|
+
config.onColourSpreadCensus(censusColourSpread(container, doc));
|
|
3488
|
+
} catch {
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
if (config.onValuePlacementCensus) {
|
|
3492
|
+
try {
|
|
3493
|
+
config.onValuePlacementCensus(censusValuePlacement(container, data));
|
|
3494
|
+
} catch {
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
if (fitOpts !== false) {
|
|
3498
|
+
try {
|
|
3499
|
+
const r = fitRenderedChart(container, fitOpts === true ? {} : fitOpts);
|
|
3500
|
+
config.onFit?.(r);
|
|
3501
|
+
} catch {
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
const renderFailure = (err) => {
|
|
3506
|
+
const message = String(err?.message ?? err ?? "");
|
|
3507
|
+
if (config.onInvalidSentinel && isInvalidSentinelError(message)) {
|
|
3508
|
+
stopAnim();
|
|
3509
|
+
container.innerHTML = "";
|
|
3510
|
+
config.onInvalidSentinel({ reason: invalidSentinelReason(message), message });
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
return explainRenderFailure(err, d3);
|
|
3514
|
+
};
|
|
3426
3515
|
const host = {
|
|
3427
3516
|
render() {
|
|
3428
3517
|
if (destroyed) return;
|
|
3518
|
+
const seq = ++renderSeq;
|
|
3429
3519
|
stopAnim();
|
|
3430
3520
|
container.innerHTML = "";
|
|
3431
3521
|
if (!renderFn) {
|
|
@@ -3449,78 +3539,53 @@ function createChartHost(container, config) {
|
|
|
3449
3539
|
if (resolved.geoPoint === void 0 && data.geoPoint) resolved = { ...resolved, geoPoint: data.geoPoint };
|
|
3450
3540
|
if (resolved.geoPointDest === void 0 && data.geoPointDest) resolved = { ...resolved, geoPointDest: data.geoPointDest };
|
|
3451
3541
|
if (resolved.dateCellsAreUtcDays === void 0 && data.dateCellsAreUtcDays) resolved = { ...resolved, dateCellsAreUtcDays: true };
|
|
3542
|
+
let outcome;
|
|
3452
3543
|
try {
|
|
3453
|
-
renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
|
|
3544
|
+
outcome = renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
|
|
3454
3545
|
} catch (err) {
|
|
3455
|
-
const
|
|
3456
|
-
if (
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
config.onInvalidSentinel({ reason: invalidSentinelReason(message), message });
|
|
3460
|
-
return;
|
|
3546
|
+
const fatal = renderFailure(err);
|
|
3547
|
+
if (fatal) {
|
|
3548
|
+
settle(Promise.reject(fatal));
|
|
3549
|
+
throw fatal;
|
|
3461
3550
|
}
|
|
3462
|
-
|
|
3551
|
+
settle(Promise.resolve());
|
|
3552
|
+
return;
|
|
3463
3553
|
}
|
|
3464
|
-
if (
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
const bg = resolved.backgroundColor ?? resolved.themeBg;
|
|
3469
|
-
if (typeof bg === "string" && bg) lc.pageBg = bg;
|
|
3470
|
-
}
|
|
3471
|
-
const r = applyLabelContrast(container, lc);
|
|
3472
|
-
config.onLabelContrast?.(r);
|
|
3473
|
-
} catch {
|
|
3474
|
-
}
|
|
3554
|
+
if (!isThenable(outcome)) {
|
|
3555
|
+
runPostRenderPasses();
|
|
3556
|
+
settle(Promise.resolve());
|
|
3557
|
+
return;
|
|
3475
3558
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
(
|
|
3490
|
-
|
|
3491
|
-
);
|
|
3492
|
-
} catch {
|
|
3559
|
+
settle(new Promise((resolve, reject) => {
|
|
3560
|
+
const timer = setTimeout(() => {
|
|
3561
|
+
reject(new Error(
|
|
3562
|
+
`[@bicharts/chart-host] the chart did not finish drawing within ${Math.round(ASYNC_RENDER_TIMEOUT_MS / 1e3)}s, so the host stopped waiting for it. Nothing past that point ran: no label-contrast pass, no hit-target heal, no census, no fit. A render() that returns a promise has to settle it.`
|
|
3563
|
+
));
|
|
3564
|
+
}, ASYNC_RENDER_TIMEOUT_MS);
|
|
3565
|
+
const done = (fn) => {
|
|
3566
|
+
clearTimeout(timer);
|
|
3567
|
+
fn();
|
|
3568
|
+
};
|
|
3569
|
+
Promise.resolve(outcome).then(
|
|
3570
|
+
() => done(() => {
|
|
3571
|
+
if (destroyed || seq !== renderSeq) {
|
|
3572
|
+
resolve();
|
|
3573
|
+
return;
|
|
3493
3574
|
}
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
} catch {
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
if (config.onValuePlacementCensus) {
|
|
3512
|
-
try {
|
|
3513
|
-
config.onValuePlacementCensus(censusValuePlacement(container, data));
|
|
3514
|
-
} catch {
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
if (fitOpts !== false) {
|
|
3518
|
-
try {
|
|
3519
|
-
const r = fitRenderedChart(container, fitOpts === true ? {} : fitOpts);
|
|
3520
|
-
config.onFit?.(r);
|
|
3521
|
-
} catch {
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3575
|
+
runPostRenderPasses();
|
|
3576
|
+
resolve();
|
|
3577
|
+
}),
|
|
3578
|
+
(err) => done(() => {
|
|
3579
|
+
if (destroyed || seq !== renderSeq) {
|
|
3580
|
+
resolve();
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
const fatal = renderFailure(err);
|
|
3584
|
+
if (fatal) reject(fatal);
|
|
3585
|
+
else resolve();
|
|
3586
|
+
})
|
|
3587
|
+
);
|
|
3588
|
+
}));
|
|
3524
3589
|
},
|
|
3525
3590
|
setOptions(partial) {
|
|
3526
3591
|
raw = { ...raw, ...partial };
|
|
@@ -3534,6 +3599,11 @@ function createChartHost(container, config) {
|
|
|
3534
3599
|
get options() {
|
|
3535
3600
|
return resolved;
|
|
3536
3601
|
},
|
|
3602
|
+
// A GETTER, not a captured value: `rendered` names the LATEST render, so a host that
|
|
3603
|
+
// read it once would otherwise hold the promise of a draw long since replaced.
|
|
3604
|
+
get rendered() {
|
|
3605
|
+
return renderedPromise;
|
|
3606
|
+
},
|
|
3537
3607
|
selection: {
|
|
3538
3608
|
onChange(cb) {
|
|
3539
3609
|
subs.add(cb);
|
package/dist/index.mjs
CHANGED
package/dist/react.mjs
CHANGED
package/dist/types/host.d.ts
CHANGED
|
@@ -184,6 +184,20 @@ export interface ChartHostConfig {
|
|
|
184
184
|
}
|
|
185
185
|
export interface ChartHost {
|
|
186
186
|
render(): void;
|
|
187
|
+
/**
|
|
188
|
+
* THE LAST RENDER, SETTLED. render() stays synchronous — it returns nothing, exactly as it
|
|
189
|
+
* always has — but a generated chart whose drawing library has no synchronous API returns a
|
|
190
|
+
* PROMISE from its own render(), and every post-render pass (label contrast, hit-target
|
|
191
|
+
* heal, census, fit) has to wait for that before it measures anything. This is how a caller
|
|
192
|
+
* waits for the same moment: it resolves once those passes have run, and rejects with the
|
|
193
|
+
* error render() would have thrown had the chart drawn synchronously.
|
|
194
|
+
*
|
|
195
|
+
* On a synchronous chart it is an ALREADY-RESOLVED promise, so awaiting it is free and
|
|
196
|
+
* correct for both kinds and a host needs no test for which it has. Replaced on every
|
|
197
|
+
* render(), so read it after the render() whose finish you mean; when renders overlap, the
|
|
198
|
+
* LAST one wins and the superseded one settles without touching the chart.
|
|
199
|
+
*/
|
|
200
|
+
readonly rendered: Promise<void>;
|
|
187
201
|
/** Merge raw option changes and re-render — the live-restyle path (colour scale,
|
|
188
202
|
* aggregation, animMaxIdealFrames, timeline style…). Never a regeneration. */
|
|
189
203
|
setOptions(partial: ResolveOptionsInput): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.117",
|
|
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",
|