151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
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;
|