igroffman commited on
Commit
ef24e52
·
verified ·
1 Parent(s): 6c6d98f

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +103 -86
app.R CHANGED
@@ -2942,11 +2942,36 @@ create_pitcher_pdf <- function(game_df, pitcher_name, output_file, pitch_colors)
2942
  invisible(output_file)
2943
  }
2944
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2945
 
2946
- # =====================================================================
2947
- # ====================== UMPIRE CODE ================================
2948
- # =====================================================================
2949
 
 
 
 
2950
  umpire_process_data <- function(df) {
2951
  df <- df %>%
2952
  filter(PitchCall %in% c("StrikeCalled", "BallCalled", "BallinDirt"))
@@ -2961,6 +2986,10 @@ umpire_process_data <- function(df) {
2961
  df
2962
  }
2963
 
 
 
 
 
2964
  umpire_create_report_pdf <- function(data,
2965
  output_file,
2966
  left_logo_path = NULL,
@@ -2970,31 +2999,48 @@ umpire_create_report_pdf <- function(data,
2970
  rows_per_page = 30) {
2971
 
2972
  suppressPackageStartupMessages({
2973
- library(dplyr); library(grid); library(gridExtra); library(ggplot2); library(stringr)
 
2974
  })
2975
  `%||%` <- function(a, b) if (!is.null(a)) a else b
2976
 
 
 
 
 
2977
  raw_strike_miss <- data %>%
2978
  filter(PitchCall == "StrikeCalled") %>%
2979
- filter(PlateLocSide < -0.83083 | PlateLocSide > 0.83083 |
2980
- PlateLocHeight > 3.37750 | PlateLocHeight < 1.5) %>% nrow()
 
2981
 
2982
  raw_ball_miss <- data %>%
2983
  filter(PitchCall %in% c("BallCalled", "BallinDirt")) %>%
2984
- filter(PlateLocSide > -0.83083 & PlateLocSide < 0.83083 &
2985
- PlateLocHeight < 3.37750 & PlateLocHeight > 1.5) %>% nrow()
 
2986
 
2987
  total_called <- nrow(data)
2988
  total_missed <- raw_strike_miss + raw_ball_miss
2989
  correct <- total_called - total_missed
2990
  overall_pct <- paste0(sprintf("%.0f", 100 * (correct / total_called)), "%")
2991
 
 
 
 
2992
  buffer_strike_miss <- data %>%
2993
  filter(PitchCall == "StrikeCalled") %>%
2994
- filter(PlateLocSide < -0.9975 | PlateLocSide > 0.9975 |
2995
- PlateLocHeight > 3.5 | PlateLocHeight < 1.3775) %>% nrow()
 
2996
 
2997
- buffer_total_missed <- raw_ball_miss + buffer_strike_miss
 
 
 
 
 
 
2998
  buffer_correct <- total_called - buffer_total_missed
2999
  buffer_pct <- paste0(sprintf("%.0f", 100 * (buffer_correct / total_called)), "%")
3000
 
@@ -3007,24 +3053,18 @@ umpire_create_report_pdf <- function(data,
3007
  paste("Umpire Report —", umpire_name)
3008
  } else "Umpire Report"
3009
 
3010
- strike_zone_rect <- data.frame(xmin = -0.83083, xmax = 0.83083, ymin = 1.5, ymax = 3.37750)
3011
- buffer_zone_rect <- data.frame(xmin = -0.9975, xmax = 0.9975, ymin = 1.3775, ymax = 3.5)
3012
-
3013
- home_plate <- data.frame(
3014
- x = c(-0.708, 0.708, 0.708, 0.000, -0.708),
3015
- y = c( 0.150, 0.150, 0.300, 0.500, 0.300)
3016
- )
3017
-
3018
  # ---------------------------------------------------------------
3019
- # BUILD MISSED CALLS TABLE EARLY (before plots) so we can number them
3020
  # ---------------------------------------------------------------
3021
  MissedCalls <- dplyr::bind_rows(
 
3022
  data %>% filter(PitchCall %in% c("BallCalled", "BallinDirt"),
3023
- PlateLocSide > -0.83083, PlateLocSide < 0.83083,
3024
- PlateLocHeight < 3.37750, PlateLocHeight > 1.5),
 
3025
  data %>% filter(PitchCall == "StrikeCalled") %>%
3026
- filter(PlateLocSide < -0.83083 | PlateLocSide > 0.83083 |
3027
- PlateLocHeight > 3.37750 | PlateLocHeight < 1.5)
3028
  ) %>%
3029
  arrange(PitchNo) %>%
3030
  mutate(CallNo = row_number()) %>%
@@ -3033,21 +3073,18 @@ umpire_create_report_pdf <- function(data,
3033
  Height = paste0(sprintf("%.0f", PlateLocHeight * 12), '"')
3034
  )
3035
 
3036
- # Keep PlateLocSide/PlateLocHeight/BatterSide/BatterTeam available for plot filtering
3037
- # but select display columns for the table at the end
3038
-
3039
  # ---------------------------------------------------------------
3040
- # BASE ZONE HELPER
3041
  # ---------------------------------------------------------------
3042
  base_zone <- function() {
3043
  ggplot() +
3044
- geom_rect(data = buffer_zone_rect,
3045
  aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
3046
  fill = NA, color = "gray50", linewidth = 0.6, linetype = "dotted") +
3047
- geom_rect(data = strike_zone_rect,
3048
  aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
3049
  fill = NA, color = "black", linewidth = 0.8) +
3050
- geom_polygon(data = home_plate, aes(x = x, y = y),
3051
  fill = NA, color = "gray40", linewidth = 0.5) +
3052
  coord_equal() +
3053
  scale_x_continuous(limits = c(-1.8, 1.8)) +
@@ -3066,17 +3103,18 @@ umpire_create_report_pdf <- function(data,
3066
  }
3067
 
3068
  # ---------------------------------------------------------------
3069
- # PLOT HELPERS — now use MissedCalls with CallNo labels
3070
  # ---------------------------------------------------------------
3071
  umpire_create_ball_plot <- function(mc, side_label, title_label) {
3072
  pts <- mc %>%
3073
  filter(BatterSide == side_label,
3074
  PitchCall %in% c("BallCalled", "BallinDirt"),
3075
- PlateLocSide > -0.83083, PlateLocSide < 0.83083,
3076
- PlateLocHeight < 3.37750, PlateLocHeight > 1.5)
3077
  base_zone() +
3078
- geom_point(data = pts, aes(x = PlateLocSide, y = PlateLocHeight),
3079
- pch = 21, fill = "#006F71", color = "black", size = 5) +
 
3080
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3081
  color = "white", size = 2.2, fontface = "bold") +
3082
  ggtitle(title_label)
@@ -3085,11 +3123,12 @@ umpire_create_report_pdf <- function(data,
3085
  umpire_create_strike_plot <- function(mc, side_label, title_label) {
3086
  pts <- mc %>%
3087
  filter(BatterSide == side_label, PitchCall == "StrikeCalled") %>%
3088
- filter(PlateLocSide < -0.83083 | PlateLocSide > 0.83083 |
3089
- PlateLocHeight > 3.37750 | PlateLocHeight < 1.5)
3090
  base_zone() +
3091
- geom_point(data = pts, aes(x = PlateLocSide, y = PlateLocHeight),
3092
- pch = 21, fill = "#006F71", color = "black", size = 5) +
 
3093
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3094
  color = "white", size = 2.2, fontface = "bold") +
3095
  ggtitle(title_label)
@@ -3099,11 +3138,12 @@ umpire_create_report_pdf <- function(data,
3099
  pts <- mc %>%
3100
  filter(if (is_ccu) BatterTeam == "COA_CHA" else BatterTeam != "COA_CHA",
3101
  PitchCall %in% c("BallCalled", "BallinDirt"),
3102
- PlateLocSide > -0.83083, PlateLocSide < 0.83083,
3103
- PlateLocHeight < 3.37750, PlateLocHeight > 1.5)
3104
  base_zone() +
3105
- geom_point(data = pts, aes(x = PlateLocSide, y = PlateLocHeight),
3106
- pch = 21, fill = "#006F71", color = "black", size = 5) +
 
3107
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3108
  color = "white", size = 2.2, fontface = "bold") +
3109
  ggtitle(title_label)
@@ -3113,18 +3153,19 @@ umpire_create_report_pdf <- function(data,
3113
  pts <- mc %>%
3114
  filter(if (is_ccu) BatterTeam == "COA_CHA" else BatterTeam != "COA_CHA",
3115
  PitchCall == "StrikeCalled") %>%
3116
- filter(PlateLocSide < -0.83083 | PlateLocSide > 0.83083 |
3117
- PlateLocHeight > 3.37750 | PlateLocHeight < 1.5)
3118
  base_zone() +
3119
- geom_point(data = pts, aes(x = PlateLocSide, y = PlateLocHeight),
3120
- pch = 21, fill = "#006F71", color = "black", size = 5) +
 
3121
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3122
  color = "white", size = 2.2, fontface = "bold") +
3123
  ggtitle(title_label)
3124
  }
3125
 
3126
  # ---------------------------------------------------------------
3127
- # CREATE ALL PLOTS (pass MissedCalls instead of data)
3128
  # ---------------------------------------------------------------
3129
  plot_ball_lhb <- umpire_create_ball_plot(MissedCalls, "Left", "Ball Called v LHB")
3130
  plot_ball_rhb <- umpire_create_ball_plot(MissedCalls, "Right", "Ball Called v RHB")
@@ -3137,7 +3178,7 @@ umpire_create_report_pdf <- function(data,
3137
  plot_strike_opp <- umpire_create_strike_plot_team(MissedCalls, FALSE, "Strike Called v Opp Hitters")
3138
 
3139
  # ---------------------------------------------------------------
3140
- # PREPARE DISPLAY TABLE (select only display columns, CallNo first)
3141
  # ---------------------------------------------------------------
3142
  MissedCallsDisplay <- MissedCalls %>%
3143
  select(dplyr::any_of(c("CallNo","PitchNo","Inning","Top/Bottom","TopBottom",
@@ -3181,61 +3222,41 @@ umpire_create_report_pdf <- function(data,
3181
  })
3182
 
3183
  draw_logo_url <- function(url, x, just) {
3184
- img <- try(
3185
- magick::image_read(url),
3186
- silent = TRUE
3187
- )
3188
-
3189
  if (inherits(img, "try-error")) return(NULL)
3190
-
3191
  img <- magick::image_resize(img, "x130")
3192
-
3193
  grid.draw(
3194
  grid::rasterGrob(
3195
  as.raster(img),
3196
  interpolate = TRUE,
3197
  vp = viewport(
3198
- x = x,
3199
- y = 0.96,
3200
- width = 0.13,
3201
- height = 0.08,
3202
  just = c(just, "center")
3203
  )
3204
  )
3205
  )
3206
  }
3207
 
3208
- ## --- EMBEDDED LOGO LINKS ---
3209
  left_logo_url <- "https://i.imgur.com/zjTu3JS.png"
3210
  right_logo_url <- "https://i.ibb.co/Q3kFXXd9/8acd1b8a-7920-403a-8e9d-86742634effb.png"
3211
 
3212
  draw_logo_url(left_logo_url, 0.05, "left")
3213
  draw_logo_url(right_logo_url, 0.95, "right")
3214
 
3215
- ## --- HEADER TEXT ---
3216
- grid.text(
3217
- title_text,
3218
- y = 0.975,
3219
- gp = gpar(fontsize = 16, fontface = "bold")
3220
- )
3221
-
3222
- grid.text(
3223
- subhead_text,
3224
- y = 0.948,
3225
- gp = gpar(fontsize = 12, fontface = "bold")
3226
- )
3227
-
3228
  if (!is.na(game_date)) {
3229
- grid.text(
3230
- game_date,
3231
- y = 0.925,
3232
- gp = gpar(fontsize = 9)
3233
- )
3234
  }
3235
  }
3236
 
 
 
 
3237
  grDevices::pdf(output_file, width = 8.5, height = 11)
3238
 
 
3239
  grid.newpage()
3240
  draw_header()
3241
 
@@ -3253,6 +3274,7 @@ umpire_create_report_pdf <- function(data,
3253
  grid.table(buffer_table, rows = NULL, theme = ttheme_green)
3254
  popViewport()
3255
 
 
3256
  grid.newpage()
3257
 
3258
  grid::grid.text("Data: TrackMan | Report Generated: Coastal Carolina Baseball Analytics",
@@ -3264,23 +3286,19 @@ umpire_create_report_pdf <- function(data,
3264
  pushViewport(viewport(x = 0.72, y = 0.60, width = 0.47, height = 0.27)); print(plot_strike_opp, newpage = FALSE); popViewport()
3265
 
3266
  if (nrow(MissedCallsDisplay) > 0) {
3267
- # First page can fit 15 rows in the bottom half
3268
  first_page_rows <- 15
3269
- remaining_page_rows <- rows_per_page # 30 rows per full page
3270
 
3271
  if (nrow(MissedCallsDisplay) <= first_page_rows) {
3272
- # Fits on current page
3273
  pushViewport(viewport(x = 0.5, y = 0.30, width = 0.92, height = 0.50))
3274
  grid.table(MissedCallsDisplay, rows = NULL, theme = ttheme_green_small)
3275
  popViewport()
3276
  } else {
3277
- # First batch on current page
3278
  first_chunk <- MissedCallsDisplay[1:first_page_rows, , drop = FALSE]
3279
  pushViewport(viewport(x = 0.5, y = 0.30, width = 0.92, height = 0.50))
3280
  grid.table(first_chunk, rows = NULL, theme = ttheme_green_small)
3281
  popViewport()
3282
 
3283
- # Remaining rows paginated onto new pages
3284
  remaining <- MissedCallsDisplay[(first_page_rows + 1):nrow(MissedCallsDisplay), , drop = FALSE]
3285
  remaining_chunks <- split(remaining, ceiling(seq_len(nrow(remaining)) / remaining_page_rows))
3286
 
@@ -3299,7 +3317,6 @@ umpire_create_report_pdf <- function(data,
3299
  invisible(output_file)
3300
  }
3301
 
3302
-
3303
  # Advanced Pitcher Functions
3304
  `%||%` <- function(a, b) if (!is.null(a)) a else b
3305
 
 
2942
  invisible(output_file)
2943
  }
2944
 
2945
+ # --- NCAA CONSTANTS (all in feet) ---
2946
+ BALL_RADIUS_FT <- 1.47 / 12 # 0.1225 ft (half of 2.94" diameter)
2947
+ PLATE_HALF_FT <- 8.5 / 12 # 0.70833 ft
2948
+
2949
+ # Raw zone: true plate + ball radius on all sides
2950
+ RAW_SIDE <- PLATE_HALF_FT + BALL_RADIUS_FT # 0.83083 ft
2951
+ RAW_BOTTOM <- (18.00 / 12) - BALL_RADIUS_FT # 1.3775 ft (was 1.5)
2952
+ RAW_TOP <- (40.53 / 12) + BALL_RADIUS_FT # 3.5 ft (was 3.3775)
2953
+
2954
+ # Buffer zone: raw zone + 2" buffer (NCAA document values)
2955
+ BUFFER <- 2.0 / 12 # 0.1667 ft
2956
+ BUFFER_SIDE <- RAW_SIDE + BUFFER # 0.9975 ft
2957
+ BUFFER_BOTTOM <- RAW_BOTTOM - BUFFER # 1.2108 ft (was 1.3775)
2958
+ BUFFER_TOP <- RAW_TOP + BUFFER # 3.6667 ft (was 3.5)
2959
+
2960
+ # Strike zone rectangles for plotting (center-of-ball comparison)
2961
+ STRIKE_ZONE_RECT <- data.frame(xmin = -RAW_SIDE, xmax = RAW_SIDE,
2962
+ ymin = RAW_BOTTOM, ymax = RAW_TOP)
2963
+ BUFFER_ZONE_RECT <- data.frame(xmin = -BUFFER_SIDE, xmax = BUFFER_SIDE,
2964
+ ymin = BUFFER_BOTTOM, ymax = BUFFER_TOP)
2965
+
2966
+ HOME_PLATE <- data.frame(
2967
+ x = c(-0.708, 0.708, 0.708, 0.000, -0.708),
2968
+ y = c( 0.150, 0.150, 0.300, 0.500, 0.300)
2969
+ )
2970
 
 
 
 
2971
 
2972
+ # ============================================================
2973
+ # Data processing (unchanged)
2974
+ # ============================================================
2975
  umpire_process_data <- function(df) {
2976
  df <- df %>%
2977
  filter(PitchCall %in% c("StrikeCalled", "BallCalled", "BallinDirt"))
 
2986
  df
2987
  }
2988
 
2989
+
2990
+ # ============================================================
2991
+ # PDF Report Generator
2992
+ # ============================================================
2993
  umpire_create_report_pdf <- function(data,
2994
  output_file,
2995
  left_logo_path = NULL,
 
2999
  rows_per_page = 30) {
3000
 
3001
  suppressPackageStartupMessages({
3002
+ library(dplyr); library(grid); library(gridExtra)
3003
+ library(ggplot2); library(stringr); library(ggforce)
3004
  })
3005
  `%||%` <- function(a, b) if (!is.null(a)) a else b
3006
 
3007
+ # ---------------------------------------------------------------
3008
+ # RAW SCORE — uses raw zone (plate + ball radius, all 4 sides)
3009
+ # "Does any edge of the ball touch the true strike zone?"
3010
+ # ---------------------------------------------------------------
3011
  raw_strike_miss <- data %>%
3012
  filter(PitchCall == "StrikeCalled") %>%
3013
+ filter(PlateLocSide < -RAW_SIDE | PlateLocSide > RAW_SIDE |
3014
+ PlateLocHeight > RAW_TOP | PlateLocHeight < RAW_BOTTOM) %>%
3015
+ nrow()
3016
 
3017
  raw_ball_miss <- data %>%
3018
  filter(PitchCall %in% c("BallCalled", "BallinDirt")) %>%
3019
+ filter(PlateLocSide > -RAW_SIDE & PlateLocSide < RAW_SIDE &
3020
+ PlateLocHeight < RAW_TOP & PlateLocHeight > RAW_BOTTOM) %>%
3021
+ nrow()
3022
 
3023
  total_called <- nrow(data)
3024
  total_missed <- raw_strike_miss + raw_ball_miss
3025
  correct <- total_called - total_missed
3026
  overall_pct <- paste0(sprintf("%.0f", 100 * (correct / total_called)), "%")
3027
 
3028
+ # ---------------------------------------------------------------
3029
+ # ADJUSTED / BUFFER SCORE — raw zone + 2" buffer on all sides
3030
+ # ---------------------------------------------------------------
3031
  buffer_strike_miss <- data %>%
3032
  filter(PitchCall == "StrikeCalled") %>%
3033
+ filter(PlateLocSide < -BUFFER_SIDE | PlateLocSide > BUFFER_SIDE |
3034
+ PlateLocHeight > BUFFER_TOP | PlateLocHeight < BUFFER_BOTTOM) %>%
3035
+ nrow()
3036
 
3037
+ buffer_ball_miss <- data %>%
3038
+ filter(PitchCall %in% c("BallCalled", "BallinDirt")) %>%
3039
+ filter(PlateLocSide > -RAW_SIDE & PlateLocSide < RAW_SIDE &
3040
+ PlateLocHeight < RAW_TOP & PlateLocHeight > RAW_BOTTOM) %>%
3041
+ nrow()
3042
+
3043
+ buffer_total_missed <- buffer_ball_miss + buffer_strike_miss
3044
  buffer_correct <- total_called - buffer_total_missed
3045
  buffer_pct <- paste0(sprintf("%.0f", 100 * (buffer_correct / total_called)), "%")
3046
 
 
3053
  paste("Umpire Report —", umpire_name)
3054
  } else "Umpire Report"
3055
 
 
 
 
 
 
 
 
 
3056
  # ---------------------------------------------------------------
3057
+ # BUILD MISSED CALLS TABLE
3058
  # ---------------------------------------------------------------
3059
  MissedCalls <- dplyr::bind_rows(
3060
+ # Balls called inside the raw zone (should have been strikes)
3061
  data %>% filter(PitchCall %in% c("BallCalled", "BallinDirt"),
3062
+ PlateLocSide > -RAW_SIDE, PlateLocSide < RAW_SIDE,
3063
+ PlateLocHeight < RAW_TOP, PlateLocHeight > RAW_BOTTOM),
3064
+ # Strikes called outside the raw zone (should have been balls)
3065
  data %>% filter(PitchCall == "StrikeCalled") %>%
3066
+ filter(PlateLocSide < -RAW_SIDE | PlateLocSide > RAW_SIDE |
3067
+ PlateLocHeight > RAW_TOP | PlateLocHeight < RAW_BOTTOM)
3068
  ) %>%
3069
  arrange(PitchNo) %>%
3070
  mutate(CallNo = row_number()) %>%
 
3073
  Height = paste0(sprintf("%.0f", PlateLocHeight * 12), '"')
3074
  )
3075
 
 
 
 
3076
  # ---------------------------------------------------------------
3077
+ # BASE ZONE HELPER — draws both raw and buffer rectangles
3078
  # ---------------------------------------------------------------
3079
  base_zone <- function() {
3080
  ggplot() +
3081
+ geom_rect(data = BUFFER_ZONE_RECT,
3082
  aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
3083
  fill = NA, color = "gray50", linewidth = 0.6, linetype = "dotted") +
3084
+ geom_rect(data = STRIKE_ZONE_RECT,
3085
  aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
3086
  fill = NA, color = "black", linewidth = 0.8) +
3087
+ geom_polygon(data = HOME_PLATE, aes(x = x, y = y),
3088
  fill = NA, color = "gray40", linewidth = 0.5) +
3089
  coord_equal() +
3090
  scale_x_continuous(limits = c(-1.8, 1.8)) +
 
3103
  }
3104
 
3105
  # ---------------------------------------------------------------
3106
+ # PLOT HELPERS — ggforce::geom_circle for to-scale ball rendering
3107
  # ---------------------------------------------------------------
3108
  umpire_create_ball_plot <- function(mc, side_label, title_label) {
3109
  pts <- mc %>%
3110
  filter(BatterSide == side_label,
3111
  PitchCall %in% c("BallCalled", "BallinDirt"),
3112
+ PlateLocSide > -RAW_SIDE, PlateLocSide < RAW_SIDE,
3113
+ PlateLocHeight < RAW_TOP, PlateLocHeight > RAW_BOTTOM)
3114
  base_zone() +
3115
+ ggforce::geom_circle(data = pts,
3116
+ aes(x0 = PlateLocSide, y0 = PlateLocHeight, r = BALL_RADIUS_FT),
3117
+ fill = "#006F71", color = "black", linewidth = 0.3, alpha = 0.85) +
3118
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3119
  color = "white", size = 2.2, fontface = "bold") +
3120
  ggtitle(title_label)
 
3123
  umpire_create_strike_plot <- function(mc, side_label, title_label) {
3124
  pts <- mc %>%
3125
  filter(BatterSide == side_label, PitchCall == "StrikeCalled") %>%
3126
+ filter(PlateLocSide < -RAW_SIDE | PlateLocSide > RAW_SIDE |
3127
+ PlateLocHeight > RAW_TOP | PlateLocHeight < RAW_BOTTOM)
3128
  base_zone() +
3129
+ ggforce::geom_circle(data = pts,
3130
+ aes(x0 = PlateLocSide, y0 = PlateLocHeight, r = BALL_RADIUS_FT),
3131
+ fill = "#006F71", color = "black", linewidth = 0.3, alpha = 0.85) +
3132
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3133
  color = "white", size = 2.2, fontface = "bold") +
3134
  ggtitle(title_label)
 
3138
  pts <- mc %>%
3139
  filter(if (is_ccu) BatterTeam == "COA_CHA" else BatterTeam != "COA_CHA",
3140
  PitchCall %in% c("BallCalled", "BallinDirt"),
3141
+ PlateLocSide > -RAW_SIDE, PlateLocSide < RAW_SIDE,
3142
+ PlateLocHeight < RAW_TOP, PlateLocHeight > RAW_BOTTOM)
3143
  base_zone() +
3144
+ ggforce::geom_circle(data = pts,
3145
+ aes(x0 = PlateLocSide, y0 = PlateLocHeight, r = BALL_RADIUS_FT),
3146
+ fill = "#006F71", color = "black", linewidth = 0.3, alpha = 0.85) +
3147
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3148
  color = "white", size = 2.2, fontface = "bold") +
3149
  ggtitle(title_label)
 
3153
  pts <- mc %>%
3154
  filter(if (is_ccu) BatterTeam == "COA_CHA" else BatterTeam != "COA_CHA",
3155
  PitchCall == "StrikeCalled") %>%
3156
+ filter(PlateLocSide < -RAW_SIDE | PlateLocSide > RAW_SIDE |
3157
+ PlateLocHeight > RAW_TOP | PlateLocHeight < RAW_BOTTOM)
3158
  base_zone() +
3159
+ ggforce::geom_circle(data = pts,
3160
+ aes(x0 = PlateLocSide, y0 = PlateLocHeight, r = BALL_RADIUS_FT),
3161
+ fill = "#006F71", color = "black", linewidth = 0.3, alpha = 0.85) +
3162
  geom_text(data = pts, aes(x = PlateLocSide, y = PlateLocHeight, label = CallNo),
3163
  color = "white", size = 2.2, fontface = "bold") +
3164
  ggtitle(title_label)
3165
  }
3166
 
3167
  # ---------------------------------------------------------------
3168
+ # CREATE ALL PLOTS
3169
  # ---------------------------------------------------------------
3170
  plot_ball_lhb <- umpire_create_ball_plot(MissedCalls, "Left", "Ball Called v LHB")
3171
  plot_ball_rhb <- umpire_create_ball_plot(MissedCalls, "Right", "Ball Called v RHB")
 
3178
  plot_strike_opp <- umpire_create_strike_plot_team(MissedCalls, FALSE, "Strike Called v Opp Hitters")
3179
 
3180
  # ---------------------------------------------------------------
3181
+ # PREPARE DISPLAY TABLE
3182
  # ---------------------------------------------------------------
3183
  MissedCallsDisplay <- MissedCalls %>%
3184
  select(dplyr::any_of(c("CallNo","PitchNo","Inning","Top/Bottom","TopBottom",
 
3222
  })
3223
 
3224
  draw_logo_url <- function(url, x, just) {
3225
+ img <- try(magick::image_read(url), silent = TRUE)
 
 
 
 
3226
  if (inherits(img, "try-error")) return(NULL)
 
3227
  img <- magick::image_resize(img, "x130")
 
3228
  grid.draw(
3229
  grid::rasterGrob(
3230
  as.raster(img),
3231
  interpolate = TRUE,
3232
  vp = viewport(
3233
+ x = x, y = 0.96,
3234
+ width = 0.13, height = 0.08,
 
 
3235
  just = c(just, "center")
3236
  )
3237
  )
3238
  )
3239
  }
3240
 
 
3241
  left_logo_url <- "https://i.imgur.com/zjTu3JS.png"
3242
  right_logo_url <- "https://i.ibb.co/Q3kFXXd9/8acd1b8a-7920-403a-8e9d-86742634effb.png"
3243
 
3244
  draw_logo_url(left_logo_url, 0.05, "left")
3245
  draw_logo_url(right_logo_url, 0.95, "right")
3246
 
3247
+ grid.text(title_text, y = 0.975, gp = gpar(fontsize = 16, fontface = "bold"))
3248
+ grid.text(subhead_text, y = 0.948, gp = gpar(fontsize = 12, fontface = "bold"))
 
 
 
 
 
 
 
 
 
 
 
3249
  if (!is.na(game_date)) {
3250
+ grid.text(game_date, y = 0.925, gp = gpar(fontsize = 9))
 
 
 
 
3251
  }
3252
  }
3253
 
3254
+ # ---------------------------------------------------------------
3255
+ # RENDER PDF
3256
+ # ---------------------------------------------------------------
3257
  grDevices::pdf(output_file, width = 8.5, height = 11)
3258
 
3259
+ # --- PAGE 1: Header + Raw Score + LHB/RHB plots + Adjusted Score ---
3260
  grid.newpage()
3261
  draw_header()
3262
 
 
3274
  grid.table(buffer_table, rows = NULL, theme = ttheme_green)
3275
  popViewport()
3276
 
3277
+ # --- PAGE 2: Team plots + Missed Calls table ---
3278
  grid.newpage()
3279
 
3280
  grid::grid.text("Data: TrackMan | Report Generated: Coastal Carolina Baseball Analytics",
 
3286
  pushViewport(viewport(x = 0.72, y = 0.60, width = 0.47, height = 0.27)); print(plot_strike_opp, newpage = FALSE); popViewport()
3287
 
3288
  if (nrow(MissedCallsDisplay) > 0) {
 
3289
  first_page_rows <- 15
3290
+ remaining_page_rows <- rows_per_page
3291
 
3292
  if (nrow(MissedCallsDisplay) <= first_page_rows) {
 
3293
  pushViewport(viewport(x = 0.5, y = 0.30, width = 0.92, height = 0.50))
3294
  grid.table(MissedCallsDisplay, rows = NULL, theme = ttheme_green_small)
3295
  popViewport()
3296
  } else {
 
3297
  first_chunk <- MissedCallsDisplay[1:first_page_rows, , drop = FALSE]
3298
  pushViewport(viewport(x = 0.5, y = 0.30, width = 0.92, height = 0.50))
3299
  grid.table(first_chunk, rows = NULL, theme = ttheme_green_small)
3300
  popViewport()
3301
 
 
3302
  remaining <- MissedCallsDisplay[(first_page_rows + 1):nrow(MissedCallsDisplay), , drop = FALSE]
3303
  remaining_chunks <- split(remaining, ceiling(seq_len(nrow(remaining)) / remaining_page_rows))
3304
 
 
3317
  invisible(output_file)
3318
  }
3319
 
 
3320
  # Advanced Pitcher Functions
3321
  `%||%` <- function(a, b) if (!is.null(a)) a else b
3322