import React, { useState, useEffect, useMemo } from "react";
// Helper checking if >80% of values are numbers
export function isNumeric(values) {
if (!values || values.length === 0) return false;
const numCount = values.filter(v => typeof v === "number" && !isNaN(v)).length;
return (numCount / values.length) > 0.8;
}
// Helper checking if >80% of values match YYYY-MM-DD format
export function looksLikeDate(values) {
if (!values || values.length === 0) return false;
const dateRegex = /^\d{4}-\d{2}-\d{2}/;
const dateCount = values.filter(v => typeof v === "string" && dateRegex.test(v)).length;
return (dateCount / values.length) > 0.8;
}
export default function ChartView({ result }) {
const [mounted, setMounted] = useState(false);
const [hoveredIdx, setHoveredIdx] = useState(null);
useEffect(() => {
const timer = setTimeout(() => setMounted(true), 50);
return () => clearTimeout(timer);
}, [result]);
const chartMeta = useMemo(() => {
if (!result || !result.results || !result.results.columns || !result.results.rows || result.results.rows.length === 0) {
return null;
}
const columns = result.results.columns;
const rows = result.results.rows;
let chartType = null;
let xIdx = 0;
let yIdx = 1;
// Detect column roles
if (columns.length >= 2) {
const col1Values = rows.map(r => r[0]);
const col2Values = rows.map(r => r[1]);
const col1IsNumeric = isNumeric(col1Values);
const col2IsNumeric = isNumeric(col2Values);
const col1IsDate = looksLikeDate(col1Values);
if (!col1IsNumeric && col2IsNumeric) {
xIdx = 0;
yIdx = 1;
if (col1IsDate) {
chartType = "LINE";
} else if (rows.length <= 6) {
chartType = "PIE";
} else {
chartType = "BAR";
}
} else if (col1IsNumeric && !col2IsNumeric) {
xIdx = 1;
yIdx = 0;
const col2IsDate = looksLikeDate(col2Values);
if (col2IsDate) {
chartType = "LINE";
} else if (rows.length <= 6) {
chartType = "PIE";
} else {
chartType = "BAR";
}
} else if (columns.length >= 3) {
// 3+ columns. Look for first text/date and first numeric
let foundX = -1;
let foundY = -1;
for (let i = 0; i < columns.length; i++) {
const vals = rows.map(r => r[i]);
if (isNumeric(vals)) {
if (foundY === -1) foundY = i;
} else {
if (foundX === -1) foundX = i;
}
}
if (foundX !== -1 && foundY !== -1) {
xIdx = foundX;
yIdx = foundY;
chartType = "BAR";
}
}
}
if (!chartType) return null;
// Format data points (limit to first 20 rows for layout readability)
const points = rows.slice(0, 20).map((row, idx) => {
const rawX = row[xIdx];
const rawY = row[yIdx];
return {
xVal: rawX === null || rawX === undefined ? "NULL" : String(rawX),
yVal: Number(rawY || 0),
originalRowIdx: idx
};
});
return {
chartType,
points,
xLabel: columns[xIdx],
yLabel: columns[yIdx],
totalCount: rows.length
};
}, [result]);
if (!chartMeta) {
return (
Chart not available for this query type.
View data in the Table tab.
);
}
const { chartType, points, xLabel, yLabel, totalCount } = chartMeta;
// Format large numbers cleanly
const formatNumber = (num) => {
if (Math.abs(num) >= 1_000_000) return (num / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
if (Math.abs(num) >= 1_000) return (num / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
if (num % 1 !== 0) return num.toFixed(2);
return num.toString();
};
// --- 1. BAR CHART RENDERER ---
if (chartType === "BAR") {
const svgWidth = 600;
const svgHeight = 350;
const padding = { top: 40, right: 30, bottom: 60, left: 65 };
const graphWidth = svgWidth - padding.left - padding.right;
const graphHeight = svgHeight - padding.top - padding.bottom;
const yValues = points.map(p => p.yVal);
const maxY = Math.max(...yValues, 0);
const minY = Math.min(...yValues, 0);
const upperLimit = maxY === 0 ? 10 : maxY * 1.15; // 15% top padding
// Grid ticks (5 marks)
const tickCount = 4;
const ticks = Array.from({ length: tickCount + 1 }).map((_, i) => {
const val = (upperLimit / tickCount) * i;
const y = padding.top + graphHeight - (val / upperLimit) * graphHeight;
return { val, y };
});
const colWidth = graphWidth / points.length;
const barPadding = 0.35;
const barWidth = colWidth * (1 - barPadding);
const barOffset = colWidth * (barPadding / 2);
return (
);
}
// --- 2. LINE CHART RENDERER ---
if (chartType === "LINE") {
const svgWidth = 600;
const svgHeight = 350;
const padding = { top: 40, right: 40, bottom: 50, left: 65 };
const graphWidth = svgWidth - padding.left - padding.right;
const graphHeight = svgHeight - padding.top - padding.bottom;
const yValues = points.map(p => p.yVal);
const maxY = Math.max(...yValues, 0);
const upperLimit = maxY === 0 ? 10 : maxY * 1.15;
// Grid ticks (5 marks)
const tickCount = 4;
const ticks = Array.from({ length: tickCount + 1 }).map((_, i) => {
const val = (upperLimit / tickCount) * i;
const y = padding.top + graphHeight - (val / upperLimit) * graphHeight;
return { val, y };
});
// Compute coordinates
const segmentWidth = graphWidth / Math.max(points.length - 1, 1);
const coords = points.map((pt, idx) => {
const x = padding.left + idx * segmentWidth;
const y = padding.top + graphHeight - (pt.yVal / upperLimit) * graphHeight;
return { x, y, pt, idx };
});
// Generate polyline string points
const polylinePoints = coords.map(c => `${c.x},${c.y}`).join(" ");
// Generate gradient fill area path points
const areaPath = coords.length > 0
? `M ${coords[0].x},${padding.top + graphHeight} ` +
coords.map(c => `L ${c.x},${c.y}`).join(" ") +
` L ${coords[coords.length - 1].x},${padding.top + graphHeight} Z`
: "";
return (
);
}
// --- 3. PIE CHART RENDERER ---
if (chartType === "PIE") {
const svgWidth = 400;
const svgHeight = 300;
const cx = 130;
const cy = 150;
const r = 95;
const colors = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"];
const yValues = points.map(p => p.yVal);
const sum = yValues.reduce((a, b) => a + b, 0);
let currentAngle = -Math.PI / 2; // Start at 12 o'clock
const slices = points.map((pt, idx) => {
const val = pt.yVal;
const pct = sum === 0 ? 0 : val / sum;
const angle = pct * 2 * Math.PI;
const startAngle = currentAngle;
const endAngle = currentAngle + angle;
currentAngle = endAngle;
const midAngle = (startAngle + endAngle) / 2;
return {
pt,
pct,
startAngle,
endAngle,
midAngle,
color: colors[idx % colors.length],
idx
};
});
return (
);
}
return null;
}