DaveM2430 commited on
Commit
15854cc
Β·
verified Β·
1 Parent(s): 888674f

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +70 -94
app.R CHANGED
@@ -27,6 +27,12 @@ pitch_colors <- c(
27
  "Cutter" = "#8B4513", "Splitter" = "mediumpurple3"
28
  )
29
 
 
 
 
 
 
 
30
  clean_pitch_type <- function(pt) {
31
  case_when(
32
  pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
@@ -41,7 +47,6 @@ clean_pitch_type <- function(pt) {
41
  )
42
  }
43
 
44
- # ── Pitch type mapper ──────────────────────────────────────────────────────────
45
  map_pitch_type <- function(pt) {
46
  case_when(
47
  pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
@@ -110,7 +115,7 @@ compute_stuff_plus <- function(data) {
110
  summarise(
111
  `Stuff+` = round(weighted.mean(
112
  ((expected_whiff_rate - league_avg) / league_sd) * 10 + 100,
113
- w = rep(1, n()), na.rm=TRUE), 0),
114
  .groups="drop"
115
  )
116
  }
@@ -142,42 +147,17 @@ compute_location_plus <- function(data) {
142
  summarise(
143
  `Location+` = round(weighted.mean(
144
  (-(avg_xRV288 - league_avg) / league_sd) * 10 + 100,
145
- w = rep(1, n()), na.rm=TRUE), 0),
146
  .groups="drop"
147
  )
148
  }
149
 
150
  # ── Pitcher stats ──────────────────────────────────────────────────────────────
151
- build_pitcher_stats <- function(df, min_pitches) {
152
-
153
- format_name <- function(name) {
154
- parts <- strsplit(name, ", ")[[1]]
155
- if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else name
156
- }
157
 
158
  team_lookup <- df %>%
159
  select(Pitcher, PitcherTeam) %>%
160
- distinct(Pitcher, .keep_all = TRUE)
161
-
162
- # compute models upfront outside the pipe
163
- stuff_scores <- tryCatch(
164
- compute_stuff_plus(df),
165
- error = function(e) {
166
- message("Stuff+ failed: ", e$message)
167
- tibble(Pitcher=character(), `Stuff+`=numeric())
168
- }
169
- )
170
-
171
- loc_scores <- tryCatch(
172
- compute_location_plus(df),
173
- error = function(e) {
174
- message("Location+ failed: ", e$message)
175
- tibble(Pitcher=character(), `Location+`=numeric())
176
- }
177
- )
178
-
179
- message("Stuff+ rows: ", nrow(stuff_scores))
180
- message("Location+ rows: ", nrow(loc_scores))
181
 
182
  df %>%
183
  mutate(
@@ -250,21 +230,13 @@ build_pitcher_stats <- function(df, min_pitches) {
250
  round(fb_velo_sum / fb_velo_n, 1)),
251
  `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
252
  `Plot` = paste0('<button class="plot-btn" data-pitcher="',
253
- gsub('"', '&quot;', Pitcher),
254
  '">&#128202; View</button>'),
255
  `Twitter` = paste0('<a href="https://www.google.com/search?q=',
256
- gsub(" ", "+", sapply(Pitcher, function(n) {
257
- parts <- strsplit(n, ", ")[[1]]
258
- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else n
259
- })),
260
- '+baseball+twitter" target="_blank">',
261
- '&#x1F426; Search</a>')
262
- ) %>%
263
- filter(
264
- !is.na(Name),
265
- trimws(Name) != "",
266
- grepl(" ", trimws(Name))
267
  ) %>%
 
268
  select(Name, Level, Team, Pitches, `FB Velo`, `Zone%`, `Strike%`,
269
  `K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
270
  `Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
@@ -316,9 +288,8 @@ ui <- fluidPage(
316
  .plot-btn:hover { background:#003366; }
317
 
318
  table.dataTable tbody td a {
319
- color:#002147; font-weight:600; font-size:12px;
320
- text-decoration:none; background:#e8f0fe; border-radius:4px;
321
- padding:3px 8px; display:inline-block;
322
  }
323
  table.dataTable tbody td a:hover { background:#002147; color:white; }
324
 
@@ -330,16 +301,13 @@ ui <- fluidPage(
330
  box-shadow:0 8px 32px rgba(0,0,0,0.25);
331
  max-height:92vh; overflow-y:auto; }
332
  .modal-close { position:absolute; top:14px; right:18px; font-size:22px;
333
- cursor:pointer; color:#888; background:none; border:none;
334
- line-height:1; }
335
  .modal-close:hover { color:#002147; }
336
- .modal-title { font-size:17px; font-weight:700; color:#002147;
337
- margin:0 0 4px 0; }
338
  .modal-subtitle { font-size:12px; color:#888; margin:0 0 16px 0; }
339
- .section-label { font-size:12px; font-weight:700; color:#002147;
340
- text-transform:uppercase; letter-spacing:0.4px;
341
- margin:16px 0 8px 0; border-top:1px solid #eee;
342
- padding-top:14px; }
343
 
344
  #splash { position:fixed; top:0; left:0; width:100%; height:100%;
345
  background:#002147; display:flex; flex-direction:column;
@@ -375,25 +343,21 @@ ui <- fluidPage(
375
  tags$script(HTML("
376
  $(document).ready(function() {
377
 
378
- // ── splash ────────────────────────────────────────────────────────────
379
  setTimeout(function() {
380
  $('#splash').addClass('fade-out');
381
  setTimeout(function() { $('#splash').remove(); }, 800);
382
  }, 2800);
383
 
384
- // ── plot button β€” delegated so it works after table renders ───────────
385
  $(document).on('click', '.plot-btn', function() {
386
  var pitcher = $(this).data('pitcher');
387
  Shiny.setInputValue('selected_pitcher', pitcher, {priority: 'event'});
388
  });
389
 
390
- // ── close modal ───────────────────────────────────────────────────────
391
  $(document).on('click', '#modal_close_btn', function() {
392
  $('#modal_wrapper').hide();
393
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
394
  });
395
 
396
- // close on backdrop click
397
  $(document).on('click', '#modal_wrapper', function(e) {
398
  if ($(e.target).is('#modal_wrapper')) {
399
  $('#modal_wrapper').hide();
@@ -404,7 +368,7 @@ ui <- fluidPage(
404
  "))
405
  ),
406
 
407
- # ── Splash ───────────────────────────────────────────────────────────────────
408
  tags$div(id="splash",
409
  tags$img(src="Butler-Bulldogs-Logo-1990.png"),
410
  tags$h1("Butler Baseball Transfer Portal"),
@@ -412,7 +376,7 @@ ui <- fluidPage(
412
  tags$div(class="bar-track", tags$div(class="bar-fill"))
413
  ),
414
 
415
- # ── Header ───────────────────────────────────────────────────────────────────
416
  tags$div(class="header",
417
  tags$div(
418
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
@@ -421,7 +385,7 @@ ui <- fluidPage(
421
  tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
422
  ),
423
 
424
- # ── Filters ──────────────────────────────────────────────────────────────────
425
  tags$div(class="filters",
426
  tags$div(
427
  tags$div("Level", class="filter-label"),
@@ -442,18 +406,16 @@ ui <- fluidPage(
442
  )
443
  ),
444
 
445
- # ── Main content ─────────────────────────────────────────────────────────────
446
  tags$div(class="main",
447
  uiOutput("main_ui")
448
  ),
449
 
450
- # ── Modal (hidden until triggered) ───────────────────────────────────────────
451
  tags$div(
452
- id = "modal_wrapper",
453
- class = "modal-overlay",
454
- style = "display:none;",
455
  tags$div(
456
- class = "modal-box",
457
  tags$button("βœ•", id="modal_close_btn", class="modal-close"),
458
  uiOutput("modal_header"),
459
  plotOutput("movement_plot", height="400px"),
@@ -466,25 +428,39 @@ ui <- fluidPage(
466
  # ── Server ─────────────────────────────────────────────────────────────────────
467
  server <- function(input, output, session) {
468
 
469
- # ── Data ────────────────────────��────────────────────────────────────────────
470
  raw_data <- reactive({
471
  req(input$level)
472
- tryCatch(
473
- load_data(input$level),
474
- error = function(e) {
475
- showNotification(paste("Failed to load:", e$message), type="error")
476
- NULL
477
- }
478
- )
 
 
 
 
 
 
 
 
 
 
479
  })
480
 
481
  stats_table <- reactive({
482
  req(raw_data(), input$min_pitches)
483
  if (input$position == "Pitcher")
484
- build_pitcher_stats(raw_data(), input$min_pitches)
 
 
 
 
 
485
  })
486
 
487
- # ── Main UI ──────────────────────────────────────────────────────────────────
488
  output$main_ui <- renderUI({
489
  if (input$position == "Pitcher") {
490
  tagList(
@@ -497,7 +473,6 @@ server <- function(input, output, session) {
497
  }
498
  })
499
 
500
- # ── Pitcher leaderboard table ─────────────────────────────────────────────────
501
  output$pitcher_table <- renderDT({
502
  req(stats_table(), nrow(stats_table()) > 0)
503
  dt <- stats_table()
@@ -510,6 +485,8 @@ server <- function(input, output, session) {
510
  # whiff_range <- c(15, 35)
511
  # chase_range <- c(25, 40)
512
  # hardhit_range <- c(25, 42)
 
 
513
  # ─────────────────────────────────────────────────────────────────────────
514
 
515
  datatable(
@@ -518,16 +495,16 @@ server <- function(input, output, session) {
518
  escape = FALSE,
519
  selection = "none",
520
  options = list(
521
- pageLength = 25,
522
- lengthMenu = c(25, 50, 100),
523
- scrollX = TRUE,
524
- order = list(list(3, "desc")),
525
- orderMulti = FALSE,
526
- dom = "lfrtip",
527
- columnDefs = list(
528
- list(className = "dt-left", targets = 0:1),
529
- list(className = "dt-center", targets = 2:16),
530
- list(orderable = FALSE, targets = c(15, 16))
531
  ),
532
  initComplete = JS("function(settings, json) {
533
  $('#loading_msg').hide();
@@ -536,13 +513,13 @@ server <- function(input, output, session) {
536
  )
537
  })
538
 
539
- # ── Modal content ─────────────────────────────────────────────────────────────
540
  current_pitcher <- reactiveVal(NULL)
541
 
542
  observeEvent(input$selected_pitcher, {
543
  req(input$selected_pitcher, raw_data())
544
  current_pitcher(input$selected_pitcher)
545
- shinyjs::show("modal_wrapper")
546
  })
547
 
548
  observeEvent(input$modal_closed, {
@@ -551,7 +528,7 @@ server <- function(input, output, session) {
551
 
552
  pitcher_plot_data <- reactive({
553
  req(current_pitcher(), raw_data())
554
- raw_data() %>%
555
  filter(Pitcher == current_pitcher()) %>%
556
  mutate(PitchType = clean_pitch_type(TaggedPitchType)) %>%
557
  filter(!is.na(PitchType), !is.na(HorzBreak), !is.na(InducedVertBreak))
@@ -572,7 +549,6 @@ server <- function(input, output, session) {
572
  output$movement_plot <- renderPlot({
573
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
574
  pdata <- pitcher_plot_data()
575
-
576
  parts <- strsplit(current_pitcher(), ", ")[[1]]
577
  display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
578
 
@@ -632,7 +608,7 @@ server <- function(input, output, session) {
632
  group_by(`Pitch Type`=PitchType) %>%
633
  summarise(
634
  `#` = n(),
635
- `Usage%` = round(n() / nrow(pdata) * 100, 1),
636
  Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
637
  iVB = round(mean(InducedVertBreak, na.rm=TRUE), 1),
638
  HB = round(mean(HorzBreak, na.rm=TRUE), 1),
@@ -641,8 +617,8 @@ server <- function(input, output, session) {
641
  RelS = round(mean(RelSide, na.rm=TRUE), 1),
642
  VAA = round(mean(VertApprAngle, na.rm=TRUE), 1),
643
  HAA = round(mean(HorzApprAngle, na.rm=TRUE), 1),
644
- `Zone%` = round(sum(IsZone, na.rm=TRUE)/n()*100, 1),
645
- `Strike%` = round(sum(IsStrike, na.rm=TRUE)/n()*100, 1),
646
  `Whiff%` = round(ifelse(sum(IsSwing)==0, NA,
647
  sum(IsWhiff)/sum(IsSwing)*100), 1),
648
  `Chase%` = round(ifelse(sum(IsOutZone)==0, NA,
 
27
  "Cutter" = "#8B4513", "Splitter" = "mediumpurple3"
28
  )
29
 
30
+ # ── Helpers ────────────────────────────────────────────────────────────────────
31
+ format_name <- function(name) {
32
+ parts <- strsplit(name, ", ")[[1]]
33
+ if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else name
34
+ }
35
+
36
  clean_pitch_type <- function(pt) {
37
  case_when(
38
  pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
 
47
  )
48
  }
49
 
 
50
  map_pitch_type <- function(pt) {
51
  case_when(
52
  pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
 
115
  summarise(
116
  `Stuff+` = round(weighted.mean(
117
  ((expected_whiff_rate - league_avg) / league_sd) * 10 + 100,
118
+ w=rep(1,n()), na.rm=TRUE), 0),
119
  .groups="drop"
120
  )
121
  }
 
147
  summarise(
148
  `Location+` = round(weighted.mean(
149
  (-(avg_xRV288 - league_avg) / league_sd) * 10 + 100,
150
+ w=rep(1,n()), na.rm=TRUE), 0),
151
  .groups="drop"
152
  )
153
  }
154
 
155
  # ── Pitcher stats ──────────────────────────────────────────────────────────────
156
+ build_pitcher_stats <- function(df, min_pitches, stuff_scores, loc_scores) {
 
 
 
 
 
157
 
158
  team_lookup <- df %>%
159
  select(Pitcher, PitcherTeam) %>%
160
+ distinct(Pitcher, .keep_all=TRUE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  df %>%
163
  mutate(
 
230
  round(fb_velo_sum / fb_velo_n, 1)),
231
  `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
232
  `Plot` = paste0('<button class="plot-btn" data-pitcher="',
233
+ gsub('"','&quot;',Pitcher),
234
  '">&#128202; View</button>'),
235
  `Twitter` = paste0('<a href="https://www.google.com/search?q=',
236
+ gsub(" ","+",sapply(Pitcher, format_name)),
237
+ '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
 
 
 
 
 
 
 
 
 
238
  ) %>%
239
+ filter(!is.na(Name), trimws(Name)!="", grepl(" ",trimws(Name))) %>%
240
  select(Name, Level, Team, Pitches, `FB Velo`, `Zone%`, `Strike%`,
241
  `K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
242
  `Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
 
288
  .plot-btn:hover { background:#003366; }
289
 
290
  table.dataTable tbody td a {
291
+ color:#002147; font-weight:600; font-size:12px; text-decoration:none;
292
+ background:#e8f0fe; border-radius:4px; padding:3px 8px; display:inline-block;
 
293
  }
294
  table.dataTable tbody td a:hover { background:#002147; color:white; }
295
 
 
301
  box-shadow:0 8px 32px rgba(0,0,0,0.25);
302
  max-height:92vh; overflow-y:auto; }
303
  .modal-close { position:absolute; top:14px; right:18px; font-size:22px;
304
+ cursor:pointer; color:#888; background:none; border:none; }
 
305
  .modal-close:hover { color:#002147; }
306
+ .modal-title { font-size:17px; font-weight:700; color:#002147; margin:0 0 4px 0; }
 
307
  .modal-subtitle { font-size:12px; color:#888; margin:0 0 16px 0; }
308
+ .section-label { font-size:12px; font-weight:700; color:#002147;
309
+ text-transform:uppercase; letter-spacing:0.4px;
310
+ margin:16px 0 8px 0; border-top:1px solid #eee; padding-top:14px; }
 
311
 
312
  #splash { position:fixed; top:0; left:0; width:100%; height:100%;
313
  background:#002147; display:flex; flex-direction:column;
 
343
  tags$script(HTML("
344
  $(document).ready(function() {
345
 
 
346
  setTimeout(function() {
347
  $('#splash').addClass('fade-out');
348
  setTimeout(function() { $('#splash').remove(); }, 800);
349
  }, 2800);
350
 
 
351
  $(document).on('click', '.plot-btn', function() {
352
  var pitcher = $(this).data('pitcher');
353
  Shiny.setInputValue('selected_pitcher', pitcher, {priority: 'event'});
354
  });
355
 
 
356
  $(document).on('click', '#modal_close_btn', function() {
357
  $('#modal_wrapper').hide();
358
  Shiny.setInputValue('modal_closed', Math.random(), {priority: 'event'});
359
  });
360
 
 
361
  $(document).on('click', '#modal_wrapper', function(e) {
362
  if ($(e.target).is('#modal_wrapper')) {
363
  $('#modal_wrapper').hide();
 
368
  "))
369
  ),
370
 
371
+ # splash
372
  tags$div(id="splash",
373
  tags$img(src="Butler-Bulldogs-Logo-1990.png"),
374
  tags$h1("Butler Baseball Transfer Portal"),
 
376
  tags$div(class="bar-track", tags$div(class="bar-fill"))
377
  ),
378
 
379
+ # header
380
  tags$div(class="header",
381
  tags$div(
382
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
 
385
  tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
386
  ),
387
 
388
+ # filters
389
  tags$div(class="filters",
390
  tags$div(
391
  tags$div("Level", class="filter-label"),
 
406
  )
407
  ),
408
 
409
+ # main
410
  tags$div(class="main",
411
  uiOutput("main_ui")
412
  ),
413
 
414
+ # modal
415
  tags$div(
416
+ id="modal_wrapper", class="modal-overlay", style="display:none;",
 
 
417
  tags$div(
418
+ class="modal-box",
419
  tags$button("βœ•", id="modal_close_btn", class="modal-close"),
420
  uiOutput("modal_header"),
421
  plotOutput("movement_plot", height="400px"),
 
428
  # ── Server ─────────────────────────────────────────────────────────────────────
429
  server <- function(input, output, session) {
430
 
431
+ # load data + compute models once per level change
432
  raw_data <- reactive({
433
  req(input$level)
434
+ tryCatch({
435
+ df <- load_data(input$level)
436
+ list(
437
+ df = df,
438
+ stuff_scores = tryCatch(
439
+ compute_stuff_plus(df),
440
+ error=function(e) tibble(Pitcher=character(), `Stuff+`=numeric())
441
+ ),
442
+ loc_scores = tryCatch(
443
+ compute_location_plus(df),
444
+ error=function(e) tibble(Pitcher=character(), `Location+`=numeric())
445
+ )
446
+ )
447
+ }, error = function(e) {
448
+ showNotification(paste("Failed to load:", e$message), type="error")
449
+ NULL
450
+ })
451
  })
452
 
453
  stats_table <- reactive({
454
  req(raw_data(), input$min_pitches)
455
  if (input$position == "Pitcher")
456
+ build_pitcher_stats(
457
+ raw_data()$df,
458
+ input$min_pitches,
459
+ raw_data()$stuff_scores,
460
+ raw_data()$loc_scores
461
+ )
462
  })
463
 
 
464
  output$main_ui <- renderUI({
465
  if (input$position == "Pitcher") {
466
  tagList(
 
473
  }
474
  })
475
 
 
476
  output$pitcher_table <- renderDT({
477
  req(stats_table(), nrow(stats_table()) > 0)
478
  dt <- stats_table()
 
485
  # whiff_range <- c(15, 35)
486
  # chase_range <- c(25, 40)
487
  # hardhit_range <- c(25, 42)
488
+ # stuff_range <- c(90, 110)
489
+ # loc_range <- c(90, 110)
490
  # ─────────────────────────────────────────────────────────────────────────
491
 
492
  datatable(
 
495
  escape = FALSE,
496
  selection = "none",
497
  options = list(
498
+ pageLength = 25,
499
+ lengthMenu = c(25, 50, 100),
500
+ scrollX = TRUE,
501
+ order = list(list(3, "desc")),
502
+ orderMulti = FALSE,
503
+ dom = "lfrtip",
504
+ columnDefs = list(
505
+ list(className="dt-left", targets=0:1),
506
+ list(className="dt-center", targets=2:16),
507
+ list(orderable=FALSE, targets=c(15,16))
508
  ),
509
  initComplete = JS("function(settings, json) {
510
  $('#loading_msg').hide();
 
513
  )
514
  })
515
 
516
+ # modal
517
  current_pitcher <- reactiveVal(NULL)
518
 
519
  observeEvent(input$selected_pitcher, {
520
  req(input$selected_pitcher, raw_data())
521
  current_pitcher(input$selected_pitcher)
522
+ shinyjs::delay(100, shinyjs::show("modal_wrapper"))
523
  })
524
 
525
  observeEvent(input$modal_closed, {
 
528
 
529
  pitcher_plot_data <- reactive({
530
  req(current_pitcher(), raw_data())
531
+ raw_data()$df %>%
532
  filter(Pitcher == current_pitcher()) %>%
533
  mutate(PitchType = clean_pitch_type(TaggedPitchType)) %>%
534
  filter(!is.na(PitchType), !is.na(HorzBreak), !is.na(InducedVertBreak))
 
549
  output$movement_plot <- renderPlot({
550
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
551
  pdata <- pitcher_plot_data()
 
552
  parts <- strsplit(current_pitcher(), ", ")[[1]]
553
  display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
554
 
 
608
  group_by(`Pitch Type`=PitchType) %>%
609
  summarise(
610
  `#` = n(),
611
+ `Usage%` = round(n()/nrow(pdata)*100, 1),
612
  Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
613
  iVB = round(mean(InducedVertBreak, na.rm=TRUE), 1),
614
  HB = round(mean(HorzBreak, na.rm=TRUE), 1),
 
617
  RelS = round(mean(RelSide, na.rm=TRUE), 1),
618
  VAA = round(mean(VertApprAngle, na.rm=TRUE), 1),
619
  HAA = round(mean(HorzApprAngle, na.rm=TRUE), 1),
620
+ `Zone%` = round(sum(IsZone, na.rm=TRUE)/n()*100, 1),
621
+ `Strike%` = round(sum(IsStrike, na.rm=TRUE)/n()*100, 1),
622
  `Whiff%` = round(ifelse(sum(IsSwing)==0, NA,
623
  sum(IsWhiff)/sum(IsSwing)*100), 1),
624
  `Chase%` = round(ifelse(sum(IsOutZone)==0, NA,