import os from shiny import App, ui, render, reactive import fastf1 as ff1 import matplotlib.pyplot as plt from matplotlib.collections import LineCollection from matplotlib import cm import numpy as np import shinyswatch # Define cache folder path cache_path = os.getcwd() + "/cache" print(f"Cache path: {cache_path}") ff1.Cache.enable_cache(cache_path) # Offline mode to prevent F1 API crashes on Hugging Face ff1.Cache.offline_mode(enabled=True) # Define drivers drivers_2023 = {'Fastest driver': 'Fastest driver', 'VER': 'Max Verstappen', 'NOR': 'Lando Norris', 'GAS': 'Pierre Gasly', 'PER': 'Sergio Perez', 'ALO': 'Fernando Alonso', 'LEC': 'Charles Leclerc', 'STR': 'Lance Stroll', 'MAG': 'Kevin Magnussen', 'TSU': 'Yuki Tsunoda', 'ALB': 'Alexander Albon', 'ZHO': 'Guanyu Zhou', 'HUL': 'Nico Hülkenberg', 'OCO': 'Esteban Ocon', 'HAM': 'Lewis Hamilton', 'SAI': 'Carlos Sainz', 'RUS': 'George Russel', 'BOT': 'Valteri Bottas', 'PIA': 'Oscar Piastri', 'VRI': 'Nyck de Vries', 'SAR': 'Logan Sargeant', 'RIC': 'Daniel Ricciardo'} drivers_2022 = {'Fastest driver': 'Fastest driver', 'VER': 'Max Verstappen', 'NOR': 'Lando Norris', 'GAS': 'Pierre Gasly', 'PER': 'Sergio Perez', 'ALO': 'Fernando Alonso', 'LEC': 'Charles Leclerc', 'STR': 'Lance Stroll', 'MAG': 'Kevin Magnussen', 'TSU': 'Yuki Tsunoda', 'ALB': 'Alexander Albon', 'ZHO': 'Guanyu Zhou', 'HUL': 'Nico Hülkenberg', 'OCO': 'Esteban Ocon', 'HAM': 'Lewis Hamilton', 'SAI': 'Carlos Sainz', 'RUS': 'George Russel', 'BOT': 'Valteri Bottas', 'VRI': 'Nyck de Vries', 'VET': 'Sebastian Vettel', 'RIC': 'Daniel Ricciardo', 'MSC': 'Mick Schumacher', 'LAT': 'Nicolas Latifi'} app_ui = ui.page_fluid( {"style": "padding: 10px"}, shinyswatch.theme.minty(), ui.include_css("styles.css"), ui.panel_title("Gear usage in fastest lap"), ui.layout_sidebar( ui.panel_sidebar( ui.input_select( "track_select", "Select track:", choices = ["Austria", "Hungary", "Spain", "Bahrain", "United-Kingdom"], selected = "Austria" ), ui.input_radio_buttons( "session_type", "Session type:", choices = {"R": "Race", "Q": "Qualification"}, selected = "R" ), ui.input_radio_buttons( "year", "Year:", choices = ["2023", "2022"], selected = "2023" ), width=2 ), ui.panel_main( ui.row( ui.column( 6, ui.input_select( "driver1_select", label="Select driver 1:", choices = ["Fastest driver"], selected = "Fastest driver" ) ), ui.column( 6, ui.input_select( "driver2_select", label="Select driver 2:", choices = ["Fastest driver"], selected = "Fastest driver" ) ) ), ui.row( ui.column( 6, ui.output_plot("gear_1"), ui.output_text("fastest_driver_1"), ui.output_text("laptime_1") ), ui.column( 6, ui.output_plot("gear_2"), ui.output_text("fastest_driver_2"), ui.output_text("laptime_2") ) ), ), ), ) def server(input, output, session): # Updating driver selection list @reactive.Effect() def _(): if input.year() == "2023": driver_options = drivers_2023 elif input.year() == "2022": driver_options = drivers_2022 ui.update_select("driver1_select", label="Select driver 1:", choices=driver_options, selected=input.driver1_select() ) ui.update_select("driver2_select", label="Select driver 2:", choices=driver_options, selected=input.driver2_select() ) # Get required data for driver 2 based on selection @reactive.Calc def get_data_1(): try: ui.notification_show("Data takes a couple seconds to load.", duration=3, type = 'default') f1_session = ff1.get_session(int(input.year()), input.track_select(), input.session_type()) f1_session.load() # Check if user input == fastest driver if input.driver1_select() == "Fastest driver": lap = f1_session.laps.pick_fastest() else: laps_driver = f1_session.laps.pick_driver(input.driver1_select()) lap = laps_driver.pick_fastest() tel = lap.get_telemetry() driver = lap['Driver'] #converting data to numpy data tables x = np.array(tel['X'].values) y = np.array(tel['Y'].values) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) gear = tel['nGear'].to_numpy().astype(float) lap_time = lap['LapTime'] return segments, gear, driver, lap_time except Exception: ui.notification_show("Data not available. Select another track or driver.", duration=10, type = 'error') # Get required data for driver 2 based on selection @reactive.Calc def get_data_2(): try: ui.notification_show("Data takes a couple seconds to load.", duration=3, type = 'default') f1_session = ff1.get_session(int(input.year()), input.track_select(), input.session_type()) f1_session.load() # Check if user input == fastest driver if input.driver2_select() == "Fastest driver": lap = f1_session.laps.pick_fastest() else: laps_driver = f1_session.laps.pick_driver(input.driver2_select()) lap = laps_driver.pick_fastest() tel = lap.get_telemetry() driver = lap['Driver'] #converting data to numpy data tables x = np.array(tel['X'].values) y = np.array(tel['Y'].values) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) gear = tel['nGear'].to_numpy().astype(float) lap_time = lap['LapTime'] return segments, gear, driver, lap_time except Exception: ui.notification_show("Data not available. Select another track or driver.", duration=10, type = 'error') @output @render.text def fastest_driver_1(): segments, gear, driver, lap_time = get_data_1() #print(f"The driver of the fastest lap this session is: {driver}") return f"Graph shows the fastest lap of: {driver}" @output @render.text def fastest_driver_2(): segments, gear, driver, lap_time = get_data_2() #print(f"The driver of the fastest lap this session is: {driver}") return f"Graph shows the fastest lap of: {driver}" @output @render.text def laptime_1(): segments, gear, driver, lap_time = get_data_1() delta_str= str(lap_time) # Split the time delta string to extract hours, minutes, and seconds time_parts = delta_str.split(" ")[-1].split(":") hours, minutes, seconds = map(float, time_parts) # Convert the extracted values to the desired format formatted_time = "{:02d}:{:06.3f}".format(int(hours * 60 + minutes), seconds) return f"The lap time is: {formatted_time}" @output @render.text def laptime_2(): segments, gear, driver, lap_time = get_data_2() delta_str= str(lap_time) # Split the time delta string to extract hours, minutes, and seconds time_parts = delta_str.split(" ")[-1].split(":") hours, minutes, seconds = map(float, time_parts) # Convert the extracted values to the desired format formatted_time = "{:02d}:{:06.3f}".format(int(hours * 60 + minutes), seconds) return f"The lap time is: {formatted_time}" @output @render.plot def gear_1(): try: segments, gear, driver, lap_time = get_data_1() cmap = cm.get_cmap('Paired') lc_comp = LineCollection(segments, norm=plt.Normalize(1, cmap.N+1), cmap=cmap) lc_comp.set_array(gear) lc_comp.set_linewidth(4) plt.gca().add_collection(lc_comp) plt.axis('equal') plt.tick_params(labelleft=False, left=False, labelbottom=False, bottom=False) cbar = plt.colorbar(mappable=lc_comp, label="Gear", boundaries=np.arange(1, 10)) cbar.set_ticks(np.arange(1.5, 9.5)) cbar.set_ticklabels(np.arange(1, 9)) plt except Exception: pass @output @render.plot def gear_2(): try: segments, gear, driver, lap_time = get_data_2() cmap = cm.get_cmap('Paired') lc_comp = LineCollection(segments, norm=plt.Normalize(1, cmap.N+1), cmap=cmap) lc_comp.set_array(gear) lc_comp.set_linewidth(4) plt.gca().add_collection(lc_comp) plt.axis('equal') plt.tick_params(labelleft=False, left=False, labelbottom=False, bottom=False) cbar = plt.colorbar(mappable=lc_comp, label="Gear", boundaries=np.arange(1, 10)) cbar.set_ticks(np.arange(1.5, 9.5)) cbar.set_ticklabels(np.arange(1, 9)) plt except Exception: pass app = App(app_ui, server)