feat: auto-bind chart fields based on dataset columns

When creating a chart, automatically assign first text column to X axis
and first number column to Y axis (varies by chart type).
Charts now show data immediately instead of blank.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hailin 2026-04-05 01:57:54 -07:00
parent ccbe63f9e4
commit fe6cf014dc
1 changed files with 66 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import { addChart, setActiveChart } from '@/adapters/state/redux/chartSlice';
import { addLayoutItem } from '@/adapters/state/redux/layoutSlice';
import { type ChartType } from '@/domain';
import { type ChartInstance } from '@/domain/entities/ChartInstance';
import { type FieldBinding } from '@/domain/entities/FieldBinding';
function mapBackendChart(raw: any): ChartInstance {
return {
@ -31,9 +32,58 @@ function mapBackendChart(raw: any): ChartInstance {
};
}
/**
* Auto-generate default bindings based on chart type and available columns.
*/
function autoBindings(
chartType: string,
columns: { name: string; type: string }[],
): FieldBinding[] {
const textCols = columns.filter((c) => c.type === 'text' || c.type === 'geo');
const numCols = columns.filter((c) => c.type === 'number' || c.type === 'percentage');
const dateCols = columns.filter((c) => c.type === 'date');
const bindings: FieldBinding[] = [];
switch (chartType) {
case 'pie':
case 'donut':
case 'wordcloud':
case 'radar':
if (textCols[0]) bindings.push({ axis: 'label', columnName: textCols[0].name });
if (numCols[0]) bindings.push({ axis: 'value', columnName: numCols[0].name });
break;
case 'map':
const geoCols = columns.filter((c) => c.type === 'geo');
if (geoCols[0]) bindings.push({ axis: 'geo', columnName: geoCols[0].name });
else if (textCols[0]) bindings.push({ axis: 'geo', columnName: textCols[0].name });
if (numCols[0]) bindings.push({ axis: 'value', columnName: numCols[0].name });
break;
case 'kpi':
if (numCols[0]) bindings.push({ axis: 'value', columnName: numCols[0].name });
if (textCols[0]) bindings.push({ axis: 'label', columnName: textCols[0].name });
break;
case 'scatter':
case 'boston-matrix':
if (numCols[0]) bindings.push({ axis: 'x', columnName: numCols[0].name });
if (numCols[1]) bindings.push({ axis: 'y', columnName: numCols[1].name });
break;
default: // bar, line, area, grouped-bar, stacked-bar, horizontal-bar, combo, heatmap, data-table
if (dateCols[0]) bindings.push({ axis: 'x', columnName: dateCols[0].name });
else if (textCols[0]) bindings.push({ axis: 'x', columnName: textCols[0].name });
if (numCols[0]) bindings.push({ axis: 'y', columnName: numCols[0].name });
break;
}
return bindings;
}
export function useCreateChart() {
const dispatch = useAppDispatch();
const activeDataSetId = useAppSelector((s) => s.data.activeDataSetId);
const activeDataSet = useAppSelector((s) =>
s.data.dataSets.find((ds) => ds.id === s.data.activeDataSetId),
);
const layoutCount = useAppSelector((s) => s.layout.layouts.length);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -48,12 +98,27 @@ export function useCreateChart() {
setLoading(true);
setError(null);
try {
// Auto-generate bindings from dataset columns
const columns = (activeDataSet?.columns ?? []).map((c) => ({
name: c.name,
type: c.type,
}));
const defaultBindings = autoBindings(chartType ?? 'bar', columns);
const rawChart = await chartServiceClient.createChart(
activeDataSetId,
chartType ?? 'bar',
defaultBindings.map((b) => ({
axis: b.axis,
column_name: b.columnName,
aggregation: b.aggregation ?? null,
})),
);
const chart = mapBackendChart(rawChart);
// Override bindings with our auto-generated ones (backend may not have them)
chart.bindings = defaultBindings;
dispatch(addChart(chart));
dispatch(setActiveChart(chart.id));
@ -76,7 +141,7 @@ export function useCreateChart() {
setLoading(false);
}
},
[dispatch, activeDataSetId, layoutCount],
[dispatch, activeDataSetId, activeDataSet, layoutCount],
);
return { handleCreate, activeDataSetId, loading, error };