Source code for floodlight.core.pitch

import warnings
from dataclasses import dataclass
from typing import Tuple

import matplotlib
import matplotlib.pyplot as plt

from floodlight.vis.pitches import plot_handball_pitch, plot_football_pitch
from floodlight.utils.types import Numeric


[docs]@dataclass class Pitch: """ Pitch and coordinate system specifications. Core class of floodlight. Parameters ---------- xlim: Tuple[Numeric, Numeric] Limits of pitch boundaries in longitudinal direction. This tuple has the form (x_min, x_max) and delimits the length of the pitch (not of any actual data) within the coordinate system. ylim: Tuple[Numeric, Numeric] Limits of pitch boundaries in lateral direction. This tuple has the form (y_min, y_max) and delimits the width of the pitch (not of any actual data) within the coordinate system. unit: {'m', 'cm', 'percent', 'normed'} The unit in which data is measured along axes. The values 'percent' and 'normed' can be used to denote standardized pitches where data is scaled along the axes independent of the actual pitch size. In this case, 'percent' refers to a scaling onto the range (0, 100), and 'normed' to all other scalings. To get non-distored calculations from these unit-systems, the `length` and `width` attributes need to be specified. boundaries: str One of {'fixed', 'flexible'}. Here, 'fixed' denotes coordinate systems that limit axes to the respective xlim and ylim. On the contrary, 'flexible' coordinate systems do not explicitly specify a limit. Instead, the limit is implicitly set by the actual pitch length and width. length: Numeric, optional Actual pitch length in *m*. width: Numeric, optional Actual pitch width in *m*. sport: str, optional Sport for which the pitch is used. This is used to automatically generate lines and markings. Attributes ---------- center: tuple Returns coordinates of the pitch center. is_metrical: bool Returns True if the object's unit is metrical, False otherwise. """ xlim: Tuple[Numeric, Numeric] ylim: Tuple[Numeric, Numeric] unit: str boundaries: str length: Numeric = None width: Numeric = None sport: str = None def __str__(self): return ( f"Floodlight Pitch object with axes x = {self.xlim} / y = {self.ylim} " f"({self.boundaries}) in [{self.unit}]" )
[docs] @classmethod def from_template(cls, template_name: str, **kwargs): """ Creates a Pitch object representing common data provider formats. Parameters ---------- template_name: str The name of the template the pitch should follow. Currently supported are {'dfl', 'eigd', 'opta', 'statsbomb', 'secondspectrum', 'statsperform_event', 'statsperform_tracking', 'statsperform_open', 'tracab'}. kwargs: You may pass optional arguments (`length`, `width`, `sport`) used for class instantiation. For some data providers, additional kwargs are needed to represent their format correctly. For example, pass the `length` and `width` argument to create a Pitch object in the 'tracab' format. Returns ------- pitch: Pitch A class instance of the given provider format. """ if template_name == "dfl": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact DFL (German Football League) " "Pitch object, `length` and `width` of the pitch need" "to be passed as keyworded arguments" ) x_half = round(kwargs["length"] / 2, 3) y_half = round(kwargs["width"] / 2, 3) return cls( xlim=(-x_half, x_half), ylim=(-y_half, y_half), unit="m", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "opta": return cls( xlim=(0.0, 100.0), ylim=(0.0, 100.0), unit="percent", boundaries="fixed", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "statsperform_open": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact StatsPerform Pitch object, " "`length` and `width` of the pitch need " "to be passed as keyworded arguments" ) x_half = round(kwargs["length"] / 2, 3) y_half = round(kwargs["width"] / 2, 3) return cls( xlim=(-x_half, x_half), ylim=(-y_half, y_half), unit="m", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "secondspectrum": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact Second Spectrum Pitch object, " "`length` and `width` of the pitch need " "to be passed as keyworded arguments" ) x_half = round(kwargs["length"] / 2, 3) y_half = round(kwargs["width"] / 2, 3) return cls( xlim=(-x_half, x_half), ylim=(-y_half, y_half), unit="m", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "statsperform_event": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact StatsPerform Pitch object, " "`length` and `width` of the pitch need " "to be passed as keyworded arguments" ) x_half = round((kwargs["length"] * 100) / 2, 3) y_half = round((kwargs["width"] * 100) / 2, 3) return cls( xlim=(-x_half, x_half), ylim=(-y_half, y_half), unit="cm", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "statsperform_tracking": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact StatsPerform Pitch object, " "`length` and `width` of the pitch need " "to be passed as keyworded arguments" ) return cls( xlim=(0, kwargs["length"]), ylim=(0, kwargs["width"]), unit="m", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), ) elif template_name == "tracab": if "length" not in kwargs or "width" not in kwargs: raise TypeError( "For an exact TRACAB (ChyronHego, international format) " "Pitch object, `length` and `width` of the pitch need " "to be passed as keyworded arguments" ) x_half = round((kwargs["length"] * 100) / 2, 3) y_half = round((kwargs["width"] * 100) / 2, 3) return cls( xlim=(-x_half, x_half), ylim=(-y_half, y_half), unit="cm", boundaries="flexible", length=kwargs.get("length"), width=kwargs.get("width"), sport=kwargs.get("sport"), ) elif template_name == "eigd": return cls( xlim=(0, 40), ylim=(0, 20), unit="m", boundaries="fixed", length=40, width=20, sport="handball", ) elif template_name == "statsbomb": return cls( xlim=(0.0, 120.0), ylim=(0.0, 80.0), unit="normed", boundaries="flexible", sport="football", ) else: raise ValueError(f"Unsupported template name '{template_name}'")
@property def is_metrical(self) -> bool: is_metrical = False if self.unit in ["m", "cm"]: is_metrical = True return is_metrical @property def center(self) -> tuple: center = ( round((self.xlim[0] + self.xlim[1]) / 2, 3), round((self.ylim[0] + self.ylim[1]) / 2, 3), ) return center
[docs] def plot( self, color_scheme: str = "standard", show_axis_ticks: bool = False, ax: matplotlib.axes = None, **kwargs, ) -> matplotlib.axes: """Plots a pitch on a matplotlib.axes for a given sport. Parameters ---------- color_scheme: str, optional Color scheme of the plot. One of {'standard', 'bw'}. Defaults to 'standard'. show_axis_ticks: bool, optional If set to True, the axis ticks are visible. Defaults to False. ax: matplotlib.axes, optional Axes from matplotlib library on which the playing field is plotted. If ax is None, a default-sized matplotlib.axes object is created. kwargs: Optional keyworded arguments {'linewidth', 'zorder', 'scalex', 'scaley'} which can be used for the plot functions from matplotlib. The kwargs are only passed to all the plot functions of matplotlib. Returns ------- axes: matplotlib.axes Axes from matplotlib library on which the specified pitch is plotted. Notes ----- The kwargs are only passed to the plot functions of matplotlib. To customize the plots have a look at `matplotlib <https://matplotlib.org/3.5.0/api/_as_gen/matplotlib.axes.Axes.plot.html>`_. For example in order to modify the linewidth pass a float to the keyworded argument 'linewidth'. The same principle applies to other kwargs like 'zorder', 'scalex' and 'scaley'. Examples -------- - :ref:`Handball pitch <handball-pitch-label>` - :ref:`Football pitch <football-pitch-label>` """ # list of existing color_schemes and sports color_schemes = ["bw", "standard"] sports = ["football", "handball"] sport = self.sport # check if valide sport was chosen if sport not in sports or sport is None: raise ValueError( f"Expected self.sport to be from {sports}, got {self.sport}" ) # check if a valide color scheme was chosen if color_scheme not in color_schemes: raise ValueError( f"Expected color_scheme to be from {color_schemes}, got {color_scheme}" ) # check wether an axes to plot is given or if a new axes element has to be # created ax = ax or plt.subplots()[1] # set ratio between x and y values of the plot to ensure that the ratio between # length and width is correct regardless of the figsize. default_length = 105 default_width = 68 if self.unit != "percent": ax.set_aspect(1) # set ratio if unit is percent and sport is football elif self.unit == "percent" and sport == "football": if self.length and self.width: ax.set_aspect(self.width / self.length) # set ratio to standard pitch size of 68/105 else: # standard ratio of length and width ax.set_aspect(default_width / default_length) warnings.warn( "Since self.unit == 'percent' but self.length and self.width are " f"None the pitch is set to default values length: {default_length} " f"and width: {default_width}" ) # set ratio if unit is percent and sport is handball elif self.unit == "percent" and sport == "handball": ax.set_aspect(0.5) # create matplotlib.axes with handball pitch if sport == "handball": return plot_handball_pitch( self.xlim, self.ylim, self.unit, color_scheme=color_scheme, show_axis_ticks=show_axis_ticks, ax=ax, **kwargs, ) # create matplotlib.axes with football pitch if sport == "football": return plot_football_pitch( self.xlim, self.ylim, self.length, self.width, self.unit, color_scheme=color_scheme, show_axis_ticks=show_axis_ticks, ax=ax, **kwargs, )