TimStats's picture
Update app.R
d0848d9 verified
Raw
History Blame Contribute Delete
27 kB
library(shiny)
library(DT)
library(dplyr)
#library(tidyverse)
library(xgboost)
library(httr)
library(bslib)
library(rtabulator)
library(purrr)
library(arrow)
updatedDate <- Sys.Date() - 1
download_private_csv <- function(repo_id, filename) {
url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename)
response <- GET(url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
if (status_code(response) == 200) {
# Get content as text first to check if it's an LFS pointer
content_text <- content(response, "text", encoding = "UTF-8")
# Check if this is an LFS pointer (LFS files start with "version https://git-lfs.github.com/spec/")
if (grepl("^version https://git-lfs.github.com/spec/", content_text)) {
# This is an LFS file - extract the oid (hash) from the pointer
oid_line <- grep("oid sha256:", strsplit(content_text, "\n")[[1]], value = TRUE)
oid <- gsub("oid sha256:", "", oid_line)
oid <- trimws(oid)
# Construct the LFS content URL
lfs_url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/.git/lfs/objects/",
substr(oid, 1, 2), "/", substr(oid, 3, 4), "/", oid)
# Get the actual content from LFS storage
lfs_response <- GET(lfs_url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
if (status_code(lfs_response) == 200) {
content_text <- content(lfs_response, "text", encoding = "UTF-8")
} else {
# Alternative LFS URL format
lfs_url <- paste0("https://huggingface.co/datasets/", repo_id, "/lfs/resolve/main/", filename, "?download=true")
lfs_response <- GET(lfs_url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
if (status_code(lfs_response) == 200) {
content_text <- content(lfs_response, "text", encoding = "UTF-8")
} else {
stop(paste("Failed to download LFS content. Status code:", status_code(lfs_response)))
}
}
}
# Process the content (whether it was LFS or regular)
con <- textConnection(content_text)
tryCatch({
data <- read.csv(con,
header = TRUE,
check.names = FALSE,
fileEncoding = "UTF-8",
stringsAsFactors = FALSE)
return(data)
}, error = function(e) {
close(con)
stop(paste("Error parsing CSV:", e$message))
}, finally = {
close(con)
})
} else {
stop(paste("Failed to download dataset. Status code:", status_code(response)))
}
}
download_private_parquet <- function(repo_id, filename) {
library(httr)
library(arrow)
# Create the direct download URL based on your example
url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename, "?download=true")
# Create a temporary file
temp_file <- tempfile(fileext = ".parquet")
# Download directly to file
response <- GET(
url,
add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))),
write_disk(temp_file, overwrite = TRUE)
)
# Check if download was successful
if (status_code(response) == 200) {
tryCatch({
# Read the parquet file
data <- read_parquet(temp_file)
file.remove(temp_file)
return(data)
}, error = function(e) {
file.remove(temp_file)
stop(paste("Error reading parquet file:", e$message))
})
} else {
file.remove(temp_file)
stop(paste("Failed to download file. Status code:", status_code(response)))
}
}
MLB25 <- download_private_parquet("TimStats/StatcastDataAll", "MLB25.parquet")
MLB25$level <- "MLB"
AAA25 <- download_private_parquet("TimStats/StatcastDataAll", "AAA25.parquet")
AAA25$level <- "AAA"
FSL25 <- download_private_parquet("TimStats/StatcastDataAll", "FSL25.parquet")
FSL25$level <- "FSL"
#ST <- read.csv("SpringT25.csv", header = TRUE, check.names = FALSE, fileEncoding = "UTF-8")
MLB26 <- download_private_parquet("TimStats/StatcastDataAll", "MLB26.parquet")
MLB26$level <- "MLB"
AAA26 <- download_private_parquet("TimStats/StatcastDataAll", "AAA26.parquet")
AAA26$level <- "AAA"
#FSL26 <- download_private_parquet("TimStats/StatcastDataAll", "FSL26.parquet")
#FSL26$level <- "FSL"
#names(ST)
MLB <- download_private_parquet("TimStats/StatcastDataAll", "MLB.parquet")
MLB$level <- "MLB"
AAA <- download_private_parquet("TimStats/StatcastDataAll", "AAA.parquet")
AAA$level <- "AAA"
FSLAll <- download_private_parquet("TimStats/StatcastDataAll", "FSL.parquet")
FSLAll$level <- "FSL"
MLB <- rbind(MLB,MLB25,MLB26)
print("aaa")
AAA <- rbind(AAA,AAA25,AAA26)
print("fsl")
FSL <- rbind(FSLAll,FSL25)
is_barrel <- function(df) {
df$barrel <- with(df, ifelse(hit_angle <= 50 & hit_speed >= 97 & hit_speed * 1.5 -
hit_angle >= 117 & hit_speed + hit_angle >= 123, 1, 0))
return(df)
}
VAA <- function(milbtotal){
milbtotal <- milbtotal |>
mutate(vaa = -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi))
}
calculate_EAA <- function(extension) {
extension / 6.3
}
calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
inSA <- ifelse(inSA < 0, inSA + 360, inSA)
SADiff <- spinDirection - inSA
SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
return(SADiff)
}
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
}
calculate_timstuff <- function(game) {
# game <- calculate_primary(game)
game <- game %>%
mutate(
#VAA = calculate_VAA(vz0, ay, az, vy0, y0),
EAA = calculate_EAA(extension),
SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection),
#team_fielding_id = ifelse(description %in% c("Called Strike", "Swinging Strike", "Swinging Strike (Blocked)"), 1, 0),
# swing = ifelse(description %in% c("Foul", "Foul Pitchout", "In play, no out", "In play, out(s)", "In play, run(s)", "Swinging Strike", "Swinging Strike (Blocked)", "Foul Tip"), 1, 0),
#is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
#Pitch = pitch_name,
ishandL = ifelse(phand == "L",1,0))
# game <- calculate_primary(game)
feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff")
complete_rows <- complete.cases(game[, feature_vars])
game_complete <- game[complete_rows, ]
game_na <- game[!complete_rows,]
game_na$TimStuff <- NA
rhp <- game_complete
# rhp <- game_complete[game_complete$ishandL == 0]
#
# lhp$TimStuff <- scale_TimStuff(predict(model, as.matrix(cbind(lhp$ishandL,lhp$start_speed, lhp$IVB, lhp$HB, lhp$EAA, lhp$x0, lhp$z0, lhp$spin_rate, lhp$SADiff,lhp$primary_speed,lhp$primary_IVB,lhp$primary_HB))), -0.00249975, 0.007566558)
rhp$TimStuff <- scale_TimStuff(predict(model, as.matrix(cbind(rhp$ishandL,rhp$start_speed, rhp$IVB, rhp$HB, rhp$EAA, rhp$x0, rhp$z0, rhp$spin_rate, rhp$SADiff))), -0.002620635, 0.006021368)
game_complete <- rbind(rhp,game_na)
return(game_complete)
}
scale_TimStuff <- function(raw_score, model_mean, model_sd) {
scaled_score <- (raw_score - model_mean) / model_sd
result <- 100 - (scaled_score * 10)
return(result)
}
model <- xgb.load('TimStuff2.ubj')
#######
Swing <- function(milbtotal){
milbtotal <- milbtotal %>%
mutate(swing = ifelse(description == "Foul" |
description == "Foul Pitchout" |
description == "In play, no out" |
description == "In play, out(s)" |
description == "In play, run(s)" |
description == "Swinging Strike" |
description == "swinging Strike (Blocked)" |
description == "Foul Tip",1,0))
}
addChecks <- function(df){
df <- is_barrel(df)
df %>% mutate(
InPlayCheck = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE),
SwingCheck = case_when(description %in% c('Foul','Foul Bunt','Foul Pitchout','Foul Tip',
'In play, run(s)','In play, out(s)','In play, no out',
'Swinging Strike','Swinging Strike (Blocked)',
'Missed Bunt') ~ TRUE, TRUE ~ FALSE),
ConCheck = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out',
'Foul','Foul Bunt','Foul Pitchout') ~ TRUE, TRUE ~ FALSE),
WhiffCheck = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)',
'Missed Bunt','Foul Tip') ~ TRUE, TRUE ~ FALSE),
CalledStrikeCheck = case_when(description %in% c('Called Strike') ~ TRUE, TRUE ~ FALSE),
CSWCheck = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)',
'Missed Bunt','Foul Tip','Called Strike') ~ TRUE, TRUE ~ FALSE),
StrikeCheck = case_when(description %in% c('Called Strike','Foul','Foul Bunt','Foul Pitchout',
'Foul Tip','In play, no out','In play, out(s)',
'In play, run(s)','Missed Bunt','Pitchout',
'Swinging Strike','Swinging Strike (Blocked)') ~ TRUE, TRUE ~ FALSE),
BallCheck = case_when(description %in% c('Ball','Ball In Dirt','Hit By Pitch') ~ TRUE, TRUE ~ FALSE),
SweetSpotCheck = case_when(between(hit_angle,10,30) ~ TRUE, TRUE ~ FALSE),
HardHitCheck = case_when(hit_speed >= 95 ~ TRUE, TRUE ~ FALSE),
ZoneCheck = ifelse(zone <= 9, TRUE, FALSE),
Single = case_when(result == "Single" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
Double = case_when(result == "Double" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
Triple = case_when(result == "Triple" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
`Home Run` = case_when(result == "Home Run" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
WalkCheck = case_when(balls >= 4 & result == "Walk" ~ TRUE, TRUE ~ FALSE),
HBPCheck = case_when(description == "Hit By Pitch" & result == "Hit By Pitch" ~ TRUE, TRUE ~ FALSE),
StrikeoutCheck = case_when(strikes >= 3 & result %in% c("Strikeout",'Strikeout Double Play') ~ TRUE, TRUE ~ FALSE),
SacrificeCheck = case_when(InPlayCheck == TRUE & result %in% c('Sac Fly','Sac Bunt',
'Sac Fly Double Play','Sac Bunt Double Play') ~ TRUE, TRUE ~ FALSE),
IBBCheck = case_when(pitchNum == 1 & result == "Intent Walk" ~ TRUE, TRUE ~ FALSE),
ABCheck = StrikeoutCheck + InPlayCheck - SacrificeCheck,
PACheck = ABCheck + WalkCheck + HBPCheck,
TopZoneCheck = if_else(zone < 4,TRUE,FALSE),
BotZoneCheck = if_else(zone > 6 & zone < 10,TRUE,FALSE),
CompSwingCheck = if_else(bat_speed >= 60 & hit_speed >= 90,TRUE,FALSE),
CompSwingCheck = ifelse(bat_speed >= quantile(bat_speed,.1,na.rm = TRUE),1,0)
)
}
#' test <- addChecks(MLB) %>% group_by(`Pitcher Name`,season,pitch_name) %>%
#' summarise(
#' Pitches = n(),
#' 'Avg Velo' = mean(start_speed,na.rm = TRUE),
#' 'Top Velo' = max(start_speed,na.rm = TRUE),
#' #'TimStuff+' = mean(TimStuff,na.rm = TRUE),
#' 'Max EV' = max(hit_speed,na.rm = TRUE),
#' 'Avg EV' = mean(hit_speed,na.rm = TRUE),
#' 'EV90' = quantile(hit_speed,0.9,na.rm = TRUE),
#' 'Avg LA' = mean(hit_angle,na.rm = TRUE),
#' 'stdevLA' = sd(hit_angle,na.rm = TRUE),
#' 'HardHit%' = mean(HardHitCheck[InPlayCheck == TRUE],na.rm = TRUE),
#' 'Barrel%' = mean(barrel[InPlayCheck == TRUE],na.rm = TRUE),
#' 'Sweet Spot%' = mean(SweetSpotCheck[InPlayCheck == TRUE],na.rm = TRUE),
#' 'xwOBA' = mean(expected_woba,na.rm = TRUE),
#' 'xwOBACON' = mean(expected_woba[InPlayCheck == TRUE],na.rm = TRUE),
#' 'Contact%' = mean(ConCheck[SwingCheck == TRUE],na.rm = TRUE),
#' 'ZCon%' = mean(ConCheck[ZoneCheck == TRUE] & SwingCheck == TRUE,na.rm = TRUE),
#' 'ZSwing%' = mean(SwingCheck[ZoneCheck == TRUE],na.rm = TRUE),
#' 'OCon%' = mean(ConCheck[ZoneCheck == FALSE] & SwingCheck == TRUE,na.rm = TRUE),
#' 'Chase%' = mean(SwingCheck[ZoneCheck == FALSE],na.rm = TRUE),
#' 'SwStr%' = mean(WhiffCheck,na.rm = TRUE),
#' 'Whiff%' = mean(WhiffCheck[SwingCheck == TRUE],na.rm = TRUE),
#' 'Zone%' = mean(ZoneCheck,na.rm = TRUE),
#' 'Strike%' = mean(StrikeCheck,na.rm = TRUE),
#' 'Swing%' = mean(SwingCheck,na.rm = TRUE),
#' 'Spin Rate' = mean(spin_rate,na.rm = TRUE),
#' 'Extension' = mean(extension,na.rm = TRUE),
#' 'IVB' = mean(IVB,na.rm = TRUE),
#' 'HB' = mean(HB,na.rm = TRUE),
#' # 'VAA' = mean(vaa,na.rm = TRUE),
#' # 't3VAA' = mean(vaa[TopZoneCheck == TRUE],na.rm = TRUE),
#' # 'b3VAA' = mean(vaa[BotZoneCheck == TRUE],na.rm = TRUE),
#' 'CSW%' = mean(CSWCheck,na.rm = TRUE),
#' 'Arm Angle' = mean(arm_angle,na.rm = TRUE),
#' 'Bat Speed' = mean(bat_speed[CompSwingCheck == TRUE],na.rm = TRUE)
#' )
print("mlbp")
print(colnames(MLB))
MLB_processed <- MLB %>%
as.data.frame() %>%
calculate_timstuff() %>%
addChecks() %>%
VAA() %>%
mutate('Pitch Name' = pitch_name) %>%
mutate('Season' = season) %>%
mutate('Level' = level) %>%
mutate('Pitch Type' = case_when(
pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
"Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
TRUE ~ NA_character_ # Corrected default case
)) %>%
mutate('Batter Side' = bside) %>%
mutate('Pitcher Hand' = phand) %>%
mutate('Stadium' = venue_name) %>%
mutate('Batter Home/Away' = ifelse(`Batter Team` == teams_home_team_name,"Home","Away")) %>%
mutate('Pitcher Home/Away' = ifelse(`Pitcher Team` == teams_home_team_name,"Home","Away"))
print("aaap")
AAA_processed <- AAA %>%
as.data.frame() %>%
calculate_timstuff() %>%
addChecks()%>%
VAA()%>%
mutate('Pitch Name' = pitch_name) %>%
mutate('Season' = season) %>%
mutate('Level' = level) %>%
mutate('Pitch Type' = case_when(
pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
"Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
TRUE ~ NA_character_ # Corrected default case
))%>%
mutate('Batter Side' = bside) %>%
mutate('Pitcher Hand' = phand) %>%
mutate('Stadium' = venue_name) %>%
mutate('Batter Home/Away' = ifelse(`Batter Team` == teams_home_team_name,"Home","Away")) %>%
mutate('Pitcher Home/Away' = ifelse(`Pitcher Team` == teams_home_team_name,"Home","Away"))
print("fslp")
FSL_processed <- FSL %>%
as.data.frame() %>%
calculate_timstuff() %>%
addChecks()%>%
VAA()%>%
mutate('Pitch Name' = pitch_name) %>%
mutate('Season' = season) %>%
mutate('Level' = level) %>%
mutate('Pitch Type' = case_when(
pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
"Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
TRUE ~ NA_character_ # Corrected default case
))%>%
mutate('Batter Side' = bside) %>%
mutate('Pitcher Hand' = phand) %>%
mutate('Stadium' = venue_name) %>%
mutate('Batter Home/Away' = ifelse(`Batter Team` == teams_home_team_name,"Home","Away")) %>%
mutate('Pitcher Home/Away' = ifelse(`Pitcher Team` == teams_home_team_name,"Home","Away"))
# Available stats for the UI
available_stats <- c("Pitches", "Avg Velo", "Top Velo", "TimStuff", "Max EV", "Avg EV",
"EV90", "Avg LA", "stdevLA", "HardHit%", "Barrel%", "SwSpt%",
"xwOBA", "xDamage", "Contact%", "ZCon%", "ZSwing%", "OCon%",
"Chase%", "SwStr%", "Whiff%", "Zone%", "Strike%", "Swing%",
"Spin Rate", "Extension", "IVB", "HB", "CSW%", "Arm Angle", "Bat Speed")
# Then your UI and server code
ui <- fluidPage(
theme = bs_theme(preset = "united"),
titlePanel("MLB/AAA/FSL Statcast Data"),
fluidRow(
# Left column (sidebar)
column(width = 3,
div(
style = "height: 100%; padding: 10px; background-color: #f8f9fa; border-right: 1px solid #dee2e6;",
p("X: ", a("(@TimStats)", href = "https://twitter.com/timstats")),
p("Data Contains 2020-2025"),
p("Data Updated To:", updatedDate),
p("Please avoid large multi-year, multi-league queries as they tend to crash the app"),
p("Arm Angle for the previous week will populate on Mondays, bat speed is a daily update"),
div(
class = "mb-3",
style = "background-color: white; padding: 15px; border-radius: 5px; margin-bottom: 15px;",
selectInput("level", "Level:",
c("MLB", "AAA", "FSL"),
multiple = TRUE
),
selectInput("group", "Group By:",
c("Pitcher Name", "Pitcher ID", "Pitch Name", "Batter Name",
"Batter ID", "Season", "Level", "Pitch Type", "Stadium",
"Batter Home/Away", "Pitcher Home/Away", "Pitcher Team",
"Batter Team","Batter Side"),
multiple = TRUE
),
selectInput("stats", "Select Stats:",
choices = available_stats,
multiple = TRUE
),
dateRangeInput("date_range", "Date Range:",
start = "2026-03-18",
end = updatedDate,
#min = "2024-02-23",
max = Sys.Date() - 1
)
),
actionButton("add_filter", "Add Filter", class = "btn-primary mb-3"),
uiOutput("filter_container"),
hr(),
actionButton("update_table", "Update Table", class = "btn-success")
)
),
# Right column (main content)
column(width = 9,
div(
style = "padding: 10px;",
tabulatorOutput("table", height = "800px")
)
)
)
)
server <- function(input, output, session) {
filters <- reactiveVal(list())
counter <- reactiveVal(0)
table_trigger <- reactiveVal(0)
last_click <- reactiveVal(0)
available_columns <- reactive({
stat_cols <- setNames(
input$stats,
input$stats
)
c(stat_cols)
})
removeClicks <- reactiveVal(0)
# observeEvent(input$update_table, {
# table_trigger(table_trigger() + 1)
# })
processed_data <- eventReactive(input$update_table, {
req(input$level, input$stats, input$group)
# Combine preprocessed data from selected levels
combined_data <- bind_rows(
if ("MLB" %in% input$level) MLB_processed,
if ("AAA" %in% input$level) AAA_processed,
if ("FSL" %in% input$level) FSL_processed
) %>%
filter(between(as.Date(date), input$date_range[1], input$date_range[2]))
# First group and calculate all stats
grouped_data <- combined_data %>%
group_by(across(all_of(input$group))) %>%
summarise(
Pitches = n(),
'Avg Velo' = mean(start_speed, na.rm = TRUE),
'Top Velo' = max(start_speed, na.rm = TRUE),
'TimStuff' = mean(TimStuff, na.rm = TRUE),
'Max EV' = max(hit_speed, na.rm = TRUE),
'Avg EV' = mean(hit_speed, na.rm = TRUE),
'EV90' = quantile(hit_speed, 0.9, na.rm = TRUE),
'Avg LA' = mean(hit_angle, na.rm = TRUE),
'stdevLA' = sd(hit_angle, na.rm = TRUE),
'HardHit%' = mean(HardHitCheck[InPlayCheck == TRUE], na.rm = TRUE) * 100,
'Barrel%' = mean(barrel[InPlayCheck == TRUE], na.rm = TRUE) * 100,
'SwSpt%' = mean(SweetSpotCheck[InPlayCheck == TRUE], na.rm = TRUE) * 100,
'xwOBA' = mean(expected_woba, na.rm = TRUE),
'xDamage' = mean(expected_woba[InPlayCheck == TRUE], na.rm = TRUE),
'Contact%' = mean(ConCheck[SwingCheck == TRUE], na.rm = TRUE) * 100,
'ZCon%' = mean(ConCheck[ZoneCheck == TRUE & SwingCheck == TRUE], na.rm = TRUE) * 100,
'ZSwing%' = mean(SwingCheck[ZoneCheck == TRUE], na.rm = TRUE) * 100,
'OCon%' = mean(ConCheck[ZoneCheck == FALSE & SwingCheck == TRUE], na.rm = TRUE) * 100,
'Chase%' = mean(SwingCheck[ZoneCheck == FALSE], na.rm = TRUE) * 100,
'SwStr%' = mean(WhiffCheck, na.rm = TRUE) * 100,
'Whiff%' = mean(WhiffCheck[SwingCheck == TRUE], na.rm = TRUE) * 100,
'Zone%' = mean(ZoneCheck, na.rm = TRUE) * 100,
'Strike%' = mean(StrikeCheck, na.rm = TRUE) * 100,
'Swing%' = mean(SwingCheck, na.rm = TRUE) * 100,
'Spin Rate' = mean(spin_rate, na.rm = TRUE),
'Extension' = mean(extension, na.rm = TRUE),
'IVB' = mean(IVB, na.rm = TRUE),
'HB' = mean(HB, na.rm = TRUE),
'VAA' = mean(vaa, na.rm = TRUE),
't3VAA' = mean(vaa[TopZoneCheck == TRUE], na.rm = TRUE),
'b3VAA' = mean(vaa[BotZoneCheck == TRUE], na.rm = TRUE),
'CSW%' = mean(CSWCheck, na.rm = TRUE) * 100,
'Arm Angle' = mean(arm_angle, na.rm = TRUE),
'Bat Speed' = mean(bat_speed[CompSwingCheck == TRUE], na.rm = TRUE),
.groups = 'drop'
) %>%
mutate(
across(c('xwOBA', 'xDamage'), ~round(., 3)), # wOBA metrics to 3 decimals
across(where(is.numeric) & !c('xwOBA', 'xDamage'), ~round(., 1)) # everything else to 1 decimal
)
# Then apply filters to the summarized data
filtered_data <- grouped_data
current_filters <- filters()
for (filter in current_filters) {
column <- input[[filter$column_id]]
operator <- input[[filter$operator_id]]
value <- input[[filter$value_id]]
if (!is.null(column) && !is.null(operator) && !is.null(value) && value != "") {
filtered_data <- switch(operator,
"eq" = filtered_data %>% filter(!!sym(column) == value),
"like" = filtered_data %>% filter(grepl(value, !!sym(column), ignore.case = TRUE)),
"gt" = filtered_data %>% filter(!!sym(column) > as.numeric(value)),
"lt" = filtered_data %>% filter(!!sym(column) < as.numeric(value)),
"gte" = filtered_data %>% filter(!!sym(column) >= as.numeric(value)),
"lte" = filtered_data %>% filter(!!sym(column) <= as.numeric(value)),
filtered_data
)
}
}
# Finally select only the requested columns
filtered_data %>%
select(all_of(c(input$group, input$stats)))
})
# Tabulator rendering with formatting
output$table <- renderTabulator({
req(processed_data())
group_columns <- map(input$group, function(col) {
list(
field = col,
title = col,
width = 175,
headerFilter = "input", # Add text filtering
headerFilterPlaceholder = paste("Filter", col)
)
})
# Create column definitions for stat columns
stat_columns <- map(input$stats, function(col) {
list(
field = col,
title = col,
width = 110,
formatter = if(col %in% c("xwOBA", "xDamage")) "number" else "number",
formatterParams = if(col %in% c("xwOBA", "xDamage")) {
list(precision = 3)
} else {
list(precision = 1)
}
)
})
# Combine column definitions
column_defs <- c(group_columns, stat_columns)
tabulator(
processed_data(),
options = list(
pagination = TRUE, # Enable pagination
paginationSize = 20, # Set page size to 10 rows
paginationSizeSelector = c(10, 20, 50, 100), # Allow users to change page size
selectable = TRUE,
layout = "fitColumns",
columns = column_defs
)
)
})
# Add observers for filter management and other reactive elements
observeEvent(input$add_filter, {
isolate({
current_counter <- counter()
filter_id <- paste0("filter_", current_counter)
new_filter <- list(
id = filter_id,
column_id = paste0("column_", filter_id),
operator_id = paste0("operator_", filter_id),
value_id = paste0("value_", filter_id)
)
current_filters <- filters()
filters(c(current_filters, list(new_filter)))
counter(current_counter + 1)
})
})
observe({
current_filters <- filters()
lapply(current_filters, function(filter) {
observeEvent(input[[paste0("remove_", filter$id)]], {
isolate({
removeClicks(removeClicks() + 1)
new_filters <- current_filters[sapply(current_filters, function(f) f$id != filter$id)]
filters(new_filters)
})
}, ignoreInit = TRUE, ignoreNULL = TRUE)
})
})
# Filter container UI
output$filter_container <- renderUI({
removeClicks()
current_filters <- filters()
lapply(current_filters, function(filter) {
div(
class = "mb-1",
div(
style = "display: flex; gap: 10px; align-items: center;",
selectInput(filter$column_id, "Column",
choices = available_columns(),
width = "200px",
selected = input[[filter$column_id]]
),
selectInput(filter$operator_id, "Stat",
choices = c(
"=" = "eq",
"contains" = "like",
">" = "gt",
"<" = "lt",
">=" = "gte",
"<=" = "lte"
),
width = "100px",
selected = input[[filter$operator_id]]
),
textInput(filter$value_id, "Value",
value = input[[filter$value_id]],
width = "150px"
),
actionButton(
paste0("remove_", filter$id),
icon("trash"),
class = "btn-danger btn-sm",
style = "margin-top: 22px;"
)
)
)
})
})
}
# Run the application
shinyApp(ui = ui, server = server)