Data inspector: input/resolved rows below the chart, with a resizable divider

A collapsible Data panel under the Live Preview and Chart Builder charts shows the rows the chart actually uses, switching between Input (parsed source) and Resolved (post-transform) views read from the live Vega view. Collapsed by default; the open-state and a draggable height divider persist.

Rows come through a new RenderHandle.inspectData() accessor, so no component touches the view: core/result-data picks the most-upstream source and most-downstream result from the compiled dataflow, read lazily. The divider reuses the window-splitter pattern (horizontal variant).

Consolidations: a shared DataTable primitive replaces the inspector's and the builder's duplicate read-only tables; useResizeDrag merges the col/row drag-gesture twins.

Docs: spec 04/06 and arch 05/10 updated; the now-shipped exploration memo removed.
This commit is contained in:
2026-06-18 02:22:03 +03:00
parent 223646398e
commit efb5a9bbe0
35 changed files with 1210 additions and 184 deletions
+49
View File
@@ -12,6 +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 type { FontAsset } from '@core/font-asset';
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
@@ -43,6 +44,15 @@ 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. */
input: ReadonlyArray<Record<string, unknown>>;
/** Post-transform rows the chart draws — the output (equals `input` when the
* spec has no transforms). */
resolved: ReadonlyArray<Record<string, unknown>>;
}
export interface RenderHandle {
/** Finalize the underlying Vega view and clear the node. */
destroy(): void;
@@ -67,6 +77,21 @@ export interface RenderHandle {
* (fixed ones have no such handler), which is exactly right for Width/Height.
*/
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`.
*
* 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.
*/
inspectData(): InspectedData | null;
}
export interface RenderOptions {
@@ -235,8 +260,13 @@ export async function renderSpec(
tooltip: { disableDefaultStyle: true },
});
// Reads of a finalized view throw; `inspectData` checks this to no-op safely
// after destroy() (the inspector may read on a render that resolved late).
let finalized = false;
return {
destroy() {
finalized = true;
result.view.finalize();
node.replaceChildren();
},
@@ -265,5 +295,24 @@ export async function renderSpec(
// no-op after destroy().
if (typeof window !== 'undefined') window.dispatchEvent(new Event('resize'));
},
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) };
},
};
}