Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -8,10 +8,11 @@ library(webshot2)
|
|
| 8 |
library(htmlwidgets)
|
| 9 |
|
| 10 |
ui <- dashboardPage(
|
| 11 |
-
dashboardHeader(title = "
|
| 12 |
dashboardSidebar(
|
| 13 |
sidebarMenu(
|
| 14 |
menuItem("Bullpen Session", tabName = "bullpen", icon = icon("baseball")),
|
|
|
|
| 15 |
menuItem("Leaderboard", tabName = "leaderboard", icon = icon("chart-bar")),
|
| 16 |
menuItem("Export Data", tabName = "export", icon = icon("download"))
|
| 17 |
)
|
|
@@ -52,6 +53,7 @@ ui <- dashboardPage(
|
|
| 52 |
),
|
| 53 |
|
| 54 |
tabItems(
|
|
|
|
| 55 |
tabItem(
|
| 56 |
tabName = "bullpen",
|
| 57 |
fluidRow(
|
|
@@ -95,7 +97,7 @@ ui <- dashboardPage(
|
|
| 95 |
),
|
| 96 |
|
| 97 |
hr(),
|
| 98 |
-
h4("2. Intended Location
|
| 99 |
p("Select vertical OR horizontal OR both", style = "font-size: 12px; color: #666;"),
|
| 100 |
fluidRow(
|
| 101 |
column(6, actionButton("int_up", "UP", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")),
|
|
@@ -108,41 +110,12 @@ ui <- dashboardPage(
|
|
| 108 |
),
|
| 109 |
|
| 110 |
hr(),
|
| 111 |
-
h4("3.
|
| 112 |
-
div(
|
| 113 |
-
style = "text-align: center; max-width: 500px; margin: 0 auto;",
|
| 114 |
-
actionButton("zone_12", "12", class = "border-zone-btn btn-outline-secondary", style = "width: 66%; margin: 5px auto;"),
|
| 115 |
-
div(
|
| 116 |
-
style = "display: flex; gap: 5px; justify-content: center; align-items: center;",
|
| 117 |
-
actionButton("zone_14", "14", class = "border-zone-btn btn-outline-secondary", style = "width: 60px; height: 280px;"),
|
| 118 |
-
div(
|
| 119 |
-
class = "zone-grid",
|
| 120 |
-
actionButton("zone_3", "3", class = "zone-btn btn-primary"),
|
| 121 |
-
actionButton("zone_2", "2", class = "zone-btn btn-primary"),
|
| 122 |
-
actionButton("zone_1", "1", class = "zone-btn btn-primary"),
|
| 123 |
-
actionButton("zone_6", "6", class = "zone-btn btn-primary"),
|
| 124 |
-
actionButton("zone_5", "5", class = "zone-btn btn-success"),
|
| 125 |
-
actionButton("zone_4", "4", class = "zone-btn btn-primary"),
|
| 126 |
-
actionButton("zone_9", "9", class = "zone-btn btn-info"),
|
| 127 |
-
actionButton("zone_8", "8", class = "zone-btn btn-info"),
|
| 128 |
-
actionButton("zone_7", "7", class = "zone-btn btn-info")
|
| 129 |
-
),
|
| 130 |
-
actionButton("zone_11", "11", class = "border-zone-btn btn-outline-secondary", style = "width: 60px; height: 280px;")
|
| 131 |
-
),
|
| 132 |
-
actionButton("zone_13", "13", class = "border-zone-btn btn-outline-secondary", style = "width: 66%; margin: 5px auto;")
|
| 133 |
-
),
|
| 134 |
-
|
| 135 |
-
hr(),
|
| 136 |
-
h4("4. Result"),
|
| 137 |
fluidRow(
|
| 138 |
column(6, actionButton("result_strike", "STRIKE", class = "btn-success", style = "width: 100%; min-height: 80px; font-size: 24px;")),
|
| 139 |
column(6, actionButton("result_ball", "BALL", class = "btn-danger", style = "width: 100%; min-height: 80px; font-size: 24px;"))
|
| 140 |
),
|
| 141 |
|
| 142 |
-
hr(),
|
| 143 |
-
h4("5. Velocity (Required)"),
|
| 144 |
-
numericInput("velo", "MPH:", value = NULL, min = 40, max = 110, width = "200px"),
|
| 145 |
-
|
| 146 |
hr(),
|
| 147 |
actionButton("clearPitch", "Clear Selection", class = "btn-warning btn-block")
|
| 148 |
)
|
|
@@ -159,6 +132,81 @@ ui <- dashboardPage(
|
|
| 159 |
)
|
| 160 |
),
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
tabItem(
|
| 163 |
tabName = "leaderboard",
|
| 164 |
fluidRow(
|
|
@@ -194,12 +242,7 @@ ui <- dashboardPage(
|
|
| 194 |
"Total Pitches" = "Total",
|
| 195 |
"Overall Strike %" = "StrikeRate",
|
| 196 |
"FB Strike %" = "FB_StrikeRate",
|
| 197 |
-
"OS Strike %" = "OS_StrikeRate"
|
| 198 |
-
"Avg FB Velo" = "AvgFBVelo",
|
| 199 |
-
"Max FB Velo" = "MaxFBVelo",
|
| 200 |
-
"Avg Velo" = "AvgVelo",
|
| 201 |
-
"Hit Spot %" = "HitSpotRate",
|
| 202 |
-
"In Zone %" = "InZoneRate"
|
| 203 |
),
|
| 204 |
selected = c("Pitcher", "FB_StrikeRate", "OS_StrikeRate", "StrikeRate"),
|
| 205 |
inline = TRUE
|
|
@@ -221,17 +264,27 @@ ui <- dashboardPage(
|
|
| 221 |
)
|
| 222 |
),
|
| 223 |
|
|
|
|
| 224 |
tabItem(
|
| 225 |
tabName = "export",
|
| 226 |
fluidRow(
|
| 227 |
box(
|
| 228 |
-
title = "Export
|
| 229 |
status = "success",
|
| 230 |
solidHeader = TRUE,
|
| 231 |
-
width =
|
| 232 |
-
textInput("exportFilename", "Session Name
|
| 233 |
placeholder = "LastName1_LastName2_Bullpen"),
|
| 234 |
-
downloadButton("downloadData", "Download CSV", class = "btn-primary btn-lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
)
|
| 236 |
)
|
| 237 |
)
|
|
@@ -248,9 +301,15 @@ server <- function(input, output, session) {
|
|
| 248 |
currentPitchType = NULL,
|
| 249 |
currentIntendedVert = NULL,
|
| 250 |
currentIntendedHorz = NULL,
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
)
|
| 253 |
|
|
|
|
| 254 |
output$selectionDisplay <- renderUI({
|
| 255 |
parts <- c()
|
| 256 |
if (!is.null(values$currentPitchType)) {
|
|
@@ -260,12 +319,6 @@ server <- function(input, output, session) {
|
|
| 260 |
loc <- paste(c(values$currentIntendedVert, values$currentIntendedHorz), collapse = " + ")
|
| 261 |
parts <- c(parts, paste("Intended:", loc))
|
| 262 |
}
|
| 263 |
-
if (!is.null(values$currentZone)) {
|
| 264 |
-
parts <- c(parts, paste("Zone:", values$currentZone))
|
| 265 |
-
}
|
| 266 |
-
if (!is.null(input$velo) && !is.na(input$velo)) {
|
| 267 |
-
parts <- c(parts, paste(input$velo, "MPH"))
|
| 268 |
-
}
|
| 269 |
|
| 270 |
if (length(parts) == 0) {
|
| 271 |
HTML("<span style='color: #999;'>Select pitch details...</span>")
|
|
@@ -304,17 +357,12 @@ server <- function(input, output, session) {
|
|
| 304 |
os_strikes <- sum(os_data$Result == "Strike")
|
| 305 |
os_pct <- if (os_total > 0) round(os_strikes / os_total * 100, 1) else 0
|
| 306 |
|
| 307 |
-
hit_spot <- sum(data$HitSpot, na.rm = TRUE)
|
| 308 |
-
hit_spot_pct <- round(hit_spot / total * 100, 1)
|
| 309 |
-
|
| 310 |
paste0(
|
| 311 |
"Total Pitches: ", total, "\n",
|
| 312 |
"==================\n",
|
| 313 |
"Overall Strike%: ", strike_pct, "%\n",
|
| 314 |
"FB Strike%: ", fb_pct, "% (", fb_total, " FB)\n",
|
| 315 |
-
"OS Strike%: ", os_pct, "% (", os_total, " OS)
|
| 316 |
-
"==================\n",
|
| 317 |
-
"Hit Spot%: ", hit_spot_pct, "% (", hit_spot, "/", total, ")"
|
| 318 |
)
|
| 319 |
})
|
| 320 |
|
|
@@ -332,85 +380,18 @@ server <- function(input, output, session) {
|
|
| 332 |
observeEvent(input$int_middle, { values$currentIntendedHorz <- "Middle" })
|
| 333 |
observeEvent(input$int_in, { values$currentIntendedHorz <- "In" })
|
| 334 |
|
| 335 |
-
observeEvent(input$zone_1, { values$currentZone <- 1 })
|
| 336 |
-
observeEvent(input$zone_2, { values$currentZone <- 2 })
|
| 337 |
-
observeEvent(input$zone_3, { values$currentZone <- 3 })
|
| 338 |
-
observeEvent(input$zone_4, { values$currentZone <- 4 })
|
| 339 |
-
observeEvent(input$zone_5, { values$currentZone <- 5 })
|
| 340 |
-
observeEvent(input$zone_6, { values$currentZone <- 6 })
|
| 341 |
-
observeEvent(input$zone_7, { values$currentZone <- 7 })
|
| 342 |
-
observeEvent(input$zone_8, { values$currentZone <- 8 })
|
| 343 |
-
observeEvent(input$zone_9, { values$currentZone <- 9 })
|
| 344 |
-
observeEvent(input$zone_11, { values$currentZone <- 11 })
|
| 345 |
-
observeEvent(input$zone_12, { values$currentZone <- 12 })
|
| 346 |
-
observeEvent(input$zone_13, { values$currentZone <- 13 })
|
| 347 |
-
observeEvent(input$zone_14, { values$currentZone <- 14 })
|
| 348 |
-
|
| 349 |
recordPitch <- function(result) {
|
| 350 |
-
req(input$currentPitcher, values$currentPitchType
|
| 351 |
-
|
| 352 |
-
if (is.null(input$velo) || is.na(input$velo)) {
|
| 353 |
-
showNotification("Please enter velocity!", type = "error")
|
| 354 |
-
return()
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
batter_side <- input$batterSide
|
| 358 |
-
zone <- values$currentZone
|
| 359 |
-
|
| 360 |
-
hit_spot <- NA
|
| 361 |
-
|
| 362 |
-
if (!is.null(values$currentIntendedVert) || !is.null(values$currentIntendedHorz)) {
|
| 363 |
-
vert_match <- FALSE
|
| 364 |
-
horz_match <- FALSE
|
| 365 |
-
|
| 366 |
-
if (!is.null(values$currentIntendedVert)) {
|
| 367 |
-
if (values$currentIntendedVert == "Up" && zone %in% c(1, 2, 3, 12)) {
|
| 368 |
-
vert_match <- TRUE
|
| 369 |
-
} else if (values$currentIntendedVert == "Down" && zone %in% c(7, 8, 9, 13)) {
|
| 370 |
-
vert_match <- TRUE
|
| 371 |
-
}
|
| 372 |
-
} else {
|
| 373 |
-
vert_match <- TRUE
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
if (!is.null(values$currentIntendedHorz)) {
|
| 377 |
-
if (batter_side == "R") {
|
| 378 |
-
if (values$currentIntendedHorz == "Away" && zone %in% c(3, 6, 9, 14)) {
|
| 379 |
-
horz_match <- TRUE
|
| 380 |
-
} else if (values$currentIntendedHorz == "In" && zone %in% c(1, 4, 7, 11)) {
|
| 381 |
-
horz_match <- TRUE
|
| 382 |
-
} else if (values$currentIntendedHorz == "Middle" && zone %in% c(2, 5, 8)) {
|
| 383 |
-
horz_match <- TRUE
|
| 384 |
-
}
|
| 385 |
-
} else {
|
| 386 |
-
if (values$currentIntendedHorz == "Away" && zone %in% c(1, 4, 7, 11)) {
|
| 387 |
-
horz_match <- TRUE
|
| 388 |
-
} else if (values$currentIntendedHorz == "In" && zone %in% c(3, 6, 9, 14)) {
|
| 389 |
-
horz_match <- TRUE
|
| 390 |
-
} else if (values$currentIntendedHorz == "Middle" && zone %in% c(2, 5, 8)) {
|
| 391 |
-
horz_match <- TRUE
|
| 392 |
-
}
|
| 393 |
-
}
|
| 394 |
-
} else {
|
| 395 |
-
horz_match <- TRUE
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
hit_spot <- vert_match && horz_match
|
| 399 |
-
}
|
| 400 |
|
| 401 |
-
|
|
|
|
| 402 |
|
| 403 |
new_pitch <- data.frame(
|
| 404 |
Pitcher = input$currentPitcher,
|
| 405 |
PitchType = values$currentPitchType,
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
IntendedVert = if (!is.null(values$currentIntendedVert)) values$currentIntendedVert else NA,
|
| 409 |
-
IntendedHorz = if (!is.null(values$currentIntendedHorz)) values$currentIntendedHorz else NA,
|
| 410 |
-
ActualZone = values$currentZone,
|
| 411 |
Result = result,
|
| 412 |
-
HitSpot = hit_spot,
|
| 413 |
-
InZone = in_zone,
|
| 414 |
Timestamp = Sys.time(),
|
| 415 |
stringsAsFactors = FALSE
|
| 416 |
)
|
|
@@ -420,8 +401,6 @@ server <- function(input, output, session) {
|
|
| 420 |
values$currentPitchType <- NULL
|
| 421 |
values$currentIntendedVert <- NULL
|
| 422 |
values$currentIntendedHorz <- NULL
|
| 423 |
-
values$currentZone <- NULL
|
| 424 |
-
updateNumericInput(session, "velo", value = NA)
|
| 425 |
|
| 426 |
showNotification(paste(result, "recorded!"), type = "message", duration = 1)
|
| 427 |
}
|
|
@@ -433,10 +412,127 @@ server <- function(input, output, session) {
|
|
| 433 |
values$currentPitchType <- NULL
|
| 434 |
values$currentIntendedVert <- NULL
|
| 435 |
values$currentIntendedHorz <- NULL
|
| 436 |
-
values$currentZone <- NULL
|
| 437 |
-
updateNumericInput(session, "velo", value = NA)
|
| 438 |
})
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
observeEvent(input$uploadCSV, {
|
| 441 |
req(input$uploadCSV)
|
| 442 |
|
|
@@ -460,20 +556,6 @@ server <- function(input, output, session) {
|
|
| 460 |
showNotification("Cleared uploaded data", type = "warning")
|
| 461 |
})
|
| 462 |
|
| 463 |
-
output$recentPitches <- DT::renderDataTable({
|
| 464 |
-
if (nrow(values$pitchData) == 0) {
|
| 465 |
-
return(data.frame(Message = "No pitches recorded"))
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
recent <- tail(values$pitchData, 20) %>%
|
| 469 |
-
arrange(desc(Timestamp)) %>%
|
| 470 |
-
select(Pitcher, PitchType, Velocity, BatterSide, IntendedVert, IntendedHorz, ActualZone, Result, HitSpot)
|
| 471 |
-
|
| 472 |
-
DT::datatable(recent, options = list(pageLength = 20, dom = 't'), rownames = FALSE) %>%
|
| 473 |
-
formatStyle('HitSpot', backgroundColor = styleEqual(c(TRUE, FALSE), c('#90EE90', '#FFB6C1')))
|
| 474 |
-
})
|
| 475 |
-
|
| 476 |
-
# Function to generate leaderboard data
|
| 477 |
generate_leaderboard <- function() {
|
| 478 |
all_data <- bind_rows(values$pitchData, values$uploadedData)
|
| 479 |
|
|
@@ -493,11 +575,6 @@ server <- function(input, output, session) {
|
|
| 493 |
OS_Total = sum(PitchType %in% c("Curveball", "Slider", "Changeup")),
|
| 494 |
OS_Strikes = sum(Result == "Strike" & PitchType %in% c("Curveball", "Slider", "Changeup")),
|
| 495 |
OS_StrikeRate = if_else(OS_Total > 0, round(OS_Strikes / OS_Total * 100, 1), NA_real_),
|
| 496 |
-
AvgFBVelo = round(mean(Velocity[PitchType %in% c("Fastball", "Sinker", "Cutter")], na.rm = TRUE), 1),
|
| 497 |
-
MaxFBVelo = suppressWarnings(max(Velocity[PitchType %in% c("Fastball", "Sinker", "Cutter")], na.rm = TRUE)),
|
| 498 |
-
AvgVelo = round(mean(Velocity, na.rm = TRUE), 1),
|
| 499 |
-
HitSpotRate = round(mean(HitSpot, na.rm = TRUE) * 100, 1),
|
| 500 |
-
InZoneRate = round(mean(InZone) * 100, 1),
|
| 501 |
.groups = 'drop'
|
| 502 |
) %>%
|
| 503 |
filter(Total >= input$minPitches)
|
|
@@ -505,24 +582,16 @@ server <- function(input, output, session) {
|
|
| 505 |
return(leaderboard)
|
| 506 |
}
|
| 507 |
|
| 508 |
-
# Function to prepare leaderboard display (used by both render and download)
|
| 509 |
prepare_leaderboard_display <- function(leaderboard) {
|
| 510 |
if (is.null(leaderboard)) return(NULL)
|
| 511 |
|
| 512 |
-
# Rename columns for display
|
| 513 |
leaderboard_display <- leaderboard
|
| 514 |
if ("Pitcher" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(PLAYER = Pitcher)
|
| 515 |
if ("FB_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`FB STRIKE%` = FB_StrikeRate)
|
| 516 |
if ("OS_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OS STRIKE%` = OS_StrikeRate)
|
| 517 |
if ("StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OVR STRIKE%` = StrikeRate)
|
| 518 |
-
if ("AvgFBVelo" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`AVG FB VELO` = AvgFBVelo)
|
| 519 |
-
if ("MaxFBVelo" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`MAX FB VELO` = MaxFBVelo)
|
| 520 |
-
if ("AvgVelo" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`AVG VELO` = AvgVelo)
|
| 521 |
-
if ("HitSpotRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`HIT SPOT%` = HitSpotRate)
|
| 522 |
-
if ("InZoneRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`IN ZONE%` = InZoneRate)
|
| 523 |
if ("Total" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`TOTAL PITCHES` = Total)
|
| 524 |
|
| 525 |
-
# Select only chosen columns
|
| 526 |
if (!is.null(input$leaderboardCols) && length(input$leaderboardCols) > 0) {
|
| 527 |
cols_to_keep <- input$leaderboardCols
|
| 528 |
|
|
@@ -531,12 +600,7 @@ server <- function(input, output, session) {
|
|
| 531 |
"Total" = "TOTAL PITCHES",
|
| 532 |
"StrikeRate" = "OVR STRIKE%",
|
| 533 |
"FB_StrikeRate" = "FB STRIKE%",
|
| 534 |
-
"OS_StrikeRate" = "OS STRIKE%"
|
| 535 |
-
"AvgFBVelo" = "AVG FB VELO",
|
| 536 |
-
"MaxFBVelo" = "MAX FB VELO",
|
| 537 |
-
"AvgVelo" = "AVG VELO",
|
| 538 |
-
"HitSpotRate" = "HIT SPOT%",
|
| 539 |
-
"InZoneRate" = "IN ZONE%"
|
| 540 |
)
|
| 541 |
|
| 542 |
display_cols <- col_mapping[cols_to_keep]
|
|
@@ -583,9 +647,8 @@ server <- function(input, output, session) {
|
|
| 583 |
border = '2px solid black'
|
| 584 |
)
|
| 585 |
|
| 586 |
-
# Apply conditional formatting to percentage columns
|
| 587 |
pct_cols <- intersect(
|
| 588 |
-
c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"
|
| 589 |
colnames(leaderboard_display)
|
| 590 |
)
|
| 591 |
|
|
@@ -606,7 +669,6 @@ server <- function(input, output, session) {
|
|
| 606 |
dt
|
| 607 |
})
|
| 608 |
|
| 609 |
-
# Download leaderboard as CSV
|
| 610 |
output$downloadLeaderboardCSV <- downloadHandler(
|
| 611 |
filename = function() {
|
| 612 |
paste0("Leaderboard_", format(Sys.Date(), "%m%d"), ".csv")
|
|
@@ -622,9 +684,9 @@ server <- function(input, output, session) {
|
|
| 622 |
}
|
| 623 |
)
|
| 624 |
|
| 625 |
-
# Download leaderboard as PNG
|
| 626 |
output$downloadLeaderboardPNG <- downloadHandler(
|
| 627 |
-
filename = function() {
|
|
|
|
| 628 |
},
|
| 629 |
content = function(file) {
|
| 630 |
leaderboard <- generate_leaderboard()
|
|
@@ -634,7 +696,6 @@ server <- function(input, output, session) {
|
|
| 634 |
return()
|
| 635 |
}
|
| 636 |
|
| 637 |
-
# Prepare display version using the shared function
|
| 638 |
leaderboard_display <- prepare_leaderboard_display(leaderboard)
|
| 639 |
|
| 640 |
if (is.null(leaderboard_display)) {
|
|
@@ -642,7 +703,6 @@ server <- function(input, output, session) {
|
|
| 642 |
return()
|
| 643 |
}
|
| 644 |
|
| 645 |
-
# Create datatable widget with conditional formatting
|
| 646 |
dt <- DT::datatable(
|
| 647 |
leaderboard_display,
|
| 648 |
options = list(
|
|
@@ -669,9 +729,8 @@ server <- function(input, output, session) {
|
|
| 669 |
border = '2px solid black'
|
| 670 |
)
|
| 671 |
|
| 672 |
-
# Apply conditional formatting to percentage columns
|
| 673 |
pct_cols <- intersect(
|
| 674 |
-
c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"
|
| 675 |
colnames(leaderboard_display)
|
| 676 |
)
|
| 677 |
|
|
@@ -689,11 +748,9 @@ server <- function(input, output, session) {
|
|
| 689 |
)
|
| 690 |
}
|
| 691 |
|
| 692 |
-
# Save as HTML temporarily
|
| 693 |
temp_html <- tempfile(fileext = ".html")
|
| 694 |
htmlwidgets::saveWidget(dt, temp_html, selfcontained = TRUE)
|
| 695 |
|
| 696 |
-
# Convert to PNG using webshot2
|
| 697 |
tryCatch({
|
| 698 |
webshot2::webshot(temp_html, file = file, vwidth = 1400, vheight = 1000, delay = 0.5)
|
| 699 |
showNotification("Leaderboard PNG downloaded successfully!", type = "message")
|
|
@@ -701,11 +758,11 @@ server <- function(input, output, session) {
|
|
| 701 |
showNotification(paste("Error creating PNG:", e$message), type = "error")
|
| 702 |
})
|
| 703 |
|
| 704 |
-
# Clean up temp file
|
| 705 |
unlink(temp_html)
|
| 706 |
}
|
| 707 |
)
|
| 708 |
|
|
|
|
| 709 |
output$downloadData <- downloadHandler(
|
| 710 |
filename = function() {
|
| 711 |
base_name <- if (!is.null(input$exportFilename) && input$exportFilename != "") {
|
|
@@ -720,6 +777,21 @@ server <- function(input, output, session) {
|
|
| 720 |
write_csv(values$pitchData, file)
|
| 721 |
}
|
| 722 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
}
|
| 724 |
|
| 725 |
shinyApp(ui = ui, server = server)
|
|
|
|
| 8 |
library(htmlwidgets)
|
| 9 |
|
| 10 |
ui <- dashboardPage(
|
| 11 |
+
dashboardHeader(title = "Pitching Tracker"),
|
| 12 |
dashboardSidebar(
|
| 13 |
sidebarMenu(
|
| 14 |
menuItem("Bullpen Session", tabName = "bullpen", icon = icon("baseball")),
|
| 15 |
+
menuItem("Game Tracking", tabName = "game", icon = icon("gamepad")),
|
| 16 |
menuItem("Leaderboard", tabName = "leaderboard", icon = icon("chart-bar")),
|
| 17 |
menuItem("Export Data", tabName = "export", icon = icon("download"))
|
| 18 |
)
|
|
|
|
| 53 |
),
|
| 54 |
|
| 55 |
tabItems(
|
| 56 |
+
# BULLPEN TAB - SIMPLIFIED
|
| 57 |
tabItem(
|
| 58 |
tabName = "bullpen",
|
| 59 |
fluidRow(
|
|
|
|
| 97 |
),
|
| 98 |
|
| 99 |
hr(),
|
| 100 |
+
h4("2. Intended Location"),
|
| 101 |
p("Select vertical OR horizontal OR both", style = "font-size: 12px; color: #666;"),
|
| 102 |
fluidRow(
|
| 103 |
column(6, actionButton("int_up", "UP", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")),
|
|
|
|
| 110 |
),
|
| 111 |
|
| 112 |
hr(),
|
| 113 |
+
h4("3. Result"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
fluidRow(
|
| 115 |
column(6, actionButton("result_strike", "STRIKE", class = "btn-success", style = "width: 100%; min-height: 80px; font-size: 24px;")),
|
| 116 |
column(6, actionButton("result_ball", "BALL", class = "btn-danger", style = "width: 100%; min-height: 80px; font-size: 24px;"))
|
| 117 |
),
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
hr(),
|
| 120 |
actionButton("clearPitch", "Clear Selection", class = "btn-warning btn-block")
|
| 121 |
)
|
|
|
|
| 132 |
)
|
| 133 |
),
|
| 134 |
|
| 135 |
+
# GAME TAB - NEW
|
| 136 |
+
tabItem(
|
| 137 |
+
tabName = "game",
|
| 138 |
+
fluidRow(
|
| 139 |
+
box(
|
| 140 |
+
title = "Game Setup",
|
| 141 |
+
status = "primary",
|
| 142 |
+
solidHeader = TRUE,
|
| 143 |
+
width = 4,
|
| 144 |
+
textInput("gamePitcherName", "Pitcher Name:", placeholder = "Enter name"),
|
| 145 |
+
actionButton("addGamePitcher", "Add Pitcher", class = "btn-success btn-block"),
|
| 146 |
+
hr(),
|
| 147 |
+
selectInput("currentGamePitcher", "Currently Pitching:", choices = NULL),
|
| 148 |
+
selectInput("gameBatterSide", "Batter Side:",
|
| 149 |
+
choices = c("Right (Default)" = "R", "Left" = "L"),
|
| 150 |
+
selected = "R"),
|
| 151 |
+
hr(),
|
| 152 |
+
div(
|
| 153 |
+
style = "background: #f4f4f4; padding: 15px; border-radius: 10px;",
|
| 154 |
+
h4("Game Stats"),
|
| 155 |
+
verbatimTextOutput("gameStats")
|
| 156 |
+
)
|
| 157 |
+
),
|
| 158 |
+
|
| 159 |
+
box(
|
| 160 |
+
title = "Record Game Pitch",
|
| 161 |
+
status = "success",
|
| 162 |
+
solidHeader = TRUE,
|
| 163 |
+
width = 8,
|
| 164 |
+
div(class = "selection-display", uiOutput("gameSelectionDisplay")),
|
| 165 |
+
|
| 166 |
+
h4("1. Pitch Call"),
|
| 167 |
+
fluidRow(
|
| 168 |
+
column(4, actionButton("game_pitch_fb", "Fastball", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 169 |
+
column(4, actionButton("game_pitch_cb", "Curveball", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 170 |
+
column(4, actionButton("game_pitch_ch", "Changeup", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;"))
|
| 171 |
+
),
|
| 172 |
+
fluidRow(
|
| 173 |
+
column(4, actionButton("game_pitch_sl", "Slider", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 174 |
+
column(4, actionButton("game_pitch_ct", "Cutter", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 175 |
+
column(4, actionButton("game_pitch_sn", "Sinker", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;"))
|
| 176 |
+
),
|
| 177 |
+
|
| 178 |
+
hr(),
|
| 179 |
+
h4("2. Intended Location"),
|
| 180 |
+
p("Select vertical OR horizontal OR both", style = "font-size: 12px; color: #666;"),
|
| 181 |
+
fluidRow(
|
| 182 |
+
column(6, actionButton("game_int_up", "UP", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 183 |
+
column(6, actionButton("game_int_down", "DOWN", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;"))
|
| 184 |
+
),
|
| 185 |
+
fluidRow(
|
| 186 |
+
column(4, actionButton("game_int_away", "AWAY", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 187 |
+
column(4, actionButton("game_int_middle", "MIDDLE", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")),
|
| 188 |
+
column(4, actionButton("game_int_in", "IN", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;"))
|
| 189 |
+
),
|
| 190 |
+
|
| 191 |
+
hr(),
|
| 192 |
+
actionButton("recordGamePitch", "RECORD PITCH", class = "btn-success btn-block", style = "min-height: 80px; font-size: 24px;"),
|
| 193 |
+
hr(),
|
| 194 |
+
actionButton("clearGamePitch", "Clear Selection", class = "btn-warning btn-block")
|
| 195 |
+
)
|
| 196 |
+
),
|
| 197 |
+
|
| 198 |
+
fluidRow(
|
| 199 |
+
box(
|
| 200 |
+
title = "Game Pitches",
|
| 201 |
+
status = "info",
|
| 202 |
+
solidHeader = TRUE,
|
| 203 |
+
width = 12,
|
| 204 |
+
DT::dataTableOutput("gamePitches")
|
| 205 |
+
)
|
| 206 |
+
)
|
| 207 |
+
),
|
| 208 |
+
|
| 209 |
+
# LEADERBOARD TAB
|
| 210 |
tabItem(
|
| 211 |
tabName = "leaderboard",
|
| 212 |
fluidRow(
|
|
|
|
| 242 |
"Total Pitches" = "Total",
|
| 243 |
"Overall Strike %" = "StrikeRate",
|
| 244 |
"FB Strike %" = "FB_StrikeRate",
|
| 245 |
+
"OS Strike %" = "OS_StrikeRate"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
),
|
| 247 |
selected = c("Pitcher", "FB_StrikeRate", "OS_StrikeRate", "StrikeRate"),
|
| 248 |
inline = TRUE
|
|
|
|
| 264 |
)
|
| 265 |
),
|
| 266 |
|
| 267 |
+
# EXPORT TAB
|
| 268 |
tabItem(
|
| 269 |
tabName = "export",
|
| 270 |
fluidRow(
|
| 271 |
box(
|
| 272 |
+
title = "Export Bullpen Data",
|
| 273 |
status = "success",
|
| 274 |
solidHeader = TRUE,
|
| 275 |
+
width = 6,
|
| 276 |
+
textInput("exportFilename", "Bullpen Session Name:",
|
| 277 |
placeholder = "LastName1_LastName2_Bullpen"),
|
| 278 |
+
downloadButton("downloadData", "Download Bullpen CSV", class = "btn-primary btn-lg")
|
| 279 |
+
),
|
| 280 |
+
box(
|
| 281 |
+
title = "Export Game Data",
|
| 282 |
+
status = "info",
|
| 283 |
+
solidHeader = TRUE,
|
| 284 |
+
width = 6,
|
| 285 |
+
textInput("exportGameFilename", "Game Name:",
|
| 286 |
+
placeholder = "Game_vs_Opponent"),
|
| 287 |
+
downloadButton("downloadGameData", "Download Game CSV", class = "btn-primary btn-lg")
|
| 288 |
)
|
| 289 |
)
|
| 290 |
)
|
|
|
|
| 301 |
currentPitchType = NULL,
|
| 302 |
currentIntendedVert = NULL,
|
| 303 |
currentIntendedHorz = NULL,
|
| 304 |
+
# Game tracking
|
| 305 |
+
gamePitchers = character(0),
|
| 306 |
+
gameData = data.frame(),
|
| 307 |
+
gamePitchType = NULL,
|
| 308 |
+
gameIntendedVert = NULL,
|
| 309 |
+
gameIntendedHorz = NULL
|
| 310 |
)
|
| 311 |
|
| 312 |
+
# BULLPEN SECTION
|
| 313 |
output$selectionDisplay <- renderUI({
|
| 314 |
parts <- c()
|
| 315 |
if (!is.null(values$currentPitchType)) {
|
|
|
|
| 319 |
loc <- paste(c(values$currentIntendedVert, values$currentIntendedHorz), collapse = " + ")
|
| 320 |
parts <- c(parts, paste("Intended:", loc))
|
| 321 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
if (length(parts) == 0) {
|
| 324 |
HTML("<span style='color: #999;'>Select pitch details...</span>")
|
|
|
|
| 357 |
os_strikes <- sum(os_data$Result == "Strike")
|
| 358 |
os_pct <- if (os_total > 0) round(os_strikes / os_total * 100, 1) else 0
|
| 359 |
|
|
|
|
|
|
|
|
|
|
| 360 |
paste0(
|
| 361 |
"Total Pitches: ", total, "\n",
|
| 362 |
"==================\n",
|
| 363 |
"Overall Strike%: ", strike_pct, "%\n",
|
| 364 |
"FB Strike%: ", fb_pct, "% (", fb_total, " FB)\n",
|
| 365 |
+
"OS Strike%: ", os_pct, "% (", os_total, " OS)"
|
|
|
|
|
|
|
| 366 |
)
|
| 367 |
})
|
| 368 |
|
|
|
|
| 380 |
observeEvent(input$int_middle, { values$currentIntendedHorz <- "Middle" })
|
| 381 |
observeEvent(input$int_in, { values$currentIntendedHorz <- "In" })
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
recordPitch <- function(result) {
|
| 384 |
+
req(input$currentPitcher, values$currentPitchType)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
+
intended_loc <- paste(c(values$currentIntendedVert, values$currentIntendedHorz), collapse = " + ")
|
| 387 |
+
if (intended_loc == "") intended_loc <- NA
|
| 388 |
|
| 389 |
new_pitch <- data.frame(
|
| 390 |
Pitcher = input$currentPitcher,
|
| 391 |
PitchType = values$currentPitchType,
|
| 392 |
+
BatterSide = input$batterSide,
|
| 393 |
+
IntendedLocation = intended_loc,
|
|
|
|
|
|
|
|
|
|
| 394 |
Result = result,
|
|
|
|
|
|
|
| 395 |
Timestamp = Sys.time(),
|
| 396 |
stringsAsFactors = FALSE
|
| 397 |
)
|
|
|
|
| 401 |
values$currentPitchType <- NULL
|
| 402 |
values$currentIntendedVert <- NULL
|
| 403 |
values$currentIntendedHorz <- NULL
|
|
|
|
|
|
|
| 404 |
|
| 405 |
showNotification(paste(result, "recorded!"), type = "message", duration = 1)
|
| 406 |
}
|
|
|
|
| 412 |
values$currentPitchType <- NULL
|
| 413 |
values$currentIntendedVert <- NULL
|
| 414 |
values$currentIntendedHorz <- NULL
|
|
|
|
|
|
|
| 415 |
})
|
| 416 |
|
| 417 |
+
output$recentPitches <- DT::renderDataTable({
|
| 418 |
+
if (nrow(values$pitchData) == 0) {
|
| 419 |
+
return(data.frame(Message = "No pitches recorded"))
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
recent <- tail(values$pitchData, 20) %>%
|
| 423 |
+
arrange(desc(Timestamp)) %>%
|
| 424 |
+
select(Pitcher, PitchType, BatterSide, IntendedLocation, Result)
|
| 425 |
+
|
| 426 |
+
DT::datatable(recent, options = list(pageLength = 20, dom = 't'), rownames = FALSE)
|
| 427 |
+
})
|
| 428 |
+
|
| 429 |
+
# GAME SECTION
|
| 430 |
+
output$gameSelectionDisplay <- renderUI({
|
| 431 |
+
parts <- c()
|
| 432 |
+
if (!is.null(values$gamePitchType)) {
|
| 433 |
+
parts <- c(parts, paste("Call:", values$gamePitchType))
|
| 434 |
+
}
|
| 435 |
+
if (!is.null(values$gameIntendedVert) || !is.null(values$gameIntendedHorz)) {
|
| 436 |
+
loc <- paste(c(values$gameIntendedVert, values$gameIntendedHorz), collapse = " + ")
|
| 437 |
+
parts <- c(parts, paste("Location:", loc))
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
if (length(parts) == 0) {
|
| 441 |
+
HTML("<span style='color: #999;'>Select pitch call and location...</span>")
|
| 442 |
+
} else {
|
| 443 |
+
HTML(paste(parts, collapse = " | "))
|
| 444 |
+
}
|
| 445 |
+
})
|
| 446 |
+
|
| 447 |
+
observeEvent(input$addGamePitcher, {
|
| 448 |
+
req(input$gamePitcherName)
|
| 449 |
+
name <- trimws(input$gamePitcherName)
|
| 450 |
+
if (name != "" && !(name %in% values$gamePitchers)) {
|
| 451 |
+
values$gamePitchers <- c(values$gamePitchers, name)
|
| 452 |
+
updateSelectInput(session, "currentGamePitcher", choices = values$gamePitchers, selected = name)
|
| 453 |
+
updateTextInput(session, "gamePitcherName", value = "")
|
| 454 |
+
showNotification(paste("Added", name, "to game"), type = "message")
|
| 455 |
+
}
|
| 456 |
+
})
|
| 457 |
+
|
| 458 |
+
output$gameStats <- renderText({
|
| 459 |
+
req(input$currentGamePitcher)
|
| 460 |
+
data <- values$gameData %>% filter(Pitcher == input$currentGamePitcher)
|
| 461 |
+
if (nrow(data) == 0) return("No pitches yet")
|
| 462 |
+
|
| 463 |
+
total <- nrow(data)
|
| 464 |
+
|
| 465 |
+
fb_data <- data %>% filter(PitchCall %in% c("Fastball", "Sinker", "Cutter"))
|
| 466 |
+
fb_total <- nrow(fb_data)
|
| 467 |
+
|
| 468 |
+
os_data <- data %>% filter(PitchCall %in% c("Curveball", "Slider", "Changeup"))
|
| 469 |
+
os_total <- nrow(os_data)
|
| 470 |
+
|
| 471 |
+
paste0(
|
| 472 |
+
"Total Pitches: ", total, "\n",
|
| 473 |
+
"==================\n",
|
| 474 |
+
"Fastballs: ", fb_total, "\n",
|
| 475 |
+
"Offspeed: ", os_total
|
| 476 |
+
)
|
| 477 |
+
})
|
| 478 |
+
|
| 479 |
+
observeEvent(input$game_pitch_fb, { values$gamePitchType <- "Fastball" })
|
| 480 |
+
observeEvent(input$game_pitch_cb, { values$gamePitchType <- "Curveball" })
|
| 481 |
+
observeEvent(input$game_pitch_ch, { values$gamePitchType <- "Changeup" })
|
| 482 |
+
observeEvent(input$game_pitch_sl, { values$gamePitchType <- "Slider" })
|
| 483 |
+
observeEvent(input$game_pitch_ct, { values$gamePitchType <- "Cutter" })
|
| 484 |
+
observeEvent(input$game_pitch_sn, { values$gamePitchType <- "Sinker" })
|
| 485 |
+
|
| 486 |
+
observeEvent(input$game_int_up, { values$gameIntendedVert <- "Up" })
|
| 487 |
+
observeEvent(input$game_int_down, { values$gameIntendedVert <- "Down" })
|
| 488 |
+
|
| 489 |
+
observeEvent(input$game_int_away, { values$gameIntendedHorz <- "Away" })
|
| 490 |
+
observeEvent(input$game_int_middle, { values$gameIntendedHorz <- "Middle" })
|
| 491 |
+
observeEvent(input$game_int_in, { values$gameIntendedHorz <- "In" })
|
| 492 |
+
|
| 493 |
+
observeEvent(input$recordGamePitch, {
|
| 494 |
+
req(input$currentGamePitcher, values$gamePitchType)
|
| 495 |
+
|
| 496 |
+
intended_loc <- paste(c(values$gameIntendedVert, values$gameIntendedHorz), collapse = " + ")
|
| 497 |
+
if (intended_loc == "") intended_loc <- NA
|
| 498 |
+
|
| 499 |
+
new_pitch <- data.frame(
|
| 500 |
+
Pitcher = input$currentGamePitcher,
|
| 501 |
+
PitchCall = values$gamePitchType,
|
| 502 |
+
BatterSide = input$gameBatterSide,
|
| 503 |
+
IntendedLocation = intended_loc,
|
| 504 |
+
Timestamp = Sys.time(),
|
| 505 |
+
stringsAsFactors = FALSE
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
values$gameData <- rbind(values$gameData, new_pitch)
|
| 509 |
+
|
| 510 |
+
values$gamePitchType <- NULL
|
| 511 |
+
values$gameIntendedVert <- NULL
|
| 512 |
+
values$gameIntendedHorz <- NULL
|
| 513 |
+
|
| 514 |
+
showNotification("Pitch recorded!", type = "message", duration = 1)
|
| 515 |
+
})
|
| 516 |
+
|
| 517 |
+
observeEvent(input$clearGamePitch, {
|
| 518 |
+
values$gamePitchType <- NULL
|
| 519 |
+
values$gameIntendedVert <- NULL
|
| 520 |
+
values$gameIntendedHorz <- NULL
|
| 521 |
+
})
|
| 522 |
+
|
| 523 |
+
output$gamePitches <- DT::renderDataTable({
|
| 524 |
+
if (nrow(values$gameData) == 0) {
|
| 525 |
+
return(data.frame(Message = "No pitches recorded"))
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
recent <- tail(values$gameData, 50) %>%
|
| 529 |
+
arrange(desc(Timestamp)) %>%
|
| 530 |
+
select(Pitcher, PitchCall, BatterSide, IntendedLocation)
|
| 531 |
+
|
| 532 |
+
DT::datatable(recent, options = list(pageLength = 50, dom = 't'), rownames = FALSE)
|
| 533 |
+
})
|
| 534 |
+
|
| 535 |
+
# LEADERBOARD SECTION
|
| 536 |
observeEvent(input$uploadCSV, {
|
| 537 |
req(input$uploadCSV)
|
| 538 |
|
|
|
|
| 556 |
showNotification("Cleared uploaded data", type = "warning")
|
| 557 |
})
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
generate_leaderboard <- function() {
|
| 560 |
all_data <- bind_rows(values$pitchData, values$uploadedData)
|
| 561 |
|
|
|
|
| 575 |
OS_Total = sum(PitchType %in% c("Curveball", "Slider", "Changeup")),
|
| 576 |
OS_Strikes = sum(Result == "Strike" & PitchType %in% c("Curveball", "Slider", "Changeup")),
|
| 577 |
OS_StrikeRate = if_else(OS_Total > 0, round(OS_Strikes / OS_Total * 100, 1), NA_real_),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
.groups = 'drop'
|
| 579 |
) %>%
|
| 580 |
filter(Total >= input$minPitches)
|
|
|
|
| 582 |
return(leaderboard)
|
| 583 |
}
|
| 584 |
|
|
|
|
| 585 |
prepare_leaderboard_display <- function(leaderboard) {
|
| 586 |
if (is.null(leaderboard)) return(NULL)
|
| 587 |
|
|
|
|
| 588 |
leaderboard_display <- leaderboard
|
| 589 |
if ("Pitcher" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(PLAYER = Pitcher)
|
| 590 |
if ("FB_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`FB STRIKE%` = FB_StrikeRate)
|
| 591 |
if ("OS_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OS STRIKE%` = OS_StrikeRate)
|
| 592 |
if ("StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OVR STRIKE%` = StrikeRate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
if ("Total" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`TOTAL PITCHES` = Total)
|
| 594 |
|
|
|
|
| 595 |
if (!is.null(input$leaderboardCols) && length(input$leaderboardCols) > 0) {
|
| 596 |
cols_to_keep <- input$leaderboardCols
|
| 597 |
|
|
|
|
| 600 |
"Total" = "TOTAL PITCHES",
|
| 601 |
"StrikeRate" = "OVR STRIKE%",
|
| 602 |
"FB_StrikeRate" = "FB STRIKE%",
|
| 603 |
+
"OS_StrikeRate" = "OS STRIKE%"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
)
|
| 605 |
|
| 606 |
display_cols <- col_mapping[cols_to_keep]
|
|
|
|
| 647 |
border = '2px solid black'
|
| 648 |
)
|
| 649 |
|
|
|
|
| 650 |
pct_cols <- intersect(
|
| 651 |
+
c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"),
|
| 652 |
colnames(leaderboard_display)
|
| 653 |
)
|
| 654 |
|
|
|
|
| 669 |
dt
|
| 670 |
})
|
| 671 |
|
|
|
|
| 672 |
output$downloadLeaderboardCSV <- downloadHandler(
|
| 673 |
filename = function() {
|
| 674 |
paste0("Leaderboard_", format(Sys.Date(), "%m%d"), ".csv")
|
|
|
|
| 684 |
}
|
| 685 |
)
|
| 686 |
|
|
|
|
| 687 |
output$downloadLeaderboardPNG <- downloadHandler(
|
| 688 |
+
filename = function() {
|
| 689 |
+
paste0("Leaderboard_", format(Sys.Date(), "%m%d"), ".png")
|
| 690 |
},
|
| 691 |
content = function(file) {
|
| 692 |
leaderboard <- generate_leaderboard()
|
|
|
|
| 696 |
return()
|
| 697 |
}
|
| 698 |
|
|
|
|
| 699 |
leaderboard_display <- prepare_leaderboard_display(leaderboard)
|
| 700 |
|
| 701 |
if (is.null(leaderboard_display)) {
|
|
|
|
| 703 |
return()
|
| 704 |
}
|
| 705 |
|
|
|
|
| 706 |
dt <- DT::datatable(
|
| 707 |
leaderboard_display,
|
| 708 |
options = list(
|
|
|
|
| 729 |
border = '2px solid black'
|
| 730 |
)
|
| 731 |
|
|
|
|
| 732 |
pct_cols <- intersect(
|
| 733 |
+
c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"),
|
| 734 |
colnames(leaderboard_display)
|
| 735 |
)
|
| 736 |
|
|
|
|
| 748 |
)
|
| 749 |
}
|
| 750 |
|
|
|
|
| 751 |
temp_html <- tempfile(fileext = ".html")
|
| 752 |
htmlwidgets::saveWidget(dt, temp_html, selfcontained = TRUE)
|
| 753 |
|
|
|
|
| 754 |
tryCatch({
|
| 755 |
webshot2::webshot(temp_html, file = file, vwidth = 1400, vheight = 1000, delay = 0.5)
|
| 756 |
showNotification("Leaderboard PNG downloaded successfully!", type = "message")
|
|
|
|
| 758 |
showNotification(paste("Error creating PNG:", e$message), type = "error")
|
| 759 |
})
|
| 760 |
|
|
|
|
| 761 |
unlink(temp_html)
|
| 762 |
}
|
| 763 |
)
|
| 764 |
|
| 765 |
+
# EXPORT HANDLERS
|
| 766 |
output$downloadData <- downloadHandler(
|
| 767 |
filename = function() {
|
| 768 |
base_name <- if (!is.null(input$exportFilename) && input$exportFilename != "") {
|
|
|
|
| 777 |
write_csv(values$pitchData, file)
|
| 778 |
}
|
| 779 |
)
|
| 780 |
+
|
| 781 |
+
output$downloadGameData <- downloadHandler(
|
| 782 |
+
filename = function() {
|
| 783 |
+
base_name <- if (!is.null(input$exportGameFilename) && input$exportGameFilename != "") {
|
| 784 |
+
gsub(" ", "_", trimws(input$exportGameFilename))
|
| 785 |
+
} else {
|
| 786 |
+
"Game_Data"
|
| 787 |
+
}
|
| 788 |
+
date_str <- format(Sys.Date(), "%m%d")
|
| 789 |
+
paste0(base_name, "_", date_str, ".csv")
|
| 790 |
+
},
|
| 791 |
+
content = function(file) {
|
| 792 |
+
write_csv(values$gameData, file)
|
| 793 |
+
}
|
| 794 |
+
)
|
| 795 |
}
|
| 796 |
|
| 797 |
shinyApp(ui = ui, server = server)
|