mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Inspector: live data updates under interactive selections
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user