Pet Project · Visualization

Quali Duel MVP — how the app will be built

Historical F1 qualifying lap comparison. OpenF1-first data, Next.js + TypeScript UI, comparison-first one-page experience. Four phases, ~8 days of solo work.

Stack: Next.js + TypeScript Data: OpenF1 (historical only) Charts: visx / D3 Scope: qualifying · two-lap compare · share via URL
P1 priority No live timing No auth No paid infra FastF1 = optional fallback

Phase flow & dependencies

Each phase blocks the next. Phase 1 locks scope & contracts → Phase 2 builds the data spine → Phase 3 turns payload into product → Phase 4 hardens it.

flowchart LR
  classDef phase fill:#1a2331,stroke:#2a3442,color:#d7dee7,rx:10,ry:10
  classDef p1 fill:#142031,stroke:#6ec8ff,color:#d7dee7
  classDef p2 fill:#142031,stroke:#6ec8ff,color:#d7dee7
  classDef p3 fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7
  classDef p4 fill:#2a1f10,stroke:#ffb02e,color:#d7dee7

  subgraph P1["Phase 1 · Research & scope · 1d"]
    direction TB
    P1A["Pick stack: Next.js + TS"]
    P1B["Define data contract
SessionOption · DriverOption · LapOption
NormalizedTelemetrySample · LapComparisonPayload"] P1C["Lock v1 scope:
qualifying · 2-lap compare · historical"] P1D["Design tokens & visual direction"] end subgraph P2["Phase 2 · Data access layer · 2d"] direction TB P2A["OpenF1 client (typed)"] P2B["Route handlers
/api/sessions · /api/lap-comparison"] P2C["Telemetry merge
nearest-sample by timestamp"] P2D["Lap-progress normalize"] P2E["Comparison payload mapper"] P2F["Recorded fixtures"] end subgraph P3["Phase 3 · Comparison UI · 3d"] direction TB P3A["Page shell + selectors"] P3B["Summary cards
lap time · sector · top speed"] P3C["Delta chart (hero)"] P3D["Telemetry traces
speed · throttle · brake · gear · RPM"] P3E["Track-progress map
gain/loss segments"] P3F["Shareable URL state"] end subgraph P4["Phase 4 · Test polish & docs · 2d"] direction TB P4A["Unit tests · merge math"] P4B["Integration tests · routes"] P4C["E2E · one full compare"] P4D["A11y + perf audit"] P4E["README · architecture · backlog"] end P1 ==> P2 ==> P3 ==> P4 class P1 p1 class P2 p2 class P3 p3 class P4 p4
P1/P2 — foundations (scope, data) P3 — product surface (UI & charts) P4 — polish & reliability

Runtime data flow — how a comparison loads

Browser drives selectors → BFF route handlers fetch & normalize → UI renders charts. URL is the shareable state.

flowchart LR
  classDef ui fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7,rx:8,ry:8
  classDef api fill:#142031,stroke:#6ec8ff,color:#d7dee7,rx:8,ry:8
  classDef lib fill:#1a2331,stroke:#2a3442,color:#d7dee7,rx:8,ry:8
  classDef ext fill:#2a1f10,stroke:#ffb02e,color:#d7dee7,rx:8,ry:8

  U["User · pick session + 2 laps"]:::ui
  URL["URL search params
?session=...&lapA=...&lapB=..."]:::ui UI["Comparison page
delta · telemetry · track map"]:::ui R1["/api/sessions"]:::api R2["/api/lap-comparison"]:::api C["openf1-client.ts"]:::lib M["merge-lap-samples.ts
nearest by timestamp"]:::lib N["normalize-lap-progress.ts
relative progress + distance"]:::lib B["build-lap-comparison-payload.ts
delta · gain/loss · max/min"]:::lib OF1[("OpenF1 API
sessions · drivers · laps
car_data · location")]:::ext U --> URL --> R2 U --> R1 --> C --> OF1 R2 --> C C --> M --> N --> B --> R2 R2 --> UI UI -.shareable.-> URL
Browser surface Server BFF route Normalization library External · OpenF1

Success bar

When this is true, MVP ships:

flowchart TB
  classDef ok fill:#1f2a18,stroke:#b7ff2a,color:#d7dee7,rx:8,ry:8
  S1["Open one page · pick session · compare 2 laps"]:::ok
  S2["Gain/loss zones are obvious without tooltips"]:::ok
  S3["Shared URL fully restores the comparison"]:::ok
  S4["UI looks portfolio-grade, not admin-tool"]:::ok
  S5["Data layer modular enough for “Stint Story” v2"]:::ok
  S1 --> S2 --> S3 --> S4 --> S5