diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70afc957d..5292ceaaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Create Release on: release: - types: [released] + types: [released, prereleased] workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 9cb05c714..98fc55832 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ fastf1/tests/mpl-baseline-new/ # documentation build directories docs/_build/ -docs/examples_gallery/ +docs/gen_modules/ **/sg_execution_times.rst # all variations of cache directories diff --git a/docs/_plots/colormap_overview.py b/docs/_plots/colormap_overview.py new file mode 100644 index 000000000..ad09f48f3 --- /dev/null +++ b/docs/_plots/colormap_overview.py @@ -0,0 +1,77 @@ +import matplotlib.pyplot as plt + +from fastf1.plotting._constants import Constants + + +n = len(Constants) # total number of years + +# dynamically adjust figure size depending on number of required subplots +fig = plt.figure(figsize=(10, 3 * n)) + +# slightly paranoid, sort years explicitly (order is not necessarily +# guaranteed in the internal code) +years_sorted = [str(year) for year in + sorted((int(year) for year in Constants.keys()), reverse=True)] + +# generate one axis/graphic for each year +for i, year in enumerate(years_sorted): + teams = Constants[year].Teams + + ax = fig.add_subplot(n, 1, i + 1) + + x_labels = list() + x_ranges = list() + default_colors = list() + official_colors = list() + + for j, (name, team) in enumerate(teams.items()): + x_labels.append(team.ShortName) + default_colors.append(team.TeamColor.FastF1) + official_colors.append(team.TeamColor.Official) + + x_ranges.append((j + 0.5, 1)) + + # draw color rectangles as horizontal bar graph + ax.broken_barh(x_ranges, (0.5, 0.9), facecolors=official_colors) + ax.broken_barh(x_ranges, (1.5, 0.9), facecolors=default_colors) + + # configure y axis and label + ax.set_ylim((0.5, 2.5)) + ax.set_yticks([1, 2]) + ax.set_yticklabels(['official', 'default']) + + # configure x axis and label + ax.set_xlim((0.5, len(x_ranges) + 0.5)) + ax.set_xticks(range(1, len(x_labels) + 1)) + ax.set_xticklabels(x_labels) + + # disable frame around axis + ax.spines['top'].set_visible(False) + ax.spines['bottom'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + + # disable tick markers everywhere, label x axis at the top + ax.tick_params(top=False, labeltop=True, bottom=False, labelbottom=False, + left=False, labelleft=True, right=False, labelright=False) + + # set tick label text color (grey, so it works on light and dark theme and + # isn't too distracting next to the colors) + ax.tick_params(colors='#787878') + + # set background color within axes + # (transparent, fallback white if transparency not supported) + ax.set_facecolor('#ffffff00') + + # set axes title (grey, so it works on light and dark theme and + # isn't too distracting next to the colors) + ax.set_title(year, color='#787878') + +# set background color for figure/margins around axes +# (transparent, fallback white if transparency not supported) +fig.patch.set_facecolor('#ffffff00') + +# adjust margins between and around axes +plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95, hspace=0.5) + +plt.show() diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index e5eb155ea..bce493ab2 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -3,5 +3,5 @@ Release Notes Looking for :ref:`previous-release-notes`? -.. include:: v3.3.x.rst +.. include:: v3.4.x.rst diff --git a/docs/changelog/previous.rst b/docs/changelog/previous.rst index 6e02c04a2..cfe3d24a9 100644 --- a/docs/changelog/previous.rst +++ b/docs/changelog/previous.rst @@ -8,6 +8,7 @@ Release Notes for Older Versions .. toctree:: :maxdepth: 1 + v3.3.x v3.2.x v3.1.x v3.0.x diff --git a/docs/changelog/v3.4.x.rst b/docs/changelog/v3.4.x.rst new file mode 100644 index 000000000..fcd173145 --- /dev/null +++ b/docs/changelog/v3.4.x.rst @@ -0,0 +1,37 @@ + +What's new in v3.4.0 +-------------------- + +(released dd/mm/yyyy) + + +New Features +^^^^^^^^^^^^ + + + +Bug Fixes +^^^^^^^^^ + + +Deprecations +^^^^^^^^^^^^ + +- The following module level properties of :mod:`fastf1.plotting` have been + deprecated: + :attr:`~fastf1.plotting.COMPOUND_COLORS`, + :attr:`~fastf1.plotting.DRIVER_TRANSLATE`, + :attr:`~fastf1.plotting.TEAM_COLORS`, + :attr:`~fastf1.plotting.TEAM_TRANSLATE`, + :attr:`~fastf1.plotting.COLOR_PALETTE` + + +- The following functions in :mod:`fastf1.plotting` have been deprecated: + :func:`~fastf1.plotting.driver_color`, + :func:`~fastf1.plotting.team_color` + + +Removals +^^^^^^^^ + +- ``fastf1.plotting.lapnumber_axis`` has been removed \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index e31a5dc34..389f4a0f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,10 @@ warnings.filterwarnings(action='ignore', message=r'`utils.delta_time` is considered ' r'deprecated.*') +warnings.filterwarnings(action='ignore', + message=r'(COMPOUND_COLORS|DRIVER_COLORS|' + r'DRIVER_TRANSLATE|TEAM_COLORS|TEAM_TRANSLATE|' + r'COLOR_PALETTE) is deprecated and.*') doc_cache = os.path.abspath('../doc_cache') @@ -84,6 +88,7 @@ (r'py:.*', r'pandas\..*'), (r'py:.*', r'pd\..*'), (r'py:.*', r'numpy\..*'), + (r'py:.*', r'matplotlib\..*'), (r'py:mod', r'logging'), (r'py:class', r'logging.Logger'), ] @@ -151,14 +156,19 @@ def sphinx_gallery_setup(gallery_conf, fname): sphinx_gallery_conf = { 'examples_dirs': '../examples', - 'gallery_dirs': 'examples_gallery', + 'gallery_dirs': 'gen_modules/examples_gallery', 'download_all_examples': False, 'remove_config_comments': True, 'image_scrapers': ('matplotlib', # default plotly_sg_scraper), # for plotly thumbnail 'reset_modules': ('matplotlib', 'seaborn', # defaults sphinx_gallery_setup), # custom setup - 'expected_failing_examples': ('../examples/plot_qualifying_results.py', ), + # directory where function/class granular galleries are stored + 'backreferences_dir': 'gen_modules/backreferences', + + # Modules for which function/class level galleries are created. In + # this case sphinx_gallery and numpy in a tuple of strings. + 'doc_module': ('fastf1', ), } diff --git a/docs/examples/basics.rst b/docs/examples/basics.rst index 9a8260c8e..6c91086de 100644 --- a/docs/examples/basics.rst +++ b/docs/examples/basics.rst @@ -336,5 +336,5 @@ So let's see what the fastest lap time was and who is on pole. Check out this example that shows how you can plot lap times: -:ref:`sphx_glr_examples_gallery_plot_qualifying_results.py` +:ref:`sphx_glr_gen_modules_examples_gallery_plot_qualifying_results.py` diff --git a/docs/examples/index.rst b/docs/examples/index.rst index d11e7b5de..1a2e0cd46 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -39,7 +39,7 @@ For some more advanced stuff, it's just a few more steps. import fastf1 import fastf1.plotting - fastf1.plotting.setup_mpl() + fastf1.plotting.setup_mpl(misc_mpl_mods=False, color_scheme='fastf1') session = fastf1.get_session(2019, 'Monza', 'Q') diff --git a/docs/index.rst b/docs/index.rst index a1b3ce874..10a833a6c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,7 +25,7 @@ tyre data, weather data, the event schedule and session results. To get a quick overview over how to use FastF1, check out -:doc:`examples/index` or the :doc:`examples_gallery/index`. +:doc:`examples/index` or the :doc:`gen_modules/examples_gallery/index`. Note that FastF1 handles big chunks of data (~50-100mb per session). To improve performance, data is per default cached locally. The default placement @@ -141,7 +141,7 @@ Contents :caption: Contents: examples/index - examples_gallery/index + gen_modules/examples_gallery/index fastf1 core events diff --git a/docs/plotting.rst b/docs/plotting.rst index 81c5931aa..f18f8b6d7 100644 --- a/docs/plotting.rst +++ b/docs/plotting.rst @@ -1,6 +1,158 @@ Plotting - :mod:`fastf1.plotting` ================================= +Helper functions for creating data plots. + +:mod:`fastf1.plotting` provides optional functionality with the intention of +making it easy to create nice plots. + +This module mainly offers: + - team names and colors + - driver names and driver abbreviations + - Matplotlib integration and helper functions + +FastF1 focuses on plotting with Matplotlib or related libraries like Seaborn. +If you wish to use these libraries, it is highly recommended to enable +extend support for these by calling :func:`~fastf1.plotting.setup_mpl`. + + +Team Colormaps +-------------- + +Currently, two team colormaps are supported. Each colormap provides one color +for each team. All functions that return colors for teams or drivers accept an +optional ``colormap`` argument. If this argument is not provided, the default +colormap is used. The default colormap can be changed by using +:func:`~fastf1.plotting.set_default_colormap`. + +The ``'fastf1'`` colormap is FastF1's default colormap. These colors are teams' +primary colors or accent colors as they are used by the teams on their website +or in promotional material. The colors are chosen to maximize readability in +plots by creating a stronger contrast while still being associated with the +team. Colors are constant over the course of a season. + +The ``'official'`` colormap contains the colors exactly as they are used by +F1 in official graphics and in the TV broadcast. Those colors are often +slightly muted. While that makes them more pleasing to look at in some graphics, +it also reduces the contrast between colors which is often bad for +readability of plots. These colors may change during the season if they are +updated by F1. + +See here for a complete list of all colors: :ref:`Team-Colormaps-Overview` + + +.. note:: **Driver Colors** + + Previously, individual colors for each driver were provided. This is no + longer the case. The driver color is now equivalent to the team color, + meaning that drivers from the same team have the exact same color. This + change was made because different colors for 20 drivers end up looking + very similar in a lot of cases. Therefore, it is not a good solution to + use driver specific colors to distinguish between different drivers. Other + means of plot styling should be used instead. + + + +Overview +-------- + + +Configuration and Setup +^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + setup_mpl + + +Get Colors, Names and Abbreviations for Drivers or Teams +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + get_compound_color, + get_driver_abbreviation, + get_driver_abbreviations_by_team, + get_driver_color, + get_driver_name, + get_driver_names_by_team, + get_driver_style, + get_team_color, + get_team_name, + get_team_name_by_driver + + +List all Names and Abbreviations for Drivers/Teams in a Session +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + get_compound_mapping, + get_driver_color_mapping, + list_compounds, + list_driver_abbreviations, + list_driver_names, + list_team_names + + +Plot Styling +^^^^^^^^^^^^ + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + add_sorted_driver_legend, + set_default_colormap + + +Advanced Functionality +^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + override_team_constants + + +Deprecated Functionality +^^^^^^^^^^^^^^^^^^^^^^^^ + +The following module-level attributes are deprecated since version 3.4.0 and +will be removed in a future release. + + +.. automodule:: fastf1.plotting + :noindex: + :no-members: + :autosummary: + :autosummary-members: + driver_color, + lapnumber_axis, + team_color, + COMPOUND_COLORS, + DRIVER_TRANSLATE, + DRIVER_COLORS, + TEAM_COLORS, + TEAM_TRANSLATE, + COLOR_PALETTE + + + +Plotting API Reference +---------------------- + .. automodule:: fastf1.plotting :members: - :show-inheritance: diff --git a/docs/plotting_colormaps.rst b/docs/plotting_colormaps.rst new file mode 100644 index 000000000..9bc68cfe7 --- /dev/null +++ b/docs/plotting_colormaps.rst @@ -0,0 +1,14 @@ + +:orphan: + +.. _Team-Colormaps-Overview: + +Overview over Team Colormaps +---------------------------- + +(Note that the official colors that are shown here may be different from those +that are returned by FastF1 for some sessions as these colors may be updated +by F1 during the season.) + +.. plot:: _plots/colormap_overview.py + :include-source: False diff --git a/examples/plot_annotate_speed_trace.py b/examples/plot_annotate_speed_trace.py index 9c5c483ac..631cf4aa1 100644 --- a/examples/plot_annotate_speed_trace.py +++ b/examples/plot_annotate_speed_trace.py @@ -10,9 +10,10 @@ import fastf1.plotting -# enable some matplotlib patches for plotting timedelta values and load -# FastF1's default color scheme -fastf1.plotting.setup_mpl(misc_mpl_mods=False) +# Enable Matplotlib patches for plotting timedelta values and load +# FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme='fastf1') # load a session and its telemetry data session = fastf1.get_session(2021, 'Spanish Grand Prix', 'Q') @@ -35,7 +36,8 @@ # Finally, we create a plot and plot the speed trace as well as the corner # markers. -team_color = fastf1.plotting.team_color(fastest_lap['Team']) +team_color = fastf1.plotting.get_team_color(fastest_lap['Team'], + session=session) fig, ax = plt.subplots() ax.plot(car_data['Distance'], car_data['Speed'], diff --git a/examples/plot_driver_laptimes.py b/examples/plot_driver_laptimes.py index 3b1ec0501..63c5a3f39 100755 --- a/examples/plot_driver_laptimes.py +++ b/examples/plot_driver_laptimes.py @@ -11,8 +11,11 @@ import fastf1.plotting -# The misc_mpl_mods option enables minor grid lines which clutter the plot -fastf1.plotting.setup_mpl(misc_mpl_mods=False) +# Enable Matplotlib patches for plotting timedelta values and load +# FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme='fastf1') + ############################################################################### # Load the race session. @@ -39,7 +42,7 @@ y="LapTime", ax=ax, hue="Compound", - palette=fastf1.plotting.COMPOUND_COLORS, + palette=fastf1.plotting.get_compound_mapping(session=race), s=80, linewidth=0, legend='auto') diff --git a/examples/plot_driver_styling.py b/examples/plot_driver_styling.py new file mode 100644 index 000000000..4b8d3f403 --- /dev/null +++ b/examples/plot_driver_styling.py @@ -0,0 +1,108 @@ +"""Driver specific plot styling +=============================== + +Create some plots and show the usage of ``fastf1.plotting.get_driver_style``. +""" + +from matplotlib import pyplot as plt + +import fastf1 +from fastf1 import plotting + + +# Enable Matplotlib patches for plotting timedelta values and load +# FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme='fastf1') + + +############################################################################### +# Load the race session. + +race = fastf1.get_session(2023, "Azerbaijan", 'R') +race.load() + +############################################################################### +# Basic driver-specific plot styling +# ---------------------------------- +# Plot all the laps for Hamilton, Russel, Perez and Verstappen. +# Filter out slow laps as they distort the graph axis. +# Note: as LapTime is represented by timedelta, calling ``setup_mpl`` earlier +# is required. + +fig, ax = plt.subplots(figsize=(8, 5)) + +for driver in ('HAM', 'PER', 'VER', 'RUS'): + laps = race.laps.pick_driver(driver).pick_quicklaps().reset_index() + style = plotting.get_driver_style(identifier=driver, + style=['color', 'linestyle'], + session=race) + ax.plot(laps['LapTime'], **style, label=driver) + +# add axis labels and a legend +ax.set_xlabel("Lap Number") +ax.set_ylabel("Lap Time") +ax.legend() + +############################################################################### +# Sorting the legend +# ------------------ +# That plot looks pretty good already, but the order of the labels in the +# legend is slightly chaotic. Instead of trying to order the labels manually, +# use :func:`fastf1.plotting.add_sorted_driver_legend`. +# Let's create the exact same plot again, but this time with a sorted legend +# which means, we only change the very last function call. + +fig, ax = plt.subplots(figsize=(8, 5)) + +for driver in ('HAM', 'PER', 'VER', 'RUS'): + laps = race.laps.pick_driver(driver).pick_quicklaps().reset_index() + style = plotting.get_driver_style(identifier=driver, + style=['color', 'linestyle'], + session=race) + ax.plot(laps['LapTime'], **style, label=driver) + +# add axis labels and a legend +ax.set_xlabel("Lap Number") +ax.set_ylabel("Lap Time") +plotting.add_sorted_driver_legend(ax, race) + +############################################################################### +# Creating fully custom styles +# ---------------------------- +# If you want to fully customize the plot style, you can define your own +# styling variants. +# +# Note that the value ``'auto'`` is treated as a magic keyword when used in +# combination with a color. It will be replaced with the team color. +# +# We define two styles, one for the first driver and one for the second driver +# in any team. +# +# The plot that is generated here isn't intended to be very readable, but it +# shows how you can customize any plot styling parameter. + +my_styles = [ + # style for each first driver + {'color': 'auto', 'linestyle': 'solid', 'linewidth': 5, 'alpha': 0.3}, + # style for each second driver + {'color': 'auto', 'linestyle': 'solid', 'linewidth': 1, 'alpha': 0.7} +] + +fig, ax = plt.subplots(figsize=(8, 5)) + +for driver in ('HAM', 'PER', 'VER', 'RUS'): + laps = race.laps.pick_driver(driver).pick_quicklaps().reset_index() + + # here, we now use ``style=my_style`` to use the custom styling + style = plotting.get_driver_style(identifier=driver, + style=my_styles, + session=race) + + ax.plot(laps['LapTime'], **style, label=driver) + +# add axis labels and a legend +ax.set_xlabel("Lap Number") +ax.set_ylabel("Lap Time") +plotting.add_sorted_driver_legend(ax, race) +plt.show() diff --git a/examples/plot_laptimes_distribution.py b/examples/plot_laptimes_distribution.py index 0d331206a..196f0c523 100755 --- a/examples/plot_laptimes_distribution.py +++ b/examples/plot_laptimes_distribution.py @@ -10,8 +10,11 @@ import fastf1.plotting -# enabling misc_mpl_mods will turn on minor grid lines that clutters the plot -fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False) +# Enable Matplotlib patches for plotting timedelta values and load +# FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme='fastf1') + ############################################################################### # Load the race session @@ -34,15 +37,6 @@ finishing_order = [race.get_driver(i)["Abbreviation"] for i in point_finishers] print(finishing_order) -############################################################################### -# We need to modify the DRIVER_COLORS palette. -# Its keys are the driver's full names but we need the keys to be the drivers' -# three-letter abbreviations. -# We can do this with the DRIVER_TRANSLATE mapping. -driver_colors = {abv: fastf1.plotting.DRIVER_COLORS[driver] for abv, - driver in fastf1.plotting.DRIVER_TRANSLATE.items()} -print(driver_colors) - ############################################################################### # First create the violin plots to show the distributions. # Then use the swarm plot to show the actual laptimes. @@ -50,7 +44,7 @@ # create the figure fig, ax = plt.subplots(figsize=(10, 5)) -# Seaborn doesn't have proper timedelta support +# Seaborn doesn't have proper timedelta support, # so we have to convert timedelta to float (in seconds) driver_laps["LapTime(s)"] = driver_laps["LapTime"].dt.total_seconds() @@ -61,7 +55,7 @@ inner=None, density_norm="area", order=finishing_order, - palette=driver_colors + palette=fastf1.plotting.get_driver_color_mapping(session=race) ) sns.swarmplot(data=driver_laps, @@ -69,7 +63,7 @@ y="LapTime(s)", order=finishing_order, hue="Compound", - palette=fastf1.plotting.COMPOUND_COLORS, + palette=fastf1.plotting.get_compound_mapping(session=race), hue_order=["SOFT", "MEDIUM", "HARD"], linewidth=0, size=4, diff --git a/examples/plot_position_changes.py b/examples/plot_position_changes.py index fff1726a4..a8d31a140 100644 --- a/examples/plot_position_changes.py +++ b/examples/plot_position_changes.py @@ -10,7 +10,10 @@ import fastf1.plotting -fastf1.plotting.setup_mpl(misc_mpl_mods=False) +# Load FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False, + color_scheme='fastf1') + ############################################################################## # Load the session and create the plot @@ -28,10 +31,12 @@ drv_laps = session.laps.pick_driver(drv) abb = drv_laps['Driver'].iloc[0] - color = fastf1.plotting.driver_color(abb) + style = fastf1.plotting.get_driver_style(identifier=abb, + style=['color', 'linestyle'], + session=session) ax.plot(drv_laps['LapNumber'], drv_laps['Position'], - label=abb, color=color) + label=abb, **style) # sphinx_gallery_defer_figures ############################################################################## diff --git a/examples/plot_qualifying_results.py b/examples/plot_qualifying_results.py index f466a8c7c..8e056ddfd 100644 --- a/examples/plot_qualifying_results.py +++ b/examples/plot_qualifying_results.py @@ -14,9 +14,10 @@ from fastf1.core import Laps -# we only want support for timedelta plotting in this example -fastf1.plotting.setup_mpl(mpl_timedelta_support=True, color_scheme=None, - misc_mpl_mods=False) +# Enable Matplotlib patches for plotting timedelta values +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme=None) + session = fastf1.get_session(2021, 'Spanish Grand Prix', 'Q') session.load() @@ -30,7 +31,7 @@ ############################################################################## -# After that we'll get each drivers fastest lap, create a new laps object +# After that we'll get each driver's fastest lap, create a new laps object # from these laps, sort them by lap time and have pandas reindex them to # number them nicely by starting position. @@ -45,7 +46,7 @@ ############################################################################## # The plot is nicer to look at and more easily understandable if we just plot -# the time differences. Therefore we subtract the fastest lap time from all +# the time differences. Therefore, we subtract the fastest lap time from all # other lap times. pole_lap = fastest_laps.pick_fastest() @@ -64,7 +65,7 @@ # Finally, we'll create a list of team colors per lap to color our plot. team_colors = list() for index, lap in fastest_laps.iterlaps(): - color = fastf1.plotting.team_color(lap['Team']) + color = fastf1.plotting.get_team_color(lap['Team'], session=session) team_colors.append(color) diff --git a/examples/plot_speed_traces.py b/examples/plot_speed_traces.py index ab179be0a..f09e8ecd0 100644 --- a/examples/plot_speed_traces.py +++ b/examples/plot_speed_traces.py @@ -10,9 +10,10 @@ import fastf1.plotting -# enable some matplotlib patches for plotting timedelta values and load -# FastF1's default color scheme -fastf1.plotting.setup_mpl(misc_mpl_mods=False) +# Enable Matplotlib patches for plotting timedelta values and load +# FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=True, misc_mpl_mods=False, + color_scheme='fastf1') # load a session and its telemetry data session = fastf1.get_session(2021, 'Spanish Grand Prix', 'Q') @@ -35,8 +36,8 @@ # Finally, we create a plot and plot both speed traces. # We color the individual lines with the driver's team colors. -rbr_color = fastf1.plotting.team_color('RBR') -mer_color = fastf1.plotting.team_color('MER') +rbr_color = fastf1.plotting.get_team_color(ver_lap['Team'], session=session) +mer_color = fastf1.plotting.get_team_color(ham_lap['Team'], session=session) fig, ax = plt.subplots() ax.plot(ver_tel['Distance'], ver_tel['Speed'], color=rbr_color, label='VER') diff --git a/examples/plot_strategy.py b/examples/plot_strategy.py index ef33421a1..96ecabeeb 100644 --- a/examples/plot_strategy.py +++ b/examples/plot_strategy.py @@ -55,11 +55,13 @@ for idx, row in driver_stints.iterrows(): # each row contains the compound name and stint length # we can use these information to draw horizontal bars + compound_color = fastf1.plotting.get_compound_color(row["Compound"], + session=session) plt.barh( y=driver, width=row["StintLength"], left=previous_stint_end, - color=fastf1.plotting.COMPOUND_COLORS[row["Compound"]], + color=compound_color, edgecolor="black", fill=True ) diff --git a/examples/plot_team_pace_ranking.py b/examples/plot_team_pace_ranking.py index 1087abee2..fc4221dcf 100755 --- a/examples/plot_team_pace_ranking.py +++ b/examples/plot_team_pace_ranking.py @@ -10,8 +10,10 @@ import fastf1.plotting -# activate the fastf1 color scheme (and no other modifications) -fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False) +# Load FastF1's dark color scheme +fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False, + color_scheme='fastf1') + ############################################################################### # Load the race session. @@ -40,7 +42,8 @@ print(team_order) # make a color palette associating team names to hex codes -team_palette = {team: fastf1.plotting.team_color(team) for team in team_order} +team_palette = {team: fastf1.plotting.get_team_color(team, session=race) + for team in team_order} ############################################################################### fig, ax = plt.subplots(figsize=(15, 10)) diff --git a/examples/plot_who_can_still_win_wdc.py b/examples/plot_who_can_still_win_wdc.py index 5f56e9518..77b976961 100644 --- a/examples/plot_who_can_still_win_wdc.py +++ b/examples/plot_who_can_still_win_wdc.py @@ -24,7 +24,7 @@ ############################################################################## # Get the current driver standings from Ergast. -# Reference https://theoehrly.github.io/Fast-F1-Pre-Release-Documentation/ergast.html#fastf1.ergast.Ergast.get_driver_standings +# Reference https://docs.fastf1.dev/ergast.html#fastf1.ergast.Ergast.get_driver_standings def get_drivers_standings(): ergast = Ergast() standings = ergast.get_driver_standings(season=SEASON, round=ROUND) diff --git a/fastf1/core.py b/fastf1/core.py index e01eab58c..e25ad54fc 100644 --- a/fastf1/core.py +++ b/fastf1/core.py @@ -3067,9 +3067,6 @@ def pick_team(self, name: str) -> "Laps": mercedes = session_laps.pick_team('Mercedes') alfa_romeo = session_laps.pick_team('Alfa Romeo') - Have a look to :attr:`fastf1.plotting.TEAM_COLORS` for a quick - reference on team names. - Args: name (str): Team name diff --git a/fastf1/events.py b/fastf1/events.py index a88e3fff4..a935141c8 100644 --- a/fastf1/events.py +++ b/fastf1/events.py @@ -199,21 +199,12 @@ ) import dateutil.parser - - -with warnings.catch_warnings(): - warnings.filterwarnings( - 'ignore', message="Using slow pure-python SequenceMatcher" - ) - # suppress that warning, it's confusing at best here, we don't need fast - # sequence matching and the installation (on windows) requires some effort - from rapidfuzz import fuzz - import pandas as pd import fastf1._api import fastf1.ergast from fastf1.core import Session +from fastf1.internals.fuzzy import fuzzy_matcher from fastf1.internals.pandas_base import ( BaseDataFrame, BaseSeries @@ -939,54 +930,33 @@ def _remove_common_words(event_name): for word in common_words: event_name = event_name.replace(word, "") - return event_name.replace(" ", "") + return event_name def _matcher_strings(ev): strings = list() - if 'Location' in ev: + if ('Location' in ev) and ev['Location']: strings.append(ev['Location'].casefold()) - if 'Country' in ev: + if ('Country' in ev) and ev['Country']: strings.append(ev['Country'].casefold()) - if 'EventName' in ev: + if ('EventName' in ev) and ev['EventName']: strings.append(_remove_common_words(ev["EventName"])) - if 'OfficialEventName' in ev: + if ('OfficialEventName' in ev) and ev['OfficialEventName']: strings.append(_remove_common_words(ev["OfficialEventName"])) return strings user_input = name name = _remove_common_words(name) - full_partial_match_indices = [] - # check partial matches first - # if there is either zero or multiple 100% matches - # fall back to the full ratio - for i, event in self.iterrows(): - if any([name in val for val in _matcher_strings(event)]): - full_partial_match_indices.append(i) - - if len(full_partial_match_indices) == 1: - return self.loc[full_partial_match_indices[0]] - - max_ratio = 0 - max_index = 0 - - for i, event in self.loc[full_partial_match_indices - or self.index].iterrows(): - ratio = max( - [fuzz.ratio(val, name) - for val in _matcher_strings(event)] - ) - if ratio > max_ratio: - max_ratio = ratio - max_index = i - - if max_ratio != 100: - _logger.warning(( - "Correcting user input " - f"'{user_input}' to'{self.loc[max_index].EventName}'" - ) - ) - return self.loc[max_index] + reference = [_matcher_strings(event) for _, event in self.iterrows()] + + index, exact = fuzzy_matcher(name, reference) + event = self.iloc[index] + + if not exact: + _logger.warning(f"Correcting user input '{user_input}' to " + f"'{event.EventName}'") + + return event def get_event_by_name( self, diff --git a/fastf1/internals/fuzzy.py b/fastf1/internals/fuzzy.py new file mode 100644 index 000000000..4a5406464 --- /dev/null +++ b/fastf1/internals/fuzzy.py @@ -0,0 +1,125 @@ +import warnings +from typing import List + +import numpy as np + + +with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', message="Using slow pure-python SequenceMatcher" + ) + # suppress that warning, it's confusing at best here, we don't need fast + # sequence matching and the installation (on windows) requires some effort + from rapidfuzz import fuzz + + +def fuzzy_matcher( + query: str, + reference: List[List[str]], + abs_confidence: float = 0.0, + rel_confidence: float = 0.0 +) -> (int, bool): + """ + Match a query string to a reference list of lists of strings using fuzzy + string matching. + + The reference is a list of sub-lists where each sub-list represents one + element. The sub-lists contain one or multiple feature strings. The idea is + that each element can be described by multiple feature strings. The + function tries to find the best matching element in the reference list + for the given query string. + + The function first checks for exact substring matches with the individual + feature strings. If there is exactly one sub-list, where the query + is a substring of a feature string, this index is returned as an + "accurate match". Else, the function uses fuzzy string matching to find the + best match in the reference list. The index of the best matching element is + then returned as an "inaccurate match". + + Args: + query: The query string to match. + reference: A list of lists where each sub-list contains one or multiple + feature strings describing an element. + abs_confidence: The minimum absolute confidence that the match must + have when fuzzy matched. Must be a value between 0.0 and 1.0, where + 1.0 is equivalent to a perfect match. Set to 0.0 to disable. + If the best match has a lower confidence, a KeyError is raised. + rel_confidence: The minimum relative confidence that the match must + have (compared with the second-best match). Must be a value greater + than 0.0, where 0.5 would mean that the best match must have a 50% + higher score than the second-best match. Set to 0.0 to disable. + If the best match has a lower relative confidence, a KeyError is + raised. + + Returns: + (int, bool): Index of the best matching element in the + reference (outer) list and a boolean indicating if the match is + accurate or not. + + """ + # Preprocess the query and reference strings + query = query.casefold().replace(" ", "") + for i in range(len(reference)): + for j in range(len(reference[i])): + reference[i][j] = reference[i][j].casefold().replace(" ", "") + + # Check for exact substring matches with the individual feature strings + # first. If there is exactly one reference tuple, where the query is a + # substring of a feature string, return this index as accurate match. + full_partial_match_indices = [] + for i, feature_strings in enumerate(reference): + if any([query in val for val in feature_strings]): + full_partial_match_indices.append(i) + + if len(full_partial_match_indices) == 1: + # return index as accurate match + return full_partial_match_indices[0], True + + # Zero or multiple reference tuples had substring matches, so we need to + # do fuzzy matching + reference = np.array(reference) + ratios = np.zeros_like(reference, dtype=int) + + # If we have multiple substring matches, we only fuzzy match on these, + # else we fuzzy match on all reference tuples + if full_partial_match_indices: + candidate_indices = full_partial_match_indices + else: + candidate_indices = range(len(reference)) + + # Calculate the fuzz ratio for each feature string in each reference tuple + for i in candidate_indices: + feature_strings = reference[i] + ratios[i] = [fuzz.ratio(val, query) for val in feature_strings] + + max_ratio = np.max(ratios) + max_row_ratios = np.max(ratios, axis=1) + # if there are multiple rows with the same maximum ratio, we need to remove + # the corresponding ratios from the comparison so that we can match based + # on the remaining feature string ratios + if np.sum(max_row_ratios == max_ratio) > 1: + # get counts of all unique ratios and remove all that are not unique + # in the array by setting them to zero + unique, counts = np.unique(reference, return_counts=True) + count_dict = dict(zip(unique, counts)) + mask = ((np.vectorize(count_dict.get)(reference) > 1) + & (ratios == max_ratio)) + ratios[mask] = 0 + + # get the index of the row that contains the maximum ratio + max_index = np.argmax(ratios) // ratios.shape[1] + + # optional confidence checks + if abs_confidence and (max_ratio < (abs_confidence * 100)): + raise KeyError(f"Found no match for '{query}' with sufficient " + f"absolute confidence") + + if rel_confidence and (max_ratio / np.partition(ratios.flatten(), -2)[-2] + < (1 + rel_confidence)): + # max ratio divided by second-largest ratio is less + # than 1 + rel_confidence + raise KeyError(f"Found no match for '{query}' with sufficient " + f"relative confidence") + + # return index as inaccurate match + return max_index, False diff --git a/fastf1/legacy.py b/fastf1/legacy.py index ba59315d3..e2a1d3bc0 100644 --- a/fastf1/legacy.py +++ b/fastf1/legacy.py @@ -22,7 +22,7 @@ import numpy as np import matplotlib.pyplot as plt - fastf1.plotting.setup_mpl() + fastf1.plotting.setup_mpl(misc_mpl_mods=False, color_scheme='fastf1') session = fastf1.get_session(2020, 'Italy', 'R') session.load() diff --git a/fastf1/plotting/__init__.py b/fastf1/plotting/__init__.py new file mode 100644 index 000000000..691d8ce99 --- /dev/null +++ b/fastf1/plotting/__init__.py @@ -0,0 +1,175 @@ +import warnings +from typing import ( + Dict, + List +) + +from fastf1.plotting._constants import \ + LEGACY_DRIVER_COLORS as _LEGACY_DRIVER_COLORS +from fastf1.plotting._constants import \ + LEGACY_DRIVER_TRANSLATE as _LEGACY_DRIVER_TRANSLATE +from fastf1.plotting._constants import \ + LEGACY_TEAM_TRANSLATE as _LEGACY_TEAM_TRANSLATE +from fastf1.plotting._constants import Constants as _Constants +from fastf1.plotting._interface import ( # noqa: F401 + _get_driver_team_mapping, + add_sorted_driver_legend, + get_compound_color, + get_compound_mapping, + get_driver_abbreviation, + get_driver_abbreviations_by_team, + get_driver_color, + get_driver_color_mapping, + get_driver_name, + get_driver_names_by_team, + get_driver_style, + get_team_color, + get_team_name, + get_team_name_by_driver, + list_compounds, + list_driver_abbreviations, + list_driver_names, + list_team_names, + override_team_constants, + set_default_colormap +) +from fastf1.plotting._plotting import ( # noqa: F401 + _COLOR_PALETTE, + driver_color, + lapnumber_axis, + setup_mpl, + team_color +) + + +__all__ = [ + # imported, current + 'add_sorted_driver_legend', + 'get_compound_color', + 'get_compound_mapping', + 'get_driver_abbreviation', + 'get_driver_abbreviations_by_team', + 'get_driver_color', + 'get_driver_color_mapping', + 'get_driver_name', + 'get_driver_names_by_team', + 'get_driver_style', + 'get_team_color', + 'get_team_name', + 'get_team_name_by_driver', + 'list_compounds', + 'list_driver_abbreviations', + 'list_driver_names', + 'list_team_names', + 'override_team_constants', + 'set_default_colormap', + 'setup_mpl', + + # imported, legacy + 'driver_color', + 'lapnumber_axis', + 'team_color', + + # legacy + 'COMPOUND_COLORS', + 'DRIVER_COLORS', + 'DRIVER_TRANSLATE', + 'TEAM_COLORS', + 'TEAM_TRANSLATE', + 'COLOR_PALETTE' +] + + +def __getattr__(name): + if name in ('COMPOUND_COLORS', 'DRIVER_TRANSLATE', 'DRIVER_COLORS', + 'TEAM_COLORS', 'TEAM_TRANSLATE', 'COLOR_PALETTE'): + warnings.warn(f"{name} is deprecated and will be removed in a future " + f"version.", FutureWarning) + + return globals()[f"_DEPR_{name}"] + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +_DEPR_COMPOUND_COLORS: Dict[str, str] = { + key: val for key, val + in _Constants['2024'].CompoundColors.items() +} +COMPOUND_COLORS: Dict[str, str] +""" +Mapping of tyre compound names to compound colors (hex color codes). +(current season only) + +.. deprecated:: 3.4.0 + The ``COMPOUND_COLORS`` dictionary is deprecated and will be removed in a + future version. Use :func:`~fastf1.plotting.get_compound_color` or + :func:`~fastf1.plotting.get_compound_mapping` instead. +""" + + +_DEPR_DRIVER_COLORS: Dict[str, str] = _LEGACY_DRIVER_COLORS.copy() +DRIVER_COLORS: Dict[str, str] +""" +Mapping of driver names to driver colors (hex color codes). + +.. warning:: + This dictionary will no longer be updated to include new drivers. Use + the new API instead. + +.. deprecated:: 3.4.0 + The ``DRIVER_COLORS`` dictionary is deprecated and will ber removed in a + future version. Use :func:`~fastf1.plotting.get_driver_color` or + :func:`~fastf1.plotting.get_driver_color_mapping` instead. +""" + + +_DEPR_DRIVER_TRANSLATE: Dict[str, str] = _LEGACY_DRIVER_TRANSLATE.copy() +DRIVER_TRANSLATE: Dict[str, str] +""" +Mapping of driver names to theirs respective abbreviations. + +.. warning:: + This dictionary will no longer be updated to include new drivers. Use + the new API instead. + + +.. deprecated:: 3.4.0 + The ``DRIVER_TRANSLATE`` dictionary is deprecated and will be removed in a + future version. Use :func:`~fastf1.plotting.get_driver_name` instead. +""" + +_DEPR_TEAM_COLORS: Dict[str, str] = { + # str(key.value): val for key, val + # in _Constants['2024'].Colormaps[_Colormaps.Default].items() + name.replace("kick ", ""): team.TeamColor.FastF1 for name, team + in _Constants['2024'].Teams.items() +} +TEAM_COLORS: Dict[str, str] +""" +Mapping of team names to team colors (hex color codes). +(current season only) + +.. deprecated:: 3.4.0 + The ``TEAM_COLORS`` dictionary is deprecated and will be removed in a + future version. Use :func:`~fastf1.plotting.get_team_color` instead. +""" + +_DEPR_TEAM_TRANSLATE: Dict[str, str] = _LEGACY_TEAM_TRANSLATE.copy() +TEAM_TRANSLATE: Dict[str, str] +""" +Mapping of team names to theirs respective abbreviations. + +.. deprecated:: 3.4.0 + The ``TEAM_TRANSLATE`` dictionary is deprecated and will be removed in a + future version. Use :func:`~fastf1.plotting.get_team_name` instead. +""" + +_DEPR_COLOR_PALETTE: List[str] = _COLOR_PALETTE.copy() +COLOR_PALETTE: List[str] +""" +The default color palette for matplotlib plot lines in fastf1's color scheme. + +.. deprecated:: 3.4.0 + The ``COLOR_PALETTE`` list is deprecated and will be removed in a + future version with no replacement. +""" diff --git a/fastf1/plotting/_backend.py b/fastf1/plotting/_backend.py new file mode 100644 index 000000000..02c3ac2ea --- /dev/null +++ b/fastf1/plotting/_backend.py @@ -0,0 +1,82 @@ +import dataclasses +from typing import ( + Dict, + List +) + +import fastf1._api +from fastf1.plotting._base import ( + _Driver, + _logger, + _normalize_string, + _Team +) +from fastf1.plotting._constants import Constants + + +def _load_drivers_from_f1_livetiming( + *, api_path: str, year: str +) -> List[_Team]: + # load the driver information for the determined session + driver_info = fastf1._api.driver_info(api_path) + + # parse the data into the required format + teams: Dict[str, _Team] = dict() + + # Sorting by driver number here will directly guarantee that drivers + # are sorted by driver number within each team. This has two advantages: + # - the driver index in a team is consistent as long as the drivers don't + # change/reserver drivers are used/... + # - the reigning champion (number 1) always has index 0, i.e. gets the + # primary style + for num in sorted(driver_info.keys()): + driver_entry = driver_info[num] + team_name = driver_entry.get('TeamName') + + if team_name in teams: + team = teams[team_name] + else: + team = _Team() + team.value = team_name + + abbreviation = driver_entry.get('Tla') + + name = ' '.join((driver_entry.get('FirstName'), + driver_entry.get('LastName'))) + driver = _Driver() + driver.value = name + driver.normalized_value = _normalize_string(name).lower() + driver.abbreviation = abbreviation + driver.team = team + + team.drivers.append(driver) + + if team not in teams: + normalized_full_team_name = _normalize_string(team_name).lower() + for ref_team_name, team_consts in Constants[year].Teams.items(): + if ref_team_name in normalized_full_team_name: + team.normalized_value = ref_team_name + + # copy team constants, update the official color if it + # is available from the API and add the constants to the + # team + if team_color := driver_entry.get('TeamColour'): + replacements = {'Official': f"#{team_color}"} + else: + replacements = {} + colors = dataclasses.replace( + team_consts.TeamColor, **replacements + ) + team.constants = dataclasses.replace( + team_consts, TeamColor=colors + ) + + break + else: + _logger.warning(f"Encountered unknown team '{team_name}' " + f"while loading driver-team mapping.") + continue + + teams[team_name] = team + + return list(teams.values()) diff --git a/fastf1/plotting/_base.py b/fastf1/plotting/_base.py new file mode 100644 index 000000000..e1a4cdfc2 --- /dev/null +++ b/fastf1/plotting/_base.py @@ -0,0 +1,56 @@ +import unicodedata +from typing import ( + Dict, + List +) + +from fastf1.logger import get_logger +from fastf1.plotting._constants.base import TeamConst + + +_logger = get_logger(__package__) + + +class _Driver: + value: str = '' + normalized_value: str = '' + abbreviation: str = '' + team: "_Team" + + +class _Team: + value: str = '' + normalized_value: str = '' + constants: TeamConst = None + + def __init__(self): + super().__init__() + self.drivers: List["_Driver"] = list() + + +class _DriverTeamMapping: + def __init__( + self, + year: str, + teams: List[_Team], + ): + self.year = year + self.teams = teams + + self.drivers_by_normalized: Dict[str, _Driver] = dict() + self.drivers_by_abbreviation: Dict[str, _Driver] = dict() + self.teams_by_normalized: Dict[str, _Team] = dict() + + for team in teams: + for driver in team.drivers: + self.drivers_by_normalized[driver.normalized_value] = driver + self.drivers_by_abbreviation[driver.abbreviation] = driver + self.teams_by_normalized[team.normalized_value] = team + + +def _normalize_string(name: str) -> str: + # removes accents from a string and returns the closest possible + # ascii representation (https://stackoverflow.com/a/518232) + stripped = ''.join(c for c in unicodedata.normalize('NFD', name) + if unicodedata.category(c) != 'Mn') + return stripped diff --git a/fastf1/plotting/_constants/__init__.py b/fastf1/plotting/_constants/__init__.py new file mode 100644 index 000000000..34fe5fbc4 --- /dev/null +++ b/fastf1/plotting/_constants/__init__.py @@ -0,0 +1,117 @@ +from typing import Dict + +from fastf1.plotting._constants import ( # noqa: F401, unused import used through globals() + season2018, + season2019, + season2020, + season2021, + season2022, + season2023, + season2024 +) +from fastf1.plotting._constants.base import BaseSeasonConst + + +Constants: Dict[str, BaseSeasonConst] = dict() + +for year in range(2018, 2025): + season = globals()[f"season{year}"] + Constants[str(year)] = BaseSeasonConst( + CompoundColors=season.CompoundColors, + Teams=season.Teams + ) + + +# Deprecated, will be removed for 2025 +LEGACY_TEAM_COLORS = { + 'mercedes': '#00d2be', 'ferrari': '#dc0000', + 'red bull': '#fcd700', 'mclaren': '#ff8700', + 'alpine': '#fe86bc', 'aston martin': '#006f62', + 'sauber': '#00e701', 'visa rb': '#1634cb', + 'haas': '#ffffff', 'williams': '#00a0dd' +} + + +LEGACY_TEAM_TRANSLATE: Dict[str, str] = { + 'MER': 'mercedes', + 'FER': 'ferrari', + 'RBR': 'red bull', + 'MCL': 'mclaren', + 'APN': 'alpine', + 'AMR': 'aston martin', + 'SAU': 'sauber', + 'RB': 'rb', + 'HAA': 'haas', + 'WIL': 'williams' +} + + +LEGACY_DRIVER_COLORS: Dict[str, str] = { + "valtteri bottas": "#00e701", + "zhou guanyu": "#008d01", + "theo pourchaire": "#004601", + + "nyck de vries": "#1e3d61", + "yuki tsunoda": "#356cac", + "daniel ricciardo": "#2b4562", + "liam lawson": "#2b4562", + "isack hadjar": "#1e6176", + "ayumu iwasa": "#1e6176", + + "pierre gasly": "#fe86bc", + "esteban ocon": "#ff117c", + "jack doohan": "#894667", + + "fernando alonso": "#006f62", + "lance stroll": "#00413b", + "felipe drugovich": "#2f9b90", + + "charles leclerc": "#dc0000", + "carlos sainz": "#ff8181", + "robert shwartzman": "#9c0000", + "oliver bearman": "#c40000", + + "kevin magnussen": "#ffffff", + "nico hulkenberg": "#cacaca", + + "oscar piastri": "#ff8700", + "lando norris": "#eeb370", + "pato oward": "#ee6d3a", + + "lewis hamilton": "#00d2be", + "george russell": "#24ffff", + "frederik vesti": "#00a6ff", + + "max verstappen": "#fcd700", + "sergio perez": "#ffec7b", + "jake dennis": "#907400", + + "alexander albon": "#005aff", + "logan sargeant": "#012564", + "zak osullivan": "#1b3d97", + "franco colapinto": "#639aff" +} + + +LEGACY_DRIVER_TRANSLATE: Dict[str, str] = { + 'LEC': 'charles leclerc', 'SAI': 'carlos sainz', + 'SHW': 'robert shwartzman', + 'VER': 'max verstappen', 'PER': 'sergio perez', + 'DEN': 'jake dennis', + 'PIA': 'oscar piastri', 'NOR': 'lando norris', + 'OWA': 'pato oward', + 'GAS': 'pierre gasly', 'OCO': 'esteban ocon', + 'DOO': 'jack doohan', + 'BOT': 'valtteri bottas', 'ZHO': 'zhou guanyu', + 'POU': 'theo pourchaire', + 'DEV': 'nyck de vries', 'TSU': 'yuki tsunoda', + 'RIC': 'daniel ricciardo', 'LAW': 'liam lawson', + 'HAD': 'isack hadjar', 'IWA': 'ayumu iwasa', + 'MAG': 'kevin magnussen', 'HUL': 'nico hulkenberg', + 'BEA': 'oliver bearman', + 'ALO': 'fernando alonso', 'STR': 'lance stroll', + 'DRU': 'felipe drugovich', + 'HAM': 'lewis hamilton', 'RUS': 'george russell', + 'VES': 'frederik vesti', + 'ALB': 'alexander albon', 'SAR': 'logan sargeant', + 'OSU': 'zak osullivan', 'COL': 'franco colapinto'} diff --git a/fastf1/plotting/_constants/base.py b/fastf1/plotting/_constants/base.py new file mode 100644 index 000000000..b492fbaee --- /dev/null +++ b/fastf1/plotting/_constants/base.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from typing import Dict + + +class CompoundsConst: + HyperSoft = "HYPERSOFT" + UltraSoft = "ULTRASOFT" + SuperSoft = "SUPERSOFT" + Soft = "SOFT" + Medium = "MEDIUM" + Hard = "HARD" + SuperHard = "SUPERHARD" + Intermediate = "INTERMEDIATE" + Wet = "WET" + Unknown = "UNKNOWN" + TestUnknown = "TEST-UNKNOWN" + + +@dataclass(frozen=True) +class TeamColorsConst: + Official: str + FastF1: str + + +@dataclass(frozen=True) +class TeamConst: + ShortName: str + TeamColor: TeamColorsConst + + +@dataclass(frozen=True) +class BaseSeasonConst: + CompoundColors: Dict[str, str] + Teams: Dict[str, TeamConst] diff --git a/fastf1/plotting/_constants/season2018.py b/fastf1/plotting/_constants/season2018.py new file mode 100644 index 000000000..c8bb81280 --- /dev/null +++ b/fastf1/plotting/_constants/season2018.py @@ -0,0 +1,106 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#dc0000', + FastF1='#dc0000' + ) + ), + 'force india': TeamConst( + ShortName='Force India', + TeamColor=TeamColorsConst( + Official='#f596c8', + FastF1='#ff87bc' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#828282', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#ff8000', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#00d2be', + FastF1='#00f5d0' + ) + ), + 'racing point': TeamConst( + ShortName='Racing Point', + TeamColor=TeamColorsConst( + Official='#f596c8', + FastF1='#ff87bc' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#1e41ff', + FastF1='#1e41ff' + ) + ), + 'renault': TeamConst( + ShortName='Renault', + TeamColor=TeamColorsConst( + Official='#fff500', + FastF1='#fff500' + ) + ), + 'sauber': TeamConst( + ShortName='Sauber', + TeamColor=TeamColorsConst( + Official='#9b0000', + FastF1='#900000' + ) + ), + 'toro rosso': TeamConst( + ShortName='Toro Rosso', + TeamColor=TeamColorsConst( + Official='#469bff', + FastF1='#2b4562' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#ffffff', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.HyperSoft: "#feb1c1", + CompoundsConst.UltraSoft: "#b24ba7", + CompoundsConst.SuperSoft: "#fc2b2a", + CompoundsConst.Soft: "#ffd318", + CompoundsConst.Medium: "#f0f0f0", + CompoundsConst.Hard: "#00a2f5", + CompoundsConst.SuperHard: "#fd7d3c", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2019.py b/fastf1/plotting/_constants/season2019.py new file mode 100644 index 000000000..7c786d602 --- /dev/null +++ b/fastf1/plotting/_constants/season2019.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alfa romeo': TeamConst( + ShortName='Alfa Romeo', + TeamColor=TeamColorsConst( + Official='#9b0000', + FastF1='#900000' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#bd9e57', + FastF1='#bd9e57' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#dc0000', + FastF1='#da291c' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#ff8700', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#00d2be', + FastF1='#00d2be' + ) + ), + 'racing point': TeamConst( + ShortName='Racing Point', + TeamColor=TeamColorsConst( + Official='#f596c8', + FastF1='#ff87bc' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#1e41ff', + FastF1='#1e41ff' + ) + ), + 'renault': TeamConst( + ShortName='Renault', + TeamColor=TeamColorsConst( + Official='#fff500', + FastF1='#fff500' + ) + ), + 'toro rosso': TeamConst( + ShortName='Toro Rosso', + TeamColor=TeamColorsConst( + Official='#469bff', + FastF1='#2b4562' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#ffffff', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2020.py b/fastf1/plotting/_constants/season2020.py new file mode 100644 index 000000000..c4f13ddfe --- /dev/null +++ b/fastf1/plotting/_constants/season2020.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alfa romeo': TeamConst( + ShortName='Alfa Romeo', + TeamColor=TeamColorsConst( + Official='#9B0000', + FastF1='#900000' + ) + ), + 'alphatauri': TeamConst( + ShortName='AlphaTauri', + TeamColor=TeamColorsConst( + Official='#ffffff', + FastF1='#2b4562' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#dc0000', + FastF1='#dc0000' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#787878', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#ff8700', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#00d2be', + FastF1='#00d2be' + ) + ), + 'racing point': TeamConst( + ShortName='Racing Point', + TeamColor=TeamColorsConst( + Official='#f596c8', + FastF1='#ff87bc' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#1e41ff', + FastF1='#1e41ff' + ) + ), + 'renault': TeamConst( + ShortName='Renault', + TeamColor=TeamColorsConst( + Official='#fff500', + FastF1='#fff500' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#0082fa', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2021.py b/fastf1/plotting/_constants/season2021.py new file mode 100644 index 000000000..3a03b274e --- /dev/null +++ b/fastf1/plotting/_constants/season2021.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alfa romeo': TeamConst( + ShortName='Alfa Romeo', + TeamColor=TeamColorsConst( + Official='#900000', + FastF1='#900000' + ) + ), + 'alphatauri': TeamConst( + ShortName='AlphaTauri', + TeamColor=TeamColorsConst( + Official='#2b4562', + FastF1='#2b4562' + ) + ), + 'alpine': TeamConst( + ShortName='Alpine', + TeamColor=TeamColorsConst( + Official='#0090ff', + FastF1='#0755ab' + ) + ), + 'aston martin': TeamConst( + ShortName='Aston Martin', + TeamColor=TeamColorsConst( + Official='#006f62', + FastF1='#00665e' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#dc0004', + FastF1='#dc0004' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#ffffff', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#ff9800', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#00d2be', + FastF1='#00f5d0' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#0600ef', + FastF1='#0600ef' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#005aff', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2022.py b/fastf1/plotting/_constants/season2022.py new file mode 100644 index 000000000..4f09b282c --- /dev/null +++ b/fastf1/plotting/_constants/season2022.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alfa romeo': TeamConst( + ShortName='Alfa Romeo', + TeamColor=TeamColorsConst( + Official='#b12039', + FastF1='#900000' + ) + ), + 'alphatauri': TeamConst( + ShortName='AlphaTauri', + TeamColor=TeamColorsConst( + Official='#4e7c9b', + FastF1='#2b4562' + ) + ), + 'alpine': TeamConst( + ShortName='Alpine', + TeamColor=TeamColorsConst( + Official='#2293d1', + FastF1='#fe86bc' + ) + ), + 'aston martin': TeamConst( + ShortName='Aston Martin', + TeamColor=TeamColorsConst( + Official='#2d826d', + FastF1='#00665e' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#ed1c24', + FastF1='#da291c' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#b6babd', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#f58020', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#6cd3bf', + FastF1='#00f5d0' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#1e5bc6', + FastF1='#0600ef' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#37bedd', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2023.py b/fastf1/plotting/_constants/season2023.py new file mode 100644 index 000000000..be88f828e --- /dev/null +++ b/fastf1/plotting/_constants/season2023.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, it the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alfa romeo': TeamConst( + ShortName='Alfa Romeo', + TeamColor=TeamColorsConst( + Official='#C92D4B', + FastF1='#900000' + ) + ), + 'alphatauri': TeamConst( + ShortName='AlphaTauri', + TeamColor=TeamColorsConst( + Official='#5E8FAA', + FastF1='#2b4562' + ) + ), + 'alpine': TeamConst( + ShortName='Alpine', + TeamColor=TeamColorsConst( + Official='#2293D1', + FastF1='#fe86bc' + ) + ), + 'aston martin': TeamConst( + ShortName='Aston Martin', + TeamColor=TeamColorsConst( + Official='#358C75', + FastF1='#00665e' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#F91536', + FastF1='#da291c' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#b6babd', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#F58020', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#6CD3BF', + FastF1='#00f5d0' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#3671C6', + FastF1='#0600ef' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#37BEDD', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_constants/season2024.py b/fastf1/plotting/_constants/season2024.py new file mode 100644 index 000000000..b067d74b1 --- /dev/null +++ b/fastf1/plotting/_constants/season2024.py @@ -0,0 +1,95 @@ +from typing import Dict + +from fastf1.plotting._constants.base import ( + CompoundsConst, + TeamColorsConst, + TeamConst +) + + +# NOTE: the team constants are copied when loading the driver-team-mapping +# and values may be modified there, if the used API provides different values + + +Teams: Dict[str, TeamConst] = { + 'alpine': TeamConst( + ShortName='Alpine', + TeamColor=TeamColorsConst( + Official='#0093cc', + FastF1='#ff87bc' + ) + ), + 'aston martin': TeamConst( + ShortName='Aston Martin', + TeamColor=TeamColorsConst( + Official='#229971', + FastF1='#00665f' + ) + ), + 'ferrari': TeamConst( + ShortName='Ferrari', + TeamColor=TeamColorsConst( + Official='#e8002d', + FastF1='#e8002d' + ) + ), + 'haas': TeamConst( + ShortName='Haas', + TeamColor=TeamColorsConst( + Official='#b6babd', + FastF1='#b6babd' + ) + ), + 'mclaren': TeamConst( + ShortName='McLaren', + TeamColor=TeamColorsConst( + Official='#ff8000', + FastF1='#ff8000' + ) + ), + 'mercedes': TeamConst( + ShortName='Mercedes', + TeamColor=TeamColorsConst( + Official='#27f4d2', + FastF1='#27f4d2' + ) + ), + 'rb': TeamConst( + ShortName='RB', + TeamColor=TeamColorsConst( + Official='#6692ff', + FastF1='#364aa9' + ) + ), + 'red bull': TeamConst( + ShortName='Red Bull', + TeamColor=TeamColorsConst( + Official='#3671c6', + FastF1='#0600ef' + ) + ), + 'kick sauber': TeamConst( + ShortName='Sauber', + TeamColor=TeamColorsConst( + Official='#52e252', + FastF1='#00e700' + ) + ), + 'williams': TeamConst( + ShortName='Williams', + TeamColor=TeamColorsConst( + Official='#64c4ff', + FastF1='#00a0dd' + ) + ) +} + +CompoundColors: Dict[CompoundsConst, str] = { + CompoundsConst.Soft: "#da291c", + CompoundsConst.Medium: "#ffd12e", + CompoundsConst.Hard: "#f0f0ec", + CompoundsConst.Intermediate: "#43b02a", + CompoundsConst.Wet: "#0067ad", + CompoundsConst.Unknown: "#00ffff", + CompoundsConst.TestUnknown: "#434649" +} diff --git a/fastf1/plotting/_interface.py b/fastf1/plotting/_interface.py new file mode 100644 index 000000000..d0d085836 --- /dev/null +++ b/fastf1/plotting/_interface.py @@ -0,0 +1,828 @@ +import dataclasses +from typing import ( + Any, + Dict, + List, + Literal, + Optional, + Sequence, + Union +) + +import matplotlib.axes + +from fastf1.core import Session +from fastf1.internals.fuzzy import fuzzy_matcher +from fastf1.plotting._backend import _load_drivers_from_f1_livetiming +from fastf1.plotting._base import ( + _Driver, + _DriverTeamMapping, + _logger, + _normalize_string, + _Team +) +from fastf1.plotting._constants import Constants as _Constants + + +_DEFAULT_COLOR_MAP: Literal['fastf1', 'official'] = 'fastf1' +_DRIVER_TEAM_MAPPINGS = dict() + + +def _get_driver_team_mapping( + session: Session +) -> "_DriverTeamMapping": + # driver-team mappings are generated once for each session and then reused + # on future calls + api_path = session.api_path + year = str(session.event['EventDate'].year) + + if api_path not in _DRIVER_TEAM_MAPPINGS: + teams = _load_drivers_from_f1_livetiming( + api_path=api_path, year=year + ) + mapping = _DriverTeamMapping(year, teams) + _DRIVER_TEAM_MAPPINGS[api_path] = mapping + + return _DRIVER_TEAM_MAPPINGS[api_path] + + +def _get_driver( + identifier: str, session: Session, *, exact_match: bool = False +) -> _Driver: + if exact_match: + return _get_driver_exact(identifier, session) + return _get_driver_fuzzy(identifier, session) + + +def _get_driver_fuzzy(identifier: str, session: Session) -> _Driver: + dtm = _get_driver_team_mapping(session) + identifier = _normalize_string(identifier).lower() + + # try driver abbreviation first + if (abb := identifier.upper()) in dtm.drivers_by_abbreviation: + return dtm.drivers_by_abbreviation[abb] + + # check for an exact driver name match + if identifier in dtm.drivers_by_normalized: + return dtm.drivers_by_normalized[identifier] + + # check for exact partial string match + for normalized_driver in dtm.drivers_by_normalized.keys(): + if identifier in normalized_driver: + return dtm.drivers_by_normalized[normalized_driver] + + # do fuzzy string matching + drivers = list(dtm.drivers_by_normalized.values()) + strings = [[driver.normalized_value, ] for driver in drivers] + index, exact = fuzzy_matcher(query=identifier, + reference=strings, + abs_confidence=0.35, + rel_confidence=0.30) + normalized_driver = drivers[index].normalized_value + + if not exact: + _logger.warning(f"Correcting user input '{identifier}' to " + f"'{normalized_driver}'") + + return dtm.drivers_by_normalized[normalized_driver] + + +def _get_driver_exact(identifier: str, session: Session) -> _Driver: + dtm = _get_driver_team_mapping(session) + identifier = _normalize_string(identifier).lower() + + # try driver abbreviation first + if (abb := identifier.upper()) in dtm.drivers_by_abbreviation: + return dtm.drivers_by_abbreviation[abb] + + # check for an exact driver name match + if identifier in dtm.drivers_by_normalized: + return dtm.drivers_by_normalized[identifier] + + raise KeyError(f"No driver found for '{identifier}' (exact match only)") + + +def _get_team( + identifier: str, session: Session, *, exact_match=False +) -> _Team: + if exact_match: + return _get_team_exact(identifier, session) + return _get_team_fuzzy(identifier, session) + + +def _get_team_fuzzy(identifier: str, session: Session) -> _Team: + dtm = _get_driver_team_mapping(session) + identifier = _normalize_string(identifier).lower() + + # remove common non-unique words + for word in ('racing', 'team', 'f1', 'scuderia'): + identifier = identifier.replace(word, "") + + # check for an exact team name match + if identifier in dtm.teams_by_normalized.keys(): + return dtm.teams_by_normalized[identifier] + + # check full match with full team name or for exact partial string + # match with normalized team name + for normalized, team in dtm.teams_by_normalized.items(): + if (identifier == team.value.casefold()) or (identifier in normalized): + return dtm.teams_by_normalized[normalized] + + # do fuzzy string match + teams = list(dtm.teams_by_normalized.values()) + strings = [[team.normalized_value, ] for team in teams] + index, exact = fuzzy_matcher(query=identifier, + reference=strings, + abs_confidence=0.35, + rel_confidence=0.30) + normalized_team_name = teams[index].normalized_value + + if not exact: + _logger.warning(f"Correcting user input '{identifier}' to " + f"'{normalized_team_name}'") + + return dtm.teams_by_normalized[normalized_team_name] + + +def _get_team_exact(identifier: str, session: Session) -> _Team: + dtm = _get_driver_team_mapping(session) + identifier = _normalize_string(identifier).lower() + + # check for an exact normalized team name match + if identifier in dtm.teams_by_normalized.keys(): + return dtm.teams_by_normalized[identifier] + + # check full match with full team name + for normalized, full in dtm.teams_by_normalized.items(): + if identifier == full.value.casefold(): + return dtm.teams_by_normalized[normalized] + + raise KeyError(f"No team found for '{identifier}' (exact match only)") + + +def _get_driver_color( + identifier: str, + session: Session, + *, + colormap: str = 'default', + exact_match: bool = False, + _variants: bool = False +) -> str: + driver = _get_driver(identifier, session, exact_match=exact_match) + team_name = driver.team.normalized_value + + return _get_team_color(team_name, session, colormap=colormap, + exact_match=True) + + +def _get_team_color( + identifier: str, + session: Session, + *, + colormap: str = 'default', + exact_match: bool = False +) -> str: + dtm = _get_driver_team_mapping(session) + + if dtm.year not in _Constants.keys(): + raise ValueError(f"No team colors for year '{dtm.year}'") + + team = _get_team( + identifier, session, exact_match=exact_match + ) + + if colormap == 'default': + colormap = _DEFAULT_COLOR_MAP + + if colormap == 'fastf1': + return team.constants.TeamColor.FastF1 + elif colormap == 'official': + return team.constants.TeamColor.Official + else: + raise ValueError(f"Invalid colormap '{colormap}'") + + +def get_team_name( + identifier: str, + session: Session, + *, + short: bool = False, + exact_match: bool = False +) -> str: + """ + Get a full or shortened team name based on a recognizable and identifiable + part of the team name. + + The short version of the team name is intended for saving space when + annotating plots and may skip parts of the official team name, for example + "Haas F1 Team" becomes just "Haas". + + Args: + identifier: a recognizable part of the team name + session: the session for which the data should be obtained + short: if True, a shortened version of the team name will be returned + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + team = _get_team(identifier, session, exact_match=exact_match) + + if short: + return team.constants.ShortName + + return team.value + + +def get_team_name_by_driver( + identifier: str, + session: Session, + *, + short: bool = False, + exact_match: bool = False +) -> str: + """ + Get a full team name based on a driver's abbreviation or based on a + recognizable and identifiable part of a driver's name. + + Alternatively, a shortened version of the team name can be returned. The + short version is intended for saving as much space as possible when + annotating plots and may skip parts of the official team name. + + Args: + identifier: driver abbreviation or recognizable part of the driver name + session: the session for which the data should be obtained + short: if True, a shortened version of the team name will be returned + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + driver = _get_driver(identifier, session, exact_match=exact_match) + team = driver.team + + if short: + return team.constants.ShortName + + return team.value + + +def get_team_color( + identifier: str, + session: Session, + *, + colormap: str = 'default', + exact_match: bool = False +) -> str: + """ + Get a team color based on a recognizable and identifiable part of + the team name. + + The team color is returned as a hexadecimal RGB color code. + + Args: + identifier: a recognizable part of the team name + session: the session for which the data should be obtained + colormap: one of ``'default'``, ``'fastf1'`` or ``'official'``. + The default colormap is ``'fastf1'``. Use + :func:`~fastf1.plotting.set_default_colormap` to change it. + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + + Returns: + A hexadecimal RGB color code + """ + return _get_team_color(identifier, session, + colormap=colormap, + exact_match=exact_match) + + +def get_driver_name( + identifier: str, session: Session, *, exact_match: bool = False +) -> str: + """ + Get a full driver name based on the driver's abbreviation or based on + a recognizable and identifiable part of the driver's name. + + Args: + identifier: driver abbreviation or recognizable part of the driver name + session: the session for which the data should be obtained + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + driver = _get_driver(identifier, session, exact_match=exact_match) + return driver.value + + +def get_driver_abbreviation( + identifier: str, session: Session, *, exact_match: bool = False +) -> str: + """ + Get a driver's abbreviation based on a recognizable and identifiable + part of the driver's name. + + Note that the driver's abbreviation, if given exactly, is also a valid + identifier. In this case the same value is returned as was given as the + identifier. + + Args: + identifier: recognizable part of the driver's name (or the + driver's abbreviation) + session: the session for which the data should be obtained + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + driver = _get_driver(identifier, session, exact_match=exact_match) + return driver.abbreviation + + +def get_driver_names_by_team( + identifier: str, session: Session, *, exact_match: bool = False +) -> List[str]: + """ + Get a list of full names of all drivers that drove for a team in a given + session based on a recognizable and identifiable part of the team name. + + Args: + identifier: a recognizable part of the team name + session: the session for which the data should be obtained + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + team = _get_team(identifier, session, exact_match=exact_match) + return [driver.value for driver in team.drivers] + + +def get_driver_abbreviations_by_team( + identifier: str, session: Session, *, exact_match: bool = False +) -> List[str]: + """ + Get a list of abbreviations of all drivers that drove for a team in a given + session based on a recognizable and identifiable part of the team name. + + Args: + identifier: a recognizable part of the team name + session: the session for which the data should be obtained + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + """ + team = _get_team(identifier, session, exact_match=exact_match) + return [driver.abbreviation for driver in team.drivers] + + +def get_driver_color( + identifier: str, + session: Session, + *, + colormap: str = 'default', + exact_match: bool = False +) -> str: + """ + Get the color that is associated with a driver based on the driver's + abbreviation or based on a recognizable and identifiable part of the + driver's name. + + .. note:: This will simply return the team color of the team that the + driver participated for in this session. Contrary to older versions + of FastF1, there are no separate colors for each driver. You should use + styling options other than color if you need to differentiate drivers + of the same team. The function + :func:`~fastf1.plotting.get_driver_style` can help you to customize + the plot styling for each driver. + + Args: + identifier: driver abbreviation or recognizable part of the driver name + session: the session for which the data should be obtained + colormap: one of ``'default'``, ``'fastf1'`` or ``'official'``. + The default colormap is ``'fastf1'``. Use + :func:`~fastf1.plotting.set_default_colormap` to change it. + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + + Returns: + A hexadecimal RGB color code + + """ + return _get_driver_color(identifier, session, colormap=colormap, + exact_match=exact_match) + + +def get_driver_style( + identifier: str, + style: Union[str, Sequence[str], Sequence[dict]], + session: Session, + *, + colormap: str = 'default', + additional_color_kws: Union[list, tuple] = (), + exact_match: bool = False +) -> Dict[str, Any]: + """ + Get a plotting style that is unique for a driver based on the driver's + abbreviation or based on a recognizable and identifiable part of the + driver's name. + + This function simplifies the task of generating unique and easily + distinguishable visual styles for multiple drivers in a plot. + Primarily, the focus is on plotting with Matplotlib, but it is possible + to customize the behaviour for compatibility with other plotting libraries. + + The general idea for creating visual styles is as follows: + + 1. Set the primary color of the style to the color of the team for + which a driver is driving. This may be the line color in a line plot, + the marker color in a scatter plot, and so on. + + 2. Use one or multiple other styling options (line style, markers, ...) + to differentiate drivers in the same team. + + .. note:: It cannot be guaranteed that the styles are consistent throughout + a full season, especially in case of driver changes within a team. + + + **Option 1**: Rely on built-in styling options + + By default, this function supports the following Matplotlib plot arguments: + ``linestyle``, ``marker``, ``color``, ``facecolor``, ``edgecolor`` as well + as almost all other color-related arguments. + + The styling options include one color for each team and up to four + different line styles and marker styles within a team. That means that no + more than four different drivers are supported for a team in a single + session. This should be sufficent in almost all scenarios. + + The following example obtains the driver style for Alonso and Stroll in a + race in the 2023 season. The drivers should be represented using the + ``color`` and ``marker`` arguments, as may be useful in a scatter plot. + Both drivers were driving for the Aston Martin team, therefore, both + automatically get assigned the same color, which is the Aston Martin team + color. But both drivers get assigned a different marker style, so they can + be uniquely identified in the plot. + + Example: + + .. doctest:: + + >>> from fastf1 import get_session + >>> from fastf1.plotting import get_driver_style + >>> session = get_session(2023, 10, 'R') + >>> get_driver_style('ALO', ['color', 'marker'], session) + {'color': '#00665e', 'marker': 'x'} + >>> get_driver_style('STR', ['color', 'marker'], session) + {'color': '#00665e', 'marker': 'o'} + + **Option 2**: Provide a custom list of styling variants + + To allow for almost unlimited styling options, it is possible to specify + custom styling variants. These are not tied to any specific plotting + library. + + In the following example, a list with two custom stlyes is defined that are + then used to generate driver specific styles. Each style is represented by + a dictionary of keywords and values. + + The first driver in a team gets assigned the first style, the second driver + the second style and so on (if there are more than two drivers). It is + necessary to define at least as many styles as there are drivers in a team. + + The following things need to be noted: + + 1. The notion of first or second driver does not refer to any particular + reference and no specific order for drivers within a team is intended or + guranteed. + + 2. Any color-related key can make use of the "magic" ``'auto'`` value as + shown with Alonso in this example. When the color value is set to + ``'auto'`` it will automatically be replaced with the team color for this + driver. All color keywords that are used in Matplotlib should be recognized + automatically. You can define custom arguments as color arguments through + the ``additional_color_kws`` argument. + + 3. Each style dictionary can contain arbitrary keys and value. Therefore, + you are not limited to any particular plotting library. + + Example: + + .. doctest:: + + >>> from fastf1 import get_session + >>> from fastf1.plotting import get_driver_style + >>> session = get_session(2023, 10, 'R') + >>> my_styles = [ + ... {'linestyle': 'solid', 'color': 'auto', 'custom_arg': True}, + ... {'linestyle': 'dotted', 'color': '#FF0060', 'other_arg': 10} + ... ] + >>> get_driver_style('ALO', my_styles, session) + {'linestyle': 'solid', 'color': '#00665e', 'custom_arg': True} + >>> get_driver_style('STR', my_styles, session) + {'linestyle': 'dotted', 'color': '#FF0060', 'other_arg': 10} + + Args: + identifier: driver abbreviation or recognizable part of the driver name + style: list of matplotlib plot arguments that should be used for + styling or a list of custom style dictionaries + session: the session for which the data should be obtained + colormap: one of ``'default'``, ``'fastf1'`` or ``'official'``. + The default colormap is ``'fastf1'``. Use + :func:`~fastf1.plotting.set_default_colormap` to change it. + additional_color_kws: A list of keys that should additionally be + treated as colors. This is most usefull for making the magic + ``'auto'`` color work with custom styling options. + exact_match: match the identifier exactly (case-insensitive, special + characters are converted to their nearest ASCII equivalent) + + Returns: a dictionary of plot style arguments that can be directly passed + to a matplotlib plot function using the ``**`` expansion operator + + + .. minigallery:: fastf1.plotting.get_driver_style + :add-heading: + """ + stylers = { + 'linestyle': ['solid', 'dashed', 'dashdot', 'dotted'], + 'marker': ['x', 'o', '^', 'D'] + } + + # color keyword arguments that are supported by various matplotlib + # functions + color_kwargs = ( + # generic + 'color', 'colors', 'c', + # .plot + 'gapcolor', + 'markeredgecolor', 'mec', + 'markerfacecolor', 'mfc', + 'markerfacecoloralt', 'mfcalt', + # .scatter + 'facecolor', 'facecolors', 'fc', + 'edgecolor', 'edgecolors', 'ec', + # .errorbar + 'ecolor', + # add user defined color keyword arguments + *additional_color_kws + ) + + driver = _get_driver(identifier, session, exact_match=exact_match) + team = driver.team + idx = team.drivers.index(driver) + + if not style: + # catches empty list, tuple, str + raise ValueError("The provided style info is empty!") + + if isinstance(style, str): + style = [style] + + plot_style = dict() + + if isinstance(style[0], str): + # generate the plot style based on the provided keyword + # arguments + for opt in style: + if opt in color_kwargs: + value = _get_team_color(team.normalized_value, + session, + colormap=colormap, + exact_match=True) + elif opt in stylers: + value = stylers[opt][idx] + else: + raise ValueError(f"'{opt}' is not a supported styling " + f"option") + plot_style[opt] = value + + else: + try: + custom_style = style[idx] + except IndexError: + raise ValueError(f"The provided custom style info does not " + f"contain enough variants! (Has: {len(style)}, " + f"Required: {idx})") + + if not isinstance(custom_style, dict): + raise ValueError("The provided style info has an invalid format!") + + # copy the correct user provided style and replace any 'auto' + # colors with the correct color value + plot_style = custom_style.copy() + for kwarg in color_kwargs: + if plot_style.get(kwarg, None) == 'auto': + color = _get_team_color(team.normalized_value, + session, + colormap=colormap, + exact_match=True) + plot_style[kwarg] = color + + return plot_style + + +def get_compound_color(compound: str, session: Session) -> str: + """ + Get the compound color as hexadecimal RGB color code for a given compound. + + Args: + compound: the name of the compound + session: the session for which the data should be obtained + + Returns: + A hexadecimal RGB color code + """ + year = str(session.event['EventDate'].year) + return _Constants[year].CompoundColors[compound.upper()] + + +def get_compound_mapping(session: Session) -> Dict[str, str]: + """ + Returns a dictionary that maps compound names to their associated + colors. The colors are given as hexadecimal RGB color codes. + + Args: + session: the session for which the data should be obtained + + Returns: + dictionary mapping compound names to RGB hex colors + """ + year = str(session.event['EventDate'].year) + return _Constants[year].CompoundColors.copy() + + +def get_driver_color_mapping( + session: Session, *, colormap: str = 'default', +) -> Dict[str, str]: + """ + Returns a dictionary that maps driver abbreviations to their associated + colors. The colors are given as hexadecimal RGB color codes. + + Args: + session: the session for which the data should be obtained + colormap: one of ``'default'``, ``'fastf1'`` or ``'official'``. + The default colormap is ``'fastf1'``. Use + :func:`~fastf1.plotting.set_default_colormap` to change it. + Returns: + dictionary mapping driver abbreviations to RGB hex colors + """ + dtm = _get_driver_team_mapping(session) + + if colormap == 'default': + colormap = _DEFAULT_COLOR_MAP + + if colormap == 'fastf1': + colors = { + abb: driver.team.constants.TeamColor.FastF1 + for abb, driver in dtm.drivers_by_abbreviation.items() + } + elif colormap == 'official': + colors = { + abb: driver.team.constants.TeamColor.Official + for abb, driver in dtm.drivers_by_abbreviation.items() + } + else: + raise ValueError(f"Invalid colormap '{colormap}'") + + return colors + + +def list_team_names(session: Session, *, short: bool = False) -> List[str]: + """Returns a list of team names of all teams in the ``session``. + + By default, the full team names are returned. Use the ``short`` argument + to get shortened versions of the team names instead. + + Args: + session: the session for which the data should be obtained + short: if True, a list of the shortened team names is returned + + Returns: + a list of team names + """ + dtm = _get_driver_team_mapping(session) + + if short: + return list(team.constants.ShortName + for team in dtm.teams_by_normalized.values()) + + return list(team.value for team in dtm.teams_by_normalized.values()) + + +def list_driver_abbreviations(session: Session) -> List[str]: + """Returns a list of abbreviations of all drivers in the ``session``.""" + dtm = _get_driver_team_mapping(session) + return list(dtm.drivers_by_abbreviation.keys()) + + +def list_driver_names(session: Session) -> List[str]: + """Returns a list of full names of all drivers in the ``session``.""" + dtm = _get_driver_team_mapping(session) + return list(driver.value for driver in dtm.drivers_by_normalized.values()) + + +def list_compounds(session: Session) -> List[str]: + """Returns a list of all compound names for this season (not session).""" + year = str(session.event['EventDate'].year) + return list(_Constants[year].CompoundColors.keys()) + + +def add_sorted_driver_legend(ax: matplotlib.axes.Axes, session: Session): + """ + Adds a legend to the axis where drivers are grouped by team and within each + team they are shown in the same order that is used for selecting plot + styles. + + This function is a drop-in replacement for calling Matplotlib's + ``ax.legend()`` method. It can only be used when driver names or driver + abbreviations are used as labels for the legend. + + There is no particular need to use this function except to make the + legend more visually pleasing. + + Args: + ax: An instance of a Matplotlib ``Axes`` object + session: the session for which the data should be obtained + + Returns: + ``matplotlib.legend.Legend`` + + .. minigallery:: fastf1.plotting.add_sorted_driver_legend + :add-heading: + + """ + dtm = _get_driver_team_mapping(session) + handles, labels = ax.get_legend_handles_labels() + + teams_list = list(dtm.teams_by_normalized.values()) + driver_list = list(dtm.drivers_by_normalized.values()) + + # create an intermediary list where each element is a tuple that + # contains (team_idx, driver_idx, handle, label). Then sort this list + # based on the team_idx and driver_idx. As a result, drivers from the + # same team will be next to each other and in the same order as their + # styles are cycled. + ref = list() + for hdl, lbl in zip(handles, labels): + driver = _get_driver(lbl, session) + team = driver.team + + team_idx = teams_list.index(team) + driver_idx = driver_list.index(driver) + + ref.append((team_idx, driver_idx, hdl, lbl)) + + # sort based only on team_idx and driver_idx (i.e. first two entries) + ref.sort(key=lambda e: e[:2]) + + handles_new = list() + labels_new = list() + for elem in ref: + handles_new.append(elem[2]) + labels_new.append(elem[3]) + + return ax.legend(handles_new, labels_new) + + +def set_default_colormap(colormap: str): + """ + Set the default colormap that is used for color lookups. + + Args: + colormap: one of ``'fastf1'`` or ``'official'`` + """ + global _DEFAULT_COLOR_MAP + if colormap not in ('fastf1', 'official'): + raise ValueError(f"Invalid colormap '{colormap}'") + _DEFAULT_COLOR_MAP = colormap + + +def override_team_constants( + identifier: str, + session: Session, + *, + short_name: Optional[str] = None, + official_color: Optional[str] = None, + fastf1_color: Optional[str] = None +): + """ + Override the default team constants for a specific team. + + This function is intended for advanced users who want to customize the + default team constants. The changes are only applied for the current + session and do not persist. + + Args: + identifier: A part of the team name. Note that this function does + not support fuzzy matching and will raise a ``KeyError`` if no + exact and unambiguous match is found! + session: The session for which the override should be applied + short_name: New value for the short name of the team + official_color: New value for the team color in the "official" + color map; must be a hexadecimal RGB color code + fastf1_color: New value for the team color in the "fastf1" color map; + must be a hexadecimal RGB color code + """ + team = _get_team(identifier, session, exact_match=True) + + colors = team.constants.TeamColor + if official_color is not None: + colors = dataclasses.replace(colors, Official=official_color) + if fastf1_color is not None: + colors = dataclasses.replace(colors, FastF1=fastf1_color) + if (official_color is not None) or (fastf1_color is not None): + team.constants = dataclasses.replace(team.constants, TeamColor=colors) + + if short_name is not None: + team.constants = dataclasses.replace(team.constants, + ShortName=short_name) diff --git a/fastf1/plotting.py b/fastf1/plotting/_plotting.py similarity index 54% rename from fastf1/plotting.py rename to fastf1/plotting/_plotting.py index 9ac64d7b1..a75d4560a 100644 --- a/fastf1/plotting.py +++ b/fastf1/plotting/_plotting.py @@ -1,37 +1,12 @@ -""" -Helper functions for creating data plots. - -:mod:`fastf1.plotting` provides optional functionality with the intention of -making it easy to create nice plots. - -This module offers mainly two things: - - team names and colors - - matplotlib mods and helper functions - -Fast-F1 focuses on plotting data with matplotlib. Of course, you are not -required to use matplotlib and you can use any other tool you like. - -If you wish to use matplotlib, it is highly recommended to enable some -helper functions by calling :func:`setup_mpl`. - -If you don't want to use matplotlib, you can still use the team names -and colors which are provided below. - - -.. note:: Plotting related functionality is likely to change in a future - release. -""" import warnings from typing import ( - Dict, - List + List, + Optional ) import numpy as np import pandas as pd -from fastf1.logger import get_logger - try: import matplotlib @@ -39,166 +14,60 @@ from matplotlib import pyplot as plt except ImportError: warnings.warn("Failed to import optional dependency 'matplotlib'!" - "Plotting functionality will be unavailable!", UserWarning) + "Plotting functionality will be unavailable!", + RuntimeWarning) try: import timple except ImportError: warnings.warn("Failed to import optional dependency 'timple'!" "Plotting of timedelta values will be restricted!", - UserWarning) - -_logger = get_logger(__name__) - -with warnings.catch_warnings(): - warnings.filterwarnings('ignore', - message="Using slow pure-python SequenceMatcher") - # suppress that warning, it's confusing at best here, we don't need fast - # sequence matching and the installation (on windows) some effort - from rapidfuzz import fuzz - - -class __TeamColorsWarnDict(dict): - """Implements userwarning on KeyError in :any:`TEAM_COLORS` after - changing team names.""" - - def get(self, key, default=None): - value = super().get(key, default) - if value is None: - self.warn_change() - return value - - def __getitem__(self, item): - try: - return super().__getitem__(item) - except KeyError as err: - self.warn_change() - raise err - except Exception as err: - raise err - - def warn_change(self): - warnings.warn( - "Team names in `TEAM_COLORS` are now lower-case and only contain " - "the most identifying part of the name. " - "Use function `.team_color` alternatively.", UserWarning - ) + RuntimeWarning) + +from rapidfuzz import fuzz + +from fastf1.logger import get_logger +from fastf1.plotting._constants import ( + LEGACY_DRIVER_COLORS, + LEGACY_DRIVER_TRANSLATE, + LEGACY_TEAM_COLORS, + LEGACY_TEAM_TRANSLATE +) + + +_logger = get_logger(__package__) + + +_COLOR_PALETTE: List[str] = ['#FF79C6', '#50FA7B', '#8BE9FD', '#BD93F9', + '#FFB86C', '#FF5555', '#F1FA8C'] +# The default color palette for matplotlib plot lines in fastf1's color scheme -TEAM_COLORS = __TeamColorsWarnDict({ - 'mercedes': '#00d2be', 'ferrari': '#dc0000', - 'red bull': '#fcd700', 'mclaren': '#ff8700', - 'alpine': '#fe86bc', 'aston martin': '#006f62', - 'sauber': '#00e701', 'visa rb': '#1634cb', - 'haas': '#ffffff', 'williams': '#00a0dd' -}) -"""Mapping of team names to team colors (hex color codes). -(current season only)""" - -TEAM_TRANSLATE: Dict[str, str] = { - 'MER': 'mercedes', 'FER': 'ferrari', - 'RBR': 'red bull', 'MCL': 'mclaren', - 'APN': 'alpine', 'AMR': 'aston martin', - 'SAU': 'sauber', 'VRB': 'visa rb', - 'HAA': 'haas', 'WIL': 'williams'} -"""Mapping of team names to theirs respective abbreviations.""" - -DRIVER_COLORS: Dict[str, str] = { - "valtteri bottas": "#00e701", - "zhou guanyu": "#008d01", - "theo pourchaire": "#004601", - - "nyck de vries": "#1e3d61", - "yuki tsunoda": "#356cac", - "daniel ricciardo": "#2b4562", - "liam lawson": "#2b4562", - "isack hadjar": "#1e6176", - "ayumu iwasa": "#1e6176", - - "pierre gasly": "#fe86bc", - "esteban ocon": "#ff117c", - "jack doohan": "#894667", - - "fernando alonso": "#006f62", - "lance stroll": "#00413b", - "felipe drugovich": "#2f9b90", - - "charles leclerc": "#dc0000", - "carlos sainz": "#ff8181", - "robert shwartzman": "#9c0000", - "oliver bearman": "#c40000", - - "kevin magnussen": "#ffffff", - "nico hulkenberg": "#cacaca", - - "oscar piastri": "#ff8700", - "lando norris": "#eeb370", - "pato oward": "#ee6d3a", - - "lewis hamilton": "#00d2be", - "george russell": "#24ffff", - "frederik vesti": "#00a6ff", - - "max verstappen": "#fcd700", - "sergio perez": "#ffec7b", - "jake dennis": "#907400", - - "alexander albon": "#005aff", - "logan sargeant": "#012564", - "zak osullivan": "#1b3d97", - "franco colapinto": "#639aff" -} -"""Mapping of driver names to driver colors (hex color codes). -(current season only)""" - -DRIVER_TRANSLATE: Dict[str, str] = { - 'LEC': 'charles leclerc', 'SAI': 'carlos sainz', - 'SHW': 'robert shwartzman', - 'VER': 'max verstappen', 'PER': 'sergio perez', - 'DEN': 'jake dennis', - 'PIA': 'oscar piastri', 'NOR': 'lando norris', - 'OWA': 'pato oward', - 'GAS': 'pierre gasly', 'OCO': 'esteban ocon', - 'DOO': 'jack doohan', - 'BOT': 'valtteri bottas', 'ZHO': 'zhou guanyu', - 'POU': 'theo pourchaire', - 'DEV': 'nyck de vries', 'TSU': 'yuki tsunoda', - 'RIC': 'daniel ricciardo', 'LAW': 'liam lawson', - 'HAD': 'isack hadjar', 'IWA': 'ayumu iwasa', - 'MAG': 'kevin magnussen', 'HUL': 'nico hulkenberg', - 'BEA': 'oliver bearman', - 'ALO': 'fernando alonso', 'STR': 'lance stroll', - 'DRU': 'felipe drugovich', - 'HAM': 'lewis hamilton', 'RUS': 'george russell', - 'VES': 'frederik vesti', - 'ALB': 'alexander albon', 'SAR': 'logan sargeant', - 'OSU': 'zak osullivan', 'COL': 'franco colapinto'} -"""Mapping of driver names to theirs respective abbreviations.""" - -COMPOUND_COLORS: Dict[str, str] = { - "SOFT": "#da291c", - "MEDIUM": "#ffd12e", - "HARD": "#f0f0ec", - "INTERMEDIATE": "#43b02a", - "WET": "#0067ad", - "UNKNOWN": "#00ffff", - "TEST-UNKNOWN": "#434649" -} -"""Mapping of tyre compound names to compound colors (hex color codes). -(current season only)""" - -COLOR_PALETTE: List[str] = ['#FF79C6', '#50FA7B', '#8BE9FD', '#BD93F9', - '#FFB86C', '#FF5555', '#F1FA8C'] -"""The default color palette for matplotlib plot lines in fastf1's color -scheme.""" +class __DefaultStringArgType(str): + pass + + +__color_scheme_default_arg = __DefaultStringArgType('fastf1') def setup_mpl( - mpl_timedelta_support: bool = True, color_scheme: str = 'fastf1', + mpl_timedelta_support: bool = True, + color_scheme: Optional[str] = __color_scheme_default_arg, misc_mpl_mods: bool = True): """Setup matplotlib for use with fastf1. This is optional but, at least partly, highly recommended. + .. deprecated:: 3.4.0 + + The optional argument ``misc_mpls_mods`` is deprecated. + + .. deprecated:: 3.4.0 + + The default value for ``color_scheme`` will change from ``'fastf1'`` + to ``None``. You should explicitly set the desired value when calling + this function. + + Parameters: mpl_timedelta_support (bool): Matplotlib itself offers very limited functionality for plotting @@ -217,13 +86,25 @@ def setup_mpl( Valid color scheme names are: ['fastf1', None] misc_mpl_mods (bool): - This enables a collection of patches for the following mpl - features: - - - ``.savefig`` (saving of figures) - - ``.bar``/``.barh`` (plotting of bar graphs) - - ``plt.subplots`` (for creating a nice background grid) + This argument is deprecated since v3.4.0 and should no longer be + used. """ + if color_scheme is __color_scheme_default_arg: + warnings.warn( + "FastF1 will no longer silently modify the default Matplotlib " + "colors in the future.\nTo remove this warning, explicitly set " + "`color_scheme=None` or `color_scheme='fastf1'` when calling " + "`.setup_mpl()`.", FutureWarning + ) + + if misc_mpl_mods: + warnings.warn( + "FastF1 will stop modifying the default Matplotlib settings in " + "the future.\nTo opt-in to the new behaviour and remove this " + "warning, explicitly set `misc_mpl_mods=False` when calling " + "`.setup_mpl()`.", FutureWarning + ) + if mpl_timedelta_support: _enable_timple() if color_scheme == 'fastf1': @@ -233,33 +114,34 @@ def setup_mpl( def driver_color(identifier: str) -> str: - """Get a driver's color from a driver name or abbreviation. + """ + Get a driver's color from a driver name or abbreviation. + + .. deprecated:: 3.4.0 + This function is deprecated and will be removed in a future version. + Use :func:`~fastf1.plotting.get_driver_color` instead. This function will try to find a matching driver for any identifier string - that is passed to it. This involves case insensitive matching and partial + that is passed to it. This involves case-insensitive matching and partial string matching. - If you want exact string matching, you should use the - :any:`DRIVER_COLORS` dictionary directly, using :any:`DRIVER_TRANSLATE` to - convert abbreviations to team names if necessary. - Example:: - >>> driver_color('charles leclerc') + >>> driver_color('charles leclerc') # doctest: +SKIP '#dc0000' - >>> driver_color('max verstappen') + >>> driver_color('max verstappen') # doctest: +SKIP '#fcd700' - >>> driver_color('ver') + >>> driver_color('ver') # doctest: +SKIP '#fcd700' - >>> driver_color('lec') + >>> driver_color('lec') # doctest: +SKIP '#dc0000' shortened driver names and typos can be dealt with too (within reason) - >>> driver_color('Max Verst') + >>> driver_color('Max Verst') # doctest: +SKIP '#fcd700' - >>> driver_color('Charles') + >>> driver_color('Charles') # doctest: +SKIP '#dc0000' Args: @@ -269,25 +151,31 @@ def driver_color(identifier: str) -> str: Returns: str: hex color code """ + warnings.warn("The function `driver_color` is deprecated and will be " + "removed in a future version. Use " + "`fastf1.plotting.get_driver_color` instead.", + FutureWarning) - if identifier.upper() in DRIVER_TRANSLATE: + if identifier.upper() in LEGACY_DRIVER_TRANSLATE: # try short team abbreviations first - return DRIVER_COLORS[DRIVER_TRANSLATE[identifier.upper()]] + return LEGACY_DRIVER_COLORS[ + LEGACY_DRIVER_TRANSLATE[identifier.upper()] + ] else: identifier = identifier.lower() # check for an exact team name match - if identifier in DRIVER_COLORS: - return DRIVER_COLORS[identifier] + if identifier in LEGACY_DRIVER_COLORS: + return LEGACY_DRIVER_COLORS[identifier] # check for exact partial string match - for team_name, color in DRIVER_COLORS.items(): + for team_name, color in LEGACY_DRIVER_COLORS.items(): if identifier in team_name: return color # do fuzzy string matching key_ratios = list() - for existing_key in DRIVER_COLORS: + for existing_key in LEGACY_DRIVER_COLORS: ratio = fuzz.ratio(identifier, existing_key) key_ratios.append((ratio, existing_key)) key_ratios.sort(reverse=True) @@ -304,41 +192,42 @@ def driver_color(identifier: str) -> str: # than second best) raise KeyError best_matched_key = key_ratios[0][1] - return DRIVER_COLORS[best_matched_key] + return LEGACY_DRIVER_COLORS[best_matched_key] def team_color(identifier: str) -> str: - """Get a team's color from a team name or abbreviation. + """ + Get a team's color from a team name or abbreviation. + + .. deprecated:: 3.4.0 + This function is deprecated and will be removed in a future version. + Use :func:`~fastf1.plotting.get_team_color` instead. This function will try to find a matching team for any identifier string - that is passed to it. This involves case insensitive matching and partial + that is passed to it. This involves case-insensitive matching and partial string matching. - If you want exact string matching, you should use the - :any:`TEAM_COLORS` dictionary directly, using :any:`TEAM_TRANSLATE` to - convert abbreviations to team names if necessary. - Example:: - >>> team_color('Red Bull') + >>> team_color('Red Bull') # doctest: +SKIP '#fcd700' - >>> team_color('redbull') + >>> team_color('redbull') # doctest: +SKIP '#fcd700' - >>> team_color('Red') + >>> team_color('Red') # doctest: +SKIP '#fcd700' - >>> team_color('RBR') + >>> team_color('RBR') # doctest: +SKIP '#fcd700' - shortened team names, included sponsors and typos can be dealt with - too (within reason) + # shortened team names, included sponsors and typos can be dealt with + # too (within reason) - >>> team_color('Mercedes') + >>> team_color('Mercedes') # doctest: +SKIP '#00d2be' - >>> team_color('Merc') + >>> team_color('Merc') # doctest: +SKIP '#00d2be' - >>> team_color('Merecds') + >>> team_color('Merecds') # doctest: +SKIP '#00d2be' - >>> team_color('Mercedes-AMG Petronas F1 Team') + >>> team_color('Mercedes-AMG Petronas F1 Team') # doctest: +SKIP '#00d2be' Args: @@ -348,9 +237,14 @@ def team_color(identifier: str) -> str: Returns: str: hex color code """ - if identifier.upper() in TEAM_TRANSLATE: + warnings.warn("The function `team_color` is deprecated and will be " + "removed in a future version. Use " + "`fastf1.plotting.get_team_color` instead.", + FutureWarning) + + if identifier.upper() in LEGACY_TEAM_TRANSLATE: # try short team abbreviations first - return TEAM_COLORS[TEAM_TRANSLATE[identifier.upper()]] + return LEGACY_TEAM_COLORS[LEGACY_TEAM_TRANSLATE[identifier.upper()]] else: identifier = identifier.lower() # remove common non-unique words @@ -358,17 +252,17 @@ def team_color(identifier: str) -> str: identifier = identifier.replace(word, "") # check for an exact team name match - if identifier in TEAM_COLORS: - return TEAM_COLORS[identifier] + if identifier in LEGACY_TEAM_COLORS: + return LEGACY_TEAM_COLORS[identifier] # check for exact partial string match - for team_name, color in TEAM_COLORS.items(): + for team_name, color in LEGACY_TEAM_COLORS.items(): if identifier in team_name: return color # do fuzzy string matching key_ratios = list() - for existing_key in TEAM_COLORS.keys(): + for existing_key in LEGACY_TEAM_COLORS.keys(): ratio = fuzz.ratio(identifier, existing_key) key_ratios.append((ratio, existing_key)) key_ratios.sort(reverse=True) @@ -385,24 +279,7 @@ def team_color(identifier: str) -> str: # than second best) raise KeyError best_matched_key = key_ratios[0][1] - return TEAM_COLORS[best_matched_key] - - -def lapnumber_axis(ax, axis='xaxis'): - """Set axis to integer ticks only." - - Args: - ax: matplotlib axis - axis: can be 'xaxis' or 'yaxis' - - Returns: - the modified axis instance - - """ - getattr(ax, axis).get_major_locator().set_params(integer=True, - min_n_ticks=0) - - return ax + return LEGACY_TEAM_COLORS[best_matched_key] def _enable_timple(): @@ -501,9 +378,33 @@ def _enable_fastf1_color_scheme(): plt.rcParams['axes.titlesize'] = '19' plt.rcParams['axes.titlepad'] = '12' plt.rcParams['axes.titleweight'] = 'light' - plt.rcParams['axes.prop_cycle'] = cycler('color', COLOR_PALETTE) + plt.rcParams['axes.prop_cycle'] = cycler('color', _COLOR_PALETTE) plt.rcParams['legend.fancybox'] = False plt.rcParams['legend.facecolor'] = (0.1, 0.1, 0.1, 0.7) plt.rcParams['legend.edgecolor'] = (0.1, 0.1, 0.1, 0.9) plt.rcParams['savefig.transparent'] = False plt.rcParams['axes.axisbelow'] = True + + +def lapnumber_axis(ax, axis='xaxis'): + """ + Set axis to integer ticks only. + + .. deprecated:: 3.4.0 + The function ``lapnumber_axis`` is deprecated and will be removed in a + future version without replacement. + + Args: + ax: matplotlib axis + axis: can be 'xaxis' or 'yaxis' + + Returns: + the modified axis instance + """ + warnings.warn("The function `lapnumber_axis` is deprecated and will be " + "removed without replacement in a future version.", + FutureWarning) + getattr(ax, axis).get_major_locator().set_params(integer=True, + min_n_ticks=0) + + return ax diff --git a/fastf1/tests/mpl-baseline/test_doc_example_delta_time.png b/fastf1/tests/mpl-baseline/test_doc_example_delta_time.png index 91ac060be..847f505be 100644 Binary files a/fastf1/tests/mpl-baseline/test_doc_example_delta_time.png and b/fastf1/tests/mpl-baseline/test_doc_example_delta_time.png differ diff --git a/fastf1/tests/mpl-baseline/test_doc_example_fast_lec.png b/fastf1/tests/mpl-baseline/test_doc_example_fast_lec.png index 2be5f9801..7f6013a97 100644 Binary files a/fastf1/tests/mpl-baseline/test_doc_example_fast_lec.png and b/fastf1/tests/mpl-baseline/test_doc_example_fast_lec.png differ diff --git a/fastf1/tests/mpl-baseline/test_readme_example.png b/fastf1/tests/mpl-baseline/test_readme_example.png index b8ac55693..406d144e3 100644 Binary files a/fastf1/tests/mpl-baseline/test_readme_example.png and b/fastf1/tests/mpl-baseline/test_readme_example.png differ diff --git a/fastf1/tests/mpl-baseline/test_speed_trace.png b/fastf1/tests/mpl-baseline/test_speed_trace.png index e410c6306..d160f9b8b 100644 Binary files a/fastf1/tests/mpl-baseline/test_speed_trace.png and b/fastf1/tests/mpl-baseline/test_speed_trace.png differ diff --git a/fastf1/tests/test_example_plots.py b/fastf1/tests/test_example_plots.py index 454b0a4b8..aa6ddb2ff 100644 --- a/fastf1/tests/test_example_plots.py +++ b/fastf1/tests/test_example_plots.py @@ -6,10 +6,10 @@ import fastf1.utils -fastf1.plotting.setup_mpl() +fastf1.plotting.setup_mpl(misc_mpl_mods=False, color_scheme='fastf1') # generate baseline with -# >pytest tests --mpl-generate-path=tests/mpl-baseline +# >pytest --mpl-generate-path=fastf1/tests/mpl-baseline @pytest.mark.f1telapi @@ -61,14 +61,17 @@ def test_doc_example_delta_time(): fig, ax = plt.subplots() ax.plot(lec.telemetry['Distance'], lec.telemetry['Speed'], - color=fastf1.plotting.team_color(lec['Team'])) + color=fastf1.plotting.get_team_color(lec['Team'], + session=session)) ax.plot(ham.telemetry['Distance'], ham.telemetry['Speed'], - color=fastf1.plotting.team_color(ham['Team'])) + color=fastf1.plotting.get_team_color(ham['Team'], + session=session)) twin = ax.twinx() delta_time, ham_car_data, lec_car_data = fastf1.utils.delta_time(ham, lec) ham_car_data = ham_car_data.add_distance() twin.plot(ham_car_data['Distance'], delta_time, '--', - color=fastf1.plotting.team_color(lec['Team'])) + color=fastf1.plotting.get_team_color(lec['Team'], + session=session)) return fig diff --git a/fastf1/tests/test_plotting.py b/fastf1/tests/test_plotting.py index 84b8a01e0..493c49bad 100644 --- a/fastf1/tests/test_plotting.py +++ b/fastf1/tests/test_plotting.py @@ -1,32 +1,454 @@ +import matplotlib.pyplot as plt import pytest -from fastf1.plotting import ( - DRIVER_COLORS, - DRIVER_TRANSLATE, - TEAM_COLORS, - TEAM_TRANSLATE +import fastf1 +import fastf1.plotting +from fastf1.plotting._constants import season2023 +from fastf1.plotting._constants.base import CompoundsConst + + +OFFICIAL_MERC_COLOR = season2023.Teams['mercedes'].TeamColor.Official +OFFICIAL_RB_COLOR = season2023.Teams['red bull'].TeamColor.Official +DEFAULT_MERC_COLOR = season2023.Teams['mercedes'].TeamColor.FastF1 +DEFAULT_RB_COLOR = season2023.Teams['red bull'].TeamColor.FastF1 + + +@pytest.mark.parametrize( + "use_exact", (True, False) +) +@pytest.mark.parametrize( + "identifier, expected, can_match_exact", + ( + ('VER', 'max verstappen', True), # test abbreviation + ('HAM', 'lewis hamilton', True), + ('max verstappen', 'max verstappen', True), # exact name match + ('lewis hamilton', 'lewis hamilton', True), + ('verstappen', 'max verstappen', False), # partial name match + ('hamilton', 'lewis hamilton', False), + ('verstaapen', 'max verstappen', False), # test fuzzy (typos) + ('hamiltime', 'lewis hamilton', False), + ) +) +def test_internal_get_driver(identifier, expected, can_match_exact, use_exact): + session = fastf1.get_session(2023, 10, 'R') + + if use_exact and not can_match_exact: + with pytest.raises(KeyError, match="No driver found"): + _ = fastf1.plotting._interface._get_driver( + identifier, session, exact_match=use_exact + ) + return + + else: + driver = fastf1.plotting._interface._get_driver( + identifier, session, exact_match=use_exact + ) + assert driver.normalized_value == expected + + +@pytest.mark.parametrize( + "use_exact", (True, False) +) +@pytest.mark.parametrize( + "identifier, expected, can_match_exact", + ( + ('red bull', 'red bull', True), # exact name match + ('mercedes', 'mercedes', True), + ('bull', 'red bull', False), # partial name match + ('haas', 'haas', True), + ('Red Bull Racing', 'red bull', True), # exact match with full name + ('Haas F1 Team', 'haas', True), + ('merciless', 'mercedes', False), # test fuzzy (typos) + ('alfadauri', 'alphatauri', False), + ) +) +def test_internal_get_team(identifier, expected, can_match_exact, use_exact): + session = fastf1.get_session(2023, 10, 'R') + + if use_exact and not can_match_exact: + with pytest.raises(KeyError, match="No team found"): + _ = fastf1.plotting._interface._get_team( + identifier, session, exact_match=use_exact + ) + return + + else: + team = fastf1.plotting._interface._get_team( + identifier, session, exact_match=use_exact + ) + assert team.normalized_value == expected + + +def test_fuzzy_driver_team_key_error(): + session = fastf1.get_session(2023, 10, 'R') + + with pytest.raises(KeyError): + _ = fastf1.plotting._interface._get_team('andretti', session) + + with pytest.raises(KeyError): + _ = fastf1.plotting._interface._get_driver('toto wolf', session) + + +@pytest.mark.parametrize( + "identifier, kwargs, expected", + ( + ('red bull', {'colormap': 'default'}, DEFAULT_RB_COLOR), + ('mercedes', {'colormap': 'default'}, DEFAULT_MERC_COLOR), + ('mercedes', {'colormap': 'official'}, OFFICIAL_MERC_COLOR), + ) +) +def test_internal_get_team_color(identifier, kwargs, expected): + session = fastf1.get_session(2023, 10, 'R') + color = fastf1.plotting._interface._get_team_color( + identifier, session, **kwargs + ) + assert color == expected + + +def test_internal_get_team_color_exceptions(): + session = fastf1.get_session(2023, 10, 'R') + with pytest.raises(ValueError, match="Invalid colormap"): + fastf1.plotting._interface._get_team_color( + 'mercedes', session, colormap='bullshit' + ) + + +@pytest.mark.parametrize( + "identifier, kwargs, expected", + ( + ('Red Bull', {'short': False}, 'Red Bull Racing'), + ('Red Bull', {'short': True}, 'Red Bull'), + ('merciless', {'short': True}, 'Mercedes'), # test fuzzy (typos) + ) +) +def test_get_team_name(identifier, kwargs, expected): + session = fastf1.get_session(2023, 10, 'R') + name = fastf1.plotting.get_team_name(identifier, session, **kwargs) + assert name == expected + + +@pytest.mark.parametrize( + "identifier, kwargs, expected", + ( + ('max verstappen', {'short': False}, 'Red Bull Racing'), # long + ('max verstappen', {'short': True}, 'Red Bull'), # test short + ('HAM', {'short': True}, 'Mercedes'), # test abbreviation + ('verstaapen', {'short': True}, 'Red Bull'), # test fuzzy (typos) + ) +) +def test_get_team_name_by_driver(identifier, kwargs, expected): + session = fastf1.get_session(2023, 10, 'R') + name = fastf1.plotting.get_team_name_by_driver( + identifier, session, **kwargs + ) + assert name == expected + + +@pytest.mark.parametrize( + "identifier, kwargs, expected", + ( + ('red bull', {'colormap': 'default'}, + DEFAULT_RB_COLOR), + ('mercedes', {'colormap': 'default'}, + DEFAULT_MERC_COLOR), + ('mercedes', {'colormap': 'official'}, + OFFICIAL_MERC_COLOR), + ) +) +def test_get_team_color(identifier, kwargs, expected): + session = fastf1.get_session(2023, 10, 'R') + color = fastf1.plotting.get_team_color( + identifier, session, **kwargs + ) + assert color == expected + + +@pytest.mark.parametrize( + "identifier, expected", + ( + ('VER', 'Max Verstappen'), # test abbreviation + ('HAM', 'Lewis Hamilton'), + ('max verstappen', 'Max Verstappen'), # exact name match + ('lewis hamilton', 'Lewis Hamilton'), + ('verstappen', 'Max Verstappen'), # exact partial name match + ('hamilton', 'Lewis Hamilton'), + ('verstaapen', 'Max Verstappen'), # test fuzzy (typos) + ('hamiltime', 'Lewis Hamilton'), + ) +) +def test_get_driver_name(identifier, expected): + session = fastf1.get_session(2023, 10, 'R') + name = fastf1.plotting.get_driver_name(identifier, session) + assert name == expected + + +@pytest.mark.parametrize( + "identifier, expected", + ( + ('VER', 'VER'), # test abbreviation + ('HAM', 'HAM'), + ('max verstappen', 'VER'), # exact name match + ('lewis hamilton', 'HAM'), + ('verstappen', 'VER'), # exact partial name match + ('hamilton', 'HAM'), + ('verstaapen', 'VER'), # test fuzzy (typos) + ('hamiltime', 'HAM'), + ) +) +def test_get_driver_abbreviation(identifier, expected): + session = fastf1.get_session(2023, 10, 'R') + abb = fastf1.plotting.get_driver_abbreviation(identifier, session) + assert abb == expected + + +@pytest.mark.parametrize( + "identifier, expected", + ( + ('red bull', ['Max Verstappen', 'Sergio Perez']), # exact name match + ('mercedes', ['Lewis Hamilton', 'George Russell']), + ) +) +def test_get_driver_names_by_team(identifier, expected): + session = fastf1.get_session(2023, 10, 'R') + names = fastf1.plotting.get_driver_names_by_team(identifier, session) + for name in expected: + assert name in names + assert len(names) == len(expected) + + +@pytest.mark.parametrize( + "identifier, expected", + ( + ('red bull', ['VER', 'PER']), # exact name match + ('mercedes', ['HAM', 'RUS']), + ) ) +def test_get_driver_abbreviations_by_team(identifier, expected): + session = fastf1.get_session(2023, 10, 'R') + abbs = fastf1.plotting.get_driver_abbreviations_by_team(identifier, session) + for abb in expected: + assert abb in abbs + assert len(abbs) == len(expected) -def test_team_colors_dict_warning(): +@pytest.mark.parametrize( + "identifier, kwargs, expected", + ( + ('verstappen', {'colormap': 'default'}, + DEFAULT_RB_COLOR), + ('perez', {'colormap': 'default'}, + DEFAULT_RB_COLOR), + ('hamilton', {'colormap': 'default'}, + DEFAULT_MERC_COLOR), + ('hamilton', {'colormap': 'official'}, + OFFICIAL_MERC_COLOR), + ) +) +def test_get_driver_color(identifier, kwargs, expected): + session = fastf1.get_session(2023, 10, 'R') + color = fastf1.plotting.get_driver_color( + identifier, session, **kwargs + ) + assert color == expected + + +@pytest.mark.parametrize( + "identifier, style, colormap, expected", + ( + ('verstappen', 'linestyle', 'default', + {'linestyle': 'solid'} + ), + ('perez', ['marker'], 'default', + {'marker': 'o'} + ), + ('verstappen', ['marker', 'color'], 'default', + {'marker': 'x', 'color': DEFAULT_RB_COLOR} + ), + ('hamilton', ['edgecolor', 'facecolor'], 'official', + {'edgecolor': OFFICIAL_MERC_COLOR, + 'facecolor': OFFICIAL_MERC_COLOR}), + + ) +) +def test_get_driver_style_default_styles( + identifier, style, colormap, expected +): + session = fastf1.get_session(2023, 10, 'R') + color = fastf1.plotting.get_driver_style( + identifier, style, session, colormap=colormap + ) + assert color == expected + + +def test_get_driver_style_custom_style(): + session = fastf1.get_session(2023, 10, 'R') + + custom_style = ( + {'color': '#00ff00', 'rain': 'yes', 'snow': False, 'skycolor': 'auto'}, + {'rain': 'no', 'snow': True, 'sun': 100, 'edgecolor': 'auto'}, + ) + + ver_style = fastf1.plotting.get_driver_style( + 'verstappen', + custom_style, + session, + colormap='default', + additional_color_kws=('skycolor', ) # register custom color key + ) + + assert ver_style == { + 'color': '#00ff00', # static color on color keyword + 'rain': 'yes', # string option + 'snow': False, # bool option + # 'sun': 100 # no sun option + 'skycolor': DEFAULT_RB_COLOR, # auto color on custom registered key + } + + per_style = fastf1.plotting.get_driver_style( + 'perez', + custom_style, + session, + colormap='default', + additional_color_kws=('skycolor', ) + ) + + assert per_style == { + # 'color': '#00ff00', # no color entry + 'rain': 'no', # string option + 'snow': True, # bool option + 'sun': 100, # int option + # 'skycolor': DEFAULT_RB_COLOR_0, no skycolor entry + 'edgecolor': DEFAULT_RB_COLOR, # auto color on default key + } + + +def test_get_compound_color(): + session = fastf1.get_session(2023, 10, 'R') + assert (fastf1.plotting.get_compound_color('HARD', session) + == season2023.CompoundColors[CompoundsConst.Hard]) + + assert (fastf1.plotting.get_compound_color('sOfT', session) + == season2023.CompoundColors[CompoundsConst.Soft]) + with pytest.raises(KeyError): - with pytest.warns(UserWarning): - TEAM_COLORS['Ferrari'] + fastf1.plotting.get_compound_color('HYPERSOFT', session) + + +def test_get_compound_mapping(): + session = fastf1.get_session(2023, 10, 'R') + assert (fastf1.plotting.get_compound_mapping(session) + == season2023.CompoundColors) + + +def test_get_driver_color_mapping(): + session = fastf1.get_session(2023, 10, 'R') + + default = fastf1.plotting.get_driver_color_mapping(session, + colormap='default') + assert default['VER'] == default['PER'] == DEFAULT_RB_COLOR + assert default['HAM'] == default['RUS'] == DEFAULT_MERC_COLOR + assert len(default) == 20 + + official = fastf1.plotting.get_driver_color_mapping(session, + colormap='official') + assert official['VER'] == official['PER'] == OFFICIAL_RB_COLOR + assert official['HAM'] == official['RUS'] == OFFICIAL_MERC_COLOR + assert len(default) == 20 + + +def test_list_team_names(): + session = fastf1.get_session(2023, 10, 'R') + names = fastf1.plotting.list_team_names(session) + + assert 'Red Bull Racing' in names + assert 'Haas F1 Team' in names + assert 'Aston Martin' in names + assert len(names) == 10 + + +def test_list_short_team_names(): + session = fastf1.get_session(2023, 10, 'R') + names = fastf1.plotting.list_team_names(session, short=True) + + assert 'Red Bull' in names + assert 'Haas' in names + assert 'Aston Martin' in names + assert len(names) == 10 + + +def test_list_driver_abbreviations(): + session = fastf1.get_session(2023, 10, 'R') + abbs = fastf1.plotting.list_driver_abbreviations(session) + + assert 'VER' in abbs + assert 'RUS' in abbs + assert len(abbs) == 20 + + +def test_list_driver_names(): + session = fastf1.get_session(2023, 10, 'R') + names = fastf1.plotting.list_driver_names(session) + + assert 'Max Verstappen' in names + assert 'George Russell' in names + assert len(names) == 20 + + +def test_list_compounds(): + session = fastf1.get_session(2023, 10, 'R') + compounds = fastf1.plotting.list_compounds(session) + + reference = ('HARD', 'MEDIUM', 'SOFT', 'INTERMEDIATE', 'WET', + 'TEST-UNKNOWN', 'UNKNOWN') + + for compound in reference: + assert compound in compounds + + assert len(compounds) == len(reference) + + +def test_add_sorted_lapnumber_axis(): + session = fastf1.get_session(2023, 10, 'R') + ax = plt.figure().subplots() + + ax.plot(0, 0, label='HAM') + ax.plot(0, 0, label='RUS') + ax.plot(0, 0, label='PER') + ax.plot(0, 0, label='VER') + + legend = fastf1.plotting.add_sorted_driver_legend(ax, session) + + # sorting is generally done by driver number to guarantee consistency + # in 2023, VER was #1, so he is first, then followed by PER; + # Red Bull as a team before Mercedes, again because VER has the lower number + # within Mercedes, Hamilton has the lower number + assert ([txt.get_text() for txt in legend.texts] + == ['VER', 'PER', 'HAM', 'RUS']) + - with pytest.warns(UserWarning): - TEAM_COLORS.get('Ferrari', None) +def test_override_team_constants(): + session = fastf1.get_session(2023, 10, 'R') + fastf1.plotting.override_team_constants( + 'Haas', session, + short_name='Gene', + fastf1_color='#badbad', + official_color='#bada55' + ) - TEAM_COLORS['ferrari'] - TEAM_COLORS.get('ferrari', None) + assert fastf1.plotting.get_team_name('Haas', session) == 'Haas F1 Team' + assert fastf1.plotting.get_team_name('Haas', session, short=True) == 'Gene' + assert fastf1.plotting.get_team_color( + 'Haas', session, colormap='fastf1' + ) == '#badbad' -def test_team_color_name_abbreviation_integrity(): - for value in TEAM_TRANSLATE.values(): - assert value in TEAM_COLORS - assert len(TEAM_COLORS) == len(TEAM_TRANSLATE) + assert fastf1.plotting.get_team_color( + 'Haas', session, colormap='official' + ) == '#bada55' + # cleanup: explicitly clear the driver-team-mapping to avoid side effects + # in other tests + fastf1.plotting._interface._DRIVER_TEAM_MAPPINGS = dict() -def test_driver_color_name_abbreviation_integrity(): - for value in DRIVER_TRANSLATE.values(): - assert value in DRIVER_COLORS - assert len(DRIVER_COLORS) == len(DRIVER_TRANSLATE) + if fastf1.plotting.get_team_name('Haas', session, short=True) != 'Haas': + raise RuntimeError("Test cleanup failed!") diff --git a/fastf1/tests/test_plotting_deprecated.py b/fastf1/tests/test_plotting_deprecated.py new file mode 100644 index 000000000..4beb77e96 --- /dev/null +++ b/fastf1/tests/test_plotting_deprecated.py @@ -0,0 +1,115 @@ +import warnings + +import pytest + +import fastf1.plotting + + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + from fastf1.plotting import ( + DRIVER_COLORS, + DRIVER_TRANSLATE, + TEAM_COLORS, + TEAM_TRANSLATE, + COLOR_PALETTE, + COMPOUND_COLORS + ) + +import matplotlib.pyplot as plt + + +@pytest.mark.parametrize( + "property_name", + ('DRIVER_COLORS', 'DRIVER_TRANSLATE', + 'TEAM_COLORS', 'TEAM_TRANSLATE', + 'COLOR_PALETTE', 'COMPOUND_COLORS') +) +def test_all_warn_deprecated(property_name): + with pytest.warns(FutureWarning, match="is deprecated"): + obj = getattr(fastf1.plotting, property_name) + assert isinstance(obj, (dict, list)) + + +def test_driver_colors_driver_translate(): + for abb, name in DRIVER_TRANSLATE.items(): + assert len(abb) == 3 + assert abb.isalpha() and abb.isupper() + assert len(name) > 3 + assert name.replace(' ', '').isalpha() and name.islower() + + assert name in DRIVER_COLORS + color = DRIVER_COLORS[name] + assert color.startswith('#') + assert len(color) == 7 + _ = int(color[1:], base=16) # ensure that it's a valid hex color + + +def test_team_colors_team_translate(): + for abb, name in TEAM_TRANSLATE.items(): + assert (len(abb) == 3) or (len(abb) == 2) + assert abb.isalpha() and abb.isupper() + assert (len(name) > 3) or (name.lower() == 'rb') + assert name.replace(' ', '').isalpha() and name.islower() + + assert name in TEAM_COLORS + color = TEAM_COLORS[name] + assert color.startswith('#') + assert len(color) == 7 + _ = int(color[1:], base=16) # ensure that it's a valid hex color + + +def test_compound_colors(): + for compound, color in COMPOUND_COLORS.items(): + assert len(compound) >= 3 + assert compound.replace('-', '').isalpha() + assert compound.replace('-', '').isupper() + + assert color.startswith('#') + assert len(color) == 7 + _ = int(color[1:], base=16) # ensure that it's a valid hex color + + +def test_color_palette(): + assert len(COLOR_PALETTE) == 7 + for color in COLOR_PALETTE: + assert color.startswith('#') + assert len(color) == 7 + _ = int(color[1:], base=16) # ensure that it's a valid hex color + + +@pytest.mark.parametrize( + "func_name", + ("setup_mpl", "driver_color", "team_color", "lapnumber_axis") +) +def test_functions_exist(func_name): + assert hasattr(fastf1.plotting, func_name) + func = getattr(fastf1.plotting, func_name) + assert callable(func) + + +def test_driver_color(): + with pytest.warns(FutureWarning, match="is deprecated"): + color_ver = fastf1.plotting.driver_color('VER') + color_per = fastf1.plotting.driver_color('PER') + + assert color_ver.startswith('#') + assert len(color_ver) == 7 + _ = int(color_ver[1:], base=16) # ensure that it's a valid hex color + + assert color_per != color_ver + + +def test_team_color(): + with pytest.warns(FutureWarning, match="is deprecated"): + color = fastf1.plotting.team_color('ferrari') + + assert color.startswith('#') + assert len(color) == 7 + _ = int(color[1:], base=16) # ensure that it's a valid hex color + + +def test_lapnumber_axis(): + ax = plt.figure().subplots() + with pytest.warns(FutureWarning, match="is deprecated"): + fastf1.plotting.lapnumber_axis(ax) diff --git a/fastf1/utils.py b/fastf1/utils.py index fb2a02ea1..4e35f0314 100644 --- a/fastf1/utils.py +++ b/fastf1/utils.py @@ -50,7 +50,7 @@ def delta_time( from fastf1 import utils from matplotlib import pyplot as plt - plotting.setup_mpl() + plotting.setup_mpl(misc_mpl_mods=False, color_scheme='fastf1') session = ff1.get_session(2021, 'Emilia Romagna', 'Q') session.load() @@ -62,11 +62,11 @@ def delta_time( fig, ax = plt.subplots() # use telemetry returned by .delta_time for best accuracy, - # this ensure the same applied interpolation and resampling + # this ensures the same applied interpolation and resampling ax.plot(ref_tel['Distance'], ref_tel['Speed'], - color=plotting.team_color(ham['Team'])) + color=plotting.get_team_color(ham['Team'], session)) ax.plot(compare_tel['Distance'], compare_tel['Speed'], - color=plotting.team_color(lec['Team'])) + color=plotting.get_team_color(lec['Team'], session)) twin = ax.twinx() twin.plot(ref_tel['Distance'], delta_time, '--', color='white')