nesticot commited on
Commit
e47526b
·
verified ·
1 Parent(s): e9fc1ea

Update app.py

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