@bicharts/chart-host 0.5.116 → 0.5.118

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.
@@ -3165,11 +3165,26 @@ 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",
3172
+ // AND THE NAME A REAL CHART ACTUALLY WRITES. This one is not a member of the mermaid
3173
+ // package at all: it is a helper the SERVER prepends into the generated code, and the
3174
+ // helper is what reaches for the library. So a genuine diagram chart contains
3175
+ // `d3.llmMermaid(` and never once writes `d3.mermaid` — measured on the first real
3176
+ // generation of that type, for which this scan returned an EMPTY list. It drew, because
3177
+ // the host it ran in happened to carry the library; a host relying on this answer to
3178
+ // decide what to install would have been told nothing and failed several frames deep.
3179
+ //
3180
+ // Mapping the helper is right rather than a hack, because this map answers "what must a
3181
+ // host INSTALL for this code to run" and not "which package exports this symbol". For
3182
+ // code that calls the helper, the answer is the library the helper reaches for.
3183
+ llmMermaid: "mermaid"
3169
3184
  };
3170
3185
  function requiredD3Plugins(code) {
3171
3186
  const out = /* @__PURE__ */ new Set();
3172
- for (const m2 of String(code || "").matchAll(/\bd3\s*\.\s*(\w+)\s*\(/g)) {
3187
+ for (const m2 of String(code || "").matchAll(/\bd3\s*\.\s*(\w+)\s*[.(]/g)) {
3173
3188
  const pkg = D3_PLUGIN_PACKAGES[m2[1]];
3174
3189
  if (pkg) out.add(pkg);
3175
3190
  }
@@ -3182,6 +3197,11 @@ function explainRenderFailure(err, d3) {
3182
3197
  "[@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
3198
  );
3184
3199
  }
3200
+ if (typeof d3?.mermaid?.render !== "function" && /\bmermaid\b/i.test(msg)) {
3201
+ return new Error(
3202
+ "[@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
3203
+ );
3204
+ }
3185
3205
  const m2 = /d3\.(\w+) is not a function|(\w+) is not a function/.exec(msg);
3186
3206
  const name = m2?.[1] ?? m2?.[2] ?? "";
3187
3207
  const pkg = D3_PLUGIN_PACKAGES[name];
@@ -3218,6 +3238,8 @@ function compileRenderFn(code, win, doc, d3) {
3218
3238
  if (typeof fn !== "function") throw new Error("generated code did not define render(container, data, options)");
3219
3239
  return fn;
3220
3240
  }
3241
+ var ASYNC_RENDER_TIMEOUT_MS = 15e3;
3242
+ var isThenable = (v3) => !!v3 && (typeof v3 === "object" || typeof v3 === "function") && typeof v3.then === "function";
3221
3243
  function createChartHost(container, config) {
3222
3244
  const doc = container.ownerDocument;
3223
3245
  const win = doc?.defaultView ?? globalThis;
@@ -3264,6 +3286,13 @@ function createChartHost(container, config) {
3264
3286
  let destroyed = false;
3265
3287
  let warnedGeo = false;
3266
3288
  let warnedBlank = false;
3289
+ let renderSeq = 0;
3290
+ let renderedPromise = Promise.resolve();
3291
+ const settle = (p2) => {
3292
+ renderedPromise = p2;
3293
+ p2.catch(() => {
3294
+ });
3295
+ };
3267
3296
  let current = null;
3268
3297
  let activeTickEl = null;
3269
3298
  let selectionIsPeriod = false;
@@ -3423,9 +3452,82 @@ function createChartHost(container, config) {
3423
3452
  }
3424
3453
  }
3425
3454
  };
3455
+ const runPostRenderPasses = () => {
3456
+ if (labelContrastOpts !== false) {
3457
+ try {
3458
+ const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
3459
+ if (!lc.pageBg) {
3460
+ const bg = resolved.backgroundColor ?? resolved.themeBg;
3461
+ if (typeof bg === "string" && bg) lc.pageBg = bg;
3462
+ }
3463
+ const r = applyLabelContrast(container, lc);
3464
+ config.onLabelContrast?.(r);
3465
+ } catch {
3466
+ }
3467
+ }
3468
+ ensureCrossfilterHitTargets(container, doc);
3469
+ paintSelection();
3470
+ try {
3471
+ const census = censusMarks(container);
3472
+ if (isBlankRender({
3473
+ markCount: census.markCount,
3474
+ rows: data.rows ? data.rows.length : 0,
3475
+ animated: config.animated,
3476
+ contractUntagged: config.contractUntagged
3477
+ })) {
3478
+ if (!warnedBlank) {
3479
+ warnedBlank = true;
3480
+ try {
3481
+ (win?.console ?? console)?.warn(
3482
+ `[@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.`
3483
+ );
3484
+ } catch {
3485
+ }
3486
+ }
3487
+ config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
3488
+ }
3489
+ } catch {
3490
+ }
3491
+ if (config.onHitBandCensus) {
3492
+ try {
3493
+ config.onHitBandCensus(censusHitBands(container, doc));
3494
+ } catch {
3495
+ }
3496
+ }
3497
+ if (config.onColourSpreadCensus) {
3498
+ try {
3499
+ config.onColourSpreadCensus(censusColourSpread(container, doc));
3500
+ } catch {
3501
+ }
3502
+ }
3503
+ if (config.onValuePlacementCensus) {
3504
+ try {
3505
+ config.onValuePlacementCensus(censusValuePlacement(container, data));
3506
+ } catch {
3507
+ }
3508
+ }
3509
+ if (fitOpts !== false) {
3510
+ try {
3511
+ const r = fitRenderedChart(container, fitOpts === true ? {} : fitOpts);
3512
+ config.onFit?.(r);
3513
+ } catch {
3514
+ }
3515
+ }
3516
+ };
3517
+ const renderFailure = (err) => {
3518
+ const message = String(err?.message ?? err ?? "");
3519
+ if (config.onInvalidSentinel && isInvalidSentinelError(message)) {
3520
+ stopAnim();
3521
+ container.innerHTML = "";
3522
+ config.onInvalidSentinel({ reason: invalidSentinelReason(message), message });
3523
+ return null;
3524
+ }
3525
+ return explainRenderFailure(err, d3);
3526
+ };
3426
3527
  const host = {
3427
3528
  render() {
3428
3529
  if (destroyed) return;
3530
+ const seq = ++renderSeq;
3429
3531
  stopAnim();
3430
3532
  container.innerHTML = "";
3431
3533
  if (!renderFn) {
@@ -3449,78 +3551,53 @@ function createChartHost(container, config) {
3449
3551
  if (resolved.geoPoint === void 0 && data.geoPoint) resolved = { ...resolved, geoPoint: data.geoPoint };
3450
3552
  if (resolved.geoPointDest === void 0 && data.geoPointDest) resolved = { ...resolved, geoPointDest: data.geoPointDest };
3451
3553
  if (resolved.dateCellsAreUtcDays === void 0 && data.dateCellsAreUtcDays) resolved = { ...resolved, dateCellsAreUtcDays: true };
3554
+ let outcome;
3452
3555
  try {
3453
- renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
3556
+ outcome = renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
3454
3557
  } catch (err) {
3455
- const message = String(err?.message ?? err ?? "");
3456
- if (config.onInvalidSentinel && isInvalidSentinelError(message)) {
3457
- stopAnim();
3458
- container.innerHTML = "";
3459
- config.onInvalidSentinel({ reason: invalidSentinelReason(message), message });
3460
- return;
3558
+ const fatal = renderFailure(err);
3559
+ if (fatal) {
3560
+ settle(Promise.reject(fatal));
3561
+ throw fatal;
3461
3562
  }
3462
- throw explainRenderFailure(err, d3);
3563
+ settle(Promise.resolve());
3564
+ return;
3463
3565
  }
3464
- if (labelContrastOpts !== false) {
3465
- try {
3466
- const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
3467
- if (!lc.pageBg) {
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
- }
3566
+ if (!isThenable(outcome)) {
3567
+ runPostRenderPasses();
3568
+ settle(Promise.resolve());
3569
+ return;
3475
3570
  }
3476
- ensureCrossfilterHitTargets(container, doc);
3477
- paintSelection();
3478
- try {
3479
- const census = censusMarks(container);
3480
- if (isBlankRender({
3481
- markCount: census.markCount,
3482
- rows: data.rows ? data.rows.length : 0,
3483
- animated: config.animated,
3484
- contractUntagged: config.contractUntagged
3485
- })) {
3486
- if (!warnedBlank) {
3487
- warnedBlank = true;
3488
- try {
3489
- (win?.console ?? console)?.warn(
3490
- `[@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.`
3491
- );
3492
- } catch {
3571
+ settle(new Promise((resolve, reject) => {
3572
+ const timer = setTimeout(() => {
3573
+ reject(new Error(
3574
+ `[@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.`
3575
+ ));
3576
+ }, ASYNC_RENDER_TIMEOUT_MS);
3577
+ const done = (fn) => {
3578
+ clearTimeout(timer);
3579
+ fn();
3580
+ };
3581
+ Promise.resolve(outcome).then(
3582
+ () => done(() => {
3583
+ if (destroyed || seq !== renderSeq) {
3584
+ resolve();
3585
+ return;
3493
3586
  }
3494
- }
3495
- config.onBlankRender?.({ census, rows: data.rows ? data.rows.length : 0 });
3496
- }
3497
- } catch {
3498
- }
3499
- if (config.onHitBandCensus) {
3500
- try {
3501
- config.onHitBandCensus(censusHitBands(container, doc));
3502
- } catch {
3503
- }
3504
- }
3505
- if (config.onColourSpreadCensus) {
3506
- try {
3507
- config.onColourSpreadCensus(censusColourSpread(container, doc));
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
- }
3587
+ runPostRenderPasses();
3588
+ resolve();
3589
+ }),
3590
+ (err) => done(() => {
3591
+ if (destroyed || seq !== renderSeq) {
3592
+ resolve();
3593
+ return;
3594
+ }
3595
+ const fatal = renderFailure(err);
3596
+ if (fatal) reject(fatal);
3597
+ else resolve();
3598
+ })
3599
+ );
3600
+ }));
3524
3601
  },
3525
3602
  setOptions(partial) {
3526
3603
  raw = { ...raw, ...partial };
@@ -3534,6 +3611,11 @@ function createChartHost(container, config) {
3534
3611
  get options() {
3535
3612
  return resolved;
3536
3613
  },
3614
+ // A GETTER, not a captured value: `rendered` names the LATEST render, so a host that
3615
+ // read it once would otherwise hold the promise of a draw long since replaced.
3616
+ get rendered() {
3617
+ return renderedPromise;
3618
+ },
3537
3619
  selection: {
3538
3620
  onChange(cb) {
3539
3621
  subs.add(cb);
package/dist/index.mjs CHANGED
@@ -130,7 +130,7 @@ import {
130
130
  toRGBA,
131
131
  unpinScrolledAxis,
132
132
  valuePlacementFlag
133
- } from "./chunk-W5XDW6OK.mjs";
133
+ } from "./chunk-2UU3N4AG.mjs";
134
134
  import "./chunk-A2GMXZP7.mjs";
135
135
  import "./chunk-GHODWOAL.mjs";
136
136
 
package/dist/react.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createChartHost,
5
5
  geoFromCache,
6
6
  loadGeo
7
- } from "./chunk-W5XDW6OK.mjs";
7
+ } from "./chunk-2UU3N4AG.mjs";
8
8
  import "./chunk-A2GMXZP7.mjs";
9
9
  import "./chunk-GHODWOAL.mjs";
10
10
 
@@ -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.116",
3
+ "version": "0.5.118",
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",