Spaces:
Running
Running
Update app.R
Browse files
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)
|
|
|
|
| 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 < -
|
| 2980 |
-
PlateLocHeight >
|
|
|
|
| 2981 |
|
| 2982 |
raw_ball_miss <- data %>%
|
| 2983 |
filter(PitchCall %in% c("BallCalled", "BallinDirt")) %>%
|
| 2984 |
-
filter(PlateLocSide > -
|
| 2985 |
-
PlateLocHeight <
|
|
|
|
| 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 < -
|
| 2995 |
-
PlateLocHeight >
|
|
|
|
| 2996 |
|
| 2997 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 3020 |
# ---------------------------------------------------------------
|
| 3021 |
MissedCalls <- dplyr::bind_rows(
|
|
|
|
| 3022 |
data %>% filter(PitchCall %in% c("BallCalled", "BallinDirt"),
|
| 3023 |
-
PlateLocSide > -
|
| 3024 |
-
PlateLocHeight <
|
|
|
|
| 3025 |
data %>% filter(PitchCall == "StrikeCalled") %>%
|
| 3026 |
-
filter(PlateLocSide < -
|
| 3027 |
-
PlateLocHeight >
|
| 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 =
|
| 3045 |
aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
|
| 3046 |
fill = NA, color = "gray50", linewidth = 0.6, linetype = "dotted") +
|
| 3047 |
-
geom_rect(data =
|
| 3048 |
aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax),
|
| 3049 |
fill = NA, color = "black", linewidth = 0.8) +
|
| 3050 |
-
geom_polygon(data =
|
| 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 —
|
| 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 > -
|
| 3076 |
-
PlateLocHeight <
|
| 3077 |
base_zone() +
|
| 3078 |
-
|
| 3079 |
-
|
|
|
|
| 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 < -
|
| 3089 |
-
PlateLocHeight >
|
| 3090 |
base_zone() +
|
| 3091 |
-
|
| 3092 |
-
|
|
|
|
| 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 > -
|
| 3103 |
-
PlateLocHeight <
|
| 3104 |
base_zone() +
|
| 3105 |
-
|
| 3106 |
-
|
|
|
|
| 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 < -
|
| 3117 |
-
PlateLocHeight >
|
| 3118 |
base_zone() +
|
| 3119 |
-
|
| 3120 |
-
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
|