SearchAlgorithms / frontend /src /components /Stats /ComparisonDashboard.tsx
Kacemath's picture
feat: update with latest changes
47bba68
import React, { useMemo } from 'react';
import { Bar, BarChart, XAxis, YAxis, CartesianGrid, Cell, ResponsiveContainer } from 'recharts';
import { useGridStore } from '../../store/gridStore';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import {
BarChart3,
Activity,
Clock,
HardDrive,
Route,
Play,
RefreshCw,
Check,
FlaskConical,
Trophy,
Medal,
TrendingDown,
} from 'lucide-react';
export const ComparisonDashboard: React.FC = () => {
const { comparisonResults, runComparison, isLoading, grid } = useGridStore();
// Sort results by cost (best to worst)
const sortedResults = useMemo(() => {
if (!comparisonResults) return null;
return [...comparisonResults].sort((a, b) => {
if (a.cost === Infinity && b.cost === Infinity) return 0;
if (a.cost === Infinity) return 1;
if (b.cost === Infinity) return -1;
return a.cost - b.cost;
});
}, [comparisonResults]);
// Prepare sorted chart data for each metric
const chartDataByMetric = useMemo(() => {
if (!comparisonResults) return null;
const baseData = comparisonResults.map((r) => ({
name: r.algorithm,
fullName: r.name,
nodesExpanded: r.nodesExpanded,
runtime: r.runtimeMs,
cost: r.cost === Infinity ? 0 : r.cost,
memory: r.memoryKb,
isOptimal: r.isOptimal,
}));
return {
nodes: [...baseData].sort((a, b) => b.nodesExpanded - a.nodesExpanded),
runtime: [...baseData].sort((a, b) => b.runtime - a.runtime),
cost: [...baseData].sort((a, b) => b.cost - a.cost),
memory: [...baseData].sort((a, b) => b.memory - a.memory),
};
}, [comparisonResults]);
if (!grid) {
return (
<div className="h-full flex items-center justify-center bg-zinc-900 rounded-lg border border-zinc-800">
<div className="text-center">
<BarChart3 className="w-12 h-12 text-zinc-700 mx-auto mb-4" />
<p className="text-zinc-500 text-sm">Generate a grid first</p>
<p className="text-zinc-600 text-xs mt-1">Then compare all algorithms</p>
</div>
</div>
);
}
if (!comparisonResults) {
return (
<div className="h-full flex items-center justify-center bg-zinc-900 rounded-lg border border-zinc-800">
<div className="text-center">
<FlaskConical className="w-12 h-12 text-zinc-700 mx-auto mb-4" />
<p className="text-zinc-500 text-sm mb-4">Compare Search Algorithms</p>
<Button
onClick={runComparison}
disabled={isLoading}
variant="primary"
className="gap-2"
>
<Play className="w-4 h-4" />
{isLoading ? 'Running...' : 'Run Comparison'}
</Button>
</div>
</div>
);
}
const getRankIcon = (index: number) => {
if (index === 0) return <Trophy className="w-4 h-4 text-amber-500" />;
if (index === 1) return <Medal className="w-4 h-4 text-zinc-400" />;
if (index === 2) return <Medal className="w-4 h-4 text-amber-700" />;
return <span className="w-4 h-4 flex items-center justify-center text-zinc-600 text-xs font-mono">{index + 1}</span>;
};
// Custom tooltip
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 shadow-xl">
<p className="text-zinc-200 text-xs font-medium">{data.fullName}</p>
<p className="text-zinc-400 text-xs mt-1">
{payload[0].name}: <span className="text-zinc-100 font-mono">{typeof payload[0].value === 'number' ? payload[0].value.toLocaleString(undefined, { maximumFractionDigits: 2 }) : payload[0].value}</span>
</p>
</div>
);
}
return null;
};
// Chart component with gradient
const GradientBarChart = ({
data,
dataKey,
title,
icon: Icon,
gradientId,
}: {
data: any[];
dataKey: string;
title: string;
icon: React.ElementType;
gradientId: string;
unit?: string;
}) => {
return (
<Card className="overflow-hidden">
<CardHeader className="pb-2 bg-zinc-800/30">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2 text-zinc-400 text-xs font-medium">
<Icon className="w-3.5 h-3.5" />
{title}
</div>
<div className="flex items-center gap-1 text-zinc-600 text-[10px]">
<TrendingDown className="w-3 h-3" />
Sorted high to low
</div>
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<ResponsiveContainer width="100%" height={180}>
<BarChart
data={data}
margin={{ top: 5, right: 5, left: -20, bottom: 5 }}
barCategoryGap="20%"
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#a1a1aa" stopOpacity={0.9} />
<stop offset="50%" stopColor="#71717a" stopOpacity={0.8} />
<stop offset="100%" stopColor="#52525b" stopOpacity={0.7} />
</linearGradient>
<linearGradient id={`${gradientId}-best`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fbbf24" stopOpacity={0.9} />
<stop offset="50%" stopColor="#f59e0b" stopOpacity={0.8} />
<stop offset="100%" stopColor="#d97706" stopOpacity={0.7} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="#27272a"
vertical={false}
/>
<XAxis
dataKey="name"
tick={{ fill: '#71717a', fontSize: 9, fontFamily: 'ui-monospace' }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
interval={0}
/>
<YAxis
tick={{ fill: '#52525b', fontSize: 9 }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => value.toLocaleString()}
/>
<Bar
dataKey={dataKey}
radius={[4, 4, 0, 0]}
maxBarSize={50}
>
{data.map((entry, index) => {
// Highlight the best performer (lowest value for cost, or mark optimal)
const isBest = entry.isOptimal || (dataKey === 'cost' && entry[dataKey] === Math.min(...data.filter(d => d[dataKey] > 0).map(d => d[dataKey])));
return (
<Cell
key={`cell-${index}`}
fill={isBest ? `url(#${gradientId}-best)` : `url(#${gradientId})`}
stroke={isBest ? '#fbbf24' : '#3f3f46'}
strokeWidth={1}
/>
);
})}
</Bar>
<CustomTooltip />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
};
return (
<div className="h-full overflow-auto bg-zinc-900 rounded-lg border border-zinc-800 p-4">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-sm font-semibold text-zinc-300">Algorithm Comparison</h2>
<p className="text-xs text-zinc-600">8 algorithms on the same problem instance</p>
</div>
<Button
onClick={runComparison}
disabled={isLoading}
variant="outline"
size="sm"
className="gap-2"
>
<RefreshCw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} />
Re-run
</Button>
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<GradientBarChart
data={chartDataByMetric!.nodes}
dataKey="nodesExpanded"
title="Nodes Expanded"
icon={Activity}
gradientId="nodesGradient"
/>
<GradientBarChart
data={chartDataByMetric!.runtime}
dataKey="runtime"
title="Runtime (ms)"
icon={Clock}
gradientId="runtimeGradient"
/>
<GradientBarChart
data={chartDataByMetric!.cost}
dataKey="cost"
title="Path Cost"
icon={Route}
gradientId="costGradient"
/>
<GradientBarChart
data={chartDataByMetric!.memory}
dataKey="memory"
title="Memory (MB)"
icon={HardDrive}
gradientId="memoryGradient"
/>
</div>
{/* Results Table - Sorted by best to worst */}
<Card>
<CardHeader className="pb-2 bg-zinc-800/30">
<CardTitle className="flex items-center gap-2 text-zinc-400 text-xs font-medium">
<Trophy className="w-3.5 h-3.5" />
Rankings (Best to Worst by Path Cost)
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-zinc-800 bg-zinc-800/20">
<th className="px-4 py-3 text-left text-zinc-500 font-medium w-12">Rank</th>
<th className="px-4 py-3 text-left text-zinc-500 font-medium">Algorithm</th>
<th className="px-4 py-3 text-right text-zinc-500 font-medium">Cost</th>
<th className="px-4 py-3 text-right text-zinc-500 font-medium">Nodes</th>
<th className="px-4 py-3 text-right text-zinc-500 font-medium">Runtime</th>
<th className="px-4 py-3 text-right text-zinc-500 font-medium">Memory</th>
<th className="px-4 py-3 text-center text-zinc-500 font-medium">Optimal</th>
</tr>
</thead>
<tbody>
{sortedResults?.map((result, index) => (
<tr
key={result.algorithm}
className={`border-b border-zinc-800/50 last:border-0 ${
index === 0 ? 'bg-gradient-to-r from-amber-500/10 to-transparent' :
index === 1 ? 'bg-gradient-to-r from-zinc-500/10 to-transparent' :
index === 2 ? 'bg-gradient-to-r from-amber-700/10 to-transparent' :
''
} hover:bg-zinc-800/50 transition-colors`}
>
<td className="px-4 py-3">
{getRankIcon(index)}
</td>
<td className="px-4 py-3">
<span className={index === 0 ? 'text-zinc-100 font-medium' : 'text-zinc-300'}>{result.name}</span>
<span className="text-zinc-600 ml-1.5">({result.algorithm})</span>
</td>
<td className={`px-4 py-3 text-right font-mono ${
result.cost === Infinity ? 'text-red-400' :
index === 0 ? 'text-amber-400 font-semibold' :
result.isOptimal ? 'text-zinc-200' : 'text-zinc-400'
}`}>
{result.cost === Infinity ? 'No path' : result.cost}
</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">
{result.nodesExpanded.toLocaleString()}
</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">
{result.runtimeMs.toFixed(2)}ms
</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">
{result.memoryKb.toFixed(2)}KB
</td>
<td className="px-4 py-3 text-center">
{result.isOptimal && (
<Check className="w-4 h-4 text-emerald-500 mx-auto" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
};
export default ComparisonDashboard;