diff --git a/frontend/src/components/StrategyView.tsx b/frontend/src/components/StrategyView.tsx
index 4904fd8..ba46783 100644
--- a/frontend/src/components/StrategyView.tsx
+++ b/frontend/src/components/StrategyView.tsx
@@ -12,7 +12,7 @@ interface Props {
hasStints: boolean
}
-export function StrategyView({ results, stints, pit_stops: _pitStops, hasStints }: Props) {
+export function StrategyView({ results, stints, pit_stops, hasStints }: Props) {
if (!hasStints) {
return (
@@ -88,6 +88,10 @@ export function StrategyView({ results, stints, pit_stops: _pitStops, hasStints
lapEnd: s.lap_end,
isNew: s.tyre_age_at_start === 0,
})),
+ pitStops: pit_stops
+ .filter((p) => p.driver_number === driver.driver_number)
+ .map((p) => p.lap_number)
+ .sort((a, b) => a - b),
}))
return (
diff --git a/frontend/src/components/charts/TyreStintTimeline.tsx b/frontend/src/components/charts/TyreStintTimeline.tsx
index 731574c..1727a22 100644
--- a/frontend/src/components/charts/TyreStintTimeline.tsx
+++ b/frontend/src/components/charts/TyreStintTimeline.tsx
@@ -12,6 +12,8 @@ export interface StintTimelineRow {
label: string
color: string
stints: StintTimelineStint[]
+ /** Lap numbers where the driver pitted; optional — rows without stops render normally. */
+ pitStops?: number[]
}
interface TyreStintTimelineProps {
@@ -57,6 +59,14 @@ function stintBarW(stint: StintTimelineStint, totalLaps: number): number {
return Math.max(2, (stintLength(stint) / totalLaps) * BAR_W)
}
+function pitMarkerX(lapNumber: number, totalLaps: number): number {
+ return LEFT + ((lapNumber - 1) / totalLaps) * BAR_W
+}
+
+function pitMarkerTitle(driverLabel: string, lapNumber: number): string {
+ return `${driverLabel} pit stop · L${lapNumber}`
+}
+
function axisTicks(totalLaps: number): number[] {
const ticks: number[] = []
for (let lap = 0; lap <= totalLaps; lap += 10) {
@@ -130,6 +140,21 @@ export function TyreStintTimeline({ rows, totalLaps }: TyreStintTimelineProps) {
{stintTitle(stint)}
))}
+
+ {(row.pitStops ?? []).map((lapNumber, pi) => (
+
+ {pitMarkerTitle(row.label, lapNumber)}
+
+ ))}
)
})}
diff --git a/frontend/src/styles/stint-timeline.css b/frontend/src/styles/stint-timeline.css
index 38e9219..8198770 100644
--- a/frontend/src/styles/stint-timeline.css
+++ b/frontend/src/styles/stint-timeline.css
@@ -40,6 +40,13 @@
stroke-dasharray: 2 1;
}
+.stint-timeline__pit-marker {
+ stroke: var(--text);
+ stroke-width: 1.5;
+ opacity: 0.7;
+ pointer-events: stroke;
+}
+
.stint-timeline__axis-tick {
font-family: var(--f-mono);
font-size: 9px;
diff --git a/frontend/src/test/StrategyView.test.tsx b/frontend/src/test/StrategyView.test.tsx
index 03b35f1..7cf4fb8 100644
--- a/frontend/src/test/StrategyView.test.tsx
+++ b/frontend/src/test/StrategyView.test.tsx
@@ -106,6 +106,17 @@ describe('StrategyView — stints available', () => {
)
expect(screen.queryByText(/Stints not available/i)).not.toBeInTheDocument()
})
+
+ it('maps pit_stops into timeline pit markers for the matching driver', () => {
+ const { container } = render(
+
+ )
+ const markers = container.querySelectorAll('[data-testid="pit-marker"]')
+ expect(markers).toHaveLength(1)
+ expect(markers[0]).toHaveAttribute('data-lap', '19')
+ const titles = [...container.querySelectorAll('title')].map((t) => t.textContent)
+ expect(titles).toContain('HAM pit stop · L19')
+ })
})
describe('StrategyView — stints missing', () => {
diff --git a/frontend/src/test/tyre-stint-timeline.test.tsx b/frontend/src/test/tyre-stint-timeline.test.tsx
index b090c2e..5eebc24 100644
--- a/frontend/src/test/tyre-stint-timeline.test.tsx
+++ b/frontend/src/test/tyre-stint-timeline.test.tsx
@@ -71,6 +71,61 @@ describe('TyreStintTimeline', () => {
expect(screen.getByTestId('stint-timeline-empty')).toBeInTheDocument()
expect(screen.getByText(/No stint data/i)).toBeInTheDocument()
})
+
+ it('renders one pit marker per stop at the correct lap position', () => {
+ const rowsWithPits: StintTimelineRow[] = [
+ {
+ label: 'HAM',
+ color: '#E8002D',
+ stints: [{ compound: 'SOFT', lapStart: 1, lapEnd: 18 }],
+ pitStops: [19],
+ },
+ {
+ label: 'VER',
+ color: '#3671C6',
+ stints: [
+ { compound: 'MEDIUM', lapStart: 1, lapEnd: 30 },
+ { compound: 'SOFT', lapStart: 31, lapEnd: 78 },
+ ],
+ pitStops: [31, 52],
+ },
+ ]
+ const { container } = render(
+ ,
+ )
+ const markers = container.querySelectorAll('[data-testid="pit-marker"]')
+ expect(markers).toHaveLength(3)
+ expect(markers[0]).toHaveAttribute('data-lap', '19')
+ expect(markers[1]).toHaveAttribute('data-lap', '31')
+ expect(markers[2]).toHaveAttribute('data-lap', '52')
+ expect(container.querySelectorAll('.stint-timeline__bar')).toHaveLength(3)
+ })
+
+ it('positions pit markers using lap_number and includes driver in tooltip', () => {
+ const rows: StintTimelineRow[] = [
+ {
+ label: 'HAM',
+ color: '#E8002D',
+ stints: [{ compound: 'SOFT', lapStart: 1, lapEnd: 18 }],
+ pitStops: [19],
+ },
+ ]
+ const { container } = render()
+ const marker = container.querySelector('[data-testid="pit-marker"]') as SVGLineElement
+ expect(marker).toBeTruthy()
+ // lap 19 → x = 48 + (18/78) * 580 ≈ 181.85
+ expect(Number(marker.getAttribute('x1'))).toBeCloseTo(181.85, 1)
+ const titles = [...container.querySelectorAll('title')].map((t) => t.textContent)
+ expect(titles).toContain('HAM pit stop · L19')
+ })
+
+ it('leaves rows without pit data unchanged', () => {
+ const { container } = render(
+ ,
+ )
+ expect(container.querySelectorAll('[data-testid="pit-marker"]')).toHaveLength(0)
+ expect(container.querySelectorAll('.stint-timeline__bar')).toHaveLength(3)
+ })
})
const results: EnrichedResult[] = [