import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types' import { gridDelta, gridDeltaClass, formatDuration, formatGap } from '../utils' interface Props { data: { results: EnrichedResult[] starting_grid: EnrichedGrid[] positions: PositionSample[] laps: Lap[] datasets: Record } } export function RaceStoryCanvas({ data }: Props) { const { results, starting_grid: grid, positions, datasets } = data const hasPositions = datasets['positions']?.status === 'available' // Position Evolution Chart Logic const allTimes = [...new Set(positions.map((p) => p.date))].sort() const hasChartData = hasPositions && allTimes.length > 0 let chartContent = null if (hasChartData) { const tMin = new Date(allTimes[0]).getTime() const tMax = new Date(allTimes[allTimes.length - 1]).getTime() const tRange = Math.max(tMax - tMin, 1) const byDriver = new Map>() for (const p of positions) { if (!byDriver.has(p.driver_number)) byDriver.set(p.driver_number, []) byDriver.get(p.driver_number)!.push({ t: (new Date(p.date).getTime() - tMin) / tRange, pos: p.position, }) } for (const samples of byDriver.values()) { samples.sort((a, b) => a.t - b.t) } const maxPos = Math.max(...positions.map((p) => p.position), results.length, 2) const colorByDriver = new Map(results.map((r) => [r.driver_number, r.team_colour])) const acronymByDriver = new Map(results.map((r) => [r.driver_number, r.name_acronym])) const W = 640 const H = 180 const PL = 40 const PR = 48 const PT = 8 const PB = 8 const plotW = W - PL - PR const plotH = H - PT - PB const toX = (t: number) => PL + t * plotW const toY = (pos: number) => PT + ((pos - 1) / Math.max(maxPos - 1, 1)) * plotH chartContent = (
{Array.from({ length: maxPos }, (_, i) => i + 1).map((pos) => ( P{pos} ))} {Array.from(byDriver.entries()).map(([dNum, samples]) => { const colour = colorByDriver.get(dNum) const color = colour ? `#${colour}` : '#888' const pts = samples.map((s) => `${toX(s.t)},${toY(s.pos)}`).join(' ') const last = samples[samples.length - 1] return ( {samples.map((s, i) => ( ))} {last && ( {acronymByDriver.get(dNum) ?? dNum} )} ) })}
) } return (
{hasChartData ? ( chartContent ) : (
Lap-by-lap positions not available. This session does not have ingested position samples in /api/v1/race-hub.
)} {results.length > 0 && (
{results.map((r, i) => { const gridPos = grid.find((g) => g.driver_number === r.driver_number)?.position ?? 0 const isWinner = i === 0 && r.position === 1 const pClass = r.position === 1 ? 'rs-pos-p1' : r.position === 2 ? 'rs-pos-p2' : r.position === 3 ? 'rs-pos-p3' : '' return (
{r.position}
{r.name_acronym || r.driver_number} {r.team_name}
{gridDelta(r.position, gridPos)} Grid
{isWinner ? formatDuration(r.duration) : formatGap(r.gap_to_leader)} {isWinner ? 'Time' : 'Gap'}
0 ? 'var(--text)' : 'var(--text-3)' }}> {r.points} Pts
) })}
)}
) }