Inspector: live data updates under interactive selections

This commit is contained in:
2026-06-29 09:58:32 +03:00
parent 6656811b8e
commit 8aa05e503d
13 changed files with 269 additions and 21 deletions
+67
View File
@@ -66,6 +66,60 @@ export interface InspectedData {
tables: InspectableTable[];
}
/**
* Debounce for the live data inspector (spec §04; multi-view scope doc M5). A
* brush drag pulses `addDataListener` continuously; coalescing to one re-read per
* ~quiet-frame keeps the table feeling live without thrashing the grid on every
* pixel of the drag.
*/
export const LIVE_INSPECT_DEBOUNCE_MS = 120;
/** The minimal Vega `View` surface the live-inspect watcher needs. */
interface DataChangeView {
addDataListener(name: string, handler: () => void): unknown;
removeDataListener(name: string, handler: () => void): unknown;
}
/**
* Attach debounced listeners to every table the inspector shows so an interactive
* selection that recomputes a drawn table (a `filter: {param}` brush) re-reads the
* inspector live — see `RenderHandle.onDataChange`. Watches the union of each
* drawn table's resolved + input names (deduped); a highlight selection changes no
* data, so none fire. Returns an unsubscribe that cancels any pending re-read and
* detaches the listeners — but skips detaching once the view is finalized, since
* `view.finalize()` has already dropped every listener (and the unmount cleanup
* order can run this after the destroy). `isFinalized` is a getter, not a boolean,
* so it reflects the view's state at unsubscribe time, not subscribe time.
*/
export function watchInspectableData(
view: DataChangeView,
vgSpec: unknown,
onChange: () => void,
isFinalized: () => boolean,
): () => void {
if (isFinalized()) return () => {};
const names = [...new Set(inspectableViews(vgSpec).flatMap((v) => [v.resolved, v.input]))];
if (names.length === 0) return () => {};
let timer: ReturnType<typeof setTimeout> | null = null;
const handler = (): void => {
if (timer !== null) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
onChange();
}, LIVE_INSPECT_DEBOUNCE_MS);
};
for (const name of names) view.addDataListener(name, handler);
return () => {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
if (!isFinalized()) for (const name of names) view.removeDataListener(name, handler);
};
}
export interface RenderHandle {
/** Finalize the underlying Vega view and clear the node. */
destroy(): void;
@@ -104,6 +158,16 @@ export interface RenderHandle {
* "no chart" so the inspector can say which.
*/
inspectData(): InspectedData | null;
/**
* Subscribe to live changes of the inspected tables, for the data inspector's
* live mode (spec §04; multi-view scope doc M5). An interactive selection that
* *filters* a downstream view recomputes that view's compiled table in place —
* no re-embed — so a static inspector would show stale rows until the next full
* render; this fires (debounced) so the caller can re-read via `inspectData()`.
* A highlight selection (a `condition` encoding) changes no data, so it never
* fires. Returns an unsubscribe; a no-op when the view is already finalized.
*/
onDataChange(listener: () => void): () => void;
}
export interface RenderOptions {
@@ -323,5 +387,8 @@ export async function renderSpec(
}));
return { tables };
},
onDataChange(listener) {
return watchInspectableData(result.view, result.vgSpec, listener, () => finalized);
},
};
}