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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user