Chart builder: discoverable entry points — Build Chart door, dataset picker, no-datasets state

This commit is contained in:
2026-06-12 21:46:25 +03:00
parent dbb4522d78
commit ca70b3e491
22 changed files with 625 additions and 75 deletions
+66 -6
View File
@@ -61,7 +61,7 @@ import type { ColumnType } from '@core/type-inference';
import { DatasetNotFoundError, prepareSpecForRender } from '@core/rendering';
import { chartConfigFor } from '@core/vega-themes';
import { ChartTooLargeError, renderSpec, type RenderHandle } from '../services/chart-renderer';
import { closeModal } from '../modals/ModalCoordinator';
import { closeModal, openModal } from '../modals/ModalCoordinator';
import { useAppStore } from '../stores/AppStore';
import { useDatasetStore } from '../stores/DatasetStore';
import {
@@ -1264,9 +1264,62 @@ function ChartProps() {
);
}
/**
* The dataset picker at the top of the config pane (spec §06 → Dataset picker):
* which data the chart builds from is itself a builder choice, so the builder can
* open without a preselected dataset and the data can be switched without leaving.
* Switching re-derives smart defaults while the config is untouched, and rebases
* (keeps chart-level intent, sheds bindings to missing columns) once it isn't.
*/
function DatasetPicker({ datasetId }: { datasetId: number | null }) {
const datasets = useDatasetStore(useShallow((s) => s.datasets));
const switchDataset = useChartBuilderStore((s) => s.switchDataset);
return (
<div className={styles.datasetRow}>
<span className={styles.fieldLabel}>Dataset</span>
<SelectControl
id="cb-dataset"
label="Dataset to build from"
heading="Dataset"
options={datasets.map((d) => ({ value: String(d.id), label: d.name }))}
value={datasetId !== null ? String(datasetId) : undefined}
onSelect={(v) => switchDataset(Number(v))}
triggerContent={datasetId === null ? 'Choose a dataset…' : undefined}
/>
</div>
);
}
/**
* The no-datasets state (spec §06 → Opening; Carbon no-data empty state): says
* what the builder does and offers the one next step — never a dead end. The
* action swaps this modal for the Datasets manager opened on its create form.
*/
function NoDatasets() {
return (
<div className={styles.emptyState}>
<h3 className={styles.emptyTitle}>No datasets yet</h3>
<p className={styles.emptyBody}>
The Chart Builder turns a saved dataset into a chart pick columns, watch the chart take
shape, and save it as a snippet. Add a dataset to start building.
</p>
<button
type="button"
className={`${styles.action} ${styles.primary}`}
onClick={() => {
openModal('datasets');
useDatasetStore.getState().startCreate();
}}
>
Add a dataset
</button>
</div>
);
}
export function ChartBuilderModal() {
const datasetId = useChartBuilderStore((s) => s.datasetId);
const datasetName = useChartBuilderStore((s) => s.config.datasetName);
const datasetCount = useDatasetStore((s) => s.datasets.length);
const mark = useChartBuilderStore((s) => s.config.mark);
const sort = useChartBuilderStore((s) => s.config.sort);
const stack = useChartBuilderStore((s) => s.config.stack);
@@ -1317,7 +1370,16 @@ export function ChartBuilderModal() {
}, [warnings]);
if (datasetId === null) {
return <p className={styles.muted}>No dataset loaded. Open this from a dataset in Datasets.</p>;
if (datasetCount === 0) return <NoDatasets />;
// Datasets exist but none is loaded (init auto-picks, so this is a fallback
// for a builder restored into an odd state): offer the choice directly.
return (
<div className={styles.emptyState}>
<h3 className={styles.emptyTitle}>Choose a dataset</h3>
<p className={styles.emptyBody}>Pick the dataset to build a chart from.</p>
<DatasetPicker datasetId={null} />
</div>
);
}
return (
@@ -1326,9 +1388,7 @@ export function ChartBuilderModal() {
<div className="visually-hidden" role="status" aria-live="polite">
{fixAnnouncement}
</div>
<p className={styles.datasetName}>
Building from <strong>{datasetName}</strong>
</p>
<DatasetPicker datasetId={datasetId} />
<DataSection />