Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/buckaroo-js-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
TooltipModule,
TextFilterModule,
ScrollApiModule,
EventApiModule,
RowApiModule,
RenderApiModule,
ClientSideRowModelApiModule,
SortChangedEvent,
CellClassParams,
RefreshCellsParams,
Expand All @@ -49,6 +53,10 @@ ModuleRegistry.registerModules([
TooltipModule,
TextFilterModule,
ScrollApiModule,
EventApiModule,
RowApiModule,
RenderApiModule,
ClientSideRowModelApiModule,
]);

const AccentColor = "#2196F3"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -233,6 +243,7 @@ export function DFViewerInfinite({
effectiveScheme={effectiveScheme}
view_name={view_name}
data_key={data_key}
onGridApiReady={onGridApiReady}
/>
</div>
</div>)
Expand All @@ -250,6 +261,7 @@ export function DFViewerInfiniteInner({
effectiveScheme,
view_name,
data_key,
onGridApiReady,
}: {
data_wrapper: DatasourceOrRaw;
df_viewer_config: DFViewerConfig;
Expand All @@ -266,6 +278,7 @@ export function DFViewerInfiniteInner({
effectiveScheme?: 'light' | 'dark';
view_name?: string;
data_key?: string;
onGridApiReady?: (api: GridApi) => void;
}) {
/*
const lastProps = useRef<any>(null);
Expand Down Expand Up @@ -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 }}
></AgGridReact>
Expand Down Expand Up @@ -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 (
<DFViewerInfinite
data_wrapper={{
Expand All @@ -647,7 +665,8 @@ const getDsGridOptions = (origGridOptions: GridOptions, maxRowsWithoutScrolling:
df_viewer_config={df_viewer_config}
summary_stats_data={summary_stats_data}
activeCol={activeCol}
setActiveCol={sac} />
setActiveCol={sac}
onGridApiReady={onGridApiReady} />
);
}

36 changes: 36 additions & 0 deletions packages/buckaroo-js-core/src/selection/SelectionBus.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
70 changes: 70 additions & 0 deletions packages/buckaroo-js-core/src/selection/SelectionBus.ts
Original file line number Diff line number Diff line change
@@ -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<SelectionId>, 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<SelectionMessage>).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;
}
Loading
Loading