TMSumC / app.R
TimStats's picture
Update app.R
fdb7bad verified
Raw
History Blame Contribute Delete
35.4 kB
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(lightgbm)
library(httr)
library(jpeg)
library(zoo)
library(gtable)
library(xgboost)
options(shiny.maxRequestSize = 10000000 * 1024^2)
pdf(file = NULL)
Sys.setenv(TZ='EST')
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("25062090")
return(result)
}
}
return(FALSE)
}
model <- xgb.load('TimStuff2.model')
# Helper functions
download_and_process_image <- function(url) {
tryCatch({
response <- GET(url)
content_type <- http_type(response)
if (content_type %in% c("image/png", "image/jpeg")) {
temp_file <- tempfile(fileext = ifelse(content_type == "image/png", ".png", ".jpg"))
writeBin(content(response, "raw"), temp_file)
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(tmilb){
tmilb %>%
select(Batter,Pitcher,PlayResult,PitchCall,TaggedPitchType,RelSpeed,SpinRate,
Extension,PlateLocSide,PlateLocHeight,RelSide,RelHeight,ax0,ay0,az0,vx0,
vz0,vy0,pfxx,pfxz,InducedVertBreak,HorzBreak,SpinAxis,PitcherThrows,BatterSide,PitchofPA,VertApprAngle,AutoPitchType) %>%
rename(
"Batter Name" = Batter, "Pitcher Name" = Pitcher, result = PlayResult,
description = PitchCall, pitch_name = TaggedPitchType, start_speed = RelSpeed,
spin_rate = SpinRate, extension = Extension, px = PlateLocSide, pz = PlateLocHeight,
x0 = RelSide, z0 = RelHeight, ax = ax0, ay = ay0, az = az0, vx0 = vx0,
vz0 = vz0, vy0 = vy0, pfxX = pfxx, pfxZ = pfxz, IVB = InducedVertBreak,
HB = HorzBreak, spinDirection = SpinAxis, phand = PitcherThrows,
bhand = BatterSide, PitchNum = PitchofPA, VAA = VertApprAngle, AutoPitchType = AutoPitchType
) %>%
mutate(
is_strike_swinging = ifelse(description == "StrikeSwinging", TRUE, FALSE),
phand = ifelse(phand == "Right", "R", "L"),
bhand = ifelse(bhand == "Right", "R", "L")
)
}
break_plot <- function(game){
game <- game %>% mutate(
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
))
pitch_colors <- c(
"Four-Seam"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00",
"Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED",
"Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b",
"Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Two-Seam Fastball"= "#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))
}
pitch_plot <- function(game, title) {
game <- game %>% mutate(
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
))
pitch_colors <- c(
"Four-Seam"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00",
"Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED",
"Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b",
"Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Two-Seam Fastball"= "#F012BE",
"Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD",
"Screwball"= "#FF69B4"
)
ggplot(game, aes(x = px, y = pz, color = pitch_name)) +
geom_point(size = 3.5) +
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 = title) +
xlim(-3, 3) + ylim(0.2, 4) + coord_fixed(ratio = 1) + theme_minimal() +
theme(
legend.position = "bottom",
plot.title = element_text(hjust = 0.5, face = "bold"),
axis.text = element_blank(),
axis.ticks = element_blank()
) +
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") %>% mutate(
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
))
game_rhb <- game %>% filter(bhand == "R") %>% mutate(
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
))
# Define home plate coordinates (adjusted position)
home_plate <- data.frame(
x = c(0.6, -0.6, -0.7083, 0, 0.7083), # Keep x-coordinates
y = c(0.5, 0.5, 0.25, 0, 0.25) # Adjust y-coordinates to put point at 0
)
pitch_colors <- c(
"Four-Seam"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00",
"Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED",
"Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b",
"Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Two-Seam Fastball"= "#F012BE",
"Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD",
"Screwball"= "#FF69B4"
)
# Create left-handed batter plot
plot_lhb <- ggplot(game_lhb, aes(x = -px, y = pz, color = pitch_name)) +
# Add home plate first (so it appears under the points)
geom_polygon(data = home_plate, aes(x = x, y = y),
fill = "white", color = "black", inherit.aes = FALSE) +
# Add batter image (now using left_batter on right side)
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()
)
# Create right-handed batter plot
plot_rhb <- ggplot(game_rhb, aes(x = -px, y = pz, color = pitch_name)) +
# Add home plate first
geom_polygon(data = home_plate, aes(x = x, y = y),
fill = "white", color = "black", inherit.aes = FALSE) +
# Add batter image (now using right_batter on left side)
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)
}
transform_statcast_data <- function(df,
release_extension_col = "extension",
vx0_col = "vx0",
vy0_col = "vy0",
vz0_col = "vz0",
ax_col = "ax",
ay_col = "ay",
az_col = "az",
arm_angle_col = NULL,
p_throws_col = NULL) {
# Constants
MOUND_DISTANCE <- 60.5
STATCAST_INITIAL_MEASUREMENT <- 50.0
Z_CONSTANT <- 32.174
HOME_PLATE_HEIGHT <- 17/12 # Convert 17 inches to feet
print("etds")
# Compute release position
df$release_pos_y <- MOUND_DISTANCE - df[[release_extension_col]]
print("etds1")
# Compute release time
df$release_time <- (-df[[vy0_col]] -
sqrt((df[[vy0_col]]^2) -
(2 * df[[ay_col]] * (STATCAST_INITIAL_MEASUREMENT - df$release_pos_y)))) / df[[ay_col]]
print("etds23")
# Compute release velocities
df$vxR <- df[[vx0_col]] + (df[[ax_col]] * df$release_time)
df$vyR <- df[[vy0_col]] + (df[[ay_col]] * df$release_time)
df$vzR <- df[[vz0_col]] + (df[[az_col]] * df$release_time)
print("etds2")
# Compute time to home plate
df$tf <- (-df$vyR - sqrt(df$vyR^2 - 2 * df[[ay_col]] * (df$release_pos_y - HOME_PLATE_HEIGHT))) / df[[ay_col]]
print("etds3")
# Compute average velocities
df$vxbar <- (2 * df$vxR + df[[ax_col]] * df$tf) / 2
df$vybar <- (2 * df$vyR + df[[ay_col]] * df$tf) / 2
df$vzbar <- (2 * df$vzR + df[[az_col]] * df$tf) / 2
# Compute average velocity magnitude
df$vbar <- sqrt(df$vxbar^2 + df$vybar^2 + df$vzbar^2)
print("etds4")
# Compute drag acceleration
df$adrag <- -(df[[ax_col]] * df$vxbar +
df[[ay_col]] * df$vybar +
(df[[az_col]] + Z_CONSTANT) * df$vzbar) / df$vbar
# Compute Magnus force components
df$amagx <- df[[ax_col]] + df$adrag * df$vxbar / df$vbar
df$amagy <- df[[ay_col]] + df$adrag * df$vybar / df$vbar
df$amagz <- df[[az_col]] + df$adrag * df$vzbar / df$vbar + Z_CONSTANT
# Compute Magnus force magnitude
df$amag <- sqrt(df$amagx^2 + df$amagy^2 + df$amagz^2)
# Original transformation calculations
df$v_mag <- sqrt(df$vxR^2 + df$vyR^2 + df$vzR^2)
# Compute tangential unit vectors
df$tang_x <- df$vxR / df$v_mag
df$tang_y <- df$vyR / df$v_mag
df$tang_z <- df$vzR / df$v_mag
print("etds5")
# Handle arm angle and handedness
if (!is.null(arm_angle_col)) {
df$handedness_factor <- ifelse(df[[p_throws_col]] == "R", 1, -1)
df$temp_x <- cos(df[[arm_angle_col]]) * df$handedness_factor
df$temp_y <- 0
df$temp_z <- sin(df[[arm_angle_col]])
} else {
df$temp_x <- 0
df$temp_y <- rep(0, nrow(df))
df$temp_z <- 1
}
# Compute horizontal vector
df$horz_x <- df$temp_y * df$tang_z - df$temp_z * df$tang_y
df$horz_y <- df$temp_z * df$tang_x - df$temp_x * df$tang_z
df$horz_z <- df$temp_x * df$tang_y - df$temp_y * df$tang_x
print("etds6")
# Normalize horizontal vector
df$horz_mag <- sqrt(df$horz_x^2 + df$horz_y^2 + df$horz_z^2)
df$horz_x <- df$horz_x / df$horz_mag
df$horz_y <- df$horz_y / df$horz_mag
df$horz_z <- df$horz_z / df$horz_mag
# Compute vertical vector
df$vert_x <- df$tang_y * df$horz_z - df$tang_z * df$horz_y
df$vert_y <- df$tang_z * df$horz_x - df$tang_x * df$horz_z
df$vert_z <- df$tang_x * df$horz_y - df$tang_y * df$horz_x
print("etds7")
# Project accelerations
df$a_tang <- df[[ax_col]] * df$tang_x + df[[ay_col]] * df$tang_y + df[[az_col]] * df$tang_z
df$a_horz <- df[[ax_col]] * df$horz_x + df[[ay_col]] * df$horz_y + df[[az_col]] * df$horz_z
df$a_vert <- df[[ax_col]] * df$vert_x + df[[ay_col]] * df$vert_y + df[[az_col]] * df$vert_z
print("ets")
# Clean up intermediate columns
intermediate_cols <- c("temp_x", "temp_y", "temp_z", "handedness_factor",
"horz_mag", "horz_x", "horz_y", "horz_z",
"vert_x", "vert_y", "vert_z",
"tang_x", "tang_y", "tang_z")
df <- df[, !names(df) %in% intermediate_cols]
return(df)
}
calculate_timstuff <- function(game) {
game <- game %>%
mutate(
VAA = calculate_VAA(vz0, ay, az, vy0, 50),
EAA = calculate_EAA(extension),
SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection),
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 <- if(any(!complete_rows)) {
na_rows <- game[!complete_rows, ]
na_rows$TimStuff <- NA
na_rows
} else {
# Create empty data frame with same structure plus TimStuff
empty_df <- game_complete[0, ] # Get structure from game_complete
empty_df$TimStuff <- numeric() # Add empty TimStuff column
empty_df
}
if(nrow(game_complete) > 0) {
pred_matrix <- as.matrix(game_complete[, feature_vars])
predictions <- predict(model, pred_matrix)
game_complete$TimStuff <- scale_TimStuff(predictions, -0.002620635, 0.006021368)
}
if(nrow(game_complete) > 0 && nrow(game_na) > 0) {
game_result <- rbind(game_complete, game_na)
} else if(nrow(game_complete) > 0) {
game_result <- game_complete
} else if(nrow(game_na) > 0) {
game_result <- game_na
} else {
game_result <- game
}
return(game_result)
}
summary_table <- function(game) {
game <- calculate_timstuff(game)
if("TimStuff" %in% colnames(game)) {
game <- game %>%
mutate(TimStuff = ifelse(TimStuff < 50, NA, TimStuff))
}
# Calculate main summary
main_summary <- game %>%
mutate(
team_fielding_id = ifelse(description %in% c("StrikeCalled", "StrikeSwinging"), 1, 0),
swing = ifelse(description %in% c("FoulBall", "StrikeSwinging", "InPlay","FoulBallNotFieldable"), 1, 0),
is_strike_swinging = as.integer(is_strike_swinging),
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
),
Pitch = pitch_name,
FPS = case_when(description %in% c("StrikeCalled","StrikeSwinging","FoulBallNotFieldable","FoulBall","InPlay") ~ TRUE, TRUE ~ FALSE),
) %>%
group_by(Pitch) %>%
summarize(
Pitches = n(),
'Pitch%' = round(n() / nrow(game) * 100, 1),
"FP Strike%" = round(mean(FPS[PitchNum == 1], na.rm = TRUE) * 100, 1),
'Avg. Velo' = round(mean(start_speed, na.rm = TRUE), 1),
'Top Velo' = round(max(start_speed, na.rm = TRUE), 1), # Add Top Velo
'Spin Rate' = round(mean(spin_rate, na.rm = TRUE), 0),
'Extension' = round(mean(extension, na.rm = TRUE), 1),
'IVB' = round(mean(IVB, na.rm = TRUE), 1),
'HB' = round(mean(HB, na.rm = TRUE), 1),
'RelH' = round(mean(z0, na.rm = TRUE), digits = 1),
'RelS' = round(mean(x0, na.rm = TRUE), digits = 1),
'VAA' = round(mean(VAA, na.rm = TRUE), 1),
'CSW%' = round(sum(team_fielding_id, na.rm = TRUE) / sum(!is.na(team_fielding_id)) * 100, 1),
'Whiff%' = round(sum(is_strike_swinging, na.rm = TRUE) / sum(swing, na.rm = TRUE) * 100, 1),
'TimStuff+' = if("TimStuff" %in% colnames(game)) round(mean(TimStuff, na.rm = TRUE), 0) else NA
) %>%
arrange(-Pitches)
# Calculate totals
total_row <- game %>%
mutate(
team_fielding_id = ifelse(description %in% c("StrikeCalled", "StrikeSwinging"), 1, 0),
swing = ifelse(description %in% c("FoulBall", "StrikeSwinging", "InPlay","FoulBallNotFieldable"), 1, 0),
is_strike_swinging = as.integer(is_strike_swinging),
FPS = case_when(description %in% c("StrikeCalled","StrikeSwinging","FoulBallNotFieldable","FoulBall") ~ TRUE, TRUE ~ FALSE)
) %>%
summarize(
Pitch = "Total",
Pitches = n(),
'Pitch%' = 100,
"FP Strike%" = round(mean(FPS[PitchNum == 1], na.rm = TRUE) * 100, 1),
'Avg. Velo' = NA_real_,
'Top Velo' = round(max(start_speed, na.rm = TRUE), 1), # Add overall top velo
'Spin Rate' = NA_real_,
'Extension' = round(mean(extension, na.rm = TRUE), 1),
'IVB' = NA_real_,
'HB' = NA_real_,
'RelH' = round(mean(z0, na.rm = TRUE), digits = 1),
'RelS' = round(mean(x0, na.rm = TRUE), digits = 1),
'VAA' = round(mean(VAA, na.rm = TRUE), 1),
'CSW%' = round(sum(team_fielding_id, na.rm = TRUE) / sum(!is.na(team_fielding_id)) * 100, 1),
'Whiff%' = round(sum(is_strike_swinging, na.rm = TRUE) / sum(swing, na.rm = TRUE) * 100, 1),
'TimStuff+' = if("TimStuff" %in% colnames(game)) round(mean(TimStuff, na.rm = TRUE), 0) else NA
)
# Combine main summary with total row
combined_summary <- bind_rows(main_summary, total_row) %>%
rename(
"Type" = Pitch, "#" = Pitches, "Velo" = `Avg. Velo`,
"Top" = `Top Velo`, # Rename Top Velo to shorter column name
"Spin" = `Spin Rate`, "Ext" = Extension, "Usage%" = `Pitch%`
) %>%
mutate(
"Usage%" = paste0(`Usage%`, "%"),
"Spin" = ifelse(Type == "Total", "-", format(round(as.numeric(Spin)), big.mark = ",")),
"Velo" = ifelse(Type == "Total", "-", as.character(Velo)),
"IVB" = ifelse(Type == "Total", "-", as.character(IVB)),
"HB" = ifelse(Type == "Total", "-", as.character(HB)),
`CSW%` = paste0(`CSW%`, "%"),
`Whiff%` = paste0(`Whiff%`, "%"),
`FP Strike%` = paste0(`FP Strike%`, "%")
)
return(combined_summary)
}
# Modify the UI
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 {
fluidPage(
titlePanel("Trackman Summary Card"),
sidebarLayout(
sidebarPanel(
width = 3,
fileInput("incsv", "Input File", accept = c(".csv"), multiple = FALSE),
actionButton("update", "Load Pitchers", icon("magnifying-glass"),
class = "btn-primary btn-block"),
selectizeInput("pitcher", "Pitcher Name:", NULL),
numericInput("rollnum", "Rolling Pitch #", c("1")),
actionButton("update1", "Make Card", icon("plus"),
class = "btn-success btn-block"),
downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
),
mainPanel(
div(style = "width: 1000px; height: 1000px; overflow: auto;",
plotOutput("combinedPlot", width = "100%", height = "100%")
)
)
)
)
}
})
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 Hall of Fame tier subscription.",
easyClose = TRUE
))
}
})
game_data <- reactiveVal()
observeEvent(input$update, {
req(input$incsv)
df <- read.csv(input$incsv$datapath)
pname <- pitcher_summary(df)
game_data(pname)
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)
}
})
rolling_timstuff <- reactive({
req(input$update1, game_data())
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)# %>%
# mutate(
# pitch pitch_name %in% c("ChangeUp") ~ "Changeup",
# _name = case_when(
# pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
# pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
# pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
# pitch_name %in% c("CutFastBall") ~ "Cutter",
# TRUE ~ pitch_name
# ))
game <- calculate_timstuff(game)
print("Test")
print(game)
print(nrow(game))
#names(game)
game <- game %>%
mutate(TimStuff = ifelse(TimStuff < 40, NA, TimStuff))
custom_rollmean <- function(x, k) {
n <- length(x)
result <- numeric(n)
for (i in 1:n) {
if (i < k) {
# For the first k-1 points, use a window size of 1 (i.e., the point itself)
result[i] <- x[i]
} else {
# For k and onwards, use the specified rolling window
window <- x[(i-k+1):i]
result[i] <- mean(window, na.rm = TRUE)
}
}
return(result)
}
result <- game %>%
group_by(pitch_name) %>%
mutate(
TimStuff = as.numeric(TimStuff),
rolling_timstuff = custom_rollmean(TimStuff, k = input$rollnum),
pitch_number = row_number()
) %>%
ungroup()
return(result)
})
combinedPlot <- reactiveVal()
observeEvent(input$update1,{
req(game_data())
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
if(nrow(game) == 0) {
showNotification("No data available for the selected pitcher.", type = "warning")
return()
}
get_logo <- function() {
if (FALSE) {
# Try to get team logo using the selected team ID
logo_url <- paste0("https://static.trumedianetworks.com/images/ncaabaseball/teams/", input$teamName, ".png")
img_result <- download_and_process_image(logo_url)
if (!is.null(img_result)) {
return(rasterGrob(img_result$img, interpolate = TRUE))
}
}
# Fallback to default logo
default_url <- "https://pbs.twimg.com/profile_images/1754697221595910144/xziLC-5v_400x400.jpg"
img_result <- download_and_process_image(default_url)
if (!is.null(img_result)) {
return(rasterGrob(img_result$img, interpolate = TRUE))
} else {
return(textGrob("Image not available", gp = gpar(col = "red", fontsize = 20)))
}
}
# Get the logo once and use it twice
logo_grob <- get_logo()
break_plot <- break_plot(game) + theme(legend.position = "none")
pitch_plots <- pitch_plot_split(game)
table_data <- summary_table(game)
num_rows <- nrow(table_data)
print(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.07, # Type
0.04, # #
0.08, # Usage%
0.08, # FP Strike%
0.05, # Velo
0.05, # Top
0.08, # Spin
0.05, # Ext
0.05, # IVB
0.05, # HB
0.04, # RelH
0.04, # RelS
0.06, # VAA
0.06, # CSW%
0.06, # Whiff%
0.06 # Stuff+
), "npc")
for(i in seq(2, nrow(table_plot), 2)) {
table_plot$grobs[[i]]$gp$fill <- "#f9f9f9"
}
milb_url <- "https://x.com/TimStats/photo"
mlb_url <- "https://pbs.twimg.com/profile_images/1754697221595910144/xziLC-5v_400x400.jpg"
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))
}
pitch_colors <- c(
"Four-Seam"= "#FF4136", "Sinker"= "#FF851B", "Cutter"= "#FFDC00",
"Changeup"= "#2ECC40", "Slider"= "#0074D9", "Sweeper"= "#ED68ED",
"Curveball"= "#B10DC9", "Splitter"= "#01FF70", "Knuckle Curve"= "#85144b",
"Slurve"= "#3D9970", "Knuckle Ball"= "#39CCCC", "Two-Seam Fastball"= "#F012BE",
"Eephus"= "#AAAAAA", "Fastball"= "#7FDBFF", "Slow Curve"= "#DDDDDD",
"Screwball"= "#FF69B4"
)
# Create the rolling TimStuff+ graph
rolling_data <- rolling_timstuff() %>%
mutate(
pitch_name = case_when(
pitch_name %in% c("Fastball", "FourSeamFastBall") ~ "Four-Seam",
pitch_name %in% c("TwoSeamFastBall", "OneSeamFastBall", "Sinker") ~ "Sinker",
pitch_name %in% c("ChangeUp") ~ "Changeup",
pitch_name %in% c("KnuckleCurve") ~ "Knuckle Curve",
pitch_name %in% c("CutFastBall") ~ "Cutter",
pitch_name %in% c('Undefined') ~ AutoPitchType,
TRUE ~ pitch_name
))
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 = paste0(input$rollnum,"-Pitch Rolling TimStuff+"),
x = "Pitch Number", y = "TimStuff+") +
scale_y_continuous(limits = c(70, 130), na.value = NA) +
scale_x_continuous(breaks = seq(input$rollnum, max(rolling_data$pitch_number, na.rm = TRUE), by = input$rollnum), limits = c(input$rollnum, 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 there's no valid data, display a message
if(all(is.na(rolling_data$rolling_timstuff))) {
timstuff_plot <- ggplot() +
theme_void() +
labs(title = "Insufficient data for Stuff+ calculation") +
theme(
plot.title = element_text(hjust = 0.5, face = "bold")
)
}
title_text <- textGrob(
paste(input$pitcher, input$title, "Summary Card by @TimStats"),
gp = gpar(fontsize = 16, fontface = "bold")
)
data_source_text <- textGrob(
" ",
gp = gpar(fontsize = 8),
x = unit(1, "npc") - unit(2, "mm"),
y = unit(2, "mm"),
just = c("right", "bottom")
)
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())
}
# Combine all plots
combined <- grid.arrange(
# Row 1: Title and Player Image
arrangeGrob(
img_grob,
title_text,
logo_grob,
ncol = 3,
widths = c(1, 2, 1)
),
# Row 2: Rolling TimStuff+ graph and boxscore
arrangeGrob(
timstuff_plot,
break_plot,
ncol = 2,
widths = c(2,1)
),
# Row 3: Three plots in a row
arrangeGrob(
pitch_plots$lhb,
pitch_plots$rhb,
ncol = 2,
widths = c(1,1)
),
# Row 4: Legend
legend,
# Row 5: Pitch summary table
table_plot,
# Layout parameters
nrow = 5,
heights = c(0.8, 1.2, 1.2, 0.2, 1)
)
grid.arrange(
gtable_add_padding(
combined,
padding = unit(c(20, 20, 20, 20), "points") # top, right, bottom, left margins
)
)
combinedPlot(combined)
output$combinedPlot <- renderPlot({
grid.draw(combinedPlot())
}, width = 1200, height = 1200)
output$combinedPlot <- renderPlot({
grid.draw(combinedPlot())
}, width = 1000, height = 1000) # Modified to be square
# Update the download handler dimensions
output$downloadPlot <- downloadHandler(
filename = function() {
paste("baseball_card_", Sys.Date(), ".png", sep = "")
},
content = function(file) {
ggsave(file, plot = combinedPlot(), width = 15, height = 15, dpi = 300, units = "in") # Modified to be square
}
)
})
}
# Run the application
shinyApp(ui = ui, server = server)