import { AgCharts } from "ag-charts-react"; import type { AgChartOptions } from "ag-charts-community"; import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; import type { Race } from "../api"; import { formatRaceDate, getPaceLabel, isCloseDistance, parseFinishTimeToSeconds, parseRaceDate } from "../lib"; type PaceTrendChartProps = { races: Race[]; distanceKm: number; }; type PaceDatum = { id: string; date: string; dateLabel: string; minutes: number; race: Race; }; function formatAxisTime(minutes: number): string { const totalSeconds = Math.round(minutes * 60); const hours = Math.floor(totalSeconds / 3600); const rest = totalSeconds % 3600; return hours ? `${hours}:${String(Math.floor(rest / 60)).padStart(2, "0")}` : `${Math.floor(rest / 60)}:${String(rest % 60).padStart(2, "0")}`; } function formatShortDate(date: string): string { return parseRaceDate(date).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }); } function cssToken(name: string, fallback: string): string { if (typeof window === "undefined") { return fallback; } return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; } /** Линейный график: время финиша по завершённым стартам выбранной дистанции. */ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.Element { const navigate = useNavigate(); const series = useMemo( () => races .filter((race) => race.status === "completed" && isCloseDistance(race.distanceKm, distanceKm) && parseFinishTimeToSeconds(race.finishTime) != null) .sort((a, b) => parseRaceDate(a.date).getTime() - parseRaceDate(b.date).getTime()) .map((race) => ({ id: race.id, date: race.date, dateLabel: formatShortDate(race.date), minutes: parseFinishTimeToSeconds(race.finishTime)! / 60, race, })), [distanceKm, races], ); const options = useMemo>(() => ({ data: series, autoSize: true, minHeight: 280, padding: { top: 16, right: 20, bottom: 16, left: 16 }, background: { fill: "transparent" }, animation: { enabled: false }, keyboard: { enabled: true, tabIndex: 0, initialFocus: "data-start" }, axes: { x: { type: "category", position: "bottom", title: { text: "Дата старта" } }, y: { type: "number", position: "left", title: { text: "Время" }, label: { formatter: ({ value }) => formatAxisTime(Number(value)) }, }, }, series: [{ type: "line", xKey: "dateLabel", xName: "Дата старта", yKey: "minutes", yName: "Время", stroke: cssToken("--color-accent", "#1168d8"), strokeWidth: 3, marker: { enabled: true, size: 8, fill: cssToken("--color-surface", "#ffffff"), stroke: cssToken("--color-accent", "#1168d8"), strokeWidth: 2, }, tooltip: { renderer: ({ datum }) => ({ title: datum.race.title, data: [ { label: "Дата", value: formatRaceDate(datum.date) }, { label: "Время", value: datum.race.finishTime ?? "—" }, { label: "Темп", value: getPaceLabel(datum.race.finishTime, datum.race.distanceKm) ?? "—" }, ], }), }, }], listeners: { seriesNodeClick: ({ datum }) => navigate(`/races/${datum.race.id}`), }, }), [navigate, series]); if (series.length < 2) { return

Нужно минимум два завершённых старта с временем на выбранной дистанции.

; } return (

Наведите на точку или выберите её клавиатурой.

); }