SummaryCard / app.R
TimStats's picture
Update app.R
9dabf8b verified
Raw
History Blame
16 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)
pdf(file = NULL)
Sys.setenv(TZ='EST')
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)
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")
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){
ggplot()+
geom_point(game,mapping =
aes(x=HB, y = IVB,color = pitch_name),size = 4) +
geom_vline(xintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
geom_hline(yintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
xlab("Horizontal Break from Pitcher's Perspective (in) ")+
ylab("Induced Verical Break (in)") +
xlim(-25,25) +
ylim(-25,25) +
ggthemes::theme_igray()+
theme(panel.grid.minor = element_line(color = "gray",
size = 0.25,
linetype = 1))+
guides(color = guide_legend(title = "Pitch Type"))
}
pitch_plot <- function(game){
ggplot()+
geom_point(game,mapping =
aes(x=px, y = pz,color = pitch_name),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))+
xlab(" ")+
ylab(" ") +
xlim(-3,3)+
ylim(0.2,4)+
coord_fixed(ratio = 1) +
ggthemes::theme_few()+
guides(color = guide_legend(title = "Pitch Type"))+
theme(axis.text.x=element_blank(),
axis.ticks.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.y=element_blank())
}
summary_table <- function(game){
rows <- nrow(game)
sumtable <- game |>
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)) |>
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) |>
group_by(Pitch)|>
summarize(Pitches = n(),
'Pitch%' = round(sum(Pitches)/sum(rows) * 100,digits = 1),
'Avg. Velo' = round(sum(start_speed)/Pitches,digits = 1),
'Spin Rate' = round(sum(spin_rate)/ Pitches,digits = 1),
'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)/
sum(!is.na(team_fielding_id))*100,digits = 1),
'Whiff%' = round(sum(is_strike_swinging)/
sum(swing)*100,digits = 1))|>
arrange(-Pitches)
}
mlbid <- mlb_schedule(season = 2024, level_ids = "1")
mlbteamH <- mlbid |>
select(teams_home_team_name)
mlbteamH <- distinct(mlbteamH)
mlbteamA <- mlbid |>
select(teams_away_team_name)
mlbteamA <- distinct(mlbteamA)
aaaid <- mlb_schedule(season = 2024, level_ids = "11")
aaateamH <- aaaid |>
select(teams_home_team_name)
aaateamH <- distinct(aaateamH)
aaateamA <- aaaid |>
select(teams_away_team_name)
aaateamA <- distinct(aaateamA)
fslid <- mlb_schedule(season = 2024, level_ids = "14")
fslid <- fslid |>
filter(teams_home_team_name == "Daytona Tortugas" |
teams_home_team_name == "Jupiter Hammerheads" |
teams_home_team_name == "Palm Beach Cardinals" |
teams_home_team_name == "St. Lucie Mets" |
teams_home_team_name == "Bradenton Marauders" |
teams_home_team_name == "Clearwater Threshers" |
teams_home_team_name == "Dunedin Blue Jays" |
teams_home_team_name == "Fort Myers Mighty Mussels" |
teams_home_team_name == "Lakeland Flying Tigers" |
teams_home_team_name == "Tampa Tarpons")
fslteamH <- fslid |>
select(teams_home_team_name)
fslteamH <- distinct(fslteamH)
fslteamA <- fslid |>
select(teams_away_team_name)
fslteamA <- distinct(fslteamA)
sbid <- mlb_schedule(season = 2024, level_ids = "22")
sbteamH <- sbid |>
select(teams_home_team_name)
sbteamH <- distinct(sbteamH)
sbteamA <- sbid |>
select(teams_away_team_name)
sbteamA <- distinct(sbteamA)
ui <- fluidPage(
tags$head(
tags$style(HTML("
@media (max-width: 768px) {
.sidebar { width: 100%; float: none; }
.main-content { margin-left: 0; }
.selectize-input { font-size: 14px; }
.form-group { margin-bottom: 10px; }
.action-button { width: 100%; }
}
"))
),
titlePanel("2024 MLB/AAA/FSL Summary Cards"),
sidebarLayout(
sidebarPanel(
width = 2,
dateInput("date", "Date:"),
selectizeInput("level", "Level:", c("MLB", "AAA", "FSL", "College (Statcast Parks Only)")),
selectizeInput("homeT", "Home Team:", mlbteamH[,1]),
selectizeInput("awayT", "Away Team:", mlbteamA[,1]),
selectizeInput("gamenum", "Game Number (For Doubleheaders):", c("1", "2")),
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
selectizeInput("pitcher", "Pitcher Name:", c(" ")),
selectizeInput("league", "MLB or MiLB Photo", c("MLB", "MiLB")),
textInput("title", "Card Title"),
actionButton("update1", "Make Card", icon("plus"),
style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
downloadButton("downloadPlot", "Download Card")
),
mainPanel(
plotOutput("combinedPlot", height = "900px",width = "1350px")
)
)
)
server <- function(input, output, session) {
observeEvent(input$level, {
if(input$level == "AAA"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = aaateamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = aaateamA[,1])
}
if(input$level == "FSL"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = fslteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = fslteamA[,1])
}
if(input$level == "MLB"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = mlbteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = mlbteamA[,1])
}
if(input$level == "College (Statcast Parks Only)"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = sbteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = sbteamA[,1])
}
})
observeEvent(input$update, {
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 <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
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 <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
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 <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
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 <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
})
combinedPlot <- reactiveVal()
observeEvent(input$update1, {
if(input$level == "MLB"){
id <- 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)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "FSL"){
id <- fslid |>
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)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "AAA"){
id <- aaaid |>
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)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "College (Statcast Parks Only)"){
id <- sbid |>
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)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
break_plot <- break_plot(game)
pitch_plot <- pitch_plot(game)
# Create table plot
table_plot <- tableGrob(summary_table(game),theme = ttheme_default(
core = list(fg_params = list(cex = 1.5)), # Increase text size
colhead = list(fg_params = list(cex = 1.5)),
rowhead = list(fg_params = list(cex = 2.5))))
table_plot$widths[[1]] <- unit(0, "cm")
# Find the text grobs in the first column
text_grobs <- which(sapply(table_plot$grobs, function(g) {
g$name == "core-fg-1" && inherits(g$children[[1]], "text")
}))
# Remove the numbers from these text grobs
for (i in text_grobs) {
text <- table_plot$grobs[[i]]$children[[1]]$label
table_plot$grobs[[i]]$children[[1]]$label <- sub("^\\d+\\s*", "", text)
}
# Draw the modified table
grid.newpage()
grid.draw(table_plot)
# Download and process image
if(input$league == "MLB"){
y <- paste("https://midfield.mlbstatic.com/v1/people/","/mlb/300?circle=false",sep = id)
} else {
y <- paste("https://midfield.mlbstatic.com/v1/people/","/milb/300?circle=false",sep = id)
}
photo <- tempfile()
download.file(y, photo, mode = 'wb')
img <- readPNG(photo)
img_grob <- rasterGrob(img, interpolate = TRUE)
# Combine all elements into one plot
combined <- grid.arrange(
arrangeGrob(table_plot, img_grob, ncol = 2, widths = c(2/3, 1/3)),
arrangeGrob(break_plot, pitch_plot, ncol = 2),
nrow = 2,
top = textGrob(paste(input$title, "Made by @TimStats", sep = " "), gp = gpar(fontsize = 20, font = 2))
)
combinedPlot(combined)
output$combinedPlot <- renderPlot({
grid.draw(combinedPlot())
})
})
output$downloadPlot <- downloadHandler(
filename = function() {
paste("baseball_card_", Sys.Date(), ".png", sep = "")
},
content = function(file) {
ggsave(file, plot = combinedPlot(), width = 18, height = 12, dpi = 300)
}
)
}
shinyApp(ui, server)