nesticot commited on
Commit
562ce62
·
verified ·
1 Parent(s): ecac748

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +857 -857
app.py CHANGED
@@ -1,858 +1,858 @@
1
- import polars as pl
2
- import numpy as np
3
- import pandas as pd
4
- import api_scraper
5
- scrape = api_scraper.MLB_Scrape()
6
- from functions import df_update
7
- from functions import pitch_summary_functions
8
- update = df_update.df_update()
9
- from stuff_model import feature_engineering as fe
10
- from stuff_model import stuff_apply
11
- import requests
12
- import joblib
13
- from matplotlib.gridspec import GridSpec
14
- from shiny import App, reactive, ui, render
15
- from shiny.ui import h2, tags
16
- import matplotlib.pyplot as plt
17
- import matplotlib.gridspec as gridspec
18
- import seaborn as sns
19
- from functions.pitch_summary_functions import *
20
- from shiny import App, reactive, ui, render
21
- from shiny.ui import h2, tags
22
-
23
- colour_palette = ['#FFB000','#648FFF','#785EF0',
24
- '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
25
-
26
-
27
- year_list = [2017,2018,2019,2020,2021,2022,2023,2024,2025]
28
-
29
-
30
-
31
- level_dict = {'1':'MLB',
32
- '11':'AAA',
33
- # '12':'AA',
34
- #'13':'A+',
35
- '14':'A',
36
- '17':'AFL',
37
- '22':'College',
38
- '21':'Prospects',
39
- '51':'International' }
40
-
41
- function_dict={
42
- 'velocity_kdes':'Velocity Distributions',
43
- 'break_plot':'Pitch Movement',
44
- 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
45
- 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
46
- 'location_plot_lhb':'Locations vs LHB',
47
- 'location_plot_rhb':'Locations vs RHB',
48
- }
49
-
50
-
51
- split_dict = {'all':'All',
52
- 'left':'LHH',
53
- 'right':'RHH'}
54
-
55
- split_dict_hand = {'all':['L','R'],
56
- 'left':['L'],
57
- 'right':['R']}
58
-
59
-
60
- type_dict = {'R':'Regular Season',
61
- 'S':'Spring',
62
- 'P':'Playoffs' }
63
-
64
-
65
-
66
- # List of MLB teams and their corresponding ESPN logo URLs
67
- mlb_teams = [
68
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
69
- {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
70
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
71
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
72
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
73
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
74
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
75
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
76
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
77
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
78
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
79
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
80
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
81
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
82
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
83
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
84
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
85
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
86
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
87
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
88
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
89
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
90
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
91
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
92
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
93
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
94
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
95
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
96
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
97
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
98
- {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
99
- ]
100
-
101
-
102
- df_image = pd.DataFrame(mlb_teams)
103
- image_dict = df_image.set_index('team')['logo_url'].to_dict()
104
- image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
105
-
106
-
107
-
108
- import requests
109
-
110
- import os
111
- CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
112
- ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
113
- BACKUP_PW = os.getenv("BACKUP_PW")
114
- ADMIN_PW = os.getenv("ADMIN_PW")
115
-
116
- url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
117
-
118
- headers = {
119
- "Authorization": f"Bearer {ACCESS_TOKEN}"
120
- }
121
-
122
- # Simple parameters, requesting the member's email and currently entitled tiers
123
- params = {
124
- "fields[member]": "full_name,email", # Request the member's email
125
- "include": "currently_entitled_tiers", # Include the currently entitled tiers
126
- "page[size]": 1000 # Fetch up to 1000 patrons per request
127
- }
128
-
129
- response = requests.get(url, headers=headers, params=params)
130
-
131
-
132
- VALID_PASSWORDS = []
133
- if response.status_code == 200:
134
- data = response.json()
135
- for patron in data['data']:
136
- try:
137
- tiers = patron['relationships']['currently_entitled_tiers']['data']
138
- if any(tier['id'] == '9078921' for tier in tiers):
139
- full_name = patron['attributes']['email']
140
- VALID_PASSWORDS.append(full_name)
141
- except KeyError:
142
- continue
143
- VALID_PASSWORDS.append(BACKUP_PW)
144
- VALID_PASSWORDS.append(ADMIN_PW)
145
- VALID_PASSWORDS.append('')
146
-
147
- from shiny import App, reactive, ui, render
148
- from shiny.ui import h2, tags
149
-
150
- from datetime import datetime
151
-
152
- def is_valid_date(date_str):
153
- try:
154
- datetime.strptime(date_str, "%Y-%m-%d") # Attempt to parse the date
155
- return True
156
- except ValueError:
157
- return False # If parsing fails, it's not in the correct format
158
-
159
- # Define the login UI
160
- login_ui = ui.page_fluid(
161
- ui.card(
162
- ui.h2([
163
- "TJStats Pitching Summary App ",
164
- ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
165
- ]),
166
- ui.p(
167
- "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
168
- ui.tags.a("Patreon post", href="https://www.patreon.com/posts/116064432", target="_blank"),
169
- "."
170
- ),
171
- ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="50%"),
172
- ui.tags.input(
173
- type="checkbox",
174
- id="authenticated",
175
- value=False,
176
- disabled=True
177
- ),
178
- ui.input_action_button("login", "Login", class_="btn-primary"),
179
- ui.output_text("login_message"),
180
- )
181
- )
182
-
183
-
184
- # Define the UI layout for the app
185
- main_ui = ui.page_sidebar(
186
- ui.sidebar(
187
- # Row for selecting season and level
188
- ui.row(
189
- ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
190
- ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
191
- ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
192
- ),
193
- # Row for the action button to get player list
194
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
195
- # Row for selecting the player
196
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
197
- # Row for selecting the date range
198
- ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
199
-
200
- # Rows for selecting plots and split options
201
- ui.row(
202
- ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
203
- ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
204
- ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
205
- ),
206
- ui.row(
207
- ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
208
- ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
209
- ),
210
- ui.row(
211
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
212
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
213
- ),
214
-
215
- # Row for the action button to generate plot
216
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
217
- width="400px" # Added this parameter to control sidebar width
218
- ),
219
-
220
- # Main content area with tabs (placed directly in page_sidebar)
221
- ui.navset_tab(
222
- ui.nav_panel("Pitching Summary",
223
- ui.output_text("status"),
224
- ui.output_plot('plot', width='2100px', height='2100px')
225
- ),
226
- ui.nav_panel("Game Summary",
227
- ui.output_text("status2"),
228
- ui.output_plot('game_plot', width='2100px', height='2100px')
229
- ),
230
- ui.nav_panel("Table Range",
231
- ui.output_data_frame("grid")),
232
- ui.nav_panel("Table Game",
233
- ui.output_data_frame("grid_game")),
234
- id="tabset"
235
- )
236
- )
237
-
238
- # Combined UI with conditional panel
239
- app_ui = ui.page_fluid(
240
- ui.tags.head(
241
- ui.tags.script(src="script.js")
242
- ),
243
-
244
- ui.panel_conditional(
245
- "!input.authenticated",
246
- login_ui
247
- ),
248
- ui.panel_conditional(
249
- "input.authenticated",
250
- main_ui
251
- )
252
- )
253
-
254
-
255
- def server(input, output, session):
256
-
257
- @reactive.Effect
258
- @reactive.event(input.login)
259
- def check_password():
260
- if input.password() in VALID_PASSWORDS:
261
- ui.update_checkbox("authenticated", value=True)
262
- ui.update_text("login_message", value="")
263
- else:
264
- ui.update_text("login_message", value="Invalid password!")
265
- ui.update_text("password", value="")
266
-
267
- @output
268
- @render.text
269
- def login_message():
270
- return ""
271
-
272
- @reactive.calc
273
- @reactive.event(input.pitcher_id, input.date_id,input.split_id)
274
- def cached_data():
275
-
276
- year_input = int(input.year_input())
277
- sport_id = int(input.level_input())
278
- player_input = int(input.pitcher_id())
279
-
280
-
281
- start_date = str(input.date_id()[0])
282
- end_date = str(input.date_id()[1])
283
- game_list = scrape.get_player_games_list(sport_id = sport_id,
284
- season = year_input,
285
- player_id = player_input,
286
- start_date = start_date,
287
- end_date = end_date,
288
- game_type = [input.type_input()])
289
-
290
- # if input.tabset() == 'Game Summary':
291
- # print(year_input, sport_id, player_input, 'yup')
292
- # print(input.date_id())
293
- # game_list = [input.date_id()]
294
-
295
-
296
- data_list = scrape.get_data(game_list_input = game_list[:])
297
-
298
- try:
299
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
300
- (pl.col("pitcher_id") == player_input)&
301
- (pl.col("is_pitch") == True)&
302
- (pl.col("start_speed") >= 50)&
303
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
304
-
305
- )))).with_columns(
306
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
307
- ))
308
- return df
309
-
310
-
311
- except TypeError:
312
- print("NONE")
313
- return None
314
-
315
- @reactive.calc
316
- @reactive.event(input.pitcher_id, input.date_id,input.split_id,input.tabset)
317
- def cached_data_daily():
318
-
319
- year_input = int(input.year_input())
320
- sport_id = int(input.level_input())
321
- player_input = int(input.pitcher_id())
322
-
323
-
324
- # start_date = str(input.date_id()[0])
325
- # end_date = str(input.date_id()[1])
326
- game_list = [int(input.date_id())]
327
- print(game_list)
328
-
329
- # if input.tabset() == 'Game Summary':
330
- # print(year_input, sport_id, player_input, 'yup')
331
- # print(input.date_id())
332
- # game_list =
333
-
334
-
335
- data_list = scrape.get_data(game_list_input = game_list[:])
336
-
337
- try:
338
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
339
- (pl.col("pitcher_id") == player_input)&
340
- (pl.col("is_pitch") == True)&
341
- (pl.col("start_speed") >= 50)&
342
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
343
-
344
- )))).with_columns(
345
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
346
- ))
347
- return df
348
-
349
-
350
- except TypeError:
351
- print("NONE")
352
- return None
353
-
354
-
355
- @render.ui
356
- @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
357
- def player_select_ui():
358
- # Get the list of pitchers for the selected level and season
359
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
360
- (pl.col("position").is_in(['P','TWP']))|
361
- (pl.col("player_id").is_in([686846]))
362
-
363
- ).sort("name")
364
-
365
- # Create a dictionary of pitcher IDs and names
366
- pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
367
-
368
-
369
-
370
-
371
- # Return a select input for choosing a pitcher
372
- return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
373
-
374
- @render.ui
375
- @reactive.event(input.player_button, input.pitcher_id,input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
376
- def date_id():
377
- if input.tabset() == 'Pitching Summary' or input.tabset() == 'Table Range':
378
- # Create a date range input for selecting the date range within the selected year
379
- return ui.input_date_range("date_id", "Select Date Range",
380
- start=f"{int(input.year_input())}-01-01",
381
- end=f"{int(input.year_input())}-12-31",
382
- min=f"{int(input.year_input())}-01-01",
383
- max=f"{int(input.year_input())}-12-31")
384
-
385
- if input.tabset() == 'Game Summary' or input.tabset() == 'Table Game':
386
- year_input = int(input.year_input())
387
- sport_id = int(input.level_input())
388
- player_input = int(input.pitcher_id())
389
- print('game summary')
390
- # start_date = str(input.date_id()[0])
391
- # end_date = str(input.date_id()[1])
392
-
393
- game_list = scrape.get_player_games_list(player_id = player_input,
394
- season = year_input,
395
- sport_id=sport_id,
396
- game_type=[input.type_input()],
397
- pitching = True)
398
-
399
- schedule_df = scrape.get_schedule(year_input=[year_input],
400
- sport_id= [sport_id],
401
- game_type = [input.type_input()])
402
-
403
- player_schedule_df = schedule_df.filter(pl.col('game_id').is_in(game_list)).to_pandas().sort_values('date')
404
-
405
- player_schedule_df['def'] = player_schedule_df['date'].astype(str) + ' - ' + player_schedule_df['away'] + ' @ ' + player_schedule_df['home'] + ' '
406
-
407
-
408
- game_dict = dict(zip(player_schedule_df['game_id'], player_schedule_df['def']))
409
- # print(game_dict)
410
-
411
- return ui.input_select("date_id", "Select Game", game_dict)
412
-
413
- @output
414
- @render.text
415
- def status():
416
- # Only show status when generating
417
- if input.generate == 0:
418
- return ""
419
- return ""
420
-
421
-
422
-
423
-
424
- @output
425
- @render.plot
426
- @reactive.event(input.generate_plot, ignore_none=False)
427
- def plot():
428
- # Show progress/loading notification
429
-
430
-
431
-
432
- with ui.Progress(min=0, max=1) as p:
433
- p.set(message="Generating plot", detail="This may take a while...")
434
-
435
-
436
- p.set(0.3, "Gathering data...")
437
- year_input = int(input.year_input())
438
- sport_id = int(input.level_input())
439
- player_input = int(input.pitcher_id())
440
- start_date = str(input.date_id()[0])
441
- end_date = str(input.date_id()[1])
442
- if not is_valid_date(start_date):
443
- fig = plt.figure(figsize=(26,26))
444
- fig.text(x=0.1,y=0.9,s='Select Date Range and Generate Plot',fontsize=36,ha='left')
445
- return fig
446
- print(year_input, sport_id, player_input, start_date, end_date)
447
-
448
- df = cached_data()
449
-
450
- if df is None:
451
- fig = plt.figure(figsize=(26,26))
452
- fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
453
- return fig
454
-
455
- df = df.clone()
456
-
457
-
458
- p.set(0.6, "Creating plot...")
459
-
460
-
461
- #plt.rcParams["figure.figsize"] = [10,10]
462
- fig = plt.figure(figsize=(26,26))
463
- plt.rcParams.update({'figure.autolayout': True})
464
- fig.set_facecolor('white')
465
- sns.set_theme(style="whitegrid", palette=colour_palette)
466
- print('this is the one plot')
467
-
468
- gs = gridspec.GridSpec(6, 8,
469
- height_ratios=[6,20,12,36,36,6],
470
- width_ratios=[4,18,18,18,18,18,18,4])
471
-
472
-
473
- gs.update(hspace=0.2, wspace=0.5)
474
-
475
- # Define the positions of each subplot in the grid
476
- ax_headshot = fig.add_subplot(gs[1,1:3])
477
- ax_bio = fig.add_subplot(gs[1,3:5])
478
- ax_logo = fig.add_subplot(gs[1,5:7])
479
-
480
- ax_season_table = fig.add_subplot(gs[2,1:7])
481
-
482
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
483
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
484
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
485
-
486
- ax_table = fig.add_subplot(gs[4,1:7])
487
-
488
- ax_footer = fig.add_subplot(gs[-1,1:7])
489
- ax_header = fig.add_subplot(gs[0,1:7])
490
- ax_left = fig.add_subplot(gs[:,0])
491
- ax_right = fig.add_subplot(gs[:,-1])
492
-
493
- # Hide axes for footer, header, left, and right
494
- ax_footer.axis('off')
495
- ax_header.axis('off')
496
- ax_left.axis('off')
497
- ax_right.axis('off')
498
-
499
- sns.set_theme(style="whitegrid", palette=colour_palette)
500
- fig.set_facecolor('white')
501
-
502
- df_teams = scrape.get_teams()
503
-
504
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
505
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
506
-
507
- if input.switch():
508
-
509
- # Get the logo URL from the image dictionary using the team abbreviation
510
- logo_url = input.logo_select()
511
-
512
- # Send a GET request to the logo URL
513
- response = requests.get(logo_url)
514
-
515
- # Open the image from the response content
516
- img = Image.open(BytesIO(response.content))
517
-
518
- # Display the image on the axis
519
- ax_logo.set_xlim(0, 1.3)
520
- ax_logo.set_ylim(0, 1)
521
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
522
-
523
- # Turn off the axis
524
- ax_logo.axis('off')
525
-
526
- else:
527
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
528
-
529
- stat_summary_table(df=df,
530
- ax=ax_season_table,
531
- player_input=player_input,
532
- split=input.split_id(),
533
- sport_id=sport_id,
534
- game_type=[input.type_input()])
535
-
536
- # break_plot(df=df_plot,ax=ax2)
537
- for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
538
- if x == 'velocity_kdes':
539
- velocity_kdes(df,
540
- ax=y,
541
- gs=gs,
542
- gs_x=[3,4],
543
- gs_y=[z,z+2],
544
- fig=fig)
545
- if x == 'tj_stuff_roling':
546
- tj_stuff_roling(df=df,
547
- window=int(input.rolling_window()),
548
- ax=y)
549
-
550
- if x == 'tj_stuff_roling_game':
551
- tj_stuff_roling_game(df=df,
552
- window=int(input.rolling_window()),
553
- ax=y)
554
-
555
- if x == 'break_plot':
556
- break_plot(df = df,ax=y)
557
-
558
- if x == 'location_plot_lhb':
559
- location_plot(df = df,ax=y,hand='L')
560
-
561
- if x == 'location_plot_rhb':
562
- location_plot(df = df,ax=y,hand='R')
563
-
564
- summary_table(df=df,
565
- ax=ax_table)
566
-
567
- plot_footer(ax_footer)
568
-
569
- ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
570
- # Hide axes ticks and labels
571
- ax_watermark.set_xticks([])
572
- ax_watermark.set_yticks([])
573
- ax_watermark.set_frame_on(False) # Optional: Hide border
574
-
575
- img = Image.open('tj stats circle-01_new.jpg')
576
- # Display the image
577
- ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
578
-
579
-
580
- ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
581
- ax_watermark2.set_xlim(0,1)
582
- ax_watermark2.set_ylim(0,1)
583
- # Hide axes ticks and labels
584
- ax_watermark2.set_xticks([])
585
- ax_watermark2.set_yticks([])
586
- ax_watermark2.set_frame_on(False) # Optional: Hide border
587
-
588
- # Open the image
589
- img = Image.open('tj stats circle-01_new.jpg')
590
- # Get the original size
591
- width, height = img.size
592
- # Calculate the new size (50% larger)
593
- new_width = int(width * 0.5)
594
- new_height = int(height * 0.5)
595
- # Resize the image
596
- img_resized = img.resize((new_width, new_height))
597
- # Display the image
598
- ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
599
-
600
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
601
-
602
-
603
-
604
-
605
- @output
606
- @render.plot
607
- @reactive.event(input.generate_plot, ignore_none=False)
608
- def game_plot():
609
- # Show progress/loading notification
610
- with ui.Progress(min=0, max=1) as p:
611
- print(input.date_id(),'TEST')
612
-
613
- if isinstance(input.date_id(), tuple):
614
- fig = plt.figure(figsize=(26,26))
615
- fig.text(x=0.1,y=0.9,s='Select Game and Generate Plot',fontsize=36,ha='left')
616
- return fig
617
-
618
-
619
- p.set(message="Generating plot", detail="This may take a while...")
620
-
621
-
622
- p.set(0.3, "Gathering data...")
623
- year_input = int(input.year_input())
624
- sport_id = int(input.level_input())
625
- player_input = int(input.pitcher_id())
626
-
627
- # print(input.game_id())
628
- # print(year_input, sport_id, player_input)
629
-
630
- # print(year_input, sport_id, player_input, start_date, end_date)
631
-
632
- df = cached_data_daily()
633
-
634
- # start_date = str(df['game_date'][0])
635
- # end_date = str(df['game_date'][0])
636
-
637
- if df is None:
638
- fig = plt.figure(figsize=(26,26))
639
- fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
640
- return fig
641
-
642
- df = df.clone()
643
-
644
-
645
- p.set(0.6, "Creating plot...")
646
-
647
-
648
- #plt.rcParams["figure.figsize"] = [10,10]
649
- fig = plt.figure(figsize=(26,26))
650
- plt.rcParams.update({'figure.autolayout': True})
651
- fig.set_facecolor('white')
652
- sns.set_theme(style="whitegrid", palette=colour_palette)
653
- print('this is the one plot')
654
-
655
- gs = gridspec.GridSpec(6, 8,
656
- height_ratios=[6,20,12,36,36,6],
657
- width_ratios=[4,18,18,18,18,18,18,4])
658
-
659
-
660
- gs.update(hspace=0.2, wspace=0.5)
661
-
662
- # Define the positions of each subplot in the grid
663
- ax_headshot = fig.add_subplot(gs[1,1:3])
664
- ax_bio = fig.add_subplot(gs[1,3:5])
665
- ax_logo = fig.add_subplot(gs[1,5:7])
666
-
667
- ax_season_table = fig.add_subplot(gs[2,1:7])
668
-
669
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
670
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
671
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
672
-
673
- ax_table = fig.add_subplot(gs[4,1:7])
674
-
675
- ax_footer = fig.add_subplot(gs[-1,1:7])
676
- ax_header = fig.add_subplot(gs[0,1:7])
677
- ax_left = fig.add_subplot(gs[:,0])
678
- ax_right = fig.add_subplot(gs[:,-1])
679
-
680
- # Hide axes for footer, header, left, and right
681
- ax_footer.axis('off')
682
- ax_header.axis('off')
683
- ax_left.axis('off')
684
- ax_right.axis('off')
685
-
686
- sns.set_theme(style="whitegrid", palette=colour_palette)
687
- fig.set_facecolor('white')
688
-
689
- df_teams = scrape.get_teams()
690
-
691
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
692
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
693
-
694
- if input.switch():
695
-
696
- # Get the logo URL from the image dictionary using the team abbreviation
697
- logo_url = input.logo_select()
698
-
699
- # Send a GET request to the logo URL
700
- response = requests.get(logo_url)
701
-
702
- # Open the image from the response content
703
- img = Image.open(BytesIO(response.content))
704
-
705
- # Display the image on the axis
706
- ax_logo.set_xlim(0, 1.3)
707
- ax_logo.set_ylim(0, 1)
708
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
709
-
710
- # Turn off the axis
711
- ax_logo.axis('off')
712
-
713
- else:
714
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
715
-
716
- stat_summary_table(df=df,
717
- ax=ax_season_table,
718
- player_input=player_input,
719
- split=input.split_id(),
720
- sport_id=sport_id,
721
- game_type=[input.type_input()])
722
-
723
- # break_plot(df=df_plot,ax=ax2)
724
- for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
725
- if x == 'velocity_kdes':
726
- velocity_kdes(df,
727
- ax=y,
728
- gs=gs,
729
- gs_x=[3,4],
730
- gs_y=[z,z+2],
731
- fig=fig)
732
- if x == 'tj_stuff_roling':
733
- tj_stuff_roling(df=df,
734
- window=int(input.rolling_window()),
735
- ax=y)
736
-
737
- if x == 'tj_stuff_roling_game':
738
- tj_stuff_roling_game(df=df,
739
- window=int(input.rolling_window()),
740
- ax=y)
741
-
742
- if x == 'break_plot':
743
- break_plot(df = df,ax=y)
744
-
745
- if x == 'location_plot_lhb':
746
- location_plot(df = df,ax=y,hand='L')
747
-
748
- if x == 'location_plot_rhb':
749
- location_plot(df = df,ax=y,hand='R')
750
-
751
- summary_table(df=df,
752
- ax=ax_table)
753
-
754
- plot_footer(ax_footer)
755
-
756
- ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
757
- # Hide axes ticks and labels
758
- ax_watermark.set_xticks([])
759
- ax_watermark.set_yticks([])
760
- ax_watermark.set_frame_on(False) # Optional: Hide border
761
-
762
- img = Image.open('tj stats circle-01_new.jpg')
763
- # Display the image
764
- ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
765
-
766
-
767
- ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
768
- ax_watermark2.set_xlim(0,1)
769
- ax_watermark2.set_ylim(0,1)
770
- # Hide axes ticks and labels
771
- ax_watermark2.set_xticks([])
772
- ax_watermark2.set_yticks([])
773
- ax_watermark2.set_frame_on(False) # Optional: Hide border
774
-
775
- # Open the image
776
- img = Image.open('tj stats circle-01_new.jpg')
777
- # Get the original size
778
- width, height = img.size
779
- # Calculate the new size (50% larger)
780
- new_width = int(width * 0.5)
781
- new_height = int(height * 0.5)
782
- # Resize the image
783
- img_resized = img.resize((new_width, new_height))
784
- # Display the image
785
- ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
786
-
787
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
788
-
789
-
790
-
791
- @output
792
- @render.data_frame
793
- @reactive.event(input.generate_plot, ignore_none=False)
794
- def grid():
795
-
796
- start_date = str(input.date_id()[0])
797
- if not is_valid_date(start_date):
798
- return pd.DataFrame({"Message": ["Select range to generate table"]})
799
- df = cached_data()
800
- df = df.clone()
801
- features_table = ['start_speed',
802
- 'spin_rate',
803
- 'extension',
804
- 'ivb',
805
- 'hb',
806
- 'x0',
807
- 'z0']
808
-
809
-
810
-
811
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
812
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
813
-
814
-
815
-
816
- return render.DataGrid(
817
- df.select(selection).to_pandas().round(1),
818
- row_selection_mode='multiple',
819
- height='700px',
820
- width='fit-content',
821
- filters=True,
822
- )
823
-
824
-
825
- @output
826
- @render.data_frame
827
- @reactive.event(input.generate_plot, ignore_none=False)
828
- def grid_game():
829
- if isinstance(input.date_id(), tuple):
830
- return pd.DataFrame({"Message": ["Select game to generate table"]})
831
-
832
- df = cached_data_daily()
833
- df = df.clone()
834
- features_table = ['start_speed',
835
- 'spin_rate',
836
- 'extension',
837
- 'ivb',
838
- 'hb',
839
- 'x0',
840
- 'z0']
841
-
842
-
843
-
844
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
845
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
846
-
847
-
848
-
849
- return render.DataGrid(
850
- df.select(selection).to_pandas().round(1),
851
- row_selection_mode='multiple',
852
- height='700px',
853
- width='fit-content',
854
- filters=True,
855
- )
856
-
857
-
858
  app = App(app_ui, server)
 
1
+ import polars as pl
2
+ import numpy as np
3
+ import pandas as pd
4
+ import api_scraper
5
+ scrape = api_scraper.MLB_Scrape()
6
+ from functions import df_update
7
+ from functions import pitch_summary_functions
8
+ update = df_update.df_update()
9
+ from stuff_model import feature_engineering as fe
10
+ from stuff_model import stuff_apply
11
+ import requests
12
+ import joblib
13
+ from matplotlib.gridspec import GridSpec
14
+ from shiny import App, reactive, ui, render
15
+ from shiny.ui import h2, tags
16
+ import matplotlib.pyplot as plt
17
+ import matplotlib.gridspec as gridspec
18
+ import seaborn as sns
19
+ from functions.pitch_summary_functions import *
20
+ from shiny import App, reactive, ui, render
21
+ from shiny.ui import h2, tags
22
+
23
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
24
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
25
+
26
+
27
+ year_list = [2017,2018,2019,2020,2021,2022,2023,2024,2025]
28
+
29
+
30
+
31
+ level_dict = {'1':'MLB',
32
+ '11':'AAA',
33
+ # '12':'AA',
34
+ #'13':'A+',
35
+ '14':'A',
36
+ '17':'AFL',
37
+ '22':'College',
38
+ '21':'Prospects',
39
+ '51':'International' }
40
+
41
+ function_dict={
42
+ 'velocity_kdes':'Velocity Distributions',
43
+ 'break_plot':'Pitch Movement',
44
+ 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
45
+ 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
46
+ 'location_plot_lhb':'Locations vs LHB',
47
+ 'location_plot_rhb':'Locations vs RHB',
48
+ }
49
+
50
+
51
+ split_dict = {'all':'All',
52
+ 'left':'LHH',
53
+ 'right':'RHH'}
54
+
55
+ split_dict_hand = {'all':['L','R'],
56
+ 'left':['L'],
57
+ 'right':['R']}
58
+
59
+
60
+ type_dict = {'R':'Regular Season',
61
+ 'S':'Spring',
62
+ 'P':'Playoffs' }
63
+
64
+
65
+
66
+ # List of MLB teams and their corresponding ESPN logo URLs
67
+ mlb_teams = [
68
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
69
+ {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
70
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
71
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
72
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
73
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
74
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
75
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
76
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
77
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
78
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
79
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
80
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
81
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
82
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
83
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
84
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
85
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
86
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
87
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
88
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
89
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
90
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
91
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
92
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
93
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
94
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
95
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
96
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
97
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
98
+ {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
99
+ ]
100
+
101
+
102
+ df_image = pd.DataFrame(mlb_teams)
103
+ image_dict = df_image.set_index('team')['logo_url'].to_dict()
104
+ image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
105
+
106
+
107
+
108
+ import requests
109
+
110
+ import os
111
+ CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
112
+ ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
113
+ BACKUP_PW = os.getenv("BACKUP_PW")
114
+ ADMIN_PW = os.getenv("ADMIN_PW")
115
+
116
+ url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
117
+
118
+ headers = {
119
+ "Authorization": f"Bearer {ACCESS_TOKEN}"
120
+ }
121
+
122
+ # Simple parameters, requesting the member's email and currently entitled tiers
123
+ params = {
124
+ "fields[member]": "full_name,email", # Request the member's email
125
+ "include": "currently_entitled_tiers", # Include the currently entitled tiers
126
+ "page[size]": 1000 # Fetch up to 1000 patrons per request
127
+ }
128
+
129
+ response = requests.get(url, headers=headers, params=params)
130
+
131
+
132
+ VALID_PASSWORDS = []
133
+ if response.status_code == 200:
134
+ data = response.json()
135
+ for patron in data['data']:
136
+ try:
137
+ tiers = patron['relationships']['currently_entitled_tiers']['data']
138
+ if any(tier['id'] == '9078921' for tier in tiers):
139
+ full_name = patron['attributes']['email']
140
+ VALID_PASSWORDS.append(full_name)
141
+ except KeyError:
142
+ continue
143
+ VALID_PASSWORDS.append(BACKUP_PW)
144
+ VALID_PASSWORDS.append(ADMIN_PW)
145
+ VALID_PASSWORDS.append('')
146
+
147
+ from shiny import App, reactive, ui, render
148
+ from shiny.ui import h2, tags
149
+
150
+ from datetime import datetime
151
+
152
+ def is_valid_date(date_str):
153
+ try:
154
+ datetime.strptime(date_str, "%Y-%m-%d") # Attempt to parse the date
155
+ return True
156
+ except ValueError:
157
+ return False # If parsing fails, it's not in the correct format
158
+
159
+ # Define the login UI
160
+ login_ui = ui.page_fluid(
161
+ ui.card(
162
+ ui.h2([
163
+ "TJStats Pitching Summary App ",
164
+ ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
165
+ ]),
166
+ ui.p(
167
+ "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
168
+ ui.tags.a("Patreon post", href="https://www.patreon.com/posts/116064432", target="_blank"),
169
+ "."
170
+ ),
171
+ ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="50%"),
172
+ ui.tags.input(
173
+ type="checkbox",
174
+ id="authenticated",
175
+ value=False,
176
+ disabled=True
177
+ ),
178
+ ui.input_action_button("login", "Login", class_="btn-primary"),
179
+ ui.output_text("login_message"),
180
+ )
181
+ )
182
+
183
+
184
+ # Define the UI layout for the app
185
+ main_ui = ui.page_sidebar(
186
+ ui.sidebar(
187
+ # Row for selecting season and level
188
+ ui.row(
189
+ ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2025)),
190
+ ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
191
+ ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
192
+ ),
193
+ # Row for the action button to get player list
194
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
195
+ # Row for selecting the player
196
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
197
+ # Row for selecting the date range
198
+ ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
199
+
200
+ # Rows for selecting plots and split options
201
+ ui.row(
202
+ ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
203
+ ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
204
+ ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
205
+ ),
206
+ ui.row(
207
+ ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
208
+ ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
209
+ ),
210
+ ui.row(
211
+ ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
212
+ ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
213
+ ),
214
+
215
+ # Row for the action button to generate plot
216
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
217
+ width="400px" # Added this parameter to control sidebar width
218
+ ),
219
+
220
+ # Main content area with tabs (placed directly in page_sidebar)
221
+ ui.navset_tab(
222
+ ui.nav_panel("Pitching Summary",
223
+ ui.output_text("status"),
224
+ ui.output_plot('plot', width='2100px', height='2100px')
225
+ ),
226
+ ui.nav_panel("Game Summary",
227
+ ui.output_text("status2"),
228
+ ui.output_plot('game_plot', width='2100px', height='2100px')
229
+ ),
230
+ ui.nav_panel("Table Range",
231
+ ui.output_data_frame("grid")),
232
+ ui.nav_panel("Table Game",
233
+ ui.output_data_frame("grid_game")),
234
+ id="tabset"
235
+ )
236
+ )
237
+
238
+ # Combined UI with conditional panel
239
+ app_ui = ui.page_fluid(
240
+ ui.tags.head(
241
+ ui.tags.script(src="script.js")
242
+ ),
243
+
244
+ ui.panel_conditional(
245
+ "!input.authenticated",
246
+ login_ui
247
+ ),
248
+ ui.panel_conditional(
249
+ "input.authenticated",
250
+ main_ui
251
+ )
252
+ )
253
+
254
+
255
+ def server(input, output, session):
256
+
257
+ @reactive.Effect
258
+ @reactive.event(input.login)
259
+ def check_password():
260
+ if input.password() in VALID_PASSWORDS:
261
+ ui.update_checkbox("authenticated", value=True)
262
+ ui.update_text("login_message", value="")
263
+ else:
264
+ ui.update_text("login_message", value="Invalid password!")
265
+ ui.update_text("password", value="")
266
+
267
+ @output
268
+ @render.text
269
+ def login_message():
270
+ return ""
271
+
272
+ @reactive.calc
273
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id)
274
+ def cached_data():
275
+
276
+ year_input = int(input.year_input())
277
+ sport_id = int(input.level_input())
278
+ player_input = int(input.pitcher_id())
279
+
280
+
281
+ start_date = str(input.date_id()[0])
282
+ end_date = str(input.date_id()[1])
283
+ game_list = scrape.get_player_games_list(sport_id = sport_id,
284
+ season = year_input,
285
+ player_id = player_input,
286
+ start_date = start_date,
287
+ end_date = end_date,
288
+ game_type = [input.type_input()])
289
+
290
+ # if input.tabset() == 'Game Summary':
291
+ # print(year_input, sport_id, player_input, 'yup')
292
+ # print(input.date_id())
293
+ # game_list = [input.date_id()]
294
+
295
+
296
+ data_list = scrape.get_data(game_list_input = game_list[:])
297
+
298
+ try:
299
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
300
+ (pl.col("pitcher_id") == player_input)&
301
+ (pl.col("is_pitch") == True)&
302
+ (pl.col("start_speed") >= 50)&
303
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
304
+
305
+ )))).with_columns(
306
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
307
+ ))
308
+ return df
309
+
310
+
311
+ except TypeError:
312
+ print("NONE")
313
+ return None
314
+
315
+ @reactive.calc
316
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id,input.tabset)
317
+ def cached_data_daily():
318
+
319
+ year_input = int(input.year_input())
320
+ sport_id = int(input.level_input())
321
+ player_input = int(input.pitcher_id())
322
+
323
+
324
+ # start_date = str(input.date_id()[0])
325
+ # end_date = str(input.date_id()[1])
326
+ game_list = [int(input.date_id())]
327
+ print(game_list)
328
+
329
+ # if input.tabset() == 'Game Summary':
330
+ # print(year_input, sport_id, player_input, 'yup')
331
+ # print(input.date_id())
332
+ # game_list =
333
+
334
+
335
+ data_list = scrape.get_data(game_list_input = game_list[:])
336
+
337
+ try:
338
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
339
+ (pl.col("pitcher_id") == player_input)&
340
+ (pl.col("is_pitch") == True)&
341
+ (pl.col("start_speed") >= 50)&
342
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
343
+
344
+ )))).with_columns(
345
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
346
+ ))
347
+ return df
348
+
349
+
350
+ except TypeError:
351
+ print("NONE")
352
+ return None
353
+
354
+
355
+ @render.ui
356
+ @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
357
+ def player_select_ui():
358
+ # Get the list of pitchers for the selected level and season
359
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
360
+ (pl.col("position").is_in(['P','TWP']))|
361
+ (pl.col("player_id").is_in([686846]))
362
+
363
+ ).sort("name")
364
+
365
+ # Create a dictionary of pitcher IDs and names
366
+ pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
367
+
368
+
369
+
370
+
371
+ # Return a select input for choosing a pitcher
372
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
373
+
374
+ @render.ui
375
+ @reactive.event(input.player_button, input.pitcher_id,input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
376
+ def date_id():
377
+ if input.tabset() == 'Pitching Summary' or input.tabset() == 'Table Range':
378
+ # Create a date range input for selecting the date range within the selected year
379
+ return ui.input_date_range("date_id", "Select Date Range",
380
+ start=f"{int(input.year_input())}-01-01",
381
+ end=f"{int(input.year_input())}-12-31",
382
+ min=f"{int(input.year_input())}-01-01",
383
+ max=f"{int(input.year_input())}-12-31")
384
+
385
+ if input.tabset() == 'Game Summary' or input.tabset() == 'Table Game':
386
+ year_input = int(input.year_input())
387
+ sport_id = int(input.level_input())
388
+ player_input = int(input.pitcher_id())
389
+ print('game summary')
390
+ # start_date = str(input.date_id()[0])
391
+ # end_date = str(input.date_id()[1])
392
+
393
+ game_list = scrape.get_player_games_list(player_id = player_input,
394
+ season = year_input,
395
+ sport_id=sport_id,
396
+ game_type=[input.type_input()],
397
+ pitching = True)
398
+
399
+ schedule_df = scrape.get_schedule(year_input=[year_input],
400
+ sport_id= [sport_id],
401
+ game_type = [input.type_input()])
402
+
403
+ player_schedule_df = schedule_df.filter(pl.col('game_id').is_in(game_list)).to_pandas().sort_values('date')
404
+
405
+ player_schedule_df['def'] = player_schedule_df['date'].astype(str) + ' - ' + player_schedule_df['away'] + ' @ ' + player_schedule_df['home'] + ' '
406
+
407
+
408
+ game_dict = dict(zip(player_schedule_df['game_id'], player_schedule_df['def']))
409
+ # print(game_dict)
410
+
411
+ return ui.input_select("date_id", "Select Game", game_dict)
412
+
413
+ @output
414
+ @render.text
415
+ def status():
416
+ # Only show status when generating
417
+ if input.generate == 0:
418
+ return ""
419
+ return ""
420
+
421
+
422
+
423
+
424
+ @output
425
+ @render.plot
426
+ @reactive.event(input.generate_plot, ignore_none=False)
427
+ def plot():
428
+ # Show progress/loading notification
429
+
430
+
431
+
432
+ with ui.Progress(min=0, max=1) as p:
433
+ p.set(message="Generating plot", detail="This may take a while...")
434
+
435
+
436
+ p.set(0.3, "Gathering data...")
437
+ year_input = int(input.year_input())
438
+ sport_id = int(input.level_input())
439
+ player_input = int(input.pitcher_id())
440
+ start_date = str(input.date_id()[0])
441
+ end_date = str(input.date_id()[1])
442
+ if not is_valid_date(start_date):
443
+ fig = plt.figure(figsize=(26,26))
444
+ fig.text(x=0.1,y=0.9,s='Select Date Range and Generate Plot',fontsize=36,ha='left')
445
+ return fig
446
+ print(year_input, sport_id, player_input, start_date, end_date)
447
+
448
+ df = cached_data()
449
+
450
+ if df is None:
451
+ fig = plt.figure(figsize=(26,26))
452
+ fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
453
+ return fig
454
+
455
+ df = df.clone()
456
+
457
+
458
+ p.set(0.6, "Creating plot...")
459
+
460
+
461
+ #plt.rcParams["figure.figsize"] = [10,10]
462
+ fig = plt.figure(figsize=(26,26))
463
+ plt.rcParams.update({'figure.autolayout': True})
464
+ fig.set_facecolor('white')
465
+ sns.set_theme(style="whitegrid", palette=colour_palette)
466
+ print('this is the one plot')
467
+
468
+ gs = gridspec.GridSpec(6, 8,
469
+ height_ratios=[6,20,12,36,36,6],
470
+ width_ratios=[4,18,18,18,18,18,18,4])
471
+
472
+
473
+ gs.update(hspace=0.2, wspace=0.5)
474
+
475
+ # Define the positions of each subplot in the grid
476
+ ax_headshot = fig.add_subplot(gs[1,1:3])
477
+ ax_bio = fig.add_subplot(gs[1,3:5])
478
+ ax_logo = fig.add_subplot(gs[1,5:7])
479
+
480
+ ax_season_table = fig.add_subplot(gs[2,1:7])
481
+
482
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
483
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
484
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
485
+
486
+ ax_table = fig.add_subplot(gs[4,1:7])
487
+
488
+ ax_footer = fig.add_subplot(gs[-1,1:7])
489
+ ax_header = fig.add_subplot(gs[0,1:7])
490
+ ax_left = fig.add_subplot(gs[:,0])
491
+ ax_right = fig.add_subplot(gs[:,-1])
492
+
493
+ # Hide axes for footer, header, left, and right
494
+ ax_footer.axis('off')
495
+ ax_header.axis('off')
496
+ ax_left.axis('off')
497
+ ax_right.axis('off')
498
+
499
+ sns.set_theme(style="whitegrid", palette=colour_palette)
500
+ fig.set_facecolor('white')
501
+
502
+ df_teams = scrape.get_teams()
503
+
504
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
505
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
506
+
507
+ if input.switch():
508
+
509
+ # Get the logo URL from the image dictionary using the team abbreviation
510
+ logo_url = input.logo_select()
511
+
512
+ # Send a GET request to the logo URL
513
+ response = requests.get(logo_url)
514
+
515
+ # Open the image from the response content
516
+ img = Image.open(BytesIO(response.content))
517
+
518
+ # Display the image on the axis
519
+ ax_logo.set_xlim(0, 1.3)
520
+ ax_logo.set_ylim(0, 1)
521
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
522
+
523
+ # Turn off the axis
524
+ ax_logo.axis('off')
525
+
526
+ else:
527
+ plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
528
+
529
+ stat_summary_table(df=df,
530
+ ax=ax_season_table,
531
+ player_input=player_input,
532
+ split=input.split_id(),
533
+ sport_id=sport_id,
534
+ game_type=[input.type_input()])
535
+
536
+ # break_plot(df=df_plot,ax=ax2)
537
+ for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
538
+ if x == 'velocity_kdes':
539
+ velocity_kdes(df,
540
+ ax=y,
541
+ gs=gs,
542
+ gs_x=[3,4],
543
+ gs_y=[z,z+2],
544
+ fig=fig)
545
+ if x == 'tj_stuff_roling':
546
+ tj_stuff_roling(df=df,
547
+ window=int(input.rolling_window()),
548
+ ax=y)
549
+
550
+ if x == 'tj_stuff_roling_game':
551
+ tj_stuff_roling_game(df=df,
552
+ window=int(input.rolling_window()),
553
+ ax=y)
554
+
555
+ if x == 'break_plot':
556
+ break_plot(df = df,ax=y)
557
+
558
+ if x == 'location_plot_lhb':
559
+ location_plot(df = df,ax=y,hand='L')
560
+
561
+ if x == 'location_plot_rhb':
562
+ location_plot(df = df,ax=y,hand='R')
563
+
564
+ summary_table(df=df,
565
+ ax=ax_table)
566
+
567
+ plot_footer(ax_footer)
568
+
569
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
570
+ # Hide axes ticks and labels
571
+ ax_watermark.set_xticks([])
572
+ ax_watermark.set_yticks([])
573
+ ax_watermark.set_frame_on(False) # Optional: Hide border
574
+
575
+ img = Image.open('tj stats circle-01_new.jpg')
576
+ # Display the image
577
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
578
+
579
+
580
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
581
+ ax_watermark2.set_xlim(0,1)
582
+ ax_watermark2.set_ylim(0,1)
583
+ # Hide axes ticks and labels
584
+ ax_watermark2.set_xticks([])
585
+ ax_watermark2.set_yticks([])
586
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
587
+
588
+ # Open the image
589
+ img = Image.open('tj stats circle-01_new.jpg')
590
+ # Get the original size
591
+ width, height = img.size
592
+ # Calculate the new size (50% larger)
593
+ new_width = int(width * 0.5)
594
+ new_height = int(height * 0.5)
595
+ # Resize the image
596
+ img_resized = img.resize((new_width, new_height))
597
+ # Display the image
598
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
599
+
600
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
601
+
602
+
603
+
604
+
605
+ @output
606
+ @render.plot
607
+ @reactive.event(input.generate_plot, ignore_none=False)
608
+ def game_plot():
609
+ # Show progress/loading notification
610
+ with ui.Progress(min=0, max=1) as p:
611
+ print(input.date_id(),'TEST')
612
+
613
+ if isinstance(input.date_id(), tuple):
614
+ fig = plt.figure(figsize=(26,26))
615
+ fig.text(x=0.1,y=0.9,s='Select Game and Generate Plot',fontsize=36,ha='left')
616
+ return fig
617
+
618
+
619
+ p.set(message="Generating plot", detail="This may take a while...")
620
+
621
+
622
+ p.set(0.3, "Gathering data...")
623
+ year_input = int(input.year_input())
624
+ sport_id = int(input.level_input())
625
+ player_input = int(input.pitcher_id())
626
+
627
+ # print(input.game_id())
628
+ # print(year_input, sport_id, player_input)
629
+
630
+ # print(year_input, sport_id, player_input, start_date, end_date)
631
+
632
+ df = cached_data_daily()
633
+
634
+ # start_date = str(df['game_date'][0])
635
+ # end_date = str(df['game_date'][0])
636
+
637
+ if df is None:
638
+ fig = plt.figure(figsize=(26,26))
639
+ fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
640
+ return fig
641
+
642
+ df = df.clone()
643
+
644
+
645
+ p.set(0.6, "Creating plot...")
646
+
647
+
648
+ #plt.rcParams["figure.figsize"] = [10,10]
649
+ fig = plt.figure(figsize=(26,26))
650
+ plt.rcParams.update({'figure.autolayout': True})
651
+ fig.set_facecolor('white')
652
+ sns.set_theme(style="whitegrid", palette=colour_palette)
653
+ print('this is the one plot')
654
+
655
+ gs = gridspec.GridSpec(6, 8,
656
+ height_ratios=[6,20,12,36,36,6],
657
+ width_ratios=[4,18,18,18,18,18,18,4])
658
+
659
+
660
+ gs.update(hspace=0.2, wspace=0.5)
661
+
662
+ # Define the positions of each subplot in the grid
663
+ ax_headshot = fig.add_subplot(gs[1,1:3])
664
+ ax_bio = fig.add_subplot(gs[1,3:5])
665
+ ax_logo = fig.add_subplot(gs[1,5:7])
666
+
667
+ ax_season_table = fig.add_subplot(gs[2,1:7])
668
+
669
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
670
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
671
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
672
+
673
+ ax_table = fig.add_subplot(gs[4,1:7])
674
+
675
+ ax_footer = fig.add_subplot(gs[-1,1:7])
676
+ ax_header = fig.add_subplot(gs[0,1:7])
677
+ ax_left = fig.add_subplot(gs[:,0])
678
+ ax_right = fig.add_subplot(gs[:,-1])
679
+
680
+ # Hide axes for footer, header, left, and right
681
+ ax_footer.axis('off')
682
+ ax_header.axis('off')
683
+ ax_left.axis('off')
684
+ ax_right.axis('off')
685
+
686
+ sns.set_theme(style="whitegrid", palette=colour_palette)
687
+ fig.set_facecolor('white')
688
+
689
+ df_teams = scrape.get_teams()
690
+
691
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
692
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
693
+
694
+ if input.switch():
695
+
696
+ # Get the logo URL from the image dictionary using the team abbreviation
697
+ logo_url = input.logo_select()
698
+
699
+ # Send a GET request to the logo URL
700
+ response = requests.get(logo_url)
701
+
702
+ # Open the image from the response content
703
+ img = Image.open(BytesIO(response.content))
704
+
705
+ # Display the image on the axis
706
+ ax_logo.set_xlim(0, 1.3)
707
+ ax_logo.set_ylim(0, 1)
708
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
709
+
710
+ # Turn off the axis
711
+ ax_logo.axis('off')
712
+
713
+ else:
714
+ plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
715
+
716
+ stat_summary_table(df=df,
717
+ ax=ax_season_table,
718
+ player_input=player_input,
719
+ split=input.split_id(),
720
+ sport_id=sport_id,
721
+ game_type=[input.type_input()])
722
+
723
+ # break_plot(df=df_plot,ax=ax2)
724
+ for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
725
+ if x == 'velocity_kdes':
726
+ velocity_kdes(df,
727
+ ax=y,
728
+ gs=gs,
729
+ gs_x=[3,4],
730
+ gs_y=[z,z+2],
731
+ fig=fig)
732
+ if x == 'tj_stuff_roling':
733
+ tj_stuff_roling(df=df,
734
+ window=int(input.rolling_window()),
735
+ ax=y)
736
+
737
+ if x == 'tj_stuff_roling_game':
738
+ tj_stuff_roling_game(df=df,
739
+ window=int(input.rolling_window()),
740
+ ax=y)
741
+
742
+ if x == 'break_plot':
743
+ break_plot(df = df,ax=y)
744
+
745
+ if x == 'location_plot_lhb':
746
+ location_plot(df = df,ax=y,hand='L')
747
+
748
+ if x == 'location_plot_rhb':
749
+ location_plot(df = df,ax=y,hand='R')
750
+
751
+ summary_table(df=df,
752
+ ax=ax_table)
753
+
754
+ plot_footer(ax_footer)
755
+
756
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
757
+ # Hide axes ticks and labels
758
+ ax_watermark.set_xticks([])
759
+ ax_watermark.set_yticks([])
760
+ ax_watermark.set_frame_on(False) # Optional: Hide border
761
+
762
+ img = Image.open('tj stats circle-01_new.jpg')
763
+ # Display the image
764
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
765
+
766
+
767
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
768
+ ax_watermark2.set_xlim(0,1)
769
+ ax_watermark2.set_ylim(0,1)
770
+ # Hide axes ticks and labels
771
+ ax_watermark2.set_xticks([])
772
+ ax_watermark2.set_yticks([])
773
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
774
+
775
+ # Open the image
776
+ img = Image.open('tj stats circle-01_new.jpg')
777
+ # Get the original size
778
+ width, height = img.size
779
+ # Calculate the new size (50% larger)
780
+ new_width = int(width * 0.5)
781
+ new_height = int(height * 0.5)
782
+ # Resize the image
783
+ img_resized = img.resize((new_width, new_height))
784
+ # Display the image
785
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
786
+
787
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
788
+
789
+
790
+
791
+ @output
792
+ @render.data_frame
793
+ @reactive.event(input.generate_plot, ignore_none=False)
794
+ def grid():
795
+
796
+ start_date = str(input.date_id()[0])
797
+ if not is_valid_date(start_date):
798
+ return pd.DataFrame({"Message": ["Select range to generate table"]})
799
+ df = cached_data()
800
+ df = df.clone()
801
+ features_table = ['start_speed',
802
+ 'spin_rate',
803
+ 'extension',
804
+ 'ivb',
805
+ 'hb',
806
+ 'x0',
807
+ 'z0']
808
+
809
+
810
+
811
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
812
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
813
+
814
+
815
+
816
+ return render.DataGrid(
817
+ df.select(selection).to_pandas().round(1),
818
+ row_selection_mode='multiple',
819
+ height='700px',
820
+ width='fit-content',
821
+ filters=True,
822
+ )
823
+
824
+
825
+ @output
826
+ @render.data_frame
827
+ @reactive.event(input.generate_plot, ignore_none=False)
828
+ def grid_game():
829
+ if isinstance(input.date_id(), tuple):
830
+ return pd.DataFrame({"Message": ["Select game to generate table"]})
831
+
832
+ df = cached_data_daily()
833
+ df = df.clone()
834
+ features_table = ['start_speed',
835
+ 'spin_rate',
836
+ 'extension',
837
+ 'ivb',
838
+ 'hb',
839
+ 'x0',
840
+ 'z0']
841
+
842
+
843
+
844
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
845
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
846
+
847
+
848
+
849
+ return render.DataGrid(
850
+ df.select(selection).to_pandas().round(1),
851
+ row_selection_mode='multiple',
852
+ height='700px',
853
+ width='fit-content',
854
+ filters=True,
855
+ )
856
+
857
+
858
  app = App(app_ui, server)