@bicharts/chart-host 0.6.4 → 0.6.6

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/react.mjs CHANGED
@@ -1,10 +1,13 @@
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
- buildRenderPayload,
3
+ assembleD3,
4
+ createChartGroup,
4
5
  createChartHost,
5
6
  geoFromCache,
6
- loadGeo
7
- } from "./chunk-AQMNNUBY.mjs";
7
+ loadGeo,
8
+ syncMemberSelection,
9
+ toSourceRows
10
+ } from "./chunk-ZPWRXCS5.mjs";
8
11
  import "./chunk-A2GMXZP7.mjs";
9
12
  import "./chunk-GHODWOAL.mjs";
10
13
 
@@ -17,32 +20,28 @@ function useBicSelection() {
17
20
  return {
18
21
  rows: g?.selection.rows ?? [],
19
22
  sourceId: g?.selection.sourceId ?? null,
20
- clear: () => g?.clear()
23
+ clear: () => g?.group.clear()
21
24
  };
22
25
  }
23
26
  function BicChartGroup({ rows, columns, geo, point, children }) {
24
- const [sel, setSel] = useState({ sourceId: null, rows: [] });
25
- const value = useMemo(() => ({
26
- rows,
27
- columns,
28
- geo,
29
- point,
30
- selection: sel,
31
- publish: (id, sourceRowIdxs) => setSel(sourceRowIdxs.length ? { sourceId: id, rows: sourceRowIdxs } : { sourceId: null, rows: [] }),
32
- // A chart never filters itself — that is what keeps the gesture reversible:
33
- // the origin chart still shows every mark, so there is always something left
34
- // to click to change or clear the selection.
35
- filterFor: (id) => sel.rows.length && sel.sourceId && sel.sourceId !== id ? sel.rows : null,
36
- clear: () => setSel({ sourceId: null, rows: [] })
37
- }), [rows, columns, geo, point, sel]);
27
+ const groupRef = useRef(null);
28
+ if (!groupRef.current) groupRef.current = createChartGroup(columns, rows, { geo, point });
29
+ const group = groupRef.current;
30
+ const [sel, setSel] = useState(group.selection);
31
+ const source = useMemo(() => {
32
+ group.setSource(columns, rows, { geo, point });
33
+ return {};
34
+ }, [group, rows, columns, geo, point]);
35
+ useEffect(() => group.onChange((s, change) => {
36
+ if (change !== "source") setSel(s);
37
+ }), [group]);
38
+ const value = useMemo(
39
+ () => ({ group, selection: sel }),
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ [group, sel, source]
42
+ );
38
43
  return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
39
44
  }
40
- function payloadFor(g, sourceIdxs) {
41
- const idxs = sourceIdxs ?? g.rows.map((_, i) => i);
42
- const subset = idxs.map((i) => g.rows[i]);
43
- const p = buildRenderPayload(g.columns, subset, g.geo ?? void 0, g.point ?? void 0);
44
- return { payload: p, rowMap: idxs };
45
- }
46
45
  function BicChart(props) {
47
46
  const {
48
47
  code,
@@ -64,15 +63,15 @@ function BicChart(props) {
64
63
  const ref = useRef(null);
65
64
  const hostRef = useRef(null);
66
65
  const rowMapRef = useRef(null);
67
- const group = useContext(Ctx);
68
- const active = group ? group.selection : null;
69
- const incoming = !group ? null : filteredBy ? active.sourceId === filteredBy ? group.filterFor(id) : null : group.filterFor(id);
66
+ const ctx = useContext(Ctx);
67
+ const group = ctx ? ctx.group : null;
68
+ const incoming = group ? group.incomingFor(id, filteredBy) : null;
70
69
  const highlightMode = respondsWith === "highlight";
71
70
  const filterSel = highlightMode ? null : incoming;
72
71
  const built = useMemo(() => {
73
72
  if (!group) return null;
74
- return payloadFor(group, filterSel);
75
- }, [group, filterSel && filterSel.join(",")]);
73
+ return group.payloadFor(filterSel);
74
+ }, [ctx, filterSel && filterSel.join(",")]);
76
75
  const data = built ? built.payload : props.data;
77
76
  rowMapRef.current = built ? built.rowMap : null;
78
77
  const onSelectRef = useRef(onSelect);
@@ -120,8 +119,7 @@ function BicChart(props) {
120
119
  hostRef.current = host;
121
120
  const off = host.selection.onChange((payloadIdxs, source) => {
122
121
  if (source === "host") return;
123
- const map = rowMapRef.current;
124
- const sourceIdxs = map ? payloadIdxs.map((i) => map[i]).filter((i) => i !== void 0) : payloadIdxs;
122
+ const sourceIdxs = toSourceRows(rowMapRef.current, payloadIdxs);
125
123
  if (group && id) group.publish(id, sourceIdxs);
126
124
  onSelectRef.current?.(sourceIdxs);
127
125
  });
@@ -141,19 +139,33 @@ function BicChart(props) {
141
139
  }, [data]);
142
140
  useEffect(() => {
143
141
  const host = hostRef.current;
144
- if (!host || !group) return;
145
- const mine = group.selection.sourceId === id && group.selection.rows.length > 0;
146
- if (mine) return;
147
- if (highlightMode && incoming && incoming.length) {
148
- host.selection.highlight(incoming);
149
- return;
150
- }
151
- if (host.selection.current && host.selection.current.length) host.selection.clear();
152
- }, [group?.selection, id, group, highlightMode, incoming && incoming.join(",")]);
142
+ if (!host || !ctx) return;
143
+ syncMemberSelection(host, ctx.selection, id, highlightMode, incoming);
144
+ }, [ctx?.selection, id, ctx, highlightMode, incoming && incoming.join(",")]);
153
145
  return /* @__PURE__ */ jsx("div", { ref, className, style });
154
146
  }
147
+ function useMeasuredSize(ref) {
148
+ const [size, setSize] = useState({ width: 0, height: 0 });
149
+ useLayoutEffect(() => {
150
+ const el = ref.current;
151
+ if (!el) return;
152
+ const read = () => {
153
+ const r = el.getBoundingClientRect();
154
+ const width = Math.round(r.width), height = Math.round(r.height);
155
+ setSize((s) => s.width === width && s.height === height ? s : { width, height });
156
+ };
157
+ read();
158
+ if (typeof ResizeObserver === "undefined") return;
159
+ const ro = new ResizeObserver(read);
160
+ ro.observe(el);
161
+ return () => ro.disconnect();
162
+ }, [ref]);
163
+ return size;
164
+ }
155
165
  export {
156
166
  BicChart,
157
167
  BicChartGroup,
158
- useBicSelection
168
+ assembleD3,
169
+ useBicSelection,
170
+ useMeasuredSize
159
171
  };
@@ -0,0 +1,88 @@
1
+ import type { ChartHost } from "./host";
2
+ import { type GeoPointBinding, type RenderPayload } from "./payload";
3
+ /** The group's one active selection, in SOURCE row indices. `sourceId` is the member that made it. */
4
+ export interface ChartGroupSelection {
5
+ readonly sourceId: string | null;
6
+ readonly rows: readonly number[];
7
+ }
8
+ /** The table every member's payload is derived from, and the geo bindings applied to each. */
9
+ export interface ChartGroupSourceOptions {
10
+ /** Choropleth join binding, applied when building each member's payload. */
11
+ geo?: {
12
+ column: string;
13
+ kind: string;
14
+ } | null;
15
+ /** Point-map geocoding binding (city/state/zip/lat/lon column names). */
16
+ point?: GeoPointBinding | null;
17
+ }
18
+ /** How one member takes part in the group. */
19
+ export interface ChartGroupMemberOptions {
20
+ /** Respond only to this member's selection (one-way wiring). Absent: respond to any sibling. */
21
+ filteredBy?: string;
22
+ /** "filter" (default) re-derives the member's payload; "highlight" keeps it and paints. */
23
+ respondsWith?: "filter" | "highlight";
24
+ }
25
+ /** A member's payload and the map from each payload row to its source row. */
26
+ export interface ChartGroupPayload {
27
+ payload: RenderPayload;
28
+ /** `rowMap[k]` is the source row index of payload row `k`. */
29
+ rowMap: number[];
30
+ }
31
+ /** Why the group notified: a member's selection, a clear, or a new source table. */
32
+ export type ChartGroupChange = "select" | "clear" | "source";
33
+ /** A live chart wired into the group by `attach`. */
34
+ export interface ChartGroupMember {
35
+ /** The payload-row -> source-row map of the payload the member is drawing now. */
36
+ readonly rowMap: readonly number[];
37
+ /** Unwire the member: its clicks stop publishing and the group stops redrawing it. */
38
+ detach(): void;
39
+ }
40
+ export interface ChartGroup {
41
+ /** The active selection. A new object on every publish and clear; kept across a new source. */
42
+ readonly selection: ChartGroupSelection;
43
+ readonly columns: readonly any[];
44
+ readonly rows: readonly Record<string, any>[];
45
+ /**
46
+ * The source rows member `id` should be FILTERED to now, or null for all of them. Never the
47
+ * member's own selection. With `filteredBy`, only that partner's selection counts.
48
+ */
49
+ incomingFor(id: string | undefined, filteredBy?: string): number[] | null;
50
+ /** A payload for a subset of source rows (null = every row), with its row map. */
51
+ payloadFor(sourceRowIdxs: readonly number[] | null): ChartGroupPayload;
52
+ /** The payload member `id` should draw now, under the given options. */
53
+ memberPayload(id: string | undefined, opts?: ChartGroupMemberOptions): ChartGroupPayload;
54
+ /** Publish member `id`'s selection in SOURCE rows. An empty list clears the selection. */
55
+ publish(id: string, sourceRowIdxs: readonly number[]): void;
56
+ /** Clear the group's selection. */
57
+ clear(): void;
58
+ /** Replace the source table (and bindings). The selection is kept, as source indices. */
59
+ setSource(columns: readonly any[], rows: readonly Record<string, any>[], opts?: ChartGroupSourceOptions): void;
60
+ /** Subscribe to every change. Returns the unsubscribe. */
61
+ onChange(cb: (selection: ChartGroupSelection, change: ChartGroupChange) => void): () => void;
62
+ /**
63
+ * Wire a live chart host into the group as member `id` (undefined: a member that responds but
64
+ * never publishes). Create the host with `memberPayload(id, opts).payload` as its data. From
65
+ * then on its clicks publish SOURCE rows (and reach `onSelect`), and every group change hands
66
+ * it a re-derived payload and paints or clears the selection it should show.
67
+ */
68
+ attach(id: string | undefined, host: ChartHost, opts?: ChartGroupMemberOptions & {
69
+ onSelect?: (sourceRowIdxs: number[]) => void;
70
+ }): ChartGroupMember;
71
+ /** Detach every member and drop every subscriber. */
72
+ destroy(): void;
73
+ }
74
+ /** Payload row indices -> source row indices, dropping any the map does not cover. */
75
+ export declare function toSourceRows(rowMap: readonly number[] | null | undefined, payloadRowIdxs: readonly number[]): number[];
76
+ /**
77
+ * Make a member's painted selection agree with the group's. A member that made the selection
78
+ * keeps what its host already painted. A highlight member with an incoming selection paints it.
79
+ * Any other member that still shows a selection clears it - a stale highlight left on the origin
80
+ * after the selection moved elsewhere, or after a clear. Both paths notify with source "host",
81
+ * so nothing done here is republished.
82
+ */
83
+ export declare function syncMemberSelection(host: ChartHost, selection: ChartGroupSelection, id: string | undefined, highlight: boolean, incoming: readonly number[] | null): void;
84
+ /**
85
+ * Create a cross-filter group over one source table: `rows` are objects keyed by column name,
86
+ * `columns` the column descriptors (`ingest()` returns both).
87
+ */
88
+ export declare function createChartGroup(columns: readonly any[], rows: readonly Record<string, any>[], opts?: ChartGroupSourceOptions): ChartGroup;
@@ -235,6 +235,16 @@ export interface ChartHost {
235
235
  initialMark(): Element | null;
236
236
  destroy(): void;
237
237
  }
238
+ /**
239
+ * ONE d3 OBJECT FROM d3 AND ITS PLUGINS - the object a chart is handed as `d3`.
240
+ *
241
+ * In a bundled app d3 and each plugin (d3-sankey, d3-hexbin, ...) are separate module
242
+ * namespaces, and a chart reaches all of them through one `d3`. A namespace is also FROZEN, and a
243
+ * generated chart installs its own helpers onto the d3 it is given, so the object must be a plain
244
+ * extensible copy: `assembleD3(d3, d3Sankey, d3Hexbin)`. Later arguments win on a name clash.
245
+ * `requiredD3Plugins(code)` names the plugin packages a given chart needs.
246
+ */
247
+ export declare function assembleD3(base: object, ...plugins: object[]): Record<string, any>;
238
248
  export declare function requiredD3Plugins(code: string): string[];
239
249
  export declare function explainRenderFailure(err: unknown, d3: any): unknown;
240
250
  export declare function stripEsmExports(code: string): string;
@@ -6,7 +6,8 @@ export { loadGeo, geoFromCache, registerGeo, registerGeoAsset, geoAssetFor, clea
6
6
  export declare function registerCityTable(packed: string): void;
7
7
  export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./payload";
8
8
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
9
- export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
9
+ export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
10
+ export { createChartGroup, toSourceRows, syncMemberSelection, type ChartGroup, type ChartGroupMember, type ChartGroupSelection, type ChartGroupChange, type ChartGroupMemberOptions, type ChartGroupSourceOptions, type ChartGroupPayload } from "./group";
10
11
  export { createMarkResolver, isInsideControl, type MarkResolver, type MarkResolverEnv } from "./selection";
11
12
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
12
13
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, type SnapshotOptions } from "./snapshot";
@@ -1,7 +1,8 @@
1
- import type { ReactNode } from "react";
1
+ import type { ReactNode, RefObject } from "react";
2
2
  import { type ChartHostConfig } from "./host";
3
3
  import type { ResolveOptionsInput } from "./defaults";
4
- import { type GeoPointBinding } from "./payload";
4
+ import type { GeoPointBinding } from "./payload";
5
+ export { assembleD3 } from "./host";
5
6
  export interface BicChartProps {
6
7
  /** Generated render() source (an ES module's `code` export, or the raw string). */
7
8
  code?: string;
@@ -89,3 +90,15 @@ export interface BicChartGroupProps {
89
90
  */
90
91
  export declare function BicChartGroup({ rows, columns, geo, point, children }: BicChartGroupProps): import("react").JSX.Element;
91
92
  export declare function BicChart(props: BicChartProps): import("react").JSX.Element;
93
+ /**
94
+ * THE SIZE A CHART SHOULD DRAW AT: the element's measured box, kept current as it resizes.
95
+ *
96
+ * A chart draws to `options.width` / `options.height`, and a page that hard-codes them draws the
97
+ * same size in a phone and on a wall. Give the element its size in CSS (a width of 100%, a height
98
+ * or an aspect ratio) and pass what this measures. `{ width: 0, height: 0 }` until the element has
99
+ * been measured - skip rendering until then rather than drawing at zero.
100
+ */
101
+ export declare function useMeasuredSize(ref: RefObject<Element | null>): {
102
+ width: number;
103
+ height: number;
104
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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",