import { useEffect, useMemo, useState } from 'react' import { teamColor } from '../../utils' import type { LiveTimingRow } from '../../lib/live' import { compoundClass, driverCode, tyreLabel } from '../../lib/live' import { sparklinePoints } from '../../lib/gapHistory' import { isRaceSession } from '../../lib/battles' import type { StintHistoryMap } from '../../lib/tyredeg' import { PIT_LOSS_SECONDS, degradationModel, estimatePitRejoin, formatSlope, recordStintSamples, stintInputFromRow, } from '../../lib/tyredeg' import { tyreAgeMeaning } from '../../lib/meaning' import { Meaning } from '../Meaning' import '../../styles/tyredeg.css' const TOP_DRIVER_COUNT = 10 const SPARK_WIDTH = 64 const SPARK_HEIGHT = 16 interface Props { rows: LiveTimingRow[] sessionType: string | undefined pinned: string[] } function StintSparkline({ seconds }: { seconds: number[] }) { if (seconds.length < 2) return · const points = sparklinePoints(seconds, SPARK_WIDTH, SPARK_HEIGHT) return ( ) } export function TyreDegPanel({ rows, sessionType, pinned }: Props) { const isRace = isRaceSession(sessionType) // In practice/qualifying deg trends are secondary — collapse by default so // the Timing Tower stays above the fold. Races keep it open. const [collapsed, setCollapsed] = useState(!isRace) const [stints, setStints] = useState({}) // One lap-history update per received snapshot (rows is rebuilt per snapshot). useEffect(() => { if (rows.length === 0) return setStints((prev) => recordStintSamples(prev, rows.map(stintInputFromRow))) }, [rows]) const visible = useMemo( () => rows.filter( (row) => row.Position > 0 && !row.Driver.Retired && (row.Position <= TOP_DRIVER_COUNT || pinned.includes(row.RacingNumber)), ), [rows, pinned], ) if (visible.length === 0) return null return (
{!collapsed && (
{visible.map((row) => { const model = degradationModel(stints[row.RacingNumber]?.samples ?? []) const rejoin = isRace ? estimatePitRejoin(rows, row.RacingNumber) : null const ageAnnotation = tyreAgeMeaning(row.Tyre?.Compound, row.Tyre?.Age) return (
P{row.Position} {driverCode(row)} {model ? ( <> sample.seconds)} /> {formatSlope(model.slope)} ) : ( warming up )} {rejoin && ( → ~P{rejoin.rejoinPosition} {rejoin.aheadCode && rejoin.behindCode && ( {rejoin.aheadCode} · {rejoin.behindCode} )} )}
) })}
)}
) }