Inspector: multi-view data inspection with a per-table view picker

This commit is contained in:
2026-06-28 22:13:34 +03:00
parent a75ea5b59e
commit 8cad80f738
10 changed files with 537 additions and 258 deletions
+42 -33
View File
@@ -12,7 +12,7 @@ import type { VisualizationSpec } from 'vega-embed';
import type { Config } from 'vega-lite';
import { collectFontFamilies } from '@core/custom-theme';
import { embedFontsInSvg } from '@core/chart-export';
import { pickResultDataset, pickSourceDataset } from '@core/result-data';
import { inspectViewLabel, inspectableViews } from '@core/inspect-views';
import type { FontAsset } from '@core/font-asset';
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
@@ -44,15 +44,28 @@ interface ImageExportOptions {
embedFonts?: ReadonlyArray<FontAsset>;
}
/** The two ends of the chart's data pipeline, for the data inspector (spec §04). */
export interface InspectedData {
/** Parsed source rows, before the spec's transforms run — the input. */
/** One inspectable drawn table — the two ends of its pipeline (spec §04). */
export interface InspectableTable {
/** Stable selection id — the resolved table's compiled name. */
id: string;
/** User-facing label (never a compiler name — `@core/inspect-views`). */
label: string;
/** Parsed source rows, before the view's transforms run — the input. */
input: ReadonlyArray<Record<string, unknown>>;
/** Post-transform rows the chart draws — the output (equals `input` when the
* spec has no transforms). */
/** Post-transform rows the marks draw — the output (equals `input` when the view
* has no transforms). */
resolved: ReadonlyArray<Record<string, unknown>>;
}
/**
* The chart's inspectable data: one table per distinct table the marks draw, in
* document order (a multi-view spec yields several). `tables` is empty when the
* chart draws nothing inspectable, distinct from a `null` handle (no chart).
*/
export interface InspectedData {
tables: InspectableTable[];
}
export interface RenderHandle {
/** Finalize the underlying Vega view and clear the node. */
destroy(): void;
@@ -78,18 +91,17 @@ export interface RenderHandle {
*/
resize(): void;
/**
* The chart's input and resolved (post-transform) rows — for the data inspector
* (spec §04). Reads the live view's compiled dataflow once: lists its datasets,
* picks the most-upstream
* source and most-downstream result (`@core/result-data`), and returns both
* tables' rows. This is the one place besides export that reaches into the view,
* so the embedding boundary holds (arch 05 §1–§2) — callers get rows, never the
* `view`.
* The chart's inspectable tables — for the data inspector (spec §04). Enumerates
* the tables the marks draw from the compiled Vega spec (`@core/inspect-views`),
* and for each reads its input + resolved rows from the live view. A multi-view
* spec yields several tables; a unit spec yields one. This is the one place
* besides export that reaches into the view, so the embedding boundary holds
* (arch 05 §1–§2) — callers get rows, never the `view`.
*
* Returns `null` when there is nothing to inspect (the view was finalized, or
* the spec produced no inspectable table). Either side can be `[]` when its
* table is empty — a real signal (e.g. a filter removed every row on the
* resolved side), kept distinct from "no chart" so the inspector can say which.
* Returns `null` when the view was finalized (no chart). The result's `tables`
* is empty when a chart draws nothing inspectable, and any table's `input`/
* `resolved` can be `[]` (e.g. a filter removed every row) — kept distinct from
* "no chart" so the inspector can say which.
*/
inspectData(): InspectedData | null;
}
@@ -297,22 +309,19 @@ export async function renderSpec(
},
inspectData() {
if (finalized) return null;
// Enumerate the dataflow's datasets once, then pick the input + result
// tables. getState with a truthy `data` filter is Vega's documented way to
// list datasets (vega/editor's Data Viewer does the same) — we read only the
// keys. Rows come from view.data(name), which hands back the live array (no copy).
const state = result.view.getState({
data: () => true,
signals: () => false,
recurse: true,
}) as { data?: Record<string, unknown> };
const names = Object.keys(state.data ?? {});
const sourceName = pickSourceDataset(names);
const resultName = pickResultDataset(names);
if (sourceName === null && resultName === null) return null;
const rows = (name: string | null): ReadonlyArray<Record<string, unknown>> =>
name === null ? [] : ((result.view.data(name) ?? []) as Record<string, unknown>[]);
return { input: rows(sourceName), resolved: rows(resultName) };
// The tables the marks draw + their input lineage come from the compiled Vega
// spec (a byproduct of the embed, not recompiled); the rows come from
// view.data(name), which hands back the live array (no copy).
const views = inspectableViews(result.vgSpec);
const rows = (name: string): ReadonlyArray<Record<string, unknown>> =>
(result.view.data(name) ?? []) as Record<string, unknown>[];
const tables = views.map((v, i) => ({
id: v.resolved,
label: inspectViewLabel(v.input, i),
input: rows(v.input),
resolved: rows(v.resolved),
}));
return { tables };
},
};
}