mono/packages/ui/src/apps/tetris/PerformanceChart.tsx
2026-02-06 21:23:24 +01:00

154 lines
8.0 KiB
TypeScript

import React from 'react';
interface GameResult {
score: number;
lines: number;
level: number;
timestamp: number;
}
interface PerformanceChartProps {
history: GameResult[];
}
export const PerformanceChart: React.FC<PerformanceChartProps> = ({ history }) => {
if (history.length === 0) {
return (
<div className="bg-black/40 backdrop-blur-sm p-6 rounded-2xl shadow-2xl border border-purple-500/20">
<h3 className="text-lg font-bold text-cyan-400 mb-3">Performance Trend</h3>
<p className="text-gray-400 text-sm">No games played yet. Start playing to see improvement!</p>
</div>
);
}
// Take last 20 games for visualization
const recentGames = history.slice(-20);
const maxScore = Math.max(...recentGames.map(g => g.score), 100);
const maxLevel = Math.max(...recentGames.map(g => g.level), 5);
// Calculate statistics
const avgScore = recentGames.reduce((sum, g) => sum + g.score, 0) / recentGames.length;
const avgLevel = recentGames.reduce((sum, g) => sum + g.level, 0) / recentGames.length;
const bestGame = recentGames.reduce((best, game) =>
game.score > best.score ? game : best
, recentGames[0]);
// Calculate trend (simple moving average)
const trendWindow = 5;
const trends = recentGames.map((_, idx) => {
const start = Math.max(0, idx - trendWindow + 1);
const window = recentGames.slice(start, idx + 1);
return window.reduce((sum, g) => sum + g.score, 0) / window.length;
});
const isImproving = trends.length > 1 && trends[trends.length - 1] > trends[0];
return (
<div className="bg-black/40 backdrop-blur-sm p-6 rounded-2xl shadow-2xl border border-purple-500/20">
<h3 className="text-lg font-bold text-cyan-400 mb-4">Performance Trend</h3>
{/* Stats Summary */}
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
<div className="text-xs text-gray-400">Avg Score</div>
<div className="text-xl font-bold text-purple-400">{Math.round(avgScore)}</div>
</div>
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
<div className="text-xs text-gray-400">Best Score</div>
<div className="text-xl font-bold text-green-400">{bestGame.score}</div>
</div>
<div className="bg-black/30 p-3 rounded-lg border border-purple-500/10">
<div className="text-xs text-gray-400">Avg Level</div>
<div className="text-xl font-bold text-cyan-400">{avgLevel.toFixed(1)}</div>
</div>
</div>
{/* Trend Indicator */}
<div className="mb-4 flex items-center gap-2">
<span className="text-sm text-gray-400">Trend:</span>
<span className={`text-sm font-semibold ${isImproving ? 'text-green-400' : 'text-yellow-400'}`}>
{isImproving ? '📈 Improving' : '📊 Stable'}
</span>
</div>
{/* Chart */}
<div className="relative h-48 bg-black/30 rounded-lg p-4 border border-gray-700">
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-12 flex flex-col justify-between text-xs text-gray-500">
<span>{maxScore >= 1000 ? `${(maxScore / 1000).toFixed(0)}k` : maxScore}</span>
<span>{maxScore >= 2000 ? `${(maxScore / 2000).toFixed(0)}k` : Math.round(maxScore / 2)}</span>
<span>0</span>
</div>
{/* Chart area */}
<div className="ml-12 h-full relative">
{/* Grid lines */}
<div className="absolute inset-0 flex flex-col justify-between">
{[0, 1, 2, 3, 4].map(i => (
<div key={i} className="border-t border-gray-800" />
))}
</div>
{/* Score bars */}
<div className="absolute inset-0 flex items-end justify-around gap-1">
{recentGames.map((game, idx) => {
const heightPercent = Math.max((game.score / maxScore) * 100, 2); // Minimum 2% height
const trendHeight = (trends[idx] / maxScore) * 100;
return (
<div key={idx} className="flex-1 flex flex-col items-center justify-end group relative min-w-0">
{/* Trend line marker */}
{trendHeight > 0 && (
<div
className="absolute w-full border-t-2 border-yellow-400/50"
style={{ bottom: `${trendHeight}%` }}
/>
)}
{/* Score bar */}
<div
className={`w-full rounded-t transition-all ${game.score === bestGame.score
? 'bg-gradient-to-t from-green-500 to-green-400'
: 'bg-gradient-to-t from-cyan-500 to-purple-500'
} hover:opacity-80 cursor-pointer`}
style={{ height: `${heightPercent}%`, minHeight: '4px' }}
>
{/* Tooltip on hover */}
<div className="opacity-0 group-hover:opacity-100 absolute bottom-full mb-2 left-1/2 -translate-x-1/2 bg-black/90 border border-cyan-400/50 rounded px-2 py-1 whitespace-nowrap text-xs pointer-events-none z-10">
<div className="font-bold text-cyan-400">Game #{history.length - recentGames.length + idx + 1}</div>
<div className="text-gray-300">Score: <span className="text-purple-400 font-bold">{game.score}</span></div>
<div className="text-gray-300">Level: <span className="text-green-400">{game.level}</span></div>
<div className="text-gray-300">Lines: <span className="text-cyan-400">{game.lines}</span></div>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* X-axis label */}
<div className="absolute bottom-0 left-0 right-0 text-center text-xs text-gray-500 mt-2">
Last {recentGames.length} Games
</div>
</div>
{/* Legend */}
<div className="mt-4 flex items-center gap-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gradient-to-t from-cyan-500 to-purple-500 rounded" />
<span className="text-gray-400">Score</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-yellow-400/50" />
<span className="text-gray-400">Trend (avg)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-gradient-to-t from-green-500 to-green-400 rounded" />
<span className="text-gray-400">Best score</span>
</div>
</div>
</div>
);
};