TimStats commited on
Commit
b741f6f
·
verified ·
1 Parent(s): 56de5ab

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +306 -246
app.R CHANGED
@@ -20,23 +20,112 @@ library(png)
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')
27
 
28
  model <- xgb.load('TimStuff2.model')
 
29
  # Helper functions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  download_and_process_image <- function(url) {
31
  tryCatch({
32
  response <- GET(url)
33
  content_type <- http_type(response)
34
 
35
- if (content_type %in% c("image/png", "image/jpeg")) {
36
- temp_file <- tempfile(fileext = ifelse(content_type == "image/png", ".png", ".jpg"))
 
 
 
37
  writeBin(content(response, "raw"), temp_file)
38
 
39
- if (content_type == "image/png") {
 
 
 
 
 
 
 
40
  img <- readPNG(temp_file)
41
  } else {
42
  img <- readJPEG(temp_file)
@@ -116,7 +205,7 @@ break_plot <- function(game){
116
  "Fastball"= "#7FDBFF",
117
  "Slow Curve"= "#DDDDDD",
118
  "Screwball"= "#FF69B4"
119
- )
120
  ggplot(game, aes(x = HB, y = IVB, color = pitch_name)) +
121
  geom_point(size = 2) +
122
  geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
@@ -131,111 +220,11 @@ break_plot <- function(game){
131
  legend.position = "bottom",
132
  plot.title = element_text(hjust = 0.5, face = "bold"),
133
  panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1),
134
- aspect.ratio = 1, # This ensures the plot is square
135
- ) +
136
- guides(color = guide_legend(title = "Pitch Type", nrow = 1))
137
- }
138
-
139
- pitch_plot <- function(game, title, bhand = NULL) {
140
- # if (!is.null(bhand)) {
141
- # game <- game %>% filter(bhand == bhand)
142
- # }
143
- #
144
- pitch_colors <- c(
145
- "Four-Seam Fastball"= "#FF4136",
146
- "Sinker"= "#FF851B",
147
- "Cutter"= "#FFDC00",
148
- "Changeup"= "#2ECC40",
149
- "Slider"= "#0074D9",
150
- "Sweeper"= "#ED68ED",
151
- "Curveball"= "#B10DC9",
152
- "Splitter"= "#01FF70",
153
- "Knuckle Curve"= "#85144b",
154
- "Slurve"= "#3D9970",
155
- "Knuckle Ball"= "#39CCCC",
156
- "Forkball"= "#F012BE",
157
- "Eephus"= "#AAAAAA",
158
- "Fastball"= "#7FDBFF",
159
- "Slow Curve"= "#DDDDDD",
160
- "Screwball"= "#FF69B4"
161
- )
162
- ggplot(game, aes(x = px, y = pz, color = pitch_name)) +
163
- geom_point(size = 3.5) +
164
- geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
165
- geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
166
- geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
167
- geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
168
- scale_color_manual(values = pitch_colors) +
169
- labs(x = NULL, y = NULL, title = title) +
170
- xlim(-3, 3) +
171
- ylim(0.2, 4) +
172
- coord_fixed(ratio = 1) +
173
- theme_minimal() +
174
- theme(
175
- legend.position = "bottom",
176
- plot.title = element_text(hjust = 0.5, face = "bold"),
177
- axis.text = element_blank(),
178
- axis.ticks = element_blank()
179
  ) +
180
  guides(color = guide_legend(title = "Pitch Type", nrow = 1))
181
  }
182
 
183
- calculate_VAA <- function(vz0, ay, az, vy0, y0) {
184
- -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
185
- ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
186
- }
187
-
188
- calculate_EAA <- function(extension) {
189
- extension / 6.3
190
- }
191
-
192
- calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
193
- inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
194
- inSA <- ifelse(inSA < 0, inSA + 360, inSA)
195
- SADiff <- spinDirection - inSA
196
- SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
197
- SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
198
- return(SADiff)
199
- }
200
-
201
- scale_TimStuff <- function(raw_score, model_mean, model_sd) {
202
- scaled_score <- (raw_score - model_mean) / model_sd
203
- result <- 100 - (scaled_score * 10)
204
- return(result)
205
- }
206
-
207
- # Modify the getBoxScore function
208
-
209
- getBoxScore <- function(game_pk, pid) {
210
- url <- paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore")
211
- bs <- fromJSON(url)
212
-
213
- t <- as.data.frame(bs[["teams"]][["away"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
214
- if (nrow(t) == 0) {
215
- t <- as.data.frame(bs[["teams"]][["home"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
216
- }
217
-
218
- # Define the stats we want and their display names
219
- stats <- c("inningsPitched", "battersFaced", "runs", "earnedRuns", "hits", "baseOnBalls", "strikeOuts", "strikePercentage")
220
- display_names <- c("IP", "TBF", "R", "ER", "H", "BB", "K", "Strike%")
221
-
222
- # Create a new data frame with only the stats we want
223
- result <- data.frame(matrix(ncol = length(stats), nrow = 1))
224
- colnames(result) <- display_names
225
-
226
- for (i in 1:length(stats)) {
227
- value <- t[[stats[i]]]
228
- if (is.null(value)) value <- "0"
229
- if (stats[i] == "strikePercentage") {
230
- value <- paste0(round(as.numeric(value) * 100, 1), "%")
231
- }
232
- result[1, i] <- as.character(value)
233
- }
234
-
235
- return(result)
236
- }
237
-
238
- # Create a new function for split pitch location plots
239
  left_batter <- png::readPNG("left_batter.png")
240
  right_batter <- png::readPNG("right_batter.png")
241
 
@@ -243,10 +232,9 @@ pitch_plot_split <- function(game, title) {
243
  game_lhb <- game %>% filter(bhand == "L")
244
  game_rhb <- game %>% filter(bhand == "R")
245
 
246
- # Define home plate coordinates (adjusted position)
247
  home_plate <- data.frame(
248
- x = c(0.6, -0.6, -0.7083, 0, 0.7083), # Keep x-coordinates
249
- y = c(0.5, 0.5, 0.25, 0, 0.25) # Adjust y-coordinates to put point at 0
250
  )
251
 
252
  pitch_colors <- c(
@@ -266,14 +254,11 @@ pitch_plot_split <- function(game, title) {
266
  "Fastball"= "#7FDBFF",
267
  "Slow Curve"= "#DDDDDD",
268
  "Screwball"= "#FF69B4"
269
- )
270
 
271
- # Create left-handed batter plot
272
  plot_lhb <- ggplot(game_lhb, aes(x = px, y = pz, color = pitch_name)) +
273
- # Add home plate first (so it appears under the points)
274
  geom_polygon(data = home_plate, aes(x = x, y = y),
275
  fill = "white", color = "black", inherit.aes = FALSE) +
276
- # Add batter image (now using left_batter on right side)
277
  annotation_custom(rasterGrob(left_batter,
278
  width = unit(.75, "npc"),
279
  height = unit(1.5, "npc")),
@@ -297,12 +282,9 @@ pitch_plot_split <- function(game, title) {
297
  axis.ticks = element_blank()
298
  )
299
 
300
- # Create right-handed batter plot
301
  plot_rhb <- ggplot(game_rhb, aes(x = px, y = pz, color = pitch_name)) +
302
- # Add home plate first
303
  geom_polygon(data = home_plate, aes(x = x, y = y),
304
  fill = "white", color = "black", inherit.aes = FALSE) +
305
- # Add batter image (now using right_batter on left side)
306
  annotation_custom(rasterGrob(right_batter,
307
  width = unit(.75, "npc"),
308
  height = unit(1.5, "npc")),
@@ -329,6 +311,56 @@ pitch_plot_split <- function(game, title) {
329
  return(list(lhb = plot_lhb, rhb = plot_rhb))
330
  }
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
  calculate_timstuff <- function(game) {
334
  game <- game %>%
@@ -417,76 +449,78 @@ summary_table <- function(game) {
417
 
418
  return(result)
419
  }
420
- # Initialize schedule data
421
- mlbid <- mlb_schedule(season = 2025, level_ids = "1")
 
 
 
 
 
 
 
 
422
  mlbteamH <- mlbid %>%
423
- select(teams_home_team_name)
424
- mlbteamH <- distinct(mlbteamH)
425
  mlbteamA <- mlbid %>%
426
- select(teams_away_team_name)
427
- mlbteamA <- distinct(mlbteamA)
428
 
429
- aaaid <- mlb_schedule(season = 2025, level_ids = "11")
 
 
 
 
 
 
 
430
  aaateamH <- aaaid %>%
431
- select(teams_home_team_name)
432
- aaateamH <- distinct(aaateamH)
433
  aaateamA <- aaaid %>%
434
- select(teams_away_team_name)
435
- aaateamA <- distinct(aaateamA)
436
 
437
- fslid <- mlb_schedule(season = 2025, level_ids = "14")
438
- fslid <- fslid %>%
439
- filter(teams_home_team_name == "Daytona Tortugas" |
440
- teams_home_team_name == "Jupiter Hammerheads" |
441
- teams_home_team_name == "Palm Beach Cardinals" |
442
- teams_home_team_name == "St. Lucie Mets" |
443
- teams_home_team_name == "Bradenton Marauders" |
444
- teams_home_team_name == "Clearwater Threshers" |
445
- teams_home_team_name == "Dunedin Blue Jays" |
446
- teams_home_team_name == "Fort Myers Mighty Mussels" |
447
- teams_home_team_name == "Lakeland Flying Tigers" |
448
- teams_home_team_name == "Tampa Tarpons")
449
  fslteamH <- fslid %>%
450
- select(teams_home_team_name)
451
- fslteamH <- distinct(fslteamH)
452
  fslteamA <- fslid %>%
453
- select(teams_away_team_name)
454
- fslteamA <- distinct(fslteamA)
455
 
456
- #sbid <- mlb_schedule(season = 2024, level_ids = "22")
457
- #sbteamH <- sbid %>%
458
- # select(teams_home_team_name)
459
- #sbteamH <- distinct(sbteamH)
460
- #sbteamA <- sbid %>%
461
- # select(teams_away_team_name)
462
- #sbteamA <- distinct(sbteamA)
463
- # AFL
464
- sbid <- mlb_schedule(2025,22)
465
- sbid <- sbid %>%
466
- # filter(teams_away_team_name == "Glendale Desert Dogs" |
467
- # teams_away_team_name == "Mesa Solar Sox" |
468
- # teams_away_team_name == "Peoria Javelinas" |
469
- # teams_away_team_name == "Salt River Rafters" |
470
- # teams_away_team_name == "Scottsdale Scorpions" |
471
- # teams_away_team_name == "Surprise Saguaros") %>%#
472
- filter(gameday_type == "E" | gameday_type == "P")
473
- sbteamH <- sbid %>%
474
- select(teams_home_team_name)
475
- sbteamH <- distinct(sbteamH)
476
- sbteamA <- sbid %>%
477
- select(teams_away_team_name)
478
- sbteamA <- distinct(sbteamA)
479
 
480
  # UI Definition
481
  ui <- fluidPage(
482
  theme = bs_theme(version = 5, bootswatch = "flatly"),
483
- titlePanel("2025 MLB/AAA/FSL Summary Cards"),
484
  sidebarLayout(
485
  sidebarPanel(
486
  width = 3,
487
  dateInput("date", "Date:"),
488
  selectizeInput("level", "Level:",
489
- c("MLB", "AAA", "FSL", "AFL"),
490
  options = list(
491
  placeholder = 'Select a level',
492
  onInitialize = I('function() { this.setValue(""); }')
@@ -502,10 +536,10 @@ ui <- fluidPage(
502
  downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
503
  ),
504
  mainPanel(
505
- div(style = "width: 1200px; height: 800px; overflow: auto;",
506
- plotOutput("combinedPlot", width = "100%", height = "100%")
507
  ),
508
- tableOutput("boxscoreTable") # Add this line for the boxscore
509
  )
510
  )
511
  )
@@ -599,7 +633,9 @@ server <- function(input, output, session) {
599
 
600
  return(result)
601
  })
 
602
  combinedPlot <- reactiveVal()
 
603
  observeEvent(input$update1, {
604
  req(game_data())
605
  tryCatch({
@@ -609,25 +645,51 @@ server <- function(input, output, session) {
609
  showNotification("No data available for the selected pitcher.", type = "warning")
610
  return()
611
  }
 
 
 
612
 
613
- break_plot <- break_plot(game) +
614
- theme(legend.position = "none")
 
 
 
 
 
615
 
616
- # Get boxscore data
 
 
 
 
 
 
 
 
 
 
 
617
  boxscore_data <- getBoxScore(unique(game$gamepk[1]), unique(game$`Pitcher ID`[1]))
 
 
618
  boxscore_table <- tableGrob(boxscore_data, rows = NULL, theme = ttheme_minimal(
619
- core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
620
- colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0")),
621
- rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
 
 
 
 
 
 
 
 
 
622
  ))
623
 
624
- # Set a fixed total height for the boxscore table
625
  boxscore_table$heights <- unit(rep(1/(nrow(boxscore_data) + 1), nrow(boxscore_data) + 1), "npc")
626
 
627
- # Get split pitch location plots
628
- pitch_plots <- pitch_plot_split(game)
629
-
630
- # Create a formatted table
631
  table_data <- summary_table(game)
632
  num_rows <- nrow(table_data)
633
 
@@ -637,21 +699,17 @@ server <- function(input, output, session) {
637
  rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
638
  ))
639
 
640
- # Set a fixed total height for the table, adjusting row heights based on number of pitches
641
  total_height <- unit(1, "npc")
642
- row_height <- total_height / (num_rows + 1) # +1 for header row
643
  table_plot$heights <- unit(rep(row_height, num_rows + 1), "npc")
 
644
 
645
- # Adjust column widths
646
- 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")
647
-
648
- # Add alternating row colors
649
  for(i in seq(2, nrow(table_plot), 2)) {
650
  table_plot$grobs[[i]]$gp$fill <- "#f9f9f9"
651
  }
652
 
 
653
  id <- as.character(game$`Pitcher ID`[1])
654
-
655
  mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
656
  milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
657
 
@@ -660,38 +718,38 @@ server <- function(input, output, session) {
660
  img_result <- download_and_process_image(milb_url)
661
  }
662
 
663
- if (!is.null(img_result)) {
664
- img_grob <- rasterGrob(img_result$img, interpolate = TRUE)
665
  } else {
666
- img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
667
  }
668
 
669
- pitch_colors <- c(
670
- "Four-Seam Fastball"= "#FF4136",
671
- "Sinker"= "#FF851B",
672
- "Cutter"= "#FFDC00",
673
- "Changeup"= "#2ECC40",
674
- "Slider"= "#0074D9",
675
- "Sweeper"= "#ED68ED",
676
- "Curveball"= "#B10DC9",
677
- "Splitter"= "#01FF70",
678
- "Knuckle Curve"= "#85144b",
679
- "Slurve"= "#3D9970",
680
- "Knuckle Ball"= "#39CCCC",
681
- "Forkball"= "#F012BE",
682
- "Eephus"= "#AAAAAA",
683
- "Fastball"= "#7FDBFF",
684
- "Slow Curve"= "#DDDDDD",
685
- "Screwball"= "#FF69B4"
686
- )
687
- # Create the rolling TimStuff+ graph
688
  rolling_data <- rolling_timstuff()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
690
  geom_line(size = 1, na.rm = TRUE) +
691
  geom_point(size = 1, na.rm = TRUE) +
692
  scale_color_manual(values = pitch_colors) +
693
  theme_minimal() +
694
- ### Workaround to not show anything but keep graph
695
  labs(title = if(any(rolling_data$pitch_number >= 5)) "5-Pitch Rolling TimStuff+" else "5-Pitch Rolling TimStuff+",
696
  x = "Pitch Number", y = "TimStuff+") +
697
  scale_y_continuous(limits = c(70, 130), na.value = NA) +
@@ -702,7 +760,6 @@ server <- function(input, output, session) {
702
  panel.grid.major.x = element_line(color = "gray", size = 0.5)
703
  )
704
 
705
- # If there's no data at all, display a message
706
  if(nrow(rolling_data) == 0) {
707
  timstuff_plot <- ggplot() +
708
  theme_void() +
@@ -717,6 +774,7 @@ server <- function(input, output, session) {
717
  paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
718
  gp = gpar(fontsize = 16, fontface = "bold")
719
  )
 
720
  data_source_text <- textGrob(
721
  "Data: MLB",
722
  gp = gpar(fontsize = 8),
@@ -725,7 +783,7 @@ server <- function(input, output, session) {
725
  just = c("right", "bottom")
726
  )
727
 
728
- # Create a horizontal legend
729
  legend <- if(nrow(rolling_data) > 0) {
730
  get_legend(
731
  ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
@@ -738,69 +796,71 @@ server <- function(input, output, session) {
738
  guides(color = guide_legend(nrow = 1))
739
  )
740
  } else {
741
- ggplotGrob(ggplot() + theme_void()) # Empty legend if no data
742
  }
743
 
744
- # Combine all plots
745
  combined <- grid.arrange(
746
- # Row 1: Picture and Rolling TimStuff+ graph
 
 
 
 
 
 
 
 
 
747
  arrangeGrob(
748
- arrangeGrob(
749
- img_grob,
750
- title_text,
751
- ncol = 1,
752
- heights = c(4, 1)
753
- ),
754
  timstuff_plot,
 
755
  ncol = 2,
756
- widths = c(1, 1)
757
  ),
758
 
759
- # Row 2: Two location graphs and movement graph
760
  arrangeGrob(
761
  pitch_plots$lhb,
762
  pitch_plots$rhb,
763
  break_plot,
764
  ncol = 3,
765
- widths = c(1, 1, 1.2)
766
  ),
767
 
768
- # Row 3: Legend
769
  legend,
770
 
771
- # Row 4: Horizontal boxscore table
772
- tableGrob(
773
- boxscore_data,
774
- rows = NULL,
775
- cols = colnames(boxscore_data),
776
- theme = ttheme_minimal(
777
- core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
778
- colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0"))
779
- )
780
- ),
781
-
782
  # Row 5: Pitch summary table
783
  table_plot,
784
 
785
- # Row 6: Data source text
786
- data_source_text,
787
-
788
- # Layout parameters
789
- nrow = 6,
790
- heights = c(1.2, 1.5, 0.1, 0.3, 1, 0.05)
791
  )
 
 
 
 
 
792
  combinedPlot(combined)
793
 
 
794
  output$combinedPlot <- renderPlot({
 
795
  grid.draw(combinedPlot())
796
- }, width = 1200, height = 800) # Fixed size output
797
 
 
798
  output$downloadPlot <- downloadHandler(
799
  filename = function() {
800
  paste("baseball_card_", Sys.Date(), ".png", sep = "")
801
  },
802
  content = function(file) {
803
- ggsave(file, plot = combinedPlot(), width = 18, height = 12, dpi = 300, units = "in")
 
 
 
 
804
  }
805
  )
806
 
 
20
  library(xgboost)
21
  library(httr)
22
  library(jpeg)
23
+ library(zoo)
24
 
25
  pdf(file = NULL)
26
  Sys.setenv(TZ='EST')
27
 
28
  model <- xgb.load('TimStuff2.model')
29
+
30
  # Helper functions
31
+ gtable_add_padding <- function(gtable, padding = unit(c(5, 5, 5, 5), "mm")) {
32
+ padding <- rep(padding, length.out = 4)
33
+
34
+ # Add padding to the layout
35
+ new_widths <- unit.c(padding[4], gtable$widths, padding[2])
36
+ new_heights <- unit.c(padding[1], gtable$heights, padding[3])
37
+
38
+ # Create the new layout with added padding
39
+ new_layout <- gtable$layout
40
+ new_layout$l <- new_layout$l + 1
41
+ new_layout$t <- new_layout$t + 1
42
+
43
+ # Create new gtable with padding
44
+ new_gtable <- gtable(
45
+ widths = new_widths,
46
+ heights = new_heights
47
+ )
48
+
49
+ # Add grobs from original gtable
50
+ new_gtable$layout <- new_layout
51
+ new_gtable$grobs <- gtable$grobs
52
+
53
+ return(new_gtable)
54
+ }
55
+ get_team_logo <- function(team_name) {
56
+ # ESPN team ID and URL mapping
57
+ team_info <- list(
58
+ "Los Angeles Angels" = "LAA",
59
+ "Arizona Diamondbacks" = "ARI",
60
+ "Baltimore Orioles" = "BAL",
61
+ "Boston Red Sox" = "BOS",
62
+ "Chicago Cubs" = "CHC",
63
+ "Cincinnati Reds" = "CIN",
64
+ "Cleveland Guardians" = "CLE",
65
+ "Colorado Rockies" = "COL",
66
+ "Detroit Tigers" = "DET",
67
+ "Houston Astros" = "HOU",
68
+ "Kansas City Royals" = "KC",
69
+ "Los Angeles Dodgers" = "LAD",
70
+ "Washington Nationals" = "WSH",
71
+ "New York Mets" = "NYM",
72
+ "Oakland Athletics" = "OAK",
73
+ "Pittsburgh Pirates" = "PIT",
74
+ "San Diego Padres" = "SD",
75
+ "Seattle Mariners" = "SEA",
76
+ "San Francisco Giants" = "SF",
77
+ "St. Louis Cardinals" = "STL",
78
+ "Tampa Bay Rays" = "TB",
79
+ "Texas Rangers" = "TEX",
80
+ "Toronto Blue Jays" = "TOR",
81
+ "Minnesota Twins" = "MIN",
82
+ "Philadelphia Phillies" = "PHI",
83
+ "Atlanta Braves" = "ATL",
84
+ "Chicago White Sox" = "CHW",
85
+ "Miami Marlins" = "MIA",
86
+ "New York Yankees" = "NYY",
87
+ "Milwaukee Brewers" = "MIL"
88
+ )
89
+
90
+ team_abbrev <- team_info[[team_name]]
91
+ if (is.null(team_abbrev)) {
92
+ return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12)))
93
+ }
94
+
95
+ logo_url <- sprintf("https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/%s.png&h=400&w=400", team_abbrev)
96
+ logo_result <- download_and_process_image(logo_url)
97
+
98
+ if (!is.null(logo_result)) {
99
+ # Set fixed size for the logo
100
+ return(rasterGrob(logo_result$img,
101
+ interpolate = TRUE,
102
+ width = unit(.75, "npc"), # Reduced size to 50% of container
103
+ height = unit(.75, "npc")))
104
+ } else {
105
+ return(textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12)))
106
+ }
107
+ }
108
+
109
  download_and_process_image <- function(url) {
110
  tryCatch({
111
  response <- GET(url)
112
  content_type <- http_type(response)
113
 
114
+ if (content_type %in% c("image/png", "image/jpeg", "image/svg+xml")) {
115
+ temp_file <- tempfile(fileext = switch(content_type,
116
+ "image/png" = ".png",
117
+ "image/jpeg" = ".jpg",
118
+ "image/svg+xml" = ".svg"))
119
  writeBin(content(response, "raw"), temp_file)
120
 
121
+ # For SVG, convert to PNG first
122
+ if (content_type == "image/svg+xml") {
123
+ svg_content <- readLines(temp_file, warn = FALSE)
124
+ # Create a PNG from the SVG
125
+ png_file <- tempfile(fileext = ".png")
126
+ rsvg::rsvg_png(temp_file, png_file)
127
+ img <- png::readPNG(png_file)
128
+ } else if (content_type == "image/png") {
129
  img <- readPNG(temp_file)
130
  } else {
131
  img <- readJPEG(temp_file)
 
205
  "Fastball"= "#7FDBFF",
206
  "Slow Curve"= "#DDDDDD",
207
  "Screwball"= "#FF69B4"
208
+ )
209
  ggplot(game, aes(x = HB, y = IVB, color = pitch_name)) +
210
  geom_point(size = 2) +
211
  geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
 
220
  legend.position = "bottom",
221
  plot.title = element_text(hjust = 0.5, face = "bold"),
222
  panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1),
223
+ aspect.ratio = 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  ) +
225
  guides(color = guide_legend(title = "Pitch Type", nrow = 1))
226
  }
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  left_batter <- png::readPNG("left_batter.png")
229
  right_batter <- png::readPNG("right_batter.png")
230
 
 
232
  game_lhb <- game %>% filter(bhand == "L")
233
  game_rhb <- game %>% filter(bhand == "R")
234
 
 
235
  home_plate <- data.frame(
236
+ x = c(0.6, -0.6, -0.7083, 0, 0.7083),
237
+ y = c(0.5, 0.5, 0.25, 0, 0.25)
238
  )
239
 
240
  pitch_colors <- c(
 
254
  "Fastball"= "#7FDBFF",
255
  "Slow Curve"= "#DDDDDD",
256
  "Screwball"= "#FF69B4"
257
+ )
258
 
 
259
  plot_lhb <- ggplot(game_lhb, aes(x = px, y = pz, color = pitch_name)) +
 
260
  geom_polygon(data = home_plate, aes(x = x, y = y),
261
  fill = "white", color = "black", inherit.aes = FALSE) +
 
262
  annotation_custom(rasterGrob(left_batter,
263
  width = unit(.75, "npc"),
264
  height = unit(1.5, "npc")),
 
282
  axis.ticks = element_blank()
283
  )
284
 
 
285
  plot_rhb <- ggplot(game_rhb, aes(x = px, y = pz, color = pitch_name)) +
 
286
  geom_polygon(data = home_plate, aes(x = x, y = y),
287
  fill = "white", color = "black", inherit.aes = FALSE) +
 
288
  annotation_custom(rasterGrob(right_batter,
289
  width = unit(.75, "npc"),
290
  height = unit(1.5, "npc")),
 
311
  return(list(lhb = plot_lhb, rhb = plot_rhb))
312
  }
313
 
314
+ calculate_VAA <- function(vz0, ay, az, vy0, y0) {
315
+ -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
316
+ ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
317
+ }
318
+
319
+ calculate_EAA <- function(extension) {
320
+ extension / 6.3
321
+ }
322
+
323
+ calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
324
+ inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
325
+ inSA <- ifelse(inSA < 0, inSA + 360, inSA)
326
+ SADiff <- spinDirection - inSA
327
+ SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
328
+ SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
329
+ return(SADiff)
330
+ }
331
+
332
+ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
333
+ scaled_score <- (raw_score - model_mean) / model_sd
334
+ result <- 100 - (scaled_score * 10)
335
+ return(result)
336
+ }
337
+
338
+ getBoxScore <- function(game_pk, pid) {
339
+ url <- paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore")
340
+ bs <- fromJSON(url)
341
+
342
+ t <- as.data.frame(bs[["teams"]][["away"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
343
+ if (nrow(t) == 0) {
344
+ t <- as.data.frame(bs[["teams"]][["home"]][["players"]][[paste0("ID", pid)]][["stats"]][["pitching"]])
345
+ }
346
+
347
+ stats <- c("inningsPitched", "battersFaced", "runs", "earnedRuns", "hits", "baseOnBalls", "strikeOuts", "strikePercentage")
348
+ display_names <- c("IP", "TBF", "R", "ER", "H", "BB", "K", "Strike%")
349
+
350
+ result <- data.frame(matrix(ncol = length(stats), nrow = 1))
351
+ colnames(result) <- display_names
352
+
353
+ for (i in 1:length(stats)) {
354
+ value <- t[[stats[i]]]
355
+ if (is.null(value)) value <- "0"
356
+ if (stats[i] == "strikePercentage") {
357
+ value <- paste0(round(as.numeric(value) * 100, 1), "%")
358
+ }
359
+ result[1, i] <- as.character(value)
360
+ }
361
+
362
+ return(result)
363
+ }
364
 
365
  calculate_timstuff <- function(game) {
366
  game <- game %>%
 
449
 
450
  return(result)
451
  }
452
+
453
+
454
+ mlbid <- bind_rows(
455
+ mlb_schedule(season = 2020, level_ids = "1"),
456
+ mlb_schedule(season = 2021, level_ids = "1"),
457
+ mlb_schedule(season = 2022, level_ids = "1"),
458
+ mlb_schedule(season = 2023, level_ids = "1"),
459
+ mlb_schedule(season = 2024, level_ids = "1"),
460
+ mlb_schedule(season = 2025, level_ids = "1")
461
+ )
462
  mlbteamH <- mlbid %>%
463
+ select(teams_home_team_name) %>%
464
+ distinct()
465
  mlbteamA <- mlbid %>%
466
+ select(teams_away_team_name) %>%
467
+ distinct()
468
 
469
+ # AAA Schedule (2021-2025)
470
+ aaaid <- bind_rows(
471
+ mlb_schedule(season = 2021, level_ids = "11"),
472
+ mlb_schedule(season = 2022, level_ids = "11"),
473
+ mlb_schedule(season = 2023, level_ids = "11"),
474
+ mlb_schedule(season = 2024, level_ids = "11"),
475
+ mlb_schedule(season = 2025, level_ids = "11")
476
+ )
477
  aaateamH <- aaaid %>%
478
+ select(teams_home_team_name) %>%
479
+ distinct()
480
  aaateamA <- aaaid %>%
481
+ select(teams_away_team_name) %>%
482
+ distinct()
483
 
484
+ # FSL Schedule (2021-2025)
485
+ fslid <- bind_rows(
486
+ mlb_schedule(season = 2021, level_ids = "14"),
487
+ mlb_schedule(season = 2022, level_ids = "14"),
488
+ mlb_schedule(season = 2023, level_ids = "14"),
489
+ mlb_schedule(season = 2024, level_ids = "14"),
490
+ mlb_schedule(season = 2025, level_ids = "14")
491
+ ) %>%
492
+ filter(gameday_type == "E" | gameday_type == "P")
 
 
 
493
  fslteamH <- fslid %>%
494
+ select(teams_home_team_name) %>%
495
+ distinct()
496
  fslteamA <- fslid %>%
497
+ select(teams_away_team_name) %>%
498
+ distinct()
499
 
500
+ # AFL Schedule (2023-2025)
501
+ # sbid <- bind_rows(
502
+ # mlb_schedule(season = 2023, level_ids = "22"),
503
+ # mlb_schedule(season = 2024, level_ids = "22"),
504
+ # mlb_schedule(season = 2025, level_ids = "22")
505
+ # ) %>%
506
+ # filter(gameday_type == "E" | gameday_type == "P")
507
+ # sbteamH <- sbid %>%
508
+ # select(teams_home_team_name) %>%
509
+ # distinct()
510
+ # sbteamA <- sbid %>%
511
+ # select(teams_away_team_name) %>%
512
+ # distinct()
 
 
 
 
 
 
 
 
 
 
513
 
514
  # UI Definition
515
  ui <- fluidPage(
516
  theme = bs_theme(version = 5, bootswatch = "flatly"),
517
+ titlePanel("202-2025 Daily MLB/AAA/FSL Summary Cards"),
518
  sidebarLayout(
519
  sidebarPanel(
520
  width = 3,
521
  dateInput("date", "Date:"),
522
  selectizeInput("level", "Level:",
523
+ c("MLB", "AAA", "FSL"),
524
  options = list(
525
  placeholder = 'Select a level',
526
  onInitialize = I('function() { this.setValue(""); }')
 
536
  downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
537
  ),
538
  mainPanel(
539
+ div(style = "width: 1100px; height: 1100px; overflow: auto;", # Increased from 1000px
540
+ plotOutput("combinedPlot", width = "100%", height = "100%")
541
  ),
542
+ tableOutput("boxscoreTable")
543
  )
544
  )
545
  )
 
633
 
634
  return(result)
635
  })
636
+
637
  combinedPlot <- reactiveVal()
638
+
639
  observeEvent(input$update1, {
640
  req(game_data())
641
  tryCatch({
 
645
  showNotification("No data available for the selected pitcher.", type = "warning")
646
  return()
647
  }
648
+ game_pk <- unique(game$gamepk)[1]
649
+ bs <- fromJSON(paste0("https://statsapi.mlb.com/api/v1/game/", game_pk, "/boxscore"))
650
+ pitcher_id <- unique(game$`Pitcher ID`)[1]
651
 
652
+ # Look for pitcher in both home and away teams
653
+ pitcher_team <- NULL
654
+ if(paste0("ID", pitcher_id) %in% names(bs$teams$away$players)) {
655
+ pitcher_team <- bs$teams$away$team$name
656
+ } else if(paste0("ID", pitcher_id) %in% names(bs$teams$home$players)) {
657
+ pitcher_team <- bs$teams$home$team$name
658
+ }
659
 
660
+ if(is.null(pitcher_team)) {
661
+ mlb_team_logo <- textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))
662
+ } else {
663
+ mlb_team_logo <- get_team_logo(pitcher_team)
664
+ if(is.null(mlb_team_logo)) {
665
+ mlb_team_logo <- textGrob("Logo not available", gp = gpar(col = "gray", fontsize = 12))
666
+ }
667
+ }
668
+ #mlb_team_logo <- get_team_logo(pitcher_team)
669
+ # Get plots and data
670
+ break_plot <- break_plot(game) + theme(legend.position = "none")
671
+ pitch_plots <- pitch_plot_split(game)
672
  boxscore_data <- getBoxScore(unique(game$gamepk[1]), unique(game$`Pitcher ID`[1]))
673
+
674
+ # Create boxscore table
675
  boxscore_table <- tableGrob(boxscore_data, rows = NULL, theme = ttheme_minimal(
676
+ core = list(
677
+ fg_params = list(hjust = 0.5, x = 0.5),
678
+ bg_params = list(fill = "white"),
679
+ # Reduce the y padding to make the table more compact
680
+ y.padding = unit(4, "mm") # Reduced from default
681
+ ),
682
+ colhead = list(
683
+ fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"),
684
+ bg_params = list(fill = "#f0f0f0"),
685
+ # Reduce the y padding for headers
686
+ y.padding = unit(4, "mm") # Reduced from default
687
+ )
688
  ))
689
 
 
690
  boxscore_table$heights <- unit(rep(1/(nrow(boxscore_data) + 1), nrow(boxscore_data) + 1), "npc")
691
 
692
+ # Create summary table
 
 
 
693
  table_data <- summary_table(game)
694
  num_rows <- nrow(table_data)
695
 
 
699
  rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
700
  ))
701
 
 
702
  total_height <- unit(1, "npc")
703
+ row_height <- total_height / (num_rows + 1)
704
  table_plot$heights <- unit(rep(row_height, num_rows + 1), "npc")
705
+ table_plot$widths <- unit(c(0.12, 0.05, 0.05, 0.07, 0.08, 0.05, 0.07, 0.07, 0.07, 0.09, 0.09, 0.09), "npc")
706
 
 
 
 
 
707
  for(i in seq(2, nrow(table_plot), 2)) {
708
  table_plot$grobs[[i]]$gp$fill <- "#f9f9f9"
709
  }
710
 
711
+ # Get player image
712
  id <- as.character(game$`Pitcher ID`[1])
 
713
  mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
714
  milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
715
 
 
718
  img_result <- download_and_process_image(milb_url)
719
  }
720
 
721
+ img_grob <- if (!is.null(img_result)) {
722
+ rasterGrob(img_result$img, interpolate = TRUE)
723
  } else {
724
+ textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
725
  }
726
 
727
+ # Create rolling TimStuff+ plot
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  rolling_data <- rolling_timstuff()
729
+ pitch_colors <- c(
730
+ "Four-Seam Fastball"= "#FF4136",
731
+ "Sinker"= "#FF851B",
732
+ "Cutter"= "#FFDC00",
733
+ "Changeup"= "#2ECC40",
734
+ "Slider"= "#0074D9",
735
+ "Sweeper"= "#ED68ED",
736
+ "Curveball"= "#B10DC9",
737
+ "Splitter"= "#01FF70",
738
+ "Knuckle Curve"= "#85144b",
739
+ "Slurve"= "#3D9970",
740
+ "Knuckle Ball"= "#39CCCC",
741
+ "Forkball"= "#F012BE",
742
+ "Eephus"= "#AAAAAA",
743
+ "Fastball"= "#7FDBFF",
744
+ "Slow Curve"= "#DDDDDD",
745
+ "Screwball"= "#FF69B4"
746
+ )
747
+
748
  timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
749
  geom_line(size = 1, na.rm = TRUE) +
750
  geom_point(size = 1, na.rm = TRUE) +
751
  scale_color_manual(values = pitch_colors) +
752
  theme_minimal() +
 
753
  labs(title = if(any(rolling_data$pitch_number >= 5)) "5-Pitch Rolling TimStuff+" else "5-Pitch Rolling TimStuff+",
754
  x = "Pitch Number", y = "TimStuff+") +
755
  scale_y_continuous(limits = c(70, 130), na.value = NA) +
 
760
  panel.grid.major.x = element_line(color = "gray", size = 0.5)
761
  )
762
 
 
763
  if(nrow(rolling_data) == 0) {
764
  timstuff_plot <- ggplot() +
765
  theme_void() +
 
774
  paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
775
  gp = gpar(fontsize = 16, fontface = "bold")
776
  )
777
+
778
  data_source_text <- textGrob(
779
  "Data: MLB",
780
  gp = gpar(fontsize = 8),
 
783
  just = c("right", "bottom")
784
  )
785
 
786
+ # Create legend
787
  legend <- if(nrow(rolling_data) > 0) {
788
  get_legend(
789
  ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
 
796
  guides(color = guide_legend(nrow = 1))
797
  )
798
  } else {
799
+ ggplotGrob(ggplot() + theme_void())
800
  }
801
 
802
+ # NEW LAYOUT: Combine all plots
803
  combined <- grid.arrange(
804
+ # Row 1: Headshot - Title - Team Logo
805
+ arrangeGrob(
806
+ img_grob,
807
+ title_text,
808
+ mlb_team_logo,
809
+ ncol = 3,
810
+ widths = c(1, 2, 1)
811
+ ),
812
+
813
+ # Row 2: TimStuff+ (2/3) and boxscore (1/3)
814
  arrangeGrob(
 
 
 
 
 
 
815
  timstuff_plot,
816
+ boxscore_table,
817
  ncol = 2,
818
+ widths = c(2, 1)
819
  ),
820
 
821
+ # Row 3: Three plots in one row
822
  arrangeGrob(
823
  pitch_plots$lhb,
824
  pitch_plots$rhb,
825
  break_plot,
826
  ncol = 3,
827
+ widths = c(1, 1, 1)
828
  ),
829
 
830
+ # Row 4: Legend
831
  legend,
832
 
 
 
 
 
 
 
 
 
 
 
 
833
  # Row 5: Pitch summary table
834
  table_plot,
835
 
836
+ # Adjust the relative heights
837
+ nrow = 5,
838
+ heights = c(0.8, 0.9, 1.2, 0.2, 1)
 
 
 
839
  )
840
+
841
+ # Add padding to the combined plot
842
+ # padded_plot <- gtable_add_padding(combined, padding = unit(c(30, 30, 30, 30), "points")) # top, right, bottom, left padding
843
+
844
+ # Update the reactive value with the padded plot
845
  combinedPlot(combined)
846
 
847
+ # Update the rendering to use the padded plot
848
  output$combinedPlot <- renderPlot({
849
+ par(mar = c(2, 2, 2, 2)) # Add margins (bottom, left, top, right)
850
  grid.draw(combinedPlot())
851
+ }, width = 1000, height = 1000, bg = "white")
852
 
853
+ # For download
854
  output$downloadPlot <- downloadHandler(
855
  filename = function() {
856
  paste("baseball_card_", Sys.Date(), ".png", sep = "")
857
  },
858
  content = function(file) {
859
+ ggsave(file, plot = combinedPlot(),
860
+ width = 15, height = 15, # Increased from 15
861
+ dpi = 300, units = "in",
862
+ device = "png",
863
+ bg = "white")
864
  }
865
  )
866