nesticot commited on
Commit
44bda9d
·
verified ·
1 Parent(s): b02077b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +640 -640
app.py CHANGED
@@ -1,641 +1,641 @@
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
- # from functions.PitchPlotFunctions import *
23
- import functions.PitchPlotFunctions as ppf
24
- ploter = ppf.PitchPlotFunctions()
25
- from shiny.plotutils import brushed_points
26
- from pytabulator import TableOptions, Tabulator, output_tabulator, render_tabulator, theme
27
- theme.tabulator_site()
28
-
29
- colour_palette = ['#FFB000','#648FFF','#785EF0',
30
- '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
31
- cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000'])
32
-
33
- year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
34
-
35
-
36
-
37
- level_dict = {'1':'MLB',
38
- '11':'AAA',
39
- '12':'AA',
40
- '13':'A+',
41
- '14':'A',
42
- '17':'AFL',
43
- '22':'College',
44
- '21':'Prospects',
45
- '51':'International' }
46
-
47
- function_dict={
48
- 'velocity_kdes':'Velocity Distributions',
49
- 'break_plot':'Pitch Movement',
50
- 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
51
- 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
52
- 'location_plot_lhb':'Locations vs LHB',
53
- 'location_plot_rhb':'Locations vs RHB',
54
- }
55
-
56
-
57
- split_dict = {'all':'All',
58
- 'left':'LHH',
59
- 'right':'RHH'}
60
-
61
- split_dict_hand = {'all':['L','R'],
62
- 'left':['L'],
63
- 'right':['R']}
64
-
65
- ### PITCH COLOURS ###
66
-
67
- # Dictionary to map pitch types to their corresponding colors and names
68
- pitch_colours = {
69
- ## Fastballs ##
70
- 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
71
- 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
72
- 'SI': {'colour': '#98165D', 'name': 'Sinker'},
73
- 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
74
-
75
- ## Offspeed ##
76
- 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
77
- 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
78
- 'SC': {'colour': '#F08223', 'name': 'Screwball'},
79
- 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
80
-
81
- ## Sliders ##
82
- 'SL': {'colour': '#67E18D', 'name': 'Slider'},
83
- 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
84
- 'SV': {'colour': '#376748', 'name': 'Slurve'},
85
-
86
- ## Curveballs ##
87
- 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
88
- 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
89
- 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
90
- 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
91
-
92
- ## Others ##
93
- 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
94
- 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'},
95
- 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
96
- 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
97
- }
98
-
99
- # Create dictionaries for pitch types and their attributes
100
- dict_colour = {key: value['colour'] for key, value in pitch_colours.items()}
101
- dict_pitch = {key: value['name'] for key, value in pitch_colours.items()}
102
- dict_pitch_desc_type = {value['name']: key for key, value in pitch_colours.items()}
103
- dict_pitch_desc_type.update({'Four-Seam Fastball':'FF'})
104
- dict_pitch_desc_type.update({'All':'All'})
105
- dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()}
106
- dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'})
107
- dict_pitch_name.update({'4-Seam':'#FF007D'})
108
-
109
-
110
- from shiny import App, reactive, ui, render
111
- from shiny.ui import h2, tags
112
-
113
- # Define the UI layout for the app
114
- app_ui = ui.page_fluid(
115
- ui.layout_sidebar(
116
- ui.panel_sidebar(
117
- # Row for selecting season and level
118
- ui.row(
119
- ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
120
- ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
121
- ),
122
- # Row for the action button to get player list
123
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
124
- # Row for selecting the player
125
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
126
- # Row for selecting the date range
127
- ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
128
-
129
- ui.row(
130
- ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
131
- ),
132
- # Row for the action button to generate plot
133
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
134
- ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")),
135
-
136
- ),
137
-
138
-
139
- ui.panel_main(
140
- # ui.navset_tab(
141
- # Tab for game summary plot
142
- # ui.nav(
143
- # "Pitching Summary",
144
- ui.card(
145
- {"style": "width: 870px;"},
146
- ui.head_content(
147
- ui.tags.script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"),
148
- ui.tags.script("""
149
- async function downloadSVG() {
150
- const content = document.getElementById('capture-section');
151
-
152
- // Create a new SVG element
153
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
154
- const bbox = content.getBoundingClientRect();
155
-
156
- // Set SVG attributes
157
- svg.setAttribute('width', bbox.width);
158
- svg.setAttribute('height', bbox.height);
159
- svg.setAttribute('viewBox', `0 0 ${bbox.width} ${bbox.height}`);
160
-
161
- // Create foreignObject to contain HTML content
162
- const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
163
- foreignObject.setAttribute('width', '100%');
164
- foreignObject.setAttribute('height', '100%');
165
- foreignObject.setAttribute('x', '0');
166
- foreignObject.setAttribute('y', '0');
167
-
168
- // Clone the content and its styles
169
- const clonedContent = content.cloneNode(true);
170
-
171
- // Add necessary style context
172
- const style = document.createElement('style');
173
- Array.from(document.styleSheets).forEach(sheet => {
174
- try {
175
- Array.from(sheet.cssRules).forEach(rule => {
176
- style.innerHTML += rule.cssText + '\\n';
177
- });
178
- } catch (e) {
179
- console.warn('Could not access stylesheet rules');
180
- }
181
- });
182
-
183
- // Create a wrapper div to hold styles and content
184
- const wrapper = document.createElement('div');
185
- wrapper.appendChild(style);
186
- wrapper.appendChild(clonedContent);
187
-
188
- foreignObject.appendChild(wrapper);
189
- svg.appendChild(foreignObject);
190
-
191
- // Convert to SVG string with XML declaration and DTD
192
- const svgString = new XMLSerializer().serializeToString(svg);
193
- const svgBlob = new Blob([
194
- '<?xml version="1.0" standalone="no"?>\\n',
195
- '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\\n',
196
- svgString
197
- ], {type: 'image/svg+xml;charset=utf-8'});
198
-
199
- // Create and trigger download
200
- const url = URL.createObjectURL(svgBlob);
201
- const link = document.createElement('a');
202
- link.href = url;
203
- link.download = 'plot_and_table.svg';
204
- document.body.appendChild(link);
205
- link.click();
206
- document.body.removeChild(link);
207
- URL.revokeObjectURL(url);
208
- }
209
-
210
- $(document).on('click', '#capture_btn', function() {
211
- downloadSVG();
212
- });
213
- """)
214
- ),
215
- ui.output_text("status"),
216
- ui.div(
217
- {
218
- "id": "capture-section",
219
- "style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;" # Added margin-right
220
- },
221
- # Plot section with relative positioning for brush
222
- ui.div(
223
- {"style": "position: relative;"},
224
- ui.output_ui("plot_ui")
225
- ),
226
- # Table section
227
- ui.div(
228
- {"style": "margin-top: 20px;"},
229
- ui.row(ui.tags.b("Pitches in Selection"), ui.output_table("in_brush")),
230
-
231
-
232
- ),
233
- ui.div({"style": "height: 20px;"})
234
- ),
235
- ui.input_action_button("capture_btn", "Save as SVG", class_="btn-primary"),
236
- )
237
- # ),
238
- # )
239
- )
240
- )
241
- )
242
-
243
-
244
- def server(input, output, session):
245
-
246
- @reactive.calc
247
- @reactive.event(input.pitcher_id, input.date_id,input.split_id)
248
- def cached_data():
249
-
250
- year_input = int(input.year_input())
251
- sport_id = int(input.level_input())
252
- player_input = int(input.pitcher_id())
253
- start_date = str(input.date_id()[0])
254
- end_date = str(input.date_id()[1])
255
- # Simulate an expensive data operation
256
- game_list = scrape.get_player_games_list(sport_id = sport_id,
257
- season = year_input,
258
- player_id = player_input,
259
- start_date = start_date,
260
- end_date = end_date)
261
-
262
- data_list = scrape.get_data(game_list_input = game_list[:])
263
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
264
- (pl.col("pitcher_id") == player_input)&
265
- (pl.col("is_pitch") == True)&
266
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
267
-
268
- )))).with_columns(
269
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
270
- ))
271
-
272
- df = df.with_columns(
273
- prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
274
- prop=pl.col('is_pitch').sum().over("pitch_type")
275
- )
276
-
277
- return df
278
-
279
- @render.ui
280
- @reactive.event(input.player_button, input.level_input,input.year_input, ignore_none=False)
281
- def player_select_ui():
282
- # Get the list of pitchers for the selected level and season
283
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
284
- pl.col("position").is_in(['P'])).sort("name")
285
-
286
- # Create a dictionary of pitcher IDs and names
287
- pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
288
-
289
- # Return a select input for choosing a pitcher
290
- return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
291
-
292
- @render.ui
293
- @reactive.event(input.player_button,input.pitcher_id,input.year_input, ignore_none=False)
294
- def date_id():
295
- # Create a date range input for selecting the date range within the selected year
296
- return ui.input_date_range("date_id", "Select Date Range",
297
- start=f"{int(input.year_input())}-01-01",
298
- end=f"{int(input.year_input())}-03-31",
299
- min=f"{int(input.year_input())}-01-01",
300
- max=f"{int(input.year_input())}-12-31")
301
- @output
302
- @render.text
303
- def status():
304
- # Only show status when generating
305
- if input.generate == 0:
306
- return ""
307
- return ""
308
-
309
- @render.ui
310
- @reactive.event(input.generate_plot)
311
- def plot_ui():
312
- brush_opts_kwargs = {}
313
- brush_opts_kwargs["direction"] = 'xy'
314
- brush_opts_kwargs["delay"] = 60 # Optional: adds a small delay for better performance
315
- brush_opts_kwargs["delay_type"] = "throttle"
316
-
317
-
318
- return ui.output_plot('plot',
319
- width='800px',
320
- height='800px',
321
- brush=ui.brush_opts(**brush_opts_kwargs))
322
-
323
- @render.table
324
- @reactive.event(input.plot_brush, input.generate_table) # Note: changed to match the brush ID
325
- def in_brush():
326
- # if input.plot_brush() is None: # Note: changed to match the brush ID
327
- # return None
328
- brushed_df = pl.DataFrame(brushed_points(
329
- cached_data().to_pandas(),
330
- input.plot_brush(),
331
- xvar="hb",
332
- yvar="ivb",
333
- all_rows=False
334
- ))
335
-
336
-
337
- brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description'])
338
- .agg([
339
- pl.col('is_pitch').drop_nans().count().alias('pitches'),
340
- pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
341
- pl.col('vb').drop_nans().mean().round(1).alias('vb'),
342
- pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
343
- pl.col('hb').drop_nans().mean().round(1).alias('hb'),
344
- pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
345
- pl.col('x0').drop_nans().mean().round(1).alias('x0'),
346
- pl.col('z0').drop_nans().mean().round(1).alias('z0'),
347
- pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
348
- ])
349
- .with_columns(
350
- (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id'))
351
- # .round(1)
352
- # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
353
- .alias('proportion')
354
- )
355
- )).sort('proportion', descending=True).
356
- select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
357
- "spin_rate", "x0", "z0",'tj_stuff_plus'])
358
- .with_columns(
359
- pl.when(pl.col("pitch_description") == "Four-Seam Fastball")
360
- .then(pl.lit("4-Seam"))
361
- .otherwise(pl.col("pitch_description"))
362
- .alias("pitch_description")
363
- )
364
- .rename({
365
- 'pitch_description': 'Pitch Type',
366
- 'pitches': 'Pitches',
367
- 'proportion': 'Prop',
368
- 'start_speed': 'Velo',
369
- 'ivb': 'iVB',
370
- 'hb': 'HB',
371
- 'spin_rate': 'Spin',
372
- 'x0': 'hRel',
373
- 'z0': 'vRel',
374
- 'tj_stuff_plus': 'tjStuff+'
375
- }))
376
-
377
- # brushed_df_final = brushed_df_final
378
-
379
- # print(brushed_df_final)
380
-
381
- def change_font(val):
382
- if val == "Cutter":
383
- return "color: red; font-weight: bold;"
384
- else:
385
- ''
386
- return "font-weight: bold;"
387
- df_brush_style = (brushed_df_final.to_pandas().style.set_precision(1)
388
-
389
- .set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{
390
- 'selector': 'caption',
391
- 'props': [
392
- ('color', ''),
393
- ('fontname', 'Century Gothic'),
394
- ('font-size', '16px'),
395
- ('font-style', 'italic'),
396
- ('font-weight', ''),
397
- ('text-align', 'centre'),
398
- ]
399
-
400
- },{'selector' :'th', 'props':[('font-size', '16px'),('text-align', 'center'),('Height','px'),('color','black'),('border', '1px black solid !important')]},{'selector' :'td', 'props':[('text-align', 'center'),('font-size', '16px'),('color','black')]}],overwrite=False)
401
- .set_properties(**{'background-color':'White','index':'White','min-width':'72px'},overwrite=False)
402
- .set_table_styles([{'selector': 'th:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
403
- .set_table_styles([{'selector': 'tr:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
404
- .set_table_styles([{'selector': 'tr', 'props': [('line-height', '20px')]}],overwrite=False)
405
- .set_properties(**{'Height': '8px'},**{'text-align': 'center'},overwrite=False)
406
- .hide_index()
407
- .set_properties(**{'border': '1px black solid !important'})
408
- .format('{:.0%}',subset=(brushed_df_final.columns[2]))
409
- .format('{:.0f}',subset=(brushed_df_final.columns[6]))
410
- .format('{:.0f}',subset=(brushed_df_final.columns[-1]))
411
- .set_properties(subset=brushed_df_final.columns, **{'height': '30px'})
412
- .set_table_styles([{'selector': 'thead th', 'props': [('height', '30px')]}], overwrite=False)
413
- # .set_table_styles([{'selector': 'table', 'props': [('width', '100px')]}], overwrite=False)
414
- .set_table_styles([{'selector': 'thead th:nth-child(1)', 'props': [('min-width', '125px')]}], overwrite=False)
415
- .set_table_styles([{'selector': 'thead th:nth-child(2)', 'props': [('min-width', '40px')]}], overwrite=False)
416
- .set_table_styles([{'selector': 'thead th:nth-child(3)', 'props': [('min-width', '40px')]}], overwrite=False)
417
- .set_table_styles([{'selector': 'thead th:nth-child(4)', 'props': [('min-width', '40px')]}], overwrite=False)
418
- .set_table_styles([{'selector': 'thead th:nth-child(5)', 'props': [('min-width', '40px')]}], overwrite=False)
419
- .set_table_styles([{'selector': 'thead th:nth-child(6)', 'props': [('min-width', '40px')]}], overwrite=False)
420
- .set_table_styles([{'selector': 'thead th:nth-child(7)', 'props': [('min-width', '40px')]}], overwrite=False)
421
- .set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False)
422
- .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120)
423
- .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type'])
424
-
425
-
426
- )
427
-
428
- return df_brush_style
429
-
430
- # return Tabulator(
431
- # brushed_df.to_pandas(),
432
- # table_options=TableOptions(
433
- # height=800,
434
- # resizable_column_fit=True,
435
- # )
436
- # )
437
- # return brushed_points(
438
- # ((brushed_df.group_by(['pitcher_id', 'pitch_description'])
439
- # .agg([
440
- # pl.col('is_pitch').drop_nans().count().alias('pitches'),
441
- # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
442
- # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
443
- # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
444
- # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
445
- # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
446
- # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
447
- # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
448
- # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
449
- # ])
450
- # .with_columns(
451
- # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
452
- # .round(1)
453
- # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
454
- # .alias('proportion')
455
- # )
456
- # )).sort('proportion', descending=True).
457
- # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
458
- # "spin_rate", "x0", "z0",'tj_stuff_plus'])
459
- # .rename({
460
- # 'pitch_description': 'Pitch Type',
461
- # 'pitches': 'Pitches',
462
- # 'proportion': 'Proportion',
463
- # 'start_speed': 'Velocity',
464
- # 'ivb': 'iVB',
465
- # 'hb': 'HB',
466
- # 'spin_rate': 'Spin Rate',
467
- # 'x0': 'hRel',
468
- # 'z0': 'vRel',
469
- # 'tj_stuff_plus': 'tjStuff+'
470
- # }).to_pandas(),
471
- # input.plot_brush(), # Note: changed to match the brush ID
472
- # xvar="HB", # Replace "x" with your actual x-axis column name
473
- # yvar="iVB", # Replace "y" with your actual y-axis column name
474
- # all_rows=False
475
- # )
476
-
477
-
478
-
479
- # return brushed_points(
480
- # ((cached_data().group_by(['pitcher_id', 'pitch_description'])
481
- # .agg([
482
- # pl.col('is_pitch').drop_nans().count().alias('pitches'),
483
- # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
484
- # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
485
- # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
486
- # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
487
- # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
488
- # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
489
- # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
490
- # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
491
- # ])
492
- # .with_columns(
493
- # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
494
- # .round(1)
495
- # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
496
- # .alias('proportion')
497
- # )
498
- # )).sort('proportion', descending=True).
499
- # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
500
- # "spin_rate", "x0", "z0",'tj_stuff_plus'])
501
- # .rename({
502
- # 'pitch_description': 'Pitch Type',
503
- # 'pitches': 'Pitches',
504
- # 'proportion': 'Prop',
505
- # 'start_speed': 'Velocity',
506
- # 'ivb': 'iVB',
507
- # 'hb': 'HB',
508
- # 'spin_rate': 'Spin Rate',
509
- # 'x0': 'hRel',
510
- # 'z0': 'vRel',
511
- # 'tj_stuff_plus': 'tjStuff+'
512
- # }).to_pandas(),
513
- # input.plot_brush(), # Note: changed to match the brush ID
514
- # xvar="HB", # Replace "x" with your actual x-axis column name
515
- # yvar="iVB", # Replace "y" with your actual y-axis column name
516
- # all_rows=False
517
- # )
518
- # @output
519
- @render.plot
520
- @reactive.event(input.generate_plot)
521
- def plot():
522
- # Show progress/loading notification
523
- with ui.Progress(min=0, max=1) as p:
524
- p.set(message="Generating plot", detail="This may take a while...")
525
-
526
-
527
- p.set(0.3, "Gathering data...")
528
- year_input = int(input.year_input())
529
- sport_id = int(input.level_input())
530
- player_input = int(input.pitcher_id())
531
- start_date = str(input.date_id()[0])
532
- end_date = str(input.date_id()[1])
533
-
534
- print(year_input, sport_id, player_input, start_date, end_date)
535
-
536
- df = cached_data()
537
- df = df.clone()
538
-
539
- p.set(0.6, "Creating plot...")
540
-
541
-
542
- ploter.final_plot(
543
- df=df,
544
- pitcher_id=player_input,
545
- plot_picker='short_form_movement',#plot_picker,
546
- sport_id=sport_id)
547
-
548
-
549
- # #plt.rcParams["figure.figsize"] = [10,10]
550
- # fig = plt.figure(figsize=(26,26))
551
- # plt.rcParams.update({'figure.autolayout': True})
552
- # fig.set_facecolor('white')
553
- # sns.set_theme(style="whitegrid", palette=colour_palette)
554
- # print('this is the one plot')
555
-
556
- # gs = gridspec.GridSpec(6, 8,
557
- # height_ratios=[5,20,12,36,36,7],
558
- # width_ratios=[4,18,18,18,18,18,18,4])
559
-
560
-
561
- # gs.update(hspace=0.2, wspace=0.5)
562
-
563
- # # Define the positions of each subplot in the grid
564
- # ax_headshot = fig.add_subplot(gs[1,1:3])
565
- # ax_bio = fig.add_subplot(gs[1,3:5])
566
- # ax_logo = fig.add_subplot(gs[1,5:7])
567
-
568
- # ax_season_table = fig.add_subplot(gs[2,1:7])
569
-
570
- # ax_plot_1 = fig.add_subplot(gs[3,1:3])
571
- # ax_plot_2 = fig.add_subplot(gs[3,3:5])
572
- # ax_plot_3 = fig.add_subplot(gs[3,5:7])
573
-
574
- # ax_table = fig.add_subplot(gs[4,1:7])
575
-
576
- # ax_footer = fig.add_subplot(gs[-1,1:7])
577
- # ax_header = fig.add_subplot(gs[0,1:7])
578
- # ax_left = fig.add_subplot(gs[:,0])
579
- # ax_right = fig.add_subplot(gs[:,-1])
580
-
581
- # # Hide axes for footer, header, left, and right
582
- # ax_footer.axis('off')
583
- # ax_header.axis('off')
584
- # ax_left.axis('off')
585
- # ax_right.axis('off')
586
-
587
- # sns.set_theme(style="whitegrid", palette=colour_palette)
588
- # fig.set_facecolor('white')
589
-
590
- # df_teams = scrape.get_teams()
591
-
592
- # player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
593
- # player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
594
- # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
595
-
596
- # stat_summary_table(df=df,
597
- # ax=ax_season_table,
598
- # player_input=player_input,
599
- # split=input.split_id(),
600
- # sport_id=sport_id)
601
-
602
- # # break_plot(df=df_plot,ax=ax2)
603
- # 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]):
604
- # if x == 'velocity_kdes':
605
- # velocity_kdes(df,
606
- # ax=y,
607
- # gs=gs,
608
- # gs_x=[3,4],
609
- # gs_y=[z,z+2],
610
- # fig=fig)
611
- # if x == 'tj_stuff_roling':
612
- # tj_stuff_roling(df=df,
613
- # window=int(input.rolling_window()),
614
- # ax=y)
615
-
616
- # if x == 'tj_stuff_roling_game':
617
- # tj_stuff_roling_game(df=df,
618
- # window=int(input.rolling_window()),
619
- # ax=y)
620
-
621
- # if x == 'break_plot':
622
- # break_plot(df = df,ax=y)
623
-
624
- # if x == 'location_plot_lhb':
625
- # location_plot(df = df,ax=y,hand='L')
626
-
627
- # if x == 'location_plot_rhb':
628
- # location_plot(df = df,ax=y,hand='R')
629
-
630
- # summary_table(df=df,
631
- # ax=ax_table)
632
-
633
- # plot_footer(ax_footer)
634
-
635
- # fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
636
-
637
- # fig.savefig('test.svg')
638
-
639
-
640
-
641
  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
+ # from functions.PitchPlotFunctions import *
23
+ import functions.PitchPlotFunctions as ppf
24
+ ploter = ppf.PitchPlotFunctions()
25
+ from shiny.plotutils import brushed_points
26
+ # from pytabulator import TableOptions, Tabulator, output_tabulator, render_tabulator, theme
27
+ # theme.tabulator_site()
28
+
29
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
30
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
31
+ cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000'])
32
+
33
+ year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
34
+
35
+
36
+
37
+ level_dict = {'1':'MLB',
38
+ '11':'AAA',
39
+ '12':'AA',
40
+ '13':'A+',
41
+ '14':'A',
42
+ '17':'AFL',
43
+ '22':'College',
44
+ '21':'Prospects',
45
+ '51':'International' }
46
+
47
+ function_dict={
48
+ 'velocity_kdes':'Velocity Distributions',
49
+ 'break_plot':'Pitch Movement',
50
+ 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
51
+ 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
52
+ 'location_plot_lhb':'Locations vs LHB',
53
+ 'location_plot_rhb':'Locations vs RHB',
54
+ }
55
+
56
+
57
+ split_dict = {'all':'All',
58
+ 'left':'LHH',
59
+ 'right':'RHH'}
60
+
61
+ split_dict_hand = {'all':['L','R'],
62
+ 'left':['L'],
63
+ 'right':['R']}
64
+
65
+ ### PITCH COLOURS ###
66
+
67
+ # Dictionary to map pitch types to their corresponding colors and names
68
+ pitch_colours = {
69
+ ## Fastballs ##
70
+ 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
71
+ 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
72
+ 'SI': {'colour': '#98165D', 'name': 'Sinker'},
73
+ 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
74
+
75
+ ## Offspeed ##
76
+ 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
77
+ 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
78
+ 'SC': {'colour': '#F08223', 'name': 'Screwball'},
79
+ 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
80
+
81
+ ## Sliders ##
82
+ 'SL': {'colour': '#67E18D', 'name': 'Slider'},
83
+ 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
84
+ 'SV': {'colour': '#376748', 'name': 'Slurve'},
85
+
86
+ ## Curveballs ##
87
+ 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
88
+ 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
89
+ 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
90
+ 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
91
+
92
+ ## Others ##
93
+ 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
94
+ 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'},
95
+ 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
96
+ 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
97
+ }
98
+
99
+ # Create dictionaries for pitch types and their attributes
100
+ dict_colour = {key: value['colour'] for key, value in pitch_colours.items()}
101
+ dict_pitch = {key: value['name'] for key, value in pitch_colours.items()}
102
+ dict_pitch_desc_type = {value['name']: key for key, value in pitch_colours.items()}
103
+ dict_pitch_desc_type.update({'Four-Seam Fastball':'FF'})
104
+ dict_pitch_desc_type.update({'All':'All'})
105
+ dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()}
106
+ dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'})
107
+ dict_pitch_name.update({'4-Seam':'#FF007D'})
108
+
109
+
110
+ from shiny import App, reactive, ui, render
111
+ from shiny.ui import h2, tags
112
+
113
+ # Define the UI layout for the app
114
+ app_ui = ui.page_fluid(
115
+ ui.layout_sidebar(
116
+ ui.panel_sidebar(
117
+ # Row for selecting season and level
118
+ ui.row(
119
+ ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
120
+ ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
121
+ ),
122
+ # Row for the action button to get player list
123
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
124
+ # Row for selecting the player
125
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
126
+ # Row for selecting the date range
127
+ ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
128
+
129
+ ui.row(
130
+ ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
131
+ ),
132
+ # Row for the action button to generate plot
133
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
134
+ ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")),
135
+
136
+ ),
137
+
138
+
139
+ ui.panel_main(
140
+ # ui.navset_tab(
141
+ # Tab for game summary plot
142
+ # ui.nav(
143
+ # "Pitching Summary",
144
+ ui.card(
145
+ {"style": "width: 870px;"},
146
+ ui.head_content(
147
+ ui.tags.script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"),
148
+ ui.tags.script("""
149
+ async function downloadSVG() {
150
+ const content = document.getElementById('capture-section');
151
+
152
+ // Create a new SVG element
153
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
154
+ const bbox = content.getBoundingClientRect();
155
+
156
+ // Set SVG attributes
157
+ svg.setAttribute('width', bbox.width);
158
+ svg.setAttribute('height', bbox.height);
159
+ svg.setAttribute('viewBox', `0 0 ${bbox.width} ${bbox.height}`);
160
+
161
+ // Create foreignObject to contain HTML content
162
+ const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
163
+ foreignObject.setAttribute('width', '100%');
164
+ foreignObject.setAttribute('height', '100%');
165
+ foreignObject.setAttribute('x', '0');
166
+ foreignObject.setAttribute('y', '0');
167
+
168
+ // Clone the content and its styles
169
+ const clonedContent = content.cloneNode(true);
170
+
171
+ // Add necessary style context
172
+ const style = document.createElement('style');
173
+ Array.from(document.styleSheets).forEach(sheet => {
174
+ try {
175
+ Array.from(sheet.cssRules).forEach(rule => {
176
+ style.innerHTML += rule.cssText + '\\n';
177
+ });
178
+ } catch (e) {
179
+ console.warn('Could not access stylesheet rules');
180
+ }
181
+ });
182
+
183
+ // Create a wrapper div to hold styles and content
184
+ const wrapper = document.createElement('div');
185
+ wrapper.appendChild(style);
186
+ wrapper.appendChild(clonedContent);
187
+
188
+ foreignObject.appendChild(wrapper);
189
+ svg.appendChild(foreignObject);
190
+
191
+ // Convert to SVG string with XML declaration and DTD
192
+ const svgString = new XMLSerializer().serializeToString(svg);
193
+ const svgBlob = new Blob([
194
+ '<?xml version="1.0" standalone="no"?>\\n',
195
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\\n',
196
+ svgString
197
+ ], {type: 'image/svg+xml;charset=utf-8'});
198
+
199
+ // Create and trigger download
200
+ const url = URL.createObjectURL(svgBlob);
201
+ const link = document.createElement('a');
202
+ link.href = url;
203
+ link.download = 'plot_and_table.svg';
204
+ document.body.appendChild(link);
205
+ link.click();
206
+ document.body.removeChild(link);
207
+ URL.revokeObjectURL(url);
208
+ }
209
+
210
+ $(document).on('click', '#capture_btn', function() {
211
+ downloadSVG();
212
+ });
213
+ """)
214
+ ),
215
+ ui.output_text("status"),
216
+ ui.div(
217
+ {
218
+ "id": "capture-section",
219
+ "style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;" # Added margin-right
220
+ },
221
+ # Plot section with relative positioning for brush
222
+ ui.div(
223
+ {"style": "position: relative;"},
224
+ ui.output_ui("plot_ui")
225
+ ),
226
+ # Table section
227
+ ui.div(
228
+ {"style": "margin-top: 20px;"},
229
+ ui.row(ui.tags.b("Pitches in Selection"), ui.output_table("in_brush")),
230
+
231
+
232
+ ),
233
+ ui.div({"style": "height: 20px;"})
234
+ ),
235
+ ui.input_action_button("capture_btn", "Save as SVG", class_="btn-primary"),
236
+ )
237
+ # ),
238
+ # )
239
+ )
240
+ )
241
+ )
242
+
243
+
244
+ def server(input, output, session):
245
+
246
+ @reactive.calc
247
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id)
248
+ def cached_data():
249
+
250
+ year_input = int(input.year_input())
251
+ sport_id = int(input.level_input())
252
+ player_input = int(input.pitcher_id())
253
+ start_date = str(input.date_id()[0])
254
+ end_date = str(input.date_id()[1])
255
+ # Simulate an expensive data operation
256
+ game_list = scrape.get_player_games_list(sport_id = sport_id,
257
+ season = year_input,
258
+ player_id = player_input,
259
+ start_date = start_date,
260
+ end_date = end_date)
261
+
262
+ data_list = scrape.get_data(game_list_input = game_list[:])
263
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
264
+ (pl.col("pitcher_id") == player_input)&
265
+ (pl.col("is_pitch") == True)&
266
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
267
+
268
+ )))).with_columns(
269
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
270
+ ))
271
+
272
+ df = df.with_columns(
273
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
274
+ prop=pl.col('is_pitch').sum().over("pitch_type")
275
+ )
276
+
277
+ return df
278
+
279
+ @render.ui
280
+ @reactive.event(input.player_button, input.level_input,input.year_input, ignore_none=False)
281
+ def player_select_ui():
282
+ # Get the list of pitchers for the selected level and season
283
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
284
+ pl.col("position").is_in(['P'])).sort("name")
285
+
286
+ # Create a dictionary of pitcher IDs and names
287
+ pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
288
+
289
+ # Return a select input for choosing a pitcher
290
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
291
+
292
+ @render.ui
293
+ @reactive.event(input.player_button,input.pitcher_id,input.year_input, ignore_none=False)
294
+ def date_id():
295
+ # Create a date range input for selecting the date range within the selected year
296
+ return ui.input_date_range("date_id", "Select Date Range",
297
+ start=f"{int(input.year_input())}-01-01",
298
+ end=f"{int(input.year_input())}-03-31",
299
+ min=f"{int(input.year_input())}-01-01",
300
+ max=f"{int(input.year_input())}-12-31")
301
+ @output
302
+ @render.text
303
+ def status():
304
+ # Only show status when generating
305
+ if input.generate == 0:
306
+ return ""
307
+ return ""
308
+
309
+ @render.ui
310
+ @reactive.event(input.generate_plot)
311
+ def plot_ui():
312
+ brush_opts_kwargs = {}
313
+ brush_opts_kwargs["direction"] = 'xy'
314
+ brush_opts_kwargs["delay"] = 60 # Optional: adds a small delay for better performance
315
+ brush_opts_kwargs["delay_type"] = "throttle"
316
+
317
+
318
+ return ui.output_plot('plot',
319
+ width='800px',
320
+ height='800px',
321
+ brush=ui.brush_opts(**brush_opts_kwargs))
322
+
323
+ @render.table
324
+ @reactive.event(input.plot_brush, input.generate_table) # Note: changed to match the brush ID
325
+ def in_brush():
326
+ # if input.plot_brush() is None: # Note: changed to match the brush ID
327
+ # return None
328
+ brushed_df = pl.DataFrame(brushed_points(
329
+ cached_data().to_pandas(),
330
+ input.plot_brush(),
331
+ xvar="hb",
332
+ yvar="ivb",
333
+ all_rows=False
334
+ ))
335
+
336
+
337
+ brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description'])
338
+ .agg([
339
+ pl.col('is_pitch').drop_nans().count().alias('pitches'),
340
+ pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
341
+ pl.col('vb').drop_nans().mean().round(1).alias('vb'),
342
+ pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
343
+ pl.col('hb').drop_nans().mean().round(1).alias('hb'),
344
+ pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
345
+ pl.col('x0').drop_nans().mean().round(1).alias('x0'),
346
+ pl.col('z0').drop_nans().mean().round(1).alias('z0'),
347
+ pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
348
+ ])
349
+ .with_columns(
350
+ (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id'))
351
+ # .round(1)
352
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
353
+ .alias('proportion')
354
+ )
355
+ )).sort('proportion', descending=True).
356
+ select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
357
+ "spin_rate", "x0", "z0",'tj_stuff_plus'])
358
+ .with_columns(
359
+ pl.when(pl.col("pitch_description") == "Four-Seam Fastball")
360
+ .then(pl.lit("4-Seam"))
361
+ .otherwise(pl.col("pitch_description"))
362
+ .alias("pitch_description")
363
+ )
364
+ .rename({
365
+ 'pitch_description': 'Pitch Type',
366
+ 'pitches': 'Pitches',
367
+ 'proportion': 'Prop',
368
+ 'start_speed': 'Velo',
369
+ 'ivb': 'iVB',
370
+ 'hb': 'HB',
371
+ 'spin_rate': 'Spin',
372
+ 'x0': 'hRel',
373
+ 'z0': 'vRel',
374
+ 'tj_stuff_plus': 'tjStuff+'
375
+ }))
376
+
377
+ # brushed_df_final = brushed_df_final
378
+
379
+ # print(brushed_df_final)
380
+
381
+ def change_font(val):
382
+ if val == "Cutter":
383
+ return "color: red; font-weight: bold;"
384
+ else:
385
+ ''
386
+ return "font-weight: bold;"
387
+ df_brush_style = (brushed_df_final.to_pandas().style.set_precision(1)
388
+
389
+ .set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{
390
+ 'selector': 'caption',
391
+ 'props': [
392
+ ('color', ''),
393
+ ('fontname', 'Century Gothic'),
394
+ ('font-size', '16px'),
395
+ ('font-style', 'italic'),
396
+ ('font-weight', ''),
397
+ ('text-align', 'centre'),
398
+ ]
399
+
400
+ },{'selector' :'th', 'props':[('font-size', '16px'),('text-align', 'center'),('Height','px'),('color','black'),('border', '1px black solid !important')]},{'selector' :'td', 'props':[('text-align', 'center'),('font-size', '16px'),('color','black')]}],overwrite=False)
401
+ .set_properties(**{'background-color':'White','index':'White','min-width':'72px'},overwrite=False)
402
+ .set_table_styles([{'selector': 'th:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
403
+ .set_table_styles([{'selector': 'tr:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
404
+ .set_table_styles([{'selector': 'tr', 'props': [('line-height', '20px')]}],overwrite=False)
405
+ .set_properties(**{'Height': '8px'},**{'text-align': 'center'},overwrite=False)
406
+ .hide_index()
407
+ .set_properties(**{'border': '1px black solid !important'})
408
+ .format('{:.0%}',subset=(brushed_df_final.columns[2]))
409
+ .format('{:.0f}',subset=(brushed_df_final.columns[6]))
410
+ .format('{:.0f}',subset=(brushed_df_final.columns[-1]))
411
+ .set_properties(subset=brushed_df_final.columns, **{'height': '30px'})
412
+ .set_table_styles([{'selector': 'thead th', 'props': [('height', '30px')]}], overwrite=False)
413
+ # .set_table_styles([{'selector': 'table', 'props': [('width', '100px')]}], overwrite=False)
414
+ .set_table_styles([{'selector': 'thead th:nth-child(1)', 'props': [('min-width', '125px')]}], overwrite=False)
415
+ .set_table_styles([{'selector': 'thead th:nth-child(2)', 'props': [('min-width', '40px')]}], overwrite=False)
416
+ .set_table_styles([{'selector': 'thead th:nth-child(3)', 'props': [('min-width', '40px')]}], overwrite=False)
417
+ .set_table_styles([{'selector': 'thead th:nth-child(4)', 'props': [('min-width', '40px')]}], overwrite=False)
418
+ .set_table_styles([{'selector': 'thead th:nth-child(5)', 'props': [('min-width', '40px')]}], overwrite=False)
419
+ .set_table_styles([{'selector': 'thead th:nth-child(6)', 'props': [('min-width', '40px')]}], overwrite=False)
420
+ .set_table_styles([{'selector': 'thead th:nth-child(7)', 'props': [('min-width', '40px')]}], overwrite=False)
421
+ .set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False)
422
+ .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120)
423
+ .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type'])
424
+
425
+
426
+ )
427
+
428
+ return df_brush_style
429
+
430
+ # return Tabulator(
431
+ # brushed_df.to_pandas(),
432
+ # table_options=TableOptions(
433
+ # height=800,
434
+ # resizable_column_fit=True,
435
+ # )
436
+ # )
437
+ # return brushed_points(
438
+ # ((brushed_df.group_by(['pitcher_id', 'pitch_description'])
439
+ # .agg([
440
+ # pl.col('is_pitch').drop_nans().count().alias('pitches'),
441
+ # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
442
+ # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
443
+ # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
444
+ # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
445
+ # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
446
+ # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
447
+ # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
448
+ # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
449
+ # ])
450
+ # .with_columns(
451
+ # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
452
+ # .round(1)
453
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
454
+ # .alias('proportion')
455
+ # )
456
+ # )).sort('proportion', descending=True).
457
+ # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
458
+ # "spin_rate", "x0", "z0",'tj_stuff_plus'])
459
+ # .rename({
460
+ # 'pitch_description': 'Pitch Type',
461
+ # 'pitches': 'Pitches',
462
+ # 'proportion': 'Proportion',
463
+ # 'start_speed': 'Velocity',
464
+ # 'ivb': 'iVB',
465
+ # 'hb': 'HB',
466
+ # 'spin_rate': 'Spin Rate',
467
+ # 'x0': 'hRel',
468
+ # 'z0': 'vRel',
469
+ # 'tj_stuff_plus': 'tjStuff+'
470
+ # }).to_pandas(),
471
+ # input.plot_brush(), # Note: changed to match the brush ID
472
+ # xvar="HB", # Replace "x" with your actual x-axis column name
473
+ # yvar="iVB", # Replace "y" with your actual y-axis column name
474
+ # all_rows=False
475
+ # )
476
+
477
+
478
+
479
+ # return brushed_points(
480
+ # ((cached_data().group_by(['pitcher_id', 'pitch_description'])
481
+ # .agg([
482
+ # pl.col('is_pitch').drop_nans().count().alias('pitches'),
483
+ # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
484
+ # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
485
+ # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
486
+ # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
487
+ # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
488
+ # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
489
+ # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
490
+ # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
491
+ # ])
492
+ # .with_columns(
493
+ # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
494
+ # .round(1)
495
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
496
+ # .alias('proportion')
497
+ # )
498
+ # )).sort('proportion', descending=True).
499
+ # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
500
+ # "spin_rate", "x0", "z0",'tj_stuff_plus'])
501
+ # .rename({
502
+ # 'pitch_description': 'Pitch Type',
503
+ # 'pitches': 'Pitches',
504
+ # 'proportion': 'Prop',
505
+ # 'start_speed': 'Velocity',
506
+ # 'ivb': 'iVB',
507
+ # 'hb': 'HB',
508
+ # 'spin_rate': 'Spin Rate',
509
+ # 'x0': 'hRel',
510
+ # 'z0': 'vRel',
511
+ # 'tj_stuff_plus': 'tjStuff+'
512
+ # }).to_pandas(),
513
+ # input.plot_brush(), # Note: changed to match the brush ID
514
+ # xvar="HB", # Replace "x" with your actual x-axis column name
515
+ # yvar="iVB", # Replace "y" with your actual y-axis column name
516
+ # all_rows=False
517
+ # )
518
+ # @output
519
+ @render.plot
520
+ @reactive.event(input.generate_plot)
521
+ def plot():
522
+ # Show progress/loading notification
523
+ with ui.Progress(min=0, max=1) as p:
524
+ p.set(message="Generating plot", detail="This may take a while...")
525
+
526
+
527
+ p.set(0.3, "Gathering data...")
528
+ year_input = int(input.year_input())
529
+ sport_id = int(input.level_input())
530
+ player_input = int(input.pitcher_id())
531
+ start_date = str(input.date_id()[0])
532
+ end_date = str(input.date_id()[1])
533
+
534
+ print(year_input, sport_id, player_input, start_date, end_date)
535
+
536
+ df = cached_data()
537
+ df = df.clone()
538
+
539
+ p.set(0.6, "Creating plot...")
540
+
541
+
542
+ ploter.final_plot(
543
+ df=df,
544
+ pitcher_id=player_input,
545
+ plot_picker='short_form_movement',#plot_picker,
546
+ sport_id=sport_id)
547
+
548
+
549
+ # #plt.rcParams["figure.figsize"] = [10,10]
550
+ # fig = plt.figure(figsize=(26,26))
551
+ # plt.rcParams.update({'figure.autolayout': True})
552
+ # fig.set_facecolor('white')
553
+ # sns.set_theme(style="whitegrid", palette=colour_palette)
554
+ # print('this is the one plot')
555
+
556
+ # gs = gridspec.GridSpec(6, 8,
557
+ # height_ratios=[5,20,12,36,36,7],
558
+ # width_ratios=[4,18,18,18,18,18,18,4])
559
+
560
+
561
+ # gs.update(hspace=0.2, wspace=0.5)
562
+
563
+ # # Define the positions of each subplot in the grid
564
+ # ax_headshot = fig.add_subplot(gs[1,1:3])
565
+ # ax_bio = fig.add_subplot(gs[1,3:5])
566
+ # ax_logo = fig.add_subplot(gs[1,5:7])
567
+
568
+ # ax_season_table = fig.add_subplot(gs[2,1:7])
569
+
570
+ # ax_plot_1 = fig.add_subplot(gs[3,1:3])
571
+ # ax_plot_2 = fig.add_subplot(gs[3,3:5])
572
+ # ax_plot_3 = fig.add_subplot(gs[3,5:7])
573
+
574
+ # ax_table = fig.add_subplot(gs[4,1:7])
575
+
576
+ # ax_footer = fig.add_subplot(gs[-1,1:7])
577
+ # ax_header = fig.add_subplot(gs[0,1:7])
578
+ # ax_left = fig.add_subplot(gs[:,0])
579
+ # ax_right = fig.add_subplot(gs[:,-1])
580
+
581
+ # # Hide axes for footer, header, left, and right
582
+ # ax_footer.axis('off')
583
+ # ax_header.axis('off')
584
+ # ax_left.axis('off')
585
+ # ax_right.axis('off')
586
+
587
+ # sns.set_theme(style="whitegrid", palette=colour_palette)
588
+ # fig.set_facecolor('white')
589
+
590
+ # df_teams = scrape.get_teams()
591
+
592
+ # player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
593
+ # player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
594
+ # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
595
+
596
+ # stat_summary_table(df=df,
597
+ # ax=ax_season_table,
598
+ # player_input=player_input,
599
+ # split=input.split_id(),
600
+ # sport_id=sport_id)
601
+
602
+ # # break_plot(df=df_plot,ax=ax2)
603
+ # 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]):
604
+ # if x == 'velocity_kdes':
605
+ # velocity_kdes(df,
606
+ # ax=y,
607
+ # gs=gs,
608
+ # gs_x=[3,4],
609
+ # gs_y=[z,z+2],
610
+ # fig=fig)
611
+ # if x == 'tj_stuff_roling':
612
+ # tj_stuff_roling(df=df,
613
+ # window=int(input.rolling_window()),
614
+ # ax=y)
615
+
616
+ # if x == 'tj_stuff_roling_game':
617
+ # tj_stuff_roling_game(df=df,
618
+ # window=int(input.rolling_window()),
619
+ # ax=y)
620
+
621
+ # if x == 'break_plot':
622
+ # break_plot(df = df,ax=y)
623
+
624
+ # if x == 'location_plot_lhb':
625
+ # location_plot(df = df,ax=y,hand='L')
626
+
627
+ # if x == 'location_plot_rhb':
628
+ # location_plot(df = df,ax=y,hand='R')
629
+
630
+ # summary_table(df=df,
631
+ # ax=ax_table)
632
+
633
+ # plot_footer(ax_footer)
634
+
635
+ # fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
636
+
637
+ # fig.savefig('test.svg')
638
+
639
+
640
+
641
  app = App(app_ui, server)