@bicharts/chart-host 0.6.90 → 0.6.91

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/README.md CHANGED
@@ -269,9 +269,11 @@ cue, a hierarchy level) is never flattened by a fix meant for another chart.
269
269
 
270
270
  **How far the rest dims, per chart.** `options.dimOpacity` (a share of full opacity, `0.01`-`1`;
271
271
  `DIM_OPACITY_MIN` is the floor) sets the level for one host without writing CSS, and it is live
272
- through `setOptions`. The host writes it as `--lch-dim-opacity` on its own container and removes
273
- only what it wrote, so with the option absent nothing is written and the CSS above decides: the
274
- stylesheet's `0.25`, or whatever your page sets on the variable.
272
+ through `setOptions`. It takes a number or a numeric string; a value above 1 is clamped to 1 (no
273
+ dim), never read as a percentage, and anything else is ignored. The host writes it as
274
+ `--lch-dim-opacity` on its own container and, when the option goes away or the host is destroyed,
275
+ puts back the value your page had set inline there, so with the option absent nothing is written
276
+ and the CSS above decides: the stylesheet's `0.25`, or whatever your page sets on the variable.
275
277
 
276
278
  ```js
277
279
  createChartHost(el, { code, data, options: { dimOpacity: 0.15 } });
@@ -410,15 +412,27 @@ const zoomHost: ZoomMemoryHost = {
410
412
  persistZoom: json => localStorage.setItem("chart-zoom", json), // "" forgets
411
413
  log: () => {},
412
414
  };
413
- const zoomMemory = createZoomMemoryHook(zoomHost); // once per container, reused by every host on it
415
+ const zoomMemory = createZoomMemoryHook(zoomHost); // once per chart, reused by every host built for it
414
416
  const host = createChartHost(el, { code, data, d3, zoomMemory });
415
417
  ```
416
418
 
417
419
  The view survives every redraw in the session whatever the preference says, and a close and reopen
418
420
  only while `rememberAcrossSessions()` is true. It is keyed by the chart's code and the viewport size
419
421
  (to the nearest 32 px), so a regenerated chart or a real resize opens at full extent instead of
420
- somewhere arbitrary. A chart with several zoomable panels is left alone. In React, pass the hook as
421
- the `zoomMemory` prop of `<BicChart>`.
422
+ somewhere arbitrary. A chart with several zoomable panels is left alone. The `ZoomMemoryHost` is the
423
+ memory: keep one per chart for as long as the chart is on the page, and hand its hook to every host
424
+ built for that chart, on the same element or, after a remount, a new one (a destroyed host releases
425
+ the hook's watcher, so the next one can take it). In React, pass the hook as the `zoomMemory` prop of
426
+ `<BicChart>`, built once (`zoomHost` as above, kept outside the render):
427
+
428
+ ```tsx
429
+ const [zoomMemory] = useState(() => createZoomMemoryHook(zoomHost)); // once, however often this renders
430
+ return <BicChart code={code} data={data} d3={d3} zoomMemory={zoomMemory} />;
431
+ ```
432
+
433
+ A hook that arrives after mount, or goes away, rebuilds the chart's host; swapping one hook for another
434
+ on a mounted `<BicChart>` rebuilds nothing, and applies at the next rebuild (a new `code`, say) or at
435
+ once if you remount it with a `key`, so a hook built inline in the props costs no rebuild.
422
436
 
423
437
  **A scrolled chart** keeps its place with no setup. A chart taller than its frame scrolls, and every
424
438
  redraw empties the container, which used to send the reader back to the top. The host now parks the
@@ -614,10 +614,14 @@ function resolveOptions(p) {
614
614
  cultureCode: p.cultureCode,
615
615
  allowTooltips: p.allowTooltips,
616
616
  noDataText: p.noDataText,
617
- // A finite number is clamped to DIM_OPACITY_MIN-1; absent, blank or junk stays undefined so the stylesheet - and a
618
- // page's own CSS override - still decides. Never defaulted here: that would override every page's CSS.
617
+ // A finite number, or a string that is numeric once trimmed, is clamped to DIM_OPACITY_MIN-1; anything else (absent,
618
+ // blank, a boolean, an array, an object) stays undefined so the stylesheet - and a page's own CSS override - still
619
+ // decides. Not numberOr: Number() reads true as 1 and false, [] and whitespace as 0, which would dim to the floor.
620
+ // Above 1 is clamped to 1, which dims nothing; it is never reinterpreted as a percentage, so the option keeps one
621
+ // unit (a share of full opacity) for every value. Never defaulted here: that would override every page's CSS.
619
622
  dimOpacity: (() => {
620
- const n = numberOr(p.dimOpacity, NaN);
623
+ const v = p.dimOpacity;
624
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
621
625
  return Number.isFinite(n) ? clamp(n, DIM_OPACITY_MIN, 1) : void 0;
622
626
  })(),
623
627
  // A recognised glyph or none: an unknown string must never reach the painter as a live mode, and "none" is
@@ -2031,6 +2035,7 @@ function compileRenderFn(code, win, doc, d3) {
2031
2035
  }
2032
2036
  var ASYNC_RENDER_TIMEOUT_MS = 15e3;
2033
2037
  var isThenable = (v) => !!v && (typeof v === "object" || typeof v === "function") && typeof v.then === "function";
2038
+ var dimVarPage = /* @__PURE__ */ new WeakMap();
2034
2039
  function createChartHost(container, config) {
2035
2040
  const doc = container.ownerDocument;
2036
2041
  const win = doc?.defaultView ?? globalThis;
@@ -2098,6 +2103,19 @@ function createChartHost(container, config) {
2098
2103
  });
2099
2104
  };
2100
2105
  let dimVarSet = false;
2106
+ const releaseDimVar = () => {
2107
+ if (!dimVarSet) return;
2108
+ dimVarSet = false;
2109
+ const page = dimVarPage.get(container);
2110
+ if (!page || --page.holders > 0) return;
2111
+ dimVarPage.delete(container);
2112
+ try {
2113
+ if (container.style?.getPropertyValue?.(DIM_OPACITY_VAR) !== page.wrote) return;
2114
+ if (page.was) container.style?.setProperty(DIM_OPACITY_VAR, page.was.value, page.was.priority);
2115
+ else container.style?.removeProperty(DIM_OPACITY_VAR);
2116
+ } catch {
2117
+ }
2118
+ };
2101
2119
  let current = null;
2102
2120
  let activeTickEl = null;
2103
2121
  let selectionIsPeriod = false;
@@ -2169,6 +2187,7 @@ function createChartHost(container, config) {
2169
2187
  }
2170
2188
  };
2171
2189
  const paintSelection = () => {
2190
+ if (destroyed) return;
2172
2191
  try {
2173
2192
  const sel = current;
2174
2193
  const active = !!sel && sel.length > 0;
@@ -2176,12 +2195,20 @@ function createChartHost(container, config) {
2176
2195
  container.classList?.[dim ? "add" : "remove"](SELECTION_ACTIVE_CLASS);
2177
2196
  const want = resolved.dimOpacity;
2178
2197
  if (typeof want === "number") {
2179
- container.style?.setProperty(DIM_OPACITY_VAR, String(want));
2180
- dimVarSet = true;
2181
- } else if (dimVarSet) {
2182
- container.style?.removeProperty(DIM_OPACITY_VAR);
2183
- dimVarSet = false;
2184
- }
2198
+ const level = String(want);
2199
+ let page = dimVarPage.get(container);
2200
+ if (!dimVarSet) {
2201
+ if (!page) {
2202
+ const value = container.style?.getPropertyValue?.(DIM_OPACITY_VAR) ?? "";
2203
+ page = { was: value ? { value, priority: container.style?.getPropertyPriority?.(DIM_OPACITY_VAR) ?? "" } : null, holders: 0, wrote: level };
2204
+ dimVarPage.set(container, page);
2205
+ }
2206
+ page.holders++;
2207
+ dimVarSet = true;
2208
+ }
2209
+ if (page) page.wrote = level;
2210
+ container.style?.setProperty(DIM_OPACITY_VAR, level);
2211
+ } else releaseDimVar();
2185
2212
  const lit = container.querySelectorAll(`.${ACTIVE_TICK_CLASS}`);
2186
2213
  for (const t of Array.from(lit)) t.classList?.remove(ACTIVE_TICK_CLASS);
2187
2214
  const tickStillDrawn = !!activeTickEl && (typeof container.contains === "function" ? container.contains(activeTickEl) : activeTickEl.isConnected !== false);
@@ -2418,6 +2445,7 @@ function createChartHost(container, config) {
2418
2445
  }
2419
2446
  };
2420
2447
  const notify = (rowIdxs, source, tick, fromChart = false) => {
2448
+ if (destroyed) return;
2421
2449
  current = rowIdxs;
2422
2450
  selectionFromChart = rowIdxs.length > 0 && fromChart;
2423
2451
  activeTickEl = rowIdxs.length ? tick ?? null : null;
@@ -2523,6 +2551,14 @@ function createChartHost(container, config) {
2523
2551
  }
2524
2552
  }
2525
2553
  };
2554
+ const releaseZoom = () => {
2555
+ if (zoomMemory) {
2556
+ try {
2557
+ zoomMemory.release?.(container);
2558
+ } catch {
2559
+ }
2560
+ }
2561
+ };
2526
2562
  const keepScroll = config.restoreScroll !== false;
2527
2563
  let chartSource;
2528
2564
  const scrollKey = () => {
@@ -2761,12 +2797,18 @@ function createChartHost(container, config) {
2761
2797
  );
2762
2798
  }));
2763
2799
  },
2800
+ // A DESTROYED HOST OWNS NOTHING ON THE CONTAINER any more. A host rebuilt on the same element has taken over
2801
+ // the chart's slots (its clear, its animation stop, its initial mark) and the clicked-marks record, so every
2802
+ // method below that reaches the container returns first: a stale reference to the old host (a closure, a late
2803
+ // callback) must not clear, stop or annotate the chart that is on screen now.
2764
2804
  setOptions(partial) {
2805
+ if (destroyed) return;
2765
2806
  raw = { ...raw, ...partial };
2766
2807
  resolved = resolveForHost(raw);
2767
2808
  host.render();
2768
2809
  },
2769
2810
  setData(next) {
2811
+ if (destroyed) return;
2770
2812
  data = next;
2771
2813
  host.render();
2772
2814
  },
@@ -2774,8 +2816,9 @@ function createChartHost(container, config) {
2774
2816
  return resolved;
2775
2817
  },
2776
2818
  setAnnotations(list) {
2819
+ if (destroyed) return;
2777
2820
  annotationLayer.set(list);
2778
- if (!destroyed) try {
2821
+ try {
2779
2822
  drawAnnotations();
2780
2823
  } catch {
2781
2824
  }
@@ -2794,6 +2837,7 @@ function createChartHost(container, config) {
2794
2837
  return () => subs.delete(cb);
2795
2838
  },
2796
2839
  clear() {
2840
+ if (destroyed) return;
2797
2841
  const s = container[CONTAINER_SLOT_XF_CLEAR];
2798
2842
  if (typeof s === "function") {
2799
2843
  try {
@@ -2806,6 +2850,7 @@ function createChartHost(container, config) {
2806
2850
  notify([], "host");
2807
2851
  },
2808
2852
  highlight(rowIdxs) {
2853
+ if (destroyed) return;
2809
2854
  const rows = Array.isArray(rowIdxs) ? rowIdxs.slice() : [];
2810
2855
  if (lasso && lasso.rows().length && !sameRowSet(rows, lasso.rows())) lasso.clear({ publish: false, source: "host" });
2811
2856
  notify(rows, "host");
@@ -2814,9 +2859,11 @@ function createChartHost(container, config) {
2814
2859
  return current;
2815
2860
  }
2816
2861
  },
2817
- animation: { stop: stopAnim },
2862
+ animation: { stop() {
2863
+ if (!destroyed) stopAnim();
2864
+ } },
2818
2865
  initialMark() {
2819
- return container[CONTAINER_SLOT_INITIAL_XF_MARK] ?? null;
2866
+ return destroyed ? null : container[CONTAINER_SLOT_INITIAL_XF_MARK] ?? null;
2820
2867
  },
2821
2868
  destroy() {
2822
2869
  if (destroyed) return;
@@ -2828,17 +2875,12 @@ function createChartHost(container, config) {
2828
2875
  clearPress(container);
2829
2876
  disposeLasso();
2830
2877
  subs.clear();
2831
- if (dimVarSet) {
2832
- try {
2833
- container.style?.removeProperty(DIM_OPACITY_VAR);
2834
- } catch {
2835
- }
2836
- dimVarSet = false;
2837
- }
2878
+ releaseDimVar();
2838
2879
  annotationLayer.destroy();
2839
2880
  if (hasRendered && keepScroll) rememberScroll(container, scrollKey());
2840
2881
  unpinScrolledAxis(container);
2841
2882
  flushZoom();
2883
+ releaseZoom();
2842
2884
  followGlyphs(false);
2843
2885
  container.innerHTML = "";
2844
2886
  if (glyphPositionWas !== null) {
package/dist/index.mjs CHANGED
@@ -97,7 +97,7 @@ import {
97
97
  toRGBA,
98
98
  toSourceRows,
99
99
  valuePlacementFlag
100
- } from "./chunk-FHHT7DBM.mjs";
100
+ } from "./chunk-RDUEXM2D.mjs";
101
101
  import {
102
102
  LASSO_AXES_SLOT,
103
103
  LASSO_COUNT_CLASS,
package/dist/react.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-FHHT7DBM.mjs";
25
+ } from "./chunk-RDUEXM2D.mjs";
26
26
  import "./chunk-QHMLMF4L.mjs";
27
27
  import "./chunk-PDVEIW42.mjs";
28
28
  import "./chunk-VEVBXK44.mjs";
@@ -113,6 +113,11 @@ function BicChart(props) {
113
113
  };
114
114
  const userSelectedRef = useRef(false);
115
115
  const optKey = useMemo(() => JSON.stringify(options ?? {}), [options]);
116
+ const zoomOn = zoomMemory != null;
117
+ const zoomRef = useRef(zoomMemory ?? null);
118
+ useLayoutEffect(() => {
119
+ zoomRef.current = zoomMemory ?? null;
120
+ });
116
121
  const builtWithRef = useRef(null);
117
122
  const onSelectRef = useRef(onSelect);
118
123
  onSelectRef.current = onSelect;
@@ -155,7 +160,7 @@ function BicChart(props) {
155
160
  d3,
156
161
  geoKind,
157
162
  viewState,
158
- zoomMemory,
163
+ zoomMemory: zoomRef.current,
159
164
  labelContrast,
160
165
  layoutPass,
161
166
  restoreScroll,
@@ -191,7 +196,7 @@ function BicChart(props) {
191
196
  host.destroy();
192
197
  hostRef.current = null;
193
198
  };
194
- }, [code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll, controls]);
199
+ }, [code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll, zoomOn, controls]);
195
200
  useLayoutEffect(() => {
196
201
  const host = hostRef.current;
197
202
  if (!host || !selects) return;
@@ -217,7 +222,7 @@ function BicChart(props) {
217
222
  b.detach();
218
223
  if (bindingRef.current === b) bindingRef.current = null;
219
224
  };
220
- }, [selects, code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll]);
225
+ }, [selects, code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll, zoomOn]);
221
226
  useEffect(() => {
222
227
  if (!hostRef.current || !options) return;
223
228
  if (builtWithRef.current?.optKey === optKey) return;
@@ -80,9 +80,10 @@ export interface ChartHostConfig {
80
80
  /**
81
81
  * REMEMBER THE READER'S ZOOM AND PAN on a chart that calls d3.zoom(), across every re-render (and across a
82
82
  * close and reopen, if the host's store says so). Build it with createZoomMemoryHook() from
83
- * "@bicharts/chart-host/zoom-memory", ONCE per container, and hand the same hook to every host created on
84
- * that container. Absent (the default), d3.zoom is never wrapped and nothing is listened for. It applies to a
85
- * chart compiled from `code` only: a precompiled `renderFn` closed over its own d3.
83
+ * "@bicharts/chart-host/zoom-memory", ONCE per chart, and hand the same hook to every host built for that chart
84
+ * (on the same element or, after a remount, a new one): a host that is destroyed releases the hook's watcher, so
85
+ * the hook can go to the next. Absent (the default), d3.zoom is never wrapped and nothing is listened for. It
86
+ * applies to a chart compiled from `code` only: a precompiled `renderFn` closed over its own d3.
86
87
  */
87
88
  zoomMemory?: ZoomMemoryHook | null;
88
89
  /** D3 v7 instance handed to the compiled code. Default: (globalThis as any).d3. */
@@ -35,8 +35,11 @@ export interface BicChartProps {
35
35
  /**
36
36
  * REMEMBER THE READER'S ZOOM AND PAN on a chart that calls d3.zoom(), across re-renders and, if
37
37
  * the hook's store keeps it, across a reload. Build it with createZoomMemoryHook() from
38
- * "@bicharts/chart-host/zoom-memory", once per chart (module scope or useMemo) with somewhere to
39
- * keep the view. Absent, nothing is remembered. See createChartHost's `zoomMemory`.
38
+ * "@bicharts/chart-host/zoom-memory" over one ZoomMemoryHost per chart (module scope, useRef or
39
+ * useMemo) with somewhere to keep the view. Absent (null or undefined), nothing is remembered. Host config:
40
+ * a hook that arrives after mount, or goes away, rebuilds the host; a change from one hook to another does
41
+ * not, and applies at the next rebuild (a new `code`, say), or at once if the component is remounted with a
42
+ * `key`. So a hook built inline in the props is harmless. See createChartHost's `zoomMemory`.
40
43
  */
41
44
  zoomMemory?: ChartHostConfig["zoomMemory"];
42
45
  /**
@@ -1,20 +1,23 @@
1
1
  import { type ZoomState } from "./zoomMemory";
2
2
  /**
3
3
  * WHAT A HOST SUPPLIES: where the remembered view lives, and whether it may outlive the session.
4
- * One per chart container, kept for as long as the container is, so it outlives a host that is destroyed
5
- * and re-created on the same element (a redraw on every resize, say).
4
+ * One per chart, kept for as long as the chart is on the page, so it outlives a host that is destroyed
5
+ * and re-created (on the same element for a redraw on every resize, say, or on a new one after a remount).
6
6
  */
7
7
  export interface ZoomMemoryHost {
8
8
  /** The in-session tier, always on: survives every re-render, never written anywhere. */
9
9
  liveZoomState: ZoomState | null;
10
10
  /** The key the watcher stamps on what it records (the latest render's). */
11
11
  zoomWatchKey: string;
12
- /** Whether the watcher's listeners are on zoomContainer (they attach once). */
12
+ /** Whether the watcher's listeners are on zoomContainer (they attach once per element, and come off when the
13
+ * host that drew into it is destroyed). */
13
14
  zoomWatchAttached: boolean;
14
15
  /** The pending settle timer, 0 when none. */
15
16
  zoomSettleTimer: number;
16
17
  /** The element the chart draws into; it outlives each render, so listeners attach to it once. Null until
17
- * known: createZoomMemoryHook's restore adopts the render's container then. */
18
+ * known: createZoomMemoryHook's restore adopts the render's container then, moves to a new one when a
19
+ * render arrives on a different element (a remount), and gives it back when the host is destroyed. An element
20
+ * set here beforehand is the page's choice: the hook watches it and neither replaces nor gives it back. */
18
21
  zoomContainer: Element | null;
19
22
  /** Is the persisted tier on NOW? Read on every restore and every write, so a change applies to the next. */
20
23
  rememberAcrossSessions(): boolean;
@@ -68,7 +71,13 @@ export interface ZoomMemoryHook {
68
71
  flush(): void;
69
72
  /** forgetZoom: drop the session view and a pending gesture. */
70
73
  forget(): void;
74
+ /** Let go of the watcher on `container`, the element a destroyed host drew into (after flush, which records a
75
+ * gesture still settling), so the next host handed this hook attaches afresh and a hook that is replaced leaves
76
+ * nothing on the element. Without `container`, wherever the watcher is. A watcher that has since moved to another
77
+ * element is left alone. Optional: a hook written by hand without it works as before. */
78
+ release?(container?: Element): void;
71
79
  }
72
- /** Bind the zoom memory to one host object. Build it once per container and hand the same hook to every
73
- * host created on that container, so the session tier outlives each one. */
80
+ /** Bind the zoom memory to one host object. Build it once per chart and hand the same hook to every
81
+ * host created for it, so the session tier outlives each one - on the same element, or on a new one
82
+ * after a remount, where the watcher moves with it. A host that is destroyed releases the watcher. */
74
83
  export declare function createZoomMemoryHook(host: ZoomMemoryHost): ZoomMemoryHook;
@@ -192,15 +192,34 @@ function attachZoomWatcher(host, key) {
192
192
  captureCurrentZoom(host);
193
193
  }, SETTLE_MS);
194
194
  };
195
- for (const evt of ["wheel", "pointerup", "pointercancel", "click", "dblclick", "keyup"]) {
195
+ for (const evt of WATCHED_EVENTS) {
196
196
  try {
197
197
  ctr.addEventListener(evt, settle, true);
198
198
  } catch {
199
199
  }
200
200
  }
201
+ attachedWatchers.set(host, { ctr, settle });
201
202
  host.zoomWatchAttached = true;
202
203
  host.log("zoom-watcher-attached", {});
203
204
  }
205
+ var WATCHED_EVENTS = ["wheel", "pointerup", "pointercancel", "click", "dblclick", "keyup"];
206
+ var attachedWatchers = /* @__PURE__ */ new WeakMap();
207
+ var adoptedContainers = /* @__PURE__ */ new WeakMap();
208
+ function detachZoomWatcher(host) {
209
+ const w = attachedWatchers.get(host);
210
+ if (w) {
211
+ for (const evt of WATCHED_EVENTS) {
212
+ try {
213
+ w.ctr.removeEventListener(evt, w.settle, true);
214
+ } catch {
215
+ }
216
+ }
217
+ attachedWatchers.delete(host);
218
+ }
219
+ if (host.zoomSettleTimer) clearTimeout(host.zoomSettleTimer);
220
+ host.zoomSettleTimer = 0;
221
+ host.zoomWatchAttached = false;
222
+ }
204
223
  function captureCurrentZoom(host) {
205
224
  try {
206
225
  const key = host.zoomWatchKey;
@@ -242,17 +261,32 @@ function forgetZoom(host) {
242
261
  host.liveZoomState = null;
243
262
  }
244
263
  function createZoomMemoryHook(host) {
245
- return {
264
+ const hook = {
246
265
  keyFor: (code, width, height) => zoomStateKey(B(String(code ?? "")), width, height),
247
266
  capture: (d3) => captureZoomBehaviors(d3, host),
248
267
  restore: (container, capture, key, d3) => {
249
- if (!host.zoomContainer) host.zoomContainer = container;
268
+ const mine = adoptedContainers.get(host);
269
+ if (host.zoomContainer && host.zoomContainer === mine && mine !== container) {
270
+ detachZoomWatcher(host);
271
+ host.zoomContainer = null;
272
+ }
273
+ if (!host.zoomContainer) {
274
+ host.zoomContainer = container;
275
+ adoptedContainers.set(host, container);
276
+ }
250
277
  restoreZoomForRender(d3, host, capture, key);
251
278
  attachZoomWatcher(host, key);
252
279
  },
253
280
  flush: () => flushPendingZoom(host),
254
- forget: () => forgetZoom(host)
281
+ forget: () => forgetZoom(host),
282
+ release: (container) => {
283
+ if (container && attachedWatchers.get(host)?.ctr !== container) return;
284
+ detachZoomWatcher(host);
285
+ if (host.zoomContainer && host.zoomContainer === adoptedContainers.get(host)) host.zoomContainer = null;
286
+ adoptedContainers.delete(host);
287
+ }
255
288
  };
289
+ return hook;
256
290
  }
257
291
  export {
258
292
  attachZoomWatcher,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.90",
3
+ "version": "0.6.91",
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",