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
+50
View File
@@ -4,6 +4,38 @@ import type { UiTheme } from '@core/theme';
import type { ChartThemeSelection } from '@core/vega-themes';
import type { ModalName } from '../modals/types';
/** Default data-inspector height (px) when first expanded, before any drag. */
const DATA_INSPECTOR_DEFAULT_HEIGHT = 220;
/** Floor (px) for the inspector and for the chart above it while the divider drags. */
const DATA_INSPECTOR_MIN_HEIGHT = 96;
const CHART_MIN_HEIGHT = 120;
/**
* Clamp a desired inspector height so it keeps its own minimum and leaves the
* chart above it at least its minimum. `availForBoth` is the height the chart and
* inspector share (the preview body, minus the divider). Pure — the single place
* the divider constraint lives, unit-tested without a DOM. Mirrors PanesStore's
* `clampSideWidth`.
*/
export function clampInspectorHeight(desired: number, availForBoth: number): number {
const max = Math.max(DATA_INSPECTOR_MIN_HEIGHT, availForBoth - CHART_MIN_HEIGHT);
return Math.max(DATA_INSPECTOR_MIN_HEIGHT, Math.min(desired, max));
}
/**
* The inspector's 0100 position for the divider's `aria-valuenow` (WAI-ARIA APG →
* Window Splitter): 0 = inspector at its minimum, 100 = at its maximum (chart at
* its minimum). Null when the span has no range, so the caller omits the attribute.
*/
export function inspectorHeightValue(height: number, availForBoth: number): number | null {
const min = DATA_INSPECTOR_MIN_HEIGHT;
const max = Math.max(min, availForBoth - CHART_MIN_HEIGHT);
if (max <= min) return null;
const pct = ((height - min) / (max - min)) * 100;
return Math.round(Math.min(100, Math.max(0, pct)));
}
/**
* Centralized cross-cutting application state, as a Zustand store. Keep this
* lean — durable, feature-specific state (snippets, datasets, settings) lands
@@ -21,6 +53,16 @@ export interface AppState {
previewFitMode: FitMode;
/** Chart theme selection (spec §04); persisted to Settings as `chartTheme`. */
chartTheme: ChartThemeSelection;
/**
* Whether the live-preview data inspector is expanded (spec §04). A preview-pane
* preference like `previewFitMode`; persisted to Settings as `dataInspectorOpen`.
*/
dataInspectorOpen: boolean;
/**
* Height (px) of the expanded data inspector — set by its resizable divider, so
* the chart above keeps the rest of the pane. Persisted as `dataInspectorHeight`.
*/
dataInspectorHeight: number;
/** The currently open modal, or null. */
activeModal: ModalName | null;
@@ -31,6 +73,10 @@ export interface AppState {
setPreviewFitMode: (mode: FitMode) => void;
/** Set the chart theme — the Live Preview settings cluster's action. */
setChartTheme: (theme: ChartThemeSelection) => void;
/** Show/hide the data inspector — its disclosure toggle. */
setDataInspectorOpen: (open: boolean) => void;
/** Set the data inspector's height (caller clamps via `clampInspectorHeight`). */
setDataInspectorHeight: (height: number) => void;
/**
* Low-level modal setter — the single primitive that mutates `activeModal`.
* High-level open/close (snapshot for unsaved-change detection, URL sync,
@@ -44,11 +90,15 @@ export const useAppStore = create<AppState>((set) => ({
uiTheme: 'light',
previewFitMode: 'default',
chartTheme: 'astrolabe',
dataInspectorOpen: false,
dataInspectorHeight: DATA_INSPECTOR_DEFAULT_HEIGHT,
activeModal: null,
setTheme: (uiTheme) => set({ uiTheme }),
toggleTheme: () => set((s) => ({ uiTheme: s.uiTheme === 'dark' ? 'light' : 'dark' })),
setPreviewFitMode: (previewFitMode) => set({ previewFitMode }),
setChartTheme: (chartTheme) => set({ chartTheme }),
setDataInspectorOpen: (dataInspectorOpen) => set({ dataInspectorOpen }),
setDataInspectorHeight: (dataInspectorHeight) => set({ dataInspectorHeight }),
setActiveModal: (activeModal) => set({ activeModal }),
}));