Chart theming: selectable chart theme + spec↔config merge/extract

This commit is contained in:
2026-06-12 16:48:48 +03:00
parent fe9d588103
commit 44a601affd
27 changed files with 1103 additions and 121 deletions
+31 -3
View File
@@ -20,7 +20,7 @@ import { useShallow } from 'zustand/react/shallow';
import type { VisualizationSpec } from 'vega-embed';
import type { FitMode } from '@core/rendering';
import { DatasetNotFoundError, prepareSpecForRender } from '@core/rendering';
import { chartConfigFor } from '@core/vega-themes';
import { CHART_THEME_OPTIONS, chartConfigForSelection } from '@core/vega-themes';
import { renderSpec, type RenderHandle } from '../services/chart-renderer';
import { useAppStore } from '../stores/AppStore';
import { useDatasetStore } from '../stores/DatasetStore';
@@ -29,6 +29,7 @@ import { selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { ChartExport } from './ChartExport';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { SelectControl } from './SelectControl';
import { RangeControl, SettingRow, SettingsPopover } from './SettingsPopover';
import styles from './LivePreview.module.css';
@@ -68,6 +69,30 @@ function FitControl() {
);
}
/**
* Chart theme picker (spec §04; docs/chart-theming-scope.md §4.2) — which config
* charts render (and export) with. A global preference, not per-snippet: a
* snippet's own `config` still overrides it property by property. Lives in the
* header, not inside PreviewSettings: SelectControl and SettingsPopover share
* the one-open-popover registry, so a select nested in the popover would close
* (and unmount) its own parent on open.
*/
// TODO: header placement/crowding parked for the batched council pass (docs/ux-second-pass.md).
function ChartThemeControl() {
const chartTheme = useAppStore((s) => s.chartTheme);
const setChartTheme = useAppStore((s) => s.setChartTheme);
return (
<SelectControl
id="preview-chart-theme"
label="Chart theme"
options={CHART_THEME_OPTIONS}
value={chartTheme}
onSelect={setChartTheme}
triggerTitle="Chart theme — how charts are styled when rendered and exported"
/>
);
}
/** Preview settings cluster (spec §07 → Performance), disclosed beside Fit. */
function PreviewSettings() {
const renderDebounce = useUserSettingsStore((s) => s.saved.performance.renderDebounce);
@@ -100,6 +125,7 @@ export function LivePreview() {
const shownText = useSnippetStore(selectShownText);
const fitMode = useAppStore((s) => s.previewFitMode);
const uiTheme = useAppStore((s) => s.uiTheme);
const chartTheme = useAppStore((s) => s.chartTheme);
// Datasets feed reference resolution (spec §04 step 1). Re-rendering on a
// dataset change keeps a referencing chart live as its data is edited.
const datasets = useDatasetStore(useShallow((s) => s.datasets));
@@ -212,7 +238,7 @@ export function LivePreview() {
try {
const prepared = prepareSpecForRender(parsed, { fitMode, datasets });
const config = chartConfigFor(uiTheme);
const config = chartConfigForSelection(chartTheme, uiTheme);
handleRef.current?.destroy();
handleRef.current = null;
const handle = await renderSpec(node, prepared as VisualizationSpec, config);
@@ -259,6 +285,7 @@ export function LivePreview() {
shownText,
fitMode,
uiTheme,
chartTheme,
datasets,
setError,
setBusy,
@@ -321,8 +348,9 @@ export function LivePreview() {
<div className={styles.preview}>
<div className={styles.header}>
<FitControl />
{/* Right cluster: export this chart, then the preview settings gear. */}
{/* Right cluster: chart theme, export this chart, then the settings gear. */}
<div className={styles.headerEnd}>
<ChartThemeControl />
<ChartExport chartReady={chartReady} getImageUrl={getImageUrl} />
<PreviewSettings />
</div>