diff --git a/frontend/src/components/RaceStoryCanvas.tsx b/frontend/src/components/RaceStoryCanvas.tsx index 8fd940a..229ff49 100644 --- a/frontend/src/components/RaceStoryCanvas.tsx +++ b/frontend/src/components/RaceStoryCanvas.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, useRef, useCallback } from 'react' -import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types' +import type { EnrichedResult, EnrichedGrid, PositionSample, Lap, Session } from '../types' import { gridDelta, gridDeltaClass, formatDuration, formatGap } from '../utils' interface Props { @@ -11,11 +11,12 @@ interface Props { datasets: Record race_control?: any[] pit_stops?: any[] + session?: Session } } export function RaceStoryCanvas({ data }: Props) { - const { results, starting_grid: grid, positions, datasets, race_control = [], pit_stops = [] } = data + const { results, starting_grid: grid, positions, datasets, race_control = [], pit_stops = [], laps = [] } = data const hasPositions = datasets['positions']?.status === 'available' const [scrubTime, setScrubTime] = useState(null) @@ -42,8 +43,12 @@ export function RaceStoryCanvas({ data }: Props) { pos: p.position, }) } - for (const samples of byDriver.values()) { + const dnfSet = new Set(results.filter(r => r.dnf || r.dns || r.dsq).map(r => r.driver_number)) + for (const [dNum, samples] of byDriver.entries()) { samples.sort((a, b) => a.t - b.t) + if (samples.length > 0 && !dnfSet.has(dNum)) { + samples.push({ t: 1, pos: samples[samples.length - 1].pos }) + } } const getInterpPos = (samples: {t: number, pos: number}[], t: number) => { @@ -85,13 +90,27 @@ export function RaceStoryCanvas({ data }: Props) { const PL = 40 const PR = 48 const PT = 8 - const PB = 8 + const PB = 20 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 + const winner = results.find(r => r.position === 1) + const winnerLaps = winner ? laps.filter(l => l.driver_number === winner.driver_number) : [] + const lapTicks: { lap: number, t: number }[] = [] + const lapInterval = winnerLaps.length < 30 ? 5 : 10 + + for (const lap of winnerLaps) { + if (lap.lap_number > 0 && lap.lap_number % lapInterval === 0) { + const t = (new Date(lap.date_start).getTime() - tMin) / tRange + if (t >= 0 && t <= 1) { + lapTicks.push({ lap: lap.lap_number, t }) + } + } + } + // Safety Car / VSC periods const scPeriods: { start: number; end: number | null; type: 'SC' | 'VSC' }[] = [] let activeSC: { start: number; type: 'SC' | 'VSC' } | null = null @@ -175,6 +194,15 @@ export function RaceStoryCanvas({ data }: Props) { ))} + {lapTicks.map(tick => ( + + + + L{tick.lap} + + + ))} + {Array.from(byDriver.entries()).map(([dNum, samples]) => { const colour = colorByDriver.get(dNum) const color = colour ? `#${colour}` : '#888' @@ -200,6 +228,7 @@ export function RaceStoryCanvas({ data }: Props) { strokeLinejoin="round" strokeLinecap="round" className="rs-driver-line" + pathLength={1} /> {driverPits.map((p, i) => { @@ -282,8 +311,16 @@ export function RaceStoryCanvas({ data }: Props) {
{displayResults.map((r, i) => { const gridPos = grid.find((g) => g.driver_number === r.driver_number)?.position ?? 0 + const currentPos = scrubTime !== null ? i + 1 : r.position 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' : '' + const pClass = currentPos === 1 ? 'rs-pos-p1' : currentPos === 2 ? 'rs-pos-p2' : currentPos === 3 ? 'rs-pos-p3' : '' + + let currentPoints: number | string = r.points + if (scrubTime !== null) { + const isSprint = data.session?.session_type?.toLowerCase().includes('sprint') + const ptsArray = isSprint ? [8, 7, 6, 5, 4, 3, 2, 1] : [25, 18, 15, 12, 10, 8, 6, 4, 2, 1] + currentPoints = currentPos <= ptsArray.length ? ptsArray[currentPos - 1] : 0 + } return (
- {scrubTime !== null ? i + 1 : r.position} + {currentPos}
- - {gridDelta(r.position, gridPos)} + + {gridDelta(currentPos, gridPos)} Grid
-
+
{isWinner ? formatDuration(r.duration) : formatGap(r.gap_to_leader)} {isWinner ? 'Time' : 'Gap'}
- 0 ? 'var(--text)' : 'var(--text-3)' }}> - {r.points} + 0 ? 'var(--text)' : 'var(--text-3)' }}> + {currentPoints} Pts
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 9de2761..44b3947 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -2501,8 +2501,8 @@ a { color: inherit; text-decoration: none; } /* Interactive Race Story Canvas elements */ .rs-driver-line { - stroke-dasharray: 4000; - stroke-dashoffset: 4000; + stroke-dasharray: 1; + stroke-dashoffset: 1; animation: drawLine 2.5s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; }