TimStats commited on
Commit
f676424
·
verified ·
1 Parent(s): 39af7a9

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +182 -57
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 = "Pitch Location") +
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;", # Fixed size container
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(rolling_timstuff = rollmean(TimStuff, k = 5, fill = NA, align = "right"),
417
- pitch_number = row_number()) %>%
418
- ungroup() # Make sure to ungroup after the grouping operations
 
 
 
 
 
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
- pitch_plot <- pitch_plot(game) +
436
- theme(legend.position = "none")
 
 
 
 
 
 
 
 
 
 
 
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
- labs(title = "5-Pitch Rolling TimStuff+",
 
484
  x = "Pitch Number", y = "TimStuff+") +
485
- ylim(70, 130) +
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 <- get_legend(
508
- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
509
- geom_point(size = 5) +
510
- theme(legend.position = "bottom",
511
- legend.title = element_blank(),
512
- text = element_text(size = 12.5),
513
- #legend.key.size = unit(.5,"cm"),
514
- legend.box = "horizontal" ) +
515
- guides(color = guide_legend(nrow = 1))
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
- pitch_plot,
528
  ncol = 2,
529
  widths = c(1, 1)
530
  ),
 
 
531
  arrangeGrob(
 
 
532
  break_plot,
533
- timstuff_plot,
534
- ncol = 2,
535
- widths = c(1, 1)
536
  ),
537
- #arrangeGrob(
 
538
  legend,
539
- #),
540
- #arrangeGrob(
 
 
 
 
 
 
 
 
 
 
 
541
  table_plot,
542
- #),
 
543
  data_source_text,
544
- nrow = 5,
545
- heights = c(1.2, 1.6, 0.1, 1.1, 0.05) # Adjusted these values
 
 
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
- shinyApp(ui, server)
 
 
 
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)