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)