feat: auto-chart detection in SQL Explorer

Adds an ' Auto' button that analyzes query results and picks the
best chart type and column mapping:

- Date/time column + numeric → line chart (time series)
- Categorical + numeric → bar chart (categories)
- Two numeric columns → scatter plot
- Shows detected type and column names as a label

Click Auto, run any query, and it figures out the rest.
This commit is contained in:
Celes Renata
2026-04-16 05:52:41 +00:00
parent 7fefc65692
commit b43ad88f5d
+76 -11
View File
@@ -29,7 +29,7 @@ interface SchemaInfo {
tables: Array<{ name: string; columns: Array<{ name: string; type: string; nullable?: boolean }> }>;
}
type ChartType = 'none' | 'bar' | 'line' | 'scatter';
type ChartType = 'none' | 'auto' | 'bar' | 'line' | 'scatter';
export function SqlExplorerPage() {
const qc = useQueryClient();
@@ -85,14 +85,74 @@ export function SqlExplorerPage() {
}, []);
// Build chart data from result
// Auto-detect best chart type and columns when chartType === 'auto'
const autoChart = (() => {
if (!result || result.columns.length < 2 || result.row_count === 0) {
return { type: 'bar' as const, x: 0, y: 1 };
}
const cols = result.columns;
const rows = result.rows;
// Classify each column as numeric or categorical
const isNumeric = cols.map((_, ci) => {
const sample = rows.slice(0, 10);
const numCount = sample.filter((r) => {
const v = r[ci];
return v != null && !isNaN(parseFloat(String(v))) && String(v).trim() !== '';
}).length;
return numCount > sample.length * 0.7;
});
// Check if a column looks like a date/time
const isDateLike = cols.map((c, ci) => {
const name = c.name.toLowerCase();
if (name.includes('date') || name.includes('time') || name.includes('_at') || name.includes('created') || name.includes('generated')) return true;
const sample = String(rows[0]?.[ci] ?? '');
return /^\d{4}-\d{2}/.test(sample);
});
const numericCols = cols.map((_, i) => i).filter((i) => isNumeric[i]);
const categoricalCols = cols.map((_, i) => i).filter((i) => !isNumeric[i]);
const dateCols = cols.map((_, i) => i).filter((i) => isDateLike[i]);
// Heuristics:
// 1. Date column + numeric → line chart (time series)
// 2. Categorical + numeric → bar chart (categories)
// 3. Two numeric columns → scatter plot
// 4. Fallback → bar chart with first categorical as X, first numeric as Y
if (dateCols.length > 0 && numericCols.length > 0) {
const xIdx = dateCols[0];
const yIdx = numericCols.find((i) => i !== xIdx) ?? numericCols[0];
return { type: 'line' as const, x: xIdx, y: yIdx };
}
if (categoricalCols.length > 0 && numericCols.length > 0) {
return { type: 'bar' as const, x: categoricalCols[0], y: numericCols[0] };
}
if (numericCols.length >= 2) {
return { type: 'scatter' as const, x: numericCols[0], y: numericCols[1] };
}
// Fallback: first col as X, second as Y, bar chart
return { type: 'bar' as const, x: 0, y: Math.min(1, cols.length - 1) };
})();
// Resolve effective chart settings (auto overrides manual selections)
const effectiveChartType = chartType === 'auto' ? autoChart.type : chartType;
const effectiveXCol = chartType === 'auto' ? autoChart.x : xCol;
const effectiveYCol = chartType === 'auto' ? autoChart.y : yCol;
const chartData = result && result.columns.length >= 2
? result.rows.map((row) => {
const yRaw = row[yCol];
const yRaw = row[effectiveYCol];
const yNum = yRaw != null ? parseFloat(String(yRaw)) : NaN;
return {
x: row[xCol],
x: row[effectiveXCol],
y: isNaN(yNum) ? 0 : yNum,
label: String(row[xCol] ?? ''),
label: String(row[effectiveXCol] ?? ''),
};
})
: [];
@@ -249,16 +309,16 @@ export function SqlExplorerPage() {
<Card className="shrink-0">
<div className="mb-2 flex items-center gap-3">
<span className="text-xs text-gray-500">Chart:</span>
{(['none', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => (
{(['none', 'auto', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => (
<button
key={ct}
onClick={() => setChartType(ct)}
className={`rounded px-2 py-0.5 text-xs ${chartType === ct ? 'bg-brand-600 text-white' : 'text-gray-400 hover:bg-surface-800'}`}
>
{ct === 'none' ? 'Off' : ct}
{ct === 'none' ? 'Off' : ct === 'auto' ? '✨ Auto' : ct}
</button>
))}
{chartType !== 'none' && (
{chartType !== 'none' && chartType !== 'auto' && (
<>
<select value={xCol} onChange={(e) => setXCol(Number(e.target.value))} className="rounded border border-surface-700 bg-surface-900 px-1 py-0.5 text-xs text-gray-300" aria-label="X axis column">
{result.columns.map((c, i) => <option key={i} value={i}>{c.name}</option>)}
@@ -269,10 +329,15 @@ export function SqlExplorerPage() {
</select>
</>
)}
{chartType === 'auto' && result && (
<span className="text-[10px] text-gray-500">
{autoChart.type} · X: {result.columns[autoChart.x]?.name} · Y: {result.columns[autoChart.y]?.name}
</span>
)}
</div>
{chartType !== 'none' && chartData.length > 0 && (
<ResponsiveContainer width="100%" height={250}>
{chartType === 'bar' ? (
{effectiveChartType === 'bar' ? (
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="label" tick={{ fill: '#6b7280', fontSize: 10 }} />
@@ -280,7 +345,7 @@ export function SqlExplorerPage() {
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: 8 }} />
<Bar dataKey="y" fill="#3b82f6" />
</BarChart>
) : chartType === 'line' ? (
) : effectiveChartType === 'line' ? (
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="label" tick={{ fill: '#6b7280', fontSize: 10 }} />
@@ -291,8 +356,8 @@ export function SqlExplorerPage() {
) : (
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="x" tick={{ fill: '#6b7280', fontSize: 10 }} name={result.columns[xCol]?.name} />
<YAxis dataKey="y" tick={{ fill: '#6b7280', fontSize: 10 }} name={result.columns[yCol]?.name} />
<XAxis dataKey="x" tick={{ fill: '#6b7280', fontSize: 10 }} name={result.columns[effectiveXCol]?.name} />
<YAxis dataKey="y" tick={{ fill: '#6b7280', fontSize: 10 }} name={result.columns[effectiveYCol]?.name} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: 8 }} />
<Scatter data={chartData} fill="#3b82f6" />
</ScatterChart>