Skip to content

Latest commit

 

History

History
625 lines (513 loc) · 21.5 KB

File metadata and controls

625 lines (513 loc) · 21.5 KB

FormulaZero - Live F1 Race Dashboard

A real-time Formula 1 race visualization dashboard that displays live timing, car positions on track, telemetry, and race events during Grand Prix weekends.

Reference Projects

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

Tech Stack

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

Data Source: OpenF1 API

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.

Endpoints Used

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

Polling Strategy

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)

Data Proxy

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

UI Layout

+-------------------------------------------------------------------+
|  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       |
+-------------------------------------------------------------------+

Responsive Breakpoints

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

Feature Specifications

1. Timing Tower (Left Panel)

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

2. Track Map (Right Panel, Top)

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 /location data 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_control DRS 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

3. Telemetry Panel (Right Panel, Bottom)

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

4. Race Control Feed (Bottom Left)

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)

5. Team Radio Player (Bottom Center)

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_radiorecording_url field
  • 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

6. Weather Widget (Bottom Right)

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).


7. Session Selector (Top Bar)

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 /sessions dates

8. Tire Strategy Timeline (Expandable Panel)

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: /stints endpoint

Color Palette & Design System

Theme: F1 Night Mode

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)

Typography

Headings:     Inter (sans-serif), bold
Body:         Inter (sans-serif), regular
Timing/Data:  JetBrains Mono (monospace) -- all numerical/timing data

Animations

  • 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)

No-Live-Session Behavior

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 /sessions sorted by date_start)
  • Users can browse any historical meeting/session via the dropdowns

Project Structure

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

Data Types

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
}

Implementation Phases

Phase 1 - Foundation

  • 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)

Phase 2 - Timing Tower

  • 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

Phase 3 - Track Map

  • 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

Phase 4 - Live Panels

  • Telemetry panel (speed, gear, throttle, brake, DRS)
  • Race control message feed
  • Team radio player with audio
  • Weather widget

Phase 5 - Polish

  • 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-motion support
  • Keyboard shortcuts (arrow keys to cycle drivers)

Deployment

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 up to run locally or on any VPS/home server
  • No cold starts, no execution time limits on the API proxy
  • Environment variables via .env file mounted into container

Non-Goals (Out of Scope)

  • 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