@bicharts/chart-host 0.6.56 → 0.6.58

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/page.mjs ADDED
@@ -0,0 +1,28 @@
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
+ import {
3
+ attachControls,
4
+ badgeKey,
5
+ bindFilter,
6
+ createControls,
7
+ createFilter,
8
+ createFilterScope,
9
+ fromVegaInteraction,
10
+ noteBadges,
11
+ notesFor,
12
+ payloadRowReader,
13
+ readControls
14
+ } from "./chunk-ZEZXNAFL.mjs";
15
+ import "./chunk-GHODWOAL.mjs";
16
+ export {
17
+ attachControls,
18
+ badgeKey,
19
+ bindFilter,
20
+ createControls,
21
+ createFilter,
22
+ createFilterScope,
23
+ fromVegaInteraction,
24
+ noteBadges,
25
+ notesFor,
26
+ payloadRowReader,
27
+ readControls
28
+ };
package/dist/react.mjs CHANGED
@@ -1,4 +1,16 @@
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
+ import {
3
+ attachControls,
4
+ badgeKey,
5
+ bindFilter,
6
+ createControls,
7
+ createFilter,
8
+ createFilterScope,
9
+ fromVegaInteraction,
10
+ noteBadges,
11
+ notesFor,
12
+ payloadRowReader
13
+ } from "./chunk-ZEZXNAFL.mjs";
2
14
  import {
3
15
  assembleD3,
4
16
  createChartGroup,
@@ -7,13 +19,14 @@ import {
7
19
  loadGeo,
8
20
  syncMemberSelection,
9
21
  toSourceRows
10
- } from "./chunk-CMHGGW3Y.mjs";
22
+ } from "./chunk-5NSU3MYW.mjs";
11
23
  import "./chunk-A2GMXZP7.mjs";
24
+ import "./chunk-WNGJUPEI.mjs";
12
25
  import "./chunk-GHODWOAL.mjs";
13
26
 
14
27
  // src/react.tsx
15
- import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
16
- import { jsx } from "react/jsx-runtime";
28
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
29
+ import { jsx, jsxs } from "react/jsx-runtime";
17
30
  var Ctx = createContext(null);
18
31
  function useBicSelection() {
19
32
  const g = useContext(Ctx);
@@ -56,7 +69,10 @@ function BicChart(props) {
56
69
  annotations,
57
70
  onAnnotationClick,
58
71
  className,
59
- style
72
+ style,
73
+ selects,
74
+ controls,
75
+ ariaLabel
60
76
  } = props;
61
77
  const ref = useRef(null);
62
78
  const hostRef = useRef(null);
@@ -72,6 +88,18 @@ function BicChart(props) {
72
88
  }, [group, ctx?.source, filterSel && filterSel.join(",")]);
73
89
  const data = built ? built.payload : props.data;
74
90
  rowMapRef.current = built ? built.rowMap : null;
91
+ const dataRef = useRef(data);
92
+ dataRef.current = data;
93
+ const groupRef = useRef(group);
94
+ groupRef.current = group;
95
+ const bindingRef = useRef(null);
96
+ const controlsRef = useRef(null);
97
+ const readControlsSoon = () => {
98
+ const h = hostRef.current, c = controlsRef.current;
99
+ if (h && c) h.rendered.then(c.read, () => {
100
+ });
101
+ };
102
+ const userSelectedRef = useRef(false);
75
103
  const optKey = useMemo(() => JSON.stringify(options ?? {}), [options]);
76
104
  const builtWithRef = useRef(null);
77
105
  const onSelectRef = useRef(onSelect);
@@ -102,11 +130,14 @@ function BicChart(props) {
102
130
  useLayoutEffect(() => {
103
131
  const el = ref.current;
104
132
  if (!el || !code && !renderFn || !data) return;
133
+ const ctl = controls ? attachControls(() => hostRef.current, el, controls) : null;
134
+ controlsRef.current = ctl;
135
+ const hostOptions = ctl ? { ...options ?? {}, ...ctl.options } : options;
105
136
  const host = createChartHost(el, {
106
137
  code,
107
138
  renderFn,
108
139
  data,
109
- options,
140
+ options: hostOptions,
110
141
  d3,
111
142
  geoKind,
112
143
  viewState,
@@ -127,23 +158,53 @@ function BicChart(props) {
127
158
  builtAnnKeyRef.current = annKey;
128
159
  const off = host.selection.onChange((payloadIdxs, source) => {
129
160
  if (source === "host") return;
161
+ userSelectedRef.current = payloadIdxs.length > 0;
130
162
  const sourceIdxs = toSourceRows(rowMapRef.current, payloadIdxs);
131
163
  if (group && id) group.publish(id, sourceIdxs);
132
164
  onSelectRef.current?.(sourceIdxs);
133
165
  });
134
166
  host.render();
167
+ readControlsSoon();
135
168
  builtWithRef.current = { data, optKey };
136
169
  return () => {
137
170
  off();
171
+ ctl?.detach();
172
+ if (controlsRef.current === ctl) controlsRef.current = null;
138
173
  host.destroy();
139
174
  hostRef.current = null;
140
175
  };
141
- }, [code, renderFn, d3, geoKind, labelContrast]);
176
+ }, [code, renderFn, d3, geoKind, labelContrast, controls]);
177
+ useLayoutEffect(() => {
178
+ const host = hostRef.current;
179
+ if (!host || !selects) return;
180
+ const payloadRows = payloadRowReader(() => dataRef.current);
181
+ const b = bindFilter(host, selects, {
182
+ rowAt(i) {
183
+ const g = groupRef.current, map = rowMapRef.current;
184
+ if (g && map) {
185
+ const s = map[i];
186
+ return s === void 0 ? null : g.rows[s] ?? null;
187
+ }
188
+ return payloadRows.rowAt(i);
189
+ },
190
+ rowCount() {
191
+ const map = rowMapRef.current;
192
+ return groupRef.current && map ? map.length : payloadRows.rowCount();
193
+ }
194
+ });
195
+ bindingRef.current = b;
196
+ b.reconcile();
197
+ return () => {
198
+ b.detach();
199
+ if (bindingRef.current === b) bindingRef.current = null;
200
+ };
201
+ }, [selects, code, renderFn, d3, geoKind, labelContrast]);
142
202
  useEffect(() => {
143
203
  if (!hostRef.current || !options) return;
144
204
  if (builtWithRef.current?.optKey === optKey) return;
145
205
  builtWithRef.current = { ...builtWithRef.current, optKey };
146
206
  hostRef.current.setOptions(options);
207
+ readControlsSoon();
147
208
  }, [optKey]);
148
209
  const builtAnnKeyRef = useRef(null);
149
210
  useEffect(() => {
@@ -156,13 +217,139 @@ function BicChart(props) {
156
217
  if (builtWithRef.current?.data === data) return;
157
218
  builtWithRef.current = { ...builtWithRef.current, data };
158
219
  hostRef.current.setData(data);
220
+ bindingRef.current?.reconcile();
221
+ readControlsSoon();
159
222
  }, [data]);
223
+ const lastSourceRef = useRef(ctx?.source);
160
224
  useEffect(() => {
161
225
  const host = hostRef.current;
162
226
  if (!host || !ctx) return;
227
+ const sourceChanged = lastSourceRef.current !== ctx.source;
228
+ lastSourceRef.current = ctx.source;
229
+ if (selects) return;
230
+ const before = (host.selection.current ?? []).length;
163
231
  syncMemberSelection(host, ctx.selection, id, highlightMode, incoming, rowMapRef.current);
164
- }, [ctx?.selection, id, ctx, highlightMode, incoming && incoming.join(",")]);
165
- return /* @__PURE__ */ jsx("div", { ref, className, style });
232
+ if (sourceChanged && before && !(host.selection.current ?? []).length && userSelectedRef.current) {
233
+ userSelectedRef.current = false;
234
+ onSelectRef.current?.([]);
235
+ }
236
+ }, [ctx?.selection, id, ctx, highlightMode, incoming && incoming.join(","), selects]);
237
+ return /* @__PURE__ */ jsx(
238
+ "div",
239
+ {
240
+ ref,
241
+ className,
242
+ style,
243
+ role: ariaLabel ? "figure" : void 0,
244
+ "aria-label": ariaLabel || void 0
245
+ }
246
+ );
247
+ }
248
+ var defaultScope = createFilterScope();
249
+ var PageCtx = createContext(defaultScope);
250
+ function BicPage({ children }) {
251
+ const ref = useRef(null);
252
+ if (!ref.current) ref.current = createFilterScope();
253
+ return /* @__PURE__ */ jsx(PageCtx.Provider, { value: ref.current, children });
254
+ }
255
+ function useBicFilter(columns, opts) {
256
+ const scope = useContext(PageCtx);
257
+ const ref = useRef(null);
258
+ if (!ref.current) ref.current = createFilter(columns, opts);
259
+ const f = ref.current;
260
+ useEffect(() => scope.add(f), [scope, f]);
261
+ useSyncExternalStore((cb) => f.onChange(() => cb()), () => f.version, () => f.version);
262
+ return f;
263
+ }
264
+ function useBicControls(initial) {
265
+ const ref = useRef(null);
266
+ if (!ref.current) ref.current = createControls(initial);
267
+ const c = ref.current;
268
+ useSyncExternalStore((cb) => c.onChange(() => cb()), () => c.version, () => c.version);
269
+ return c;
270
+ }
271
+ function useBicFilters() {
272
+ const scope = useContext(PageCtx);
273
+ const [, tick] = useState(0);
274
+ useEffect(() => scope.onChange(() => tick((t) => t + 1)), [scope]);
275
+ return { filters: scope.filters, active: scope.active, clearAll: () => scope.clearAll() };
276
+ }
277
+ function BicFilterChips({ className, chipClassName, clearAllText = "Clear all", style }) {
278
+ const { active, clearAll } = useBicFilters();
279
+ if (!active.length) return null;
280
+ const chip = {
281
+ display: "inline-flex",
282
+ alignItems: "center",
283
+ gap: 6,
284
+ padding: "2px 4px 2px 10px",
285
+ border: "1px solid currentColor",
286
+ borderRadius: 999,
287
+ font: "inherit",
288
+ lineHeight: 1.6
289
+ };
290
+ const x = {
291
+ border: 0,
292
+ background: "transparent",
293
+ color: "inherit",
294
+ cursor: "pointer",
295
+ font: "inherit",
296
+ padding: "0 6px",
297
+ lineHeight: 1
298
+ };
299
+ return /* @__PURE__ */ jsxs(
300
+ "div",
301
+ {
302
+ className: ["bic-filter-chips", className].filter(Boolean).join(" "),
303
+ role: "group",
304
+ "aria-label": "Filters",
305
+ style: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center", ...style },
306
+ children: [
307
+ active.map((f) => /* @__PURE__ */ jsxs(
308
+ "span",
309
+ {
310
+ className: ["bic-filter-chip", chipClassName].filter(Boolean).join(" "),
311
+ style: chipClassName ? void 0 : chip,
312
+ children: [
313
+ /* @__PURE__ */ jsxs("span", { className: "bic-filter-chip-label", children: [
314
+ f.label,
315
+ ": "
316
+ ] }),
317
+ /* @__PURE__ */ jsx("span", { className: "bic-filter-chip-value", children: f.text }),
318
+ /* @__PURE__ */ jsx(
319
+ "button",
320
+ {
321
+ type: "button",
322
+ className: "bic-filter-chip-clear",
323
+ "aria-label": `Clear ${f.label}`,
324
+ style: chipClassName ? void 0 : x,
325
+ onClick: () => f.clear(),
326
+ children: "\xD7"
327
+ }
328
+ )
329
+ ]
330
+ },
331
+ f.id
332
+ )),
333
+ active.length > 1 && /* @__PURE__ */ jsx(
334
+ "button",
335
+ {
336
+ type: "button",
337
+ className: "bic-filter-chips-clear-all",
338
+ onClick: clearAll,
339
+ style: {
340
+ font: "inherit",
341
+ background: "transparent",
342
+ border: 0,
343
+ color: "inherit",
344
+ textDecoration: "underline",
345
+ cursor: "pointer"
346
+ },
347
+ children: clearAllText
348
+ }
349
+ )
350
+ ]
351
+ }
352
+ );
166
353
  }
167
354
  function useMeasuredSize(ref) {
168
355
  const [size, setSize] = useState({ width: 0, height: 0 });
@@ -185,7 +372,19 @@ function useMeasuredSize(ref) {
185
372
  export {
186
373
  BicChart,
187
374
  BicChartGroup,
375
+ BicFilterChips,
376
+ BicPage,
188
377
  assembleD3,
378
+ badgeKey,
379
+ createControls,
380
+ createFilter,
381
+ createFilterScope,
382
+ fromVegaInteraction,
383
+ noteBadges,
384
+ notesFor,
385
+ useBicControls,
386
+ useBicFilter,
387
+ useBicFilters,
189
388
  useBicSelection,
190
389
  useMeasuredSize
191
390
  };
package/dist/testing.mjs CHANGED
@@ -1,6 +1,45 @@
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
+ import {
3
+ HOST_CONTAINER_CLASS,
4
+ MARK_CLASS,
5
+ MARK_SELECTED_CLASS,
6
+ ROW_IDX_ATTR
7
+ } from "./chunk-WNGJUPEI.mjs";
2
8
  import "./chunk-GHODWOAL.mjs";
3
9
 
10
+ // src/testing/interact.ts
11
+ function chartContainers(root) {
12
+ return Array.from(root.querySelectorAll(`.${HOST_CONTAINER_CLASS}`));
13
+ }
14
+ function marksOf(chart) {
15
+ return Array.from(chart.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}]`)).filter((m) => (m.getAttribute(ROW_IDX_ATTR) ?? "") !== "");
16
+ }
17
+ function findMark(chart, which) {
18
+ const marks = marksOf(chart);
19
+ if (typeof which === "function") return marks.find(which) ?? null;
20
+ return marks.find((m) => (m.getAttribute(ROW_IDX_ATTR) ?? "").split(",").map(Number).includes(which)) ?? null;
21
+ }
22
+ function clickMark(mark, opts = {}) {
23
+ const win = mark.ownerDocument?.defaultView;
24
+ const Ev = win?.MouseEvent ?? MouseEvent;
25
+ mark.dispatchEvent(new Ev("click", { bubbles: true, cancelable: true, ctrlKey: !!opts.add }));
26
+ }
27
+ function clickEmpty(chart) {
28
+ const win = chart.ownerDocument?.defaultView;
29
+ const Ev = win?.MouseEvent ?? MouseEvent;
30
+ chart.dispatchEvent(new Ev("click", { bubbles: true, cancelable: true }));
31
+ }
32
+ function selectedRows(chart) {
33
+ const out = /* @__PURE__ */ new Set();
34
+ for (const m of Array.from(chart.querySelectorAll(`.${MARK_CLASS}.${MARK_SELECTED_CLASS}`))) {
35
+ for (const p of (m.getAttribute(ROW_IDX_ATTR) ?? "").split(",")) {
36
+ const n = parseInt(p, 10);
37
+ if (Number.isFinite(n)) out.add(n);
38
+ }
39
+ }
40
+ return [...out].sort((a, b) => a - b);
41
+ }
42
+
4
43
  // src/testing/index.ts
5
44
  var ConformanceError = class extends Error {
6
45
  contract;
@@ -63,5 +102,11 @@ async function assertMarkerStoreConformance(store) {
63
102
  }
64
103
  export {
65
104
  ConformanceError,
66
- assertMarkerStoreConformance
105
+ assertMarkerStoreConformance,
106
+ chartContainers,
107
+ clickEmpty,
108
+ clickMark,
109
+ findMark,
110
+ marksOf,
111
+ selectedRows
67
112
  };
@@ -0,0 +1,50 @@
1
+ import type { ChartHost } from "./host";
2
+ export interface ControlInfo {
3
+ /** The knob's name in the chart's bag ("rate", "horizon"). */
4
+ key: string;
5
+ /** Its value as the chart stores it (a number, or an ISO date string for a date knob). */
6
+ value: unknown;
7
+ /** The label the chart draws beside it ("growth per year"), or "" if none. */
8
+ label: string;
9
+ /** The readout the chart draws ("+5.5%", "7 years") - the value in the chart's own words. */
10
+ text: string;
11
+ }
12
+ export interface Controls {
13
+ /** Every knob's current value, by key - the opening values as soon as the chart has drawn. */
14
+ readonly values: Readonly<Record<string, unknown>>;
15
+ /** Each knob in the order the chart draws them, with its label and readout. */
16
+ readonly info: readonly ControlInfo[];
17
+ /** "growth per year +5.5% · horizon 7 years": what to show beside a saved scenario. */
18
+ readonly summary: string;
19
+ /** True once a chart has drawn and reported its controls. */
20
+ readonly ready: boolean;
21
+ /** Bumped on every change. */
22
+ readonly version: number;
23
+ /** Apply values (a saved scenario): the chart redraws with them. Unknown keys are ignored by the chart. */
24
+ set(values: Readonly<Record<string, unknown>>): void;
25
+ /** Back to the chart's own defaults. */
26
+ reset(): void;
27
+ onChange(cb: (c: Controls) => void): () => void;
28
+ }
29
+ /** What attachControls hands the host: the uiState pair the chart reads and writes. */
30
+ export interface ControlsAttachment {
31
+ /** Merge into the host's options at creation (and they stay: setOptions merges). */
32
+ readonly options: {
33
+ uiState: Record<string, unknown>;
34
+ setUiState: (s: unknown) => void;
35
+ };
36
+ /** Read the chart's controls now (call once a render has settled). */
37
+ read(): void;
38
+ detach(): void;
39
+ }
40
+ /** Make a controls handle: give it to one chart (a BicChart's `controls`, or attachControls). */
41
+ export declare function createControls(initial?: Readonly<Record<string, unknown>>): Controls;
42
+ /** Read each knob off a rendered chart: values from the bag, label and readout from what it drew. */
43
+ export declare function readControls(container: HTMLElement): ControlInfo[];
44
+ /**
45
+ * Wire a controls handle to one chart. Call it before creating the host and pass
46
+ * `attachment.options` into createChartHost's options (a raw uiState / setUiState pair takes
47
+ * precedence over any viewState provider); `getHost` returns the host once it exists. Call
48
+ * `read()` after each render settles.
49
+ */
50
+ export declare function attachControls(getHost: () => ChartHost | null, container: HTMLElement, controls: Controls): ControlsAttachment;
@@ -0,0 +1,22 @@
1
+ import type { Filter, FilterKey } from "./filterScope";
2
+ /** One value as a DAX literal: text quoted with "" doubled, numbers, TRUE()/FALSE(), dates, BLANK(). */
3
+ export declare function daxLiteral(v: unknown): string;
4
+ /**
5
+ * TREATAS over a filter's selected keys (or keys given directly), onto the named model columns -
6
+ * or null when nothing is selected, so `calculateTable` leaves the query unfiltered.
7
+ */
8
+ export declare function treatAs(selection: Filter | readonly (FilterKey | unknown)[] | null | undefined, ...columnRefs: string[]): string | null;
9
+ /**
10
+ * CALCULATETABLE(table, filters...) with the null filters dropped - the table expression unchanged
11
+ * when none is left. `table` is a table expression (SUMMARIZECOLUMNS(...)), not a whole query with
12
+ * EVALUATE.
13
+ */
14
+ export declare function calculateTable(table: string, ...filters: Array<string | null | undefined | false>): string;
15
+ /**
16
+ * A whole query (`[DEFINE ...] EVALUATE <table> [ORDER BY ...]`, as a .dax file holds it) with its table filtered:
17
+ * the table expression goes inside CALCULATETABLE with the filters, DEFINE and ORDER BY stay where they are. With no
18
+ * live filter the query comes back unchanged. One EVALUATE only (throws on more).
19
+ *
20
+ * filterQuery(FORECAST_DAX, treatAs(region, "Geography[RegionCode]"))
21
+ */
22
+ export declare function filterQuery(query: string, ...filters: Array<string | null | undefined | false>): string;
@@ -0,0 +1,133 @@
1
+ import type { ChartHost } from "./host";
2
+ /** Why a filter changed. */
3
+ export type FilterChangeReason = "click" | "clear" | "page" | "data";
4
+ /** One selected key: a value per key column, in the filter's column order. */
5
+ export type FilterKey = readonly unknown[];
6
+ export interface FilterOptions {
7
+ /** What a chip calls this filter ("Country"). Defaults to the key columns. */
8
+ label?: string;
9
+ /**
10
+ * The column(s) a chip shows for a selected key, read from the selected rows ("Country" for a
11
+ * RegionCode key; ["OriginName", "DestinationName"] for a route, joined " → ").
12
+ * Defaults to the key columns.
13
+ */
14
+ display?: string | readonly string[];
15
+ /** A stable id for this filter within its scope. Generated when absent. */
16
+ id?: string;
17
+ }
18
+ export interface Filter {
19
+ readonly id: string;
20
+ /** The key column(s). */
21
+ readonly columns: readonly string[];
22
+ readonly label: string;
23
+ /** True while anything is selected. */
24
+ readonly active: boolean;
25
+ /** The selected keys, each one value per key column. */
26
+ readonly keys: readonly FilterKey[];
27
+ /** The rows the selection was made from (for display names); rows of keys set by value are absent. */
28
+ readonly rows: readonly Record<string, unknown>[];
29
+ /** The first selected row, or null. */
30
+ readonly row: Record<string, unknown> | null;
31
+ /** The first key's first value, or null - the common one-column, one-pick case. */
32
+ readonly value: unknown;
33
+ /** Every selected key's first value. */
34
+ readonly values: readonly unknown[];
35
+ /** What a chip shows: the display column(s) of each selected key, "A → B" within a key, ", " between keys. */
36
+ readonly text: string;
37
+ /**
38
+ * The display text for ANY key - a badge's, a key set by value - read from `rows` (a table carrying the display
39
+ * columns, e.g. the chart's own rows) or else from the rows this filter has seen; the key's values when neither
40
+ * has it.
41
+ */
42
+ textOf(key: FilterKey | unknown, rows?: readonly Record<string, unknown>[]): string;
43
+ /** Why the last change happened (null before any). */
44
+ readonly reason: FilterChangeReason | null;
45
+ /** Bumped on every change: a cheap identity for memoising on the filter's state. */
46
+ readonly version: number;
47
+ /** Select these rows' keys (replacing the selection). An empty list clears. */
48
+ selectRows(rows: readonly Record<string, unknown>[], reason?: FilterChangeReason): void;
49
+ /** Select keys by value: one value per key (a one-column filter) or one array per key. */
50
+ set(keys: readonly unknown[] | readonly FilterKey[] | unknown, reason?: FilterChangeReason): void;
51
+ /** Clear the selection (and every bound chart's marks). */
52
+ clear(reason?: FilterChangeReason): void;
53
+ /** Does this row carry a selected key? Read on `columns` (default: the key columns). */
54
+ has(row: Record<string, unknown>, columns?: readonly string[]): boolean;
55
+ /**
56
+ * The rows that carry a selected key - every row while nothing is selected. `columns` names
57
+ * where this table keeps the key when it differs from the filter's: a one-column filter
58
+ * matched against several columns keeps a row when ANY of them carries the key (a route
59
+ * table filtered by a country keeps routes from OR to it). The same array comes back while
60
+ * the input and the filter are unchanged, so it's safe to call during render.
61
+ */
62
+ keep<T extends Record<string, unknown>>(rows: readonly T[], columns?: readonly string[]): T[];
63
+ /** Subscribe to changes. Returns the unsubscribe. */
64
+ onChange(cb: (filter: Filter, reason: FilterChangeReason) => void): () => void;
65
+ }
66
+ /** A page's filters: what a chip bar lists and what clear-all clears. */
67
+ export interface FilterScope {
68
+ readonly filters: readonly Filter[];
69
+ /** The filters with something selected. */
70
+ readonly active: readonly Filter[];
71
+ /** Make a filter in this scope (see createFilter). */
72
+ filter(columns: string | readonly string[], opts?: FilterOptions): Filter;
73
+ /** Add an existing filter. Returns the remove. */
74
+ add(filter: Filter): () => void;
75
+ /** Clear every filter. */
76
+ clearAll(reason?: FilterChangeReason): void;
77
+ /** Subscribe to any change of any filter, and to filters being added or removed. */
78
+ onChange(cb: (scope: FilterScope) => void): () => void;
79
+ }
80
+ /** Make a filter keyed by `columns` (one name, or several for a compound key such as a route). */
81
+ export declare function createFilter(columns: string | readonly string[], opts?: FilterOptions): Filter;
82
+ /** Make a page's filter scope. */
83
+ export declare function createFilterScope(): FilterScope;
84
+ export interface FilterBinding {
85
+ /**
86
+ * Call after the chart is handed new data. A selection whose key is still drawn is repainted;
87
+ * one whose key is gone is cleared (reason "data") when this chart made it. An empty payload is
88
+ * treated as data still arriving, not as the key being gone.
89
+ */
90
+ reconcile(): void;
91
+ /** Repaint the filter's selection onto the chart's marks. */
92
+ paint(): void;
93
+ /** Unwire. The filter keeps its selection. */
94
+ detach(): void;
95
+ }
96
+ export interface BindFilterOptions {
97
+ /** The source row (keyed by column name) behind payload row `i`, or null. */
98
+ rowAt(i: number): Record<string, unknown> | null;
99
+ /** How many rows the chart is drawing now. */
100
+ rowCount(): number;
101
+ }
102
+ /** The source row behind each payload row, from a payload of { columns, rows: unknown[][] }. */
103
+ export declare function payloadRowReader(getPayload: () => {
104
+ columns: readonly {
105
+ name: string;
106
+ }[];
107
+ rows: readonly unknown[][];
108
+ } | null | undefined): BindFilterOptions;
109
+ /**
110
+ * Wire one chart host to one filter. A click on the chart sets the filter (reason "click"), the
111
+ * chart's own clear gesture clears it (reason "clear"), and any change to the filter - from this
112
+ * chart, another chart, or the page - is painted onto this chart's marks. Paints notify the host's
113
+ * subscribers with source "host", which this binding (and a chart group) never republishes.
114
+ */
115
+ export declare function bindFilter(host: ChartHost, filter: Filter, opts: BindFilterOptions): FilterBinding;
116
+ /** The interaction events @microsoft/fabric-visuals' VegaVisual and DataGrid emit (onInteraction). */
117
+ export interface VegaInteractionEvent {
118
+ action: string;
119
+ selections?: Array<{
120
+ predicates?: Array<{
121
+ type?: string;
122
+ name?: string;
123
+ values?: unknown[];
124
+ }>;
125
+ }>;
126
+ }
127
+ /**
128
+ * Apply a VegaVisual / DataGrid `onInteraction` event list to a filter, so Microsoft's charts take
129
+ * part in the same page filters as ours: `onInteraction={e => fromVegaInteraction(country, e)}`.
130
+ * A `select` sets the filter from the set predicates that name its key columns (a selection
131
+ * missing one of them is skipped); a `clear` clears it. Returns whether the filter changed.
132
+ */
133
+ export declare function fromVegaInteraction(filter: Filter, events: readonly VegaInteractionEvent[] | null | undefined): boolean;
@@ -9,7 +9,10 @@ export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./
9
9
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
10
10
  export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
11
11
  export { ANNOTATION_CLASS, ANNOTATION_LAYER_CLASS, type MarkAnnotation, type AnnotationReport } from "./annotations";
12
+ export type { MarkNoteLike, NoteBadgeOptions } from "./markNotes";
13
+ export type { Controls, ControlInfo, ControlsAttachment } from "./controls";
12
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";
13
16
  export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
14
17
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
15
18
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, tableHostOf, chartSvgOf, htmlToSvgDataUrl, htmlNaturalSize, type SnapshotOptions } from "./snapshot";
@@ -0,0 +1,30 @@
1
+ import type { MarkAnnotation } from "./annotations";
2
+ import type { Filter, FilterKey } from "./filterScope";
3
+ export interface MarkNoteLike {
4
+ chart?: string | null;
5
+ markKey?: unknown;
6
+ markKeyTo?: unknown;
7
+ body?: string | null;
8
+ }
9
+ export interface NoteBadgeOptions<N> {
10
+ /** The chart's key column(s) the note keys live in: "RegionCode", or a route's two ends. */
11
+ columns: string | readonly string[];
12
+ /** Only notes whose `chart` is this (one notes table serving several charts). */
13
+ chart?: string;
14
+ /** A note's key, one value per column. Default: [markKey] or [markKey, markKeyTo]. */
15
+ keyOf?: (note: N) => readonly unknown[];
16
+ /** A note's text. Default: body. */
17
+ textOf?: (note: N) => string;
18
+ }
19
+ /**
20
+ * One badge per noted mark: the count of its notes, the latest note as the tooltip (notes are
21
+ * taken in the order given - pass them newest first). Pass to a chart's `annotations`.
22
+ */
23
+ export declare function noteBadges<N extends MarkNoteLike>(notes: readonly N[] | null | undefined, opts: NoteBadgeOptions<N>): MarkAnnotation[];
24
+ /** The key values of a badge noteBadges made (for a click: which mark's notes to open). */
25
+ export declare function badgeKey(a: MarkAnnotation): FilterKey;
26
+ /**
27
+ * The notes on one mark - a key given directly, or the first key selected in a page filter - in
28
+ * the order given. Keys compare as trimmed text, so a numeric code and its string agree.
29
+ */
30
+ export declare function notesFor<N extends MarkNoteLike>(notes: readonly N[] | null | undefined, key: Filter | FilterKey | null | undefined, opts: Omit<NoteBadgeOptions<N>, "textOf">): N[];
@@ -0,0 +1,3 @@
1
+ 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
+ export { createControls, attachControls, readControls, type Controls, type ControlInfo, type ControlsAttachment } from "./controls";
3
+ export { noteBadges, badgeKey, notesFor, type MarkNoteLike, type NoteBadgeOptions } from "./markNotes";