DaveM2430 commited on
Commit
95312bf
·
verified ·
1 Parent(s): 88af693

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +117 -109
app.R CHANGED
@@ -24,12 +24,12 @@ load_portal <- function() {
24
  rename(Pitcher = pitcher, Portal = portal) %>%
25
  distinct(Pitcher, .keep_all = TRUE)
26
  },
27
- error = function(e) {
28
- message("Portal load failed: ", e$message)
29
- tibble(Pitcher=character(), Portal=character())
30
- }
31
- )
32
  }
 
33
  # ── Pitch colors ───────────────────────────────────────────────────────────────
34
  pitch_colors <- c(
35
  "Fastball" = "red", "Sinker" = "orange",
@@ -63,7 +63,7 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
63
 
64
  team_lookup <- df %>%
65
  select(Pitcher, PitcherTeam) %>%
66
- distinct(Pitcher, .keep_all=TRUE)
67
 
68
  df %>%
69
  mutate(
@@ -86,39 +86,39 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
86
  group_by(Pitcher, Level) %>%
87
  summarise(
88
  Pitches = n(),
89
- K = sum(IsK, na.rm=TRUE),
90
- BB = sum(IsBB, na.rm=TRUE),
91
- PA = sum(IsPA, na.rm=TRUE),
92
- zone_n = sum(IsZone, na.rm=TRUE),
93
- strike_n = sum(IsStrike, na.rm=TRUE),
94
- whiff_n = sum(IsWhiff, na.rm=TRUE),
95
- swing_n = sum(IsSwing, na.rm=TRUE),
96
- chase_n = sum(IsChase, na.rm=TRUE),
97
- outzone_n = sum(IsOutZone, na.rm=TRUE),
98
- hh_n = sum(IsHardHit, na.rm=TRUE),
99
  bip_n = sum(!is.na(ExitSpeed) &
100
  !PitchCall %in% c("FoulBall","FoulBallNotFieldable",
101
- "FoulBallFieldable","FoulTip"), na.rm=TRUE),
102
  fb_velo_sum = sum(ifelse(!is.na(RelSpeed) &
103
  TaggedPitchType %in% c("Fastball","FourSeamFastBall",
104
  "Four-Seam","FourSeam",
105
  "Sinker","TwoSeamFastBall",
106
  "OneSeamFastball","Cutter"),
107
- RelSpeed, NA), na.rm=TRUE),
108
  fb_velo_n = sum(!is.na(RelSpeed) &
109
  TaggedPitchType %in% c("Fastball","FourSeamFastBall",
110
  "Four-Seam","FourSeam",
111
  "Sinker","TwoSeamFastBall",
112
  "OneSeamFastball","Cutter"),
113
- na.rm=TRUE),
114
  .groups = "drop"
115
  ) %>%
116
- left_join(team_lookup, by="Pitcher") %>%
117
- left_join(scores %>% select(Pitcher, `Stuff+`, `Location+`), by="Pitcher") %>%
118
  filter(Pitches >= min_pitches) %>%
119
- left_join(portal, by="Pitcher") %>%
120
  mutate(
121
- Portal = ifelse(is.na(portal), "No", portal),
122
  Name = sapply(Pitcher, format_name),
123
  Team = PitcherTeam,
124
  `Zone%` = round(zone_n / Pitches * 100, 1),
@@ -137,13 +137,13 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
137
  round(fb_velo_sum / fb_velo_n, 1)),
138
  `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
139
  `Plot` = paste0('<button class="plot-btn" data-pitcher="',
140
- gsub('"','&quot;',Pitcher),
141
  '">&#128202; View</button>'),
142
  `Twitter` = paste0('<a href="https://www.google.com/search?q=',
143
- gsub(" ","+",sapply(Pitcher, format_name)),
144
  '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
145
  ) %>%
146
- filter(!is.na(Name), trimws(Name)!="", grepl(" ",trimws(Name))) %>%
147
  select(Name, Level, Team, Pitches, Portal, `FB Velo`, `Zone%`, `Strike%`,
148
  `K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
149
  `Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
@@ -199,6 +199,7 @@ ui <- fluidPage(
199
  background:#e8f0fe; border-radius:4px; padding:3px 8px; display:inline-block;
200
  }
201
  table.dataTable tbody td a:hover { background:#002147; color:white; }
 
202
  .portal-yes { color:#00840D; font-weight:700; }
203
  .portal-no { color:#aaa; font-weight:400; }
204
 
@@ -279,57 +280,57 @@ ui <- fluidPage(
279
  ),
280
 
281
  # splash
282
- tags$div(id="splash",
283
- tags$img(src="Butler-Bulldogs-Logo-1990.png"),
284
  tags$h1("Butler Baseball Transfer Portal"),
285
  tags$p("Butler Baseball Analytics"),
286
- tags$div(class="bar-track", tags$div(class="bar-fill"))
287
  ),
288
 
289
  # header
290
- tags$div(class="header",
291
  tags$div(
292
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
293
  tags$p("Butler Baseball Analytics")
294
  ),
295
- tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
296
  ),
297
 
298
  # filters
299
- tags$div(class="filters",
300
  tags$div(
301
- tags$div("Level", class="filter-label"),
302
- selectInput("level", label=NULL,
303
- choices=c("D1","D2","D3","JUCO","NAIA"),
304
- selected="D1", width="160px")
305
  ),
306
  tags$div(
307
- tags$div("Position", class="filter-label"),
308
- selectInput("position", label=NULL,
309
- choices=c("Pitcher","Hitter"),
310
- selected="Pitcher", width="160px")
311
  ),
312
  tags$div(
313
- tags$div("Min Pitches", class="filter-label"),
314
- numericInput("min_pitches", label=NULL,
315
- value=100, min=1, max=2000, step=25, width="160px")
316
  )
317
  ),
318
 
319
  # main
320
- tags$div(class="main",
321
  uiOutput("main_ui")
322
  ),
323
 
324
  # modal
325
  tags$div(
326
- id="modal_wrapper", class="modal-overlay", style="display:none;",
327
  tags$div(
328
- class="modal-box",
329
- tags$button("✕", id="modal_close_btn", class="modal-close"),
330
  uiOutput("modal_header"),
331
- plotOutput("movement_plot", height="400px"),
332
- tags$div("Pitch Specifications", class="section-label"),
333
  DTOutput("pitch_type_table")
334
  )
335
  )
@@ -347,7 +348,7 @@ server <- function(input, output, session) {
347
  portal = load_portal()
348
  )
349
  }, error = function(e) {
350
- showNotification(paste("Failed to load:", e$message), type="error")
351
  NULL
352
  })
353
  })
@@ -355,23 +356,30 @@ server <- function(input, output, session) {
355
  stats_table <- reactive({
356
  req(raw_data(), input$min_pitches)
357
  if (input$position == "Pitcher")
358
- build_pitcher_stats(
359
- raw_data()$df,
360
- input$min_pitches,
361
- raw_data()$scores,
362
- raw_data()$portal
 
 
 
 
 
 
 
363
  )
364
  })
365
 
366
  output$main_ui <- renderUI({
367
  if (input$position == "Pitcher") {
368
  tagList(
369
- tags$p(id="loading_msg", "Loading data...",
370
- style="color:#888; font-size:14px; padding:10px 0;"),
371
  DTOutput("pitcher_table")
372
  )
373
  } else {
374
- tags$p("Hitter view coming soon.", style="color:#888;")
375
  }
376
  })
377
 
@@ -404,9 +412,9 @@ server <- function(input, output, session) {
404
  orderMulti = FALSE,
405
  dom = "lfrtip",
406
  columnDefs = list(
407
- list(className="dt-left", targets=0:1),
408
- list(className="dt-center", targets=2:17),
409
- list(orderable=FALSE, targets=c(16,17))
410
  ),
411
  initComplete = JS("function(settings, json) {
412
  $('#loading_msg').hide();
@@ -443,12 +451,12 @@ server <- function(input, output, session) {
443
  output$modal_header <- renderUI({
444
  req(current_pitcher())
445
  parts <- strsplit(current_pitcher(), ", ")[[1]]
446
- display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
447
  pdata <- pitcher_plot_data()
448
- team <- if (nrow(pdata)>0) pdata$PitcherTeam[1] else ""
449
  tagList(
450
- tags$p(display_name, class="modal-title"),
451
- tags$p(team, class="modal-subtitle")
452
  )
453
  })
454
 
@@ -456,40 +464,40 @@ server <- function(input, output, session) {
456
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
457
  pdata <- pitcher_plot_data()
458
  parts <- strsplit(current_pitcher(), ", ")[[1]]
459
- display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
460
 
461
  avg_data <- pdata %>%
462
  group_by(PitchType) %>%
463
  summarise(
464
- HB = mean(HorzBreak, na.rm=TRUE),
465
- iVB = mean(InducedVertBreak, na.rm=TRUE),
466
- Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
467
- .groups="drop"
468
  )
469
 
470
  ggplot() +
471
- geom_vline(xintercept=0, color="black") +
472
- geom_hline(yintercept=0, color="black") +
473
- geom_point(data=pdata,
474
- aes(x=HorzBreak, y=InducedVertBreak, fill=PitchType),
475
- size=3, alpha=0.6, shape=21, color="black", stroke=0.4) +
476
- geom_point(data=avg_data,
477
- aes(x=HB, y=iVB, color=PitchType),
478
- size=9, alpha=0.9) +
479
- geom_text(data=avg_data,
480
- aes(x=HB, y=iVB, label=Velo),
481
- color="white", size=3, fontface="bold") +
482
- scale_fill_manual(values=pitch_colors, name=NULL, drop=TRUE) +
483
- scale_color_manual(values=pitch_colors, name=NULL, drop=TRUE) +
484
- xlim(-25,25) + ylim(-25,25) +
485
- labs(title=paste(display_name, "— Pitch Movement"),
486
- x="Horizontal Break (in)", y="Induced Vertical Break (in)") +
487
  theme_minimal() +
488
  theme(
489
- plot.title = element_text(hjust=0.5, size=13, face="bold", color="#002147"),
490
  legend.position = "bottom",
491
- legend.text = element_text(size=9),
492
- legend.key.size = unit(0.4,"cm")
493
  )
494
  })
495
 
@@ -511,26 +519,26 @@ server <- function(input, output, session) {
511
  IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
512
  !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
513
  ) %>%
514
- group_by(`Pitch Type`=PitchType) %>%
515
  summarise(
516
  `#` = n(),
517
- `Usage%` = round(n()/nrow(pdata)*100, 1),
518
- Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
519
- iVB = round(mean(InducedVertBreak, na.rm=TRUE), 1),
520
- HB = round(mean(HorzBreak, na.rm=TRUE), 1),
521
- Spin = round(mean(SpinRate, na.rm=TRUE), 0),
522
- RelH = round(mean(RelHeight, na.rm=TRUE), 1),
523
- RelS = round(mean(RelSide, na.rm=TRUE), 1),
524
- VAA = round(mean(VertApprAngle, na.rm=TRUE), 1),
525
- HAA = round(mean(HorzApprAngle, na.rm=TRUE), 1),
526
- `Zone%` = round(sum(IsZone, na.rm=TRUE)/n()*100, 1),
527
- `Strike%` = round(sum(IsStrike, na.rm=TRUE)/n()*100, 1),
528
- `Whiff%` = round(ifelse(sum(IsSwing)==0, NA,
529
- sum(IsWhiff)/sum(IsSwing)*100), 1),
530
- `Chase%` = round(ifelse(sum(IsOutZone)==0, NA,
531
- sum(IsChase)/sum(IsOutZone)*100), 1),
532
- `Hard Hit%` = round(ifelse(sum(!is.na(ExitSpeed))==0, NA,
533
- sum(IsHardHit)/sum(!is.na(ExitSpeed))*100), 1),
534
  .groups = "drop"
535
  ) %>%
536
  arrange(desc(`#`)) %>%
@@ -546,8 +554,8 @@ server <- function(input, output, session) {
546
  scrollX = TRUE,
547
  ordering = TRUE,
548
  columnDefs = list(
549
- list(className="dt-center", targets=1:15),
550
- list(className="dt-left", targets=0)
551
  )
552
  )
553
  )
 
24
  rename(Pitcher = pitcher, Portal = portal) %>%
25
  distinct(Pitcher, .keep_all = TRUE)
26
  },
27
+ error = function(e) {
28
+ message("Portal load failed: ", e$message)
29
+ tibble(Pitcher = character(), Portal = character())
30
+ })
 
31
  }
32
+
33
  # ── Pitch colors ───────────────────────────────────────────────────────────────
34
  pitch_colors <- c(
35
  "Fastball" = "red", "Sinker" = "orange",
 
63
 
64
  team_lookup <- df %>%
65
  select(Pitcher, PitcherTeam) %>%
66
+ distinct(Pitcher, .keep_all = TRUE)
67
 
68
  df %>%
69
  mutate(
 
86
  group_by(Pitcher, Level) %>%
87
  summarise(
88
  Pitches = n(),
89
+ K = sum(IsK, na.rm = TRUE),
90
+ BB = sum(IsBB, na.rm = TRUE),
91
+ PA = sum(IsPA, na.rm = TRUE),
92
+ zone_n = sum(IsZone, na.rm = TRUE),
93
+ strike_n = sum(IsStrike, na.rm = TRUE),
94
+ whiff_n = sum(IsWhiff, na.rm = TRUE),
95
+ swing_n = sum(IsSwing, na.rm = TRUE),
96
+ chase_n = sum(IsChase, na.rm = TRUE),
97
+ outzone_n = sum(IsOutZone, na.rm = TRUE),
98
+ hh_n = sum(IsHardHit, na.rm = TRUE),
99
  bip_n = sum(!is.na(ExitSpeed) &
100
  !PitchCall %in% c("FoulBall","FoulBallNotFieldable",
101
+ "FoulBallFieldable","FoulTip"), na.rm = TRUE),
102
  fb_velo_sum = sum(ifelse(!is.na(RelSpeed) &
103
  TaggedPitchType %in% c("Fastball","FourSeamFastBall",
104
  "Four-Seam","FourSeam",
105
  "Sinker","TwoSeamFastBall",
106
  "OneSeamFastball","Cutter"),
107
+ RelSpeed, NA), na.rm = TRUE),
108
  fb_velo_n = sum(!is.na(RelSpeed) &
109
  TaggedPitchType %in% c("Fastball","FourSeamFastBall",
110
  "Four-Seam","FourSeam",
111
  "Sinker","TwoSeamFastBall",
112
  "OneSeamFastball","Cutter"),
113
+ na.rm = TRUE),
114
  .groups = "drop"
115
  ) %>%
116
+ left_join(team_lookup, by = "Pitcher") %>%
117
+ left_join(scores %>% select(Pitcher, `Stuff+`, `Location+`), by = "Pitcher") %>%
118
  filter(Pitches >= min_pitches) %>%
119
+ left_join(portal %>% select(Pitcher, Portal), by = "Pitcher") %>%
120
  mutate(
121
+ Portal = ifelse(is.na(Portal), "No", as.character(Portal)),
122
  Name = sapply(Pitcher, format_name),
123
  Team = PitcherTeam,
124
  `Zone%` = round(zone_n / Pitches * 100, 1),
 
137
  round(fb_velo_sum / fb_velo_n, 1)),
138
  `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
139
  `Plot` = paste0('<button class="plot-btn" data-pitcher="',
140
+ gsub('"', '&quot;', Pitcher),
141
  '">&#128202; View</button>'),
142
  `Twitter` = paste0('<a href="https://www.google.com/search?q=',
143
+ gsub(" ", "+", sapply(Pitcher, format_name)),
144
  '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
145
  ) %>%
146
+ filter(!is.na(Name), trimws(Name) != "", grepl(" ", trimws(Name))) %>%
147
  select(Name, Level, Team, Pitches, Portal, `FB Velo`, `Zone%`, `Strike%`,
148
  `K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
149
  `Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
 
199
  background:#e8f0fe; border-radius:4px; padding:3px 8px; display:inline-block;
200
  }
201
  table.dataTable tbody td a:hover { background:#002147; color:white; }
202
+
203
  .portal-yes { color:#00840D; font-weight:700; }
204
  .portal-no { color:#aaa; font-weight:400; }
205
 
 
280
  ),
281
 
282
  # splash
283
+ tags$div(id = "splash",
284
+ tags$img(src = "Butler-Bulldogs-Logo-1990.png"),
285
  tags$h1("Butler Baseball Transfer Portal"),
286
  tags$p("Butler Baseball Analytics"),
287
+ tags$div(class = "bar-track", tags$div(class = "bar-fill"))
288
  ),
289
 
290
  # header
291
+ tags$div(class = "header",
292
  tags$div(
293
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
294
  tags$p("Butler Baseball Analytics")
295
  ),
296
+ tags$img(src = "Butler-Bulldogs-Logo-1990.png", height = "52px")
297
  ),
298
 
299
  # filters
300
+ tags$div(class = "filters",
301
  tags$div(
302
+ tags$div("Level", class = "filter-label"),
303
+ selectInput("level", label = NULL,
304
+ choices = c("D1","D2","D3","JUCO","NAIA"),
305
+ selected = "D1", width = "160px")
306
  ),
307
  tags$div(
308
+ tags$div("Position", class = "filter-label"),
309
+ selectInput("position", label = NULL,
310
+ choices = c("Pitcher","Hitter"),
311
+ selected = "Pitcher", width = "160px")
312
  ),
313
  tags$div(
314
+ tags$div("Min Pitches", class = "filter-label"),
315
+ numericInput("min_pitches", label = NULL,
316
+ value = 100, min = 1, max = 2000, step = 25, width = "160px")
317
  )
318
  ),
319
 
320
  # main
321
+ tags$div(class = "main",
322
  uiOutput("main_ui")
323
  ),
324
 
325
  # modal
326
  tags$div(
327
+ id = "modal_wrapper", class = "modal-overlay", style = "display:none;",
328
  tags$div(
329
+ class = "modal-box",
330
+ tags$button("✕", id = "modal_close_btn", class = "modal-close"),
331
  uiOutput("modal_header"),
332
+ plotOutput("movement_plot", height = "400px"),
333
+ tags$div("Pitch Specifications", class = "section-label"),
334
  DTOutput("pitch_type_table")
335
  )
336
  )
 
348
  portal = load_portal()
349
  )
350
  }, error = function(e) {
351
+ showNotification(paste("Failed to load:", e$message), type = "error")
352
  NULL
353
  })
354
  })
 
356
  stats_table <- reactive({
357
  req(raw_data(), input$min_pitches)
358
  if (input$position == "Pitcher")
359
+ tryCatch(
360
+ build_pitcher_stats(
361
+ raw_data()$df,
362
+ input$min_pitches,
363
+ raw_data()$scores,
364
+ raw_data()$portal
365
+ ),
366
+ error = function(e) {
367
+ message("build_pitcher_stats failed: ", e$message)
368
+ showNotification(paste("Stats error:", e$message), type = "error")
369
+ NULL
370
+ }
371
  )
372
  })
373
 
374
  output$main_ui <- renderUI({
375
  if (input$position == "Pitcher") {
376
  tagList(
377
+ tags$p(id = "loading_msg", "Loading data...",
378
+ style = "color:#888; font-size:14px; padding:10px 0;"),
379
  DTOutput("pitcher_table")
380
  )
381
  } else {
382
+ tags$p("Hitter view coming soon.", style = "color:#888;")
383
  }
384
  })
385
 
 
412
  orderMulti = FALSE,
413
  dom = "lfrtip",
414
  columnDefs = list(
415
+ list(className = "dt-left", targets = 0:1),
416
+ list(className = "dt-center", targets = 2:17),
417
+ list(orderable = FALSE, targets = c(16, 17))
418
  ),
419
  initComplete = JS("function(settings, json) {
420
  $('#loading_msg').hide();
 
451
  output$modal_header <- renderUI({
452
  req(current_pitcher())
453
  parts <- strsplit(current_pitcher(), ", ")[[1]]
454
+ display_name <- if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else current_pitcher()
455
  pdata <- pitcher_plot_data()
456
+ team <- if (nrow(pdata) > 0) pdata$PitcherTeam[1] else ""
457
  tagList(
458
+ tags$p(display_name, class = "modal-title"),
459
+ tags$p(team, class = "modal-subtitle")
460
  )
461
  })
462
 
 
464
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
465
  pdata <- pitcher_plot_data()
466
  parts <- strsplit(current_pitcher(), ", ")[[1]]
467
+ display_name <- if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else current_pitcher()
468
 
469
  avg_data <- pdata %>%
470
  group_by(PitchType) %>%
471
  summarise(
472
+ HB = mean(HorzBreak, na.rm = TRUE),
473
+ iVB = mean(InducedVertBreak, na.rm = TRUE),
474
+ Velo = round(mean(RelSpeed, na.rm = TRUE), 1),
475
+ .groups = "drop"
476
  )
477
 
478
  ggplot() +
479
+ geom_vline(xintercept = 0, color = "black") +
480
+ geom_hline(yintercept = 0, color = "black") +
481
+ geom_point(data = pdata,
482
+ aes(x = HorzBreak, y = InducedVertBreak, fill = PitchType),
483
+ size = 3, alpha = 0.6, shape = 21, color = "black", stroke = 0.4) +
484
+ geom_point(data = avg_data,
485
+ aes(x = HB, y = iVB, color = PitchType),
486
+ size = 9, alpha = 0.9) +
487
+ geom_text(data = avg_data,
488
+ aes(x = HB, y = iVB, label = Velo),
489
+ color = "white", size = 3, fontface = "bold") +
490
+ scale_fill_manual(values = pitch_colors, name = NULL, drop = TRUE) +
491
+ scale_color_manual(values = pitch_colors, name = NULL, drop = TRUE) +
492
+ xlim(-25, 25) + ylim(-25, 25) +
493
+ labs(title = paste(display_name, "— Pitch Movement"),
494
+ x = "Horizontal Break (in)", y = "Induced Vertical Break (in)") +
495
  theme_minimal() +
496
  theme(
497
+ plot.title = element_text(hjust = 0.5, size = 13, face = "bold", color = "#002147"),
498
  legend.position = "bottom",
499
+ legend.text = element_text(size = 9),
500
+ legend.key.size = unit(0.4, "cm")
501
  )
502
  })
503
 
 
519
  IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
520
  !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
521
  ) %>%
522
+ group_by(`Pitch Type` = PitchType) %>%
523
  summarise(
524
  `#` = n(),
525
+ `Usage%` = round(n() / nrow(pdata) * 100, 1),
526
+ Velo = round(mean(RelSpeed, na.rm = TRUE), 1),
527
+ iVB = round(mean(InducedVertBreak, na.rm = TRUE), 1),
528
+ HB = round(mean(HorzBreak, na.rm = TRUE), 1),
529
+ Spin = round(mean(SpinRate, na.rm = TRUE), 0),
530
+ RelH = round(mean(RelHeight, na.rm = TRUE), 1),
531
+ RelS = round(mean(RelSide, na.rm = TRUE), 1),
532
+ VAA = round(mean(VertApprAngle, na.rm = TRUE), 1),
533
+ HAA = round(mean(HorzApprAngle, na.rm = TRUE), 1),
534
+ `Zone%` = round(sum(IsZone, na.rm = TRUE) / n() * 100, 1),
535
+ `Strike%` = round(sum(IsStrike, na.rm = TRUE) / n() * 100, 1),
536
+ `Whiff%` = round(ifelse(sum(IsSwing) == 0, NA,
537
+ sum(IsWhiff) / sum(IsSwing) * 100), 1),
538
+ `Chase%` = round(ifelse(sum(IsOutZone) == 0, NA,
539
+ sum(IsChase) / sum(IsOutZone) * 100), 1),
540
+ `Hard Hit%` = round(ifelse(sum(!is.na(ExitSpeed)) == 0, NA,
541
+ sum(IsHardHit) / sum(!is.na(ExitSpeed)) * 100), 1),
542
  .groups = "drop"
543
  ) %>%
544
  arrange(desc(`#`)) %>%
 
554
  scrollX = TRUE,
555
  ordering = TRUE,
556
  columnDefs = list(
557
+ list(className = "dt-center", targets = 1:15),
558
+ list(className = "dt-left", targets = 0)
559
  )
560
  )
561
  )