krishna_sales/src/components/reusable/AnalyticsChart.tsx
2026-08-06 15:45:31 +05:30

265 lines
11 KiB
TypeScript

import { Card } from './Card';
import {
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend
} from 'recharts';
export interface ChartRow {
dimension: string;
value: number;
series?: string;
}
export interface ChartConfig {
chart_uid: string;
key: string;
rows: ChartRow[];
}
export interface AnalyticsChartProps {
data?: unknown[];
gridCols?: number;
}
export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
if (!data || !Array.isArray(data) || data.length === 0) return null;
const charts = data as ChartConfig[];
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#14B8A6'];
const gridClass = gridCols === 1
? "grid grid-cols-1 gap-6 w-full"
: gridCols === 2
? "grid grid-cols-1 lg:grid-cols-2 gap-6 w-full"
: "grid grid-cols-1 lg:grid-cols-3 gap-6 w-full";
return (
<div className={gridClass}>
{charts.map((chart, idx) => {
const title = (chart.key || `Chart ${idx + 1}`)
.replace(/_/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
const hasSeries = chart.rows?.some(r => r.series);
let finalData: any[] = [];
let seriesKeys: string[] = [];
if (hasSeries) {
const grouped = new Map<string, any>();
const sKeys = new Set<string>();
chart.rows?.forEach(r => {
const dim = String(r.dimension || '').trim() || 'Unknown';
const s = String(r.series || '').trim() || 'Unknown';
sKeys.add(s);
if (!grouped.has(dim)) {
grouped.set(dim, { dimension: dim });
}
const entry = grouped.get(dim);
entry[s] = Number(r.value || 0);
});
finalData = Array.from(grouped.values());
seriesKeys = Array.from(sKeys).sort(); // Sort series keys for consistency
} else {
finalData = (chart.rows || []).map(r => ({
...r,
dimension: String(r.dimension || '').trim() || 'Unknown',
value: Number(r.value || 0)
}));
seriesKeys = ['value'];
}
if (finalData.length === 0) return null;
const lowerKey = String(chart.key || '').toLowerCase();
let chartType = idx % 3; // 0 = Bar, 1 = Line, 2 = Area
if (lowerKey.includes('status')) {
chartType = 3; // Pie
} else if (lowerKey.includes('route')) {
chartType = 4; // Donut
} else if (lowerKey.includes('brand') || lowerKey.includes('product')) {
chartType = 5; // Table
} else if (lowerKey.includes('monthly') || lowerKey.includes('yearly')) {
chartType = 0; // Bar
}
// Determine if it should span full width
const isFullWidth = (gridCols === 1) || (!gridCols && charts.length <= 2) || hasSeries || lowerKey.includes('monthly') || lowerKey.includes('yearly') || finalData.length > 8;
const fullSpanClass = gridCols === 2 ? 'lg:col-span-2' : 'lg:col-span-3';
// Calculate a dynamic width if there's a lot of data to allow horizontal scrolling
const minChartWidth = chartType === 0 || chartType === 1 || chartType === 2
? Math.max(100, finalData.length * (hasSeries ? seriesKeys.length * 15 + 40 : 60))
: 100;
const renderChartContent = () => {
return (
<>
<defs>
<linearGradient id={`barGradient-${idx}-0`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#1F4D36" stopOpacity={1} />
<stop offset="100%" stopColor="#1F4D36" stopOpacity={0.6} />
</linearGradient>
<linearGradient id={`barGradient-${idx}-1`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3B82F6" stopOpacity={1} />
<stop offset="100%" stopColor="#3B82F6" stopOpacity={0.6} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" />
<XAxis
dataKey="dimension"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#64748B' }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#64748B' }}
allowDecimals={false}
/>
<Tooltip
cursor={{ fill: '#F8FAFC', stroke: '#E2E8F0', strokeWidth: 1, strokeDasharray: '3 3' }}
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
labelStyle={{ fontWeight: 'bold', color: '#0F172A', marginBottom: '4px' }}
/>
{hasSeries && <Legend wrapperStyle={{ paddingTop: '20px' }} />}
{seriesKeys.map((key, i) => {
const color = colors[i % colors.length];
if (chartType === 1) {
return (
<Line
key={key}
type="monotone"
dataKey={key}
name={hasSeries ? key : "Value"}
stroke={color}
strokeWidth={3}
dot={{ r: 4, strokeWidth: 2 }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
);
} else if (chartType === 2) {
return (
<Area
key={key}
type="monotone"
dataKey={key}
name={hasSeries ? key : "Value"}
stroke={color}
fill={color}
fillOpacity={0.2}
strokeWidth={2}
/>
);
} else {
const barFill = i === 0 ? `url(#barGradient-${idx}-0)` : (i === 1 ? `url(#barGradient-${idx}-1)` : color);
return (
<Bar
key={key}
dataKey={key}
name={hasSeries ? key : "Value"}
fill={barFill}
radius={[4, 4, 0, 0]}
maxBarSize={40}
/>
);
}
})}
</>
);
};
return (
<Card key={chart.chart_uid || idx} title={title} className={`shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)] ${isFullWidth ? fullSpanClass : ''}`}>
<div className={`h-[320px] w-full mt-4 overflow-y-hidden ${minChartWidth > 100 ? 'overflow-x-auto scrollbar-slim' : ''}`}>
{chartType === 5 ? (
<div className="w-full h-full overflow-y-auto pr-2 scrollbar-slim">
<table className="w-full text-left text-sm text-gray-600 border-collapse">
<thead className="bg-gray-50/80 text-gray-700 sticky top-0 z-10 shadow-sm">
<tr>
<th className="px-4 py-3 font-semibold border-b border-gray-200">Dimension</th>
{seriesKeys.map(k => (
<th key={k} className="px-4 py-3 font-semibold border-b border-gray-200 text-right">{hasSeries ? k : 'Value'}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{finalData.filter(row => seriesKeys.some(k => row[k] > 0)).map((row, rIdx) => (
<tr key={rIdx} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3 font-medium text-gray-900">{row.dimension}</td>
{seriesKeys.map(k => (
<td key={k} className="px-4 py-3 tabular-nums text-right">{row[k]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
) : (
<div style={{ minWidth: minChartWidth > 100 ? `${minChartWidth}px` : '100%', height: '100%' }}>
<ResponsiveContainer width="100%" height="100%" className="focus:outline-none [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none" style={{ outline: 'none' }}>
{chartType === 3 || chartType === 4 ? (
<PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
<Tooltip
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
itemStyle={{ color: '#0F172A', fontWeight: '500' }}
/>
<Legend wrapperStyle={{ paddingTop: '20px' }} />
<Pie
data={finalData}
dataKey={seriesKeys[0] || "value"}
nameKey="dimension"
cx="50%"
cy="50%"
outerRadius={100}
innerRadius={chartType === 4 ? 65 : 0}
style={{ outline: 'none' }}
activeShape={false}
className="focus:outline-none outline-none"
>
{finalData.map((_, index) => (
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} style={{ outline: 'none' }} className="focus:outline-none outline-none" />
))}
</Pie>
</PieChart>
) : chartType === 1 ? (
<LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
{renderChartContent()}
</LineChart>
) : chartType === 2 ? (
<AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
{renderChartContent()}
</AreaChart>
) : (
<BarChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
{renderChartContent()}
</BarChart>
)}
</ResponsiveContainer>
</div>
)}
</div>
</Card>
);
})}
</div>
);
}