2026-05-25 02:19:53 -04:00
|
|
|
import type { EnrichedResult, Stint, PitStop } from '../types'
|
2026-05-25 10:03:57 -04:00
|
|
|
import { compareFinishPosition } from '../utils'
|
2026-07-03 23:48:09 -04:00
|
|
|
import {
|
|
|
|
|
TyreStintTimeline,
|
|
|
|
|
type StintTimelineRow,
|
|
|
|
|
} from './charts/TyreStintTimeline'
|
2026-05-25 01:20:04 -04:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
results: EnrichedResult[]
|
2026-05-25 02:19:53 -04:00
|
|
|
stints: Stint[]
|
|
|
|
|
pit_stops: PitStop[]
|
2026-05-25 01:20:04 -04:00
|
|
|
hasStints: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 23:48:09 -04:00
|
|
|
export function StrategyView({ results, stints, pit_stops: _pitStops, hasStints }: Props) {
|
2026-05-25 01:20:04 -04:00
|
|
|
if (!hasStints) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="analysis-notice">
|
2026-05-25 02:19:53 -04:00
|
|
|
<strong>Stints not available.</strong> This session does not have ingested
|
2026-05-25 01:20:04 -04:00
|
|
|
tyre compound and stint ranges in <code>/api/v1/race-hub</code>. Strategy
|
|
|
|
|
charts require per-driver stints: compound, lap_start, lap_end.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{results.length > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="sec-header" style={{ marginTop: 'var(--s5)' }}>
|
|
|
|
|
<span className="sec-title">Laps Completed</span>
|
|
|
|
|
<span className="sec-meta">from results — hint at pit count</span>
|
|
|
|
|
</div>
|
|
|
|
|
<table className="data-table" style={{ maxWidth: 360 }}>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="c" style={{ width: 28 }}>P</th>
|
|
|
|
|
<th>Driver</th>
|
|
|
|
|
<th className="r">Laps</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{results.map((r) => (
|
|
|
|
|
<tr key={r.driver_number}>
|
|
|
|
|
<td className="c mono" style={{ color: 'var(--text-3)' }}>
|
|
|
|
|
{r.position}
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
fontFamily: 'var(--f-mono)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
color: r.team_colour ? `#${r.team_colour}` : 'var(--text)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{r.name_acronym || r.driver_number}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="r mono" style={{ color: r.number_of_laps > 0 ? 'var(--text)' : 'var(--text-3)' }}>
|
|
|
|
|
{r.number_of_laps > 0 ? r.number_of_laps : '—'}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 10:03:57 -04:00
|
|
|
const sortedDrivers = [...results].sort((a, b) => {
|
|
|
|
|
const cmp = compareFinishPosition(a.position, b.position)
|
|
|
|
|
return cmp !== 0 ? cmp : a.driver_number - b.driver_number
|
|
|
|
|
})
|
2026-07-03 23:48:09 -04:00
|
|
|
|
2026-05-25 02:19:53 -04:00
|
|
|
const totalLaps = Math.max(
|
|
|
|
|
...stints.map((s) => s.lap_end),
|
|
|
|
|
...results.map((r) => r.number_of_laps),
|
2026-07-03 23:48:09 -04:00
|
|
|
1,
|
2026-05-25 02:19:53 -04:00
|
|
|
)
|
|
|
|
|
|
2026-07-03 23:48:09 -04:00
|
|
|
const timelineRows: StintTimelineRow[] = sortedDrivers.map((driver) => ({
|
|
|
|
|
label: driver.name_acronym || String(driver.driver_number),
|
|
|
|
|
color: driver.team_colour ? `#${driver.team_colour}` : '#888',
|
|
|
|
|
stints: stints
|
|
|
|
|
.filter((s) => s.driver_number === driver.driver_number)
|
|
|
|
|
.map((s) => ({
|
|
|
|
|
compound: s.compound,
|
|
|
|
|
lapStart: s.lap_start,
|
|
|
|
|
lapEnd: s.lap_end,
|
|
|
|
|
isNew: s.tyre_age_at_start === 0,
|
|
|
|
|
})),
|
|
|
|
|
}))
|
2026-05-25 02:19:53 -04:00
|
|
|
|
2026-05-25 01:20:04 -04:00
|
|
|
return (
|
2026-05-25 02:19:53 -04:00
|
|
|
<div data-testid="strategy-chart">
|
2026-07-03 23:48:09 -04:00
|
|
|
<TyreStintTimeline rows={timelineRows} totalLaps={totalLaps} />
|
2026-05-25 02:19:53 -04:00
|
|
|
</div>
|
2026-05-25 01:20:04 -04:00
|
|
|
)
|
|
|
|
|
}
|