nesticot commited on
Commit
3b489d1
·
verified ·
1 Parent(s): 48d6f70

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -267
app.py CHANGED
@@ -31,17 +31,17 @@ colour_palette = ['#FFB000','#648FFF','#785EF0',
31
  '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
32
  cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000'])
33
 
34
- year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
35
 
36
 
37
- type_dict = {'R':'Regular Season',
38
- 'S':'Spring',
39
- 'P':'Playoffs' }
40
 
41
  level_dict = {'1':'MLB',
42
  '11':'AAA',
43
- '12':'AA',
44
- '13':'A+',
45
  '14':'A',
46
  '17':'AFL',
47
  '22':'College',
@@ -109,8 +109,12 @@ dict_pitch_desc_type.update({'All':'All'})
109
  dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()}
110
  dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'})
111
  dict_pitch_name.update({'4-Seam':'#FF007D'})
 
112
 
113
 
 
 
 
114
  from shiny import App, reactive, ui, render
115
  from shiny.ui import h2, tags
116
 
@@ -119,10 +123,29 @@ app_ui = ui.page_fluid(
119
  ui.layout_sidebar(
120
  ui.panel_sidebar(
121
  # Row for selecting season and level
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  ui.row(
123
- ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
124
  ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
125
- ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
126
  ),
127
  # Row for the action button to get player list
128
  ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
@@ -134,8 +157,16 @@ app_ui = ui.page_fluid(
134
  ui.row(
135
  ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
136
  ),
 
 
 
 
 
 
 
 
137
  # Row for the action button to generate plot
138
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
139
  ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")),
140
 
141
  ),
@@ -296,43 +327,163 @@ ui.card(
296
  )
297
  )
298
 
299
-
300
  def server(input, output, session):
 
301
 
302
- @reactive.calc
303
- @reactive.event(input.pitcher_id, input.date_id,input.split_id)
304
- def cached_data():
305
-
306
- year_input = int(input.year_input())
307
- sport_id = int(input.level_input())
308
- player_input = int(input.pitcher_id())
309
- start_date = str(input.date_id()[0])
310
- end_date = str(input.date_id()[1])
311
- print('sportid',input.type_input())
312
- # Simulate an expensive data operation
313
- game_list = scrape.get_player_games_list(sport_id = sport_id,
314
- season = year_input,
315
- player_id = player_input,
316
- start_date = start_date,
317
- end_date = end_date,
318
- game_type = [input.type_input()])
319
-
320
- data_list = scrape.get_data(game_list_input = game_list[:])
321
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
322
- (pl.col("pitcher_id") == player_input)&
323
- (pl.col("is_pitch") == True)&
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
325
-
326
- )))).with_columns(
327
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
328
- ))
329
 
330
- df = df.with_columns(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
332
  prop=pl.col('is_pitch').sum().over("pitch_type")
333
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
- return df
336
 
337
  @render.ui
338
  @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
@@ -346,9 +497,6 @@ def server(input, output, session):
346
 
347
  # Create a dictionary of pitcher IDs and names
348
  pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
349
-
350
-
351
-
352
 
353
  # Return a select input for choosing a pitcher
354
  return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
@@ -380,42 +528,97 @@ def server(input, output, session):
380
  "clip": True, # This helps constrain the brush to the plot area
381
  "fill": "#00000033", # Optional: sets a semi-transparent fill
382
  "stroke": "#000000", # Resets brush when new data is loaded
383
-
384
  }
385
 
386
-
387
  return ui.output_plot('plot',
388
  width='800px',
389
  height='800px',
390
  brush=ui.brush_opts(**brush_opts_kwargs))
391
 
392
  @render.table
393
- @reactive.event(input.plot_brush, input.generate_table) # Note: changed to match the brush ID
394
  def in_brush():
395
  # if input.plot_brush() is None: # Note: changed to match the brush ID
396
  # return None
397
- brushed_df = pl.DataFrame(brushed_points(
398
- cached_data().to_pandas(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  input.plot_brush(),
400
  xvar="hb",
401
  yvar="ivb",
402
  all_rows=False
403
  ))
404
 
 
 
405
 
406
- brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description'])
407
- .agg([
408
- pl.col('is_pitch').drop_nans().count().alias('pitches'),
409
- pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
410
- pl.col('vb').drop_nans().mean().round(1).alias('vb'),
411
- pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
412
- pl.col('hb').drop_nans().mean().round(1).alias('hb'),
413
- pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
414
- pl.col('x0').drop_nans().mean().round(1).alias('x0'),
415
- pl.col('z0').drop_nans().mean().round(1).alias('z0'),
416
- pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
417
- ])
418
- .with_columns(
419
  (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id'))
420
  # .round(1)
421
  # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
@@ -443,6 +646,9 @@ def server(input, output, session):
443
  'tj_stuff_plus': 'tjStuff+'
444
  }))
445
 
 
 
 
446
  # brushed_df_final = brushed_df_final
447
 
448
  # print(brushed_df_final)
@@ -453,7 +659,8 @@ def server(input, output, session):
453
  else:
454
  ''
455
  return "font-weight: bold;"
456
- df_brush_style = (brushed_df_final.to_pandas().style.set_precision(1)
 
457
 
458
  .set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{
459
  'selector': 'caption',
@@ -490,232 +697,93 @@ def server(input, output, session):
490
  .set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False)
491
  .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120)
492
  .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type'])
 
493
 
494
 
495
- )
 
 
496
 
497
  return df_brush_style
498
 
499
- # return Tabulator(
500
- # brushed_df.to_pandas(),
501
- # table_options=TableOptions(
502
- # height=800,
503
- # resizable_column_fit=True,
504
- # )
505
- # )
506
- # return brushed_points(
507
- # ((brushed_df.group_by(['pitcher_id', 'pitch_description'])
508
- # .agg([
509
- # pl.col('is_pitch').drop_nans().count().alias('pitches'),
510
- # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
511
- # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
512
- # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
513
- # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
514
- # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
515
- # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
516
- # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
517
- # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
518
- # ])
519
- # .with_columns(
520
- # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
521
- # .round(1)
522
- # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
523
- # .alias('proportion')
524
- # )
525
- # )).sort('proportion', descending=True).
526
- # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
527
- # "spin_rate", "x0", "z0",'tj_stuff_plus'])
528
- # .rename({
529
- # 'pitch_description': 'Pitch Type',
530
- # 'pitches': 'Pitches',
531
- # 'proportion': 'Proportion',
532
- # 'start_speed': 'Velocity',
533
- # 'ivb': 'iVB',
534
- # 'hb': 'HB',
535
- # 'spin_rate': 'Spin Rate',
536
- # 'x0': 'hRel',
537
- # 'z0': 'vRel',
538
- # 'tj_stuff_plus': 'tjStuff+'
539
- # }).to_pandas(),
540
- # input.plot_brush(), # Note: changed to match the brush ID
541
- # xvar="HB", # Replace "x" with your actual x-axis column name
542
- # yvar="iVB", # Replace "y" with your actual y-axis column name
543
- # all_rows=False
544
- # )
545
-
546
-
547
 
548
- # return brushed_points(
549
- # ((cached_data().group_by(['pitcher_id', 'pitch_description'])
550
- # .agg([
551
- # pl.col('is_pitch').drop_nans().count().alias('pitches'),
552
- # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
553
- # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
554
- # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
555
- # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
556
- # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
557
- # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
558
- # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
559
- # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
560
- # ])
561
- # .with_columns(
562
- # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
563
- # .round(1)
564
- # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
565
- # .alias('proportion')
566
- # )
567
- # )).sort('proportion', descending=True).
568
- # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
569
- # "spin_rate", "x0", "z0",'tj_stuff_plus'])
570
- # .rename({
571
- # 'pitch_description': 'Pitch Type',
572
- # 'pitches': 'Pitches',
573
- # 'proportion': 'Prop',
574
- # 'start_speed': 'Velocity',
575
- # 'ivb': 'iVB',
576
- # 'hb': 'HB',
577
- # 'spin_rate': 'Spin Rate',
578
- # 'x0': 'hRel',
579
- # 'z0': 'vRel',
580
- # 'tj_stuff_plus': 'tjStuff+'
581
- # }).to_pandas(),
582
- # input.plot_brush(), # Note: changed to match the brush ID
583
- # xvar="HB", # Replace "x" with your actual x-axis column name
584
- # yvar="iVB", # Replace "y" with your actual y-axis column name
585
- # all_rows=False
586
- # )
587
- # @output
588
  @render.plot
589
- @reactive.event(input.generate_plot)
590
- def plot():
591
- # Show progress/loading notification
592
  with ui.Progress(min=0, max=1) as p:
593
- p.set(message="Generating plot", detail="This may take a while...")
594
-
595
-
596
- p.set(0.3, "Gathering data...")
597
  year_input = int(input.year_input())
598
  sport_id = int(input.level_input())
599
  player_input = int(input.pitcher_id())
600
  start_date = str(input.date_id()[0])
601
  end_date = str(input.date_id()[1])
 
 
 
 
 
 
 
 
 
602
 
603
- print(year_input, sport_id, player_input, start_date, end_date)
604
-
605
- df = cached_data()
606
- df = df.clone()
607
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  p.set(0.6, "Creating plot...")
609
-
610
- # fig, ax = plt.subplots(figsize=(8, 8))
611
-
612
- ploter.final_plot(
613
  df=df,
614
  pitcher_id=player_input,
615
- plot_picker='short_form_movement',#plot_picker,
616
  sport_id=sport_id,
617
- game_type = [input.type_input()])
 
618
 
619
-
620
-
621
- # # Adjust the plot layout after creation
622
- # plt.subplots_adjust(
623
- # top=0.95, # Reduce top margin
624
- # bottom=0.1, # Increase bottom margin
625
- # left=0.1, # Increase left margin
626
- # right=0.95 # Reduce right margin
627
- # )
628
-
629
- # #plt.rcParams["figure.figsize"] = [10,10]
630
- # fig = plt.figure(figsize=(26,26))
631
- # plt.rcParams.update({'figure.autolayout': True})
632
- # fig.set_facecolor('white')
633
- # sns.set_theme(style="whitegrid", palette=colour_palette)
634
- # print('this is the one plot')
635
-
636
- # gs = gridspec.GridSpec(6, 8,
637
- # height_ratios=[5,20,12,36,36,7],
638
- # width_ratios=[4,18,18,18,18,18,18,4])
639
-
640
-
641
- # gs.update(hspace=0.2, wspace=0.5)
642
-
643
- # # Define the positions of each subplot in the grid
644
- # ax_headshot = fig.add_subplot(gs[1,1:3])
645
- # ax_bio = fig.add_subplot(gs[1,3:5])
646
- # ax_logo = fig.add_subplot(gs[1,5:7])
647
-
648
- # ax_season_table = fig.add_subplot(gs[2,1:7])
649
-
650
- # ax_plot_1 = fig.add_subplot(gs[3,1:3])
651
- # ax_plot_2 = fig.add_subplot(gs[3,3:5])
652
- # ax_plot_3 = fig.add_subplot(gs[3,5:7])
653
-
654
- # ax_table = fig.add_subplot(gs[4,1:7])
655
-
656
- # ax_footer = fig.add_subplot(gs[-1,1:7])
657
- # ax_header = fig.add_subplot(gs[0,1:7])
658
- # ax_left = fig.add_subplot(gs[:,0])
659
- # ax_right = fig.add_subplot(gs[:,-1])
660
-
661
- # # Hide axes for footer, header, left, and right
662
- # ax_footer.axis('off')
663
- # ax_header.axis('off')
664
- # ax_left.axis('off')
665
- # ax_right.axis('off')
666
-
667
- # sns.set_theme(style="whitegrid", palette=colour_palette)
668
- # fig.set_facecolor('white')
669
-
670
- # df_teams = scrape.get_teams()
671
-
672
- # player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
673
- # player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
674
- # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
675
-
676
- # stat_summary_table(df=df,
677
- # ax=ax_season_table,
678
- # player_input=player_input,
679
- # split=input.split_id(),
680
- # sport_id=sport_id)
681
-
682
- # # break_plot(df=df_plot,ax=ax2)
683
- # 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]):
684
- # if x == 'velocity_kdes':
685
- # velocity_kdes(df,
686
- # ax=y,
687
- # gs=gs,
688
- # gs_x=[3,4],
689
- # gs_y=[z,z+2],
690
- # fig=fig)
691
- # if x == 'tj_stuff_roling':
692
- # tj_stuff_roling(df=df,
693
- # window=int(input.rolling_window()),
694
- # ax=y)
695
-
696
- # if x == 'tj_stuff_roling_game':
697
- # tj_stuff_roling_game(df=df,
698
- # window=int(input.rolling_window()),
699
- # ax=y)
700
-
701
- # if x == 'break_plot':
702
- # break_plot(df = df,ax=y)
703
-
704
- # if x == 'location_plot_lhb':
705
- # location_plot(df = df,ax=y,hand='L')
706
-
707
- # if x == 'location_plot_rhb':
708
- # location_plot(df = df,ax=y,hand='R')
709
-
710
- # summary_table(df=df,
711
- # ax=ax_table)
712
-
713
- # plot_footer(ax_footer)
714
-
715
- # fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
716
-
717
- # fig.savefig('test.svg')
718
-
719
 
720
 
721
  app = App(app_ui, server)
 
31
  '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
32
  cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000'])
33
 
34
+ year_list = [2025]
35
 
36
 
37
+ type_dict = {'R':'Regular',
38
+ 'P':'Playoffs',
39
+ 'S':'Spring'}
40
 
41
  level_dict = {'1':'MLB',
42
  '11':'AAA',
43
+ # '12':'AA',
44
+ #'13':'A+',
45
  '14':'A',
46
  '17':'AFL',
47
  '22':'College',
 
109
  dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()}
110
  dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'})
111
  dict_pitch_name.update({'4-Seam':'#FF007D'})
112
+ dict_pitch.update({'FF':'Four-Seam Fastball'})
113
 
114
 
115
+ # Sort dict_pitch alphabetically by pitch name
116
+ dict_pitch_alpha = dict(sorted(dict_pitch.items(), key=lambda item: item[1]))
117
+
118
  from shiny import App, reactive, ui, render
119
  from shiny.ui import h2, tags
120
 
 
123
  ui.layout_sidebar(
124
  ui.panel_sidebar(
125
  # Row for selecting season and level
126
+ ui.row(
127
+ ui.markdown("## Spring Training Pitch Plots"),
128
+ ui.markdown("This app generates a movement plot for a pitcher's pitches in Spring Training games. You can highlight and update pitch types by selecting points on the plot."),
129
+ ui.column(4,ui.div(
130
+ "By: ",
131
+ ui.tags.a(
132
+ "@TJStats",
133
+ href="https://x.com/TJStats",
134
+ target="_blank"
135
+ )
136
+ ),
137
+ ui.tags.p("Data: MLB")),
138
+ ui.column(8,
139
+ ui.tags.p(
140
+ ui.tags.a(
141
+ "Support me on Patreon for more apps",
142
+ href="https://www.patreon.com/TJ_Stats",
143
+ target="_blank"
144
+ )))),
145
  ui.row(
146
+ ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2025)),
147
  ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
148
+ ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='S'))
149
  ),
150
  # Row for the action button to get player list
151
  ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
 
157
  ui.row(
158
  ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
159
  ),
160
+ ui.row( ui.column(6,ui.input_select(
161
+ "new_pitch_type",
162
+ "Update Pitch Type",
163
+ dict_pitch_alpha
164
+ )),
165
+ ui.column(6,ui.input_action_button("update_pitch_type", "Update Pitch Type", class_="btn-secondary"))),
166
+
167
+ # ui.hr(),
168
  # Row for the action button to generate plot
169
+ ui.row(ui.input_action_button("generate_plot", "Generate/Reset Plot", class_="btn-primary")),
170
  ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")),
171
 
172
  ),
 
327
  )
328
  )
329
 
 
330
  def server(input, output, session):
331
+ # This code should be inserted in your server function
332
 
333
+ # Add this near the top of the server function
334
+ modified_data = reactive.value(None)
335
+ # Add a reactive value to store the current selection state
336
+ selection_state = reactive.value(None)
337
+
338
+ # Create reactive values to track the state of all data-dependent inputs
339
+ last_pitcher_id = reactive.value(None)
340
+ last_date_id = reactive.value(None)
341
+ last_split_id = reactive.value(None)
342
+ last_type_input = reactive.value(None)
343
+ last_level_input = reactive.value(None)
344
+ last_year_input = reactive.value(None)
345
+
346
+ # Modify your brush handler to update the selection state
347
+ @reactive.effect
348
+ @reactive.event(input.plot_brush)
349
+ def _():
350
+ brush_data = input.plot_brush()
351
+ selection_state.set(brush_data) # Store the current brush data
352
+
353
+ # Reset modified data when any of the key inputs change
354
+ @reactive.effect
355
+ @reactive.event(input.pitcher_id, input.date_id, input.split_id,
356
+ input.type_input, input.level_input, input.year_input)
357
+ def _reset_on_data_change():
358
+ # Store the current values for comparison
359
+ current_pitcher = input.pitcher_id()
360
+ current_date = input.date_id()
361
+ current_split = input.split_id()
362
+ current_type = input.type_input()
363
+ current_level = input.level_input()
364
+ current_year = input.year_input()
365
+
366
+ # Check if any of the inputs have changed from their last values
367
+ # and they aren't None or initial values
368
+ pitcher_changed = (last_pitcher_id() is not None and current_pitcher != last_pitcher_id())
369
+ date_changed = (last_date_id() is not None and current_date != last_date_id())
370
+ split_changed = (last_split_id() is not None and current_split != last_split_id())
371
+ type_changed = (last_type_input() is not None and current_type != last_type_input())
372
+ level_changed = (last_level_input() is not None and current_level != last_level_input())
373
+ year_changed = (last_year_input() is not None and current_year != last_year_input())
374
+
375
+ # If any of the inputs have changed
376
+ if (pitcher_changed or date_changed or split_changed or
377
+ type_changed or level_changed or year_changed):
378
+ # Reset modified data
379
+ modified_data.set(None)
380
+
381
+ # Show notification
382
+ changed_inputs = []
383
+ if pitcher_changed: changed_inputs.append("pitcher")
384
+ if date_changed: changed_inputs.append("date range")
385
+ if split_changed: changed_inputs.append("split")
386
+ if type_changed: changed_inputs.append("game type")
387
+ if level_changed: changed_inputs.append("league level")
388
+ if year_changed: changed_inputs.append("year")
389
+
390
+ if changed_inputs:
391
+ change_text = ", ".join(changed_inputs)
392
+ ui.notification_show(f"Data filter changed ({change_text}), pitch modifications reset", type="info")
393
+
394
+ # Update the last values
395
+ last_pitcher_id.set(current_pitcher)
396
+ last_date_id.set(current_date)
397
+ last_split_id.set(current_split)
398
+ last_type_input.set(current_type)
399
+ last_level_input.set(current_level)
400
+ last_year_input.set(current_year)
401
+ @reactive.effect
402
+ @reactive.event(input.update_pitch_type)
403
+ def _():
404
+ if input.plot_brush() is None:
405
+ ui.notification_show("Please select points first", type="warning")
406
+ return
407
+
408
+ # Get the current data - either use the previously modified data or fetch fresh data
409
+ if modified_data() is not None:
410
+ # Use already modified data to preserve previous changes
411
+ df = modified_data().copy()
412
+ else:
413
+ # First time modifying, get fresh data
414
+ year_input = int(input.year_input())
415
+ sport_id = int(input.level_input())
416
+ player_input = int(input.pitcher_id())
417
+ start_date = str(input.date_id()[0])
418
+ end_date = str(input.date_id()[1])
419
+
420
+ game_list = scrape.get_player_games_list(
421
+ sport_id=sport_id,
422
+ season=year_input,
423
+ player_id=player_input,
424
+ start_date=start_date,
425
+ end_date=end_date,
426
+ game_type=[input.type_input()]
427
+ )
428
+
429
+ data_list = scrape.get_data(game_list_input=game_list[:])
430
+
431
+ df = (stuff_apply.stuff_apply(
432
+ fe.feature_engineering(
433
+ update.update(
434
+ scrape.get_data_df(data_list=data_list).filter(
435
+ (pl.col("pitcher_id") == player_input) &
436
+ (pl.col("is_pitch") == True) &
437
+ (pl.col("start_speed") >= 50) &
438
  (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
439
+ )
440
+ )
441
+ )
442
+ )).to_pandas()
443
 
444
+ # Get the brushed points
445
+ brushed = brushed_points(
446
+ df,
447
+ input.plot_brush(),
448
+ xvar="hb",
449
+ yvar="ivb",
450
+ all_rows=False
451
+ )
452
+
453
+ if len(brushed) == 0:
454
+ ui.notification_show("No points selected", type="warning")
455
+ return
456
+
457
+ # Update pitch types for brushed points
458
+ new_pitch_type = input.new_pitch_type()
459
+ indices = brushed.index
460
+ df.loc[indices, 'pitch_type'] = new_pitch_type
461
+ df.loc[indices, 'pitch_description'] = dict_pitch[new_pitch_type]
462
+
463
+ # Store the modified data for future updates
464
+ modified_data.set(df)
465
+
466
+ # Recalculate percentages and counts
467
+ pl_df = pl.from_pandas(df)
468
+ pl_df = pl_df.with_columns(
469
  prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
470
  prop=pl.col('is_pitch').sum().over("pitch_type")
471
  )
472
+
473
+ # Convert back to pandas and update the reactive value
474
+ modified_data.set(pl_df.to_pandas())
475
+
476
+ # Show success notification
477
+ ui.notification_show(f"Updated {len(indices)} pitches to {dict_pitch[new_pitch_type]}", type="success")
478
+
479
+ # Reset button handler - clear modified data to start fresh
480
+ # @reactive.effect
481
+ # @reactive.event(input.reset_changes)
482
+ # def _reset_modifications():
483
+ # modified_data.set(None)
484
+ # ui.notification_show("All pitch type changes have been reset", type="info")
485
+
486
 
 
487
 
488
  @render.ui
489
  @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
 
497
 
498
  # Create a dictionary of pitcher IDs and names
499
  pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
 
 
 
500
 
501
  # Return a select input for choosing a pitcher
502
  return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
 
528
  "clip": True, # This helps constrain the brush to the plot area
529
  "fill": "#00000033", # Optional: sets a semi-transparent fill
530
  "stroke": "#000000", # Resets brush when new data is loaded
 
531
  }
532
 
 
533
  return ui.output_plot('plot',
534
  width='800px',
535
  height='800px',
536
  brush=ui.brush_opts(**brush_opts_kwargs))
537
 
538
  @render.table
539
+ @reactive.event(input.plot_brush,input.generate_plot, input.generate_table, input.update_pitch_type)
540
  def in_brush():
541
  # if input.plot_brush() is None: # Note: changed to match the brush ID
542
  # return None
543
+
544
+
545
+ # Use modified data if available
546
+ if modified_data() is not None:
547
+ df = pl.from_pandas(modified_data())
548
+ else:
549
+
550
+ year_input = int(input.year_input())
551
+ sport_id = int(input.level_input())
552
+ player_input = int(input.pitcher_id())
553
+ start_date = str(input.date_id()[0])
554
+ end_date = str(input.date_id()[1])
555
+ print('sportid',input.type_input())
556
+ # Simulate an expensive data operation
557
+ game_list = scrape.get_player_games_list(sport_id = sport_id,
558
+ season = year_input,
559
+ player_id = player_input,
560
+ start_date = start_date,
561
+ end_date = end_date,
562
+ game_type = [input.type_input()])
563
+
564
+ data_list = scrape.get_data(game_list_input = game_list[:])
565
+
566
+ try:
567
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
568
+ (pl.col("pitcher_id") == player_input)&
569
+ (pl.col("is_pitch") == True)&
570
+ (pl.col("start_speed") >= 50)&
571
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
572
+
573
+ )))).with_columns(
574
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
575
+ ))
576
+
577
+ df = df.with_columns(
578
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
579
+ prop=pl.col('is_pitch').sum().over("pitch_type")
580
+ )
581
+
582
+
583
+
584
+
585
+
586
+ except TypeError:
587
+ print("NONE")
588
+ return None
589
+
590
+ # df = df.clone()
591
+
592
+ # print('TABLE DF:',brushed_points())
593
+
594
+ if input.plot_brush() is None:
595
+ brushed_df = df.clone()
596
+ print('TABLE DF:',df)
597
+
598
+ else:
599
+ brushed_df = pl.DataFrame(brushed_points(
600
+ df.to_pandas(),
601
  input.plot_brush(),
602
  xvar="hb",
603
  yvar="ivb",
604
  all_rows=False
605
  ))
606
 
607
+
608
+
609
 
610
+ brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description']).agg([
611
+ pl.col('is_pitch').drop_nans().count().alias('pitches'),
612
+ pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
613
+ pl.col('vb').drop_nans().mean().round(1).alias('vb'),
614
+ pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
615
+ pl.col('hb').drop_nans().mean().round(1).alias('hb'),
616
+ pl.col('spin_rate').drop_nans().mean().alias('spin_rate'),
617
+ pl.col('release_pos_x').drop_nans().mean().round(1).alias('x0'),
618
+ pl.col('release_pos_z').drop_nans().mean().round(1).alias('z0'),
619
+ pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
620
+ ])
621
+ .with_columns(
 
622
  (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id'))
623
  # .round(1)
624
  # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
 
646
  'tj_stuff_plus': 'tjStuff+'
647
  }))
648
 
649
+ brushed_df_final_pd = brushed_df_final.to_pandas()
650
+ brushed_df_final_pd['Spin'] = brushed_df_final_pd['Spin'].fillna(0)
651
+
652
  # brushed_df_final = brushed_df_final
653
 
654
  # print(brushed_df_final)
 
659
  else:
660
  ''
661
  return "font-weight: bold;"
662
+
663
+ df_brush_style = (brushed_df_final_pd.style.set_precision(1)
664
 
665
  .set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{
666
  'selector': 'caption',
 
697
  .set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False)
698
  .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120)
699
  .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type'])
700
+ .applymap(lambda x: f'background-color: black' if x == 0 else '', subset=['Spin'])
701
 
702
 
703
+ )
704
+
705
+ print('BRUSHED:',df_brush_style)
706
 
707
  return df_brush_style
708
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
+ # @output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  @render.plot
712
+ @reactive.event(input.generate_plot, input.update_pitch_type)
713
+ def plot():
714
+ # Initialize progress bar+
715
  with ui.Progress(min=0, max=1) as p:
 
 
 
 
716
  year_input = int(input.year_input())
717
  sport_id = int(input.level_input())
718
  player_input = int(input.pitcher_id())
719
  start_date = str(input.date_id()[0])
720
  end_date = str(input.date_id()[1])
721
+ game_type = [input.type_input()]
722
+ p.set(message="Generating plot", detail="This may take a while...")
723
+
724
+ # Use modified data if available
725
+ if modified_data() is not None:
726
+ df = pl.from_pandas(modified_data())
727
+ else:
728
+ # Get input parameters
729
+ p.set(0.3, "Gathering data...")
730
 
731
+
732
+ # Get game data
733
+ game_list = scrape.get_player_games_list(
734
+ sport_id=sport_id,
735
+ season=year_input,
736
+ player_id=player_input,
737
+ start_date=start_date,
738
+ end_date=end_date,
739
+ game_type=game_type
740
+ )
741
+
742
+ data_list = scrape.get_data(game_list_input=game_list[:])
743
+
744
+ # Process data
745
+ try:
746
+ df = (stuff_apply.stuff_apply(
747
+ fe.feature_engineering(
748
+ update.update(
749
+ scrape.get_data_df(data_list=data_list).filter(
750
+ (pl.col("pitcher_id") == player_input) &
751
+ (pl.col("is_pitch") == True) &
752
+ (pl.col("start_speed") >= 50) &
753
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
754
+ )
755
+ )
756
+ )
757
+ )).with_columns(
758
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
759
+ )
760
+
761
+ df = df.with_columns(
762
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
763
+ prop=pl.col('is_pitch').sum().over("pitch_type")
764
+ )
765
+
766
+ except TypeError:
767
+ print("NONE")
768
+ return None
769
+
770
+ if df is None:
771
+ fig = plt.figure(figsize=(10, 10))
772
+ fig.text(x=0.1, y=0.9, s='No Statcast Data For This Pitcher', fontsize=24, ha='left')
773
+ return fig
774
+
775
+ df = df.clone()
776
+
777
+ # Create plot
778
  p.set(0.6, "Creating plot...")
779
+ return ploter.final_plot(
 
 
 
780
  df=df,
781
  pitcher_id=player_input,
782
+ plot_picker='short_form_movement',
783
  sport_id=sport_id,
784
+ game_type=[input.type_input()]
785
+ )
786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
 
788
 
789
  app = App(app_ui, server)