import { useCallback, useMemo, useRef, useState } from 'react' import { computeCumulativeDeltas, deltaPolylineSegments, formatDeltaSeconds, type DeltaSeries, type DriverDeltaResult, } from '../../lib/delta' import '../../styles/delta-graph.css' export type { DeltaSeries } export interface DeltaTimeGraphProps { series: DeltaSeries[] referenceLabel?: string } const W = 640 const H = 220 const PL = 44 const PR = 16 const PT = 12 const PB = 28 function lapCount(series: ReadonlyArray): number { return series.reduce((max, s) => Math.max(max, s.lapTimes.length), 0) } function yExtent(drivers: ReadonlyArray): { min: number; max: number } { let min = 0 let max = 0 for (const driver of drivers) { for (const delta of driver.deltas) { if (delta === null) continue min = Math.min(min, delta) max = Math.max(max, delta) } } if (min === max) { const pad = 1 return { min: min - pad, max: max + pad } } const span = max - min const pad = span * 0.08 return { min: min - pad, max: max + pad } } function niceYTicks(min: number, max: number): number[] { const span = max - min if (span <= 0) return [0] const rough = span / 4 const magnitude = Math.pow(10, Math.floor(Math.log10(rough))) const step = Math.ceil(rough / magnitude) * magnitude const ticks: number[] = [] const start = Math.ceil(min / step) * step for (let v = start; v <= max + step * 0.01; v += step) { ticks.push(Number(v.toFixed(6))) } if (!ticks.some((t) => Math.abs(t) < step * 0.01)) { ticks.push(0) ticks.sort((a, b) => a - b) } return ticks } export function DeltaTimeGraph({ series, referenceLabel }: DeltaTimeGraphProps) { const svgRef = useRef(null) const [hoverLap, setHoverLap] = useState(null) const drivers = useMemo( () => computeCumulativeDeltas(series, referenceLabel), [series, referenceLabel], ) const laps = useMemo(() => lapCount(series), [series]) const plotW = W - PL - PR const plotH = H - PT - PB const { min: yMin, max: yMax } = useMemo(() => yExtent(drivers), [drivers]) const yTicks = useMemo(() => niceYTicks(yMin, yMax), [yMin, yMax]) const toX = useCallback( (lapIndex: number) => { if (laps <= 1) return PL + plotW / 2 return PL + (lapIndex / (laps - 1)) * plotW }, [laps, plotW], ) const toY = useCallback( (delta: number) => { const span = yMax - yMin || 1 return PT + ((delta - yMin) / span) * plotH }, [yMin, yMax, plotH], ) const lapTickNumbers = useMemo(() => { const ticks: number[] = [] for (let lap = 5; lap <= laps; lap += 5) { ticks.push(lap) } return ticks }, [laps]) const handlePointerMove = useCallback( (e: React.PointerEvent | React.MouseEvent) => { if (!svgRef.current || laps === 0) return const rect = svgRef.current.getBoundingClientRect() if (rect.width <= 0) return const x = ((e.clientX - rect.left) / rect.width) * W const frac = Math.max(0, Math.min(1, (x - PL) / plotW)) const lapIndex = laps <= 1 ? 0 : Math.round(frac * (laps - 1)) if (!Number.isFinite(lapIndex)) return setHoverLap(Math.max(0, Math.min(laps - 1, lapIndex))) }, [laps, plotW], ) const handlePointerLeave = useCallback(() => setHoverLap(null), []) if (series.length === 0 || laps === 0) { return (

No lap data to compare.

) } if (drivers.length === 0) { return (

Select at least two drivers to compare.

) } const hoverX = hoverLap !== null ? toX(hoverLap) : null const tooltipRows = hoverLap !== null ? drivers .map((d) => { const delta = d.deltas[hoverLap] if (delta == null) return null return { label: d.label, color: d.color, delta } }) .filter((row): row is { label: string; color: string; delta: number } => row !== null) : [] const tooltipH = 18 + tooltipRows.length * 14 const tooltipW = 120 const tooltipX = hoverX !== null ? Math.min(Math.max(hoverX + 8, PL), W - PR - tooltipW) : 0 const tooltipY = PT return (
{yTicks.map((tick) => ( {formatDeltaSeconds(tick)} ))} {lapTickNumbers.map((lap) => { const x = toX(lap - 1) return ( {lap} ) })} {drivers.map((driver) => { const segments = deltaPolylineSegments(driver.deltas, (lapIndex, delta) => `${toX(lapIndex).toFixed(1)},${toY(delta).toFixed(1)}`, ) return ( {segments.map((points, i) => ( ))} ) })} {hoverX !== null && ( <> {tooltipRows.length > 0 && ( Lap {hoverLap! + 1} {tooltipRows.map((row, i) => ( {row.label} {formatDeltaSeconds(row.delta)} ))} )} )}
) }