Spaces:
Sleeping
Sleeping
| 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; | |