Download app.R from TimStats/Heatmaps: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/TimStats/Heatmaps/resolve/main/app.R
- Command line
-
hf download hf://spaces/TimStats/Heatmaps/app.R
-
curl -L -o app.R https://huggingface.co/spaces/TimStats/Heatmaps/resolve/main/app.R
14.5 kB
| library(shiny) | |
| library(bslib) | |
| library(shinyWidgets) | |
| library(ggplot2) | |
| library(MASS) # For kde2d function | |
| library(viridis) # For color palette | |
| library(dplyr) # For data manipulation | |
| library(httr) | |
| library(patchwork) # For combining plots | |
| library(jsonlite) | |
| library(arrow) | |
| updatedDate <- Sys.Date() - 1 | |
| check_patreon_access <- function(email) { | |
| campaign_id <- Sys.getenv("PATREON_CAMPAIGN_ID") | |
| access_token <- Sys.getenv("PATREON_ACCESS_TOKEN") | |
| base_url <- paste0("https://www.patreon.com/api/oauth2/v2/campaigns/", campaign_id, "/members") | |
| params <- list( | |
| `include` = "currently_entitled_tiers", | |
| `fields[member]` = "patron_status,email", | |
| `fields[tier]` = "title" | |
| ) | |
| response <- GET( | |
| base_url, | |
| query = params, | |
| add_headers( | |
| `Authorization` = paste("Bearer", access_token), | |
| `User-Agent` = "R/httr" | |
| ) | |
| ) | |
| content <- fromJSON(rawToChar(response$content)) | |
| # Check in data$attributes for matching email | |
| matching_row <- which(content$data$attributes$email == email) | |
| if (length(matching_row) > 0) { | |
| # Get patron status | |
| patron_status <- content$data$attributes$patron_status[matching_row] | |
| if (patron_status == "active_patron") { | |
| # Get tier info | |
| tier_data <- content$data$relationships$currently_entitled_tiers$data[[matching_row]] | |
| tier_id <- tier_data$id | |
| result <- tier_id %in% c("25062087", "25062090") | |
| return(result) | |
| } | |
| } | |
| return(FALSE) | |
| } | |
| 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 <- content(response, "text") | |
| con <- textConnection(content) | |
| # Try different read options | |
| data <- read.csv(con, | |
| header = TRUE, | |
| check.names = FALSE, # This prevents R from modifying column names | |
| fileEncoding = "UTF-8", | |
| stringsAsFactors = FALSE) | |
| close(con) | |
| return(data) | |
| } else { | |
| stop("Failed to download dataset") | |
| } | |
| } | |
| heatMap <- function(data, gtitle, concen) { | |
| custom_colors <- c("#333333","#46363a","#5a383f","#6d3944","#803947","#933948", | |
| "#a63848","#b93647","#cb3445","#dd3340","#ee323b","#ff3333") | |
| data_clean <- data %>% | |
| filter(!is.na(px) & !is.na(pz) & is.finite(px) & is.finite(pz)) | |
| h_x <- MASS::bandwidth.nrd(data_clean$px) | |
| h_z <- MASS::bandwidth.nrd(data_clean$pz) | |
| bandwidth_factor <- concen | |
| kde <- kde2d(data_clean$px, data_clean$pz, n = 200, | |
| h = c(h_x, h_z) * bandwidth_factor, | |
| lims = c(-3, 3, 0, 5)) | |
| df <- expand.grid(x = kde$x, y = kde$y) | |
| df$density <- as.vector(kde$z) | |
| ggplot(df, aes(x = x, y = y, z = density)) + | |
| geom_contour_filled(bins = length(custom_colors) - 1) + | |
| scale_fill_manual(values = custom_colors) + | |
| coord_cartesian(xlim = c(-3,3), ylim = c(0,5)) + | |
| coord_fixed(ratio = 1) + | |
| labs(title = gtitle) + | |
| theme_void() + | |
| geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5), colour = "black") + | |
| geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6), colour = "black") + | |
| geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6), colour = "black") + | |
| geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6), colour = "black") + | |
| theme( | |
| legend.position = "none", | |
| plot.background = element_rect(fill = "#333333", color = NA), | |
| panel.background = element_rect(fill = "#333333", color = NA), | |
| text = element_text(color = "white"), | |
| panel.grid = element_blank(), | |
| plot.title = element_text(hjust = 0.5) | |
| ) | |
| } | |
| 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))) | |
| } | |
| } | |
| #font_add_google("Roboto Condensed") | |
| 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) | |
| } | |
| apply_percentile_calcs <- function(data) { | |
| # List of columns to apply percent_rank | |
| percent_rank_cols <- c("Z-Con%", "Z-Swing%", "O-Con%", "Avg EV", "Max EV", "EV90", "Barrel%", "Swing%", "wOBA", | |
| "wOBACON","xwOBA","xDamage") | |
| # List of columns to apply inverse percent_rank | |
| inverse_percent_rank_cols <- c("Chase%", "Whiff%", "stdev(LA)", "SwStr%") | |
| # Create an empty list to store results | |
| percentile_list <- list() | |
| # Calculate regular percentiles | |
| for(col in percent_rank_cols) { | |
| percentile_list[[col]] <- data.frame( | |
| `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name | |
| `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name | |
| metric = col, | |
| percentile = round(percent_rank(data[[col]]) * 100), | |
| value = data[[col]], | |
| stringsAsFactors = FALSE | |
| ) | |
| } | |
| # Calculate inverse percentiles | |
| for(col in inverse_percent_rank_cols) { | |
| percentile_list[[col]] <- data.frame( | |
| `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name | |
| `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name | |
| metric = col, | |
| percentile = round((1 - percent_rank(data[[col]])) * 100), | |
| value = data[[col]], | |
| stringsAsFactors = FALSE | |
| ) | |
| } | |
| # Combine all results into one data frame | |
| result <- do.call(rbind, percentile_list) | |
| # Reset row names | |
| rownames(result) <- NULL | |
| return(result) | |
| } | |
| 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" | |
| FSL26 <- download_private_parquet("TimStats/StatcastDataAll", "FSL26.parquet") | |
| FSL26$level <- "FSL" | |
| 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, FSL26) | |
| ui <- page_fluid( | |
| theme = bs_theme(bg = "#333333", fg = "#ffffff", primary = "#428bca"), | |
| uiOutput("page_content") | |
| # card( | |
| # card_header("2020-2024 Filterable Pitcher Heatmaps"), | |
| # card( | |
| # layout_columns( | |
| # col_widths = c(4, 4, 4), | |
| # card( | |
| # selectInput("league", "Select League:", choices = c("MLB","AAA","FSL")), | |
| # selectInput("player", "Select Player:", choices = c("")), | |
| # sliderInput("concen", "Heatmap Bandwidth Factor", value = .75, step = 0.05, min = .5, max = 1.5) | |
| # ), | |
| # card( | |
| # selectInput("hand1", "Batter Handedness", choices = c("All Batters","LHH","RHH")), | |
| # selectInput("pitch1", "Pitch:", choices = c("")), | |
| # dateRangeInput("date1", "Date Range:", start = "2024-02-09", end = "2024-09-05") | |
| # ), | |
| # card( | |
| # selectInput("hand2", "Batter Handedness", choices = c("All Batters","LHH","RHH")), | |
| # selectInput("pitch2", "Pitch:", choices = c("")), | |
| # dateRangeInput("date2", "Date Range:", start = "2024-02-09", end = "2024-09-05") | |
| # ) | |
| # ) | |
| # ), | |
| # card( | |
| # downloadButton("download", "Download Plot", class = "btn-primary mb-3"), | |
| # plotOutput("combined_graph", height = "600px") | |
| # ) | |
| # ) | |
| ) | |
| # [Server code remains exactly the same] | |
| server <- function(input, output, session) { | |
| is_authenticated <- reactiveVal(FALSE) | |
| # Render either login page or main app | |
| output$page_content <- renderUI({ | |
| #if (!is_authenticated()) { | |
| if (FALSE) { | |
| # Login page | |
| card( | |
| card_header("Authentication Required"), | |
| card_body( | |
| textInput("email", "Enter your Patreon email:"), | |
| actionButton("check_access", "Access App", class = "btn-primary"), | |
| textOutput("auth_message") | |
| ) | |
| ) | |
| } else { | |
| # Main app content (your existing UI) | |
| card( | |
| card_header("2020-2024 Filterable Pitcher Heatmaps"), | |
| card( | |
| layout_columns( | |
| col_widths = c(4, 4, 4), | |
| card( | |
| selectInput("league", "Select League:", choices = c("MLB","AAA","FSL")), | |
| selectInput("player", "Select Player:", choices = c("")), | |
| sliderInput("concen", "Heatmap Bandwidth Factor", value = .75, step = 0.05, min = .5, max = 1.5) | |
| ), | |
| card( | |
| selectInput("hand1", "Batter Handedness", choices = c("All Batters","LHH","RHH")), | |
| selectInput("pitch1", "Pitch:", choices = c("")), | |
| dateRangeInput("date1", "Date Range:", start = "2026-03-15", end = updatedDate) | |
| ), | |
| card( | |
| selectInput("hand2", "Batter Handedness", choices = c("All Batters","LHH","RHH")), | |
| selectInput("pitch2", "Pitch:", choices = c("")), | |
| dateRangeInput("date2", "Date Range:", start = "2026-03-15", end = updatedDate) | |
| ) | |
| ) | |
| ), | |
| card( | |
| downloadButton("download", "Download Plot", class = "btn-primary mb-3"), | |
| plotOutput("combined_graph", height = "600px") | |
| ) | |
| ) | |
| } | |
| }) | |
| # Handle authentication | |
| observeEvent(input$check_access, { | |
| if (check_patreon_access(input$email)) { | |
| is_authenticated(TRUE) | |
| } else { | |
| output$auth_message <- renderText({ | |
| "Access denied. Please ensure you are an active Veteran or Hall of Fame tier patron." | |
| }) | |
| } | |
| }) | |
| pname <- reactiveVal() | |
| plot_data <- reactive({ | |
| t <- pname() | |
| # Function to filter data based on inputs | |
| filter_data <- function(hand, pitch, date_range) { | |
| data <- if(hand == "All Batters") { | |
| t %>% filter(`Pitcher Name` == input$player) | |
| } else if(hand == "LHH") { | |
| t %>% filter(bside == "L", `Pitcher Name` == input$player) | |
| } else { | |
| t %>% filter(bside == "R", `Pitcher Name` == input$player) | |
| } | |
| data %>% | |
| filter(pitch_name == pitch) %>% | |
| filter(between(as.Date(date), date_range[1], date_range[2])) | |
| } | |
| graph1_data <- filter_data(input$hand1, input$pitch1, input$date1) | |
| graph2_data <- filter_data(input$hand2, input$pitch2, input$date2) | |
| title1 <- paste0(input$player, " ", input$pitch1, " vs. ", input$hand1, "\n", input$date1[1], " to ", input$date1[2]) | |
| title2 <- paste0(input$player, " ", input$pitch2, " vs. ", input$hand2, "\n", input$date2[1], " to ", input$date2[2]) | |
| plot1 <- heatMap(graph1_data, title1, input$concen) | |
| plot2 <- heatMap(graph2_data, title2, input$concen) | |
| plot1 + plot2 + | |
| plot_layout(ncol = 2) + | |
| plot_annotation( | |
| title = paste0(input$player, " Pitch Comparison"), | |
| theme = theme( | |
| plot.title = element_text(hjust = 0.5, size = 20, color = "white"), | |
| plot.background = element_rect(fill = "#333333", color = NA), | |
| plot.margin = margin(0, 0, 0, 0) | |
| ) | |
| ) & | |
| theme(plot.margin = margin(0, 0, 0, 0)) | |
| }) | |
| observeEvent(input$league, { | |
| req(input$league) | |
| tryCatch({ | |
| if(input$league == "MLB"){ | |
| t <- mlb | |
| } else if(input$league == "AAA"){ | |
| t <- aaa | |
| } else if(input$league == "FSL"){ | |
| t <- fsl | |
| } | |
| pname(t) | |
| updateSelectInput( | |
| session = session, | |
| inputId = "player", | |
| label = "Select Player:", | |
| choices = unique(t$`Pitcher Name`), | |
| selected = NULL | |
| ) | |
| }, error = function(e) { | |
| message("Error downloading or processing data: ", e$message) | |
| }) | |
| }) | |
| observeEvent(input$player, { | |
| req(input$player) | |
| tryCatch({ | |
| t <- pname() | |
| d <- t |> | |
| filter(`Pitcher Name` == input$player) | |
| updateSelectInput( | |
| session = session, | |
| inputId = "pitch1", | |
| label = "Pitch:", | |
| choices = unique(d$`pitch_name`), | |
| selected = NULL | |
| ) | |
| updateSelectInput( | |
| session = session, | |
| inputId = "pitch2", | |
| label = "Pitch:", | |
| choices = unique(d$`pitch_name`), | |
| selected = NULL | |
| ) | |
| }, error = function(e) { | |
| message("Error processing player data: ", e$message) | |
| }) | |
| }) | |
| output$combined_graph <- renderPlot({ | |
| plot_data() | |
| }, bg = "#333333", width = 800) | |
| output$download <- downloadHandler( | |
| filename = function() { | |
| paste0(input$player, "_pitch_comparison.png") | |
| }, | |
| content = function(file) { | |
| ggsave(file, plot = plot_data(), width = 12, height = 8, dpi = 300) | |
| } | |
| ) | |
| } | |
| shinyApp(ui = ui, server = server) |