From c0026105d90acb1d705de7d503d6ef348c980484 Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Tue, 26 May 2026 13:22:22 -0400 Subject: [PATCH] feat(js): linked brushing via SelectionBus + onGridApiReady prop Introduces a frontend-only selection bus for linked brushing between DFViewer instances and external publishers (e.g. Vega-Lite brushes). Subscribers receive same-page events via EventTarget; BroadcastChannel fan-out covers cross-iframe / cross-tab cases. Messages dedupe by source so a publisher does not receive its own echo. DFViewer gains an optional onGridApiReady callback so consumers can drive `applyTransaction` + `refreshCells({force: true})` directly, avoiding the remount-via-key workaround. Registers the AG-Grid v33 modules that back those APIs (Event/Row/Render/ClientSideRowModelApi). Two Storybook stories demo the bus: LinkedBrushing (two DFViewers + button publisher) and LinkedBrushingVega (a Vega-Lite scatter <-> DFViewer). vega-embed is a devDependency only and is loaded via dynamic import(), so it never ships in the published bundle. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/buckaroo-js-core/package.json | 1 + .../DFViewerParts/DFViewerInfinite.tsx | 23 +- .../src/selection/SelectionBus.test.ts | 36 ++ .../src/selection/SelectionBus.ts | 70 +++ .../src/stories/LinkedBrushing.stories.tsx | 285 +++++++++ .../stories/LinkedBrushingVega.stories.tsx | 332 ++++++++++ packages/pnpm-lock.yaml | 576 +++++++++++++++++- 7 files changed, 1320 insertions(+), 3 deletions(-) create mode 100644 packages/buckaroo-js-core/src/selection/SelectionBus.test.ts create mode 100644 packages/buckaroo-js-core/src/selection/SelectionBus.ts create mode 100644 packages/buckaroo-js-core/src/stories/LinkedBrushing.stories.tsx create mode 100644 packages/buckaroo-js-core/src/stories/LinkedBrushingVega.stories.tsx diff --git a/packages/buckaroo-js-core/package.json b/packages/buckaroo-js-core/package.json index 768324686..674b4b5bb 100644 --- a/packages/buckaroo-js-core/package.json +++ b/packages/buckaroo-js-core/package.json @@ -117,6 +117,7 @@ "ts-node": "^10.9.2", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", + "vega-embed": "^7.1.0", "vite": "^5.4.10", "vite-plugin-dts": "^4.3.0", "vite-tsconfig-paths": "^5.1.4" diff --git a/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx b/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx index 7c8fa079e..40c1388ea 100644 --- a/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx +++ b/packages/buckaroo-js-core/src/components/DFViewerParts/DFViewerInfinite.tsx @@ -25,6 +25,10 @@ import { TooltipModule, TextFilterModule, ScrollApiModule, + EventApiModule, + RowApiModule, + RenderApiModule, + ClientSideRowModelApiModule, SortChangedEvent, CellClassParams, RefreshCellsParams, @@ -49,6 +53,10 @@ ModuleRegistry.registerModules([ TooltipModule, TextFilterModule, ScrollApiModule, + EventApiModule, + RowApiModule, + RenderApiModule, + ClientSideRowModelApiModule, ]); const AccentColor = "#2196F3" @@ -150,12 +158,14 @@ export function DFViewerInfinite({ max_rows_in_configs, view_name, data_key, + onGridApiReady, }: { data_wrapper: DatasourceOrRaw; df_viewer_config: DFViewerConfig; summary_stats_data?: DFData; activeCol?: [string, string]; setActiveCol: SetColumnFunc; + onGridApiReady?: (api: GridApi) => void; // these are the parameters that could affect the table, // dfviewer doesn't need to understand them, but it does need to use // them as keys to get updated data @@ -233,6 +243,7 @@ export function DFViewerInfinite({ effectiveScheme={effectiveScheme} view_name={view_name} data_key={data_key} + onGridApiReady={onGridApiReady} /> ) @@ -250,6 +261,7 @@ export function DFViewerInfiniteInner({ effectiveScheme, view_name, data_key, + onGridApiReady, }: { data_wrapper: DatasourceOrRaw; df_viewer_config: DFViewerConfig; @@ -266,6 +278,7 @@ export function DFViewerInfiniteInner({ effectiveScheme?: 'light' | 'dark'; view_name?: string; data_key?: string; + onGridApiReady?: (api: GridApi) => void; }) { /* const lastProps = useRef(null); @@ -574,6 +587,9 @@ export function DFViewerInfiniteInner({ // Ensure pinned rows are applied once API is ready params.api.setGridOption('pinnedTopRowData', topRowsRef.current || []); } catch (_e) {} + try { + onGridApiReady?.(params.api); + } catch (_e) {} }} context={{ outside_df_params, ...extra_context }} > @@ -627,16 +643,18 @@ const getDsGridOptions = (origGridOptions: GridOptions, maxRowsWithoutScrolling: return dsGridOptions; };export function DFViewer({ df_data, df_viewer_config, summary_stats_data, activeCol, setActiveCol, + onGridApiReady, }: { df_data: DFData; df_viewer_config: DFViewerConfig; summary_stats_data?: DFData; activeCol?: [string, string]; setActiveCol?: SetColumnFunc; + onGridApiReady?: (api: GridApi) => void; }) { const defaultSetColumnFunc = (_newCol:[string, string]):void => {} const sac:SetColumnFunc = setActiveCol || defaultSetColumnFunc; - + return ( + setActiveCol={sac} + onGridApiReady={onGridApiReady} /> ); } diff --git a/packages/buckaroo-js-core/src/selection/SelectionBus.test.ts b/packages/buckaroo-js-core/src/selection/SelectionBus.test.ts new file mode 100644 index 000000000..37ac17411 --- /dev/null +++ b/packages/buckaroo-js-core/src/selection/SelectionBus.test.ts @@ -0,0 +1,36 @@ +import { SelectionBus, getSelectionBus } from "./SelectionBus"; + +describe("SelectionBus", () => { + it("delivers a published selection to a subscriber on the same channel", () => { + const bus = new SelectionBus("test-roundtrip"); + const received: number[][] = []; + bus.subscribe("ch", (msg) => received.push(msg.ids as number[])); + bus.publish("ch", [1, 2, 3], "src-A"); + expect(received).toEqual([[1, 2, 3]]); + }); + + it("filters out a subscriber's own echo via ownSource", () => { + const bus = new SelectionBus("test-echo"); + const seenBySelf: number[][] = []; + const seenByOther: number[][] = []; + bus.subscribe("ch", (m) => seenBySelf.push(m.ids as number[]), "self"); + bus.subscribe("ch", (m) => seenByOther.push(m.ids as number[]), "other"); + bus.publish("ch", [7], "self"); + expect(seenBySelf).toEqual([]); + expect(seenByOther).toEqual([[7]]); + }); + + it("does not deliver across channels", () => { + const bus = new SelectionBus("test-channels"); + const onA: number[][] = []; + bus.subscribe("a", (m) => onA.push(m.ids as number[])); + bus.publish("b", [1], "src"); + expect(onA).toEqual([]); + }); + + it("getSelectionBus returns a stable singleton on window", () => { + const a = getSelectionBus(); + const b = getSelectionBus(); + expect(a).toBe(b); + }); +}); diff --git a/packages/buckaroo-js-core/src/selection/SelectionBus.ts b/packages/buckaroo-js-core/src/selection/SelectionBus.ts new file mode 100644 index 000000000..fb748a381 --- /dev/null +++ b/packages/buckaroo-js-core/src/selection/SelectionBus.ts @@ -0,0 +1,70 @@ +/** + * Frontend-only selection bus for linked brushing. Publishers (e.g. a Vega + * brush, a row-click handler, a "select these ids" button) emit a set of + * ids on a named channel; subscribers (DFViewer instances, charts) react. + * + * Same-page subscribers receive events via an internal EventTarget. When + * `BroadcastChannel` is available, messages also cross iframe / tab + * boundaries — useful inside JupyterLab where each widget output is its + * own iframe. + * + * No Python round trip, no kernel comm. Messages dedupe by `source` so a + * publisher does not receive its own echo. + */ + +export type SelectionId = string | number; + +export interface SelectionMessage { + channel: string; + ids: SelectionId[]; + source: string; +} + +type Listener = (msg: SelectionMessage) => void; + +export class SelectionBus { + private target = new EventTarget(); + private bc: BroadcastChannel | null = null; + + constructor(broadcastName = "buckaroo-selection") { + if (typeof BroadcastChannel !== "undefined") { + this.bc = new BroadcastChannel(broadcastName); + this.bc.onmessage = (ev) => this.dispatchLocal(ev.data as SelectionMessage); + } + } + + publish(channel: string, ids: Iterable, source: string): void { + const msg: SelectionMessage = { channel, ids: Array.from(ids), source }; + this.dispatchLocal(msg); + this.bc?.postMessage(msg); + } + + subscribe(channel: string, fn: Listener, ownSource?: string): () => void { + const handler = (ev: Event) => { + const msg = (ev as CustomEvent).detail; + if (msg.channel !== channel) return; + if (ownSource && msg.source === ownSource) return; + fn(msg); + }; + this.target.addEventListener("selection", handler); + return () => this.target.removeEventListener("selection", handler); + } + + private dispatchLocal(msg: SelectionMessage) { + this.target.dispatchEvent(new CustomEvent("selection", { detail: msg })); + } +} + +declare global { + interface Window { + __buckarooSelectionBus?: SelectionBus; + } +} + +export function getSelectionBus(): SelectionBus { + if (typeof window === "undefined") return new SelectionBus(); + if (!window.__buckarooSelectionBus) { + window.__buckarooSelectionBus = new SelectionBus(); + } + return window.__buckarooSelectionBus; +} diff --git a/packages/buckaroo-js-core/src/stories/LinkedBrushing.stories.tsx b/packages/buckaroo-js-core/src/stories/LinkedBrushing.stories.tsx new file mode 100644 index 000000000..9d43a16d9 --- /dev/null +++ b/packages/buckaroo-js-core/src/stories/LinkedBrushing.stories.tsx @@ -0,0 +1,285 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { GridApi } from "ag-grid-community"; + +import "../style/dcf-npm.css"; +import { DFViewer } from "../components/DFViewerParts/DFViewerInfinite"; +import { + ColumnConfig, + DFData, + DFViewerConfig, +} from "../components/DFViewerParts/DFWhole"; +import { + SelectionId, + getSelectionBus, +} from "../selection/SelectionBus"; + +/** + * Linked brushing demo wired through `SelectionBus` — the same bus a real + * Vega-Lite bridge would publish on. Two `DFViewer` instances subscribe to + * channel "demo" and one of them publishes on row click. Buttons simulate + * an external publisher (the role a brushed Vega chart would play). + * + * On message, each grid mutates `__selected` on each row via + * `api.applyTransaction` and calls `api.refreshCells({ force: true })` — + * no remount, scroll position preserved. + */ + +const CHANNEL = "demo"; +const HIGHLIGHT = "#ffe680"; +const SELECTION_KEY = "id"; +const N_ROWS = 100; + +const REGIONS = ["North", "South", "East", "West", "Central"]; + +type Row = { + index: number; + id: number; + region: string; + revenue: number; + units: number; + __selected: 1 | null; +}; + +function seededRandom(seed: number) { + let s = seed >>> 0; + return () => { + s = (s * 1664525 + 1013904223) >>> 0; + return s / 0xffffffff; + }; +} + +const buildRows = (): Row[] => { + const rand = seededRandom(42); + return Array.from({ length: N_ROWS }, (_, i) => ({ + index: i, + id: i, + region: REGIONS[Math.floor(rand() * REGIONS.length)], + revenue: Math.round(rand() * 20000 + 1000), + units: Math.round(rand() * 500 + 10), + __selected: null as 1 | null, + })); +}; + +const BASE_ROWS = buildRows(); + +const PRESET_SELECTIONS: Record = { + "Top 10 by revenue": [...BASE_ROWS] + .sort((a, b) => b.revenue - a.revenue) + .slice(0, 10) + .map((r) => r.id), + "Region: North": BASE_ROWS.filter((r) => r.region === "North").map( + (r) => r.id, + ), + "Every 7th row": BASE_ROWS.filter((r) => r.id % 7 === 0).map((r) => r.id), +}; + +const HIGHLIGHT_RULE = { + color_rule: "color_not_null" as const, + conditional_color: HIGHLIGHT, + exist_column: "__selected", +}; + +const buildConfig = (): DFViewerConfig => { + const column_config: ColumnConfig[] = [ + { + col_name: "id", + header_name: "id", + displayer_args: { displayer: "integer", min_digits: 1, max_digits: 4 }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "region", + header_name: "Region", + displayer_args: { displayer: "string" }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "revenue", + header_name: "Revenue ($)", + displayer_args: { + displayer: "float", + min_fraction_digits: 0, + max_fraction_digits: 0, + }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "units", + header_name: "Units", + displayer_args: { displayer: "integer", min_digits: 1, max_digits: 4 }, + color_map_config: HIGHLIGHT_RULE, + }, + ]; + return { + column_config, + pinned_rows: [], + left_col_configs: [ + { + col_name: "index", + header_name: "#", + displayer_args: { displayer: "string" }, + }, + ], + }; +}; + +/** + * One grid + the wiring that turns it into a bus participant. Subscribes + * on mount, mutates rows + refreshes cells on every incoming message, + * publishes on row click. + */ +const LinkedGrid = ({ + title, + sourceId, + onSelectionCount, +}: { + title: string; + sourceId: string; + onSelectionCount?: (n: number) => void; +}) => { + const bus = useMemo(getSelectionBus, []); + const apiRef = useRef(null); + const df_viewer_config = useMemo(buildConfig, []); + const df_data: DFData = useMemo( + () => BASE_ROWS.map((r) => ({ ...r })), + [], + ); + + const applySelection = useCallback( + (ids: SelectionId[]) => { + const api = apiRef.current; + if (!api) return; + const idSet = new Set(ids); + const updates: Row[] = []; + api.forEachNode((node) => { + if (!node.data) return; + const wanted: 1 | null = idSet.has(node.data[SELECTION_KEY]) + ? 1 + : null; + if (node.data.__selected !== wanted) { + updates.push({ ...node.data, __selected: wanted }); + } + }); + if (updates.length > 0) { + api.applyTransaction({ update: updates }); + } + api.refreshCells({ force: true }); + onSelectionCount?.(idSet.size); + }, + [onSelectionCount], + ); + + useEffect(() => { + return bus.subscribe(CHANNEL, (msg) => applySelection(msg.ids), sourceId); + }, [bus, sourceId, applySelection]); + + const onApiReady = useCallback((api: GridApi) => { + apiRef.current = api; + api.addEventListener("rowClicked", (ev: any) => { + if (!ev.data) return; + bus.publish(CHANNEL, [ev.data[SELECTION_KEY]], sourceId); + }); + }, [bus, sourceId]); + + return ( +
+
{title}
+
+ +
+
+ ); +}; + +const LinkedBrushingDemo = () => { + const bus = useMemo(getSelectionBus, []); + const [count, setCount] = useState(0); + const [lastSource, setLastSource] = useState("(none)"); + + useEffect(() => { + return bus.subscribe(CHANNEL, (msg) => { + setCount(msg.ids.length); + setLastSource(msg.source); + }); + }, [bus]); + + const publish = (name: string) => { + bus.publish(CHANNEL, PRESET_SELECTIONS[name] ?? [], "buttons"); + }; + + return ( +
+
+ Publish to bus: + {Object.keys(PRESET_SELECTIONS).map((name) => ( + + ))} + + + last: {count} ids from {lastSource} + +
+
+ Both grids subscribe to channel "{CHANNEL}". + Click any row in either grid to broadcast a single-id selection; + the other grid highlights that row. Buttons simulate an + external publisher (the role a Vega-Lite brush would play). +
+
+ undefined} + /> + +
+
+ ); +}; + +const meta: Meta = { + title: "Buckaroo/LinkedBrushing", + component: LinkedBrushingDemo, + parameters: { layout: "padded" }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = {}; diff --git a/packages/buckaroo-js-core/src/stories/LinkedBrushingVega.stories.tsx b/packages/buckaroo-js-core/src/stories/LinkedBrushingVega.stories.tsx new file mode 100644 index 000000000..5de0d53dd --- /dev/null +++ b/packages/buckaroo-js-core/src/stories/LinkedBrushingVega.stories.tsx @@ -0,0 +1,332 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import type { GridApi } from "ag-grid-community"; + +import "../style/dcf-npm.css"; +import { DFViewer } from "../components/DFViewerParts/DFViewerInfinite"; +import { + ColumnConfig, + DFData, + DFViewerConfig, +} from "../components/DFViewerParts/DFWhole"; +import { + SelectionId, + getSelectionBus, +} from "../selection/SelectionBus"; + +/** + * Linked brushing between a Vega-Lite scatterplot and a Buckaroo + * `DFViewer`, communicating through the same `SelectionBus` used by the + * pure-DFViewer demo. Vega-Lite is loaded with a dynamic `import()` so it + * is not part of the buckaroo-js-core bundle — bundlers code-split this + * chunk and only fetch it when this story is rendered. In production, + * `vega-embed` would be a peer / optional dependency: consumers who want + * the bridge install it themselves. + * + * Brush the chart → buckaroo highlights the brushed ids. + * Click a buckaroo row → the chart highlights that point. + */ + +const CHANNEL = "vega-demo"; +const HIGHLIGHT = "#ffe680"; +const SELECTION_KEY = "id"; +const N_ROWS = 100; + +type Row = { + index: number; + id: number; + x: number; + y: number; + cat: string; + __selected: 1 | null; +}; + +function seededRandom(seed: number) { + let s = seed >>> 0; + return () => { + s = (s * 1664525 + 1013904223) >>> 0; + return s / 0xffffffff; + }; +} + +const CATS = ["alpha", "beta", "gamma", "delta"]; + +const BASE_ROWS: Row[] = (() => { + const rand = seededRandom(7); + return Array.from({ length: N_ROWS }, (_, i) => ({ + index: i, + id: i, + x: Math.round(rand() * 1000) / 10, + y: Math.round(rand() * 1000) / 10, + cat: CATS[Math.floor(rand() * CATS.length)], + __selected: null as 1 | null, + })); +})(); + +const HIGHLIGHT_RULE = { + color_rule: "color_not_null" as const, + conditional_color: HIGHLIGHT, + exist_column: "__selected", +}; + +const buildConfig = (): DFViewerConfig => { + const column_config: ColumnConfig[] = [ + { + col_name: "id", + header_name: "id", + displayer_args: { displayer: "integer", min_digits: 1, max_digits: 4 }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "cat", + header_name: "Category", + displayer_args: { displayer: "string" }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "x", + header_name: "x", + displayer_args: { + displayer: "float", + min_fraction_digits: 1, + max_fraction_digits: 1, + }, + color_map_config: HIGHLIGHT_RULE, + }, + { + col_name: "y", + header_name: "y", + displayer_args: { + displayer: "float", + min_fraction_digits: 1, + max_fraction_digits: 1, + }, + color_map_config: HIGHLIGHT_RULE, + }, + ]; + return { + column_config, + pinned_rows: [], + left_col_configs: [ + { + col_name: "index", + header_name: "#", + displayer_args: { displayer: "string" }, + }, + ], + }; +}; + +const BUCKAROO_SOURCE = "vega-demo-buckaroo"; +const VEGA_SOURCE = "vega-demo-chart"; +const BRUSH_NAME = "brush"; + +const vegaSpec = { + $schema: "https://vega.github.io/schema/vega-lite/v6.json", + width: 420, + height: 360, + data: { values: BASE_ROWS }, + params: [ + { + name: BRUSH_NAME, + select: { type: "interval", encodings: ["x", "y"] }, + }, + ], + mark: { type: "circle", size: 80 }, + encoding: { + x: { field: "x", type: "quantitative" }, + y: { field: "y", type: "quantitative" }, + color: { + condition: { param: BRUSH_NAME, field: "cat", type: "nominal" }, + value: "lightgray", + }, + tooltip: [ + { field: "id" }, + { field: "cat" }, + { field: "x" }, + { field: "y" }, + ], + }, +}; + +const VegaChart = () => { + const hostRef = useRef(null); + const bus = useMemo(getSelectionBus, []); + + useEffect(() => { + let cancelled = false; + let view: any = null; + let unsub: (() => void) | null = null; + + // Dynamic import keeps vega-embed out of the buckaroo-js-core bundle. + // The chunk only loads when this story is rendered. + import("vega-embed").then(({ default: embed }) => { + if (cancelled || !hostRef.current) return; + embed(hostRef.current, vegaSpec as any, { actions: false }).then( + (result) => { + if (cancelled) return; + view = result.view; + + // Vega → bus: on every brush change, look up the data + // contained in the brush region and publish their ids. + view.addSignalListener(BRUSH_NAME, (_n: string, value: any) => { + // vega-lite interval selection signal materializes + // as { x: [x0, x1], y: [y0, y1] } once the user has + // drawn a rectangle; it is `{}` or undefined while + // the brush is cleared. + const xRange: [number, number] | undefined = value?.x; + const yRange: [number, number] | undefined = value?.y; + if (!xRange && !yRange) { + bus.publish(CHANNEL, [], VEGA_SOURCE); + return; + } + const inBrush = (view.data("source_0") as Row[]).filter( + (d) => { + const xOk = xRange + ? d.x >= xRange[0] && d.x <= xRange[1] + : true; + const yOk = yRange + ? d.y >= yRange[0] && d.y <= yRange[1] + : true; + return xOk && yOk; + }, + ); + bus.publish( + CHANNEL, + inBrush.map((d) => d.id), + VEGA_SOURCE, + ); + }); + + // Bus → Vega: incoming selection becomes a highlight + // overlay on top of the brush layer. We do it by + // tweaking the dataset rather than the brush param so + // remote selections don't fight the user's drag. + unsub = bus.subscribe( + CHANNEL, + (msg) => { + const ids = new Set(msg.ids); + const updated = (view.data("source_0") as Row[]).map( + (d) => ({ + ...d, + __selected: ids.has(d.id) ? 1 : null, + }), + ); + view.change( + "source_0", + (view as any).changeset().remove(() => true).insert(updated), + ).run(); + }, + VEGA_SOURCE, + ); + }, + ); + }); + + return () => { + cancelled = true; + unsub?.(); + view?.finalize?.(); + }; + }, [bus]); + + return ( +
+
Vega-Lite scatter (brushable)
+
+
+ ); +}; + +const BuckarooGrid = () => { + const bus = useMemo(getSelectionBus, []); + const apiRef = useRef(null); + const df_viewer_config = useMemo(buildConfig, []); + const df_data: DFData = useMemo(() => BASE_ROWS.map((r) => ({ ...r })), []); + + const applySelection = useCallback((ids: SelectionId[]) => { + const api = apiRef.current; + if (!api) return; + const idSet = new Set(ids); + const updates: Row[] = []; + api.forEachNode((node) => { + if (!node.data) return; + const wanted: 1 | null = idSet.has(node.data[SELECTION_KEY]) + ? 1 + : null; + if (node.data.__selected !== wanted) { + updates.push({ ...node.data, __selected: wanted }); + } + }); + if (updates.length > 0) api.applyTransaction({ update: updates }); + api.refreshCells({ force: true }); + }, []); + + useEffect(() => { + return bus.subscribe( + CHANNEL, + (msg) => applySelection(msg.ids), + BUCKAROO_SOURCE, + ); + }, [bus, applySelection]); + + const onApiReady = useCallback( + (api: GridApi) => { + apiRef.current = api; + api.addEventListener("rowClicked", (ev: any) => { + if (!ev.data) return; + bus.publish( + CHANNEL, + [ev.data[SELECTION_KEY]], + BUCKAROO_SOURCE, + ); + }); + }, + [bus], + ); + + return ( +
+
Buckaroo DFViewer
+
+ +
+
+ ); +}; + +const LinkedBrushingVegaDemo = () => { + return ( +
+
+ Both views subscribe to channel "{CHANNEL}" on the + shared SelectionBus. Drag a rectangle on the + scatter to brush; the grid highlights the brushed ids. Click a + row in the grid; the chart re-renders with that point flagged. + Vega-Lite is loaded via dynamic import() — it is + a devDependency of this story, not a bundled dependency of + buckaroo-js-core. +
+
+ + +
+
+ ); +}; + +const meta: Meta = { + title: "Buckaroo/LinkedBrushingVega", + component: LinkedBrushingVegaDemo, + parameters: { layout: "padded" }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = {}; diff --git a/packages/pnpm-lock.yaml b/packages/pnpm-lock.yaml index a89618eb4..be780b7eb 100644 --- a/packages/pnpm-lock.yaml +++ b/packages/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: typescript-eslint: specifier: ^8.11.0 version: 8.50.0(eslint@9.39.2)(typescript@5.6.3) + vega-embed: + specifier: ^7.1.0 + version: 7.1.0(vega-lite@6.4.3(vega@6.2.0))(vega@6.2.0) vite: specifier: ^5.4.10 version: 5.4.21(@types/node@22.19.3) @@ -1440,6 +1443,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -1875,6 +1881,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1897,6 +1907,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -1959,14 +1976,44 @@ packages: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} + d3-geo-projection@4.0.0: + resolution: {integrity: sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==} + engines: {node: '>=12'} + hasBin: true + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} @@ -1975,6 +2022,14 @@ packages: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + d3-scale@4.0.2: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} @@ -2048,6 +2103,9 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2104,6 +2162,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2258,6 +2319,9 @@ packages: resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} engines: {node: '>=6.0.0'} + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2343,6 +2407,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2365,11 +2433,12 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -2782,6 +2851,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3229,6 +3301,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rollup@4.54.0: resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3237,6 +3312,9 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -3337,6 +3415,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3427,6 +3509,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -3601,6 +3687,124 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + vega-canvas@2.0.0: + resolution: {integrity: sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==} + + vega-crossfilter@5.1.0: + resolution: {integrity: sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==} + + vega-dataflow@6.1.0: + resolution: {integrity: sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==} + + vega-embed@7.1.0: + resolution: {integrity: sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==} + peerDependencies: + vega: '*' + vega-lite: '*' + + vega-encode@5.1.0: + resolution: {integrity: sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==} + + vega-event-selector@4.0.0: + resolution: {integrity: sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==} + + vega-expression@6.1.0: + resolution: {integrity: sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==} + + vega-force@5.1.0: + resolution: {integrity: sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==} + + vega-format@2.1.0: + resolution: {integrity: sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==} + + vega-functions@6.1.1: + resolution: {integrity: sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==} + + vega-geo@5.1.0: + resolution: {integrity: sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==} + + vega-hierarchy@5.1.0: + resolution: {integrity: sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==} + + vega-interpreter@2.2.1: + resolution: {integrity: sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==} + + vega-label@2.1.0: + resolution: {integrity: sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==} + + vega-lite@6.4.3: + resolution: {integrity: sha512-d/7hPjfz560UERaQuTmGgIVfXAe3g2hJWeC+igDeaGohUdEoNrHLXgR/yTOBT8vV/lIuuKnw+0/xWWblkDwkMQ==} + engines: {node: '>=20'} + hasBin: true + peerDependencies: + vega: ^6.0.0 + + vega-loader@5.1.0: + resolution: {integrity: sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==} + + vega-parser@7.1.0: + resolution: {integrity: sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==} + + vega-projection@2.1.0: + resolution: {integrity: sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==} + + vega-regression@2.1.0: + resolution: {integrity: sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==} + + vega-runtime@7.1.0: + resolution: {integrity: sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==} + + vega-scale@8.1.0: + resolution: {integrity: sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==} + + vega-scenegraph@5.1.0: + resolution: {integrity: sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==} + + vega-schema-url-parser@3.0.2: + resolution: {integrity: sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==} + + vega-selections@6.1.2: + resolution: {integrity: sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==} + + vega-statistics@2.0.0: + resolution: {integrity: sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==} + + vega-themes@3.0.0: + resolution: {integrity: sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==} + peerDependencies: + vega: '*' + vega-lite: '*' + + vega-time@3.1.0: + resolution: {integrity: sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==} + + vega-tooltip@1.0.0: + resolution: {integrity: sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==} + + vega-transforms@5.1.0: + resolution: {integrity: sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==} + + vega-typings@2.1.0: + resolution: {integrity: sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==} + + vega-util@2.1.1: + resolution: {integrity: sha512-tpNmm8bGtUa8gKfFDSjXPffxqSyPr91vaWIEBnJS/rijhoLZMwM+mgYQG6XfwdcBSN1+jkZ57P0sYSEW/jophw==} + + vega-view-transforms@5.1.0: + resolution: {integrity: sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==} + + vega-view@6.1.0: + resolution: {integrity: sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==} + + vega-voronoi@5.1.0: + resolution: {integrity: sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==} + + vega-wordcloud@5.1.0: + resolution: {integrity: sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==} + + vega@6.2.0: + resolution: {integrity: sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==} + victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} @@ -3676,10 +3880,12 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -3721,6 +3927,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -3765,10 +3975,18 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5073,6 +5291,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.19.3 @@ -5595,6 +5815,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + clsx@2.1.1: {} co@4.6.0: {} @@ -5611,6 +5837,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@2.20.3: {} + + commander@7.2.0: {} + compare-versions@6.1.1: {} concat-map@0.0.1: {} @@ -5675,16 +5905,53 @@ snapshots: d3-color@3.1.0: {} + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + d3-ease@3.0.1: {} + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + d3-format@3.1.0: {} + d3-geo-projection@4.0.0: + dependencies: + commander: 7.2.0 + d3-array: 3.2.4 + d3-geo: 3.1.1 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 d3-path@3.1.0: {} + d3-quadtree@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + d3-scale@4.0.2: dependencies: d3-array: 3.2.4 @@ -5744,6 +6011,10 @@ snapshots: define-lazy-prop@2.0.0: {} + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -5785,6 +6056,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -5999,6 +6272,8 @@ snapshots: fast-equals@5.2.2: {} + fast-json-patch@3.1.1: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -6075,6 +6350,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6746,6 +7023,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-pretty-compact@4.0.0: {} + json5@2.2.3: {} jsonfile@6.2.0: @@ -7175,6 +7454,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + robust-predicates@3.0.3: {} + rollup@4.54.0: dependencies: '@types/estree': 1.0.8 @@ -7205,6 +7486,8 @@ snapshots: rrweb-cssom@0.8.0: {} + rw@1.3.3: {} + safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 @@ -7296,6 +7579,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.1.2 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7375,6 +7664,10 @@ snapshots: dependencies: is-number: 7.0.0 + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -7529,6 +7822,270 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + vega-canvas@2.0.0: {} + + vega-crossfilter@5.1.0: + dependencies: + d3-array: 3.2.4 + vega-dataflow: 6.1.0 + vega-util: 2.1.1 + + vega-dataflow@6.1.0: + dependencies: + vega-format: 2.1.0 + vega-loader: 5.1.0 + vega-util: 2.1.1 + + vega-embed@7.1.0(vega-lite@6.4.3(vega@6.2.0))(vega@6.2.0): + dependencies: + fast-json-patch: 3.1.1 + json-stringify-pretty-compact: 4.0.0 + semver: 7.7.3 + tslib: 2.8.1 + vega: 6.2.0 + vega-interpreter: 2.2.1 + vega-lite: 6.4.3(vega@6.2.0) + vega-schema-url-parser: 3.0.2 + vega-themes: 3.0.0(vega-lite@6.4.3(vega@6.2.0))(vega@6.2.0) + vega-tooltip: 1.0.0 + + vega-encode@5.1.0: + dependencies: + d3-array: 3.2.4 + d3-interpolate: 3.0.1 + vega-dataflow: 6.1.0 + vega-scale: 8.1.0 + vega-util: 2.1.1 + + vega-event-selector@4.0.0: {} + + vega-expression@6.1.0: + dependencies: + '@types/estree': 1.0.8 + vega-util: 2.1.1 + + vega-force@5.1.0: + dependencies: + d3-force: 3.0.0 + vega-dataflow: 6.1.0 + vega-util: 2.1.1 + + vega-format@2.1.0: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-time-format: 4.1.0 + vega-time: 3.1.0 + vega-util: 2.1.1 + + vega-functions@6.1.1: + dependencies: + d3-array: 3.2.4 + d3-color: 3.1.0 + d3-geo: 3.1.1 + vega-dataflow: 6.1.0 + vega-expression: 6.1.0 + vega-scale: 8.1.0 + vega-scenegraph: 5.1.0 + vega-selections: 6.1.2 + vega-statistics: 2.0.0 + vega-time: 3.1.0 + vega-util: 2.1.1 + + vega-geo@5.1.0: + dependencies: + d3-array: 3.2.4 + d3-color: 3.1.0 + d3-geo: 3.1.1 + vega-canvas: 2.0.0 + vega-dataflow: 6.1.0 + vega-projection: 2.1.0 + vega-statistics: 2.0.0 + vega-util: 2.1.1 + + vega-hierarchy@5.1.0: + dependencies: + d3-hierarchy: 3.1.2 + vega-dataflow: 6.1.0 + vega-util: 2.1.1 + + vega-interpreter@2.2.1: + dependencies: + vega-util: 2.1.1 + + vega-label@2.1.0: + dependencies: + vega-canvas: 2.0.0 + vega-dataflow: 6.1.0 + vega-scenegraph: 5.1.0 + vega-util: 2.1.1 + + vega-lite@6.4.3(vega@6.2.0): + dependencies: + json-stringify-pretty-compact: 4.0.0 + tslib: 2.8.1 + vega: 6.2.0 + vega-event-selector: 4.0.0 + vega-expression: 6.1.0 + vega-util: 2.1.1 + yargs: 18.0.0 + + vega-loader@5.1.0: + dependencies: + d3-dsv: 3.0.1 + topojson-client: 3.1.0 + vega-format: 2.1.0 + vega-util: 2.1.1 + + vega-parser@7.1.0: + dependencies: + vega-dataflow: 6.1.0 + vega-event-selector: 4.0.0 + vega-functions: 6.1.1 + vega-scale: 8.1.0 + vega-util: 2.1.1 + + vega-projection@2.1.0: + dependencies: + d3-geo: 3.1.1 + d3-geo-projection: 4.0.0 + vega-scale: 8.1.0 + + vega-regression@2.1.0: + dependencies: + d3-array: 3.2.4 + vega-dataflow: 6.1.0 + vega-statistics: 2.0.0 + vega-util: 2.1.1 + + vega-runtime@7.1.0: + dependencies: + vega-dataflow: 6.1.0 + vega-util: 2.1.1 + + vega-scale@8.1.0: + dependencies: + d3-array: 3.2.4 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + vega-time: 3.1.0 + vega-util: 2.1.1 + + vega-scenegraph@5.1.0: + dependencies: + d3-path: 3.1.0 + d3-shape: 3.2.0 + vega-canvas: 2.0.0 + vega-loader: 5.1.0 + vega-scale: 8.1.0 + vega-util: 2.1.1 + + vega-schema-url-parser@3.0.2: {} + + vega-selections@6.1.2: + dependencies: + d3-array: 3.2.4 + vega-expression: 6.1.0 + vega-util: 2.1.1 + + vega-statistics@2.0.0: + dependencies: + d3-array: 3.2.4 + + vega-themes@3.0.0(vega-lite@6.4.3(vega@6.2.0))(vega@6.2.0): + dependencies: + vega: 6.2.0 + vega-lite: 6.4.3(vega@6.2.0) + + vega-time@3.1.0: + dependencies: + d3-array: 3.2.4 + d3-time: 3.1.0 + vega-util: 2.1.1 + + vega-tooltip@1.0.0: + dependencies: + vega-util: 2.1.1 + + vega-transforms@5.1.0: + dependencies: + d3-array: 3.2.4 + vega-dataflow: 6.1.0 + vega-statistics: 2.0.0 + vega-time: 3.1.0 + vega-util: 2.1.1 + + vega-typings@2.1.0: + dependencies: + '@types/geojson': 7946.0.16 + vega-event-selector: 4.0.0 + vega-expression: 6.1.0 + vega-util: 2.1.1 + + vega-util@2.1.1: {} + + vega-view-transforms@5.1.0: + dependencies: + vega-dataflow: 6.1.0 + vega-scenegraph: 5.1.0 + vega-util: 2.1.1 + + vega-view@6.1.0: + dependencies: + d3-array: 3.2.4 + d3-timer: 3.0.1 + vega-dataflow: 6.1.0 + vega-format: 2.1.0 + vega-functions: 6.1.1 + vega-runtime: 7.1.0 + vega-scenegraph: 5.1.0 + vega-util: 2.1.1 + + vega-voronoi@5.1.0: + dependencies: + d3-delaunay: 6.0.4 + vega-dataflow: 6.1.0 + vega-util: 2.1.1 + + vega-wordcloud@5.1.0: + dependencies: + vega-canvas: 2.0.0 + vega-dataflow: 6.1.0 + vega-scale: 8.1.0 + vega-statistics: 2.0.0 + vega-util: 2.1.1 + + vega@6.2.0: + dependencies: + vega-crossfilter: 5.1.0 + vega-dataflow: 6.1.0 + vega-encode: 5.1.0 + vega-event-selector: 4.0.0 + vega-expression: 6.1.0 + vega-force: 5.1.0 + vega-format: 2.1.0 + vega-functions: 6.1.1 + vega-geo: 5.1.0 + vega-hierarchy: 5.1.0 + vega-label: 2.1.0 + vega-loader: 5.1.0 + vega-parser: 7.1.0 + vega-projection: 2.1.0 + vega-regression: 2.1.0 + vega-runtime: 7.1.0 + vega-scale: 8.1.0 + vega-scenegraph: 5.1.0 + vega-statistics: 2.0.0 + vega-time: 3.1.0 + vega-transforms: 5.1.0 + vega-typings: 2.1.0 + vega-util: 2.1.1 + vega-view: 6.1.0 + vega-view-transforms: 5.1.0 + vega-voronoi: 5.1.0 + vega-wordcloud: 5.1.0 + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.1 @@ -7655,6 +8212,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -7678,6 +8241,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -7688,6 +8253,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yn@3.1.1: {} yocto-queue@0.1.0: {}