mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
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:
@@ -18,7 +18,7 @@
|
||||
* `LivePreview`, which is bound to the snippet editor's stores.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import type { VisualizationSpec } from 'vega-embed';
|
||||
import {
|
||||
@@ -60,7 +60,7 @@ import {
|
||||
type TimeUnit,
|
||||
} from '@core/chart-builder';
|
||||
import { referencedFields, validateExpression } from '@core/expr-validate';
|
||||
import { cellText, tabularRows } from '@core/dataset';
|
||||
import { tabularRows } from '@core/dataset';
|
||||
import type { ColumnType } from '@core/type-inference';
|
||||
import { DatasetNotFoundError, prepareSpecForRender } from '@core/rendering';
|
||||
import { chartConfigFor } from '@core/vega-themes';
|
||||
@@ -77,6 +77,8 @@ import {
|
||||
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
|
||||
import { Button } from './Button';
|
||||
import { ColorField } from './ColorField';
|
||||
import { DataInspectorPanel } from './DataInspector';
|
||||
import { DataTable } from './DataTable';
|
||||
import { IconButton } from './IconButton';
|
||||
import { SelectControl } from './SelectControl';
|
||||
import { Icon } from './Icon';
|
||||
@@ -959,43 +961,21 @@ function DataPreview() {
|
||||
|
||||
{open &&
|
||||
(rows ? (
|
||||
<div
|
||||
className={styles.previewTableWrap}
|
||||
tabIndex={0}
|
||||
role="group"
|
||||
aria-label="Data preview"
|
||||
>
|
||||
<table className={styles.previewTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
{baseColumns.columns.map((col) => (
|
||||
<th key={col} scope="col">
|
||||
<span className={styles.previewColName}>{col}</span>{' '}
|
||||
<span className={styles.previewColType}>{typeBadge(typeOf(col))}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, ri) => (
|
||||
<tr key={ri}>
|
||||
{baseColumns.columns.map((col) => (
|
||||
<td key={col}>{cellText(row[col])}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={baseColumns.columns}
|
||||
rows={rows}
|
||||
total={dataset.rowCount ?? undefined}
|
||||
ariaLabel="Data preview"
|
||||
renderHeader={(col) => (
|
||||
<>
|
||||
<span className={styles.previewColName}>{col}</span>{' '}
|
||||
<span className={styles.previewColType}>{typeBadge(typeOf(col))}</span>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<p className={styles.previewEmptyNote}>This dataset has no tabular rows to preview.</p>
|
||||
))}
|
||||
|
||||
{open && rows && dataset.rowCount != null && dataset.rowCount > rows.length && (
|
||||
<p className={styles.previewEmptyNote}>
|
||||
Showing the first {rows.length} of {dataset.rowCount.toLocaleString()} rows.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1083,6 +1063,12 @@ function BuilderPreview() {
|
||||
// Set when the chart resolves larger than the canvas backend can draw — a
|
||||
// physical render-size limit, distinct from the readability cardinality warnings.
|
||||
const [tooLarge, setTooLarge] = useState<{ heightPx: number; limitPx: number } | null>(null);
|
||||
// Resolved-data disclosure: open state (modal-local, not persisted) + an epoch
|
||||
// bumped on each settled render so the open table re-reads the post-transform rows
|
||||
// the builder's filters/calculated fields produce (the output, beside the source
|
||||
// rows in the config pane's preview — see DataInspector).
|
||||
const [dataOpen, setDataOpen] = useState(false);
|
||||
const [renderEpoch, setRenderEpoch] = useState(0);
|
||||
|
||||
const specText = useChartBuilderStore(selectBuilderSpecText);
|
||||
const valid = useChartBuilderStore(selectBuilderValid);
|
||||
@@ -1104,6 +1090,7 @@ function BuilderPreview() {
|
||||
handleRef.current = null;
|
||||
setError(null);
|
||||
setTooLarge(null);
|
||||
setRenderEpoch((e) => e + 1);
|
||||
return;
|
||||
}
|
||||
if (!node) return;
|
||||
@@ -1134,6 +1121,7 @@ function BuilderPreview() {
|
||||
handleRef.current = handle;
|
||||
setError(null);
|
||||
setTooLarge(null);
|
||||
setRenderEpoch((e) => e + 1);
|
||||
const t4 = performance.now();
|
||||
// The browser lays out/paints the (possibly huge) SVG after embed resolves;
|
||||
// a double rAF lands just after that paint, capturing the freeze the user
|
||||
@@ -1154,6 +1142,7 @@ function BuilderPreview() {
|
||||
);
|
||||
} catch (e) {
|
||||
if (mine !== generationRef.current) return;
|
||||
setRenderEpoch((epoch) => epoch + 1);
|
||||
if (e instanceof ChartTooLargeError) {
|
||||
// A physical render-size limit (canvas max dimension), not a data error.
|
||||
setTooLarge({ heightPx: e.heightPx, limitPx: e.limitPx });
|
||||
@@ -1188,6 +1177,11 @@ function BuilderPreview() {
|
||||
[],
|
||||
);
|
||||
|
||||
// The input + resolved rows the chart drew (the latter after the builder's
|
||||
// filters/calculated fields). Reads the live view through the handle; null when
|
||||
// no chart is up.
|
||||
const getInspectData = useCallback(() => handleRef.current?.inspectData() ?? null, []);
|
||||
|
||||
return (
|
||||
<div className={styles.previewPane}>
|
||||
{!valid && (
|
||||
@@ -1215,6 +1209,17 @@ function BuilderPreview() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Data inspector — input vs. resolved rows the chart drew (the latter after
|
||||
the builder's transforms). Only with a live chart, so it never duplicates
|
||||
the "map a channel" / error hints above. */}
|
||||
{valid && tooLarge === null && error === null && (
|
||||
<DataInspectorPanel
|
||||
open={dataOpen}
|
||||
onToggle={setDataOpen}
|
||||
getData={getInspectData}
|
||||
renderEpoch={renderEpoch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user