A real-time Formula 1 race visualization dashboard that displays live timing, car positions on track, telemetry, and race events during Grand Prix weekends.
| Project | Stars | What to steal |
|---|---|---|
| slowlydev/f1-dash | ~1,800 | Polished timing tower, WebSocket architecture, dark UI |
| theOehrly/Fast-F1 | ~4,900 | Track map coordinate systems, data models |
| br-g/openf1 | ~1,400 | API design, data normalization patterns |
| JustAman62/undercut-f1 | ~879 | Track visualization with car dots, race control feed |
| adn8naiagent/F1ReplayTiming | ~643 | Track map + leaderboard layout, telemetry overlays |
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | SSR, file-based routing, API routes for proxy |
| Language | TypeScript | Type safety for complex F1 data models |
| Styling | Tailwind CSS 4 | Rapid dark-theme styling, responsive |
| Track Renderer | HTML Canvas (2D) | High-perf rendering of 20 cars at ~4Hz updates |
| Charts | Lightweight custom SVG or Canvas | Gap evolution, lap time charts, tire degradation |
| State Management | Zustand | Lightweight, no boilerplate, perfect for real-time streams |
| Data Fetching | SWR + custom polling hooks | Auto-refresh, stale-while-revalidate for live data |
| Package Manager | pnpm | Fast, disk-efficient |
| Deployment | Docker (self-host) | Full control, no cold starts, no serverless timeouts |
Base URL: https://api.openf1.org/v1/
Free tier: 3 req/s, 30 req/min. Historical data only (sessions ended > 30 min ago). All data returned as JSON. Use session_key=latest for the most recent session.
| Endpoint | Data | Update Freq | Use Case |
|---|---|---|---|
GET /location |
Car X, Y, Z coordinates | ~3.7 Hz | Track map car positions |
GET /car_data |
Speed, RPM, gear, throttle, brake, DRS | ~3.7 Hz | Telemetry panel |
GET /position |
Race position per driver | ~4s | Timing tower order |
GET /intervals |
Gap to leader, interval to car ahead | ~4s | Timing tower gaps |
GET /laps |
Lap times, sector times, speeds | Per lap | Lap time table, sector colors |
GET /stints |
Tire compound, age, stint number | Per stint change | Tire strategy timeline |
GET /pit |
Pit stop duration, lap number | Per pit | Pit stop log |
GET /drivers |
Name, acronym, team, color, headshot | Per session | Driver cards, team colors |
GET /sessions |
Session type, circuit, dates | Per session | Session selector |
GET /meetings |
Grand Prix name, country, year | Per weekend | Meeting selector |
GET /weather |
Temp, humidity, rain, wind | ~60s | Weather widget |
GET /race_control |
Flags, safety car, penalties | On event | Race control feed |
GET /team_radio |
Audio recording URLs | On event | Team radio player |
To stay within rate limits (3 req/s free tier), batch requests intelligently:
Every 500ms (2 req/s budget):
- /location?session_key=latest&date>=<last_timestamp> (car positions)
Every 4s (rotating, ~0.25 req/s each):
- /position?session_key=latest
- /intervals?session_key=latest
- /car_data?session_key=latest&date>=<last_timestamp>
- /laps?session_key=latest&lap_number>=<current_lap>
Every 15s (~0.07 req/s each):
- /weather?session_key=latest
- /race_control?session_key=latest&date>=<last_timestamp>
- /team_radio?session_key=latest&date>=<last_timestamp>
- /stints?session_key=latest
Total: ~2.5 req/s (under 3 req/s limit)
All OpenF1 requests go through a Next.js API route (/api/f1/[...endpoint]) to:
- Avoid CORS issues
- Cache responses server-side (short TTL for live, longer for static)
- Add request deduplication
- Handle rate limit retries with exponential backoff
+-------------------------------------------------------------------+
| FormulaZero [Session: Race] [Circuit: Monaco] [LIVE] |
+-------------------------------------------------------------------+
| | |
| TIMING TOWER | TRACK MAP |
| | |
| P1 VER 1:12.345 S | +--------------------------+ |
| P2 NOR +1.234 M | | | |
| P3 LEC +2.567 H | | [car dots moving | |
| P4 PIA +4.891 M | | on track outline] | |
| P5 HAM +6.123 S | | | |
| P6 RUS +7.456 M | +--------------------------+ |
| ... | |
| P20 ALB +1 LAP H | SELECTED DRIVER TELEMETRY |
| | Speed: 312 km/h Gear: 8 DRS: ON |
| | Throttle: [======== ] 87% |
| | Brake: [ ] 0% |
+--------------------------+----------------------------------------+
| |
| RACE CONTROL | TEAM RADIO | WEATHER |
| > Yellow Flag S3 | > VER: "Box box" | Air: 28C Track: 42C |
| > SC deployed | > HAM: "Copy" | Humidity: 45% Rain: 0 |
| > Track clear | [play button] | Wind: 12 km/h NW |
+-------------------------------------------------------------------+
| Breakpoint | Layout |
|---|---|
>= 1440px (Desktop XL) |
Full 2-column layout as above |
>= 1024px (Desktop) |
2-column, smaller track map |
>= 768px (Tablet) |
Stacked: timing tower full-width, then track map, then panels |
< 768px (Mobile) |
Single column, tabbed navigation between views |
The core F1 experience - a live leaderboard showing all 20 drivers.
Per driver row:
| Element | Source | Details |
|---|---|---|
| Position | /position |
P1-P20, animate position swaps |
| Driver code | /drivers |
3-letter abbreviation (VER, NOR, etc.) |
| Driver number | /drivers |
#1, #4, etc. |
| Team color bar | /drivers |
Left border = team_colour hex |
| Gap/Interval | /intervals |
Toggle between gap-to-leader and interval |
| Last lap time | /laps |
Color: purple = overall best, green = personal best, yellow = normal |
| Sector times | /laps |
3 colored segments per lap (purple/green/yellow) |
| Tire compound | /stints |
Colored circle: Red=SOFT, Yellow=MEDIUM, White=HARD, Green=INTER, Blue=WET |
| Tire age | /stints |
Laps on current set |
| Pit count | /pit |
Number of pit stops made |
| Status indicator | /race_control |
Icons for: in pit, out lap, retired, penalty |
Interactions:
- Click a driver row to select them (highlights on track map, shows telemetry)
- Hover to show expanded info (full name, headshot, stint history)
- Smooth CSS transitions on position changes (rows slide up/down)
Visual style:
- Dark background (#1a1a2e)
- Monospace font for times (JetBrains Mono)
- Row height: 36px, compact enough to show all 20 without scrolling on desktop
- Selected driver row has a subtle glow matching team color
A 2D overhead view of the circuit with animated car positions.
Track outline:
- Primary: use pre-built SVG track outlines shipped with the app (one per circuit, ~24 total). Source these from open-source F1 projects or community assets
- Fallback: if no pre-built SVG exists for a circuit, derive track shape from
/locationdata by collecting all (X, Y) points from a complete lap and connecting them - Cache generated outlines per
session_key(track doesn't change within a session) - Render as a smooth Canvas path with anti-aliasing
- Track width: ~6px stroke, dark gray (#333)
- DRS zones highlighted in green dashes (from
/race_controlDRS enabled messages)
Car dots:
- 20 colored circles on the track, each = one driver
- Color = team color from
/drivers.team_colour - Size: 8px radius
- Label: 3-letter driver code next to dot
- Update positions by polling
/location?session_key=latest&date>=<last_ts> - Smooth interpolation between position updates (CSS/Canvas lerp over 500ms)
Map controls:
- Zoom in/out (scroll wheel or pinch)
- Pan (click-drag)
- Reset view button
- Toggle labels on/off
- Toggle mini-sector coloring on track
Selected driver highlight:
- Selected driver's dot is larger (12px) with a pulsing glow
- Trail: show last 5 seconds of positions as a fading line
Shows real-time telemetry for the selected driver.
Gauges/Bars:
| Metric | Visualization | Source |
|---|---|---|
| Speed | Large number + sparkline (last 30s) | /car_data speed |
| RPM | Arc gauge (0-15,000) | /car_data n_gear mapped |
| Gear | Large number (1-8, N, R) | /car_data n_gear |
| Throttle | Horizontal bar (0-100%) | /car_data throttle |
| Brake | Horizontal bar (0-100%), red | /car_data brake |
| DRS | Status indicator (open/closed/eligible) | /car_data drs |
Speed trace chart:
- Line chart of speed over the current lap
- X-axis: distance/time, Y-axis: speed (0-360 km/h)
- Compare with previous lap as a ghost line
- Canvas-rendered for performance
A scrolling log of official race events.
Message types and icons:
| Category | Icon | Color |
|---|---|---|
| Yellow Flag | warning triangle | yellow |
| Red Flag | stop sign | red |
| Green Flag | checkmark | green |
| Safety Car | car icon | orange |
| Virtual Safety Car | dashed car | orange |
| DRS Enabled/Disabled | signal icon | green/red |
| Penalty | gavel | white |
| Investigation | magnifier | white |
| Track Limits | exclamation | yellow |
| Chequered Flag | flag | white |
Behavior:
- New messages appear at top with slide-in animation
- Timestamp for each message
- Auto-scroll, but user can scroll up to read history (pauses auto-scroll)
- Sound notification option for critical events (red flag, safety car)
Play audio clips from driver-team communications.
Features:
- List of recent radio messages, newest first
- Each entry: driver code + team color + timestamp + play button
- Audio playback via HTML5
<audio>element - Source:
/team_radio→recording_urlfield - New radio messages get a subtle notification badge
- Auto-play toggle: user must click "Enable auto-play" once to unlock (browser autoplay policy), then subsequent clips play automatically
Current track conditions at a glance.
Display:
| Data | Format | Icon |
|---|---|---|
| Air temperature | XX.X C | thermometer |
| Track temperature | XX.X C | road |
| Humidity | XX% | droplet |
| Rainfall | boolean | cloud-rain |
| Wind speed | XX km/h | wind |
| Wind direction | compass direction | arrow |
| Pressure | XXXX.X mbar | gauge |
Visual: Compact card with weather icon that changes based on conditions (sun/cloud/rain).
Navigate between sessions and meetings.
Controls:
- Meeting dropdown: list of Grand Prix weekends from
/meetings - Session dropdown: FP1, FP2, FP3, Qualifying, Sprint, Race from
/sessions - Live indicator: green pulsing dot when a session is currently active
- Session clock: countdown timer or elapsed time from
/sessionsdates
Visual history of each driver's tire choices across the race.
Visualization:
- Horizontal stacked bar per driver (sorted by race position)
- Each segment = one stint
- Segment color = tire compound color
- Segment width = number of laps on that compound
- Labels: compound letter + lap count (e.g., "S 18" = Soft for 18 laps)
- Source:
/stintsendpoint
Background: #0f0f1a (deep navy black)
Surface: #1a1a2e (card backgrounds)
Surface elevated: #25253e (hover states, selected items)
Border: #2a2a4a (subtle dividers)
Text primary: #e8e8f0 (high contrast white)
Text secondary: #8888aa (muted labels)
Accent: #e10600 (F1 red, used sparingly)
Sector/Lap colors:
Purple (overall best): #a855f7
Green (personal best): #22c55e
Yellow (normal): #eab308
Red (slow/deleted): #ef4444
Tire compounds:
SOFT: #ef4444 (red)
MEDIUM: #eab308 (yellow)
HARD: #f5f5f5 (white)
INTERMEDIATE:#22c55e (green)
WET: #3b82f6 (blue)
Team colors: sourced dynamically from /drivers endpoint (team_colour field)
Headings: Inter (sans-serif), bold
Body: Inter (sans-serif), regular
Timing/Data: JetBrains Mono (monospace) -- all numerical/timing data
- Position swap: 300ms ease-in-out CSS transform
- Car dots on track: 500ms linear interpolation between updates
- New race control message: 200ms slide-in from right
- Panel transitions: 150ms fade
- Live indicator: pulsing green dot (CSS keyframes)
- No animations on reduced-motion preference (
prefers-reduced-motion)
When no session is currently live:
- Default view: auto-load the most recent completed session as a static snapshot (all data fetched once, no polling)
- Countdown: display "Next session in X days Xh Xm" with the upcoming session name and circuit (from
/sessionssorted bydate_start) - Users can browse any historical meeting/session via the dropdowns
formulaZero/
Dockerfile # Multi-stage build: build Next.js, serve with node
docker-compose.yml # Single service, port 3000
src/
app/
layout.tsx # Root layout with fonts, metadata
page.tsx # Main dashboard page
api/
f1/
[...endpoint]/
route.ts # Proxy to OpenF1 API (caching + rate limit)
components/
timing-tower/
TimingTower.tsx # Full timing tower container
DriverRow.tsx # Single driver row
SectorIndicator.tsx # Colored sector time dot
TireIndicator.tsx # Tire compound badge
track-map/
TrackMap.tsx # Canvas-based track visualization
useTrackData.ts # Hook: fetch + cache track outline
useCarPositions.ts # Hook: poll car locations
trackRenderer.ts # Canvas drawing utilities
telemetry/
TelemetryPanel.tsx # Selected driver telemetry
SpeedGauge.tsx # Speed display + sparkline
ThrottleBrakeBar.tsx # Throttle/brake horizontal bars
GearIndicator.tsx # Current gear display
race-control/
RaceControlFeed.tsx # Scrolling event log
RaceControlMessage.tsx # Single message with icon
team-radio/
TeamRadioPlayer.tsx # Radio message list + audio
RadioMessage.tsx # Single radio entry
weather/
WeatherWidget.tsx # Conditions card
session/
SessionSelector.tsx # Meeting + session dropdowns
SessionClock.tsx # Timer display
tire-strategy/
TireTimeline.tsx # Horizontal stint bars
hooks/
useOpenF1.ts # Generic OpenF1 polling hook
useSession.ts # Current session state
useLiveData.ts # Coordinated polling manager
stores/
raceStore.ts # Zustand: positions, intervals, laps
sessionStore.ts # Zustand: active session/meeting
uiStore.ts # Zustand: selected driver, panel states
lib/
openf1.ts # OpenF1 API client + types
constants.ts # Polling intervals, colors, DRS mappings, segment codes
trackUtils.ts # Coordinate transforms, interpolation
formatters.ts # Time formatting, gap display
tracks/ # Pre-built track outlines (JSON coordinate arrays per circuit)
index.ts # Circuit key -> outline lookup
monza.json # Example: { "circuit_key": 61, "points": [{x,y},...] }
monaco.json
...
types/
f1.ts # TypeScript types for all F1 data models
public/
fonts/ # JetBrains Mono, Inter
tailwind.config.ts
next.config.ts
package.json
tsconfig.json
interface Driver {
driver_number: number
full_name: string
name_acronym: string // "VER", "NOR"
team_name: string
team_colour: string // hex without #
headshot_url: string
country_code: string
}
interface CarLocation {
driver_number: number
x: number // track X coordinate
y: number // track Y coordinate
z: number // track Z (elevation)
date: string // ISO timestamp
}
interface CarData {
driver_number: number
speed: number // km/h
rpm: number
n_gear: number // 0=neutral, 1-8
throttle: number // 0-100
brake: number // 0-100
drs: number // 0-14 (various states)
date: string
}
interface Position {
driver_number: number
position: number // 1-20
date: string
}
interface Interval {
driver_number: number
gap_to_leader: number | null
interval: number | null
date: string
}
interface Lap {
driver_number: number
lap_number: number
lap_duration: number | null
duration_sector_1: number | null
duration_sector_2: number | null
duration_sector_3: number | null
i1_speed: number | null // speed trap 1
i2_speed: number | null // speed trap 2
st_speed: number | null // speed trap (finish line)
is_pit_out_lap: boolean
segments_sector_1: number[] // mini-sector status codes
segments_sector_2: number[]
segments_sector_3: number[]
}
interface Stint {
driver_number: number
compound: 'SOFT' | 'MEDIUM' | 'HARD' | 'INTERMEDIATE' | 'WET'
lap_start: number
lap_end: number
stint_number: number
tyre_age_at_start: number
}
interface PitStop {
driver_number: number
lap_number: number
pit_duration: number // seconds
date: string
}
interface Weather {
air_temperature: number
track_temperature: number
humidity: number
pressure: number
rainfall: number // 0 or 1
wind_direction: number // degrees
wind_speed: number // m/s
date: string
}
interface RaceControlMessage {
category: string // "Flag", "SafetyCar", "Drs", etc.
flag: string | null // "GREEN", "YELLOW", "RED", etc.
message: string
scope: string | null // "Track", "Sector", "Driver"
sector: number | null
driver_number: number | null
date: string
}
interface TeamRadio {
driver_number: number
recording_url: string
date: string
}
interface Session {
session_key: number
session_name: string // "Race", "Qualifying", etc.
session_type: string
date_start: string
date_end: string
circuit_key: number
circuit_short_name: string
country_name: string
year: number
}
interface Meeting {
meeting_key: number
meeting_name: string // "Monaco Grand Prix"
circuit_key: number
country_name: string
year: number
}- Project scaffolding (Next.js + TypeScript + Tailwind + pnpm)
- OpenF1 API client with types
- API proxy route with caching
- Zustand stores skeleton
- Basic layout shell (header, two-column grid)
- Session selector (meetings + sessions dropdown)
- Driver data fetching and display
- Position + interval polling
- Lap times with sector colors (purple/green/yellow)
- Tire compound badges
- Animated position swaps
- Driver selection interaction
- Canvas component setup
- Track outline generation from location data
- Car dot rendering with team colors
- Smooth position interpolation
- Zoom/pan controls
- Selected driver highlight + trail
- Telemetry panel (speed, gear, throttle, brake, DRS)
- Race control message feed
- Team radio player with audio
- Weather widget
- Tire strategy timeline
- Speed trace chart for selected driver
- Responsive layout (tablet + mobile tabs)
- Loading states and error handling
- Sound notifications for race events
-
prefers-reduced-motionsupport - Keyboard shortcuts (arrow keys to cycle drivers)
Self-hosted via Docker. No Vercel, no serverless.
Dockerfile # Multi-stage: build Next.js, then serve with node
docker-compose.yml # Single service, expose port 3000
docker compose upto run locally or on any VPS/home server- No cold starts, no execution time limits on the API proxy
- Environment variables via
.envfile mounted into container
- No user authentication or accounts
- No backend database -- all data is historical from OpenF1 free tier
- No betting or prediction features
- No video streams or F1TV integration
- No direct SignalR connection (use OpenF1 REST for simplicity)
- No mobile native app -- responsive web only
- No Vercel or serverless deployment