@bicharts/chart-host 0.6.58 → 0.6.60

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.
@@ -1,6 +1,26 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
 
3
3
  // src/filterScope.ts
4
+ function viewToken(view) {
5
+ const bytes = new TextEncoder().encode(JSON.stringify(view));
6
+ let bin = "";
7
+ bytes.forEach((b) => {
8
+ bin += String.fromCharCode(b);
9
+ });
10
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
+ }
12
+ function viewFromToken(token) {
13
+ if (!token) return null;
14
+ try {
15
+ const b64 = token.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((token.length + 3) % 4);
16
+ const bin = atob(b64);
17
+ const bytes = Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
18
+ const v = JSON.parse(new TextDecoder().decode(bytes));
19
+ return v && v.v === 1 ? v : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
4
24
  function keyString(values) {
5
25
  return JSON.stringify(values.map((v) => v instanceof Date ? `__date:${v.getTime()}` : v === void 0 ? null : v));
6
26
  }
@@ -28,6 +48,8 @@ function createFilter(columns, opts = {}) {
28
48
  let reason = null;
29
49
  let version = 0;
30
50
  const subs = /* @__PURE__ */ new Set();
51
+ let hovered = null;
52
+ const hoverSubs = /* @__PURE__ */ new Set();
31
53
  const kept = /* @__PURE__ */ new WeakMap();
32
54
  const commit = (nextKeys, nextRows, why) => {
33
55
  const nextSet = new Set(nextKeys.map(keyString));
@@ -65,6 +87,23 @@ function createFilter(columns, opts = {}) {
65
87
  let warnedKeep = false;
66
88
  const filter = {
67
89
  id: opts.id ?? `f${++nextId}`,
90
+ key: opts.key ?? cols.join("+"),
91
+ hoverEnabled: !!opts.hover,
92
+ get hovered() {
93
+ return hovered;
94
+ },
95
+ setHover(k) {
96
+ const next = k && k.length ? k.slice(0, cols.length) : null;
97
+ if ((next ? keyString(next) : null) === (hovered ? keyString(hovered) : null)) return;
98
+ hovered = next;
99
+ for (const cb of Array.from(hoverSubs)) cb(hovered);
100
+ },
101
+ onHover(cb) {
102
+ hoverSubs.add(cb);
103
+ return () => {
104
+ hoverSubs.delete(cb);
105
+ };
106
+ },
68
107
  columns: cols,
69
108
  label: opts.label ?? cols.join(" / "),
70
109
  get active() {
@@ -148,6 +187,8 @@ function createFilter(columns, opts = {}) {
148
187
  function createFilterScope() {
149
188
  const list = [];
150
189
  const offs = /* @__PURE__ */ new Map();
190
+ const ctl = /* @__PURE__ */ new Map();
191
+ const ctlOffs = /* @__PURE__ */ new Map();
151
192
  const subs = /* @__PURE__ */ new Set();
152
193
  const notify = () => {
153
194
  for (const cb of Array.from(subs)) cb(scope);
@@ -188,10 +229,60 @@ function createFilterScope() {
188
229
  return () => {
189
230
  subs.delete(cb);
190
231
  };
232
+ },
233
+ get controls() {
234
+ return ctl;
235
+ },
236
+ addControls(key, c) {
237
+ let k = key, n = 1;
238
+ while (ctl.has(k) && ctl.get(k) !== c) k = `${key}-${++n}`;
239
+ if (!ctl.has(k)) {
240
+ ctl.set(k, c);
241
+ ctlOffs.set(k, c.onChange(() => notify()));
242
+ notify();
243
+ }
244
+ return () => {
245
+ if (ctl.get(k) !== c) return;
246
+ ctlOffs.get(k)?.();
247
+ ctlOffs.delete(k);
248
+ ctl.delete(k);
249
+ notify();
250
+ };
251
+ },
252
+ save() {
253
+ const view = { v: 1 };
254
+ for (const f of list) if (f.active) (view.filters ??= {})[f.key] = f.keys.map((k) => k.slice());
255
+ for (const [k, c] of ctl) {
256
+ const values = c.values;
257
+ if (Object.keys(values).length) (view.controls ??= {})[k] = { ...values };
258
+ }
259
+ return view;
260
+ },
261
+ restore(view) {
262
+ if (!view || view.v !== 1) return;
263
+ for (const f of list) {
264
+ const keys = view.filters?.[f.key];
265
+ if (keys && keys.length) f.set(keys, "page");
266
+ else f.clear("page");
267
+ }
268
+ for (const [k, c] of ctl) {
269
+ const values = view.controls?.[k];
270
+ if (values) c.set(values);
271
+ }
191
272
  }
192
273
  };
193
274
  return scope;
194
275
  }
276
+ var LINKED_HOVER_CLASS = "bic-linked-hover";
277
+ var hoverStyled = false;
278
+ function ensureHoverStyle(doc) {
279
+ if (hoverStyled || !doc?.head) return;
280
+ hoverStyled = true;
281
+ const s = doc.createElement("style");
282
+ s.setAttribute("data-bic", "linked-hover");
283
+ s.textContent = `.${LINKED_HOVER_CLASS}{filter:drop-shadow(0 0 2px var(--bic-linked-hover,rgba(0,0,0,.65))) drop-shadow(0 0 1px var(--bic-linked-hover,rgba(0,0,0,.65)))}`;
284
+ doc.head.appendChild(s);
285
+ }
195
286
  function payloadRowReader(getPayload) {
196
287
  return {
197
288
  rowAt(i) {
@@ -258,6 +349,41 @@ function bindFilter(host, filter, opts) {
258
349
  if (o && o.keys !== ownerKeys()) owners.delete(filter);
259
350
  paint();
260
351
  });
352
+ const el = opts.container ?? null;
353
+ let offHoverDom = () => {
354
+ };
355
+ let offHover = () => {
356
+ };
357
+ if (filter.hoverEnabled && el) {
358
+ ensureHoverStyle(el.ownerDocument);
359
+ const markRow = (t) => {
360
+ const m = t?.closest?.(".d3-mark[data-row-idx]");
361
+ const v = m ? parseInt((m.getAttribute("data-row-idx") ?? "").split(",")[0], 10) : NaN;
362
+ return Number.isFinite(v) ? v : null;
363
+ };
364
+ const over = (e) => {
365
+ const i = markRow(e.target);
366
+ const r = i === null ? null : opts.rowAt(i);
367
+ if (r) filter.setHover(keyOfRow(r, filter.columns));
368
+ };
369
+ const leave = () => filter.setHover(null);
370
+ el.addEventListener("mouseover", over);
371
+ el.addEventListener("mouseleave", leave);
372
+ offHoverDom = () => {
373
+ el.removeEventListener("mouseover", over);
374
+ el.removeEventListener("mouseleave", leave);
375
+ };
376
+ const paintHover = (k) => {
377
+ const want = k ? keyString(k) : null;
378
+ el.querySelectorAll(".d3-mark[data-row-idx]").forEach((m) => {
379
+ const idx = parseInt((m.getAttribute("data-row-idx") ?? "").split(",")[0], 10);
380
+ const r = Number.isFinite(idx) ? opts.rowAt(idx) : null;
381
+ const on = !!want && !!r && keyString(keyOfRow(r, filter.columns)) === want;
382
+ m.classList.toggle(LINKED_HOVER_CLASS, on);
383
+ });
384
+ };
385
+ offHover = filter.onHover(paintHover);
386
+ }
261
387
  return {
262
388
  reconcile() {
263
389
  if (filter.active && opts.rowCount() > 0 && isMine()) {
@@ -279,6 +405,8 @@ function bindFilter(host, filter, opts) {
279
405
  detach() {
280
406
  offHost();
281
407
  offFilter();
408
+ offHoverDom();
409
+ offHover();
282
410
  const o = owners.get(filter);
283
411
  if (o && o.token === token) owners.set(filter, { ...o, orphan: true });
284
412
  }
@@ -453,8 +581,11 @@ function notesFor(notes, key, opts) {
453
581
  }
454
582
 
455
583
  export {
584
+ viewToken,
585
+ viewFromToken,
456
586
  createFilter,
457
587
  createFilterScope,
588
+ LINKED_HOVER_CLASS,
458
589
  payloadRowReader,
459
590
  bindFilter,
460
591
  fromVegaInteraction,
package/dist/page.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
+ LINKED_HOVER_CLASS,
3
4
  attachControls,
4
5
  badgeKey,
5
6
  bindFilter,
@@ -10,10 +11,13 @@ import {
10
11
  noteBadges,
11
12
  notesFor,
12
13
  payloadRowReader,
13
- readControls
14
- } from "./chunk-ZEZXNAFL.mjs";
14
+ readControls,
15
+ viewFromToken,
16
+ viewToken
17
+ } from "./chunk-XIAL6EEC.mjs";
15
18
  import "./chunk-GHODWOAL.mjs";
16
19
  export {
20
+ LINKED_HOVER_CLASS,
17
21
  attachControls,
18
22
  badgeKey,
19
23
  bindFilter,
@@ -24,5 +28,7 @@ export {
24
28
  noteBadges,
25
29
  notesFor,
26
30
  payloadRowReader,
27
- readControls
31
+ readControls,
32
+ viewFromToken,
33
+ viewToken
28
34
  };
package/dist/react.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
+ LINKED_HOVER_CLASS,
3
4
  attachControls,
4
5
  badgeKey,
5
6
  bindFilter,
@@ -9,8 +10,10 @@ import {
9
10
  fromVegaInteraction,
10
11
  noteBadges,
11
12
  notesFor,
12
- payloadRowReader
13
- } from "./chunk-ZEZXNAFL.mjs";
13
+ payloadRowReader,
14
+ viewFromToken,
15
+ viewToken
16
+ } from "./chunk-XIAL6EEC.mjs";
14
17
  import {
15
18
  assembleD3,
16
19
  createChartGroup,
@@ -179,6 +182,7 @@ function BicChart(props) {
179
182
  if (!host || !selects) return;
180
183
  const payloadRows = payloadRowReader(() => dataRef.current);
181
184
  const b = bindFilter(host, selects, {
185
+ container: ref.current,
182
186
  rowAt(i) {
183
187
  const g = groupRef.current, map = rowMapRef.current;
184
188
  if (g && map) {
@@ -261,13 +265,94 @@ function useBicFilter(columns, opts) {
261
265
  useSyncExternalStore((cb) => f.onChange(() => cb()), () => f.version, () => f.version);
262
266
  return f;
263
267
  }
264
- function useBicControls(initial) {
268
+ function useBicControls(initial, opts) {
269
+ const scope = useContext(PageCtx);
265
270
  const ref = useRef(null);
266
271
  if (!ref.current) ref.current = createControls(initial);
267
272
  const c = ref.current;
273
+ useEffect(() => scope.addControls(opts?.id ?? "controls", c), [scope, c, opts?.id]);
268
274
  useSyncExternalStore((cb) => c.onChange(() => cb()), () => c.version, () => c.version);
269
275
  return c;
270
276
  }
277
+ function useBicView() {
278
+ const scope = useContext(PageCtx);
279
+ return useMemo(() => ({
280
+ save: () => scope.save(),
281
+ restore: (v) => scope.restore(v),
282
+ token: () => viewToken(scope.save()),
283
+ restoreToken: (t) => scope.restore(viewFromToken(t))
284
+ }), [scope]);
285
+ }
286
+ function useBicUrlState(param = "view") {
287
+ const scope = useContext(PageCtx);
288
+ useEffect(() => {
289
+ if (typeof window === "undefined") return;
290
+ const url = new URL(window.location.href);
291
+ scope.restore(viewFromToken(url.searchParams.get(param)));
292
+ return scope.onChange(() => {
293
+ const u = new URL(window.location.href);
294
+ const view = scope.save();
295
+ const empty = !view.filters && !view.controls;
296
+ if (empty) u.searchParams.delete(param);
297
+ else u.searchParams.set(param, viewToken(view));
298
+ if (u.href !== window.location.href) window.history.replaceState(window.history.state, "", u.href);
299
+ });
300
+ }, [scope, param]);
301
+ }
302
+ function BicDevtools({ max = 20 }) {
303
+ const scope = useContext(PageCtx);
304
+ const [, tick] = useState(0);
305
+ const log = useRef([]);
306
+ useEffect(() => scope.onChange(() => {
307
+ const at = (/* @__PURE__ */ new Date()).toLocaleTimeString();
308
+ const changed = scope.filters.map((f) => `${f.label}: ${f.active ? f.text : "(none)"}${f.reason ? ` [${f.reason}]` : ""}`);
309
+ log.current = [`${at} ${changed.join(" | ")}`, ...log.current].slice(0, max);
310
+ tick((t) => t + 1);
311
+ }), [scope, max]);
312
+ const box = {
313
+ position: "fixed",
314
+ right: 8,
315
+ bottom: 8,
316
+ zIndex: 2147483e3,
317
+ maxWidth: 420,
318
+ maxHeight: "50vh",
319
+ overflow: "auto",
320
+ font: "12px/1.4 ui-monospace, monospace",
321
+ background: "Canvas",
322
+ color: "CanvasText",
323
+ border: "1px solid GrayText",
324
+ borderRadius: 6,
325
+ padding: 6,
326
+ opacity: 0.95
327
+ };
328
+ return /* @__PURE__ */ jsxs("details", { className: "bic-devtools", style: box, children: [
329
+ /* @__PURE__ */ jsxs("summary", { style: { cursor: "pointer" }, children: [
330
+ "chart-host: ",
331
+ scope.active.length,
332
+ " filter(s) active"
333
+ ] }),
334
+ scope.filters.map((f) => /* @__PURE__ */ jsxs("div", { children: [
335
+ /* @__PURE__ */ jsx("b", { children: f.label }),
336
+ " [",
337
+ f.key,
338
+ "] ",
339
+ f.active ? JSON.stringify(f.keys) : "(none)",
340
+ " ",
341
+ f.text && `"${f.text}"`,
342
+ f.reason ? ` - last: ${f.reason}` : "",
343
+ " v",
344
+ f.version
345
+ ] }, f.id)),
346
+ [...scope.controls].map(([k, c]) => /* @__PURE__ */ jsxs("div", { children: [
347
+ /* @__PURE__ */ jsx("b", { children: "controls" }),
348
+ " [",
349
+ k,
350
+ "] ",
351
+ c.summary || JSON.stringify(c.values)
352
+ ] }, k)),
353
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 4, opacity: 0.8 }, children: log.current.map((l, i) => /* @__PURE__ */ jsx("div", { children: l }, i)) })
354
+ ] });
355
+ }
271
356
  function useBicFilters() {
272
357
  const scope = useContext(PageCtx);
273
358
  const [, tick] = useState(0);
@@ -372,8 +457,10 @@ function useMeasuredSize(ref) {
372
457
  export {
373
458
  BicChart,
374
459
  BicChartGroup,
460
+ BicDevtools,
375
461
  BicFilterChips,
376
462
  BicPage,
463
+ LINKED_HOVER_CLASS,
377
464
  assembleD3,
378
465
  badgeKey,
379
466
  createControls,
@@ -386,5 +473,9 @@ export {
386
473
  useBicFilter,
387
474
  useBicFilters,
388
475
  useBicSelection,
389
- useMeasuredSize
476
+ useBicUrlState,
477
+ useBicView,
478
+ useMeasuredSize,
479
+ viewFromToken,
480
+ viewToken
390
481
  };
@@ -1,4 +1,5 @@
1
1
  import type { ChartHost } from "./host";
2
+ import type { Controls } from "./controls";
2
3
  /** Why a filter changed. */
3
4
  export type FilterChangeReason = "click" | "clear" | "page" | "data";
4
5
  /** One selected key: a value per key column, in the filter's column order. */
@@ -14,9 +15,26 @@ export interface FilterOptions {
14
15
  display?: string | readonly string[];
15
16
  /** A stable id for this filter within its scope. Generated when absent. */
16
17
  id?: string;
18
+ /** Its name in a saved view (scope.save / a link). Defaults to the key columns joined with "+". */
19
+ key?: string;
20
+ /**
21
+ * LINKED HOVER: hovering a mark on a chart bound to this filter lights the marks with the same key on every other
22
+ * chart bound to it (a glow, never a filter). Off by default.
23
+ */
24
+ hover?: boolean;
17
25
  }
18
26
  export interface Filter {
19
27
  readonly id: string;
28
+ /** Its name in a saved view. */
29
+ readonly key: string;
30
+ /** Whether linked hover is on (FilterOptions.hover). */
31
+ readonly hoverEnabled: boolean;
32
+ /** The key being hovered on a bound chart now, or null. */
33
+ readonly hovered: FilterKey | null;
34
+ /** Set or clear the hovered key (bound charts do this; a page may too). */
35
+ setHover(key: FilterKey | null): void;
36
+ /** Subscribe to hover changes. Returns the unsubscribe. */
37
+ onHover(cb: (key: FilterKey | null) => void): () => void;
20
38
  /** The key column(s). */
21
39
  readonly columns: readonly string[];
22
40
  readonly label: string;
@@ -74,9 +92,27 @@ export interface FilterScope {
74
92
  add(filter: Filter): () => void;
75
93
  /** Clear every filter. */
76
94
  clearAll(reason?: FilterChangeReason): void;
77
- /** Subscribe to any change of any filter, and to filters being added or removed. */
95
+ /** Subscribe to any change of any filter or controls, and to either being added or removed. */
78
96
  onChange(cb: (scope: FilterScope) => void): () => void;
97
+ /** A chart's controls, to be saved and restored with the page's view. Returns the remove. */
98
+ addControls(key: string, controls: Controls): () => void;
99
+ /** The registered controls, by key. */
100
+ readonly controls: ReadonlyMap<string, Controls>;
101
+ /** The page's view now: every filter's keys and every registered controls' values - JSON-safe. */
102
+ save(): PageView;
103
+ /** Put a saved view back: each filter set (or cleared when the view doesn't name it), each controls set. */
104
+ restore(view: PageView | null | undefined): void;
105
+ }
106
+ /** A saved page view: what save() returns and restore() takes. */
107
+ export interface PageView {
108
+ v: 1;
109
+ filters?: Record<string, unknown[][]>;
110
+ controls?: Record<string, Record<string, unknown>>;
79
111
  }
112
+ /** A view as a URL-safe token (base64url of its JSON), for a link or a bookmark. */
113
+ export declare function viewToken(view: PageView): string;
114
+ /** A token from viewToken back to a view; null when it isn't one. */
115
+ export declare function viewFromToken(token: string | null | undefined): PageView | null;
80
116
  /** Make a filter keyed by `columns` (one name, or several for a compound key such as a route). */
81
117
  export declare function createFilter(columns: string | readonly string[], opts?: FilterOptions): Filter;
82
118
  /** Make a page's filter scope. */
@@ -98,7 +134,11 @@ export interface BindFilterOptions {
98
134
  rowAt(i: number): Record<string, unknown> | null;
99
135
  /** How many rows the chart is drawing now. */
100
136
  rowCount(): number;
137
+ /** The chart's container, for linked hover (FilterOptions.hover). Without it the chart takes no part in hover. */
138
+ container?: HTMLElement | null;
101
139
  }
140
+ /** The class a mark wears while its key is hovered on another chart bound to the same filter. */
141
+ export declare const LINKED_HOVER_CLASS = "bic-linked-hover";
102
142
  /** The source row behind each payload row, from a payload of { columns, rows: unknown[][] }. */
103
143
  export declare function payloadRowReader(getPayload: () => {
104
144
  columns: readonly {
@@ -12,7 +12,7 @@ export { ANNOTATION_CLASS, ANNOTATION_LAYER_CLASS, type MarkAnnotation, type Ann
12
12
  export type { MarkNoteLike, NoteBadgeOptions } from "./markNotes";
13
13
  export type { Controls, ControlInfo, ControlsAttachment } from "./controls";
14
14
  export { createChartGroup, toSourceRows, syncMemberSelection, type ChartGroup, type ChartGroupMember, type ChartGroupSelection, type ChartGroupChange, type ChartGroupMemberOptions, type ChartGroupSourceOptions, type ChartGroupPayload } from "./group";
15
- export type { Filter, FilterScope, FilterOptions, FilterKey, FilterChangeReason, FilterBinding, BindFilterOptions, VegaInteractionEvent } from "./filterScope";
15
+ export type { Filter, FilterScope, FilterOptions, FilterKey, FilterChangeReason, FilterBinding, BindFilterOptions, VegaInteractionEvent, PageView } from "./filterScope";
16
16
  export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
17
17
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
18
18
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, tableHostOf, chartSvgOf, htmlToSvgDataUrl, htmlNaturalSize, type SnapshotOptions } from "./snapshot";
@@ -1,3 +1,4 @@
1
+ export { viewToken, viewFromToken, LINKED_HOVER_CLASS, type PageView } from "./filterScope";
1
2
  export { createFilter, createFilterScope, bindFilter, payloadRowReader, fromVegaInteraction, type Filter, type FilterScope, type FilterOptions, type FilterKey, type FilterChangeReason, type FilterBinding, type BindFilterOptions, type VegaInteractionEvent } from "./filterScope";
2
3
  export { createControls, attachControls, readControls, type Controls, type ControlInfo, type ControlsAttachment } from "./controls";
3
4
  export { noteBadges, badgeKey, notesFor, type MarkNoteLike, type NoteBadgeOptions } from "./markNotes";
@@ -2,11 +2,12 @@ import type { ReactNode, RefObject } from "react";
2
2
  import { type ChartHostConfig } from "./host";
3
3
  import type { ResolveOptionsInput } from "./defaults";
4
4
  import type { GeoPointBinding } from "./payload";
5
- import { type Filter, type FilterOptions } from "./filterScope";
5
+ import { type PageView, type Filter, type FilterOptions } from "./filterScope";
6
6
  export { assembleD3 } from "./host";
7
7
  import { type Controls } from "./controls";
8
8
  export { createControls, type Controls, type ControlInfo } from "./controls";
9
9
  export { noteBadges, badgeKey, notesFor, type MarkNoteLike, type NoteBadgeOptions } from "./markNotes";
10
+ export { viewToken, viewFromToken, LINKED_HOVER_CLASS, type PageView } from "./filterScope";
10
11
  export { createFilter, createFilterScope, fromVegaInteraction, type Filter, type FilterOptions, type FilterScope, type FilterChangeReason } from "./filterScope";
11
12
  export interface BicChartProps {
12
13
  /** Generated render() source (an ES module's `code` export, or the raw string). */
@@ -150,7 +151,33 @@ export declare function useBicFilter(columns: string | readonly string[], opts?:
150
151
  * and `summary`, `set(saved)` to apply a saved scenario, `reset()` for the defaults. Re-renders the
151
152
  * component on every change. `initial` seeds the values the chart first draws with.
152
153
  */
153
- export declare function useBicControls(initial?: Readonly<Record<string, unknown>>): Controls;
154
+ export declare function useBicControls(initial?: Readonly<Record<string, unknown>>, opts?: {
155
+ id?: string;
156
+ }): Controls;
157
+ /**
158
+ * THE PAGE'S VIEW, SAVED AND PUT BACK: every page filter's keys and every chart's controls (useBicControls). `save()`
159
+ * returns a JSON-safe view to store (a saved view, a user preference); `restore(view)` puts one back; `token()` /
160
+ * `restoreToken(t)` do the same as a URL-safe string for a link.
161
+ */
162
+ export declare function useBicView(): {
163
+ save: () => PageView;
164
+ restore: (v: PageView | null | undefined) => void;
165
+ token: () => string;
166
+ restoreToken: (t: string | null | undefined) => void;
167
+ };
168
+ /**
169
+ * THE VIEW IN THE ADDRESS BAR (opt-in): restores the page's filters and controls from `?<param>=` on mount, then keeps
170
+ * the parameter current as they change (history.replaceState - no navigation, no new history entries). A copied link
171
+ * opens the page as it was. Call once, in the page component, after its useBicFilter / useBicControls calls.
172
+ */
173
+ export declare function useBicUrlState(param?: string): void;
174
+ /**
175
+ * A DEVELOPER'S VIEW OF THE PAGE'S STATE: every filter (its keys, its text, the last change and why), every chart's
176
+ * controls, and the last changes in order. Collapsed in a corner; render it only in development.
177
+ */
178
+ export declare function BicDevtools({ max }: {
179
+ max?: number;
180
+ }): import("react").JSX.Element;
154
181
  /** Every filter on the page (or the enclosing <BicPage>), and its clear-all - for a page's own chip bar or count. */
155
182
  export declare function useBicFilters(): {
156
183
  filters: readonly Filter[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.58",
3
+ "version": "0.6.60",
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",