Source code for fastf1.plotting

"""
:mod:`fastf1.plotting` - Plotting module
========================================

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 pandas as pd
import numpy as np
import warnings

try:
    import matplotlib
    from matplotlib import pyplot as plt
    from matplotlib import cycler
except ImportError:
    warnings.warn("Failed to import optional dependency 'matplotlib'!"
                  "Plotting functionality will be unavailable!", UserWarning)
try:
    import timple
except ImportError:
    warnings.warn("Failed to import optional dependency 'timple'!"
                  "Plotting of timedelta values will be restricted!",
                  UserWarning)

import warnings
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 thefuzz 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
        )


TEAM_COLORS = __TeamColorsWarnDict({
    'mercedes': '#00d2be', 'ferrari': '#dc0000',
    'red bull': '#0600ef', 'mclaren': '#ff8700',
    'alpine': '#0090ff', 'aston martin': '#006f62',
    'alfa romeo': '#900000', 'alphatauri': '#2b4562',
    'haas': '#ffffff', 'williams': '#005aff'
})
"""Mapping of team colors (hex color code) to team names.
(current season only)"""


TEAM_TRANSLATE = {'MER': 'mercedes', 'FER': 'ferrari',
                  'RBR': 'red bull', 'MCL': 'mclaren',
                  'APN': 'alpine', 'AMR': 'aston martin',
                  'ARR': 'alfa romeo', 'APT': 'alphatauri',
                  'HAA': 'haas', 'WIL': 'williams'}
"""Mapping of team names to theirs respective abbreviations."""

COLOR_PALETTE = ['#FF79C6', '#50FA7B', '#8BE9FD', '#BD93F9',
                 '#FFB86C', '#FF5555', '#F1FA8C']
"""The default color palette for matplotlib plot lines in fastf1's color
scheme."""


[docs]def setup_mpl(mpl_timedelta_support=True, color_scheme='fastf1', misc_mpl_mods=True): """Setup matplotlib for use with fastf1. This is optional but, at least partly, highly recommended. 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 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) """ if mpl_timedelta_support: _enable_timple() if color_scheme == 'fastf1': _enable_fastf1_color_scheme() if misc_mpl_mods: _enable_misc_mpl_mods()
[docs]def team_color(identifier): """Get a team's color from a team name or abbreviation. 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. 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') '#0600ef' >>> team_color('redbull') '#0600ef' >>> team_color('Red') '#0600ef' >>> team_color('RBR') '#0600ef' shortened team names, included sponsors and typos can be dealt with too (within reason) >>> team_color('Mercedes') '#00d2be' >>> team_color('Merc') '#00d2be' >>> team_color('Merecds') '#00d2be' >>> team_color('Mercedes-AMG Petronas F1 Team') '#00d2be' Args: identifier (str): Abbreviation or uniquely identifying name of the team. Returns: str: hex color code """ if identifier.upper() in TEAM_TRANSLATE: # try short team abbreviations first return TEAM_COLORS[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 TEAM_COLORS: return TEAM_COLORS[identifier] # check for exact partial string match for team_name, color in TEAM_COLORS.items(): if identifier in team_name: return color # do fuzzy string matching key_ratios = list() for existing_key in 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] < 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 TEAM_COLORS[best_matched_key]
[docs]def lapnumber_axis(ax, axis='xaxis'): """Set axis to integer ticks only." Args: ax: matplotlib axis axis (='xaxis', optional): 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
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(b=True, which='major', color='#4f4845', linestyle='-', linewidth=1) grid(b=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