Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plot arcs method #84

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/pitch_plots/plot_arcs_around_goals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
==========================
Plotting arcs around goals
==========================

This example shows how to plot arcs around goals at specified distances on various pitch types.
"""
from mplsoccer import Pitch, VerticalPitch
import matplotlib.pyplot as plt


def plot_arcs_around_goals_on_pitches(pitch_cls):
fig, axes = plt.subplots(4, 2, figsize=(12, 14))
axes = axes.ravel()
pitch_types = ['statsbomb', 'opta', 'tracab', 'skillcorner', 'wyscout',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simplified version of examples/pitch_setup/plot_compare_pitches.py which I was using to make sure I dealt with the subtleties of the different coordinate systems and aspect ratios.

'metricasports', 'uefa', 'custom']

placements = ('top', 'bottom') if pitch_cls == VerticalPitch else ('left', 'right')
colors = ('red', 'blue')
for idx, pt in enumerate(pitch_types):
if pt in ['tracab', 'metricasports', 'custom', 'skillcorner']:
pitch = pitch_cls(pitch_type=pt, pitch_length=105, pitch_width=68)
else:
pitch = pitch_cls(pitch_type=pt)

pitch.draw(axes[idx])
axes[idx].set_title(pt, fontsize=20, c='black', pad=15)
for radius_meters in (5, 10, 20, 30):
for color, placement in zip(colors, placements):
pitch.add_arc_around_goal(axes[idx], radius_meters, placement=placement, color=color)
return fig, axes


##############################################################################
# Horizontal pitches

plot_arcs_around_goals_on_pitches(Pitch)

##############################################################################
# Vertical pitches

plot_arcs_around_goals_on_pitches(VerticalPitch)
128 changes: 128 additions & 0 deletions mplsoccer/pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,70 @@ def _reverse_vertices_if_vertical(vert):
def _reverse_annotate_if_vertical(annotate):
return annotate

def add_arc_around_goal(self, ax, radius_meters, placement='left', **kwargs):
"""Add an arc to the pitch centred on one of the goals.

Parameters
----------
ax : matplotlib axis
A matplotlib.axes.Axes to draw the arc on.
radius_meters : float
The radius of the arc in meters.
placement : str, default 'left'
Which side of the pitch to plot the arc. Should be either 'left' or 'right'.
kwargs : dict
Additional keyword arguments accepted by matplotlib.patches.Arc.

Returns
-------
Arc added to `ax` and None returned.

Raises
------
ValueError
If `placement` is not 'left' or 'right'.

Examples
--------
>>> from mplsoccer import Pitch
>>> pitch = Pitch()
>>> fig, ax = pitch.draw()
>>> pitch.add_arc_around_goal(ax, radius_meters=10, placement="left")
>>> pitch.add_arc_around_goal(ax, radius_meters=20, placement="right", color="red")
"""
xmin, xmax, ymin, ymax = self.extent
length_pixels = abs(xmax - xmin)
width_pixels = abs(ymax - ymin)

# scale down via ratio of radius to dimension
if self.pitch_type != 'metricasports':
width = 2 * length_pixels * radius_meters / self.dim.length
height = 2 * width_pixels * radius_meters / self.dim.width / self.dim.aspect
if self.pitch_type == 'tracab':
# has unit centimeters
width *= 100
height *= 100
elif self.pitch_type == 'metricasports':
width = 2 * length_pixels * radius_meters / self.dim.pitch_length
height = 2 * width_pixels * radius_meters / self.dim.pitch_width

if self.dim.origin_center:
y = 0
else:
y = width_pixels / 2 - self.pad_bottom
if placement == 'left':
self._draw_arc(
ax, xmin + self.pad_left, y, width, height, theta1=-90, theta2=90, **kwargs
)
elif placement == 'right':
self._draw_arc(
ax, xmax - self.pad_right, y, width, height, theta1=90, theta2=270, **kwargs
)
else:
raise ValueError(
f'placement={placement} should be "left" or "right"'
)


class VerticalPitch(BasePitchPlot):

Expand Down Expand Up @@ -222,3 +286,67 @@ def _reverse_vertices_if_vertical(vert):
@staticmethod
def _reverse_annotate_if_vertical(annotate):
return annotate[::-1]

def add_arc_around_goal(self, ax, radius_meters, placement, **kwargs):
"""Add an arc to the pitch centred on one of the goals.

Parameters
----------
ax : matplotlib axis
A matplotlib.axes.Axes to draw the arc on.
radius_meters : float
The radius of the arc in meters.
placement : str, default 'top'
Which side of the pitch to plot the arc. Should be either 'top' or 'bottom'.
kwargs : dict
Additional keyword arguments accepted by matplotlib.patches.Arc.

Returns
-------
Arc added to `ax` and None returned.

Raises
------
ValueError
If `placement` is not 'top' or 'bottom'.

Examples
--------
>>> from mplsoccer import Pitch
>>> pitch = Pitch()
>>> fig, ax = pitch.draw()
>>> pitch.add_arc_around_goal(ax, radius_meters=15, placement="bottom")
>>> pitch.add_arc_around_goal(ax, radius_meters=7, placement="top", color="blue")
"""
xmin, xmax, ymin, ymax = self.extent
width_pixels = abs(xmax - xmin)
length_pixels = abs(ymax - ymin)

# scale down via ratio of radius to dimension
if self.pitch_type != "metricasports":
width = 2 * width_pixels * self.dim.aspect * radius_meters / self.dim.width
height = 2 * length_pixels * radius_meters / self.dim.length
if self.pitch_type == "tracab":
# has unit centimeters
width *= 100
height *= 100
elif self.pitch_type == "metricasports":
width = 2 * width_pixels * radius_meters / self.dim.pitch_width
height = 2 * length_pixels * radius_meters / self.dim.pitch_length

if self.dim.origin_center:
x = 0
else:
x = width_pixels / 2 - self.pad_left
if placement == 'bottom':
self._draw_arc(
ax, ymin + self.pad_bottom, x, height, width, theta1=-90, theta2=90, **kwargs
)
Comment on lines +342 to +344
Copy link
Author

@minimav minimav Jan 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _draw_arc method on a VerticalPitch switches orientations for you. Due to how I worked out the co-ordinates I have 'undone' the switch in the arguments passed here so that it works out correctly. I was not really sure how to go about this in such a way that this extra switch wouldn't be needed.

elif placement == 'top':
self._draw_arc(
ax, ymax - self.pad_top, x, height, width, theta1=90, theta2=270, **kwargs
)
else:
raise ValueError(
f'placement={placement} should be "top" or "bottom"'
)