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
+31
View File
@@ -37,6 +37,8 @@ import { usePreviewStore } from '../stores/PreviewStore';
import { selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { ChartExport } from './ChartExport';
import { DataInspector } from './DataInspector';
import { InspectorSplitHandle } from './InspectorSplitHandle';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { SelectControl, type SelectControlOption } from './SelectControl';
import { RangeControl, SettingRow, SettingsPopover } from './SettingsPopover';
@@ -176,6 +178,9 @@ export function LivePreview() {
const fitMode = useAppStore((s) => s.previewFitMode);
const uiTheme = useAppStore((s) => s.uiTheme);
const chartTheme = useAppStore((s) => s.chartTheme);
// Data inspector: open-state gates the divider + table; its height is the divider's.
const inspectorOpen = useAppStore((s) => s.dataInspectorOpen);
const inspectorHeight = useAppStore((s) => s.dataInspectorHeight);
// Custom themes feed `custom:<id>` selection resolution; re-rendering on a
// change keeps the chart live while a selected theme is edited in the builder.
const customThemes = useCustomThemeStore((s) => s.themes);
@@ -201,6 +206,11 @@ export function LivePreview() {
// a ref change alone wouldn't re-render. Set true on a successful render, false
// on clear/error/unmount.
const [chartReady, setChartReady] = useState(false);
// Bumped whenever a render settles and changes the live view (success, clear,
// error) so the data inspector re-reads the resolved rows. A monotonic counter,
// not `chartReady` — consecutive successful renders keep `chartReady` true, but
// each one is new data the inspector must pick up.
const [renderEpoch, setRenderEpoch] = useState(0);
// Busy-indication timer ref: if a render exceeds ~1s we surface a non-blocking
// overlay (arch §10.2 NN/g: >1s owes a busy indication; <1s shows nothing to
@@ -284,6 +294,7 @@ export function LivePreview() {
handleRef.current?.destroy();
handleRef.current = null;
setChartReady(false);
setRenderEpoch((e) => e + 1);
setError(null);
clearBusy();
return;
@@ -306,11 +317,13 @@ export function LivePreview() {
handleRef.current = handle;
configRef.current = config;
setChartReady(true);
setRenderEpoch((e) => e + 1);
setError(null);
clearBusy();
} catch (e) {
if (mine === generationRef.current) {
setChartReady(false);
setRenderEpoch((e) => e + 1);
clearBusy();
// A missing dataset reference is not a JSON/spec problem, so it gets a
// tailored, fixable message instead of the generic syntax hint (council:
@@ -379,6 +392,11 @@ export function LivePreview() {
[],
);
// The input + resolved rows for the data inspector — reads the live view through
// the handle (null when no chart is up). Stable identity; the inspector re-reads
// on `renderEpoch`, so this need not depend on it (it always reads the latest handle).
const getInspectData = useCallback(() => handleRef.current?.inspectData() ?? null, []);
// Re-fit the chart when its container resizes (e.g. a pane drag). Vega doesn't
// observe the element, so we do: one observer on the stable host node for the
// component's life. Only responsive fit modes depend on container size;
@@ -450,6 +468,19 @@ export function LivePreview() {
</div>
)}
</div>
{/* Data inspector — input vs. resolved rows, stacked below the chart (a
collapsed disclosure by default); reads the live view through getInspectData.
When open, a draggable divider sizes it (chart absorbs the change) and the
stored height fills the table; collapsed, it's just the disclosure bar. */}
{inspectorOpen && (
<InspectorSplitHandle label="Resize data inspector" controls="preview-data-inspector" />
)}
<DataInspector
id="preview-data-inspector"
getData={getInspectData}
renderEpoch={renderEpoch}
heightPx={inspectorOpen ? inspectorHeight : undefined}
/>
</div>
);
}