Spaces:
Sleeping
Sleeping
| library(shiny) | |
| library(shinydashboard) | |
| library(DT) | |
| library(dplyr) | |
| library(shinyjs) | |
| library(readr) | |
| library(webshot2) | |
| library(htmlwidgets) | |
| ui <- dashboardPage( | |
| dashboardHeader(title = "Pitching Tracker"), | |
| dashboardSidebar( | |
| sidebarMenu( | |
| menuItem("Bullpen Session", tabName = "bullpen", icon = icon("baseball")), | |
| menuItem("Game Tracking", tabName = "game", icon = icon("gamepad")), | |
| menuItem("Leaderboard", tabName = "leaderboard", icon = icon("chart-bar")), | |
| menuItem("Export Data", tabName = "export", icon = icon("download")) | |
| ) | |
| ), | |
| dashboardBody( | |
| useShinyjs(), | |
| tags$head( | |
| tags$style(HTML(" | |
| .zone-btn { | |
| width: 100%; | |
| min-height: 80px; | |
| font-size: 28px; | |
| font-weight: bold; | |
| margin: 2px; | |
| } | |
| .border-zone-btn { | |
| min-height: 60px; | |
| font-size: 20px; | |
| font-weight: bold; | |
| } | |
| .zone-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 5px; | |
| max-width: 400px; | |
| margin: 0 auto; | |
| } | |
| .selection-display { | |
| background: #f0f0f0; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 10px 0; | |
| min-height: 60px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| } | |
| ")) | |
| ), | |
| tabItems( | |
| # BULLPEN TAB - SIMPLIFIED | |
| tabItem( | |
| tabName = "bullpen", | |
| fluidRow( | |
| box( | |
| title = "Pitcher Setup", | |
| status = "primary", | |
| solidHeader = TRUE, | |
| width = 4, | |
| textInput("pitcherName", "Pitcher Name:", placeholder = "Enter name"), | |
| actionButton("addPitcher", "Add Pitcher", class = "btn-success btn-block"), | |
| hr(), | |
| selectInput("currentPitcher", "Currently Throwing:", choices = NULL), | |
| selectInput("batterSide", "Batter Side:", | |
| choices = c("Right (Default)" = "R", "Left" = "L"), | |
| selected = "R"), | |
| hr(), | |
| div( | |
| style = "background: #f4f4f4; padding: 15px; border-radius: 10px;", | |
| h4("Current Stats"), | |
| verbatimTextOutput("currentStats") | |
| ) | |
| ), | |
| box( | |
| title = "Record Pitch", | |
| status = "success", | |
| solidHeader = TRUE, | |
| width = 8, | |
| div(class = "selection-display", uiOutput("selectionDisplay")), | |
| h4("1. Pitch Type"), | |
| fluidRow( | |
| column(4, actionButton("pitch_fb", "Fastball", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("pitch_cb", "Curveball", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("pitch_ch", "Changeup", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| fluidRow( | |
| column(4, actionButton("pitch_sl", "Slider", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("pitch_ct", "Cutter", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("pitch_sn", "Sinker", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| hr(), | |
| h4("2. Intended Location"), | |
| p("Select vertical OR horizontal OR both", style = "font-size: 12px; color: #666;"), | |
| fluidRow( | |
| column(6, actionButton("int_up", "UP", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(6, actionButton("int_down", "DOWN", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| fluidRow( | |
| column(4, actionButton("int_away", "AWAY", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("int_middle", "MIDDLE", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("int_in", "IN", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| hr(), | |
| h4("3. Result"), | |
| fluidRow( | |
| column(6, actionButton("result_strike", "STRIKE", class = "btn-success", style = "width: 100%; min-height: 80px; font-size: 24px;")), | |
| column(6, actionButton("result_ball", "BALL", class = "btn-danger", style = "width: 100%; min-height: 80px; font-size: 24px;")) | |
| ), | |
| hr(), | |
| actionButton("clearPitch", "Clear Selection", class = "btn-warning btn-block") | |
| ) | |
| ), | |
| fluidRow( | |
| box( | |
| title = "Recent Pitches", | |
| status = "info", | |
| solidHeader = TRUE, | |
| width = 12, | |
| DT::dataTableOutput("recentPitches") | |
| ) | |
| ) | |
| ), | |
| # GAME TAB - NEW | |
| tabItem( | |
| tabName = "game", | |
| fluidRow( | |
| box( | |
| title = "Game Setup", | |
| status = "primary", | |
| solidHeader = TRUE, | |
| width = 4, | |
| textInput("gamePitcherName", "Pitcher Name:", placeholder = "Enter name"), | |
| actionButton("addGamePitcher", "Add Pitcher", class = "btn-success btn-block"), | |
| hr(), | |
| selectInput("currentGamePitcher", "Currently Pitching:", choices = NULL), | |
| selectInput("gameBatterSide", "Batter Side:", | |
| choices = c("Right (Default)" = "R", "Left" = "L"), | |
| selected = "R"), | |
| hr(), | |
| div( | |
| style = "background: #f4f4f4; padding: 15px; border-radius: 10px;", | |
| h4("Game Stats"), | |
| verbatimTextOutput("gameStats") | |
| ) | |
| ), | |
| box( | |
| title = "Record Game Pitch", | |
| status = "success", | |
| solidHeader = TRUE, | |
| width = 8, | |
| div(class = "selection-display", uiOutput("gameSelectionDisplay")), | |
| h4("1. Pitch Call"), | |
| fluidRow( | |
| column(4, actionButton("game_pitch_fb", "Fastball", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_pitch_cb", "Curveball", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_pitch_ch", "Changeup", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| fluidRow( | |
| column(4, actionButton("game_pitch_sl", "Slider", class = "btn-warning btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_pitch_ct", "Cutter", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_pitch_sn", "Sinker", class = "btn-primary btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| hr(), | |
| h4("2. Intended Location"), | |
| p("Select vertical OR horizontal OR both", style = "font-size: 12px; color: #666;"), | |
| fluidRow( | |
| column(6, actionButton("game_int_up", "UP", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(6, actionButton("game_int_down", "DOWN", class = "btn-info btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| fluidRow( | |
| column(4, actionButton("game_int_away", "AWAY", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_int_middle", "MIDDLE", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")), | |
| column(4, actionButton("game_int_in", "IN", class = "btn-secondary btn-block", style = "min-height: 60px; font-size: 18px;")) | |
| ), | |
| hr(), | |
| actionButton("recordGamePitch", "RECORD PITCH", class = "btn-success btn-block", style = "min-height: 80px; font-size: 24px;"), | |
| hr(), | |
| actionButton("clearGamePitch", "Clear Selection", class = "btn-warning btn-block") | |
| ) | |
| ), | |
| fluidRow( | |
| box( | |
| title = "Game Pitches", | |
| status = "info", | |
| solidHeader = TRUE, | |
| width = 12, | |
| DT::dataTableOutput("gamePitches") | |
| ) | |
| ) | |
| ), | |
| # LEADERBOARD TAB | |
| tabItem( | |
| tabName = "leaderboard", | |
| fluidRow( | |
| box( | |
| title = "Upload Previous Sessions", | |
| status = "info", | |
| solidHeader = TRUE, | |
| width = 12, | |
| fileInput("uploadCSV", "Upload CSV File(s) from Previous Bullpens:", | |
| multiple = TRUE, | |
| accept = c(".csv")), | |
| actionButton("clearUploads", "Clear Uploaded Data", class = "btn-warning") | |
| ) | |
| ), | |
| fluidRow( | |
| box( | |
| title = "Leaderboard Options", | |
| status = "primary", | |
| solidHeader = TRUE, | |
| width = 12, | |
| fluidRow( | |
| column( | |
| 2, | |
| numericInput("minPitches", "Minimum Pitches:", value = 10, min = 1) | |
| ), | |
| column( | |
| 10, | |
| checkboxGroupInput( | |
| "leaderboardCols", | |
| "Select Columns:", | |
| choices = c( | |
| "Pitcher" = "Pitcher", | |
| "Total Pitches" = "Total", | |
| "Overall Strike %" = "StrikeRate", | |
| "FB Strike %" = "FB_StrikeRate", | |
| "OS Strike %" = "OS_StrikeRate" | |
| ), | |
| selected = c("Pitcher", "FB_StrikeRate", "OS_StrikeRate", "StrikeRate"), | |
| inline = TRUE | |
| ) | |
| ) | |
| ) | |
| ) | |
| ), | |
| fluidRow( | |
| box( | |
| title = "Session Leaderboard", | |
| status = "warning", | |
| solidHeader = TRUE, | |
| width = 12, | |
| downloadButton("downloadLeaderboardCSV", "Download CSV", class = "btn-success", style = "margin-bottom: 15px; margin-right: 10px;"), | |
| downloadButton("downloadLeaderboardPNG", "Download PNG", class = "btn-primary", style = "margin-bottom: 15px;"), | |
| DT::dataTableOutput("leaderboard") | |
| ) | |
| ) | |
| ), | |
| # EXPORT TAB | |
| tabItem( | |
| tabName = "export", | |
| fluidRow( | |
| box( | |
| title = "Export Bullpen Data", | |
| status = "success", | |
| solidHeader = TRUE, | |
| width = 6, | |
| textInput("exportFilename", "Bullpen Session Name:", | |
| placeholder = "LastName1_LastName2_Bullpen"), | |
| downloadButton("downloadData", "Download Bullpen CSV", class = "btn-primary btn-lg") | |
| ), | |
| box( | |
| title = "Export Game Data", | |
| status = "info", | |
| solidHeader = TRUE, | |
| width = 6, | |
| textInput("exportGameFilename", "Game Name:", | |
| placeholder = "Game_vs_Opponent"), | |
| downloadButton("downloadGameData", "Download Game CSV", class = "btn-primary btn-lg") | |
| ) | |
| ) | |
| ) | |
| ) | |
| ) | |
| ) | |
| server <- function(input, output, session) { | |
| values <- reactiveValues( | |
| pitchers = character(0), | |
| pitchData = data.frame(), | |
| uploadedData = data.frame(), | |
| currentPitchType = NULL, | |
| currentIntendedVert = NULL, | |
| currentIntendedHorz = NULL, | |
| # Game tracking | |
| gamePitchers = character(0), | |
| gameData = data.frame(), | |
| gamePitchType = NULL, | |
| gameIntendedVert = NULL, | |
| gameIntendedHorz = NULL | |
| ) | |
| # BULLPEN SECTION | |
| output$selectionDisplay <- renderUI({ | |
| parts <- c() | |
| if (!is.null(values$currentPitchType)) { | |
| parts <- c(parts, paste("Type:", values$currentPitchType)) | |
| } | |
| if (!is.null(values$currentIntendedVert) || !is.null(values$currentIntendedHorz)) { | |
| loc <- paste(c(values$currentIntendedVert, values$currentIntendedHorz), collapse = " + ") | |
| parts <- c(parts, paste("Intended:", loc)) | |
| } | |
| if (length(parts) == 0) { | |
| HTML("<span style='color: #999;'>Select pitch details...</span>") | |
| } else { | |
| HTML(paste(parts, collapse = " | ")) | |
| } | |
| }) | |
| observeEvent(input$addPitcher, { | |
| req(input$pitcherName) | |
| name <- trimws(input$pitcherName) | |
| if (name != "" && !(name %in% values$pitchers)) { | |
| values$pitchers <- c(values$pitchers, name) | |
| updateSelectInput(session, "currentPitcher", choices = values$pitchers, selected = name) | |
| updateTextInput(session, "pitcherName", value = "") | |
| showNotification(paste("Added", name), type = "message") | |
| } | |
| }) | |
| output$currentStats <- renderText({ | |
| req(input$currentPitcher) | |
| data <- values$pitchData %>% filter(Pitcher == input$currentPitcher) | |
| if (nrow(data) == 0) return("No pitches yet") | |
| total <- nrow(data) | |
| strikes <- sum(data$Result == "Strike") | |
| strike_pct <- round(strikes / total * 100, 1) | |
| fb_data <- data %>% filter(PitchType %in% c("Fastball", "Sinker", "Cutter")) | |
| fb_total <- nrow(fb_data) | |
| fb_strikes <- sum(fb_data$Result == "Strike") | |
| fb_pct <- if (fb_total > 0) round(fb_strikes / fb_total * 100, 1) else 0 | |
| os_data <- data %>% filter(PitchType %in% c("Curveball", "Slider", "Changeup")) | |
| os_total <- nrow(os_data) | |
| os_strikes <- sum(os_data$Result == "Strike") | |
| os_pct <- if (os_total > 0) round(os_strikes / os_total * 100, 1) else 0 | |
| paste0( | |
| "Total Pitches: ", total, "\n", | |
| "==================\n", | |
| "Overall Strike%: ", strike_pct, "%\n", | |
| "FB Strike%: ", fb_pct, "% (", fb_total, " FB)\n", | |
| "OS Strike%: ", os_pct, "% (", os_total, " OS)" | |
| ) | |
| }) | |
| observeEvent(input$pitch_fb, { values$currentPitchType <- "Fastball" }) | |
| observeEvent(input$pitch_cb, { values$currentPitchType <- "Curveball" }) | |
| observeEvent(input$pitch_ch, { values$currentPitchType <- "Changeup" }) | |
| observeEvent(input$pitch_sl, { values$currentPitchType <- "Slider" }) | |
| observeEvent(input$pitch_ct, { values$currentPitchType <- "Cutter" }) | |
| observeEvent(input$pitch_sn, { values$currentPitchType <- "Sinker" }) | |
| observeEvent(input$int_up, { values$currentIntendedVert <- "Up" }) | |
| observeEvent(input$int_down, { values$currentIntendedVert <- "Down" }) | |
| observeEvent(input$int_away, { values$currentIntendedHorz <- "Away" }) | |
| observeEvent(input$int_middle, { values$currentIntendedHorz <- "Middle" }) | |
| observeEvent(input$int_in, { values$currentIntendedHorz <- "In" }) | |
| recordPitch <- function(result) { | |
| req(input$currentPitcher, values$currentPitchType) | |
| intended_loc <- paste(c(values$currentIntendedVert, values$currentIntendedHorz), collapse = " + ") | |
| if (intended_loc == "") intended_loc <- NA | |
| new_pitch <- data.frame( | |
| Pitcher = input$currentPitcher, | |
| PitchType = values$currentPitchType, | |
| BatterSide = input$batterSide, | |
| IntendedLocation = intended_loc, | |
| Result = result, | |
| Timestamp = Sys.time(), | |
| stringsAsFactors = FALSE | |
| ) | |
| values$pitchData <- rbind(values$pitchData, new_pitch) | |
| values$currentPitchType <- NULL | |
| values$currentIntendedVert <- NULL | |
| values$currentIntendedHorz <- NULL | |
| showNotification(paste(result, "recorded!"), type = "message", duration = 1) | |
| } | |
| observeEvent(input$result_strike, { recordPitch("Strike") }) | |
| observeEvent(input$result_ball, { recordPitch("Ball") }) | |
| observeEvent(input$clearPitch, { | |
| values$currentPitchType <- NULL | |
| values$currentIntendedVert <- NULL | |
| values$currentIntendedHorz <- NULL | |
| }) | |
| output$recentPitches <- DT::renderDataTable({ | |
| if (nrow(values$pitchData) == 0) { | |
| return(data.frame(Message = "No pitches recorded")) | |
| } | |
| recent <- tail(values$pitchData, 20) %>% | |
| arrange(desc(Timestamp)) %>% | |
| select(Pitcher, PitchType, BatterSide, IntendedLocation, Result) | |
| DT::datatable(recent, options = list(pageLength = 20, dom = 't'), rownames = FALSE) | |
| }) | |
| # GAME SECTION | |
| output$gameSelectionDisplay <- renderUI({ | |
| parts <- c() | |
| if (!is.null(values$gamePitchType)) { | |
| parts <- c(parts, paste("Call:", values$gamePitchType)) | |
| } | |
| if (!is.null(values$gameIntendedVert) || !is.null(values$gameIntendedHorz)) { | |
| loc <- paste(c(values$gameIntendedVert, values$gameIntendedHorz), collapse = " + ") | |
| parts <- c(parts, paste("Location:", loc)) | |
| } | |
| if (length(parts) == 0) { | |
| HTML("<span style='color: #999;'>Select pitch call and location...</span>") | |
| } else { | |
| HTML(paste(parts, collapse = " | ")) | |
| } | |
| }) | |
| observeEvent(input$addGamePitcher, { | |
| req(input$gamePitcherName) | |
| name <- trimws(input$gamePitcherName) | |
| if (name != "" && !(name %in% values$gamePitchers)) { | |
| values$gamePitchers <- c(values$gamePitchers, name) | |
| updateSelectInput(session, "currentGamePitcher", choices = values$gamePitchers, selected = name) | |
| updateTextInput(session, "gamePitcherName", value = "") | |
| showNotification(paste("Added", name, "to game"), type = "message") | |
| } | |
| }) | |
| output$gameStats <- renderText({ | |
| req(input$currentGamePitcher) | |
| data <- values$gameData %>% filter(Pitcher == input$currentGamePitcher) | |
| if (nrow(data) == 0) return("No pitches yet") | |
| total <- nrow(data) | |
| fb_data <- data %>% filter(PitchCall %in% c("Fastball", "Sinker", "Cutter")) | |
| fb_total <- nrow(fb_data) | |
| os_data <- data %>% filter(PitchCall %in% c("Curveball", "Slider", "Changeup")) | |
| os_total <- nrow(os_data) | |
| paste0( | |
| "Total Pitches: ", total, "\n", | |
| "==================\n", | |
| "Fastballs: ", fb_total, "\n", | |
| "Offspeed: ", os_total | |
| ) | |
| }) | |
| observeEvent(input$game_pitch_fb, { values$gamePitchType <- "Fastball" }) | |
| observeEvent(input$game_pitch_cb, { values$gamePitchType <- "Curveball" }) | |
| observeEvent(input$game_pitch_ch, { values$gamePitchType <- "Changeup" }) | |
| observeEvent(input$game_pitch_sl, { values$gamePitchType <- "Slider" }) | |
| observeEvent(input$game_pitch_ct, { values$gamePitchType <- "Cutter" }) | |
| observeEvent(input$game_pitch_sn, { values$gamePitchType <- "Sinker" }) | |
| observeEvent(input$game_int_up, { values$gameIntendedVert <- "Up" }) | |
| observeEvent(input$game_int_down, { values$gameIntendedVert <- "Down" }) | |
| observeEvent(input$game_int_away, { values$gameIntendedHorz <- "Away" }) | |
| observeEvent(input$game_int_middle, { values$gameIntendedHorz <- "Middle" }) | |
| observeEvent(input$game_int_in, { values$gameIntendedHorz <- "In" }) | |
| observeEvent(input$recordGamePitch, { | |
| req(input$currentGamePitcher, values$gamePitchType) | |
| intended_loc <- paste(c(values$gameIntendedVert, values$gameIntendedHorz), collapse = " + ") | |
| if (intended_loc == "") intended_loc <- NA | |
| new_pitch <- data.frame( | |
| Pitcher = input$currentGamePitcher, | |
| PitchCall = values$gamePitchType, | |
| BatterSide = input$gameBatterSide, | |
| IntendedLocation = intended_loc, | |
| Timestamp = Sys.time(), | |
| stringsAsFactors = FALSE | |
| ) | |
| values$gameData <- rbind(values$gameData, new_pitch) | |
| values$gamePitchType <- NULL | |
| values$gameIntendedVert <- NULL | |
| values$gameIntendedHorz <- NULL | |
| showNotification("Pitch recorded!", type = "message", duration = 1) | |
| }) | |
| observeEvent(input$clearGamePitch, { | |
| values$gamePitchType <- NULL | |
| values$gameIntendedVert <- NULL | |
| values$gameIntendedHorz <- NULL | |
| }) | |
| output$gamePitches <- DT::renderDataTable({ | |
| if (nrow(values$gameData) == 0) { | |
| return(data.frame(Message = "No pitches recorded")) | |
| } | |
| recent <- tail(values$gameData, 50) %>% | |
| arrange(desc(Timestamp)) %>% | |
| select(Pitcher, PitchCall, BatterSide, IntendedLocation) | |
| DT::datatable(recent, options = list(pageLength = 50, dom = 't'), rownames = FALSE) | |
| }) | |
| # LEADERBOARD SECTION | |
| observeEvent(input$uploadCSV, { | |
| req(input$uploadCSV) | |
| tryCatch({ | |
| uploaded_list <- lapply(input$uploadCSV$datapath, function(path) { | |
| read_csv(path, show_col_types = FALSE) | |
| }) | |
| new_data <- bind_rows(uploaded_list) | |
| values$uploadedData <- bind_rows(values$uploadedData, new_data) | |
| showNotification(paste("Uploaded", nrow(new_data), "pitches from", length(uploaded_list), "file(s)"), | |
| type = "message") | |
| }, error = function(e) { | |
| showNotification(paste("Error uploading files:", e$message), type = "error") | |
| }) | |
| }) | |
| observeEvent(input$clearUploads, { | |
| values$uploadedData <- data.frame() | |
| showNotification("Cleared uploaded data", type = "warning") | |
| }) | |
| generate_leaderboard <- function() { | |
| all_data <- bind_rows(values$pitchData, values$uploadedData) | |
| if (nrow(all_data) == 0) { | |
| return(NULL) | |
| } | |
| leaderboard <- all_data %>% | |
| group_by(Pitcher) %>% | |
| summarise( | |
| Total = n(), | |
| Strikes = sum(Result == "Strike"), | |
| StrikeRate = round(Strikes / Total * 100, 1), | |
| FB_Total = sum(PitchType %in% c("Fastball", "Sinker", "Cutter")), | |
| FB_Strikes = sum(Result == "Strike" & PitchType %in% c("Fastball", "Sinker", "Cutter")), | |
| FB_StrikeRate = if_else(FB_Total > 0, round(FB_Strikes / FB_Total * 100, 1), NA_real_), | |
| OS_Total = sum(PitchType %in% c("Curveball", "Slider", "Changeup")), | |
| OS_Strikes = sum(Result == "Strike" & PitchType %in% c("Curveball", "Slider", "Changeup")), | |
| OS_StrikeRate = if_else(OS_Total > 0, round(OS_Strikes / OS_Total * 100, 1), NA_real_), | |
| .groups = 'drop' | |
| ) %>% | |
| filter(Total >= input$minPitches) | |
| return(leaderboard) | |
| } | |
| prepare_leaderboard_display <- function(leaderboard) { | |
| if (is.null(leaderboard)) return(NULL) | |
| leaderboard_display <- leaderboard | |
| if ("Pitcher" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(PLAYER = Pitcher) | |
| if ("FB_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`FB STRIKE%` = FB_StrikeRate) | |
| if ("OS_StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OS STRIKE%` = OS_StrikeRate) | |
| if ("StrikeRate" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`OVR STRIKE%` = StrikeRate) | |
| if ("Total" %in% colnames(leaderboard)) leaderboard_display <- leaderboard_display %>% rename(`TOTAL PITCHES` = Total) | |
| if (!is.null(input$leaderboardCols) && length(input$leaderboardCols) > 0) { | |
| cols_to_keep <- input$leaderboardCols | |
| col_mapping <- c( | |
| "Pitcher" = "PLAYER", | |
| "Total" = "TOTAL PITCHES", | |
| "StrikeRate" = "OVR STRIKE%", | |
| "FB_StrikeRate" = "FB STRIKE%", | |
| "OS_StrikeRate" = "OS STRIKE%" | |
| ) | |
| display_cols <- col_mapping[cols_to_keep] | |
| display_cols <- display_cols[!is.na(display_cols)] | |
| leaderboard_display <- leaderboard_display %>% select(all_of(display_cols)) | |
| } | |
| return(leaderboard_display) | |
| } | |
| output$leaderboard <- DT::renderDataTable({ | |
| leaderboard <- generate_leaderboard() | |
| if (is.null(leaderboard)) { | |
| return(data.frame(Message = "No data yet")) | |
| } | |
| leaderboard_display <- prepare_leaderboard_display(leaderboard) | |
| dt <- DT::datatable( | |
| leaderboard_display, | |
| options = list( | |
| pageLength = 25, | |
| scrollX = TRUE, | |
| ordering = TRUE, | |
| dom = 't', | |
| columnDefs = list( | |
| list(className = 'dt-center', targets = '_all') | |
| ) | |
| ), | |
| rownames = FALSE, | |
| class = 'cell-border stripe' | |
| ) %>% | |
| formatStyle( | |
| columns = colnames(leaderboard_display), | |
| fontSize = '18px', | |
| fontWeight = 'bold', | |
| color = 'black', | |
| border = '2px solid black' | |
| ) %>% | |
| formatStyle( | |
| columns = 1, | |
| fontWeight = 'bold', | |
| border = '2px solid black' | |
| ) | |
| pct_cols <- intersect( | |
| c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"), | |
| colnames(leaderboard_display) | |
| ) | |
| for (col in pct_cols) { | |
| dt <- dt %>% | |
| formatStyle( | |
| col, | |
| backgroundColor = styleInterval( | |
| cuts = c(30, 40, 50, 55, 60, 65, 70), | |
| values = c('#FF6B6B', '#FF8C8C', '#FFD700', '#FFED4E', '#90EE90', '#7CCD7C', '#32CD32', '#228B22') | |
| ), | |
| color = 'black', | |
| fontWeight = 'bold', | |
| border = '2px solid black' | |
| ) | |
| } | |
| dt | |
| }) | |
| output$downloadLeaderboardCSV <- downloadHandler( | |
| filename = function() { | |
| paste0("Leaderboard_", format(Sys.Date(), "%m%d"), ".csv") | |
| }, | |
| content = function(file) { | |
| leaderboard <- generate_leaderboard() | |
| if (!is.null(leaderboard)) { | |
| write_csv(leaderboard, file) | |
| } else { | |
| write_csv(data.frame(Message = "No data"), file) | |
| } | |
| } | |
| ) | |
| output$downloadLeaderboardPNG <- downloadHandler( | |
| filename = function() { | |
| paste0("Leaderboard_", format(Sys.Date(), "%m%d"), ".png") | |
| }, | |
| content = function(file) { | |
| leaderboard <- generate_leaderboard() | |
| if (is.null(leaderboard)) { | |
| showNotification("No data to export", type = "error") | |
| return() | |
| } | |
| leaderboard_display <- prepare_leaderboard_display(leaderboard) | |
| if (is.null(leaderboard_display)) { | |
| showNotification("Error preparing leaderboard data", type = "error") | |
| return() | |
| } | |
| dt <- DT::datatable( | |
| leaderboard_display, | |
| options = list( | |
| pageLength = nrow(leaderboard_display), | |
| dom = 't', | |
| ordering = FALSE, | |
| columnDefs = list( | |
| list(className = 'dt-center', targets = '_all') | |
| ) | |
| ), | |
| rownames = FALSE, | |
| class = 'cell-border stripe' | |
| ) %>% | |
| formatStyle( | |
| columns = colnames(leaderboard_display), | |
| fontSize = '18px', | |
| fontWeight = 'bold', | |
| color = 'black', | |
| border = '2px solid black' | |
| ) %>% | |
| formatStyle( | |
| columns = 1, | |
| fontWeight = 'bold', | |
| border = '2px solid black' | |
| ) | |
| pct_cols <- intersect( | |
| c("FB STRIKE%", "OS STRIKE%", "OVR STRIKE%"), | |
| colnames(leaderboard_display) | |
| ) | |
| for (col in pct_cols) { | |
| dt <- dt %>% | |
| formatStyle( | |
| col, | |
| backgroundColor = styleInterval( | |
| cuts = c(30, 40, 50, 55, 60, 65, 70), | |
| values = c('#FF6B6B', '#FF8C8C', '#FFD700', '#FFED4E', '#90EE90', '#7CCD7C', '#32CD32', '#228B22') | |
| ), | |
| color = 'black', | |
| fontWeight = 'bold', | |
| border = '2px solid black' | |
| ) | |
| } | |
| temp_html <- tempfile(fileext = ".html") | |
| htmlwidgets::saveWidget(dt, temp_html, selfcontained = TRUE) | |
| tryCatch({ | |
| webshot2::webshot(temp_html, file = file, vwidth = 1400, vheight = 1000, delay = 0.5) | |
| showNotification("Leaderboard PNG downloaded successfully!", type = "message") | |
| }, error = function(e) { | |
| showNotification(paste("Error creating PNG:", e$message), type = "error") | |
| }) | |
| unlink(temp_html) | |
| } | |
| ) | |
| # EXPORT HANDLERS | |
| output$downloadData <- downloadHandler( | |
| filename = function() { | |
| base_name <- if (!is.null(input$exportFilename) && input$exportFilename != "") { | |
| gsub(" ", "_", trimws(input$exportFilename)) | |
| } else { | |
| "Bullpen_Session" | |
| } | |
| date_str <- format(Sys.Date(), "%m%d") | |
| paste0(base_name, "_", date_str, ".csv") | |
| }, | |
| content = function(file) { | |
| write_csv(values$pitchData, file) | |
| } | |
| ) | |
| output$downloadGameData <- downloadHandler( | |
| filename = function() { | |
| base_name <- if (!is.null(input$exportGameFilename) && input$exportGameFilename != "") { | |
| gsub(" ", "_", trimws(input$exportGameFilename)) | |
| } else { | |
| "Game_Data" | |
| } | |
| date_str <- format(Sys.Date(), "%m%d") | |
| paste0(base_name, "_", date_str, ".csv") | |
| }, | |
| content = function(file) { | |
| write_csv(values$gameData, file) | |
| } | |
| ) | |
| } | |
| shinyApp(ui = ui, server = server) |