TimStats commited on
Commit
36764be
·
verified ·
1 Parent(s): 33fc16a

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +207 -153
app.R CHANGED
@@ -1,4 +1,3 @@
1
- # Load required libraries
2
  library(shiny)
3
  library(plotly)
4
  library(gridlayout)
@@ -21,6 +20,7 @@ library(png)
21
  library(xgboost)
22
  library(httr)
23
  library(jpeg)
 
24
 
25
  pdf(file = NULL)
26
  Sys.setenv(TZ='EST')
@@ -98,48 +98,53 @@ pitcher_summary <- function(game_pk,date){
98
  }
99
 
100
  break_plot <- function(game){
101
- ggplot()+
102
- geom_point(game,mapping =
103
- aes(x=HB, y = IVB,color = pitch_name),size = 4) +
104
- geom_vline(xintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
105
- geom_hline(yintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
106
- xlab("Horizontal Break from Pitcher's Perspective (in) ")+
107
- ylab("Induced Verical Break (in)") +
108
- xlim(-25,25) +
109
- ylim(-25,25) +
110
- ggthemes::theme_igray()+
111
- theme(panel.grid.minor = element_line(color = "gray",
112
- size = 0.25,
113
- linetype = 1))+
114
- guides(color = guide_legend(title = "Pitch Type"))
 
 
 
 
115
  }
116
 
117
  pitch_plot <- function(game){
118
- ggplot()+
119
- geom_point(game,mapping =
120
- aes(x=px, y = pz,color = pitch_name),size = 3.5) +
121
- geom_segment(aes(x=-0.71,xend = 0.71, y = 1.5,yend = 1.5))+
122
- geom_segment(aes(x=-0.71,xend = 0.71, y = 3.6,yend = 3.6))+
123
- geom_segment(aes(x= 0.71,xend = 0.71, y = 1.5,yend = 3.6))+
124
- geom_segment(aes(x=-0.71,xend = -0.71, y = 1.5,yend =3.6))+
125
- xlab(" ")+
126
- ylab(" ") +
127
- xlim(-3,3)+
128
- ylim(0.2,4)+
129
  coord_fixed(ratio = 1) +
130
- ggthemes::theme_few()+
131
- guides(color = guide_legend(title = "Pitch Type"))+
132
- theme(axis.text.x=element_blank(),
133
- axis.ticks.x=element_blank(),
134
- axis.text.y=element_blank(),
135
- axis.ticks.y=element_blank())
 
 
136
  }
137
 
138
  # Load models
139
- FB <- xgb.load('FB.model')
140
- Off <- xgb.load('Off.model')
141
- Break <- xgb.load('Break.model')
142
  model <- xgb.load('TimStuff2.model')
 
143
  calculate_VAA <- function(vz0, ay, az, vy0, y0) {
144
  -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
145
  ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
@@ -163,32 +168,24 @@ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
163
  result <- 100 - (scaled_score * 10)
164
  return(result)
165
  }
 
166
  calculate_primary <- function(data){
167
  data <- data %>%
168
- # Group by pitch_name, Pitcher Name, Pitcher Id, and date
169
  group_by(pitch_name, `Pitcher Name`, `Pitcher ID`, date) %>%
170
-
171
- # Count occurrences and calculate average start_speed, IVB, and HB for each group
172
  mutate(
173
  pitch_count = n(),
174
  avg_start_speed = mean(start_speed, na.rm = TRUE),
175
  avg_IVB = mean(IVB, na.rm = TRUE),
176
  avg_HB = mean(HB, na.rm = TRUE)
177
  ) %>%
178
-
179
  ungroup() %>%
180
- # Group by Pitcher Name, Pitcher Id, and date
181
  group_by(`Pitcher Name`, `Pitcher ID`, date) %>%
182
-
183
- # Add a column to identify the highest occurrence
184
  mutate(
185
  is_highest_occurrence = case_when(
186
  pitch_count == max(pitch_count) ~ 1,
187
  TRUE ~ 0
188
  )
189
  ) %>%
190
-
191
- # If there's a tie, use avg_start_speed as a tiebreaker
192
  mutate(
193
  is_highest_occurrence = case_when(
194
  is_highest_occurrence == 1 & pitch_count == max(pitch_count[is_highest_occurrence == 1]) &
@@ -196,15 +193,11 @@ calculate_primary <- function(data){
196
  TRUE ~ 0
197
  )
198
  ) %>%
199
-
200
- # Calculate primary pitch metrics
201
  mutate(
202
  primary_speed = avg_start_speed[is_highest_occurrence == 1][1],
203
  primary_IVB = avg_IVB[is_highest_occurrence == 1][1],
204
  primary_HB = avg_HB[is_highest_occurrence == 1][1]
205
  ) %>%
206
-
207
- # Ungroup to remove grouping structure
208
  ungroup()
209
  }
210
 
@@ -219,7 +212,6 @@ calculate_timstuff <- function(game) {
219
  is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
220
  Pitch = pitch_name,
221
  ishandL = ifelse(phand == "L",1,0))
222
- # game <- calculate_primary(game)
223
 
224
  feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff","primary_speed","primary_IVB","primary_HB")
225
  complete_rows <- complete.cases(game[, feature_vars])
@@ -229,15 +221,12 @@ calculate_timstuff <- function(game) {
229
 
230
  rhp <- game_complete
231
 
232
- # rhp <- game_complete[game_complete$ishandL == 0]
233
- #
234
- # 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)
235
-
236
  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,rhp$primary_speed,rhp$primary_IVB,rhp$primary_HB))), -0.00249975, 0.007566558)
237
 
238
  game_complete <- rbind(rhp,game_na)
239
  return(game_complete)
240
  }
 
241
  summary_table <- function(game) {
242
  rows <- nrow(game)
243
  game <- calculate_primary(game)
@@ -273,7 +262,30 @@ summary_table <- function(game) {
273
  ) %>%
274
  arrange(-Pitches)
275
 
276
- return(sumtable)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  }
278
 
279
  # Initialize schedule data
@@ -322,59 +334,57 @@ sbteamA <- distinct(sbteamA)
322
 
323
  # UI Definition
324
  ui <- fluidPage(
325
- tags$head(
326
- tags$style(HTML("
327
- @media (max-width: 768px) {
328
- .sidebar { width: 100%; float: none; }
329
- .main-content { margin-left: 0; }
330
- .selectize-input { font-size: 14px; }
331
- .form-group { margin-bottom: 10px; }
332
- .action-button { width: 100%; }
333
- }
334
- "))
335
- ),
336
  titlePanel("2024 MLB/AAA/FSL Summary Cards"),
337
  sidebarLayout(
338
  sidebarPanel(
339
- width = 2,
340
- dateInput("date", "Date:"),
341
- selectizeInput("level", "Level:", c("MLB", "AAA", "FSL", "College (Statcast Parks Only)")),
342
- selectizeInput("homeT", "Home Team:", mlbteamH[,1]),
343
- selectizeInput("awayT", "Away Team:", mlbteamA[,1]),
344
- selectizeInput("gamenum", "Game Number (For Doubleheaders):", c("1", "2")),
 
 
 
 
 
345
  actionButton("update", "Find Pitcher", icon("magnifying-glass"),
346
- style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
347
- selectizeInput("pitcher", "Pitcher Name:", c(" ")),
348
  textInput("title", "Card Title"),
349
  actionButton("update1", "Make Card", icon("plus"),
350
- style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
351
- downloadButton("downloadPlot", "Download Card")
352
  ),
353
  mainPanel(
354
- plotOutput("combinedPlot", height = "900px",width = "1350px")
355
  )
356
  )
357
  )
 
358
  server <- function(input, output, session) {
359
  observeEvent(input$level, {
360
  if(input$level == "AAA"){
361
- updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = aaateamH[,1])
362
- updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = aaateamA[,1])
363
  }
364
  if(input$level == "FSL"){
365
- updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = fslteamH[,1])
366
- updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = fslteamA[,1])
367
  }
368
  if(input$level == "MLB"){
369
- updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = mlbteamH[,1])
370
- updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = mlbteamA[,1])
371
  }
372
  if(input$level == "College (Statcast Parks Only)"){
373
- updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = sbteamH[,1])
374
- updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = sbteamA[,1])
375
  }
376
  })
377
 
 
 
378
  observeEvent(input$update, {
379
  tryCatch({
380
  if(input$level == "AAA"){
@@ -383,7 +393,7 @@ server <- function(input, output, session) {
383
  filter(teams_home_team_name == input$homeT) %>%
384
  filter(teams_away_team_name == input$awayT) %>%
385
  filter(game_number == input$gamenum)
386
- pname <- pitcher_summary(pname[,6],input$date)
387
  }
388
  if(input$level == "FSL"){
389
  pname <- fslid %>%
@@ -391,7 +401,7 @@ server <- function(input, output, session) {
391
  filter(teams_home_team_name == input$homeT) %>%
392
  filter(teams_away_team_name == input$awayT) %>%
393
  filter(game_number == input$gamenum)
394
- pname <- pitcher_summary(pname[,6],input$date)
395
  }
396
  if(input$level == "MLB"){
397
  pname <- mlbid %>%
@@ -399,7 +409,7 @@ server <- function(input, output, session) {
399
  filter(teams_home_team_name == input$homeT) %>%
400
  filter(teams_away_team_name == input$awayT) %>%
401
  filter(game_number == input$gamenum)
402
- pname <- pitcher_summary(pname[,6],input$date)
403
  }
404
  if(input$level == "College (Statcast Parks Only)"){
405
  pname <- sbid %>%
@@ -407,92 +417,73 @@ server <- function(input, output, session) {
407
  filter(teams_home_team_name == input$homeT) %>%
408
  filter(teams_away_team_name == input$awayT) %>%
409
  filter(game_number == input$gamenum)
410
- pname <- pitcher_summary(pname[,6],input$date)
411
  }
412
 
413
  if(nrow(pname) == 0) {
414
  showNotification("No pitchers found for the selected game.", type = "warning")
415
  } else {
416
- updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
 
417
  }
418
  }, error = function(e) {
419
  showNotification(paste("Error finding pitchers:", e$message), type = "error")
420
  })
421
  })
422
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  combinedPlot <- reactiveVal()
424
  observeEvent(input$update1, {
 
425
  tryCatch({
426
- if(input$level == "MLB"){
427
- id <- mlbid %>%
428
- filter(date == as.character.Date(input$date)) %>%
429
- filter(teams_home_team_name == input$homeT) %>%
430
- filter(teams_away_team_name == input$awayT) %>%
431
- filter(game_number == input$gamenum)
432
- game <- pitcher_summary(id[,6],input$date)
433
- game <- game %>%
434
- filter(`Pitcher Name` == input$pitcher)
435
- id <- as.character(game[1,4])
436
- }
437
- if(input$level == "FSL"){
438
- id <- fslid %>%
439
- filter(date == as.character.Date(input$date)) %>%
440
- filter(teams_home_team_name == input$homeT) %>%
441
- filter(teams_away_team_name == input$awayT) %>%
442
- filter(game_number == input$gamenum)
443
- game <- pitcher_summary(id[,6],input$date)
444
- game <- game %>%
445
- filter(`Pitcher Name` == input$pitcher)
446
- id <- as.character(game[1,4])
447
- }
448
- if(input$level == "AAA"){
449
- id <- aaaid %>%
450
- filter(date == as.character.Date(input$date)) %>%
451
- filter(teams_home_team_name == input$homeT) %>%
452
- filter(teams_away_team_name == input$awayT) %>%
453
- filter(game_number == input$gamenum)
454
- game <- pitcher_summary(id[,6],input$date)
455
- game <- game %>%
456
- filter(`Pitcher Name` == input$pitcher)
457
- id <- as.character(game[1,4])
458
- }
459
- if(input$level == "College (Statcast Parks Only)"){
460
- id <- sbid %>%
461
- filter(date == as.character.Date(input$date)) %>%
462
- filter(teams_home_team_name == input$homeT) %>%
463
- filter(teams_away_team_name == input$awayT) %>%
464
- filter(game_number == input$gamenum)
465
- game <- pitcher_summary(id[,6],input$date)
466
- game <- game %>%
467
- filter(`Pitcher Name` == input$pitcher)
468
- id <- as.character(game[1,4])
469
- }
470
 
471
  if(nrow(game) == 0) {
472
  showNotification("No data available for the selected pitcher.", type = "warning")
473
  return()
474
  }
475
 
476
- break_plot <- break_plot(game)
477
- pitch_plot <- pitch_plot(game)
 
 
 
478
 
479
- table_plot <- tableGrob(summary_table(game), theme = ttheme_default(
480
- core = list(fg_params = list(cex = 1.7)),
481
- colhead = list(fg_params = list(cex = 1.2)),
482
- rowhead = list(fg_params = list(cex = 2.5))))
483
- table_plot$widths[[1]] <- unit(0, "cm")
484
 
485
- text_grobs <- which(sapply(table_plot$grobs, function(g) {
486
- g$name == "core-fg-1" && inherits(g$children[[1]], "text")
487
- }))
 
 
488
 
489
- for (i in text_grobs) {
490
- text <- table_plot$grobs[[i]]$children[[1]]$label
491
- table_plot$grobs[[i]]$children[[1]]$label <- sub("^\\d+\\s*", "", text)
 
 
 
 
 
 
 
 
492
  }
493
 
494
- grid.newpage()
495
- grid.draw(table_plot)
496
 
497
  mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
498
  milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
@@ -508,13 +499,76 @@ server <- function(input, output, session) {
508
  img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
509
  }
510
 
511
- combined <- grid.arrange(
512
- arrangeGrob(table_plot, img_grob, ncol = 2, widths = c(2/3, 1/3)),
513
- arrangeGrob(break_plot, pitch_plot, ncol = 2),
514
- nrow = 2,
515
- top = textGrob(paste(input$title, "Made by @TimStats (Data: MLB)", sep = " "), gp = gpar(fontsize = 20, font = 2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  )
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  combinedPlot(combined)
519
 
520
  output$combinedPlot <- renderPlot({
 
 
1
  library(shiny)
2
  library(plotly)
3
  library(gridlayout)
 
20
  library(xgboost)
21
  library(httr)
22
  library(jpeg)
23
+ library(zoo) # For rolling mean calculation
24
 
25
  pdf(file = NULL)
26
  Sys.setenv(TZ='EST')
 
98
  }
99
 
100
  break_plot <- function(game){
101
+ ggplot(game, aes(x = HB, y = IVB, color = pitch_name)) +
102
+ geom_point(size = 2) +
103
+ geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
104
+ geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
105
+ labs(x = "Horizontal Break (in)", y = "Induced Vertical Break (in)",
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) +
132
+ theme_minimal() +
133
+ theme(
134
+ legend.position = "bottom",
135
+ plot.title = element_text(hjust = 0.5, face = "bold"),
136
+ axis.text = element_blank(),
137
+ axis.ticks = element_blank()
138
+ ) +
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)
 
168
  result <- 100 - (scaled_score * 10)
169
  return(result)
170
  }
171
+
172
  calculate_primary <- function(data){
173
  data <- data %>%
 
174
  group_by(pitch_name, `Pitcher Name`, `Pitcher ID`, date) %>%
 
 
175
  mutate(
176
  pitch_count = n(),
177
  avg_start_speed = mean(start_speed, na.rm = TRUE),
178
  avg_IVB = mean(IVB, na.rm = TRUE),
179
  avg_HB = mean(HB, na.rm = TRUE)
180
  ) %>%
 
181
  ungroup() %>%
 
182
  group_by(`Pitcher Name`, `Pitcher ID`, date) %>%
 
 
183
  mutate(
184
  is_highest_occurrence = case_when(
185
  pitch_count == max(pitch_count) ~ 1,
186
  TRUE ~ 0
187
  )
188
  ) %>%
 
 
189
  mutate(
190
  is_highest_occurrence = case_when(
191
  is_highest_occurrence == 1 & pitch_count == max(pitch_count[is_highest_occurrence == 1]) &
 
193
  TRUE ~ 0
194
  )
195
  ) %>%
 
 
196
  mutate(
197
  primary_speed = avg_start_speed[is_highest_occurrence == 1][1],
198
  primary_IVB = avg_IVB[is_highest_occurrence == 1][1],
199
  primary_HB = avg_HB[is_highest_occurrence == 1][1]
200
  ) %>%
 
 
201
  ungroup()
202
  }
203
 
 
212
  is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
213
  Pitch = pitch_name,
214
  ishandL = ifelse(phand == "L",1,0))
 
215
 
216
  feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff","primary_speed","primary_IVB","primary_HB")
217
  complete_rows <- complete.cases(game[, feature_vars])
 
221
 
222
  rhp <- game_complete
223
 
 
 
 
 
224
  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,rhp$primary_speed,rhp$primary_IVB,rhp$primary_HB))), -0.00249975, 0.007566558)
225
 
226
  game_complete <- rbind(rhp,game_na)
227
  return(game_complete)
228
  }
229
+
230
  summary_table <- function(game) {
231
  rows <- nrow(game)
232
  game <- calculate_primary(game)
 
262
  ) %>%
263
  arrange(-Pitches)
264
 
265
+ result <- sumtable %>%
266
+ select(Pitch, Pitches, `Pitch%`, `Avg. Velo`, `Spin Rate`, Extension,IVB, HB, VAA, `CSW%`, `Whiff%`, `TimStuff+`) %>%
267
+ rename(
268
+ "Type" = Pitch,
269
+ "#" = Pitches,
270
+ "Velo" = `Avg. Velo`,
271
+ "Spin" = `Spin Rate`,
272
+ "Ext" = Extension,
273
+ "Use%" = `Pitch%`
274
+ ) %>%
275
+ mutate(
276
+ "Use%" = paste0(`Use%`, "%"),
277
+ "Spin" = format(round(Spin), big.mark = ","),
278
+ Velo = round(Velo, 1),
279
+ Ext = round(Ext, 1),
280
+ IVB = round(IVB, 1),
281
+ HB = round(HB, 1),
282
+ VAA = round(VAA, 1),
283
+ `CSW%` = paste0(`CSW%`, "%"),
284
+ `Whiff%` = paste0(`Whiff%`, "%")
285
+ ) %>%
286
+ arrange(desc(`#`))
287
+
288
+ return(result)
289
  }
290
 
291
  # Initialize schedule data
 
334
 
335
  # UI Definition
336
  ui <- fluidPage(
337
+ theme = bs_theme(version = 5, bootswatch = "flatly"),
 
 
 
 
 
 
 
 
 
 
338
  titlePanel("2024 MLB/AAA/FSL Summary Cards"),
339
  sidebarLayout(
340
  sidebarPanel(
341
+ width = 3,
342
+ dateInput("date", "Date:", value = Sys.Date()),
343
+ selectizeInput("level", "Level:",
344
+ c("MLB", "AAA", "FSL", "College (Statcast Parks Only)"),
345
+ options = list(
346
+ placeholder = 'Select a level',
347
+ onInitialize = I('function() { this.setValue(""); }')
348
+ )),
349
+ selectizeInput("homeT", "Home Team:", NULL),
350
+ selectizeInput("awayT", "Away Team:", NULL),
351
+ selectizeInput("gamenum", "Game Number:", c("1", "2")),
352
  actionButton("update", "Find Pitcher", icon("magnifying-glass"),
353
+ class = "btn-primary btn-block"),
354
+ selectizeInput("pitcher", "Pitcher Name:", NULL),
355
  textInput("title", "Card Title"),
356
  actionButton("update1", "Make Card", icon("plus"),
357
+ class = "btn-success btn-block"),
358
+ downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
359
  ),
360
  mainPanel(
361
+ plotOutput("combinedPlot", height = "900px", width = "100%")
362
  )
363
  )
364
  )
365
+
366
  server <- function(input, output, session) {
367
  observeEvent(input$level, {
368
  if(input$level == "AAA"){
369
+ updateSelectizeInput(session, "homeT", "Home Team:", choices = aaateamH[,1])
370
+ updateSelectizeInput(session, "awayT", "Away Team:", choices = aaateamA[,1])
371
  }
372
  if(input$level == "FSL"){
373
+ updateSelectizeInput(session, "homeT", "Home Team:", choices = fslteamH[,1])
374
+ updateSelectizeInput(session, "awayT", "Away Team:", choices = fslteamA[,1])
375
  }
376
  if(input$level == "MLB"){
377
+ updateSelectizeInput(session, "homeT", "Home Team:", choices = mlbteamH[,1])
378
+ updateSelectizeInput(session, "awayT", "Away Team:", choices = mlbteamA[,1])
379
  }
380
  if(input$level == "College (Statcast Parks Only)"){
381
+ updateSelectizeInput(session, "homeT", "Home Team:", choices = sbteamH[,1])
382
+ updateSelectizeInput(session, "awayT", "Away Team:", choices = sbteamA[,1])
383
  }
384
  })
385
 
386
+ game_data <- reactiveVal()
387
+
388
  observeEvent(input$update, {
389
  tryCatch({
390
  if(input$level == "AAA"){
 
393
  filter(teams_home_team_name == input$homeT) %>%
394
  filter(teams_away_team_name == input$awayT) %>%
395
  filter(game_number == input$gamenum)
396
+ pname <- pitcher_summary(pname[,6], input$date)
397
  }
398
  if(input$level == "FSL"){
399
  pname <- fslid %>%
 
401
  filter(teams_home_team_name == input$homeT) %>%
402
  filter(teams_away_team_name == input$awayT) %>%
403
  filter(game_number == input$gamenum)
404
+ pname <- pitcher_summary(pname[,6], input$date)
405
  }
406
  if(input$level == "MLB"){
407
  pname <- mlbid %>%
 
409
  filter(teams_home_team_name == input$homeT) %>%
410
  filter(teams_away_team_name == input$awayT) %>%
411
  filter(game_number == input$gamenum)
412
+ pname <- pitcher_summary(pname[,6], input$date)
413
  }
414
  if(input$level == "College (Statcast Parks Only)"){
415
  pname <- sbid %>%
 
417
  filter(teams_home_team_name == input$homeT) %>%
418
  filter(teams_away_team_name == input$awayT) %>%
419
  filter(game_number == input$gamenum)
420
+ pname <- pitcher_summary(pname[,6], input$date)
421
  }
422
 
423
  if(nrow(pname) == 0) {
424
  showNotification("No pitchers found for the selected game.", type = "warning")
425
  } else {
426
+ updateSelectizeInput(session, "pitcher", "Pitcher:", choices = unique(pname$`Pitcher Name`))
427
+ game_data(pname)
428
  }
429
  }, error = function(e) {
430
  showNotification(paste("Error finding pitchers:", e$message), type = "error")
431
  })
432
  })
433
 
434
+ rolling_timstuff <- reactive({
435
+ req(input$update1, game_data())
436
+ game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
437
+ game <- calculate_timstuff(game)
438
+ game %>%
439
+ arrange(date) %>%
440
+ group_by(pitch_name) %>%
441
+ mutate(rolling_timstuff = rollmean(TimStuff, k = 5, fill = NA, align = "right"),
442
+ pitch_number = row_number()) %>%
443
+ ungroup() # Make sure to ungroup after the grouping operations
444
+ })
445
+
446
  combinedPlot <- reactiveVal()
447
  observeEvent(input$update1, {
448
+ req(game_data())
449
  tryCatch({
450
+ game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
  if(nrow(game) == 0) {
453
  showNotification("No data available for the selected pitcher.", type = "warning")
454
  return()
455
  }
456
 
457
+ break_plot <- break_plot(game) +
458
+ theme(legend.position = "none")
459
+
460
+ pitch_plot <- pitch_plot(game) +
461
+ theme(legend.position = "none")
462
 
463
+ # Create a formatted table
464
+ table_data <- summary_table(game)
465
+ num_rows <- nrow(table_data)
 
 
466
 
467
+ table_plot <- tableGrob(table_data, rows = NULL, theme = ttheme_minimal(
468
+ core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
469
+ colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0")),
470
+ rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
471
+ ))
472
 
473
+ # Set a fixed total height for the table, adjusting row heights based on number of pitches
474
+ total_height <- unit(1, "npc")
475
+ row_height <- total_height / (num_rows + 1) # +1 for header row
476
+ table_plot$heights <- unit(rep(row_height, num_rows + 1), "npc")
477
+
478
+ # Adjust column widths
479
+ table_plot$widths <- unit(c(0.1, 0.06, 0.06, 0.08, 0.1, 0.06, 0.08, 0.08, 0.08, 0.1, 0.1, 0.1), "npc")
480
+
481
+ # Add alternating row colors
482
+ for(i in seq(2, nrow(table_plot), 2)) {
483
+ table_plot$grobs[[i]]$gp$fill <- "#f9f9f9"
484
  }
485
 
486
+ id <- as.character(game$`Pitcher ID`[1])
 
487
 
488
  mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
489
  milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
 
499
  img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
500
  }
501
 
502
+ # Create the rolling TimStuff+ graph
503
+ rolling_data <- rolling_timstuff()
504
+ timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
505
+ geom_line(size = 1) +
506
+ geom_point(size = 1) +
507
+ theme_minimal() +
508
+ labs(title = "5-Pitch Rolling TimStuff+",
509
+ x = "Pitch Number", y = "TimStuff+") +
510
+ ylim(70, 130) +
511
+ scale_x_continuous(breaks = seq(5, 55, by = 5), limits = c(5,NA)) +
512
+ theme(
513
+ plot.title = element_text(hjust = 0.5, face = "bold"),
514
+ legend.position = "none",
515
+ panel.grid.major.x = element_line(color = "gray", size = 0.5)
516
+ )
517
+
518
+ # Create title and data source text
519
+ title_text <- textGrob(
520
+ paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
521
+ gp = gpar(fontsize = 16, fontface = "bold")
522
+ )
523
+ data_source_text <- textGrob(
524
+ "Data: MLB",
525
+ gp = gpar(fontsize = 8),
526
+ x = unit(1, "npc") - unit(2, "mm"),
527
+ y = unit(2, "mm"),
528
+ just = c("right", "bottom")
529
+ )
530
+
531
+ # Create a horizontal legend
532
+ legend <- get_legend(
533
+ ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
534
+ geom_point(size = 5) +
535
+ theme(legend.position = "bottom",
536
+ legend.title = element_blank(),
537
+ text = element_text(size = 12.5),
538
+ #legend.key.size = unit(.5,"cm"),
539
+ legend.box = "horizontal" ) +
540
+ guides(color = guide_legend(nrow = 1))
541
  )
542
 
543
+ # Combine all plots
544
+ combined <- grid.arrange(
545
+ arrangeGrob(
546
+ arrangeGrob(
547
+ img_grob,
548
+ title_text,
549
+ ncol = 1,
550
+ heights = c(4, 1)
551
+ ),
552
+ pitch_plot,
553
+ ncol = 2,
554
+ widths = c(1, 1)
555
+ ),
556
+ arrangeGrob(
557
+ break_plot,
558
+ timstuff_plot,
559
+ ncol = 2,
560
+ widths = c(1, 1)
561
+ ),
562
+ #arrangeGrob(
563
+ legend,
564
+ #),
565
+ #arrangeGrob(
566
+ table_plot,
567
+ #),
568
+ data_source_text,
569
+ nrow = 5,
570
+ heights = c(1.2, 1.2, 0.05, 1.1, 0.05) # Adjusted these values
571
+ )
572
  combinedPlot(combined)
573
 
574
  output$combinedPlot <- renderPlot({