Files
front/src/components/scoreboard/ScoreChart.tsx
2026-08-12 19:30:17 +03:00

151 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from 'react';
import { ScoreHistory } from '../../api/types';
import { cn } from '../../utils/helpers';
import { getTeamColor } from '../../utils/colors';
interface ScoreChartProps {
history: ScoreHistory[];
height?: number;
showLegend?: boolean;
showGrid?: boolean;
className?: string;
}
export function ScoreChart({
history,
height = 300,
showLegend = true,
showGrid = true,
className,
}: ScoreChartProps) {
// Get all rounds
const allRounds = useMemo(() => {
const rounds = new Set<number>();
history.forEach((h) => {
h.history.forEach((point) => rounds.add(point.round));
});
return Array.from(rounds).sort((a, b) => a - b);
}, [history]);
// Get max score for scaling
const maxScore = useMemo(() => {
let max = 0;
history.forEach((h) => {
h.history.forEach((point) => {
if (point.score > max) max = point.score;
});
});
return max || 100;
}, [history]);
// Generate path for a team
const generatePath = (teamHistory: ScoreHistory) => {
if (allRounds.length === 0 || teamHistory.history.length === 0) return '';
const points = allRounds.map((round) => {
const point = teamHistory.history.find((p) => p.round === round);
const score = point?.score || 0;
const x = (allRounds.indexOf(round) / (allRounds.length - 1 || 1)) * 100;
const y = 100 - (score / maxScore) * 100;
return `${x},${y}`;
});
return points.map((p, i) => (i === 0 ? `M ${p}` : `L ${p}`)).join(' ');
};
if (history.length === 0) {
return (
<div className={cn('flex items-center justify-center', className)} style={{ height }}>
<p className="text-muted-foreground">Нет данных для отображения</p>
</div>
);
}
return (
<div className={cn('space-y-4', className)}>
{/* Chart */}
<div className="relative overflow-hidden rounded-lg border border-border bg-card">
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="h-full w-full"
style={{ height }}
>
{/* Grid */}
{showGrid && (
<>
{[0, 25, 50, 75, 100].map((y) => (
<line
key={y}
x1="0"
y1={y}
x2="100"
y2={y}
stroke="hsl(var(--border))"
strokeWidth="0.2"
strokeDasharray="2,2"
/>
))}
</>
)}
{/* Lines */}
{history.map((teamHistory, index) => (
<g key={teamHistory.teamId}>
<path
d={generatePath(teamHistory)}
fill="none"
stroke={getTeamColor(index)}
strokeWidth="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Points */}
{teamHistory.history.map((point, i) => {
const x = (allRounds.indexOf(point.round) / (allRounds.length - 1 || 1)) * 100;
const y = 100 - (point.score / maxScore) * 100;
return (
<circle
key={i}
cx={x}
cy={y}
r="1"
fill={getTeamColor(index)}
className="opacity-0 hover:opacity-100 transition-opacity"
/>
);
})}
</g>
))}
</svg>
{/* X-axis labels */}
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 text-[10px] text-muted-foreground">
{allRounds.slice(0, 10).map((round) => (
<span key={round}>{round}</span>
))}
{allRounds.length > 10 && <span>...</span>}
{allRounds.length > 1 && <span>{allRounds[allRounds.length - 1]}</span>}
</div>
</div>
{/* Legend */}
{showLegend && (
<div className="flex flex-wrap gap-4">
{history.map((teamHistory, index) => (
<div key={teamHistory.teamId} className="flex items-center gap-2">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: getTeamColor(index) }}
/>
<span className="text-sm">{teamHistory.teamName}</span>
</div>
))}
</div>
)}
</div>
);
}
export default ScoreChart;