MSF
feat: Initialize Quadrant Chart Pro project structure
4bbaf1c
import React, { useState, useMemo, useCallback, useRef } from 'react';
import * as XLSX from 'xlsx';
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
ZAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ReferenceLine,
LabelList,
Legend,
Text,
Cell
} from 'recharts';
import { Upload, Settings2, BarChart2, FileSpreadsheet, Info, Download, Image as ImageIcon, X, Palette, Trash2, Check, ChevronDown, ChevronUp, Copy } from 'lucide-react';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { toPng, toBlob } from 'html-to-image';
import { ChartConfig, DataPoint, PointOverride } from './types';
// Utility for tailwind classes
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export default function App() {
const [data, setData] = useState<DataPoint[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [config, setConfig] = useState<ChartConfig>({
xAxisKey: '',
yAxisKey: '',
labelKey: '',
categoryKey: '',
groupByKeys: [],
sizeKey: '',
title: 'Campaign Performance Quadrant',
});
const [pointOverrides, setPointOverrides] = useState<Record<string, PointOverride>>({});
const [selectedPoint, setSelectedPoint] = useState<{ id: string, label: string } | null>(null);
const [isDragging, setIsDragging] = useState(false);
const chartRef = useRef<HTMLDivElement>(null);
const parseNumeric = (val: any): number => {
if (val === null || val === undefined || val === '') return NaN;
if (typeof val === 'number') return val;
const str = String(val).trim();
if (str.endsWith('%')) {
return parseFloat(str.replace('%', '')) / 100;
}
const parsed = parseFloat(str);
return isNaN(parsed) ? NaN : parsed;
};
const handleFileUpload = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const bstr = e.target?.result;
const wb = XLSX.read(bstr, { type: 'binary' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
// Use defval: "" to ensure all keys are present even if empty in some rows
const jsonData = XLSX.utils.sheet_to_json(ws, { defval: "" }) as DataPoint[];
if (jsonData.length > 0) {
// Add unique ID to each row
const dataWithIds = jsonData.map((row, idx) => ({
...row,
__id: `row-${idx}-${Date.now()}`
}));
// Get all keys from the sheet range if possible, or from the first row
const cols = Object.keys(jsonData[0]);
setColumns(cols);
setData(dataWithIds);
const numericCols = cols.filter(c => !isNaN(parseNumeric(jsonData[0][c])));
setConfig(prev => ({
...prev,
xAxisKey: numericCols.find(c => c.toUpperCase().includes('CPM')) || numericCols[0] || cols[0],
yAxisKey: numericCols.find(c => c.toUpperCase().includes('CTR')) || numericCols[1] || cols[1],
labelKey: cols.find(c => /name|campaign|product/i.test(c)) || cols[0],
categoryKey: cols.find(c => /type|category/i.test(c)) || '',
groupByKeys: [],
sizeKey: numericCols.find(c => /spend|cost|budget|impression/i.test(c)) || '',
}));
setPointOverrides({});
}
};
reader.readAsBinaryString(file);
};
const onDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls') || file.name.endsWith('.csv'))) {
handleFileUpload(file);
}
}, []);
// Removed old stats useMemo as it is now calculated per chart
const chartData = useMemo(() => {
if (!data.length || !config.xAxisKey || !config.yAxisKey) return [];
// Keep the filtering logic if needed, but the user didn't ask to change it
const campaignsToRemove = ['Gender holiday promotion', 'AIOT March promotion'];
return data
.filter(d => {
const label = String(d[config.labelKey] || '').trim().toLowerCase();
const isHidden = pointOverrides[d.__id]?.hidden;
// Filter out abnormal points (CPC, CPV, ER = empty)
const cpc = String(d['CPC'] || '').trim();
const cpv = String(d['CPV'] || '').trim();
const er = String(d['ER'] || '').trim();
const isAbnormal = (d.hasOwnProperty('CPC') && cpc === '') ||
(d.hasOwnProperty('CPV') && cpv === '') ||
(d.hasOwnProperty('ER') && er === '');
return !campaignsToRemove.some(c => c.toLowerCase() === label) && !isHidden && !isAbnormal;
})
.map(d => ({
...d,
x: parseNumeric(d[config.xAxisKey]),
y: parseNumeric(d[config.yAxisKey]),
z: config.sizeKey ? parseNumeric(d[config.sizeKey]) : 1,
color: pointOverrides[d.__id]?.color || '#6366f1'
}))
.filter(d => !isNaN(d.x) && !isNaN(d.y));
}, [data, config.xAxisKey, config.yAxisKey, config.labelKey, pointOverrides]);
const multiChartData = useMemo(() => {
if (!chartData.length) return [];
if (config.groupByKeys.length === 0) {
return [{ name: 'All Data', points: chartData }];
}
const groups: Record<string, DataPoint[]> = {};
chartData.forEach(d => {
const groupName = config.groupByKeys
.map(key => `${key}: ${d[key] || 'Other'}`)
.join(' | ');
if (!groups[groupName]) groups[groupName] = [];
groups[groupName].push(d);
});
return Object.entries(groups).map(([name, points]) => ({ name, points }));
}, [chartData, config.groupByKeys]);
const calculateStats = (points: DataPoint[]) => {
if (!points.length) return null;
const xValues = points.map(d => d.x).filter(v => !isNaN(v));
const yValues = points.map(d => d.y).filter(v => !isNaN(v));
if (xValues.length === 0 || yValues.length === 0) return null;
const xMean = xValues.reduce((a, b) => a + b, 0) / xValues.length;
const yMean = yValues.reduce((a, b) => a + b, 0) / yValues.length;
const xMin = Math.min(...xValues);
const xMax = Math.max(...xValues);
const yMin = Math.min(...yValues);
const yMax = Math.max(...yValues);
return { xMean, yMean, xMin, xMax, yMin, yMax };
};
const colors = ['#3b82f6', '#f97316', '#10b981', '#8b5cf6', '#ec4899', '#f59e0b'];
const shapes = ['circle', 'cross', 'diamond', 'square', 'star', 'triangle'];
const formatValue = (val: any) => {
const num = Number(val);
if (isNaN(num)) return val;
return num <= 1 && num >= 0 ? `${(num * 100).toFixed(2)}%` : num.toFixed(2);
};
const [copyingId, setCopyingId] = useState<string | null>(null);
const copyChart = useCallback((elementId: string) => {
const el = document.getElementById(elementId);
if (!el) return;
setCopyingId(elementId);
toBlob(el, { backgroundColor: '#ffffff' })
.then((blob) => {
if (blob) {
const item = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([item]);
setTimeout(() => setCopyingId(null), 2000);
}
})
.catch((err) => {
console.error('Failed to copy chart', err);
setCopyingId(null);
});
}, []);
const downloadPng = useCallback(() => {
if (chartRef.current === null) return;
toPng(chartRef.current, { cacheBust: true, backgroundColor: '#ffffff' })
.then((dataUrl) => {
const link = document.createElement('a');
link.download = `${config.title || 'quadrant-chart'}.png`;
link.href = dataUrl;
link.click();
})
.catch((err) => {
console.error('oops, something went wrong!', err);
});
}, [config.title]);
const renderCustomShape = (props: any) => {
const { cx, cy, fill, shapeIndex } = props;
const shape = shapes[shapeIndex % shapes.length];
switch (shape) {
case 'cross':
return (
<g transform={`translate(${cx - 6}, ${cy - 6})`}>
<path d="M0 0 L12 12 M12 0 L0 12" stroke={fill} strokeWidth={3} fill="none" />
</g>
);
case 'diamond':
return <path d={`M${cx},${cy - 8} L${cx + 8},${cy} L${cx},${cy + 8} L${cx - 8},${cy} Z`} fill={fill} />;
case 'square':
return <rect x={cx - 6} y={cy - 6} width={12} height={12} fill={fill} />;
default:
return <circle cx={cx} cy={cy} r={6} fill={fill} />;
}
};
return (
<div className="min-h-screen bg-[#f8fafc] text-slate-900 font-sans pb-20">
<header className="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-3">
<div className="bg-indigo-600 p-2 rounded-lg">
<BarChart2 className="text-white w-6 h-6" />
</div>
<h1 className="text-xl font-bold tracking-tight">Quadrant Chart Pro</h1>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => {
const sample = [
{ Name: 'Redmi note 14 launch', CPM: 0.71, CTR: 0.80, Type: 'Launch' },
{ Name: 'Xiaomi 15 launch', CPM: 0.67, CTR: 0.41, Type: 'Launch' },
{ Name: 'Redmi 15/15C launch', CPM: 0.54, CTR: 0.43, Type: 'Launch' },
{ Name: 'Redmi note 14XFF', CPM: 0.45, CTR: 0.48, Type: 'Promotion' },
{ Name: 'Xiaomi Pad 7 Pro', CPM: 0.34, CTR: 0.66, Type: 'Launch' },
{ Name: 'REDMI Pad 2 launch', CPM: 0.30, CTR: 0.56, Type: 'Launch' },
{ Name: 'Smartphone NY Promo', CPM: 0.26, CTR: 0.37, Type: 'Promotion' },
{ Name: 'TV March promotion', CPM: 0.27, CTR: 0.09, Type: 'Promotion' },
{ Name: 'Xiaomi 15T series launch', CPM: 0.19, CTR: 0.31, Type: 'Launch' },
{ Name: 'Redmi Pad 2 Pro launch', CPM: 0.15, CTR: 0.38, Type: 'Launch' },
{ Name: 'Redmi A5 launch', CPM: 0.19, CTR: 0.27, Type: 'Launch' },
{ Name: 'Xiaomi Robot Vacuum 5 launch', CPM: 0.11, CTR: 0.15, Type: 'Launch' },
{ Name: 'Band10 launch', CPM: 0.12, CTR: 0.05, Type: 'Launch' },
];
setColumns(Object.keys(sample[0]));
setData(sample);
setConfig({
xAxisKey: 'CPM',
yAxisKey: 'CTR',
labelKey: 'Name',
categoryKey: 'Type',
groupByKeys: [],
sizeKey: 'CPM', // Just for example
title: 'Campaign Performance Quadrant (CPM vs CTR)',
});
}}
className="text-sm font-medium text-indigo-600 hover:text-indigo-700"
>
Load Sample Data
</button>
<button
onClick={downloadPng}
disabled={multiChartData.length === 0}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<ImageIcon size={18} />
Save as PNG
</button>
</div>
</header>
<main className="max-w-[1600px] mx-auto p-6 grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-1 space-y-6">
<section className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
<FileSpreadsheet size={16} />
Data Source
</h2>
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={onDrop}
className={cn(
"border-2 border-dashed rounded-xl p-8 text-center transition-all cursor-pointer",
isDragging ? "border-indigo-500 bg-indigo-50" : "border-slate-200 hover:border-slate-300",
data.length > 0 ? "py-4" : "py-12"
)}
onClick={() => document.getElementById('file-upload')?.click()}
>
<input id="file-upload" type="file" className="hidden" accept=".xlsx,.xls,.csv" onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0])} />
<Upload className={cn("mx-auto mb-3 text-slate-400", data.length > 0 ? "w-6 h-6" : "w-10 h-10")} />
<p className="text-sm font-medium text-slate-600">{data.length > 0 ? "Change File" : "Drop Excel file here"}</p>
</div>
</section>
<section className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
<Settings2 size={16} />
Configuration
</h2>
<div className="space-y-4">
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">Chart Title</label>
<input type="text" value={config.title} onChange={(e) => setConfig(prev => ({ ...prev, title: e.target.value }))} className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none" />
</div>
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">X-Axis</label>
<select value={config.xAxisKey} onChange={(e) => setConfig(prev => ({ ...prev, xAxisKey: e.target.value }))} className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm outline-none">
{columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">Y-Axis</label>
<select value={config.yAxisKey} onChange={(e) => setConfig(prev => ({ ...prev, yAxisKey: e.target.value }))} className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm outline-none">
{columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">Label</label>
<select value={config.labelKey} onChange={(e) => setConfig(prev => ({ ...prev, labelKey: e.target.value }))} className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm outline-none">
{columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">Point Size (Weight)</label>
<select value={config.sizeKey} onChange={(e) => setConfig(prev => ({ ...prev, sizeKey: e.target.value }))} className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm outline-none">
<option value="">Uniform Size</option>
{columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-700 mb-1.5">Group By (Separate Charts)</label>
<div className="space-y-1 max-h-40 overflow-y-auto p-2 bg-slate-50 border border-slate-200 rounded-lg">
{columns.map(col => (
<label key={col} className="flex items-center gap-2 text-sm cursor-pointer hover:bg-slate-100 p-1 rounded">
<input
type="checkbox"
checked={config.groupByKeys.includes(col)}
onChange={(e) => {
const newKeys = e.target.checked
? [...config.groupByKeys, col]
: config.groupByKeys.filter(k => k !== col);
setConfig(prev => ({ ...prev, groupByKeys: newKeys }));
}}
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
/>
<span className="truncate">{col}</span>
</label>
))}
{columns.length === 0 && <p className="text-xs text-slate-400 italic">Upload data first</p>}
</div>
</div>
</div>
</section>
</div>
<div ref={chartRef} className="lg:col-span-3 space-y-8">
{multiChartData.length > 0 ? (
multiChartData.map((group, groupIndex) => {
const groupStats = calculateStats(group.points);
if (!groupStats) return null;
return (
<div key={group.name} id={`chart-container-${groupIndex}`} className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm h-[700px] flex flex-col overflow-hidden relative group">
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity flex gap-2 z-20">
<button
onClick={() => copyChart(`chart-container-${groupIndex}`)}
className="p-2 bg-white border border-slate-200 rounded-lg shadow-sm hover:bg-slate-50 transition-colors flex items-center gap-2 text-xs font-medium text-slate-600"
title="Copy to Clipboard"
>
{copyingId === `chart-container-${groupIndex}` ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
{copyingId === `chart-container-${groupIndex}` ? 'Copied!' : 'Copy'}
</button>
</div>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold text-slate-800 tracking-tight">{config.title}</h2>
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 mt-2 text-sm border-t border-slate-100 pt-2">
<span className="text-slate-500">
X-Axis: <span className="text-slate-900 font-semibold">{config.xAxisKey}</span>
</span>
<span className="text-slate-500">
Y-Axis: <span className="text-slate-900 font-semibold">{config.yAxisKey}</span>
</span>
{config.sizeKey && (
<span className="text-slate-500">
Size: <span className="text-slate-900 font-semibold">{config.sizeKey}</span>
</span>
)}
{config.groupByKeys.length > 0 && (
<span className="text-slate-500">
Group: <span className="text-indigo-600 font-bold">{group.name}</span>
</span>
)}
</div>
</div>
<div className="flex-1 w-full relative">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 40, right: 40, bottom: 60, left: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis
type="number"
dataKey="x"
name={config.xAxisKey}
stroke="#94a3b8"
fontSize={12}
domain={['auto', 'auto']}
tickFormatter={formatValue}
label={{ value: `${config.xAxisKey} | Mean = ${formatValue(groupStats.xMean)}`, position: 'bottom', offset: 40, style: { fill: '#475569', fontWeight: 600, fontSize: 14 } }}
/>
<YAxis
type="number"
dataKey="y"
name={config.yAxisKey}
stroke="#94a3b8"
fontSize={12}
domain={['auto', 'auto']}
tickFormatter={formatValue}
label={{ value: `${config.yAxisKey} | Mean = ${formatValue(groupStats.yMean)}`, angle: -90, position: 'left', offset: 40, style: { fill: '#475569', fontWeight: 600, fontSize: 14 } }}
/>
<ZAxis type="number" dataKey="z" range={config.sizeKey ? [50, 800] : [100, 100]} />
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const d = payload[0].payload;
return (
<div className="bg-white p-4 border border-slate-200 shadow-2xl rounded-xl text-sm min-w-[220px]">
<p className="font-bold text-slate-900 mb-3 border-b border-slate-100 pb-2 text-base">{d[config.labelKey]}</p>
<div className="space-y-2 mb-3">
<div className="flex flex-col bg-slate-50 p-2 rounded-lg border border-slate-100">
<span className="text-[10px] uppercase tracking-wider text-slate-400 font-bold">{config.xAxisKey} (X)</span>
<span className="font-mono font-bold text-indigo-600 text-lg leading-none">{formatValue(d.x)}</span>
</div>
<div className="flex flex-col bg-slate-50 p-2 rounded-lg border border-slate-100">
<span className="text-[10px] uppercase tracking-wider text-slate-400 font-bold">{config.yAxisKey} (Y)</span>
<span className="font-mono font-bold text-indigo-600 text-lg leading-none">{formatValue(d.y)}</span>
</div>
{config.sizeKey && (
<div className="flex flex-col bg-slate-50 p-2 rounded-lg border border-slate-100">
<span className="text-[10px] uppercase tracking-wider text-slate-400 font-bold">{config.sizeKey} (Size)</span>
<span className="font-mono font-bold text-indigo-600 text-lg leading-none">{formatValue(d.z)}</span>
</div>
)}
</div>
<div className="space-y-1 pt-2 border-t border-slate-100">
{columns.filter(c => c !== config.xAxisKey && c !== config.yAxisKey && c !== config.labelKey && c !== config.sizeKey && !c.startsWith('__')).slice(0, 5).map(col => (
<p key={col} className="flex justify-between gap-4 text-[11px]">
<span className="text-slate-400 truncate max-w-[120px]">{col}:</span>
<span className="text-slate-600 font-medium">{String(d[col] || '')}</span>
</p>
))}
</div>
<p className="mt-3 text-[10px] text-center text-indigo-400 italic font-medium animate-pulse">Click point to edit or remove</p>
</div>
);
}
return null;
}}
/>
<ReferenceLine x={groupStats.xMean} stroke="#6366f1" strokeDasharray="5 5" strokeWidth={2} />
<ReferenceLine y={groupStats.yMean} stroke="#6366f1" strokeDasharray="5 5" strokeWidth={2} />
{/* Quadrant Labels */}
<Text x="25%" y="15%" textAnchor="middle" fill="#cbd5e1" fontSize={14} fontWeight="bold">LOW {config.xAxisKey} / HIGH {config.yAxisKey}</Text>
<Text x="75%" y="15%" textAnchor="middle" fill="#cbd5e1" fontSize={14} fontWeight="bold">HIGH {config.xAxisKey} / HIGH {config.yAxisKey}</Text>
<Text x="25%" y="85%" textAnchor="middle" fill="#cbd5e1" fontSize={14} fontWeight="bold">LOW {config.xAxisKey} / LOW {config.yAxisKey}</Text>
<Text x="75%" y="85%" textAnchor="middle" fill="#cbd5e1" fontSize={14} fontWeight="bold">HIGH {config.xAxisKey} / LOW {config.yAxisKey}</Text>
<Scatter
name={group.name}
data={group.points}
onClick={(data) => {
if (data && data.payload) {
setSelectedPoint({
id: data.payload.__id,
label: data.payload[config.labelKey]
});
}
}}
cursor="pointer"
>
{group.points.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
<LabelList dataKey={config.labelKey} position="top" offset={10} style={{ fontSize: '10px', fill: '#64748b', fontWeight: 500 }} />
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
</div>
);
})
) : (
<div className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm h-[700px] flex flex-col items-center justify-center text-slate-300">
<BarChart2 size={80} className="mb-4 opacity-20" />
<p>Upload data and configure axes to see the chart</p>
</div>
)}
</div>
</main>
{/* Point Interaction Modal */}
{selectedPoint && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
<h3 className="font-bold text-slate-800 truncate pr-4">Edit Point: {selectedPoint.label}</h3>
<button onClick={() => setSelectedPoint(null)} className="p-1 hover:bg-slate-200 rounded-full transition-colors">
<X size={20} className="text-slate-500" />
</button>
</div>
<div className="p-6 space-y-6">
<div>
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Palette size={14} />
Change Color
</label>
<div className="grid grid-cols-6 gap-3">
{['#6366f1', '#ef4444', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6', '#06b6d4', '#475569', '#000000', '#f97316', '#84cc16', '#a855f7'].map(color => (
<button
key={color}
onClick={() => {
setPointOverrides(prev => ({
...prev,
[selectedPoint.id]: { ...prev[selectedPoint.id], color }
}));
}}
className={cn(
"w-10 h-10 rounded-full border-2 transition-all hover:scale-110",
pointOverrides[selectedPoint.id]?.color === color || (!pointOverrides[selectedPoint.id]?.color && color === '#6366f1')
? "border-slate-900 scale-110 shadow-lg"
: "border-transparent"
)}
style={{ backgroundColor: color }}
>
{(pointOverrides[selectedPoint.id]?.color === color || (!pointOverrides[selectedPoint.id]?.color && color === '#6366f1')) && (
<Check size={16} className="mx-auto text-white drop-shadow-md" />
)}
</button>
))}
</div>
</div>
<div className="pt-4 border-t border-slate-100">
<button
onClick={() => {
setPointOverrides(prev => ({
...prev,
[selectedPoint.id]: { ...prev[selectedPoint.id], hidden: true }
}));
setSelectedPoint(null);
}}
className="w-full flex items-center justify-center gap-2 py-3 px-4 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl font-semibold transition-colors"
>
<Trash2 size={18} />
Remove Point from Chart
</button>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex justify-end">
<button
onClick={() => setSelectedPoint(null)}
className="px-6 py-2 bg-slate-900 text-white rounded-xl font-semibold hover:bg-slate-800 transition-colors"
>
Done
</button>
</div>
</div>
</div>
)}
{data.length > 0 && (
<section className="max-w-[1600px] mx-auto px-6">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
<h3 className="text-sm font-bold text-slate-700">Data Preview (First 10 rows)</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm border-collapse">
<thead>
<tr className="bg-slate-50">
{columns.map(col => <th key={col} className="px-6 py-3 font-semibold text-slate-600 border-b border-slate-100">{col}</th>)}
</tr>
</thead>
<tbody>
{data.slice(0, 10).map((row, i) => (
<tr key={i} className="hover:bg-slate-50 transition-colors">
{columns.map(col => <td key={col} className="px-6 py-3 text-slate-500 border-b border-slate-100">{String(row[col] || '')}</td>)}
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
)}
</div>
);
}