Spaces:
Sleeping
Sleeping
Update app.R
Browse files
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
|
| 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
|
| 890 |
# =============================================
|
| 891 |
-
tabPanel("Baserunning",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
|
| 893 |
# =============================================
|
| 894 |
# TAB 8: Team Defensive Positioning
|
|
@@ -916,7 +968,7 @@ app_ui <- fluidPage(
|
|
| 916 |
),
|
| 917 |
|
| 918 |
# =============================================
|
| 919 |
-
# TAB 9: OAA Leaderboard
|
| 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)
|