/** * Data inspector — input vs. resolved rows (spec §04). * * Shows the chart's data with a toggle between two views of the same rendered * view: **Input** (the parsed source rows, before the spec's transforms) and * **Resolved** (the rows the chart draws, after filters / calculated fields / * aggregation). Seeing input → output side by side is how you answer "why is my * chart empty/wrong" — look at what the transforms did to the data. Both tables * come from the live Vega view via the renderer's `RenderHandle.inspectData()` * accessor, passed in as `getData` so this component never touches the view (the * embedding boundary, arch 05). * * `DataInspectorPanel` is the reusable shape (an APG disclosure, mirroring the * builder's source-rows preview); `DataInspector` binds it to the persisted * preview-pane open state for the live-preview pane. The data is read lazily — * only while expanded — because listing the view's datasets serializes them (see * `RenderHandle.inspectData`), so a collapsed inspector costs nothing. */ import { useMemo, useState } from 'react'; import type { InspectedData } from '../services/chart-renderer'; import { useAppStore } from '../stores/AppStore'; import { DataTable } from './DataTable'; import { SegmentedControl, type SegmentedOption } from './SegmentedControl'; import styles from './DataInspector.module.css'; /** Rows shown before truncating — matches the builder's source-preview cap (spec §06). */ const ROW_LIMIT = 50; type DataView = 'input' | 'resolved'; /** The two views, in input → output order (the natural reading direction). */ const VIEW_OPTIONS: ReadonlyArray> = [ // `title` doubles as the accessible name (WCAG 2.5.3 label-in-name; leads with // the visible label). { value: 'input', label: 'Input', title: 'Input — the source rows before the spec’s transforms' }, { value: 'resolved', label: 'Resolved', title: 'Resolved — the rows the chart draws, after its transforms', }, ]; interface DataInspectorPanelProps { /** Whether the panel is expanded. */ open: boolean; /** Toggle the expanded state. */ onToggle: (open: boolean) => void; /** * Reads the input + resolved rows from the live view, or null when there is no * chart to inspect. Must be stable across renders (the read is memoized on * `renderEpoch`). */ getData: () => InspectedData | null; /** Bumps whenever a render settles, so the open table re-reads the new rows. */ renderEpoch: number; /** * Explicit panel height (px) — the live-preview pane sets this from its * resizable divider so the table fills the allotted space. Omitted (the builder) * leaves the table at its default capped height. */ heightPx?: number; /** id of the panel root, for a splitter's `aria-controls` to point at. */ id?: string; } /** * The reusable disclosure: a toggle bar (APG disclosure — `aria-expanded`, * conditional content, matching the builder's source-rows preview) over an * Input | Resolved view switch and the chosen table. Expanded states, each named * (council: GOV.UK / NN/g — say what happened and the next step): * - no live chart → guidance to render one; * - the chosen view's table is empty → say which side and why (the resolved side's * transforms produced nothing; the input side's source is empty); * - rows → the grid. */ export function DataInspectorPanel({ open, onToggle, getData, renderEpoch, heightPx, id, }: DataInspectorPanelProps) { const [view, setView] = useState('resolved'); // Read both tables only while open; re-read when a render settles. `renderEpoch` // is an intentional refresh trigger — not read in the body (getData is stable and // always reads the latest view), so exhaustive-deps sees it as unnecessary. // eslint-disable-next-line react-hooks/exhaustive-deps const data = useMemo(() => (open ? getData() : null), [open, renderEpoch, getData]); const rows = data === null ? null : data[view]; return (
{open && ( <> {rows !== null && rows.length > 0 && ( {rows.length.toLocaleString()} {rows.length === 1 ? 'row' : 'rows'} )} )}
{open && (rows === null ? (

Render a chart to inspect its data.

) : rows.length === 0 ? (

{view === 'resolved' ? 'No rows — the spec’s filters or transforms left nothing to draw.' : 'The source data has no rows.'}

) : ( ))}
); } interface DataInspectorProps { /** Reads the input + resolved rows from the live preview's view (stable). */ getData: () => InspectedData | null; /** Bumps on each settled render so the open table refreshes. */ renderEpoch: number; /** Panel height (px) from the preview pane's resizable divider (only when open). */ heightPx?: number; /** id of the panel root, for the divider's `aria-controls`. */ id?: string; } /** The live-preview data inspector: the panel bound to the persisted open state. */ export function DataInspector({ getData, renderEpoch, heightPx, id }: DataInspectorProps) { const open = useAppStore((s) => s.dataInspectorOpen); const setOpen = useAppStore((s) => s.setDataInspectorOpen); return ( ); }