Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -81,13 +81,13 @@ pitcher_summary <- function(game_pk,date){
|
|
| 81 |
pitchData.coordinates.vY0,pitchData.coordinates.pfxX,pitchData.coordinates.pfxZ,
|
| 82 |
pitchData.breaks.breakVerticalInduced,pitchData.breaks.breakHorizontal,
|
| 83 |
hitData.launchSpeed,hitData.launchAngle,hitData.totalDistance,details.isInPlay,
|
| 84 |
-
last.pitch.of.ab,pitchData.breaks.spinDirection,matchup.pitchHand.code)
|
| 85 |
colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID",
|
| 86 |
"result","description","pitch_name","des","start_speed",
|
| 87 |
"plateTime","zone","spin_rate","extension","px","pz","x0",
|
| 88 |
"y0","z0","ax","ay","az","vx0","vz0","vy0","pfxX","pfxZ",
|
| 89 |
"IVB","HB","hit_speed","hit_angle","hit_distance","inPlay",
|
| 90 |
-
"lastPitch","spinDirection","phand")
|
| 91 |
tmilb <- is_barrel(tmilb)
|
| 92 |
tmilb <- tmilb %>%
|
| 93 |
mutate(is_strike_swinging = ifelse(description == "Swinging Strike" |
|
|
@@ -106,26 +106,28 @@ break_plot <- function(game){
|
|
| 106 |
title = "Pitch Movement") +
|
| 107 |
xlim(-25, 25) +
|
| 108 |
ylim(-25, 25) +
|
| 109 |
-
# scale_x_continuous(breaks = seq(-20, 20, by = 20)) +
|
| 110 |
-
# scale_y_continuous(breaks = seq(-20, 20, by = 20)) +
|
| 111 |
theme_minimal() +
|
| 112 |
theme(
|
| 113 |
legend.position = "bottom",
|
| 114 |
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 115 |
panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1),
|
| 116 |
-
aspect.ratio = 1 # This ensures the plot is square
|
| 117 |
) +
|
| 118 |
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 119 |
}
|
| 120 |
|
| 121 |
-
pitch_plot <- function(game){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
ggplot(game, aes(x = px, y = pz, color = pitch_name)) +
|
| 123 |
geom_point(size = 3.5) +
|
| 124 |
geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
|
| 125 |
geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
|
| 126 |
geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
|
| 127 |
geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
|
| 128 |
-
labs(x = NULL, y = NULL, title =
|
| 129 |
xlim(-3, 3) +
|
| 130 |
ylim(0.2, 4) +
|
| 131 |
coord_fixed(ratio = 1) +
|
|
@@ -139,12 +141,6 @@ pitch_plot <- function(game){
|
|
| 139 |
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 140 |
}
|
| 141 |
|
| 142 |
-
# Load models
|
| 143 |
-
# FB <- xgb.load('FB.model')
|
| 144 |
-
# Off <- xgb.load('Off.model')
|
| 145 |
-
# Break <- xgb.load('Break.model')
|
| 146 |
-
model <- xgb.load('TimStuff2.model')
|
| 147 |
-
|
| 148 |
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
|
| 149 |
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
|
| 150 |
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
|
|
@@ -169,8 +165,82 @@ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
|
|
| 169 |
return(result)
|
| 170 |
}
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
calculate_timstuff <- function(game) {
|
| 173 |
-
# game <- calculate_primary(game)
|
| 174 |
game <- game %>%
|
| 175 |
mutate(VAA = calculate_VAA(vz0, ay, az, vy0, y0),
|
| 176 |
EAA = calculate_EAA(extension),
|
|
@@ -180,7 +250,6 @@ calculate_timstuff <- function(game) {
|
|
| 180 |
is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
|
| 181 |
Pitch = pitch_name,
|
| 182 |
ishandL = ifelse(phand == "L",1,0))
|
| 183 |
-
# game <- calculate_primary(game)
|
| 184 |
|
| 185 |
feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff")
|
| 186 |
complete_rows <- complete.cases(game[, feature_vars])
|
|
@@ -190,10 +259,6 @@ calculate_timstuff <- function(game) {
|
|
| 190 |
|
| 191 |
rhp <- game_complete
|
| 192 |
|
| 193 |
-
# rhp <- game_complete[game_complete$ishandL == 0]
|
| 194 |
-
#
|
| 195 |
-
# lhp$TimStuff <- scale_TimStuff(predict(model, as.matrix(cbind(lhp$ishandL,lhp$start_speed, lhp$IVB, lhp$HB, lhp$EAA, lhp$x0, lhp$z0, lhp$spin_rate, lhp$SADiff,lhp$primary_speed,lhp$primary_IVB,lhp$primary_HB))), -0.00249975, 0.007566558)
|
| 196 |
-
|
| 197 |
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.002549184, 0.006202895)
|
| 198 |
|
| 199 |
game_complete <- rbind(rhp,game_na)
|
|
@@ -202,7 +267,6 @@ calculate_timstuff <- function(game) {
|
|
| 202 |
|
| 203 |
summary_table <- function(game) {
|
| 204 |
rows <- nrow(game)
|
| 205 |
-
#game <- calculate_primary(game)
|
| 206 |
game <- calculate_timstuff(game)
|
| 207 |
sumtable <- game %>%
|
| 208 |
mutate(team_fielding_id = ifelse(description == "Called Strike" |
|
|
@@ -260,7 +324,6 @@ summary_table <- function(game) {
|
|
| 260 |
|
| 261 |
return(result)
|
| 262 |
}
|
| 263 |
-
|
| 264 |
# Initialize schedule data
|
| 265 |
mlbid <- mlb_schedule(season = 2024, level_ids = "1")
|
| 266 |
mlbteamH <- mlbid %>%
|
|
@@ -325,19 +388,20 @@ ui <- fluidPage(
|
|
| 325 |
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
|
| 326 |
class = "btn-primary btn-block"),
|
| 327 |
selectizeInput("pitcher", "Pitcher Name:", NULL),
|
| 328 |
-
#textInput("title", "Card Title"),
|
| 329 |
actionButton("update1", "Make Card", icon("plus"),
|
| 330 |
class = "btn-success btn-block"),
|
| 331 |
downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
|
| 332 |
),
|
| 333 |
mainPanel(
|
| 334 |
-
div(style = "width: 1200px; height: 800px; overflow: auto;",
|
| 335 |
plotOutput("combinedPlot", width = "100%", height = "100%")
|
|
|
|
|
|
|
| 336 |
)
|
| 337 |
)
|
| 338 |
)
|
| 339 |
-
)
|
| 340 |
|
|
|
|
| 341 |
server <- function(input, output, session) {
|
| 342 |
observeEvent(input$level, {
|
| 343 |
if(input$level == "AAA"){
|
|
@@ -359,6 +423,7 @@ server <- function(input, output, session) {
|
|
| 359 |
})
|
| 360 |
|
| 361 |
game_data <- reactiveVal()
|
|
|
|
| 362 |
|
| 363 |
observeEvent(input$update, {
|
| 364 |
tryCatch({
|
|
@@ -410,14 +475,18 @@ server <- function(input, output, session) {
|
|
| 410 |
req(input$update1, game_data())
|
| 411 |
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
|
| 412 |
game <- calculate_timstuff(game)
|
| 413 |
-
game %>%
|
| 414 |
arrange(date) %>%
|
| 415 |
group_by(pitch_name) %>%
|
| 416 |
-
mutate(
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
})
|
| 420 |
-
|
| 421 |
combinedPlot <- reactiveVal()
|
| 422 |
observeEvent(input$update1, {
|
| 423 |
req(game_data())
|
|
@@ -432,8 +501,19 @@ server <- function(input, output, session) {
|
|
| 432 |
break_plot <- break_plot(game) +
|
| 433 |
theme(legend.position = "none")
|
| 434 |
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
# Create a formatted table
|
| 439 |
table_data <- summary_table(game)
|
|
@@ -474,22 +554,35 @@ server <- function(input, output, session) {
|
|
| 474 |
img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
|
| 475 |
}
|
| 476 |
|
|
|
|
|
|
|
| 477 |
# Create the rolling TimStuff+ graph
|
| 478 |
rolling_data <- rolling_timstuff()
|
| 479 |
timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
|
| 480 |
-
geom_line(size = 1) +
|
| 481 |
-
geom_point(size = 1) +
|
| 482 |
theme_minimal() +
|
| 483 |
-
|
|
|
|
| 484 |
x = "Pitch Number", y = "TimStuff+") +
|
| 485 |
-
|
| 486 |
-
scale_x_continuous(breaks = seq(5, 55, by = 5), limits = c(5,NA)) +
|
| 487 |
theme(
|
| 488 |
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 489 |
legend.position = "none",
|
| 490 |
panel.grid.major.x = element_line(color = "gray", size = 0.5)
|
| 491 |
)
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
# Create title and data source text
|
| 494 |
title_text <- textGrob(
|
| 495 |
paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
|
|
@@ -504,19 +597,23 @@ server <- function(input, output, session) {
|
|
| 504 |
)
|
| 505 |
|
| 506 |
# Create a horizontal legend
|
| 507 |
-
legend <-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
# Combine all plots
|
| 519 |
combined <- grid.arrange(
|
|
|
|
| 520 |
arrangeGrob(
|
| 521 |
arrangeGrob(
|
| 522 |
img_grob,
|
|
@@ -524,25 +621,43 @@ server <- function(input, output, session) {
|
|
| 524 |
ncol = 1,
|
| 525 |
heights = c(4, 1)
|
| 526 |
),
|
| 527 |
-
|
| 528 |
ncol = 2,
|
| 529 |
widths = c(1, 1)
|
| 530 |
),
|
|
|
|
|
|
|
| 531 |
arrangeGrob(
|
|
|
|
|
|
|
| 532 |
break_plot,
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
widths = c(1, 1)
|
| 536 |
),
|
| 537 |
-
|
|
|
|
| 538 |
legend,
|
| 539 |
-
|
| 540 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
table_plot,
|
| 542 |
-
|
|
|
|
| 543 |
data_source_text,
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
| 546 |
)
|
| 547 |
combinedPlot(combined)
|
| 548 |
|
|
@@ -558,7 +673,17 @@ server <- function(input, output, session) {
|
|
| 558 |
ggsave(file, plot = combinedPlot(), width = 18, height = 12, dpi = 300, units = "in")
|
| 559 |
}
|
| 560 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
})
|
| 562 |
})
|
| 563 |
}
|
| 564 |
-
|
|
|
|
|
|
|
|
|
| 81 |
pitchData.coordinates.vY0,pitchData.coordinates.pfxX,pitchData.coordinates.pfxZ,
|
| 82 |
pitchData.breaks.breakVerticalInduced,pitchData.breaks.breakHorizontal,
|
| 83 |
hitData.launchSpeed,hitData.launchAngle,hitData.totalDistance,details.isInPlay,
|
| 84 |
+
last.pitch.of.ab,pitchData.breaks.spinDirection,matchup.pitchHand.code,matchup.batSide.code,game_pk)
|
| 85 |
colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID",
|
| 86 |
"result","description","pitch_name","des","start_speed",
|
| 87 |
"plateTime","zone","spin_rate","extension","px","pz","x0",
|
| 88 |
"y0","z0","ax","ay","az","vx0","vz0","vy0","pfxX","pfxZ",
|
| 89 |
"IVB","HB","hit_speed","hit_angle","hit_distance","inPlay",
|
| 90 |
+
"lastPitch","spinDirection","phand","bhand","gamepk")
|
| 91 |
tmilb <- is_barrel(tmilb)
|
| 92 |
tmilb <- tmilb %>%
|
| 93 |
mutate(is_strike_swinging = ifelse(description == "Swinging Strike" |
|
|
|
|
| 106 |
title = "Pitch Movement") +
|
| 107 |
xlim(-25, 25) +
|
| 108 |
ylim(-25, 25) +
|
|
|
|
|
|
|
| 109 |
theme_minimal() +
|
| 110 |
theme(
|
| 111 |
legend.position = "bottom",
|
| 112 |
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 113 |
panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1),
|
| 114 |
+
aspect.ratio = 1, # This ensures the plot is square
|
| 115 |
) +
|
| 116 |
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 117 |
}
|
| 118 |
|
| 119 |
+
pitch_plot <- function(game, title, bhand = NULL) {
|
| 120 |
+
# if (!is.null(bhand)) {
|
| 121 |
+
# game <- game %>% filter(bhand == bhand)
|
| 122 |
+
# }
|
| 123 |
+
#
|
| 124 |
ggplot(game, aes(x = px, y = pz, color = pitch_name)) +
|
| 125 |
geom_point(size = 3.5) +
|
| 126 |
geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
|
| 127 |
geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
|
| 128 |
geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
|
| 129 |
geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
|
| 130 |
+
labs(x = NULL, y = NULL, title = title) +
|
| 131 |
xlim(-3, 3) +
|
| 132 |
ylim(0.2, 4) +
|
| 133 |
coord_fixed(ratio = 1) +
|
|
|
|
| 141 |
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 142 |
}
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
|
| 145 |
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
|
| 146 |
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
|
|
|
|
| 165 |
return(result)
|
| 166 |
}
|
| 167 |
|
| 168 |
+
# Modify the getBoxScore function
|
| 169 |
+
|
| 170 |
+
getBoxScore <- function(game_pk, pid) {
|
| 171 |
+
url <- paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore")
|
| 172 |
+
bs <- fromJSON(url)
|
| 173 |
+
|
| 174 |
+
t <- as.data.frame(bs[["teams"]][["away"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
|
| 175 |
+
if (nrow(t) == 0) {
|
| 176 |
+
t <- as.data.frame(bs[["teams"]][["home"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
# Define the stats we want and their display names
|
| 180 |
+
stats <- c("inningsPitched", "battersFaced", "runs", "earnedRuns", "hits", "baseOnBalls", "strikeOuts", "strikePercentage")
|
| 181 |
+
display_names <- c("IP", "TBF", "R", "ER", "H", "BB", "K", "Strike%")
|
| 182 |
+
|
| 183 |
+
# Create a new data frame with only the stats we want
|
| 184 |
+
result <- data.frame(matrix(ncol = length(stats), nrow = 1))
|
| 185 |
+
colnames(result) <- display_names
|
| 186 |
+
|
| 187 |
+
for (i in 1:length(stats)) {
|
| 188 |
+
value <- t[[stats[i]]]
|
| 189 |
+
if (is.null(value)) value <- "0"
|
| 190 |
+
if (stats[i] == "strikePercentage") {
|
| 191 |
+
value <- paste0(round(as.numeric(value) * 100, 1), "%")
|
| 192 |
+
}
|
| 193 |
+
result[1, i] <- as.character(value)
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
return(result)
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
# Create a new function for split pitch location plots
|
| 200 |
+
pitch_plot_split <- function(game, title) {
|
| 201 |
+
game_lhb <- game %>% filter(bhand == "L")
|
| 202 |
+
game_rhb <- game %>% filter(bhand == "R")
|
| 203 |
+
|
| 204 |
+
plot_lhb <- ggplot(game_lhb, aes(x = px, y = pz, color = pitch_name)) +
|
| 205 |
+
geom_point(size = 2) +
|
| 206 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
|
| 207 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
|
| 208 |
+
geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
|
| 209 |
+
geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
|
| 210 |
+
labs(x = NULL, y = NULL, title = "LHB (Batter's Perspective)") +
|
| 211 |
+
xlim(-3, 3) +
|
| 212 |
+
ylim(0.2, 4) +
|
| 213 |
+
coord_fixed(ratio = 1) +
|
| 214 |
+
theme_minimal() +
|
| 215 |
+
theme(
|
| 216 |
+
legend.position = "none",
|
| 217 |
+
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 218 |
+
axis.text = element_blank(),
|
| 219 |
+
axis.ticks = element_blank()
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
plot_rhb <- ggplot(game_rhb, aes(x = px, y = pz, color = pitch_name)) +
|
| 223 |
+
geom_point(size = 2) +
|
| 224 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
|
| 225 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
|
| 226 |
+
geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
|
| 227 |
+
geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
|
| 228 |
+
labs(x = NULL, y = NULL, title = "RHB (Batter's Perspective)") +
|
| 229 |
+
xlim(-3, 3) +
|
| 230 |
+
ylim(0.2, 4) +
|
| 231 |
+
coord_fixed(ratio = 1) +
|
| 232 |
+
theme_minimal() +
|
| 233 |
+
theme(
|
| 234 |
+
legend.position = "none",
|
| 235 |
+
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 236 |
+
axis.text = element_blank(),
|
| 237 |
+
axis.ticks = element_blank()
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
return(list(lhb = plot_lhb, rhb = plot_rhb))
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
calculate_timstuff <- function(game) {
|
|
|
|
| 244 |
game <- game %>%
|
| 245 |
mutate(VAA = calculate_VAA(vz0, ay, az, vy0, y0),
|
| 246 |
EAA = calculate_EAA(extension),
|
|
|
|
| 250 |
is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
|
| 251 |
Pitch = pitch_name,
|
| 252 |
ishandL = ifelse(phand == "L",1,0))
|
|
|
|
| 253 |
|
| 254 |
feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff")
|
| 255 |
complete_rows <- complete.cases(game[, feature_vars])
|
|
|
|
| 259 |
|
| 260 |
rhp <- game_complete
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
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.002549184, 0.006202895)
|
| 263 |
|
| 264 |
game_complete <- rbind(rhp,game_na)
|
|
|
|
| 267 |
|
| 268 |
summary_table <- function(game) {
|
| 269 |
rows <- nrow(game)
|
|
|
|
| 270 |
game <- calculate_timstuff(game)
|
| 271 |
sumtable <- game %>%
|
| 272 |
mutate(team_fielding_id = ifelse(description == "Called Strike" |
|
|
|
|
| 324 |
|
| 325 |
return(result)
|
| 326 |
}
|
|
|
|
| 327 |
# Initialize schedule data
|
| 328 |
mlbid <- mlb_schedule(season = 2024, level_ids = "1")
|
| 329 |
mlbteamH <- mlbid %>%
|
|
|
|
| 388 |
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
|
| 389 |
class = "btn-primary btn-block"),
|
| 390 |
selectizeInput("pitcher", "Pitcher Name:", NULL),
|
|
|
|
| 391 |
actionButton("update1", "Make Card", icon("plus"),
|
| 392 |
class = "btn-success btn-block"),
|
| 393 |
downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
|
| 394 |
),
|
| 395 |
mainPanel(
|
| 396 |
+
div(style = "width: 1200px; height: 800px; overflow: auto;",
|
| 397 |
plotOutput("combinedPlot", width = "100%", height = "100%")
|
| 398 |
+
),
|
| 399 |
+
tableOutput("boxscoreTable") # Add this line for the boxscore
|
| 400 |
)
|
| 401 |
)
|
| 402 |
)
|
|
|
|
| 403 |
|
| 404 |
+
# Server Definition
|
| 405 |
server <- function(input, output, session) {
|
| 406 |
observeEvent(input$level, {
|
| 407 |
if(input$level == "AAA"){
|
|
|
|
| 423 |
})
|
| 424 |
|
| 425 |
game_data <- reactiveVal()
|
| 426 |
+
boxscore_data <- reactiveVal()
|
| 427 |
|
| 428 |
observeEvent(input$update, {
|
| 429 |
tryCatch({
|
|
|
|
| 475 |
req(input$update1, game_data())
|
| 476 |
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
|
| 477 |
game <- calculate_timstuff(game)
|
| 478 |
+
result <- game %>%
|
| 479 |
arrange(date) %>%
|
| 480 |
group_by(pitch_name) %>%
|
| 481 |
+
mutate(
|
| 482 |
+
TimStuff = as.numeric(TimStuff),
|
| 483 |
+
rolling_timstuff = if(n() >= 5) rollmean(TimStuff, k = 5, fill = NA, align = "right") else TimStuff,
|
| 484 |
+
pitch_number = row_number()
|
| 485 |
+
) %>%
|
| 486 |
+
ungroup()
|
| 487 |
+
|
| 488 |
+
return(result)
|
| 489 |
})
|
|
|
|
| 490 |
combinedPlot <- reactiveVal()
|
| 491 |
observeEvent(input$update1, {
|
| 492 |
req(game_data())
|
|
|
|
| 501 |
break_plot <- break_plot(game) +
|
| 502 |
theme(legend.position = "none")
|
| 503 |
|
| 504 |
+
# Get boxscore data
|
| 505 |
+
boxscore_data <- getBoxScore(unique(game$gamepk[1]), unique(game$`Pitcher ID`[1]))
|
| 506 |
+
boxscore_table <- tableGrob(boxscore_data, rows = NULL, theme = ttheme_minimal(
|
| 507 |
+
core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
|
| 508 |
+
colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0")),
|
| 509 |
+
rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
|
| 510 |
+
))
|
| 511 |
+
|
| 512 |
+
# Set a fixed total height for the boxscore table
|
| 513 |
+
boxscore_table$heights <- unit(rep(1/(nrow(boxscore_data) + 1), nrow(boxscore_data) + 1), "npc")
|
| 514 |
+
|
| 515 |
+
# Get split pitch location plots
|
| 516 |
+
pitch_plots <- pitch_plot_split(game)
|
| 517 |
|
| 518 |
# Create a formatted table
|
| 519 |
table_data <- summary_table(game)
|
|
|
|
| 554 |
img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
|
| 555 |
}
|
| 556 |
|
| 557 |
+
# Create the rolling TimStuff+ graph
|
| 558 |
+
# Create the rolling TimStuff+ graph
|
| 559 |
# Create the rolling TimStuff+ graph
|
| 560 |
rolling_data <- rolling_timstuff()
|
| 561 |
timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
|
| 562 |
+
geom_line(size = 1, na.rm = TRUE) +
|
| 563 |
+
geom_point(size = 1, na.rm = TRUE) +
|
| 564 |
theme_minimal() +
|
| 565 |
+
### Workaround to not show anything but keep graph
|
| 566 |
+
labs(title = if(any(rolling_data$pitch_number >= 5)) "5-Pitch Rolling TimStuff+" else "5-Pitch Rolling TimStuff+",
|
| 567 |
x = "Pitch Number", y = "TimStuff+") +
|
| 568 |
+
scale_y_continuous(limits = c(70, 130), na.value = NA) +
|
| 569 |
+
scale_x_continuous(breaks = seq(5, 55, by = 5), limits = c(5, NA)) +
|
| 570 |
theme(
|
| 571 |
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 572 |
legend.position = "none",
|
| 573 |
panel.grid.major.x = element_line(color = "gray", size = 0.5)
|
| 574 |
)
|
| 575 |
|
| 576 |
+
# If there's no data at all, display a message
|
| 577 |
+
if(nrow(rolling_data) == 0) {
|
| 578 |
+
timstuff_plot <- ggplot() +
|
| 579 |
+
theme_void() +
|
| 580 |
+
labs(title = "No pitch data available") +
|
| 581 |
+
theme(
|
| 582 |
+
plot.title = element_text(hjust = 0.5, face = "bold")
|
| 583 |
+
)
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
# Create title and data source text
|
| 587 |
title_text <- textGrob(
|
| 588 |
paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
|
|
|
|
| 597 |
)
|
| 598 |
|
| 599 |
# Create a horizontal legend
|
| 600 |
+
legend <- if(nrow(rolling_data) > 0) {
|
| 601 |
+
get_legend(
|
| 602 |
+
ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
|
| 603 |
+
geom_point(size = 5) +
|
| 604 |
+
theme(legend.position = "bottom",
|
| 605 |
+
legend.title = element_blank(),
|
| 606 |
+
text = element_text(size = 12.5),
|
| 607 |
+
legend.box = "horizontal" ) +
|
| 608 |
+
guides(color = guide_legend(nrow = 1))
|
| 609 |
+
)
|
| 610 |
+
} else {
|
| 611 |
+
ggplotGrob(ggplot() + theme_void()) # Empty legend if no data
|
| 612 |
+
}
|
| 613 |
|
| 614 |
# Combine all plots
|
| 615 |
combined <- grid.arrange(
|
| 616 |
+
# Row 1: Picture and Rolling TimStuff+ graph
|
| 617 |
arrangeGrob(
|
| 618 |
arrangeGrob(
|
| 619 |
img_grob,
|
|
|
|
| 621 |
ncol = 1,
|
| 622 |
heights = c(4, 1)
|
| 623 |
),
|
| 624 |
+
timstuff_plot,
|
| 625 |
ncol = 2,
|
| 626 |
widths = c(1, 1)
|
| 627 |
),
|
| 628 |
+
|
| 629 |
+
# Row 2: Two location graphs and movement graph
|
| 630 |
arrangeGrob(
|
| 631 |
+
pitch_plots$lhb,
|
| 632 |
+
pitch_plots$rhb,
|
| 633 |
break_plot,
|
| 634 |
+
ncol = 3,
|
| 635 |
+
widths = c(1, 1, 1.2)
|
|
|
|
| 636 |
),
|
| 637 |
+
|
| 638 |
+
# Row 3: Legend
|
| 639 |
legend,
|
| 640 |
+
|
| 641 |
+
# Row 4: Horizontal boxscore table
|
| 642 |
+
tableGrob(
|
| 643 |
+
boxscore_data,
|
| 644 |
+
rows = NULL,
|
| 645 |
+
cols = colnames(boxscore_data),
|
| 646 |
+
theme = ttheme_minimal(
|
| 647 |
+
core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
|
| 648 |
+
colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0"))
|
| 649 |
+
)
|
| 650 |
+
),
|
| 651 |
+
|
| 652 |
+
# Row 5: Pitch summary table
|
| 653 |
table_plot,
|
| 654 |
+
|
| 655 |
+
# Row 6: Data source text
|
| 656 |
data_source_text,
|
| 657 |
+
|
| 658 |
+
# Layout parameters
|
| 659 |
+
nrow = 6,
|
| 660 |
+
heights = c(1.2, 1.5, 0.1, 0.3, 1, 0.05)
|
| 661 |
)
|
| 662 |
combinedPlot(combined)
|
| 663 |
|
|
|
|
| 673 |
ggsave(file, plot = combinedPlot(), width = 18, height = 12, dpi = 300, units = "in")
|
| 674 |
}
|
| 675 |
)
|
| 676 |
+
|
| 677 |
+
# Render the boxscore table
|
| 678 |
+
output$boxscoreTable <- renderTable({
|
| 679 |
+
boxscore_data()
|
| 680 |
+
})
|
| 681 |
+
|
| 682 |
+
}, error = function(e) {
|
| 683 |
+
showNotification(paste("Error creating plot:", e$message), type = "error")
|
| 684 |
})
|
| 685 |
})
|
| 686 |
}
|
| 687 |
+
|
| 688 |
+
# Run the application
|
| 689 |
+
shinyApp(ui = ui, server = server)
|