TimStats's picture
Update app.R
96fb294 verified
Raw
History Blame Contribute Delete
13.2 kB
library(shiny)
library(dplyr)
library(ggplot2)
library(utils)
library(ggiraph)
library(htmlwidgets)
data1 <- read.csv("data1.csv", header = TRUE, check.names = FALSE, fileEncoding = "UTF-8")
data1 <- data1[,2:54]
# Sample pitch type lookup - modify if needed
pitch_type_lookup <- data.frame(
pitch_name = c("Changeup", "Curveball", "Cutter", "Eephus", "Forkball",
"Four-Seam Fastball", "Knuckle Ball", "Knuckle Curve",
"Screwball", "Sinker", "Slider", "Slurve", "Splitter", "Sweeper"),
pitch_abbr = c("CH", "CU", "FC", "EP", "FO", "FF", "KN", "KC",
"SC", "SI", "SL", "SV", "FS", "ST"),
stringsAsFactors = FALSE
)
# Define pitch colors
pitch_colors <- c(
"FF" = "#FF4136",
"SI" = "#FF851B",
"FC" = "#FFDC00",
"CH" = "#2ECC40",
"SL" = "#0074D9",
"ST" = "#ED68ED",
"CU" = "#B10DC9",
"FS" = "#01FF70",
"KC" = "#85144b",
"SV" = "#3D9970",
"KN" = "#39CCCC",
"FO" = "#F012BE",
"EP" = "#AAAAAA",
"FA" = "#7FDBFF",
"SC" = "#FF69B4"
)
break_plot_Szn <- function(game, data1, sdate, edate) {
game <- game %>% filter(between(as.Date(date), sdate, edate))
total_pitches <- nrow(game)
usage_stats <- game %>%
group_by(pitch_name) %>%
summarise(
count = n(),
usage = sprintf("%.1f%%", (count/total_pitches) * 100)
)
game <- game %>%
left_join(pitch_type_lookup, by = c("pitch_name")) %>%
left_join(usage_stats, by = "pitch_name")
game <- game %>% filter(!is.na(pitch_abbr))
title <- paste0(unique(game$`Pitcher Name`)," ",sdate," to ",edate)
pitcher_hand <- unique(game$phand)
angle_degrees <- round(mean(game$arm_angle,na.rm = TRUE),digits = 1)
angle_radians <- angle_degrees * (pi / 180)
if(pitcher_hand == "L") {
leg <- c(0.08, .22)
factor <- -1
} else {
leg <- c(0.92, .22)
factor <- 1
}
x_end <- 50 * cos(angle_radians) * factor
y_end <- 50 * sin(angle_radians)
game_pitches <- unique(game$pitch_abbr)
used_colors <- pitch_colors[game_pitches]
avg_locations <- game %>%
group_by(pitch_abbr) %>%
summarize(
avg_HB = mean(HB, na.rm = TRUE),
avg_IVB = mean(IVB, na.rm = TRUE),
avg_velo = mean(start_speed, na.rm = TRUE),
usage = first(usage)
)
legend_labels <- paste0(names(used_colors), " (",
avg_locations$usage[match(names(used_colors), avg_locations$pitch_abbr)], ")")
fill_values <- used_colors
names(fill_values) <- legend_labels
arm_angle_data <- data1 %>%
filter(!is.na(arm_angle), !is.na(phand), !is.na(pitch_name)) %>%
filter(abs(round(arm_angle) - angle_degrees) <= 2, phand == pitcher_hand) %>%
left_join(pitch_type_lookup, by = c("pitch_name")) %>%
filter(!is.na(pitch_abbr)) %>%
filter(pitch_abbr %in% game_pitches)
p <- ggplot(game, aes(x = HB, y = IVB)) +
geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
stat_ellipse(
data = arm_angle_data,
aes(fill = paste0(pitch_abbr, " (",
avg_locations$usage[match(pitch_abbr, avg_locations$pitch_abbr)], ")")),
geom = "polygon",
alpha = 0.2,
level = 0.68,
show.legend = FALSE
) +
geom_point_interactive(
data = avg_locations,
aes(x = avg_HB, y = avg_IVB,
fill = paste0(pitch_abbr, " (", usage, ")"),
tooltip = sprintf("Avg Velo: %.1f mph\nAvg IVB: %.1f in\nAvg HB: %.1f in",
avg_velo, avg_IVB, avg_HB)),
color = "black",
size = 6,
stroke = .5,
shape = 21
) +
geom_segment(x = 0, y = 0, xend = x_end, yend = y_end,
color = "red", linewidth = 1, linetype = 5) +
scale_fill_manual(values = fill_values) +
labs(
x = "Horizontal Break (in)",
y = "Induced Vertical Break (in)",
title = title,
subtitle = paste0("Arm Angle: ", angle_degrees, "\u00b0"),
caption = "Data: MLB | Viz: @TimStats | tim-stats.com\nEllipses show average movement for same-handed pitchers at similar arm angles"
) +
xlim(-25, 25) +
ylim(-25, 25) +
theme_minimal() +
theme(
legend.position = leg,
plot.title = element_text(hjust = 0.5, face = "bold", color = "white"),
plot.subtitle = element_text(hjust = 0.5, face = "italic", color = "white"),
aspect.ratio = 1,
plot.background = element_rect(fill = "#333333", color = NA),
panel.background = element_rect(fill = "#333333", color = NA),
axis.text = element_text(color = "white"),
axis.title = element_text(color = "white"),
legend.background = element_rect(fill = "#333333"),
legend.text = element_text(color = "white"),
legend.title = element_blank(),
plot.margin = margin(10, 5, 10, 5),
axis.line = element_blank(),
axis.ticks = element_line(color = "white"),
panel.grid = element_blank(),
legend.key = element_blank(),
plot.caption = element_text(color = "white", hjust = 0.5),
panel.border = element_blank()
)
return(p)
}
break_plot_tot <- function(game, data1, sdate, edate) {
game <- game %>% filter(between(as.Date(date), sdate, edate))
total_pitches <- nrow(game)
usage_stats <- game %>%
group_by(pitch_name) %>%
summarise(
count = n(),
usage = sprintf("%.1f%%", (count/total_pitches) * 100)
)
game <- game %>%
left_join(pitch_type_lookup, by = c("pitch_name")) %>%
left_join(usage_stats, by = "pitch_name")
game <- game %>% filter(!is.na(pitch_abbr))
title <- paste0(unique(game$`Pitcher Name`)," ",sdate," to ",edate)
pitcher_hand <- unique(game$phand)
angle_degrees <- round(mean(game$arm_angle,na.rm = TRUE),digits = 1)
angle_radians <- angle_degrees * (pi / 180)
if(pitcher_hand == "L") {
leg <- c(0.08, .22)
factor <- -1
} else {
leg <- c(0.92, .22)
factor <- 1
}
x_end <- 50 * cos(angle_radians) * factor
y_end <- 50 * sin(angle_radians)
game_pitches <- unique(game$pitch_abbr)
used_colors <- pitch_colors[game_pitches]
avg_locations <- game %>%
group_by(pitch_abbr) %>%
summarize(
avg_HB = mean(HB, na.rm = TRUE),
avg_IVB = mean(IVB, na.rm = TRUE),
avg_velo = mean(start_speed, na.rm = TRUE),
usage = first(usage)
)
legend_labels <- paste0(names(used_colors), " (",
avg_locations$usage[match(names(used_colors), avg_locations$pitch_abbr)], ")")
fill_values <- used_colors
names(fill_values) <- legend_labels
arm_angle_data <- data1 %>%
filter(!is.na(arm_angle), !is.na(phand), !is.na(pitch_name)) %>%
filter(abs(round(arm_angle) - angle_degrees) <= 2, phand == pitcher_hand) %>%
left_join(pitch_type_lookup, by = c("pitch_name")) %>%
filter(!is.na(pitch_abbr)) %>%
filter(pitch_abbr %in% game_pitches)
p <- ggplot(game, aes(x = HB, y = IVB)) +
geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
stat_ellipse(
data = arm_angle_data,
aes(fill = paste0(pitch_abbr, " (",
avg_locations$usage[match(pitch_abbr, avg_locations$pitch_abbr)], ")")),
geom = "polygon",
alpha = 0.2,
level = 0.68,
show.legend = FALSE
) +
geom_point_interactive(
aes(fill = paste0(pitch_abbr, " (", usage, ")"),
tooltip = sprintf("Velo: %.1f mph\nIVB: %.1f in\nHB: %.1f in",
start_speed, IVB, HB)),
color = "#c6c6c6",
size = 3,
stroke = .5,
shape = 21
) +
geom_point_interactive(
data = avg_locations,
aes(x = avg_HB, y = avg_IVB,
fill = paste0(pitch_abbr, " (", usage, ")"),
tooltip = sprintf("Avg Velo: %.1f mph\nAvg IVB: %.1f in\nAvg HB: %.1f in",
avg_velo, avg_IVB, avg_HB)),
color = "black",
size = 6,
stroke = .5,
shape = 21
) +
geom_segment(x = 0, y = 0, xend = x_end, yend = y_end,
color = "red", linewidth = 1, linetype = 5) +
scale_fill_manual(values = fill_values) +
labs(
x = "Horizontal Break (in)",
y = "Induced Vertical Break (in)",
title = title,
subtitle = paste0("Arm Angle: ", angle_degrees, "\u00b0"),
caption = "Data: MLB | Viz: @TimStats | tim-stats.com\nEllipses show average movement for same-handed pitchers at similar arm angles"
) +
xlim(-25, 25) +
ylim(-25, 25) +
theme_minimal() +
theme(
legend.position = leg,
plot.title = element_text(hjust = 0.5, face = "bold", color = "white"),
plot.subtitle = element_text(hjust = 0.5, face = "italic", color = "white"),
aspect.ratio = 1,
plot.background = element_rect(fill = "#333333", color = NA),
panel.background = element_rect(fill = "#333333", color = NA),
axis.text = element_text(color = "white"),
axis.title = element_text(color = "white"),
legend.background = element_rect(fill = "#333333"),
legend.text = element_text(color = "white"),
legend.title = element_blank(),
plot.margin = margin(10, 5, 10, 5),
axis.line = element_blank(),
axis.ticks = element_line(color = "white"),
panel.grid = element_blank(),
legend.key = element_blank(),
plot.caption = element_text(color = "white", hjust = 0.5),
panel.border = element_blank()
)
return(p)
}
ui <- fluidPage(
# Remove useGirafe() line and just have girafeOutput in mainPanel
titlePanel("2020-2024 MLB Pitch Plots"),
sidebarLayout(
sidebarPanel(
width = 3,
selectInput("player", "Select Player:",
choices = unique(data1$`Pitcher Name`),
width = "100%"),
dateRangeInput("date1",
"Dates: (Be wary of multi year plots as arm angles may vary over years)",
start = "2024-03-20",
end = "2024-10-01",
width = "100%"),
radioButtons("type", "Plot Type",
choices = c("Season Average", "All Pitches")),
actionButton("submit", "Update Plot",
class = "btn btn-primary btn-block",
style = "margin-bottom: 10px"),
downloadButton("download", "Download Plot",
class = "btn btn-success btn-block")
),
mainPanel(
width = 9,
girafeOutput("Plot", width = "750px", height = "750px")
)
)
)
server <- function(input, output, session) {
plotSettings <- reactiveVal(list(
player = NULL,
type = "Season Average",
dates = c(as.Date("2024-03-20"), as.Date("2024-10-01"))
))
observeEvent(input$submit, {
plotSettings(list(
player = input$player,
type = input$type,
dates = c(input$date1[1], input$date1[2])
))
})
output$Plot <- renderGirafe({
settings <- plotSettings()
req(settings$player)
game <- data1 %>%
filter(`Pitcher Name` == settings$player)
if(nrow(game) == 0) {
return(girafe(ggobj = ggplot() +
annotate("text", x = 0.5, y = 0.5,
label = "No data available for selected date range",
color = "white") +
theme_void() +
theme(plot.background = element_rect(fill = "#333333", color = NA))))
}
plot <- if(settings$type == "Season Average") {
break_plot_Szn(game, data1, settings$dates[1], settings$dates[2])
} else {
break_plot_tot(game, data1, settings$dates[1], settings$dates[2])
}
girafe(ggobj = plot,
width_svg = 10,
height_svg = 10,
options = list(
opts_tooltip(
opacity = 0.8,
css = "background-color: #333333; color: white; padding: 5px; border-radius: 3px;"
),
opts_hover(
css = "fill-opacity: 1; stroke: #FFF; stroke-width: 2;"
),
opts_selection(
type = "none"
)
))
})
output$download <- downloadHandler(
filename = function() {
settings <- plotSettings()
paste0(
gsub(" ", "_", settings$player), "_",
format(settings$dates[1], "%Y%m%d"), "_to_",
format(settings$dates[2], "%Y%m%d"), ".png"
)
},
content = function(file) {
settings <- plotSettings()
game <- data1 %>%
filter(`Pitcher Name` == settings$player)
plot <- if(settings$type == "Season Average") {
break_plot_Szn(game, data1, settings$dates[1], settings$dates[2])
} else {
break_plot_tot(game, data1, settings$dates[1], settings$dates[2])
}
ggsave(file,
plot = plot,
width = 10,
height = 10,
dpi = 300,
bg = "#333333")
}
)
}
# Run the application
shinyApp(ui = ui, server = server)