From b43ad88f5df611fd49b713b4c07df2b77abd497d Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Thu, 16 Apr 2026 05:52:41 +0000 Subject: [PATCH] feat: auto-chart detection in SQL Explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/pages/SqlExplorer.tsx | 87 ++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/SqlExplorer.tsx b/frontend/src/pages/SqlExplorer.tsx index 553ea99..b76e8ba 100644 --- a/frontend/src/pages/SqlExplorer.tsx +++ b/frontend/src/pages/SqlExplorer.tsx @@ -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() {
Chart: - {(['none', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => ( + {(['none', 'auto', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => ( ))} - {chartType !== 'none' && ( + {chartType !== 'none' && chartType !== 'auto' && ( <> )} + {chartType === 'auto' && result && ( + + {autoChart.type} · X: {result.columns[autoChart.x]?.name} · Y: {result.columns[autoChart.y]?.name} + + )}
{chartType !== 'none' && chartData.length > 0 && ( - {chartType === 'bar' ? ( + {effectiveChartType === 'bar' ? ( @@ -280,7 +345,7 @@ export function SqlExplorerPage() { - ) : chartType === 'line' ? ( + ) : effectiveChartType === 'line' ? ( @@ -291,8 +356,8 @@ export function SqlExplorerPage() { ) : ( - - + +