library(shiny) library(plotly) library(gridlayout) library(bslib) library(DT) library(rsconnect) library(baseballr) library(dplyr) library(tidyverse) library(rvest) library(ggplot2) library(janitor) library(ggthemes) library(ggpubr) library(jsonlite) library(utils) library(grid) library(gridExtra) library(png) library(xgboost) library(httr) library(jpeg) library(zoo) library(pkgconfig) library(rsvg) pdf(file = NULL) Sys.setenv(TZ='EST') model <- xgb.load('TimStuff2.ubj') # Helper functions 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) } gtable_add_padding <- function(gtable, padding = unit(c(5, 5, 5, 5), "mm")) { padding <- rep(padding, length.out = 4) # Add padding to the layout new_widths <- unit.c(padding[4], gtable$widths, padding[2]) new_heights <- unit.c(padding[1], gtable$heights, padding[3]) # Create the new layout with added padding new_layout <- gtable$layout new_layout$l <- new_layout$l + 1 new_layout$t <- new_layout$t + 1 # Create new gtable with padding new_gtable <- gtable( widths = new_widths, heights = new_heights ) # Add grobs from original gtable new_gtable$layout <- new_layout new_gtable$grobs <- gtable$grobs return(new_gtable) } get_team_logo <- function(team_name) { # ESPN team ID and URL mapping team_info <- list( "Los Angeles Angels" = "LAA", "Arizona Diamondbacks" = "ARI", "Baltimore Orioles" = "BAL", "Boston Red Sox" = "BOS", "Chicago Cubs" = "CHC", "Cincinnati Reds" = "CIN", "Cleveland Guardians" = "CLE", "Colorado Rockies" = "COL", "Detroit Tigers" = "DET", "Houston Astros" = "HOU", "Kansas City Royals" = "KC", "Los Angeles Dodgers" = "LAD", "Washington Nationals" = "WSH", "New York Mets" = "NYM", "Oakland Athletics" = "OAK", "Pittsburgh Pirates" = "PIT", "San Diego Padres" = "SD", "Seattle Mariners" = "SEA", "San Francisco Giants" = "SF", "St. Louis Cardinals" = "STL", "Tampa Bay Rays" = "TB", "Texas Rangers" = "TEX", "Toronto Blue Jays" = "TOR", "Minnesota Twins" = "MIN", "Philadelphia Phillies" = "PHI", "Atlanta Braves" = "ATL", "Chicago White Sox" = "CHW", "Miami Marlins" = "MIA", "New York Yankees" = "NYY", "Milwaukee Brewers" = "MIL" ) team_abbrev <- team_info[[team_name]] if (is.null(team_abbrev)) { return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))) } logo_url <- sprintf("https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/%s.png&h=400&w=400", team_abbrev) logo_result <- download_and_process_image(logo_url) if (!is.null(logo_result)) { # Set fixed size for the logo return(rasterGrob(logo_result$img, interpolate = TRUE, width = unit(.75, "npc"), # Reduced size to 50% of container height = unit(.75, "npc"))) } else { return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))) } } #logo_svg <- function(id){ # logo_url <- paste0("https://www.mlbstatic.com/team-logos/",id,".svg") # logo_result <- download_and_process_image(logo_url) # # if (!is.null(logo_result)) { # # Set fixed size for the logo # return(rasterGrob(logo_result$img, # interpolate = TRUE, # width = unit(.75, "npc"), # Reduced size to 50% of container # height = unit(.75, "npc"))) # } else { # return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))) # } #} logo_svg <- function(id) { logo_url <- paste0("https://www.mlbstatic.com/team-logos/", id, ".svg") logo_result <- download_and_process_image(logo_url) if (!is.null(logo_result)) { # Create a square viewport/container first return( gTree( children = gList( # Add a transparent square background (optional) rectGrob(gp = gpar(fill = NA, col = NA)), # Place the logo in center of the square container rasterGrob(logo_result$img, interpolate = TRUE, width = unit(.75, "npc"), height = unit(.75, "npc"), vp = viewport(width = unit(1, "snpc"), height = unit(1, "snpc"))) ), vp = viewport(width = unit(1, "npc"), height = unit(1, "npc"), layout = grid.layout(1, 1)) ) ) } else { return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))) } } download_and_process_image <- function(url) { tryCatch({ response <- GET(url) content_type <- http_type(response) if (content_type %in% c("image/png", "image/jpeg", "image/svg+xml")) { temp_file <- tempfile(fileext = switch(content_type, "image/png" = ".png", "image/jpeg" = ".jpg", "image/svg+xml" = ".svg")) writeBin(content(response, "raw"), temp_file) # For SVG, convert to PNG first if (content_type == "image/svg+xml") { svg_content <- readLines(temp_file, warn = FALSE) # Create a PNG from the SVG png_file <- tempfile(fileext = ".png") rsvg::rsvg_png(temp_file, png_file) img <- png::readPNG(png_file) } else if (content_type == "image/png") { img <- readPNG(temp_file) } else { img <- readJPEG(temp_file) } return(list(img = img, type = content_type)) } else { warning(paste("Unsupported image type:", content_type)) return(NULL) } }, error = function(e) { warning(paste("Error processing image:", e$message)) return(NULL) }) } 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)) } pitcher_summary <- function(game_pk,date){ gdate <- as.Date.character(date) gdate <- as.Date(gdate) tmilb <- mlb_pbp(game_pk) tmilb <- tmilb %>% filter(type == "pitch") tmilb <- tmilb %>% select(matchup.batter.fullName,matchup.batter.id,matchup.pitcher.fullName, matchup.pitcher.id,result.event,details.description,details.type.description, result.description,pitchData.startSpeed,pitchData.plateTime,pitchData.zone, pitchData.breaks.spinRate,pitchData.extension, pitchData.coordinates.pX, pitchData.coordinates.pZ,pitchData.coordinates.x0, pitchData.coordinates.y0, pitchData.coordinates.z0,pitchData.coordinates.aX,pitchData.coordinates.aY, pitchData.coordinates.aZ,pitchData.coordinates.vX0,pitchData.coordinates.vZ0, pitchData.coordinates.vY0,pitchData.coordinates.pfxX,pitchData.coordinates.pfxZ, pitchData.breaks.breakVerticalInduced,pitchData.breaks.breakHorizontal, hitData.launchSpeed,hitData.launchAngle,hitData.totalDistance,details.isInPlay, last.pitch.of.ab,pitchData.breaks.spinDirection,matchup.pitchHand.code,matchup.batSide.code,game_pk) colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID", "result","description","pitch_name","des","start_speed", "plateTime","zone","spin_rate","extension","px","pz","x0", "y0","z0","ax","ay","az","vx0","vz0","vy0","pfxX","pfxZ", "IVB","HB","hit_speed","hit_angle","hit_distance","inPlay", "lastPitch","spinDirection","phand","bhand","gamepk") tmilb <- is_barrel(tmilb) tmilb <- tmilb %>% mutate(is_strike_swinging = ifelse(description == "Swinging Strike" | description == "Foul Tip",TRUE,FALSE)) tmilb <- tmilb %>% mutate(date = gdate) return(tmilb) } break_plot <- function(game){ pitch_colors <- c( "Four-Seam Fastball"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00", "Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED", "Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b", "Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Forkball"= "#F012BE", "Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD", "Screwball"= "#FF69B4" ) ggplot(game, aes(x = HB, y = IVB, color = pitch_name)) + geom_point(size = 2) + geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) + geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) + scale_color_manual(values = pitch_colors) + labs(x = "Horizontal Break (in)", y = "Induced Vertical Break (in)", title = "Pitch Movement") + xlim(-25, 25) + ylim(-25, 25) + theme_minimal() + theme( legend.position = "bottom", plot.title = element_text(hjust = 0.5, face = "bold"), panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1), aspect.ratio = 1 ) + guides(color = guide_legend(title = "Pitch Type", nrow = 1)) } left_batter <- png::readPNG("left_batter.png") right_batter <- png::readPNG("right_batter.png") pitch_plot_split <- function(game, title) { game_lhb <- game %>% filter(bhand == "L") game_rhb <- game %>% filter(bhand == "R") home_plate <- data.frame( x = c(0.6, -0.6, -0.7083, 0, 0.7083), y = c(0.5, 0.5, 0.25, 0, 0.25) ) pitch_colors <- c( "Four-Seam Fastball"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00", "Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED", "Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b", "Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Forkball"= "#F012BE", "Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD", "Screwball"= "#FF69B4" ) plot_lhb <- ggplot(game_lhb, aes(x = px, y = pz, color = pitch_name)) + geom_polygon(data = home_plate, aes(x = x, y = y), fill = "white", color = "black", inherit.aes = FALSE) + annotation_custom(rasterGrob(left_batter, width = unit(.75, "npc"), height = unit(1.5, "npc")), xmin = .7, xmax = 3.2, ymin = 1, ymax = 5) + geom_point(size = 3) + geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) + geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) + geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) + geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) + scale_color_manual(values = pitch_colors) + labs(x = NULL, y = NULL, title = "LHB") + xlim(-3, 3) + ylim(0, 5) + coord_fixed(ratio = 1) + theme_void() + theme( legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"), axis.text = element_blank(), axis.ticks = element_blank() ) plot_rhb <- ggplot(game_rhb, aes(x = px, y = pz, color = pitch_name)) + geom_polygon(data = home_plate, aes(x = x, y = y), fill = "white", color = "black", inherit.aes = FALSE) + annotation_custom(rasterGrob(right_batter, width = unit(.75, "npc"), height = unit(1.5, "npc")), xmin = -3, xmax = -0.5, ymin = 1, ymax = 5) + geom_point(size = 3) + geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) + geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) + geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) + geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) + scale_color_manual(values = pitch_colors) + labs(x = NULL, y = NULL, title = "RHB") + xlim(-3, 3) + ylim(0, 5) + coord_fixed(ratio = 1) + theme_void() + theme( legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"), axis.text = element_blank(), axis.ticks = element_blank() ) return(list(lhb = plot_lhb, rhb = plot_rhb)) } 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_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) } scale_TimStuff <- function(raw_score, model_mean, model_sd) { scaled_score <- (raw_score - model_mean) / model_sd result <- 100 - (scaled_score * 10) return(result) } getBoxScore <- function(game_pk, pid) { url <- paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore") bs <- fromJSON(url) t <- as.data.frame(bs[["teams"]][["away"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]]) if (nrow(t) == 0) { t <- as.data.frame(bs[["teams"]][["home"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]]) } stats <- c("inningsPitched", "battersFaced", "runs", "earnedRuns", "hits", "baseOnBalls", "strikeOuts", "strikePercentage") display_names <- c("IP", "TBF", "R", "ER", "H", "BB", "K", "Strike%") result <- data.frame(matrix(ncol = length(stats), nrow = 1)) colnames(result) <- display_names for (i in 1:length(stats)) { value <- t[[stats[i]]] if (is.null(value)) value <- "0" if (stats[i] == "strikePercentage") { value <- paste0(round(as.numeric(value) * 100, 1), "%") } result[1, i] <- as.character(value) } return(result) } calculate_timstuff <- function(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)) 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) } summary_table <- function(game) { rows <- nrow(game) game <- calculate_timstuff(game) game <- game %>% mutate(TimStuff = ifelse(TimStuff > 150,NA,TimStuff)) %>% mutate(TimStuff = ifelse(TimStuff < 50,NA,TimStuff)) sumtable <- game %>% mutate(team_fielding_id = ifelse(description == "Called Strike" | description == "Swinging Strike" | description == "Swinging Strike (Blocked)", 1, 0)) %>% 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)) %>% mutate(is_strike_swinging = ifelse(is_strike_swinging == TRUE, 1, 0)) %>% mutate(Pitch = pitch_name) %>% rowwise() %>% group_by(Pitch) %>% summarize( Pitches = n(), 'Pitch%' = round(sum(Pitches)/sum(rows) * 100, digits = 1), 'Avg. Velo' = round(mean(start_speed, na.rm = TRUE), digits = 1), 'Spin Rate' = round(mean(spin_rate, na.rm = TRUE), digits = 0), 'Extension' = round(mean(extension, na.rm = TRUE), digits = 1), 'IVB' = round(mean(IVB, na.rm = TRUE), digits = 1), 'HB' = round(mean(HB, na.rm = TRUE), digits = 1), 'VAA' = round(mean(VAA, na.rm = TRUE), digits = 1), 'CSW%' = round(sum(team_fielding_id, na.rm = TRUE) / sum(!is.na(team_fielding_id)) * 100, digits = 1), 'Whiff%' = round(sum(is_strike_swinging, na.rm = TRUE) / sum(swing, na.rm = TRUE) * 100, digits = 1), 'TimStuff+' = round(mean(TimStuff, na.rm = TRUE), digits = 0) ) %>% arrange(-Pitches) result <- sumtable %>% select(Pitch, Pitches, `Pitch%`, `Avg. Velo`, `Spin Rate`, Extension,IVB, HB, VAA, `CSW%`, `Whiff%`, `TimStuff+`) %>% rename( "Type" = Pitch, "#" = Pitches, "Velo" = `Avg. Velo`, "Spin" = `Spin Rate`, "Ext" = Extension, "Use%" = `Pitch%` ) %>% mutate( "Use%" = paste0(`Use%`, "%"), "Spin" = format(round(Spin), big.mark = ","), Velo = round(Velo, 1), Ext = round(Ext, 1), IVB = round(IVB, 1), HB = round(HB, 1), VAA = round(VAA, 1), `CSW%` = paste0(`CSW%`, "%"), `Whiff%` = paste0(`Whiff%`, "%") ) %>% arrange(desc(`#`)) return(result) } mlbid <- bind_rows( mlb_schedule(season = 2020, level_ids = "1"), mlb_schedule(season = 2021, level_ids = "1"), mlb_schedule(season = 2022, level_ids = "1"), mlb_schedule(season = 2023, level_ids = "1"), mlb_schedule(season = 2024, level_ids = "1"), mlb_schedule(season = 2025, level_ids = "1"), mlb_schedule(season = 2026, level_ids = "1") ) mlbteamH <- mlbid %>% select(teams_home_team_name) %>% distinct() mlbteamA <- mlbid %>% select(teams_away_team_name) %>% distinct() # AAA Schedule (2021-2025) aaaid <- bind_rows( mlb_schedule(season = 2021, level_ids = "11"), mlb_schedule(season = 2022, level_ids = "11"), mlb_schedule(season = 2023, level_ids = "11"), mlb_schedule(season = 2024, level_ids = "11"), mlb_schedule(season = 2025, level_ids = "11"), mlb_schedule(season = 2026, level_ids = "11") ) aaateamH <- aaaid %>% select(teams_home_team_name) %>% distinct() aaateamA <- aaaid %>% select(teams_away_team_name) %>% distinct() # FSL Schedule (2021-2025) fslid <- bind_rows( mlb_schedule(season = 2021, level_ids = "14"), mlb_schedule(season = 2022, level_ids = "14"), mlb_schedule(season = 2023, level_ids = "14"), mlb_schedule(season = 2024, level_ids = "14"), mlb_schedule(season = 2025, level_ids = "14"), mlb_schedule(season = 2026, level_ids = "14") ) %>% filter(gameday_type == "E" | gameday_type == "P") fslteamH <- fslid %>% select(teams_home_team_name) %>% distinct() fslteamA <- fslid %>% select(teams_away_team_name) %>% distinct() # College Schedule (2023-2025) sbid <- bind_rows( mlb_schedule(season = 2023, level_ids = "22"), mlb_schedule(season = 2024, level_ids = "22"), mlb_schedule(season = 2025, level_ids = "22"), mlb_schedule(season = 2026, level_ids = "22") ) %>% filter(gameday_type == "E" | gameday_type == "P") sbteamH <- sbid %>% select(teams_home_team_name) %>% distinct() sbteamA <- sbid %>% select(teams_away_team_name) %>% distinct() #Select ACL & FCL Statcast Games cpxid <- bind_rows( mlb_schedule(season = 2025, level_ids = "17") ) #%>% # filter(gameday_type == "E" | gameday_type == "P") cpxteamH <- cpxid %>% select(teams_home_team_name) %>% distinct() cpxteamA <- cpxid %>% select(teams_away_team_name) %>% distinct() # UI Definition ui <- fluidPage( theme = bs_theme(bg = "#ffffff", fg = "#333333", primary = "#428bca"), tagList( tags$head( tags$link(href = "https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap", rel = "stylesheet"), tags$style(HTML(" * { font-family: 'Roboto Condensed', sans-serif !important; } .login-screen { max-width: 400px; margin: 100px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } .login-header { text-align: center; margin-bottom: 20px; } ")) ), # Use uiOutput to conditionally render either login or main page uiOutput("page") ) ) # Server Definition server <- function(input, output, session) { credentials <- reactiveValues(logged_in = FALSE) output$page <- renderUI({ #if (!credentials$logged_in) { # Login page #div(class = "login-screen", # div(class = "login-header", # h2("Baseball Stats Visualization"), # p("Please log in with your Patreon email") # ), # textInput("email", "Email:"), # actionButton("login", "Login", class = "btn-primary"), # p(style = "margin-top: 20px; text-align: center;", # "Access requires Veteran or Hall of Fame tier on Patreon") #) #} else { # Main application UI (your existing UI) fluidPage( theme = bs_theme(version = 5, bootswatch = "flatly"), titlePanel("2020-2025 Daily MLB/AAA/FSL Summary Cards"), sidebarLayout( sidebarPanel( width = 3, dateInput("date", "Date:"), selectizeInput("level", "Level:", c("MLB", "AAA", "FSL","College (Statcast Parks Only)", "AFL"), options = list( placeholder = 'Select a level', onInitialize = I('function() { this.setValue(""); }') )), selectizeInput("homeT", "Home Team:", NULL), selectizeInput("awayT", "Away Team:", NULL), selectizeInput("gamenum", "Game Number:", c("1", "2")), actionButton("update", "Find Pitcher", icon("magnifying-glass"), class = "btn-primary btn-block"), selectizeInput("pitcher", "Pitcher Name:", NULL), actionButton("update1", "Make Card", icon("plus"), class = "btn-success btn-block"), downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block") ), mainPanel( div(style = "width: 1100px; height: 1100px; overflow: auto;", # Increased from 1000px plotOutput("combinedPlot", width = "100%", height = "100%") ), tableOutput("boxscoreTable") ) ) ) #} }) observeEvent(input$login, { # Check Patreon access has_access <- check_patreon_access(input$email) if (has_access) { credentials$logged_in <- TRUE } else { showModal(modalDialog( title = "Access Denied", "This email does not have access. Please make sure you're using the email associated with your Patreon account and you have an active Veteran or Hall of Fame tier subscription.", easyClose = TRUE )) } }) observeEvent(input$level, { if(input$level == "AAA"){ updateSelectizeInput(session, "homeT", "Home Team:", choices = aaateamH[,1]) updateSelectizeInput(session, "awayT", "Away Team:", choices = aaateamA[,1]) } if(input$level == "FSL"){ updateSelectizeInput(session, "homeT", "Home Team:", choices = fslteamH[,1]) updateSelectizeInput(session, "awayT", "Away Team:", choices = fslteamA[,1]) } if(input$level == "MLB"){ updateSelectizeInput(session, "homeT", "Home Team:", choices = mlbteamH[,1]) updateSelectizeInput(session, "awayT", "Away Team:", choices = mlbteamA[,1]) } if(input$level == "College (Statcast Parks Only)"){ updateSelectizeInput(session, "homeT", "Home Team:", choices = sbteamH[,1]) updateSelectizeInput(session, "awayT", "Away Team:", choices = sbteamA[,1]) } if(input$level == "AFL"){ updateSelectizeInput(session, "homeT", "Home Team:", choices = cpxteamH[,1]) updateSelectizeInput(session, "awayT", "Away Team:", choices = cpxteamA[,1]) } }) game_data <- reactiveVal() boxscore_data <- reactiveVal() observeEvent(input$update, { tryCatch({ if(input$level == "AAA"){ pname <- aaaid %>% filter(date == input$date) %>% filter(teams_home_team_name == input$homeT) %>% filter(teams_away_team_name == input$awayT) %>% filter(game_number == input$gamenum) pname <- pitcher_summary(pname[,6], input$date) } if(input$level == "FSL"){ pname <- fslid %>% filter(date == input$date) %>% filter(teams_home_team_name == input$homeT) %>% filter(teams_away_team_name == input$awayT) %>% filter(game_number == input$gamenum) pname <- pitcher_summary(pname[,6], input$date) } if(input$level == "MLB"){ pname <- mlbid %>% filter(date == as.character.Date(input$date)) %>% filter(teams_home_team_name == input$homeT) %>% filter(teams_away_team_name == input$awayT) %>% filter(game_number == input$gamenum) pname <- pitcher_summary(pname[,6], input$date) } if(input$level == "College (Statcast Parks Only)"){ pname <- sbid %>% filter(date == input$date) %>% filter(teams_home_team_name == input$homeT) %>% filter(teams_away_team_name == input$awayT) %>% filter(game_number == input$gamenum) pname <- pitcher_summary(pname[,6], input$date) } if(input$level == "AFL"){ pname <- cpxid %>% filter(date == input$date) %>% filter(teams_home_team_name == input$homeT) %>% filter(teams_away_team_name == input$awayT) %>% filter(game_number == input$gamenum) pname <- pitcher_summary(pname[,6], input$date) } if(nrow(pname) == 0) { showNotification("No pitchers found for the selected game.", type = "warning") } else { updateSelectizeInput(session, "pitcher", "Pitcher:", choices = unique(pname$`Pitcher Name`)) game_data(pname) } }, error = function(e) { showNotification(paste("Error finding pitchers:", e$message), type = "error") }) }) rolling_timstuff <- reactive({ req(input$update1, game_data()) game <- game_data() %>% filter(`Pitcher Name` == input$pitcher) game <- calculate_timstuff(game) game <- game %>% mutate(TimStuff = ifelse(TimStuff > 150,NA,TimStuff)) %>% mutate(TimStuff = ifelse(TimStuff < 50,NA,TimStuff)) result <- game %>% arrange(date) %>% group_by(pitch_name) %>% mutate( TimStuff = as.numeric(TimStuff), rolling_timstuff = if(n() >= 5) rollmean(TimStuff, k = 5, fill = NA, align = "right") else TimStuff, pitch_number = row_number() ) %>% ungroup() return(result) }) combinedPlot <- reactiveVal() observeEvent(input$update1, { req(game_data()) tryCatch({ game <- game_data() %>% filter(`Pitcher Name` == input$pitcher) if(nrow(game) == 0) { showNotification("No data available for the selected pitcher.", type = "warning") return() } game_pk <- unique(game$gamepk)[1] bs <- fromJSON(paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore")) pitcher_id <- unique(game$`Pitcher ID`)[1] # Look for pitcher in both home and away teams pitcher_team <- NULL if(paste0("ID", pitcher_id) %in% names(bs$teams$away$players)) { pitcher_team <- bs$teams$away$team$name pitcher_id <- bs$teams$away$team$id } else if(paste0("ID", pitcher_id) %in% names(bs$teams$home$players)) { pitcher_team <- bs$teams$home$team$name pitcher_id <- bs$teams$home$team$id } if(is.null(pitcher_id)) { mlb_team_logo <- textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12)) } else { #mlb_team_logo <- get_team_logo(pitcher_team) mlb_team_logo <- logo_svg(pitcher_id) if(is.null(mlb_team_logo)) { mlb_team_logo <- textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12)) } } #mlb_team_logo <- get_team_logo(pitcher_team) # Get plots and data break_plot <- break_plot(game) + theme(legend.position = "none") pitch_plots <- pitch_plot_split(game) boxscore_data <- getBoxScore(unique(game$gamepk[1]), unique(game$`Pitcher ID`[1])) # Create boxscore table boxscore_table <- tableGrob(boxscore_data, rows = NULL, theme = ttheme_minimal( core = list( fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"), # Reduce the y padding to make the table more compact y.padding = unit(4, "mm") # Reduced from default ), colhead = list( fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0"), # Reduce the y padding for headers y.padding = unit(4, "mm") # Reduced from default ) )) boxscore_table$heights <- unit(rep(1/(nrow(boxscore_data) + 1), nrow(boxscore_data) + 1), "npc") # Create summary table table_data <- summary_table(game) num_rows <- nrow(table_data) table_plot <- tableGrob(table_data, rows = NULL, theme = ttheme_minimal( core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")), colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0")), rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")) )) total_height <- unit(1, "npc") row_height <- total_height / (num_rows + 1) table_plot$heights <- unit(rep(row_height, num_rows + 1), "npc") table_plot$widths <- unit(c(0.12, 0.05, 0.05, 0.07, 0.08, 0.05, 0.07, 0.07, 0.07, 0.09, 0.09, 0.09), "npc") for(i in seq(2, nrow(table_plot), 2)) { table_plot$grobs[[i]]$gp$fill <- "#f9f9f9" } # Get player image id <- as.character(game$`Pitcher ID`[1]) mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false") milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false") img_result <- download_and_process_image(mlb_url) if (is.null(img_result)) { img_result <- download_and_process_image(milb_url) } img_grob <- if (!is.null(img_result)) { rasterGrob(img_result$img, interpolate = TRUE) } else { textGrob("Image not available", gp = gpar(col = "red", fontsize = 20)) } # Create rolling TimStuff+ plot rolling_data <- rolling_timstuff() pitch_colors <- c( "Four-Seam Fastball"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00", "Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED", "Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b", "Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Forkball"= "#F012BE", "Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD", "Screwball"= "#FF69B4" ) timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) + geom_line(size = 1, na.rm = TRUE) + geom_point(size = 1, na.rm = TRUE) + scale_color_manual(values = pitch_colors) + theme_minimal() + labs(title = if(any(rolling_data$pitch_number >= 5)) "5-Pitch Rolling TimStuff+" else "5-Pitch Rolling TimStuff+", x = "Pitch Number", y = "TimStuff+") + scale_y_continuous(limits = c(70, 130), na.value = NA) + scale_x_continuous(breaks = seq(5, 55, by = 5), limits = c(5, NA)) + theme( plot.title = element_text(hjust = 0.5, face = "bold"), legend.position = "none", panel.grid.major.x = element_line(color = "gray", size = 0.5) ) if(nrow(rolling_data) == 0) { timstuff_plot <- ggplot() + theme_void() + labs(title = "No pitch data available") + theme( plot.title = element_text(hjust = 0.5, face = "bold") ) } # Create title and data source text title_text <- textGrob( paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"), gp = gpar(fontsize = 16, fontface = "bold") ) data_source_text <- textGrob( "Data: MLB", gp = gpar(fontsize = 8), x = unit(1, "npc") - unit(2, "mm"), y = unit(2, "mm"), just = c("right", "bottom") ) # Create legend legend <- if(nrow(rolling_data) > 0) { get_legend( ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) + geom_point(size = 5) + scale_color_manual(values = pitch_colors) + theme(legend.position = "bottom", legend.title = element_blank(), text = element_text(size = 12.5), legend.box = "horizontal" ) + guides(color = guide_legend(nrow = 1)) ) } else { ggplotGrob(ggplot() + theme_void()) } # NEW LAYOUT: Combine all plots combined <- grid.arrange( # Row 1: Headshot - Title - Team Logo arrangeGrob( img_grob, title_text, mlb_team_logo, ncol = 3, widths = c(1, 2, 1) ), # Row 2: TimStuff+ (2/3) and boxscore (1/3) arrangeGrob( timstuff_plot, boxscore_table, ncol = 2, widths = c(2, 1) ), # Row 3: Three plots in one row arrangeGrob( pitch_plots$lhb, pitch_plots$rhb, break_plot, ncol = 3, widths = c(1, 1, 1) ), # Row 4: Legend legend, # Row 5: Pitch summary table table_plot, # Adjust the relative heights nrow = 5, heights = c(0.8, 0.9, 1.2, 0.2, 1) ) # Add padding to the combined plot # padded_plot <- gtable_add_padding(combined, padding = unit(c(30, 30, 30, 30), "points")) # top, right, bottom, left padding # Update the reactive value with the padded plot combinedPlot(combined) # Update the rendering to use the padded plot output$combinedPlot <- renderPlot({ par(mar = c(2, 2, 2, 2)) # Add margins (bottom, left, top, right) grid.draw(combinedPlot()) }, width = 1000, height = 1000, bg = "white") # For download output$downloadPlot <- downloadHandler( filename = function() { paste("baseball_card_", Sys.Date(), ".png", sep = "") }, content = function(file) { ggsave(file, plot = combinedPlot(), width = 15, height = 15, # Increased from 15 dpi = 300, units = "in", device = "png", bg = "white") } ) # Render the boxscore table output$boxscoreTable <- renderTable({ boxscore_data() }) }, error = function(e) { showNotification(paste("Error creating plot:", e$message), type = "error") }) }) } # Run the application shinyApp(ui = ui, server = server)