SummaryCard / app.R
TimStats's picture
Update app.R
b470175 verified
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.model')
# 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")
)
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")
)
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")
) %>%
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")
) %>%
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 = "16")
) %>%
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)", "Select Complex League Games"),
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 == "Select Complex League Games"){
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 == "Select Complex League Games"){
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)