DEV Community

Cover image for Building a Live F1 Dashboard Using OpenF1 and Streamlit
Eve Loraine Nuñal
Eve Loraine Nuñal

Posted on

Building a Live F1 Dashboard Using OpenF1 and Streamlit

Formula 1 generates massive amounts of telemetry data during each race weekend — lap times, sector speeds, tire compounds, and pit stop durations. For developers who are also F1 enthusiasts, accessing this data and building visualizations is an exciting way to combine technical skills with a passion for the sport.

In this article, we'll walk through building a complete F1 dashboard using Streamlit for the user interface, Plotly for interactive visualizations, and the OpenF1 API as our data source. The final dashboard allows users to explore race sessions, compare driver lap times, and analyze pit stop performance across the 2025 and 2026 seasons.

Tech Stack Overview

Before diving into the code, let's understand the key technologies:

Technology Purpose
Streamlit Python framework that turns data scripts into web apps with minimal code
Plotly Express & Graph Objects Creates interactive, browser-based charts
Pandas Data manipulation and DataFrame operations
Requests HTTP client for API calls
OpenF1 API Free, real-time F1 data endpoint

Getting Started

Prerequisites

Before running the dashboard, ensure you have Python 3.8 or higher installed on your system. You can check your Python version with:

python --version
Enter fullscreen mode Exit fullscreen mode

Step 1: Clone the Repository

The complete source code is available on GitHub. Clone the repository to your local machine:

git clone <https://github.com/e-raine/F1-Dashboard-Using-Openf1-and-Streamlit.git>
cd F1-Dashboard-Using-Openf1-and-Streamlit
Enter fullscreen mode Exit fullscreen mode

Alternatively, if you prefer not to use Git, you can download the repository as a ZIP file from https://github.com/e-raine/F1-Dashboard-Using-Openf1-and-Streamlit and extract it.

Step 2: Create a Virtual Environment (Recommended)

Creating a virtual environment isolates the project dependencies and prevents conflicts with other Python projects:

On macOS/Linux:

python -m venv venv
source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

On Windows:

python -m venv venv
venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

You'll know the virtual environment is active when you see (venv) at the beginning of your terminal prompt.

Step 3: Install Required Packages

Install all necessary packages using pip.

pip install streamlit pandas plotly requests
Enter fullscreen mode Exit fullscreen mode

Step 4: Verify Installation

Test that everything installed correctly by checking the versions:

python -c "import streamlit; import pandas; import plotly; import requests; print('All packages installed successfully!')"
Enter fullscreen mode Exit fullscreen mode

Step 5: Run the Dashboard

Launch the Streamlit application:

streamlit run app.py
Enter fullscreen mode Exit fullscreen mode

Your default browser should automatically open to http://localhost:8501. If it doesn't, manually navigate to that address.

Understanding the OpenF1 API

OpenF1 is a community-driven, open-source API that provides live and historical Formula 1 data. It's completely free and requires no authentication — you can start making requests immediately. The API is organized into several endpoints:

Endpoint Data Provided
/meetings Race weekend information (location, country, circuit)
/sessions Individual sessions (practice, qualifying, race)
/drivers Driver details (name, number, team)
/laps Lap-by-lap timing data
/pit Pit stop durations and lap numbers
/car_data Telemetry (speed, throttle, brake, gear)
/weather Track temperature, air temperature, rainfall

The base URL is https://api.openf1.org/v1. You can filter any endpoint by adding query parameters like ?session_key=123&driver_number=44.

No API key needed – OpenF1 is open to everyone. You can even test endpoints directly in your browser. For example:
https://api.openf1.org/v1/meetings?year=2025

Project Structure

The dashboard is organized into logical sections:

  1. Configuration & Setup — Page settings and title
  2. Data Fetching Layer — Reusable API wrapper with caching
  3. Sidebar Controls — Season selection (2025/2026)
  4. Race Selection — Chronological dropdown with formatted dates
  5. Tabbed Views — Drivers, lap times, and pit stops

Let's examine each component in detail.

1. Streamlit Page Configuration

st.set_page_config(
    page_title="My F1 Dashboard",
    page_icon="🏎️",
    layout="wide"
)
Enter fullscreen mode Exit fullscreen mode

The set_page_config() call must be the first Streamlit command. Here we're setting:

  • A browser tab title
  • A favicon emoji
  • Wide layout — essential for data dashboards with multiple columns

2. Building a Robust API Fetching Function

The fetch_data() function is the backbone of the application:

@st.cache_data(ttl=300)
def fetch_data(endpoint, params=None):
    BASE_URL = "<https://api.openf1.org/v1>"
    response = requests.get(f"{BASE_URL}/{endpoint}", params=params)
    if response.status_code == 200:
        return pd.DataFrame(response.json())
    return pd.DataFrame()
Enter fullscreen mode Exit fullscreen mode

Key Design Decisions:

@st.cache_data(ttl=300) — This decorator is critical. Without caching, every user interaction would trigger a new API request. The ttl=300 parameter caches data for 5 minutes, balancing freshness with performance. Streamlit's caching is smarter than a simple memoization — it detects changes in function arguments and invalidates the cache appropriately.

Error handling for API problems — Network errors, timeouts, and HTTP error codes are now caught. Instead of crashing the app, we show a friendly error message and return an empty DataFrame. The timeout=10 prevents the app from hanging indefinitely if the API is slow.

Empty DataFrame fallback — Returning pd.DataFrame() instead of None or raising an exception ensures the rest of the code can safely call .empty checks without try/except blocks.

3. Handling the 2026 Calendar Changes

Real-world APIs require handling edge cases. For the 2026 season, the Bahrain and Saudi Arabian Grands Prix were cancelled:

if year == 2026:
    cancelled_gps = ["Bahrain", "Saudi Arabia"]
    sessions = sessions[~sessions["country_name"].isin(cancelled_gps)]
Enter fullscreen mode Exit fullscreen mode

This uses boolean indexing with the ~ (NOT) operator to filter out cancelled races. The sidebar also displays a warning to users about this calendar change.

4. Session State for Season Selection

Streamlit's reactive model means variables are re-run on every interaction. To persist state across reruns, we use st.session_state:

if "selected_year" not in st.session_state:
    st.session_state.selected_year = 2026
if select_2025:
    st.session_state.selected_year = 2025
Enter fullscreen mode Exit fullscreen mode

The two-column button layout in the sidebar creates a clean UI for season switching:

col1, col2 = st.sidebar.columns(2)
with col1:
    select_2025 = st.button("🏁 2025 Season", use_container_width=True)
Enter fullscreen mode Exit fullscreen mode

5. Race Selection with Formatted Dates

The race dropdown combines multiple data points into readable options:

race_options = [f"{formatted_dates[i]} - {race_names[i]} ({race_circuits[i]})"
                for i in range(len(race_names))]
Enter fullscreen mode Exit fullscreen mode

This produces options like "Mar 16 - Australia (Albert Park)" — much more user-friendly than showing raw session keys.

The date formatting handles API inconsistencies:

date_obj = datetime.fromisoformat(date.replace('Z', '+00:00'))
formatted_dates.append(date_obj.strftime("%b %d"))
Enter fullscreen mode Exit fullscreen mode

The 'Z' in ISO timestamps indicates UTC. Replacing it with '+00:00' makes it compatible with Python's fromisoformat().

6. Driver Display with Team Colors

In the Drivers tab, we create a responsive grid using columns and HTML:

cols = st.columns(4)
for index, (_, driver) in enumerate(drivers.iterrows()):
    col = cols[index % 4]
    team_color = driver.get("team_colour", "CCCCCC")
    if not team_color.startswith("#"):
        team_color = f"#{team_color}"
Enter fullscreen mode Exit fullscreen mode

The modulo operator (index % 4) distributes drivers across four columns. Team colors from the API sometimes lack the # prefix, so we normalize them before injecting into HTML.

The HTML card uses inline styles for a clean, color-coded border:

st.markdown(f"""
<div style="border-left: 4px solid {team_color}; padding: 10px; ...">
    <strong>{driver['full_name']}</strong><br>
    <small>{driver['team_name']}</small>
</div>
""", unsafe_allow_html=True)
Enter fullscreen mode Exit fullscreen mode

Security note: unsafe_allow_html=True is acceptable here because we're generating the HTML content programmatically, not accepting user input.

7. Lap Time Visualization with Plotly

The lap time comparison tab is the most technically interesting section. It:

  1. Fetches driver data to get driver numbers and team colors
  2. Identifies default drivers (Russell and Antonelli) using string matching
  3. Fetches lap data for each selected driver
  4. Creates an overlay chart with team-colored lines

Finding Default Drivers:

george_russell = drivers[drivers["full_name"].str.contains("Russell", case=False, na=False)]
Enter fullscreen mode Exit fullscreen mode

Using .str.contains() with case=False provides flexible matching. The na=False parameter prevents errors when encountering null values in the series.

Building the Multi-Trace Chart:

fig = go.Figure()
for driver_num in selected_drivers:
    laps = fetch_data("laps", {
        "session_key": session_key,
        "driver_number": driver_num
    })
    fig.add_trace(go.Scatter(
        x=laps["lap_number"],
        y=laps["lap_duration"],
        mode="lines+markers",
        name=driver_acronyms.get(driver_num),
        line=dict(color=f"#{team_color}", width=2)
    ))
Enter fullscreen mode Exit fullscreen mode

The chart uses lines+markers mode to show both the trend (lap time evolution) and individual data points (each lap). The hovermode="x unified" setting synchronizes tooltips across all traces.

Data Cleaning:

laps["lap_duration"] = pd.to_numeric(laps["lap_duration"], errors="coerce")
laps = laps.dropna(subset=["lap_duration"])
Enter fullscreen mode Exit fullscreen mode

The API might return strings or invalid values. errors="coerce" converts unparseable values to NaN, which we then drop. This prevents chart-breaking errors.

8. Pit Stop Analysis

The pit stop tab demonstrates a different chart type — a bar chart using Plotly Express:

pit_stops_with_names = pit_stops.merge(
    drivers[["driver_number", "full_name"]],
    on="driver_number"
)

fig = px.bar(
    pit_stops_with_names,
    x="full_name",
    y="pit_duration",
    text="pit_duration"
)
Enter fullscreen mode Exit fullscreen mode

The .merge() operation enriches pit stop data with driver names. The text parameter adds data labels directly on the bars, with formatting customized in update_traces():

fig.update_traces(texttemplate='%{text:.2f}s', textposition='outside')
Enter fullscreen mode Exit fullscreen mode

Finding the fastest pit stop uses idxmin() — a vectorized operation that's more efficient than sorting:

fastest_idx = pit_stops_with_names["pit_duration"].idxmin()
fastest = pit_stops_with_names.loc[fastest_idx]
Enter fullscreen mode Exit fullscreen mode

Error Handling & User Feedback

Throughout the dashboard, we provide clear feedback when data is unavailable:

if sessions.empty:
    st.warning(f"No race sessions found for {selected_year}")
    st.stop()
Enter fullscreen mode Exit fullscreen mode

The st.stop() method halts execution gracefully, preventing downstream errors from accessing empty DataFrames.

API-Specific Error Handling

The fetch_data() function now catches three common failure modes:

  1. Network errors – The API server might be down or unreachable.
  2. Timeout errors – The API didn't respond within 10 seconds.
  3. HTTP errors – The API returned a status code like 404 (not found) or 500 (server error).

In each case, the user sees a clear error message in the Streamlit UI, and the dashboard continues to function for other endpoints.

For upcoming races, we show a contextual message:

if session_date > now:
    st.info("📅 **Upcoming Race** - Data will be available after the session")
Enter fullscreen mode Exit fullscreen mode

Performance Considerations (For Beginners)

When building a dashboard that fetches live data, performance matters. Here's why each design choice was made:

1. Why caching matters

Every time you interact with a Streamlit app (clicking a button, selecting a dropdown), the entire script reruns from top to bottom. Without caching, that means every click would send a fresh request to the OpenF1 API. With @st.cache_data, the first request fetches data, and subsequent reruns use the cached result until it expires (5 minutes).

2. TTL (Time To Live) explained

ttl=300 means "cache this data for 300 seconds (5 minutes)". Why 5 minutes? F1 sessions are long (90+ minutes), but lap times don't change after a session ends. For live sessions, 5 minutes is a reasonable balance — you won't see updates faster than that, but you also won't hammer the API with requests every second.

3. Selective data fetching

The dashboard does not fetch all lap data for all 20 drivers at once. Instead, it only fetches lap data for the drivers you explicitly select in the multi-select box. This reduces network traffic and memory usage.

4. Vectorization vs. loops

Pandas operations like idxmin() and .isin() are implemented in C and run much faster than Python loops. For example, finding the fastest pit stop using idxmin() is about 100x faster than iterating through rows manually.

5. Session state prevents redundant queries

Without st.session_state, changing the race selection would trigger a refetch of the driver list even if the season hasn't changed. Session state preserves values across reruns, so the API is only called when absolutely necessary.

6. Empty DataFrame pattern

Returning an empty DataFrame (pd.DataFrame()) instead of None allows the rest of the code to use .empty checks. This is much faster than wrapping every API call in try/except.

Possible Improvements to the Dashboard

Here are several ways you could extend this project:

Feature Implementation Approach
Qualifying comparison Add a tab for Q1/Q2/Q3 sector times
Driver head-to-head Lap time delta chart between two drivers
Tire strategy analysis Fetch stint data from the API's stints endpoint
Track map visualization Use session_key with telemetry endpoints
Live timing Increase cache TTL and add auto-refresh
Weather overlay Fetch weather data from OpenF1's weather endpoint

Troubleshooting Common Issues

Issue Solution
"Module not found" errors Ensure you've activated the virtual environment and run pip install -r requirements.txt
Empty charts or no data Some sessions (especially upcoming races) haven't occurred yet. Try a completed race from 2025
API rate limiting OpenF1 is generous, but if you encounter limits, increase ttl to 600 seconds and avoid rapid manual refreshes
Port 8501 is busy Run streamlit run app.py --server.port 8502 to use a different port
Git clone permission denied Use HTTPS instead of SSH: git clone <https://github.com/e-raine/F1-Dashboard-Using-Openf1-and-Streamlit.git>
API connection errors Check your internet connection. OpenF1 is publicly hosted; if it's down, the dashboard will show error messages gracefully
SSL certificate errors Update your Python environment: pip install --upgrade certifi

Conclusion

This dashboard demonstrates how to build an application by combining Streamlit's reactive framework, Plotly's interactive charts, and the OpenF1 API. The key architectural patterns worth remembering are: cache API calls with TTL-based expiration to avoid rate limiting and improve performance, use Streamlit's session state to persist user selections across rerenders, provide graceful fallbacks for missing or incomplete data, format data at display time rather than storage time to keep your raw data clean, and always give users context about upcoming or ongoing sessions instead of leaving them staring at empty charts. Whether you're building this for your own F1 analytics or as a portfolio project, these same patterns scale cleanly to any real-time sports data API — from MotoGP to the NBA.


Resources


Top comments (0)