| library(shiny) |
| library(DT) |
| library(dplyr) |
| |
| 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) { |
| |
| content_text <- content(response, "text", encoding = "UTF-8") |
| |
| |
| if (grepl("^version https://git-lfs.github.com/spec/", content_text)) { |
| |
| oid_line <- grep("oid sha256:", strsplit(content_text, "\n")[[1]], value = TRUE) |
| oid <- gsub("oid sha256:", "", oid_line) |
| oid <- trimws(oid) |
| |
| |
| lfs_url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/.git/lfs/objects/", |
| substr(oid, 1, 2), "/", substr(oid, 3, 4), "/", oid) |
| |
| |
| 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 { |
| |
| 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))) |
| } |
| } |
| } |
| |
| |
| 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) |
| |
| |
| url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename, "?download=true") |
| |
| |
| temp_file <- tempfile(fileext = ".parquet") |
| |
| |
| response <- GET( |
| url, |
| add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))), |
| write_disk(temp_file, overwrite = TRUE) |
| ) |
| |
| |
| if (status_code(response) == 200) { |
| tryCatch({ |
| |
| 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" |
| |
|
|
| MLB26 <- download_private_parquet("TimStats/StatcastDataAll", "MLB26.parquet") |
| MLB26$level <- "MLB" |
| AAA26 <- download_private_parquet("TimStats/StatcastDataAll", "AAA26.parquet") |
| AAA26$level <- "AAA" |
| |
| |
|
|
| |
| |
| 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 <- game %>% |
| mutate( |
| |
| EAA = calculate_EAA(extension), |
| SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection), |
| |
| |
| |
| |
| ishandL = ifelse(phand == "L",1,0)) |
| |
| |
| 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$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) |
| ) |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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_ |
| )) %>% |
| 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_ |
| ))%>% |
| 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_ |
| ))%>% |
| 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 <- 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") |
|
|
| |
| ui <- fluidPage( |
| theme = bs_theme(preset = "united"), |
| titlePanel("MLB/AAA/FSL Statcast Data"), |
| |
| fluidRow( |
| |
| 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, |
| |
| 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") |
| ) |
| ), |
| |
| |
| 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) |
| |
| |
| |
| |
| |
| processed_data <- eventReactive(input$update_table, { |
| req(input$level, input$stats, input$group) |
| |
| |
| 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])) |
| |
| 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)), |
| across(where(is.numeric) & !c('xwOBA', 'xDamage'), ~round(., 1)) |
| ) |
| |
| |
| 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 |
| ) |
| } |
| } |
| |
| |
| filtered_data %>% |
| select(all_of(c(input$group, input$stats))) |
| }) |
| |
| |
| output$table <- renderTabulator({ |
| req(processed_data()) |
| group_columns <- map(input$group, function(col) { |
| list( |
| field = col, |
| title = col, |
| width = 175, |
| headerFilter = "input", |
| headerFilterPlaceholder = paste("Filter", col) |
| ) |
| }) |
| |
| |
| 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) |
| } |
| ) |
| }) |
| |
| |
| column_defs <- c(group_columns, stat_columns) |
| |
| tabulator( |
| processed_data(), |
| options = list( |
| pagination = TRUE, |
| paginationSize = 20, |
| paginationSizeSelector = c(10, 20, 50, 100), |
| selectable = TRUE, |
| layout = "fitColumns", |
| columns = column_defs |
| ) |
| ) |
| }) |
| |
| 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) |
| }) |
| }) |
| |
| |
| 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;" |
| ) |
| ) |
| ) |
| }) |
| }) |
| } |
|
|
| |
| shinyApp(ui = ui, server = server) |