igroffman commited on
Commit
f092e80
·
verified ·
1 Parent(s): 6b3e631

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +255 -10
app.R CHANGED
@@ -134,7 +134,7 @@ download_private_rds <- function(repo_id, filename, max_retries = 3) {
134
  tmp <- tempfile(fileext = ".rds")
135
  writeBin(content(response, "raw"), tmp)
136
  return(readRDS(tmp))
137
- } else warning(paste("Attempt", attempt, "failed with status:", status_code(response)))
138
  }, error = function(e) {
139
  warning(paste("Attempt", attempt, "failed:", e$message))
140
  if (attempt < max_retries) Sys.sleep(2)
@@ -393,6 +393,26 @@ IF_OAA_26 <- if_oaa_setup(pbp_pos_ncaa_spring26)
393
  # Defensive positioning data
394
  def_pos_data <- pbp_pos_ncaa_spring26
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  # ============================================================
397
  # FIELD DRAWING HELPERS
398
  # ============================================================
@@ -580,7 +600,6 @@ direction_oaa <- function(player, position, oaa_data) {
580
  radar_data[1, dir_levels])
581
  rownames(player_matrix) <- c("max","min",player)
582
 
583
- # Return the data for rendering in renderPlot
584
  list(player_matrix = player_matrix, axis_labels = axis_labels,
585
  player = player, position = position, table = tbl)
586
  }
@@ -886,9 +905,42 @@ app_ui <- fluidPage(
886
  tabPanel("6th Tool Fielding", h4("Coming Soon", style="text-align:center;color:darkcyan;padding:60px;")),
887
 
888
  # =============================================
889
- # TAB 7: Baserunning (placeholder)
890
  # =============================================
891
- tabPanel("Baserunning", h4("Coming Soon", style="text-align:center;color:darkcyan;padding:60px;")),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
892
 
893
  # =============================================
894
  # TAB 8: Team Defensive Positioning
@@ -916,7 +968,7 @@ app_ui <- fluidPage(
916
  ),
917
 
918
  # =============================================
919
- # TAB 9: OAA Leaderboard (placeholder)
920
  # =============================================
921
  tabPanel("OAA Leaderboard",
922
  fluidRow(
@@ -1044,7 +1096,6 @@ server <- function(input, output, session) {
1044
  # OUTFIELD OAA OUTPUTS
1045
  # ============================================================
1046
 
1047
- # Star graph
1048
  output$of_star_graph <- renderPlotly({
1049
  req(input$of_player, input$of_position)
1050
  d <- of_data()
@@ -1052,7 +1103,6 @@ server <- function(input, output, session) {
1052
  star_graph(input$of_player, pos, d)
1053
  })
1054
 
1055
- # Field graph
1056
  of_field_result <- reactive({
1057
  req(input$of_player, input$of_position)
1058
  d <- of_data()
@@ -1068,7 +1118,6 @@ server <- function(input, output, session) {
1068
  of_field_result()[[2]]
1069
  })
1070
 
1071
- # Direction OAA
1072
  of_direction_result <- reactive({
1073
  req(input$of_player, input$of_position)
1074
  d <- of_data()
@@ -1097,7 +1146,6 @@ server <- function(input, output, session) {
1097
  # INFIELD OAA OUTPUTS
1098
  # ============================================================
1099
 
1100
- # Star graph
1101
  output$if_star_graph <- renderPlotly({
1102
  req(input$if_player, input$if_position)
1103
  d <- if_data()
@@ -1105,7 +1153,6 @@ server <- function(input, output, session) {
1105
  infield_star_graph(input$if_player, pos, d)
1106
  })
1107
 
1108
- # Field graph
1109
  if_field_result <- reactive({
1110
  req(input$if_player, input$if_position)
1111
  d <- if_data()
@@ -1207,6 +1254,204 @@ server <- function(input, output, session) {
1207
  tab_style(style = cell_text(color = "white"),
1208
  locations = cells_title(groups = "title"))
1209
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1210
  }
1211
 
1212
  shinyApp(ui, server)
 
134
  tmp <- tempfile(fileext = ".rds")
135
  writeBin(content(response, "raw"), tmp)
136
  return(readRDS(tmp))
137
+ } else warning(paste("Attempt", attempt, "failed:", e$message))
138
  }, error = function(e) {
139
  warning(paste("Attempt", attempt, "failed:", e$message))
140
  if (attempt < max_retries) Sys.sleep(2)
 
393
  # Defensive positioning data
394
  def_pos_data <- pbp_pos_ncaa_spring26
395
 
396
+ # ============================================================
397
+ # LOAD BASERUNNING HUSTLE DATA
398
+ # ============================================================
399
+ baserunning_raw <- read.csv("Coastal_Carolina_Baseball_Hustle__2025_Sheet1_.csv",
400
+ stringsAsFactors = FALSE)
401
+
402
+ baserunning <- baserunning_raw %>%
403
+ mutate(across(where(is.character), str_trim)) %>%
404
+ mutate(
405
+ MaxSpeed = as.numeric(str_remove(Max.Sprint.Speed, " MPH")),
406
+ AvgSpeed = as.numeric(str_remove(Average.Sprint.Speed, " MPH")),
407
+ HustlePct = as.numeric(str_remove(Hustle.., "%")),
408
+ Speed75 = as.numeric(str_remove(X75..Hustle..Speed, " MPH")),
409
+ Speed80 = as.numeric(str_remove(X80..Hustle..Speed, " MPH")),
410
+ Speed90 = as.numeric(str_remove(X90..Hustle..Speed, " MPH")),
411
+ NoHR_Flyout = as.numeric(str_remove(No.Home.Runs.Flyouts., "%"))
412
+ ) %>%
413
+ dplyr::select(Player, MaxSpeed, AvgSpeed, HustlePct, Speed75, Speed80, Speed90, NoHR_Flyout) %>%
414
+ filter(!is.na(Player) & Player != "")
415
+
416
  # ============================================================
417
  # FIELD DRAWING HELPERS
418
  # ============================================================
 
600
  radar_data[1, dir_levels])
601
  rownames(player_matrix) <- c("max","min",player)
602
 
 
603
  list(player_matrix = player_matrix, axis_labels = axis_labels,
604
  player = player, position = position, table = tbl)
605
  }
 
905
  tabPanel("6th Tool Fielding", h4("Coming Soon", style="text-align:center;color:darkcyan;padding:60px;")),
906
 
907
  # =============================================
908
+ # TAB 7: Baserunning
909
  # =============================================
910
+ tabPanel("Baserunning",
911
+ fluidRow(
912
+ column(3,
913
+ selectInput("br_player", "Player:", choices = NULL)),
914
+ column(3,
915
+ selectInput("br_metric", "Rank By:",
916
+ choices = c("Hustle %" = "HustlePct",
917
+ "Max Sprint Speed" = "MaxSpeed",
918
+ "Avg Sprint Speed" = "AvgSpeed",
919
+ "No HR/Flyout %" = "NoHR_Flyout"),
920
+ selected = "HustlePct"))
921
+ ),
922
+ hr(),
923
+ h4("Team Hustle Leaderboard", class = "section-header"),
924
+ gt_output("br_leaderboard"),
925
+ hr(),
926
+ fluidRow(
927
+ column(6,
928
+ h4("Sprint Speed Comparison", class = "section-header"),
929
+ plotlyOutput("br_speed_chart", height = "420px")),
930
+ column(6,
931
+ h4("Hustle % Rankings", class = "section-header"),
932
+ plotlyOutput("br_hustle_chart", height = "420px"))
933
+ ),
934
+ hr(),
935
+ fluidRow(
936
+ column(6,
937
+ h4("Player Speed Profile (Team Percentiles)", class = "section-header"),
938
+ plotOutput("br_radar", height = "420px")),
939
+ column(6,
940
+ h4("Hustle Threshold Breakdown", class = "section-header"),
941
+ plotlyOutput("br_threshold_chart", height = "420px"))
942
+ )
943
+ ),
944
 
945
  # =============================================
946
  # TAB 8: Team Defensive Positioning
 
968
  ),
969
 
970
  # =============================================
971
+ # TAB 9: OAA Leaderboard
972
  # =============================================
973
  tabPanel("OAA Leaderboard",
974
  fluidRow(
 
1096
  # OUTFIELD OAA OUTPUTS
1097
  # ============================================================
1098
 
 
1099
  output$of_star_graph <- renderPlotly({
1100
  req(input$of_player, input$of_position)
1101
  d <- of_data()
 
1103
  star_graph(input$of_player, pos, d)
1104
  })
1105
 
 
1106
  of_field_result <- reactive({
1107
  req(input$of_player, input$of_position)
1108
  d <- of_data()
 
1118
  of_field_result()[[2]]
1119
  })
1120
 
 
1121
  of_direction_result <- reactive({
1122
  req(input$of_player, input$of_position)
1123
  d <- of_data()
 
1146
  # INFIELD OAA OUTPUTS
1147
  # ============================================================
1148
 
 
1149
  output$if_star_graph <- renderPlotly({
1150
  req(input$if_player, input$if_position)
1151
  d <- if_data()
 
1153
  infield_star_graph(input$if_player, pos, d)
1154
  })
1155
 
 
1156
  if_field_result <- reactive({
1157
  req(input$if_player, input$if_position)
1158
  d <- if_data()
 
1254
  tab_style(style = cell_text(color = "white"),
1255
  locations = cells_title(groups = "title"))
1256
  })
1257
+
1258
+ # ============================================================
1259
+ # BASERUNNING TAB
1260
+ # ============================================================
1261
+
1262
+ # Populate player dropdown
1263
+ observeEvent(authed(), {
1264
+ req(isTRUE(authed()))
1265
+ updateSelectInput(session, "br_player",
1266
+ choices = sort(baserunning$Player),
1267
+ selected = baserunning$Player[1])
1268
+ }, ignoreInit = TRUE)
1269
+
1270
+ # ---------- Hustle Leaderboard GT Table ----------
1271
+ output$br_leaderboard <- render_gt({
1272
+ req(isTRUE(authed()), input$br_metric)
1273
+
1274
+ baserunning %>%
1275
+ arrange(desc(.data[[input$br_metric]])) %>%
1276
+ mutate(Rank = row_number()) %>%
1277
+ dplyr::select(Rank, Player,
1278
+ `Max Speed (MPH)` = MaxSpeed,
1279
+ `Avg Speed (MPH)` = AvgSpeed,
1280
+ `Hustle %` = HustlePct,
1281
+ `75% Threshold` = Speed75,
1282
+ `80% Threshold` = Speed80,
1283
+ `90% Threshold` = Speed90,
1284
+ `No HR/Flyout %` = NoHR_Flyout) %>%
1285
+ gt() %>%
1286
+ gt_theme_guardian() %>%
1287
+ tab_header(title = "Baserunning Hustle Leaderboard") %>%
1288
+ fmt_number(columns = c(`Max Speed (MPH)`, `Avg Speed (MPH)`,
1289
+ `75% Threshold`, `80% Threshold`, `90% Threshold`),
1290
+ decimals = 2) %>%
1291
+ fmt_number(columns = c(`Hustle %`, `No HR/Flyout %`), decimals = 2) %>%
1292
+ cols_align(align = "center", columns = everything()) %>%
1293
+ cols_align(align = "left", columns = Player) %>%
1294
+ data_color(columns = `Hustle %`,
1295
+ palette = c("white", "#008b8b"), domain = c(80, 95)) %>%
1296
+ data_color(columns = `Max Speed (MPH)`,
1297
+ palette = c("white", "#cd853f"), domain = c(18, 21)) %>%
1298
+ data_color(columns = `No HR/Flyout %`,
1299
+ palette = c("white", "#008b8b"), domain = c(85, 100)) %>%
1300
+ data_color(columns = `Avg Speed (MPH)`,
1301
+ palette = c("white", "#20b2aa"), domain = c(15, 19)) %>%
1302
+ tab_options(
1303
+ heading.background.color = "darkcyan",
1304
+ column_labels.background.color = "darkcyan",
1305
+ table.border.top.color = "peru",
1306
+ table.border.bottom.color = "peru") %>%
1307
+ tab_style(style = cell_text(color = "white"),
1308
+ locations = cells_title(groups = "title"))
1309
+ })
1310
+
1311
+ # ---------- Sprint Speed Grouped Bar Chart ----------
1312
+ output$br_speed_chart <- renderPlotly({
1313
+ req(isTRUE(authed()))
1314
+
1315
+ d <- baserunning %>%
1316
+ arrange(desc(MaxSpeed)) %>%
1317
+ mutate(Player = factor(Player, levels = rev(Player))) %>%
1318
+ pivot_longer(cols = c(MaxSpeed, AvgSpeed),
1319
+ names_to = "Type", values_to = "Speed") %>%
1320
+ mutate(Type = ifelse(Type == "MaxSpeed",
1321
+ "Max Sprint Speed", "Avg Sprint Speed"))
1322
+
1323
+ p <- ggplot(d, aes(x = Player, y = Speed, fill = Type)) +
1324
+ geom_col(position = position_dodge(width = 0.7),
1325
+ width = 0.6, color = "black", size = 0.3) +
1326
+ scale_fill_manual(values = c("Max Sprint Speed" = "darkcyan",
1327
+ "Avg Sprint Speed" = "peru")) +
1328
+ coord_flip() +
1329
+ labs(x = NULL, y = "Speed (MPH)", fill = NULL) +
1330
+ theme_minimal() +
1331
+ theme(legend.position = "top",
1332
+ panel.grid.major.y = element_blank(),
1333
+ plot.background = element_rect(fill = "transparent", color = NA))
1334
+
1335
+ ggplotly(p, tooltip = c("y", "fill")) %>%
1336
+ layout(legend = list(orientation = "h", x = 0.15, y = 1.08))
1337
+ })
1338
+
1339
+ # ---------- Hustle % Lollipop Chart ----------
1340
+ output$br_hustle_chart <- renderPlotly({
1341
+ req(isTRUE(authed()))
1342
+
1343
+ d <- baserunning %>%
1344
+ arrange(HustlePct) %>%
1345
+ mutate(Player = factor(Player, levels = Player))
1346
+
1347
+ p <- ggplot(d, aes(x = Player, y = HustlePct)) +
1348
+ geom_segment(aes(xend = Player, y = 75, yend = HustlePct),
1349
+ color = "darkcyan", size = 1.2) +
1350
+ geom_point(size = 4, color = "peru") +
1351
+ geom_text(aes(label = paste0(round(HustlePct, 1), "%")),
1352
+ hjust = -0.3, size = 3, color = "darkcyan", fontface = "bold") +
1353
+ coord_flip() +
1354
+ scale_y_continuous(limits = c(75, 100),
1355
+ labels = function(x) paste0(x, "%")) +
1356
+ labs(x = NULL, y = "Hustle %") +
1357
+ theme_minimal() +
1358
+ theme(panel.grid.major.y = element_blank(),
1359
+ plot.background = element_rect(fill = "transparent", color = NA))
1360
+
1361
+ ggplotly(p, tooltip = c("y")) %>%
1362
+ layout(showlegend = FALSE)
1363
+ })
1364
+
1365
+ # ---------- Player Speed Radar Chart ----------
1366
+ output$br_radar <- renderPlot({
1367
+ req(isTRUE(authed()), input$br_player)
1368
+
1369
+ # Percentile-rank each metric within the team
1370
+ pct_data <- baserunning %>%
1371
+ mutate(across(c(MaxSpeed, AvgSpeed, HustlePct, Speed75,
1372
+ Speed80, Speed90, NoHR_Flyout),
1373
+ ~ percent_rank(.) * 100,
1374
+ .names = "pct_{.col}"))
1375
+
1376
+ player_row <- pct_data %>% filter(Player == input$br_player)
1377
+ if (nrow(player_row) == 0) return(NULL)
1378
+
1379
+ radar_cols <- c("pct_MaxSpeed", "pct_AvgSpeed", "pct_HustlePct",
1380
+ "pct_Speed75", "pct_Speed80", "pct_Speed90", "pct_NoHR_Flyout")
1381
+ labels <- c("Max Speed", "Avg Speed", "Hustle %",
1382
+ "75% Threshold", "80% Threshold", "90% Threshold", "No HR/Flyout %")
1383
+
1384
+ radar_matrix <- rbind(
1385
+ max = rep(100, length(radar_cols)),
1386
+ min = rep(0, length(radar_cols)),
1387
+ player_row[, radar_cols]
1388
+ )
1389
+ colnames(radar_matrix) <- labels
1390
+
1391
+ par(mar = c(1, 2, 3, 2))
1392
+ radarchart(as.data.frame(radar_matrix),
1393
+ axistype = 1,
1394
+ pcol = "darkcyan",
1395
+ pfcol = scales::alpha("darkcyan", 0.3),
1396
+ plwd = 3, plty = 1,
1397
+ cglcol = "grey75", cglty = 1, cglwd = 0.8,
1398
+ vlcex = 0.85, seg = 4,
1399
+ caxislabels = c("0th", "25th", "50th", "75th", "100th"))
1400
+ title(paste0(input$br_player, " \u2014 Speed & Hustle Profile"),
1401
+ cex.main = 1.3, col.main = "darkcyan")
1402
+ })
1403
+
1404
+ # ---------- Hustle Threshold Stacked Bar Chart ----------
1405
+ output$br_threshold_chart <- renderPlotly({
1406
+ req(isTRUE(authed()), input$br_player)
1407
+
1408
+ d <- baserunning %>%
1409
+ filter(Player == input$br_player) %>%
1410
+ pivot_longer(cols = c(Speed75, Speed80, Speed90),
1411
+ names_to = "Threshold", values_to = "Speed") %>%
1412
+ mutate(Threshold = case_when(
1413
+ Threshold == "Speed75" ~ "75% Hustle",
1414
+ Threshold == "Speed80" ~ "80% Hustle",
1415
+ Threshold == "Speed90" ~ "90% Hustle"),
1416
+ Threshold = factor(Threshold, levels = c("75% Hustle", "80% Hustle", "90% Hustle")))
1417
+
1418
+ # Add team avg for comparison
1419
+ team_avg <- baserunning %>%
1420
+ summarize(Speed75 = mean(Speed75, na.rm = TRUE),
1421
+ Speed80 = mean(Speed80, na.rm = TRUE),
1422
+ Speed90 = mean(Speed90, na.rm = TRUE)) %>%
1423
+ pivot_longer(cols = everything(),
1424
+ names_to = "Threshold", values_to = "TeamAvg") %>%
1425
+ mutate(Threshold = case_when(
1426
+ Threshold == "Speed75" ~ "75% Hustle",
1427
+ Threshold == "Speed80" ~ "80% Hustle",
1428
+ Threshold == "Speed90" ~ "90% Hustle"),
1429
+ Threshold = factor(Threshold, levels = c("75% Hustle", "80% Hustle", "90% Hustle")))
1430
+
1431
+ d <- left_join(d, team_avg, by = "Threshold")
1432
+
1433
+ p <- ggplot(d, aes(x = Threshold)) +
1434
+ geom_col(aes(y = Speed, fill = "Player"),
1435
+ width = 0.5, color = "black", size = 0.3) +
1436
+ geom_point(aes(y = TeamAvg, color = "Team Avg"),
1437
+ size = 5, shape = 18) +
1438
+ geom_text(aes(y = Speed, label = paste0(round(Speed, 2), " MPH")),
1439
+ vjust = -0.5, size = 3.5, fontface = "bold", color = "darkcyan") +
1440
+ scale_fill_manual(values = c("Player" = "darkcyan"), name = NULL) +
1441
+ scale_color_manual(values = c("Team Avg" = "peru"), name = NULL) +
1442
+ scale_y_continuous(limits = c(0, max(baserunning$Speed90, na.rm = TRUE) + 2),
1443
+ labels = function(x) paste0(x, " MPH")) +
1444
+ labs(x = NULL, y = "Speed (MPH)",
1445
+ title = paste0(input$br_player, " vs. Team Average")) +
1446
+ theme_minimal() +
1447
+ theme(legend.position = "top",
1448
+ plot.title = element_text(hjust = 0.5, color = "darkcyan", face = "bold"),
1449
+ panel.grid.major.x = element_blank(),
1450
+ plot.background = element_rect(fill = "transparent", color = NA))
1451
+
1452
+ ggplotly(p, tooltip = c("y")) %>%
1453
+ layout(legend = list(orientation = "h", x = 0.25, y = 1.08))
1454
+ })
1455
  }
1456
 
1457
  shinyApp(ui, server)