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("Select pitch details...")
} 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("Select pitch call and location...")
} 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)