Download app.R from TimStats/MinorLeagueStatcast: direct link, hf CLI and curl.
- Browser
- Download file 27 kB
-
https://huggingface.co/spaces/TimStats/MinorLeagueStatcast/resolve/main/app.R
- Command line
-
hf download hf://spaces/TimStats/MinorLeagueStatcast/app.R
-
curl -L -o app.R https://huggingface.co/spaces/TimStats/MinorLeagueStatcast/resolve/main/app.R
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) |