@bicharts/chart-host 0.5.115 → 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.
@@ -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*\(/g)) {
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 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;
3546
+ const fatal = renderFailure(err);
3547
+ if (fatal) {
3548
+ settle(Promise.reject(fatal));
3549
+ throw fatal;
3461
3550
  }
3462
- throw explainRenderFailure(err, d3);
3551
+ settle(Promise.resolve());
3552
+ return;
3463
3553
  }
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
- }
3554
+ if (!isThenable(outcome)) {
3555
+ runPostRenderPasses();
3556
+ settle(Promise.resolve());
3557
+ return;
3475
3558
  }
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 {
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
- 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
- }
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
@@ -130,7 +130,7 @@ import {
130
130
  toRGBA,
131
131
  unpinScrolledAxis,
132
132
  valuePlacementFlag
133
- } from "./chunk-W5XDW6OK.mjs";
133
+ } from "./chunk-VRFOZUAT.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-VRFOZUAT.mjs";
8
8
  import "./chunk-A2GMXZP7.mjs";
9
9
  import "./chunk-GHODWOAL.mjs";
10
10
 
@@ -146,12 +146,11 @@ export interface RenderOptions {
146
146
  * the scrubber when they follow. ADVISORY TELEMETRY: the host logs it, and nothing about the
147
147
  * chart depends on a host supplying it.
148
148
  *
149
- * ITEM 639. Until now no host could tell a control that fired from a control that does
150
- * nothing. The Power BI visual left an INDIRECT trace, because working a control persists
151
- * view-state and that write is logged; the Excel add-in's write is silent, so a reader
152
- * reporting "the buttons do nothing" left no record anywhere. Establishing that a control had
153
- * in fact fired meant pulling the generated source out of the database and reading it — which
154
- * is exactly what item 638 had to do.
149
+ * Until now no host could tell a control that fired from a control that does nothing. One
150
+ * host left an INDIRECT trace, because working a control persists view-state and that write
151
+ * is logged there; another host's write was silent, so a reader reporting "the buttons do
152
+ * nothing" left no record anywhere. Establishing that a control had in fact fired meant
153
+ * pulling the generated source out of storage and reading it by hand.
155
154
  *
156
155
  * CLICKS ONLY, and there is deliberately no `phase`. A control also settles on a value at
157
156
  * first paint, on every resize and on every re-render; reporting those would bury the
@@ -163,7 +162,7 @@ export interface RenderOptions {
163
162
  */
164
163
  onControlChange?: (e: ChartControlChange) => void;
165
164
  }
166
- /** One reader gesture on a chart's own control (item 639). */
165
+ /** One reader gesture on a chart's own control. */
167
166
  export interface ChartControlChange {
168
167
  /** The helper family that owns the control — "segmented" today. */
169
168
  control: string;
@@ -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.115",
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",