DaveM2430 commited on
Commit
3d70877
Β·
verified Β·
1 Parent(s): 538fa7c

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +353 -6
app.R CHANGED
@@ -201,6 +201,104 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
201
  arrange(desc(Pitches))
202
  }
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  # ── UI ─────────────────────────────────────────────────────────────────────────
205
  ui <- fluidPage(
206
  useShinyjs(),
@@ -312,22 +410,38 @@ ui <- fluidPage(
312
  setTimeout(function() { $('#splash').remove(); }, 800);
313
  }, 2800);
314
 
 
315
  $(document).on('click', '.plot-btn', function() {
316
  var pitcher = $(this).data('pitcher');
317
  Shiny.setInputValue('selected_pitcher', pitcher, {priority: 'event'});
318
  });
319
-
320
  $(document).on('click', '#modal_close_btn', function() {
321
  $('#modal_wrapper').hide();
322
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
323
  });
324
-
325
  $(document).on('click', '#modal_wrapper', function(e) {
326
  if ($(e.target).is('#modal_wrapper')) {
327
  $('#modal_wrapper').hide();
328
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
329
  }
330
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  });
332
  "))
333
  ),
@@ -390,6 +504,19 @@ ui <- fluidPage(
390
  DTOutput("pitch_type_table")
391
  )
392
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  )
394
 
395
  # ── Server ─────────────────────────────────────────────────────────────────────
@@ -399,9 +526,10 @@ server <- function(input, output, session) {
399
  req(input$level)
400
  tryCatch({
401
  list(
402
- df = load_data(input$level),
403
- scores = load_scores(input$level),
404
- portal = load_portal()
 
405
  )
406
  }, error = function(e) {
407
  showNotification(paste("Failed to load:", e$message), type = "error")
@@ -426,6 +554,20 @@ server <- function(input, output, session) {
426
  showNotification(paste("Stats error:", e$message), type = "error")
427
  NULL
428
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  }
430
  })
431
 
@@ -437,10 +579,47 @@ server <- function(input, output, session) {
437
  DTOutput("pitcher_table")
438
  )
439
  } else {
440
- tags$p("Hitter view coming soon.", style = "color:#888;")
 
 
 
 
441
  }
442
  })
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  output$pitcher_table <- renderDT({
445
  req(stats_table(), nrow(stats_table()) > 0)
446
  dt <- stats_table()
@@ -498,6 +677,174 @@ server <- function(input, output, session) {
498
  shinyjs::hide("modal_wrapper")
499
  })
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  pitcher_plot_data <- reactive({
502
  req(current_pitcher(), raw_data())
503
  raw_data()$df %>%
 
201
  arrange(desc(Pitches))
202
  }
203
 
204
+
205
+ ## Build Hitter Stats
206
+
207
+ build_hitter_stats <- function(df, min_pa, hitter_portal) {
208
+
209
+ team_lookup <- df %>%
210
+ select(Batter, BatterTeam) %>%
211
+ distinct(Batter, .keep_all = TRUE)
212
+
213
+ df %>%
214
+ mutate(
215
+ IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
216
+ "FoulBallNotFieldable","FoulTip","InPlay"),
217
+ IsContact = PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulTip","InPlay"),
218
+ IsWhiff = PitchCall == "StrikeSwinging",
219
+ IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
220
+ abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
221
+ IsChase = IsSwing & !IsZone,
222
+ IsOutZone = !IsZone,
223
+ IsZoneSwing = IsSwing & IsZone,
224
+ IsZoneWhiff = IsWhiff & IsZone,
225
+ IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
226
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
227
+ IsBIP = !is.na(ExitSpeed) &
228
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
229
+ IsGB = IsBIP & TaggedHitType %in% c("Groundball","GroundBall"),
230
+ IsFB = IsBIP & TaggedHitType == "FlyBall",
231
+ IsLD = IsBIP & TaggedHitType == "LineDrive",
232
+ IsHit = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun"),
233
+ IsAB = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun",
234
+ "Out","Error","FieldersChoice"),
235
+ IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
236
+ (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch")),
237
+ IsHR = !is.na(PlayResult) & PlayResult == "HomeRun",
238
+ IsBB = !is.na(KorBB) & KorBB == "Walk",
239
+ IsK = !is.na(KorBB) & KorBB == "Strikeout"
240
+ ) %>%
241
+ group_by(Batter, Level) %>%
242
+ summarise(
243
+ Pitches = n(),
244
+ PA = sum(IsPA, na.rm = TRUE),
245
+ AB = sum(IsAB, na.rm = TRUE),
246
+ H = sum(IsHit, na.rm = TRUE),
247
+ HR = sum(IsHR, na.rm = TRUE),
248
+ BB = sum(IsBB, na.rm = TRUE),
249
+ K = sum(IsK, na.rm = TRUE),
250
+ hh_n = sum(IsHardHit, na.rm = TRUE),
251
+ bip_n = sum(IsBIP, na.rm = TRUE),
252
+ gb_n = sum(IsGB, na.rm = TRUE),
253
+ fb_n = sum(IsFB, na.rm = TRUE),
254
+ ld_n = sum(IsLD, na.rm = TRUE),
255
+ whiff_n = sum(IsWhiff, na.rm = TRUE),
256
+ swing_n = sum(IsSwing, na.rm = TRUE),
257
+ contact_n = sum(IsContact, na.rm = TRUE),
258
+ chase_n = sum(IsChase, na.rm = TRUE),
259
+ outzone_n = sum(IsOutZone, na.rm = TRUE),
260
+ zone_swing_n = sum(IsZoneSwing, na.rm = TRUE),
261
+ zone_whiff_n = sum(IsZoneWhiff, na.rm = TRUE),
262
+ ev_sum = sum(ifelse(IsBIP, ExitSpeed, NA), na.rm = TRUE),
263
+ .groups = "drop"
264
+ ) %>%
265
+ left_join(team_lookup, by = "Batter") %>%
266
+ filter(PA >= min_pa) %>%
267
+ left_join(
268
+ hitter_portal %>% select(Batter, Portal, Position, BatSide, BA, OBP, SLG),
269
+ by = "Batter"
270
+ ) %>%
271
+ mutate(
272
+ Portal = ifelse(is.na(Portal), "No", as.character(Portal)),
273
+ BA = ifelse(Portal == "No", NA_real_, as.numeric(BA)),
274
+ OBP = ifelse(Portal == "No", NA_real_, as.numeric(OBP)),
275
+ SLG = ifelse(Portal == "No", NA_real_, as.numeric(SLG)),
276
+ Name = sapply(Batter, format_name),
277
+ Team = BatterTeam,
278
+ AVG = round(H / pmax(AB, 1), 3),
279
+ `BB%` = round(BB / pmax(PA, 1) * 100, 1),
280
+ `K%` = round(K / pmax(PA, 1) * 100, 1),
281
+ `Swing%` = round(swing_n / Pitches * 100, 1),
282
+ `Contact%` = round(contact_n / pmax(swing_n, 1) * 100, 1),
283
+ `IZ Whiff%` = round(zone_whiff_n / pmax(zone_swing_n, 1) * 100, 1),
284
+ `Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
285
+ `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
286
+ `Avg EV` = round(ev_sum / pmax(bip_n, 1), 1),
287
+ `GB%` = round(gb_n / pmax(bip_n, 1) * 100, 1),
288
+ `FB%` = round(fb_n / pmax(bip_n, 1) * 100, 1),
289
+ `LD%` = round(ld_n / pmax(bip_n, 1) * 100, 1),
290
+ `Twitter` = paste0('<a href="https://www.google.com/search?q=',
291
+ gsub(" ", "+", sapply(Batter, format_name)),
292
+ '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
293
+ ) %>%
294
+ filter(!is.na(Name), trimws(Name) != "", grepl(" ", trimws(Name))) %>%
295
+ select(Name, Level, Team, Portal, Position, BatSide, `Twitter`, PA,
296
+ AVG, OBP, SLG, `BB%`, `K%`, `Swing%`, `Contact%`, `IZ Whiff%`,
297
+ `Chase%`, `Hard Hit%`, `Avg EV`, `GB%`, `FB%`, `LD%`,
298
+ ) %>%
299
+ arrange(desc(PA))
300
+ }
301
+
302
  # ── UI ─────────────────────────────────────────────────────────────────────────
303
  ui <- fluidPage(
304
  useShinyjs(),
 
410
  setTimeout(function() { $('#splash').remove(); }, 800);
411
  }, 2800);
412
 
413
+ // pitcher modal
414
  $(document).on('click', '.plot-btn', function() {
415
  var pitcher = $(this).data('pitcher');
416
  Shiny.setInputValue('selected_pitcher', pitcher, {priority: 'event'});
417
  });
 
418
  $(document).on('click', '#modal_close_btn', function() {
419
  $('#modal_wrapper').hide();
420
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
421
  });
 
422
  $(document).on('click', '#modal_wrapper', function(e) {
423
  if ($(e.target).is('#modal_wrapper')) {
424
  $('#modal_wrapper').hide();
425
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
426
  }
427
  });
428
+
429
+ // hitter modal
430
+ $(document).on('click', '.hitter-name-btn', function() {
431
+ var batter = $(this).data('batter');
432
+ Shiny.setInputValue('selected_batter', batter, {priority: 'event'});
433
+ });
434
+ $(document).on('click', '#hitter_modal_close_btn', function() {
435
+ $('#hitter_modal_wrapper').hide();
436
+ Shiny.setInputValue('hitter_modal_closed', Math.random(), {priority: 'event'});
437
+ });
438
+ $(document).on('click', '#hitter_modal_wrapper', function(e) {
439
+ if ($(e.target).is('#hitter_modal_wrapper')) {
440
+ $('#hitter_modal_wrapper').hide();
441
+ Shiny.setInputValue('hitter_modal_closed', Math.random(), {priority: 'event'});
442
+ }
443
+ });
444
+
445
  });
446
  "))
447
  ),
 
504
  DTOutput("pitch_type_table")
505
  )
506
  )
507
+ # hitter modal
508
+ tags$div(
509
+ id = "hitter_modal_wrapper", class = "modal-overlay", style = "display:none;",
510
+ tags$div(
511
+ class = "modal-box",
512
+ tags$button("βœ•", id = "hitter_modal_close_btn", class = "modal-close"),
513
+ uiOutput("hitter_modal_header"),
514
+ tags$div("vs. Handedness", class = "section-label"),
515
+ DTOutput("hitter_hand_table"),
516
+ tags$div("vs. Pitch Type", class = "section-label"),
517
+ DTOutput("hitter_pitch_table")
518
+ )
519
+ )
520
  )
521
 
522
  # ── Server ─────────────────────────────────────────────────────────────────────
 
526
  req(input$level)
527
  tryCatch({
528
  list(
529
+ df = load_data(input$level),
530
+ scores = load_scores(input$level),
531
+ portal = load_portal(),
532
+ hitter_portal = load_hitter_portal()
533
  )
534
  }, error = function(e) {
535
  showNotification(paste("Failed to load:", e$message), type = "error")
 
554
  showNotification(paste("Stats error:", e$message), type = "error")
555
  NULL
556
  })
557
+ } else {
558
+ tryCatch({
559
+ df <- build_hitter_stats(
560
+ raw_data()$df,
561
+ input$min_pitches,
562
+ raw_data()$hitter_portal
563
+ )
564
+ if (input$portal_only) df <- df %>% filter(Portal == "Yes")
565
+ df
566
+ }, error = function(e) {
567
+ message("build_hitter_stats failed: ", e$message)
568
+ showNotification(paste("Stats error:", e$message), type = "error")
569
+ NULL
570
+ })
571
  }
572
  })
573
 
 
579
  DTOutput("pitcher_table")
580
  )
581
  } else {
582
+ tagList(
583
+ tags$p(id = "loading_msg", "Loading data...",
584
+ style = "color:#888; font-size:14px; padding:10px 0;"),
585
+ DTOutput("hitter_table")
586
+ )
587
  }
588
  })
589
 
590
+ output$hitter_table <- renderDT({
591
+ req(stats_table(), nrow(stats_table()) > 0)
592
+ dt <- stats_table()
593
+
594
+ datatable(
595
+ dt,
596
+ rownames = FALSE,
597
+ escape = FALSE,
598
+ selection = "none",
599
+ options = list(
600
+ pageLength = 25,
601
+ lengthMenu = c(25, 50, 100),
602
+ scrollX = TRUE,
603
+ order = list(list(7, "desc")),
604
+ orderMulti = FALSE,
605
+ dom = "lfrtip",
606
+ columnDefs = list(
607
+ list(className = "dt-left", targets = 0:1),
608
+ list(className = "dt-center", targets = 2:21),
609
+ list(orderable = FALSE, targets = 6)
610
+ ),
611
+ initComplete = JS("function(settings, json) {
612
+ $('#loading_msg').hide();
613
+ }")
614
+ )
615
+ ) %>%
616
+ formatStyle(
617
+ "Portal",
618
+ color = styleEqual(c("Yes","No"), c("#00840D","#aaaaaa")),
619
+ fontWeight = styleEqual(c("Yes","No"), c("700","400"))
620
+ )
621
+ })
622
+
623
  output$pitcher_table <- renderDT({
624
  req(stats_table(), nrow(stats_table()) > 0)
625
  dt <- stats_table()
 
677
  shinyjs::hide("modal_wrapper")
678
  })
679
 
680
+ # ── Hitter modal ─────────────────────────────────────────────────────────────
681
+ current_batter <- reactiveVal(NULL)
682
+
683
+ observeEvent(input$selected_batter, {
684
+ req(input$selected_batter, raw_data())
685
+ current_batter(input$selected_batter)
686
+ shinyjs::delay(100, shinyjs::show("hitter_modal_wrapper"))
687
+ })
688
+
689
+ observeEvent(input$hitter_modal_closed, {
690
+ shinyjs::hide("hitter_modal_wrapper")
691
+ })
692
+
693
+ batter_data <- reactive({
694
+ req(current_batter(), raw_data())
695
+ raw_data()$df %>%
696
+ filter(Batter == current_batter()) %>%
697
+ mutate(
698
+ PitchGroup = case_when(
699
+ TaggedPitchType %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam",
700
+ "Sinker","TwoSeamFastBall","OneSeamFastball","Cutter") ~ "Fastball",
701
+ TaggedPitchType %in% c("Slider","Sweeper","Curveball","CurveBall") ~ "Breaker",
702
+ TaggedPitchType %in% c("Changeup","ChangeUp","Splitter") ~ "Offspeed",
703
+ TRUE ~ NA_character_
704
+ )
705
+ )
706
+ })
707
+
708
+ output$hitter_modal_header <- renderUI({
709
+ req(current_batter())
710
+ parts <- strsplit(current_batter(), ", ")[[1]]
711
+ display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_batter()
712
+ pdata <- batter_data()
713
+ team <- if (nrow(pdata)>0) pdata$BatterTeam[1] else ""
714
+ tagList(
715
+ tags$p(display_name, class = "modal-title"),
716
+ tags$p(team, class = "modal-subtitle")
717
+ )
718
+ })
719
+
720
+ output$hitter_hand_table <- renderDT({
721
+ req(batter_data(), nrow(batter_data()) > 0)
722
+ pdata <- batter_data()
723
+
724
+ hand_table <- pdata %>%
725
+ mutate(
726
+ IsHit = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun"),
727
+ IsAB = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun",
728
+ "Out","Error","FieldersChoice"),
729
+ IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
730
+ (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch")),
731
+ IsHR = !is.na(PlayResult) & PlayResult == "HomeRun",
732
+ IsBB = !is.na(KorBB) & KorBB == "Walk",
733
+ IsK = !is.na(KorBB) & KorBB == "Strikeout",
734
+ IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
735
+ "FoulBallNotFieldable","FoulTip","InPlay"),
736
+ IsWhiff = PitchCall == "StrikeSwinging",
737
+ IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
738
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
739
+ IsBIP = !is.na(ExitSpeed) &
740
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
741
+ ) %>%
742
+ filter(!is.na(PitcherThrows), PitcherThrows != "") %>%
743
+ group_by(Hand = PitcherThrows) %>%
744
+ summarise(
745
+ PA = sum(IsPA, na.rm=TRUE),
746
+ AB = sum(IsAB, na.rm=TRUE),
747
+ H = sum(IsHit, na.rm=TRUE),
748
+ HR = sum(IsHR, na.rm=TRUE),
749
+ BB = sum(IsBB, na.rm=TRUE),
750
+ K = sum(IsK, na.rm=TRUE),
751
+ hh_n = sum(IsHardHit, na.rm=TRUE),
752
+ bip_n = sum(IsBIP, na.rm=TRUE),
753
+ whiff_n = sum(IsWhiff, na.rm=TRUE),
754
+ swing_n = sum(IsSwing, na.rm=TRUE),
755
+ .groups = "drop"
756
+ ) %>%
757
+ mutate(
758
+ AVG = round(H / pmax(AB, 1), 3),
759
+ OBP = round((H + BB) / pmax(PA, 1), 3),
760
+ `K%` = round(K / pmax(PA, 1) * 100, 1),
761
+ `BB%` = round(BB / pmax(PA, 1) * 100, 1),
762
+ `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
763
+ `Whiff%` = round(whiff_n / pmax(swing_n, 1) * 100, 1)
764
+ ) %>%
765
+ select(Hand, PA, AVG, OBP, HR, `BB%`, `K%`, `Hard Hit%`, `Whiff%`)
766
+
767
+ datatable(
768
+ hand_table,
769
+ rownames = FALSE,
770
+ selection = "none",
771
+ options = list(
772
+ pageLength = 5, dom = "t", scrollX = TRUE, ordering = TRUE,
773
+ columnDefs = list(
774
+ list(className = "dt-center", targets = 1:8),
775
+ list(className = "dt-left", targets = 0)
776
+ )
777
+ )
778
+ )
779
+ })
780
+
781
+ output$hitter_pitch_table <- renderDT({
782
+ req(batter_data(), nrow(batter_data()) > 0)
783
+ pdata <- batter_data()
784
+
785
+ pitch_table <- pdata %>%
786
+ mutate(
787
+ IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
788
+ abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
789
+ IsHit = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun"),
790
+ IsAB = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun",
791
+ "Out","Error","FieldersChoice"),
792
+ IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
793
+ (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch")),
794
+ IsHR = !is.na(PlayResult) & PlayResult == "HomeRun",
795
+ IsBB = !is.na(KorBB) & KorBB == "Walk",
796
+ IsK = !is.na(KorBB) & KorBB == "Strikeout",
797
+ IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
798
+ "FoulBallNotFieldable","FoulTip","InPlay"),
799
+ IsWhiff = PitchCall == "StrikeSwinging",
800
+ IsChase = IsSwing & !IsZone,
801
+ IsOutZone = !IsZone,
802
+ IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
803
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
804
+ IsBIP = !is.na(ExitSpeed) &
805
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
806
+ ) %>%
807
+ filter(!is.na(PitchGroup)) %>%
808
+ group_by(`Pitch Type` = PitchGroup) %>%
809
+ summarise(
810
+ Pitches = n(),
811
+ PA = sum(IsPA, na.rm=TRUE),
812
+ AB = sum(IsAB, na.rm=TRUE),
813
+ H = sum(IsHit, na.rm=TRUE),
814
+ HR = sum(IsHR, na.rm=TRUE),
815
+ K = sum(IsK, na.rm=TRUE),
816
+ hh_n = sum(IsHardHit, na.rm=TRUE),
817
+ bip_n = sum(IsBIP, na.rm=TRUE),
818
+ whiff_n = sum(IsWhiff, na.rm=TRUE),
819
+ swing_n = sum(IsSwing, na.rm=TRUE),
820
+ chase_n = sum(IsChase, na.rm=TRUE),
821
+ outzone_n = sum(IsOutZone, na.rm=TRUE),
822
+ .groups = "drop"
823
+ ) %>%
824
+ mutate(
825
+ AVG = round(H / pmax(AB, 1), 3),
826
+ `K%` = round(K / pmax(PA, 1) * 100, 1),
827
+ `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
828
+ `Whiff%` = round(whiff_n / pmax(swing_n, 1) * 100, 1),
829
+ `Chase%` = round(chase_n / pmax(outzone_n,1) * 100, 1)
830
+ ) %>%
831
+ select(`Pitch Type`, Pitches, PA, AVG, HR, `K%`, `Hard Hit%`, `Whiff%`, `Chase%`) %>%
832
+ arrange(desc(Pitches))
833
+
834
+ datatable(
835
+ pitch_table,
836
+ rownames = FALSE,
837
+ selection = "none",
838
+ options = list(
839
+ pageLength = 5, dom = "t", scrollX = TRUE, ordering = TRUE,
840
+ columnDefs = list(
841
+ list(className = "dt-center", targets = 1:8),
842
+ list(className = "dt-left", positions = 0)
843
+ )
844
+ )
845
+ )
846
+ })
847
+
848
  pitcher_plot_data <- reactive({
849
  req(current_pitcher(), raw_data())
850
  raw_data()$df %>%