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
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
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
On Windows:
python -m venv venv
venv\Scripts\activate
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
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!')"
Step 5: Run the Dashboard
Launch the Streamlit application:
streamlit run app.py
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:
- Configuration & Setup — Page settings and title
- Data Fetching Layer — Reusable API wrapper with caching
- Sidebar Controls — Season selection (2025/2026)
- Race Selection — Chronological dropdown with formatted dates
- 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"
)
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()
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)]
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
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)
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))]
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"))
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}"
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)
Security note:
unsafe_allow_html=Trueis 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:
- Fetches driver data to get driver numbers and team colors
- Identifies default drivers (Russell and Antonelli) using string matching
- Fetches lap data for each selected driver
- Creates an overlay chart with team-colored lines
Finding Default Drivers:
george_russell = drivers[drivers["full_name"].str.contains("Russell", case=False, na=False)]
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)
))
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"])
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"
)
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')
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]
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()
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:
- Network errors – The API server might be down or unreachable.
- Timeout errors – The API didn't respond within 10 seconds.
- 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")
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
- GitHub Repository: https://github.com/e-raine/F1-Dashboard-Using-Openf1-and-Streamlit
- OpenF1 API Documentation: https://docs.openf1.org
- Streamlit Documentation: https://docs.streamlit.io
- Plotly Python Documentation: https://plotly.com/python
Top comments (0)