This commit is contained in:
2026-08-12 19:30:17 +03:00
parent 40c324d7fe
commit b0b7196518
152 changed files with 24657 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
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;