Source code for fastf1.plotting._plotting

import warnings
from typing import Optional

import numpy as np
import pandas as pd


try:
    import matplotlib
    from matplotlib import cycler
    from matplotlib import pyplot as plt
except ImportError:
    warnings.warn("Failed to import optional dependency 'matplotlib'!"
                  "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!",
                  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


class __DefaultStringArgType(str):
    pass


__color_scheme_default_arg = __DefaultStringArgType('fastf1')


[docs] def setup_mpl( 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 timedelta values. (Lap times, sector times and other kinds of time spans are represented as timedelta.) Enabling this option will patch some internal matplotlib functions and register converters, formatters and locators for tick formatting. The heavy lifting for this is done by an external package called 'Timple'. See https://github.com/theOehrly/Timple if you wish to customize the tick formatting for timedelta. color_scheme (str, None): This enables the Fast-F1 color scheme that you can see in all example images. Valid color scheme names are: ['fastf1', None] misc_mpl_mods (bool): 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': _enable_fastf1_color_scheme() if misc_mpl_mods: _enable_misc_mpl_mods()
[docs] def driver_color(identifier: str) -> str: """ 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 string matching. Example:: >>> driver_color('charles leclerc') # doctest: +SKIP '#dc0000' >>> driver_color('max verstappen') # doctest: +SKIP '#fcd700' >>> driver_color('ver') # doctest: +SKIP '#fcd700' >>> driver_color('lec') # doctest: +SKIP '#dc0000' shortened driver names and typos can be dealt with too (within reason) >>> driver_color('Max Verst') # doctest: +SKIP '#fcd700' >>> driver_color('Charles') # doctest: +SKIP '#dc0000' Args: identifier (str): Abbreviation or uniquely identifying name of the driver. 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 LEGACY_DRIVER_TRANSLATE: # try short team abbreviations first return LEGACY_DRIVER_COLORS[ LEGACY_DRIVER_TRANSLATE[identifier.upper()] ] else: identifier = identifier.lower() # check for an exact team name match if identifier in LEGACY_DRIVER_COLORS: return LEGACY_DRIVER_COLORS[identifier] # check for exact partial string match 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 LEGACY_DRIVER_COLORS: ratio = fuzz.ratio(identifier, existing_key) key_ratios.append((ratio, existing_key)) key_ratios.sort(reverse=True) if key_ratios[0][0] != 100: _logger.warning( "Correcting invalid user input " f"'{identifier}' to '{key_ratios[0][1]}'." ) if ((key_ratios[0][0] < 35) or (key_ratios[0][0] / key_ratios[1][0] < 1.2)): # ensure that the best match has a minimum accuracy (35 out of # 100) and that it has a minimum confidence (at least 20% better # than second best) raise KeyError best_matched_key = key_ratios[0][1] return LEGACY_DRIVER_COLORS[best_matched_key]
[docs] def team_color(identifier: str) -> str: """ 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 string matching. Example:: >>> team_color('Red Bull') # doctest: +SKIP '#fcd700' >>> team_color('redbull') # doctest: +SKIP '#fcd700' >>> team_color('Red') # doctest: +SKIP '#fcd700' >>> team_color('RBR') # doctest: +SKIP '#fcd700' # shortened team names, included sponsors and typos can be dealt with # too (within reason) >>> team_color('Mercedes') # doctest: +SKIP '#00d2be' >>> team_color('Merc') # doctest: +SKIP '#00d2be' >>> team_color('Merecds') # doctest: +SKIP '#00d2be' >>> team_color('Mercedes-AMG Petronas F1 Team') # doctest: +SKIP '#00d2be' Args: identifier (str): Abbreviation or uniquely identifying name of the team. Returns: str: hex color code """ 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 LEGACY_TEAM_COLORS[LEGACY_TEAM_TRANSLATE[identifier.upper()]] else: identifier = 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 LEGACY_TEAM_COLORS: return LEGACY_TEAM_COLORS[identifier] # check for exact partial string match 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 LEGACY_TEAM_COLORS.keys(): ratio = fuzz.ratio(identifier, existing_key) key_ratios.append((ratio, existing_key)) key_ratios.sort(reverse=True) if key_ratios[0][0] != 100: _logger.warning( "Correcting invalid user input " f"'{identifier}' to '{key_ratios[0][1]}'." ) if ((key_ratios[0][0] < 35) or (key_ratios[0][0] / key_ratios[1][0] < 1.2)): # ensure that the best match has a minimum accuracy (35 out of # 100) and that it has a minimum confidence (at least 20% better # than second best) raise KeyError best_matched_key = key_ratios[0][1] return LEGACY_TEAM_COLORS[best_matched_key]
def _enable_timple(): # use external package timple to patch matplotlib # this adds converters, locators and formatters for # plotting timedelta values tick_formats = [ "%d %day", "%H:00", "%H:%m", "%M:%s.0", "%M:%s.%ms" ] tmpl = timple.Timple(converter='concise', formatter_args={'show_offset_zero': False, 'formats': tick_formats}) tmpl.enable() def _enable_misc_mpl_mods(): def _bar_sorted(bar): def _bar_sorted_decorator(*args, **kwargs): if 'edgecolor' not in kwargs: kwargs['edgecolor'] = 'none' if 'sort' in kwargs and len(val := args[-1]): s = kwargs['sort'] if s == 'increasing' or s == 1: s = False if s == 'decreasing' or s == -1: s = True _ids = [list(val).index(a) for a in sorted(val, reverse=s)] _args = [[args[-2][i] for i in _ids], [args[-1][i] for i in _ids]] if len(args) > 2: _args.insert(0, args[0]) args = _args for key in kwargs: if isinstance(kwargs[key], (pd.core.series.Series)): kwargs[key] = kwargs[key].to_numpy() if isinstance(kwargs[key], (list, np.ndarray)): kwargs[key] = [kwargs[key][i] for i in _ids] kwargs.pop('sort', None) return bar(*args, **kwargs) return _bar_sorted_decorator plt.bar = _bar_sorted(plt.bar) plt.barh = _bar_sorted(plt.barh) matplotlib.axes.Axes.bar = _bar_sorted(matplotlib.axes.Axes.bar) matplotlib.axes.Axes.barh = _bar_sorted(matplotlib.axes.Axes.barh) def _nice_grid(ax): if isinstance(ax, np.ndarray): [_nice_grid(_ax) for _ax in ax] else: ax.minorticks_on() grid = getattr(ax, 'grid') grid(visible=True, which='major', color='#4f4845', linestyle='-', linewidth=1) grid(visible=True, which='minor', color='#3f3a38', linestyle='--', linewidth=0.5) _subplots_placeholder = plt.subplots def _subplots(*args, **kwargs): fig, ax = _subplots_placeholder(*args, **kwargs) _nice_grid(ax) return fig, ax plt.subplots = _subplots _savefig_placeholder = matplotlib.figure.Figure.savefig def _save(*args, **kwargs): if 'facecolor' not in kwargs: kwargs['facecolor'] = args[0].get_facecolor() if 'edgecolors' not in kwargs: kwargs['edgecolor'] = 'none' return _savefig_placeholder(*args, **kwargs) matplotlib.figure.Figure.savefig = _save def _enable_fastf1_color_scheme(): plt.rcParams['figure.facecolor'] = '#292625' plt.rcParams['axes.edgecolor'] = '#2d2928' plt.rcParams['xtick.color'] = '#f1f2f3' plt.rcParams['ytick.color'] = '#f1f2f3' plt.rcParams['axes.labelcolor'] = '#F1f2f3' plt.rcParams['axes.facecolor'] = '#1e1c1b' # plt.rcParams['axes.facecolor'] = '#292625' plt.rcParams['axes.titlesize'] = 'x-large' # plt.rcParams['font.family'] = 'Gravity' plt.rcParams['font.weight'] = 'medium' plt.rcParams['text.color'] = '#F1F1F3' 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['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
[docs] 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