nesticot commited on
Commit
30337a9
·
verified ·
1 Parent(s): 1d21300

Update PitchPlotFunctions.py

Browse files
Files changed (1) hide show
  1. PitchPlotFunctions.py +623 -622
PitchPlotFunctions.py CHANGED
@@ -1,622 +1,623 @@
1
- import polars as pl
2
- import numpy as np
3
- import matplotlib.pyplot as plt
4
- import seaborn as sns
5
- from PIL import Image
6
- import requests
7
- from io import BytesIO
8
- from matplotlib.offsetbox import OffsetImage, AnnotationBbox
9
- from matplotlib.ticker import FuncFormatter
10
- import matplotlib.transforms as transforms
11
- from matplotlib.patches import Ellipse
12
- import matplotlib.gridspec as gridspec
13
- import matplotlib.patches as mpatches
14
- import matplotlib.lines as mlines
15
- from matplotlib.figure import Figure
16
- import streamlit as st
17
- import api_scraper
18
-
19
- # Initialize the scraper
20
- scraper = api_scraper.MLB_Scrape()
21
-
22
- class PitchPlotFunctions:
23
- # Define the pitch_colours method
24
- def pitch_colours(self):
25
- # Dictionary of pitch types and their corresponding colors and names
26
- pitch_colours = {
27
- 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
28
- 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
29
- 'SI': {'colour': '#98165D', 'name': 'Sinker'},
30
- 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
31
- 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
32
- 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
33
- 'SC': {'colour': '#F08223', 'name': 'Screwball'},
34
- 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
35
- 'SL': {'colour': '#67E18D', 'name': 'Slider'},
36
- 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
37
- 'SV': {'colour': '#376748', 'name': 'Slurve'},
38
- 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
39
- 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
40
- 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
41
- 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
42
- 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
43
- 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
44
- 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
45
- }
46
-
47
- # Create dictionaries mapping pitch types to their colors and names
48
- dict_colour = dict(zip(pitch_colours.keys(), [pitch_colours[key]['colour'] for key in pitch_colours]))
49
- dict_pitch = dict(zip(pitch_colours.keys(), [pitch_colours[key]['name'] for key in pitch_colours]))
50
-
51
- return dict_colour, dict_pitch
52
-
53
- # Define the sns_custom_theme method
54
- def sns_custom_theme(self):
55
- # Custom theme for seaborn plots
56
- custom_theme = {
57
- "axes.facecolor": "white",
58
- "axes.edgecolor": ".8",
59
- "axes.grid": True,
60
- "axes.axisbelow": True,
61
- "axes.labelcolor": ".15",
62
- "figure.facecolor": "#f9f9f9",
63
- "grid.color": ".8",
64
- "grid.linestyle": "-",
65
- "text.color": ".15",
66
- "xtick.color": ".15",
67
- "ytick.color": ".15",
68
- "xtick.direction": "out",
69
- "ytick.direction": "out",
70
- "lines.solid_capstyle": "round",
71
- "patch.edgecolor": "w",
72
- "patch.force_edgecolor": True,
73
- "image.cmap": "rocket",
74
- "font.family": ["sans-serif"],
75
- "font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", "Bitstream Vera Sans", "sans-serif"],
76
- "xtick.bottom": False,
77
- "xtick.top": False,
78
- "ytick.left": False,
79
- "ytick.right": False,
80
- "axes.spines.left": True,
81
- "axes.spines.bottom": True,
82
- "axes.spines.right": True,
83
- "axes.spines.top": True
84
- }
85
-
86
- # Color palette for the plots
87
- colour_palette = ['#FFB000', '#648FFF', '#785EF0', '#DC267F', '#FE6100', '#3D1EB2', '#894D80', '#16AA02', '#B5592B', '#A3C1ED']
88
-
89
- return custom_theme, colour_palette
90
-
91
- # Define the sport_id_dict method
92
- def sport_id_dict(self):
93
- # Dictionary mapping sport IDs to their names
94
- dict = {1: 'MLB', 11: 'AAA'}
95
- return dict
96
-
97
- # Define the team_logos method
98
- def team_logos(self):
99
- # List of MLB teams and their corresponding ESPN logo URLs
100
- mlb_teams = [
101
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
102
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
103
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
104
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
105
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
106
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
107
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
108
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
109
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
110
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
111
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
112
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
113
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
114
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
115
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
116
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
117
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
118
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
119
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
120
- {"team": "OAK", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
121
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
122
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
123
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
124
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
125
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
126
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
127
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
128
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
129
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
130
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"}
131
- ]
132
-
133
- # Create a DataFrame from the list of dictionaries
134
- df_image = pl.DataFrame(mlb_teams)
135
- # Set the index to 'team' and convert 'logo_url' to a dictionary
136
- image_dict = df_image.select(['team', 'logo_url']).to_dict(as_series=False)['logo_url']
137
-
138
- # Convert to the desired dictionary format
139
- image_dict = {row['team']: row['logo_url'] for row in df_image.select(['team', 'logo_url']).to_dicts()}
140
-
141
- return image_dict
142
-
143
- # Function to get an image from a URL and display it on the given axis
144
- def player_headshot(self, pitcher_id: str, ax: plt.Axes, sport_id: int):
145
- """
146
- Fetches and displays the player's headshot image on the given axis.
147
-
148
- Parameters:
149
- pitcher_id (str): The ID of the pitcher.
150
- ax (plt.Axes): The matplotlib axis to display the image on.
151
- sport_id (int): The sport ID to determine the URL format.
152
- """
153
- # Construct the URL for the player's headshot image
154
- if sport_id == 1:
155
- url = f'https://img.mlbstatic.com/mlb-photos/image/'\
156
- f'upload/d_people:generic:headshot:67:current.png'\
157
- f'/w_640,q_auto:best/v1/people/{pitcher_id}/headshot/silo/current.png'
158
- else:
159
- url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
160
-
161
- # Send a GET request to the URL
162
- response = requests.get(url)
163
- # Open the image from the response content
164
- img = Image.open(BytesIO(response.content))
165
- # Display the image on the axis
166
- ax.set_xlim(0, 2)
167
- ax.set_ylim(0, 1)
168
- ax.imshow(img, extent=[0.0, 1, 0, 1], origin='upper')
169
- # Turn off the axis
170
- ax.axis('off')
171
-
172
- # Function to display player bio information on the given axis
173
- def player_bio(self, pitcher_id: str, ax: plt.Axes, start_date: str, end_date: str, batter_hand: list):
174
- """
175
- Fetches and displays the player's bio information on the given axis.
176
-
177
- Parameters:
178
- pitcher_id (str): The ID of the pitcher.
179
- ax (plt.Axes): The matplotlib axis to display the bio information on.
180
- start_date (str): The start date for the bio information.
181
- end_date (str): The end date for the bio information.
182
- batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
183
- """
184
- # Construct the URL to fetch player data
185
- url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
186
- # Send a GET request to the URL and parse the JSON response
187
- data = requests.get(url).json()
188
- # Extract player information from the JSON data
189
- player_name = data['people'][0]['fullName']
190
- pitcher_hand = data['people'][0]['pitchHand']['code']
191
- age = data['people'][0]['currentAge']
192
- height = data['people'][0]['height']
193
- weight = data['people'][0]['weight']
194
- # Display the player's name, handedness, age, height, and weight on the axis
195
- ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=48)
196
- ax.text(0.5, 0.6, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=22)
197
- # Determine the batter hand text
198
- if batter_hand == ['R']:
199
- batter_hand_text = ', vs RHH'
200
- elif batter_hand == ['L']:
201
- batter_hand_text = ', vs LHH'
202
- else:
203
- batter_hand_text = ''
204
- ax.text(0.5, 0.35, f'{start_date} to {end_date}{batter_hand_text}', va='top', ha='center', fontsize=22, fontstyle='italic')
205
- # Turn off the axis
206
- ax.axis('off')
207
-
208
- # Function to display the team logo on the given axis
209
- def plot_logo(self, pitcher_id: str, ax: plt.Axes):
210
- """
211
- Fetches and displays the team logo on the given axis.
212
-
213
- Parameters:
214
- pitcher_id (str): The ID of the pitcher.
215
- ax (plt.Axes): The matplotlib axis to display the logo on.
216
- """
217
- # Construct the URL to fetch player data
218
- url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
219
- # Send a GET request to the URL and parse the JSON response
220
- data = requests.get(url).json()
221
- # Construct the URL to fetch team data
222
- url_team = 'https://statsapi.mlb.com/' + data['people'][0]['currentTeam']['link']
223
- # Send a GET request to the team URL and parse the JSON response
224
- data_team = requests.get(url_team).json()
225
- # Get the logo URL from the image dictionary using the team abbreviation
226
- try:
227
- if data_team['teams'][0]['sport']['id'] == 1:
228
- team_abb = data_team['teams'][0]['abbreviation']
229
- logo_url = self.team_logos()[team_abb]
230
- else:
231
- team_abb = data_team['teams'][0]['parentOrgId']
232
- logo_url = self.team_logos()[dict(scraper.get_teams().select(['team_id', 'parent_org_abbreviation']).iter_rows())[team_abb]]
233
- except KeyError:
234
- logo_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=500&h=500&transparent=true"
235
- # Send a GET request to the logo URL
236
- response = requests.get(logo_url)
237
- # Open the image from the response content
238
- img = Image.open(BytesIO(response.content))
239
- # Display the image on the axis
240
- ax.set_xlim(0, 2)
241
- ax.set_ylim(0, 1)
242
- ax.imshow(img, extent=[1, 2, 0, 1], origin='upper')
243
- # Turn off the axis
244
- ax.axis('off')
245
-
246
- ### PITCH ELLIPSE ###
247
- def confidence_ellipse( self,
248
- x:np.array,
249
- y:np.array,
250
- ax:plt.Axes,
251
- n_std:float=3.0,
252
- facecolor:str='none',
253
- **kwargs):
254
- """
255
- Create a plot of the covariance confidence ellipse of *x* and *y*.
256
- Parameters
257
- ----------
258
- x, y : array-like, shape (n, )
259
- Input data.
260
- ax : matplotlib.axes.Axes
261
- The axes object to draw the ellipse into.
262
- n_std : float
263
- The number of standard deviations to determine the ellipse's radiuses.
264
- **kwargs
265
- Forwarded to `~matplotlib.patches.Ellipse`
266
- Returns
267
- -------
268
- matplotlib.patches.Ellipse
269
- """
270
-
271
- if x.shape != y.shape:
272
- raise ValueError("x and y must be the same size")
273
- try:
274
- cov = np.cov(x, y)
275
- pearson = cov[0, 1]/np.sqrt(cov[0, 0] * cov[1, 1])
276
- # Using a special case to obtain the eigenvalues of this
277
- # two-dimensional dataset.
278
- ell_radius_x = np.sqrt(1 + pearson)
279
- ell_radius_y = np.sqrt(1 - pearson)
280
- ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2,
281
- facecolor=facecolor,linewidth=2,linestyle='--', **kwargs)
282
-
283
-
284
- # Calculating the standard deviation of x from
285
- # the squareroot of the variance and multiplying
286
- # with the given number of standard deviations.
287
- scale_x = np.sqrt(cov[0, 0]) * n_std
288
- mean_x = x.mean()
289
-
290
-
291
- # calculating the standard deviation of y ...
292
- scale_y = np.sqrt(cov[1, 1]) * n_std
293
- mean_y = y.mean()
294
-
295
-
296
- transf = transforms.Affine2D() \
297
- .rotate_deg(45) \
298
- .scale(scale_x, scale_y) \
299
- .translate(mean_x, mean_y)
300
-
301
-
302
-
303
- ellipse.set_transform(transf + ax.transData)
304
- except ValueError:
305
- return
306
-
307
- return ax.add_patch(ellipse)
308
-
309
-
310
- def break_plot_big(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
311
- """
312
- Plots a big break plot for the given DataFrame on the provided axis.
313
-
314
- Parameters:
315
- df (pl.DataFrame): The DataFrame containing pitch data.
316
- ax (plt.Axes): The matplotlib axis to plot on.
317
- sport_id (int): The sport ID to determine the plot title.
318
- """
319
- # Set font properties for different elements of the plot
320
- font_properties = {'size': 20}
321
- font_properties_titles = {'size': 32}
322
- font_properties_axes = {'size': 24}
323
-
324
- # Get unique pitch types sorted by 'prop' and 'pitch_type'
325
- label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
326
- j = 0
327
- dict_colour, dict_pitch = self.pitch_colours()
328
- custom_theme, colour_palette = self.sns_custom_theme()
329
-
330
- # Loop through each pitch type and plot confidence ellipses
331
- for label in label_labels:
332
- subset = df.filter(pl.col('pitch_type') == label)
333
- if len(subset) > 4:
334
- try:
335
- if df['pitcher_hand'][0] == 'R':
336
- self.confidence_ellipse(subset['hb'], subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
337
- if df['pitcher_hand'][0] == 'L':
338
- self.confidence_ellipse(subset['hb'] * -1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
339
- except ValueError:
340
- return
341
- j += 1
342
- else:
343
- j += 1
344
-
345
- # Plot scatter plot of pitch data
346
- if df['pitcher_hand'][0] == 'R':
347
- sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
348
- if df['pitcher_hand'][0] == 'L':
349
- sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
350
-
351
- # Set plot limits and labels
352
- ax.set_xlim((-25, 25))
353
- ax.set_ylim((-25, 25))
354
- ax.hlines(y=0, xmin=-50, xmax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
355
- ax.vlines(x=0, ymin=-50, ymax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
356
- ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
357
- ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
358
- ax.set_title(f"{self.sport_id_dict()[sport_id]} - Short Form Pitch Movement Plot", fontdict=font_properties_titles)
359
-
360
- # Remove legend and set tick labels
361
- ax.get_legend().remove()
362
- ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
363
- ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
364
-
365
- # Add text annotations based on pitcher hand
366
- if df['pitcher_hand'][0] == 'R':
367
- ax.text(-24.5, -24.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
368
- ax.text(24.5, -24.5, s='Arm Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
369
- if df['pitcher_hand'][0] == 'L':
370
- ax.invert_xaxis()
371
- ax.text(24.5, -24.5, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
372
- ax.text(-24.5, -24.5, s='Glove Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
373
-
374
- # Set aspect ratio and format tick labels
375
- ax.set_aspect('equal', adjustable='box')
376
- ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
377
- ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
378
-
379
- ### BREAK PLOT ###
380
- def break_plot_big_long(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
381
- """
382
- Plots a long break plot for the given DataFrame on the provided axis.
383
-
384
- Parameters:
385
- df (pl.DataFrame): The DataFrame containing pitch data.
386
- ax (plt.Axes): The matplotlib axis to plot on.
387
- sport_id (int): The sport ID to determine the plot title.
388
- """
389
- # Set font properties for different elements of the plot
390
- font_properties = {'size': 20}
391
- font_properties_titles = {'size': 32}
392
- font_properties_axes = {'size': 24}
393
-
394
- # Get unique pitch types sorted by 'prop' and 'pitch_type'
395
- label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
396
- dict_colour, dict_pitch = self.pitch_colours()
397
- custom_theme, colour_palette = self.sns_custom_theme()
398
- j = 0
399
-
400
- # Loop through each pitch type and plot confidence ellipses
401
- for label in label_labels:
402
- subset = df.filter(pl.col('pitch_type') == label)
403
- print(label)
404
- if len(subset) > 4:
405
- try:
406
- if df['pitcher_hand'][0] == 'R':
407
- self.confidence_ellipse(subset['hb'], subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
408
- if df['pitcher_hand'][0] == 'L':
409
- self.confidence_ellipse(subset['hb'] * -1, subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
410
- except ValueError:
411
- return
412
- j += 1
413
- else:
414
- j += 1
415
-
416
- # Plot scatter plot of pitch data
417
- if df['pitcher_hand'][0] == 'R':
418
- sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
419
- if df['pitcher_hand'][0] == 'L':
420
- sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
421
-
422
- # Set plot limits and labels
423
- ax.set_xlim((-40, 40))
424
- ax.set_ylim((-80, 0))
425
- ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
426
- ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
427
- ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
428
- ax.set_ylabel('Vertical Break (in)', fontdict=font_properties_axes)
429
- ax.set_title(f"{self.sport_id_dict()[sport_id]} - Long Form Pitch Movement Plot", fontdict=font_properties_titles)
430
-
431
- # Remove legend and set tick labels
432
- ax.get_legend().remove()
433
- ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
434
- ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
435
-
436
- # Add text annotations based on pitcher hand
437
- if df['pitcher_hand'][0] == 'R':
438
- ax.text(-39.5, -79.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
439
- ax.text(39.5, -79.5, s='Arm Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
440
- if df['pitcher_hand'][0] == 'L':
441
- ax.invert_xaxis()
442
- ax.text(39.5, -79.5, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
443
- ax.text(-39.5, -79.5, s='Glove Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
444
-
445
- # Set aspect ratio and format tick labels
446
- ax.set_aspect('equal', adjustable='box')
447
- ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
448
- ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
449
-
450
- ### BREAK PLOT ###
451
- def release_point_plot(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
452
- """
453
- Plots the release points for the given DataFrame on the provided axis.
454
-
455
- Parameters:
456
- df (pl.DataFrame): The DataFrame containing pitch data.
457
- ax (plt.Axes): The matplotlib axis to plot on.
458
- sport_id (int): The sport ID to determine the plot title.
459
- """
460
- # Set font properties for different elements of the plot
461
- font_properties = {'size': 20}
462
- font_properties_titles = {'size': 32}
463
- font_properties_axes = {'size': 24}
464
- dict_colour, dict_pitch = self.pitch_colours()
465
- custom_theme, colour_palette = self.sns_custom_theme()
466
-
467
- # Plot scatter plot of release points based on pitcher hand
468
- if df['pitcher_hand'][0] == 'R':
469
- sns.scatterplot(ax=ax, x=df['x0'] * -1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
470
- if df['pitcher_hand'][0] == 'L':
471
- sns.scatterplot(ax=ax, x=df['x0'] * 1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
472
-
473
- # Add patches to the plot
474
- ax.add_patch(plt.Circle((0, 10 / 12 - 18), radius=18, edgecolor='black', facecolor='#a63b17'))
475
- ax.add_patch(plt.Rectangle((-0.5, 9 / 12), 1, 1 / 6, edgecolor='black', facecolor='white'))
476
-
477
- # Set plot limits and labels
478
- ax.set_xlim((-4, 4))
479
- ax.set_ylim((0, 8))
480
- ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
481
- ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
482
- ax.set_ylabel('Vertical Release (ft)', fontdict=font_properties_axes)
483
- ax.set_xlabel('Horizontal Release (ft)', fontdict=font_properties_axes)
484
- ax.set_title(f"{self.sport_id_dict()[sport_id]} - Release Points Catcher Perspective", fontdict=font_properties_titles)
485
-
486
- # Remove legend and set tick labels
487
- ax.get_legend().remove()
488
- ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
489
- ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
490
-
491
- # Add text annotations based on pitcher hand
492
- if df['pitcher_hand'][0] == 'L':
493
- ax.text(-3.95, 0.05, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
494
- ax.text(3.95, 0.05, s='Arm Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
495
- if df['pitcher_hand'][0] == 'R':
496
- ax.invert_xaxis()
497
- ax.text(3.95, 0.05, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
498
- ax.text(-3.95, 0.05, s='Glove Side ', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
499
-
500
- # Set aspect ratio and format tick labels
501
- ax.set_aspect('equal', adjustable='box')
502
- ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
503
- ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
504
-
505
- def df_to_polars(self, df_original: pl.DataFrame, pitcher_id: str, start_date: str, end_date: str, batter_hand: list):
506
- """
507
- Filters and processes the original DataFrame to a Polars DataFrame.
508
-
509
- Parameters:
510
- df_original (pl.DataFrame): The original DataFrame containing pitch data.
511
- pitcher_id (str): The ID of the pitcher.
512
- start_date (str): The start date for filtering the data.
513
- end_date (str): The end date for filtering the data.
514
- batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
515
-
516
- Returns:
517
- pl.DataFrame: The filtered and processed Polars DataFrame.
518
- """
519
- df = df_original.clone()
520
- df = df.filter((pl.col('pitcher_id') == pitcher_id) &
521
- (pl.col('is_pitch')) & (pl.col('pitch_type').is_not_null()) &
522
- (pl.col('pitch_type') != 'NaN') &
523
- (pl.col('game_date') >= start_date) &
524
- (pl.col('game_date') <= end_date) &
525
- (pl.col('batter_hand').is_in(batter_hand)))
526
- df = df.with_columns(
527
- prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
528
- prop=pl.col('is_pitch').sum().over("pitch_type")
529
- )
530
- return df
531
-
532
- def final_plot(self, df: pl.DataFrame, pitcher_id: str, plot_picker: str, sport_id: int):
533
- """
534
- Creates a final plot with player headshot, bio, logo, and pitch movement plots.
535
-
536
- Parameters:
537
- df (pl.DataFrame): The DataFrame containing pitch data.
538
- pitcher_id (str): The ID of the pitcher.
539
- plot_picker (str): The type of plot to create ('short_form_movement', 'long_form_movement', 'release_point').
540
- sport_id (int): The sport ID to determine the plot title.
541
- """
542
- # Set the theme for seaborn plots
543
- sns.set_theme(style="whitegrid", rc=self.sns_custom_theme()[0])
544
-
545
- # Create a figure and a gridspec with 6 rows and 5 columns
546
- fig = plt.figure(figsize=(16, 16), dpi=400)
547
- gs = gridspec.GridSpec(6, 5, figure=fig, height_ratios=[0.00000000005, 5, 30, 5, 2, 0.00000000005], width_ratios=[1, 10, 10, 10, 1])
548
-
549
- # Create subplots for player headshot, bio, and logo
550
- ax_headshot = fig.add_subplot(gs[1, 1])
551
- ax_bio = fig.add_subplot(gs[1, 2])
552
- ax_logo = fig.add_subplot(gs[1, 3])
553
-
554
- # Get the start and end dates and unique batter hands from the DataFrame
555
- start_date = df['game_date'].min()
556
- end_date = df['game_date'].max()
557
- batter_hand = list(df['batter_hand'].unique())
558
-
559
- # Plot player headshot, bio, and logo
560
- self.player_headshot(pitcher_id=pitcher_id, ax=ax_headshot, sport_id=sport_id)
561
- self.player_bio(pitcher_id=pitcher_id, ax=ax_bio, start_date=start_date, end_date=end_date, batter_hand=batter_hand)
562
- self.plot_logo(pitcher_id=pitcher_id, ax=ax_logo)
563
-
564
- # Create subplot for the main plot
565
- ax_main_plot = fig.add_subplot(gs[2, :])
566
-
567
- # Create subplot for the legend
568
- ax_legend = fig.add_subplot(gs[3, :])
569
- ax_legend.axis('off')
570
-
571
- # Create subplot for the footer
572
- ax_footer = fig.add_subplot(gs[-2, :])
573
-
574
- # Plot the selected pitch movement plot
575
- if plot_picker == 'short_form_movement':
576
- self.break_plot_big(df, ax_main_plot, sport_id=sport_id)
577
- elif plot_picker == 'long_form_movement':
578
- self.break_plot_big_long(df, ax_main_plot, sport_id=sport_id)
579
- elif plot_picker == 'release_point':
580
- self.release_point_plot(df, ax_main_plot, sport_id=sport_id)
581
-
582
- # Sort the DataFrame and get unique pitch types
583
- items_in_order = list(df.sort(by=['prop', 'pitch_type'], descending=[True, True])['pitch_type'].unique(maintain_order=True))
584
-
585
- # Get pitch colors and names
586
- dict_colour, dict_pitch = self.pitch_colours()
587
- ordered_colors = [dict_colour[x] for x in items_in_order]
588
- items_in_order = [dict_pitch[x] for x in items_in_order]
589
-
590
- # Create custom legend handles with circles
591
- legend_handles = [mlines.Line2D([], [], color=color, marker='o', linestyle='None', markersize=8, label=label) for color, label in zip(ordered_colors, items_in_order)]
592
-
593
- # Add legend to ax_legend
594
- ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.5), ncol=5, fancybox=True, loc='lower center', fontsize=8, framealpha=1.0, markerscale=2, prop={'size': 16})
595
-
596
- # Add footer text
597
- ax_footer.text(x=0.075, y=1, s='By: Thomas Nestico\n @TJStats', fontname='Calibri', ha='left', fontsize=24, va='top')
598
- ax_footer.text(x=1-0.075, y=1, s='Data: MLB', ha='right', fontname='Calibri', fontsize=24, va='top')
599
- ax_footer.axis('off')
600
-
601
- # Create subplots for the borders
602
- ax_top_border = fig.add_subplot(gs[0, :])
603
- ax_left_border = fig.add_subplot(gs[:, 0])
604
- ax_right_border = fig.add_subplot(gs[:, -1])
605
- ax_bottom_border = fig.add_subplot(gs[-1, :])
606
-
607
- # Turn off the axes for the border subplots
608
- ax_top_border.axis('off')
609
- ax_left_border.axis('off')
610
- ax_right_border.axis('off')
611
- ax_bottom_border.axis('off')
612
-
613
- # Adjust layout and show the figure
614
- fig.tight_layout()
615
- fig.subplots_adjust(hspace=0.1, wspace=0.1)
616
- st.pyplot(fig)
617
-
618
-
619
-
620
-
621
-
622
-
 
 
1
+ import polars as pl
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import seaborn as sns
5
+ from PIL import Image
6
+ import requests
7
+ from io import BytesIO
8
+ from matplotlib.offsetbox import OffsetImage, AnnotationBbox
9
+ from matplotlib.ticker import FuncFormatter
10
+ import matplotlib.transforms as transforms
11
+ from matplotlib.patches import Ellipse
12
+ import matplotlib.gridspec as gridspec
13
+ import matplotlib.patches as mpatches
14
+ import matplotlib.lines as mlines
15
+ from matplotlib.figure import Figure
16
+ import streamlit as st
17
+ import api_scraper
18
+
19
+ # Initialize the scraper
20
+ scraper = api_scraper.MLB_Scrape()
21
+
22
+ class PitchPlotFunctions:
23
+ # Define the pitch_colours method
24
+ def pitch_colours(self):
25
+ # Dictionary of pitch types and their corresponding colors and names
26
+ pitch_colours = {
27
+ 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
28
+ 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
29
+ 'SI': {'colour': '#98165D', 'name': 'Sinker'},
30
+ 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
31
+ 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
32
+ 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
33
+ 'SC': {'colour': '#F08223', 'name': 'Screwball'},
34
+ 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
35
+ 'SL': {'colour': '#67E18D', 'name': 'Slider'},
36
+ 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
37
+ 'SV': {'colour': '#376748', 'name': 'Slurve'},
38
+ 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
39
+ 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
40
+ 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
41
+ 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
42
+ 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
43
+ 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
44
+ 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
45
+ }
46
+
47
+ # Create dictionaries mapping pitch types to their colors and names
48
+ dict_colour = dict(zip(pitch_colours.keys(), [pitch_colours[key]['colour'] for key in pitch_colours]))
49
+ dict_pitch = dict(zip(pitch_colours.keys(), [pitch_colours[key]['name'] for key in pitch_colours]))
50
+
51
+ return dict_colour, dict_pitch
52
+
53
+ # Define the sns_custom_theme method
54
+ def sns_custom_theme(self):
55
+ # Custom theme for seaborn plots
56
+ custom_theme = {
57
+ "axes.facecolor": "white",
58
+ "axes.edgecolor": ".8",
59
+ "axes.grid": True,
60
+ "axes.axisbelow": True,
61
+ "axes.labelcolor": ".15",
62
+ "figure.facecolor": "#f9f9f9",
63
+ "grid.color": ".8",
64
+ "grid.linestyle": "-",
65
+ "text.color": ".15",
66
+ "xtick.color": ".15",
67
+ "ytick.color": ".15",
68
+ "xtick.direction": "out",
69
+ "ytick.direction": "out",
70
+ "lines.solid_capstyle": "round",
71
+ "patch.edgecolor": "w",
72
+ "patch.force_edgecolor": True,
73
+ "image.cmap": "rocket",
74
+ "font.family": ["sans-serif"],
75
+ "font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", "Bitstream Vera Sans", "sans-serif"],
76
+ "xtick.bottom": False,
77
+ "xtick.top": False,
78
+ "ytick.left": False,
79
+ "ytick.right": False,
80
+ "axes.spines.left": True,
81
+ "axes.spines.bottom": True,
82
+ "axes.spines.right": True,
83
+ "axes.spines.top": True
84
+ }
85
+
86
+ # Color palette for the plots
87
+ colour_palette = ['#FFB000', '#648FFF', '#785EF0', '#DC267F', '#FE6100', '#3D1EB2', '#894D80', '#16AA02', '#B5592B', '#A3C1ED']
88
+
89
+ return custom_theme, colour_palette
90
+
91
+ # Define the sport_id_dict method
92
+ def sport_id_dict(self):
93
+ # Dictionary mapping sport IDs to their names
94
+ dict = {1: 'MLB', 11: 'AAA'}
95
+ return dict
96
+
97
+ # Define the team_logos method
98
+ def team_logos(self):
99
+ # List of MLB teams and their corresponding ESPN logo URLs
100
+ mlb_teams = [
101
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
102
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
103
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
104
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
105
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
106
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
107
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
108
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
109
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
110
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
111
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
112
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
113
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
114
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
115
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
116
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
117
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
118
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
119
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
120
+ {"team": "OAK", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
121
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
122
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
123
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
124
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
125
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
126
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
127
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
128
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
129
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
130
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"}
131
+ ]
132
+
133
+ # Create a DataFrame from the list of dictionaries
134
+ df_image = pl.DataFrame(mlb_teams)
135
+ # Set the index to 'team' and convert 'logo_url' to a dictionary
136
+ image_dict = df_image.select(['team', 'logo_url']).to_dict(as_series=False)['logo_url']
137
+
138
+ # Convert to the desired dictionary format
139
+ image_dict = {row['team']: row['logo_url'] for row in df_image.select(['team', 'logo_url']).to_dicts()}
140
+
141
+ return image_dict
142
+
143
+ # Function to get an image from a URL and display it on the given axis
144
+ def player_headshot(self, pitcher_id: str, ax: plt.Axes, sport_id: int):
145
+ """
146
+ Fetches and displays the player's headshot image on the given axis.
147
+
148
+ Parameters:
149
+ pitcher_id (str): The ID of the pitcher.
150
+ ax (plt.Axes): The matplotlib axis to display the image on.
151
+ sport_id (int): The sport ID to determine the URL format.
152
+ """
153
+ # Construct the URL for the player's headshot image
154
+ if sport_id == 1:
155
+ url = f'https://img.mlbstatic.com/mlb-photos/image/'\
156
+ f'upload/d_people:generic:headshot:67:current.png'\
157
+ f'/w_640,q_auto:best/v1/people/{pitcher_id}/headshot/silo/current.png'
158
+ else:
159
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
160
+
161
+ # Send a GET request to the URL
162
+ response = requests.get(url)
163
+ # Open the image from the response content
164
+ img = Image.open(BytesIO(response.content))
165
+ # Display the image on the axis
166
+ ax.set_xlim(0, 2)
167
+ ax.set_ylim(0, 1)
168
+ ax.imshow(img, extent=[0.0, 1, 0, 1], origin='upper')
169
+ # Turn off the axis
170
+ ax.axis('off')
171
+
172
+ # Function to display player bio information on the given axis
173
+ def player_bio(self, pitcher_id: str, ax: plt.Axes, start_date: str, end_date: str, batter_hand: list):
174
+ """
175
+ Fetches and displays the player's bio information on the given axis.
176
+
177
+ Parameters:
178
+ pitcher_id (str): The ID of the pitcher.
179
+ ax (plt.Axes): The matplotlib axis to display the bio information on.
180
+ start_date (str): The start date for the bio information.
181
+ end_date (str): The end date for the bio information.
182
+ batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
183
+ """
184
+ # Construct the URL to fetch player data
185
+ url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
186
+ # Send a GET request to the URL and parse the JSON response
187
+ data = requests.get(url).json()
188
+ # Extract player information from the JSON data
189
+ player_name = data['people'][0]['fullName']
190
+ pitcher_hand = data['people'][0]['pitchHand']['code']
191
+ age = data['people'][0]['currentAge']
192
+ height = data['people'][0]['height']
193
+ weight = data['people'][0]['weight']
194
+ # Display the player's name, handedness, age, height, and weight on the axis
195
+ ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=48)
196
+ ax.text(0.5, 0.6, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=22)
197
+ # Determine the batter hand text
198
+ if batter_hand == ['R']:
199
+ batter_hand_text = ', vs RHH'
200
+ elif batter_hand == ['L']:
201
+ batter_hand_text = ', vs LHH'
202
+ else:
203
+ batter_hand_text = ''
204
+ ax.text(0.5, 0.35, f'{start_date} to {end_date}{batter_hand_text}', va='top', ha='center', fontsize=22, fontstyle='italic')
205
+ # Turn off the axis
206
+ ax.axis('off')
207
+
208
+ # Function to display the team logo on the given axis
209
+ def plot_logo(self, pitcher_id: str, ax: plt.Axes):
210
+ """
211
+ Fetches and displays the team logo on the given axis.
212
+
213
+ Parameters:
214
+ pitcher_id (str): The ID of the pitcher.
215
+ ax (plt.Axes): The matplotlib axis to display the logo on.
216
+ """
217
+ # Construct the URL to fetch player data
218
+ url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
219
+ # Send a GET request to the URL and parse the JSON response
220
+ data = requests.get(url).json()
221
+ # Construct the URL to fetch team data
222
+ try:
223
+ url_team = 'https://statsapi.mlb.com/' + data['people'][0]['currentTeam']['link']
224
+ # Send a GET request to the team URL and parse the JSON response
225
+ data_team = requests.get(url_team).json()
226
+ # Get the logo URL from the image dictionary using the team abbreviation
227
+
228
+ if data_team['teams'][0]['sport']['id'] == 1:
229
+ team_abb = data_team['teams'][0]['abbreviation']
230
+ logo_url = self.team_logos()[team_abb]
231
+ else:
232
+ team_abb = data_team['teams'][0]['parentOrgId']
233
+ logo_url = self.team_logos()[dict(scraper.get_teams().select(['team_id', 'parent_org_abbreviation']).iter_rows())[team_abb]]
234
+ except KeyError:
235
+ logo_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=500&h=500&transparent=true"
236
+ # Send a GET request to the logo URL
237
+ response = requests.get(logo_url)
238
+ # Open the image from the response content
239
+ img = Image.open(BytesIO(response.content))
240
+ # Display the image on the axis
241
+ ax.set_xlim(0, 2)
242
+ ax.set_ylim(0, 1)
243
+ ax.imshow(img, extent=[1, 2, 0, 1], origin='upper')
244
+ # Turn off the axis
245
+ ax.axis('off')
246
+
247
+ ### PITCH ELLIPSE ###
248
+ def confidence_ellipse( self,
249
+ x:np.array,
250
+ y:np.array,
251
+ ax:plt.Axes,
252
+ n_std:float=3.0,
253
+ facecolor:str='none',
254
+ **kwargs):
255
+ """
256
+ Create a plot of the covariance confidence ellipse of *x* and *y*.
257
+ Parameters
258
+ ----------
259
+ x, y : array-like, shape (n, )
260
+ Input data.
261
+ ax : matplotlib.axes.Axes
262
+ The axes object to draw the ellipse into.
263
+ n_std : float
264
+ The number of standard deviations to determine the ellipse's radiuses.
265
+ **kwargs
266
+ Forwarded to `~matplotlib.patches.Ellipse`
267
+ Returns
268
+ -------
269
+ matplotlib.patches.Ellipse
270
+ """
271
+
272
+ if x.shape != y.shape:
273
+ raise ValueError("x and y must be the same size")
274
+ try:
275
+ cov = np.cov(x, y)
276
+ pearson = cov[0, 1]/np.sqrt(cov[0, 0] * cov[1, 1])
277
+ # Using a special case to obtain the eigenvalues of this
278
+ # two-dimensional dataset.
279
+ ell_radius_x = np.sqrt(1 + pearson)
280
+ ell_radius_y = np.sqrt(1 - pearson)
281
+ ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2,
282
+ facecolor=facecolor,linewidth=2,linestyle='--', **kwargs)
283
+
284
+
285
+ # Calculating the standard deviation of x from
286
+ # the squareroot of the variance and multiplying
287
+ # with the given number of standard deviations.
288
+ scale_x = np.sqrt(cov[0, 0]) * n_std
289
+ mean_x = x.mean()
290
+
291
+
292
+ # calculating the standard deviation of y ...
293
+ scale_y = np.sqrt(cov[1, 1]) * n_std
294
+ mean_y = y.mean()
295
+
296
+
297
+ transf = transforms.Affine2D() \
298
+ .rotate_deg(45) \
299
+ .scale(scale_x, scale_y) \
300
+ .translate(mean_x, mean_y)
301
+
302
+
303
+
304
+ ellipse.set_transform(transf + ax.transData)
305
+ except ValueError:
306
+ return
307
+
308
+ return ax.add_patch(ellipse)
309
+
310
+
311
+ def break_plot_big(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
312
+ """
313
+ Plots a big break plot for the given DataFrame on the provided axis.
314
+
315
+ Parameters:
316
+ df (pl.DataFrame): The DataFrame containing pitch data.
317
+ ax (plt.Axes): The matplotlib axis to plot on.
318
+ sport_id (int): The sport ID to determine the plot title.
319
+ """
320
+ # Set font properties for different elements of the plot
321
+ font_properties = {'size': 20}
322
+ font_properties_titles = {'size': 32}
323
+ font_properties_axes = {'size': 24}
324
+
325
+ # Get unique pitch types sorted by 'prop' and 'pitch_type'
326
+ label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
327
+ j = 0
328
+ dict_colour, dict_pitch = self.pitch_colours()
329
+ custom_theme, colour_palette = self.sns_custom_theme()
330
+
331
+ # Loop through each pitch type and plot confidence ellipses
332
+ for label in label_labels:
333
+ subset = df.filter(pl.col('pitch_type') == label)
334
+ if len(subset) > 4:
335
+ try:
336
+ if df['pitcher_hand'][0] == 'R':
337
+ self.confidence_ellipse(subset['hb'], subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
338
+ if df['pitcher_hand'][0] == 'L':
339
+ self.confidence_ellipse(subset['hb'] * -1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
340
+ except ValueError:
341
+ return
342
+ j += 1
343
+ else:
344
+ j += 1
345
+
346
+ # Plot scatter plot of pitch data
347
+ if df['pitcher_hand'][0] == 'R':
348
+ sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
349
+ if df['pitcher_hand'][0] == 'L':
350
+ sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
351
+
352
+ # Set plot limits and labels
353
+ ax.set_xlim((-25, 25))
354
+ ax.set_ylim((-25, 25))
355
+ ax.hlines(y=0, xmin=-50, xmax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
356
+ ax.vlines(x=0, ymin=-50, ymax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
357
+ ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
358
+ ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
359
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Short Form Pitch Movement Plot", fontdict=font_properties_titles)
360
+
361
+ # Remove legend and set tick labels
362
+ ax.get_legend().remove()
363
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
364
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
365
+
366
+ # Add text annotations based on pitcher hand
367
+ if df['pitcher_hand'][0] == 'R':
368
+ ax.text(-24.5, -24.5, s=' Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
369
+ ax.text(24.5, -24.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
370
+ if df['pitcher_hand'][0] == 'L':
371
+ ax.invert_xaxis()
372
+ ax.text(24.5, -24.5, s=' Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
373
+ ax.text(-24.5, -24.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
374
+
375
+ # Set aspect ratio and format tick labels
376
+ ax.set_aspect('equal', adjustable='box')
377
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
378
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
379
+
380
+ ### BREAK PLOT ###
381
+ def break_plot_big_long(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
382
+ """
383
+ Plots a long break plot for the given DataFrame on the provided axis.
384
+
385
+ Parameters:
386
+ df (pl.DataFrame): The DataFrame containing pitch data.
387
+ ax (plt.Axes): The matplotlib axis to plot on.
388
+ sport_id (int): The sport ID to determine the plot title.
389
+ """
390
+ # Set font properties for different elements of the plot
391
+ font_properties = {'size': 20}
392
+ font_properties_titles = {'size': 32}
393
+ font_properties_axes = {'size': 24}
394
+
395
+ # Get unique pitch types sorted by 'prop' and 'pitch_type'
396
+ label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
397
+ dict_colour, dict_pitch = self.pitch_colours()
398
+ custom_theme, colour_palette = self.sns_custom_theme()
399
+ j = 0
400
+
401
+ # Loop through each pitch type and plot confidence ellipses
402
+ for label in label_labels:
403
+ subset = df.filter(pl.col('pitch_type') == label)
404
+ print(label)
405
+ if len(subset) > 4:
406
+ try:
407
+ if df['pitcher_hand'][0] == 'R':
408
+ self.confidence_ellipse(subset['hb'], subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
409
+ if df['pitcher_hand'][0] == 'L':
410
+ self.confidence_ellipse(subset['hb'] * -1, subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
411
+ except ValueError:
412
+ return
413
+ j += 1
414
+ else:
415
+ j += 1
416
+
417
+ # Plot scatter plot of pitch data
418
+ if df['pitcher_hand'][0] == 'R':
419
+ sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
420
+ if df['pitcher_hand'][0] == 'L':
421
+ sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
422
+
423
+ # Set plot limits and labels
424
+ ax.set_xlim((-40, 40))
425
+ ax.set_ylim((-80, 0))
426
+ ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
427
+ ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
428
+ ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
429
+ ax.set_ylabel('Vertical Break (in)', fontdict=font_properties_axes)
430
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Long Form Pitch Movement Plot", fontdict=font_properties_titles)
431
+
432
+ # Remove legend and set tick labels
433
+ ax.get_legend().remove()
434
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
435
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
436
+
437
+ # Add text annotations based on pitcher hand
438
+ if df['pitcher_hand'][0] == 'R':
439
+ ax.text(-39.5, -79.5, s=' Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
440
+ ax.text(39.5, -79.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
441
+ if df['pitcher_hand'][0] == 'L':
442
+ ax.invert_xaxis()
443
+ ax.text(39.5, -79.5, s=' Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
444
+ ax.text(-39.5, -79.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
445
+
446
+ # Set aspect ratio and format tick labels
447
+ ax.set_aspect('equal', adjustable='box')
448
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
449
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
450
+
451
+ ### BREAK PLOT ###
452
+ def release_point_plot(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
453
+ """
454
+ Plots the release points for the given DataFrame on the provided axis.
455
+
456
+ Parameters:
457
+ df (pl.DataFrame): The DataFrame containing pitch data.
458
+ ax (plt.Axes): The matplotlib axis to plot on.
459
+ sport_id (int): The sport ID to determine the plot title.
460
+ """
461
+ # Set font properties for different elements of the plot
462
+ font_properties = {'size': 20}
463
+ font_properties_titles = {'size': 32}
464
+ font_properties_axes = {'size': 24}
465
+ dict_colour, dict_pitch = self.pitch_colours()
466
+ custom_theme, colour_palette = self.sns_custom_theme()
467
+
468
+ # Plot scatter plot of release points based on pitcher hand
469
+ if df['pitcher_hand'][0] == 'R':
470
+ sns.scatterplot(ax=ax, x=df['x0'] * -1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
471
+ if df['pitcher_hand'][0] == 'L':
472
+ sns.scatterplot(ax=ax, x=df['x0'] * 1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
473
+
474
+ # Add patches to the plot
475
+ ax.add_patch(plt.Circle((0, 10 / 12 - 18), radius=18, edgecolor='black', facecolor='#a63b17'))
476
+ ax.add_patch(plt.Rectangle((-0.5, 9 / 12), 1, 1 / 6, edgecolor='black', facecolor='white'))
477
+
478
+ # Set plot limits and labels
479
+ ax.set_xlim((-4, 4))
480
+ ax.set_ylim((0, 8))
481
+ ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
482
+ ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
483
+ ax.set_ylabel('Vertical Release (ft)', fontdict=font_properties_axes)
484
+ ax.set_xlabel('Horizontal Release (ft)', fontdict=font_properties_axes)
485
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Release Points Catcher Perspective", fontdict=font_properties_titles)
486
+
487
+ # Remove legend and set tick labels
488
+ ax.get_legend().remove()
489
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
490
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
491
+
492
+ # Add text annotations based on pitcher hand
493
+ if df['pitcher_hand'][0] == 'L':
494
+ ax.text(-3.95, 0.05, s=' Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
495
+ ax.text(3.95, 0.05, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
496
+ if df['pitcher_hand'][0] == 'R':
497
+ ax.invert_xaxis()
498
+ ax.text(3.95, 0.05, s=' Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
499
+ ax.text(-3.95, 0.05, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
500
+
501
+ # Set aspect ratio and format tick labels
502
+ ax.set_aspect('equal', adjustable='box')
503
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
504
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
505
+
506
+ def df_to_polars(self, df_original: pl.DataFrame, pitcher_id: str, start_date: str, end_date: str, batter_hand: list):
507
+ """
508
+ Filters and processes the original DataFrame to a Polars DataFrame.
509
+
510
+ Parameters:
511
+ df_original (pl.DataFrame): The original DataFrame containing pitch data.
512
+ pitcher_id (str): The ID of the pitcher.
513
+ start_date (str): The start date for filtering the data.
514
+ end_date (str): The end date for filtering the data.
515
+ batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
516
+
517
+ Returns:
518
+ pl.DataFrame: The filtered and processed Polars DataFrame.
519
+ """
520
+ df = df_original.clone()
521
+ df = df.filter((pl.col('pitcher_id') == pitcher_id) &
522
+ (pl.col('is_pitch')) & (pl.col('pitch_type').is_not_null()) &
523
+ (pl.col('pitch_type') != 'NaN') &
524
+ (pl.col('game_date') >= start_date) &
525
+ (pl.col('game_date') <= end_date) &
526
+ (pl.col('batter_hand').is_in(batter_hand)))
527
+ df = df.with_columns(
528
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
529
+ prop=pl.col('is_pitch').sum().over("pitch_type")
530
+ )
531
+ return df
532
+
533
+ def final_plot(self, df: pl.DataFrame, pitcher_id: str, plot_picker: str, sport_id: int):
534
+ """
535
+ Creates a final plot with player headshot, bio, logo, and pitch movement plots.
536
+
537
+ Parameters:
538
+ df (pl.DataFrame): The DataFrame containing pitch data.
539
+ pitcher_id (str): The ID of the pitcher.
540
+ plot_picker (str): The type of plot to create ('short_form_movement', 'long_form_movement', 'release_point').
541
+ sport_id (int): The sport ID to determine the plot title.
542
+ """
543
+ # Set the theme for seaborn plots
544
+ sns.set_theme(style="whitegrid", rc=self.sns_custom_theme()[0])
545
+
546
+ # Create a figure and a gridspec with 6 rows and 5 columns
547
+ fig = plt.figure(figsize=(16, 16), dpi=400)
548
+ gs = gridspec.GridSpec(6, 5, figure=fig, height_ratios=[0.00000000005, 5, 30, 5, 2, 0.00000000005], width_ratios=[1, 10, 10, 10, 1])
549
+
550
+ # Create subplots for player headshot, bio, and logo
551
+ ax_headshot = fig.add_subplot(gs[1, 1])
552
+ ax_bio = fig.add_subplot(gs[1, 2])
553
+ ax_logo = fig.add_subplot(gs[1, 3])
554
+
555
+ # Get the start and end dates and unique batter hands from the DataFrame
556
+ start_date = df['game_date'].min()
557
+ end_date = df['game_date'].max()
558
+ batter_hand = list(df['batter_hand'].unique())
559
+
560
+ # Plot player headshot, bio, and logo
561
+ self.player_headshot(pitcher_id=pitcher_id, ax=ax_headshot, sport_id=sport_id)
562
+ self.player_bio(pitcher_id=pitcher_id, ax=ax_bio, start_date=start_date, end_date=end_date, batter_hand=batter_hand)
563
+ self.plot_logo(pitcher_id=pitcher_id, ax=ax_logo)
564
+
565
+ # Create subplot for the main plot
566
+ ax_main_plot = fig.add_subplot(gs[2, :])
567
+
568
+ # Create subplot for the legend
569
+ ax_legend = fig.add_subplot(gs[3, :])
570
+ ax_legend.axis('off')
571
+
572
+ # Create subplot for the footer
573
+ ax_footer = fig.add_subplot(gs[-2, :])
574
+
575
+ # Plot the selected pitch movement plot
576
+ if plot_picker == 'short_form_movement':
577
+ self.break_plot_big(df, ax_main_plot, sport_id=sport_id)
578
+ elif plot_picker == 'long_form_movement':
579
+ self.break_plot_big_long(df, ax_main_plot, sport_id=sport_id)
580
+ elif plot_picker == 'release_point':
581
+ self.release_point_plot(df, ax_main_plot, sport_id=sport_id)
582
+
583
+ # Sort the DataFrame and get unique pitch types
584
+ items_in_order = list(df.sort(by=['prop', 'pitch_type'], descending=[True, True])['pitch_type'].unique(maintain_order=True))
585
+
586
+ # Get pitch colors and names
587
+ dict_colour, dict_pitch = self.pitch_colours()
588
+ ordered_colors = [dict_colour[x] for x in items_in_order]
589
+ items_in_order = [dict_pitch[x] for x in items_in_order]
590
+
591
+ # Create custom legend handles with circles
592
+ legend_handles = [mlines.Line2D([], [], color=color, marker='o', linestyle='None', markersize=8, label=label) for color, label in zip(ordered_colors, items_in_order)]
593
+
594
+ # Add legend to ax_legend
595
+ ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.5), ncol=5, fancybox=True, loc='lower center', fontsize=8, framealpha=1.0, markerscale=2, prop={'size': 16})
596
+
597
+ # Add footer text
598
+ ax_footer.text(x=0.075, y=1, s='By: Thomas Nestico\n @TJStats', fontname='Calibri', ha='left', fontsize=24, va='top')
599
+ ax_footer.text(x=1-0.075, y=1, s='Data: MLB', ha='right', fontname='Calibri', fontsize=24, va='top')
600
+ ax_footer.axis('off')
601
+
602
+ # Create subplots for the borders
603
+ ax_top_border = fig.add_subplot(gs[0, :])
604
+ ax_left_border = fig.add_subplot(gs[:, 0])
605
+ ax_right_border = fig.add_subplot(gs[:, -1])
606
+ ax_bottom_border = fig.add_subplot(gs[-1, :])
607
+
608
+ # Turn off the axes for the border subplots
609
+ ax_top_border.axis('off')
610
+ ax_left_border.axis('off')
611
+ ax_right_border.axis('off')
612
+ ax_bottom_border.axis('off')
613
+
614
+ # Adjust layout and show the figure
615
+ fig.tight_layout()
616
+ fig.subplots_adjust(hspace=0.1, wspace=0.1)
617
+ st.pyplot(fig)
618
+
619
+
620
+
621
+
622
+
623
+