Spaces:
Running
Running
Update app.R
Browse files
app.R
CHANGED
|
@@ -2316,6 +2316,7 @@ create_advanced_pitcher_summary <- function(data, player_name) {
|
|
| 2316 |
list(stats = summary_stats, colors = colors)
|
| 2317 |
}
|
| 2318 |
|
|
|
|
| 2319 |
create_advanced_pitch_characteristics <- function(data, player_name) {
|
| 2320 |
data <- normalize_columns(data)
|
| 2321 |
|
|
@@ -2329,7 +2330,6 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2329 |
} else "Right"
|
| 2330 |
|
| 2331 |
if (!is.null(stuffplus_model) && nrow(pitcher_data) > 0) {
|
| 2332 |
-
|
| 2333 |
# If reference data exists, use the league-standardization path
|
| 2334 |
if (!is.null(reference_data_for_stuff)) {
|
| 2335 |
combined <- dplyr::bind_rows(
|
|
@@ -2338,11 +2338,8 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2338 |
reference_data_for_stuff$sbc
|
| 2339 |
)
|
| 2340 |
pitcher_data <- standardize_stuffplus_to_league(pitcher_data, combined)
|
| 2341 |
-
|
| 2342 |
} else {
|
| 2343 |
-
# Reference data not available → fall back to local standardization using the model
|
| 2344 |
message("WARNING: No reference data available. Using local standardization.")
|
| 2345 |
-
|
| 2346 |
pitcher_data$raw_stuff <- tryCatch({
|
| 2347 |
predict(stuffplus_model, pitcher_data, type = "numeric")$.pred
|
| 2348 |
}, error = function(e) {
|
|
@@ -2357,63 +2354,80 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2357 |
q <- quantile(finite_raw, probs = c(0.01, 0.99), na.rm = TRUE)
|
| 2358 |
lo <- q[1]; hi <- q[2]
|
| 2359 |
pitcher_data$raw_stuff_winz <- pmin(pmax(pitcher_data$raw_stuff, lo), hi)
|
| 2360 |
-
|
| 2361 |
raw_mean <- mean(pitcher_data$raw_stuff_winz, na.rm = TRUE)
|
| 2362 |
raw_sd <- sd(pitcher_data$raw_stuff_winz, na.rm = TRUE)
|
| 2363 |
if (!is.finite(raw_sd) || raw_sd == 0) raw_sd <- 1e-8
|
| 2364 |
-
|
| 2365 |
pitcher_data$stuff_plus <- ((pitcher_data$raw_stuff_winz - raw_mean) / raw_sd) * 10 + 100
|
| 2366 |
}
|
| 2367 |
}
|
| 2368 |
-
|
| 2369 |
} else {
|
| 2370 |
-
# Either no model or no pitcher data
|
| 2371 |
if (is.null(stuffplus_model)) message("Stuff+ model not loaded")
|
| 2372 |
if (nrow(pitcher_data) == 0) message("No pitcher data for Stuff+ prediction")
|
| 2373 |
-
|
| 2374 |
pitcher_data$raw_stuff <- NA_real_
|
| 2375 |
pitcher_data$stuff_plus <- NA_real_
|
| 2376 |
}
|
| 2377 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2378 |
|
| 2379 |
pitch_stats <- pitcher_data %>%
|
| 2380 |
dplyr::group_by(Pitch = TaggedPitchType) %>%
|
| 2381 |
dplyr::summarise(
|
| 2382 |
Count = dplyr::n(),
|
| 2383 |
-
`Usage%` = 100 * dplyr::n() / nrow(pitcher_data),
|
| 2384 |
-
`Avg Velo` = round(mean(RelSpeed,
|
| 2385 |
-
`Max Velo` = round(max(RelSpeed,
|
| 2386 |
-
`Avg Spin` = round(mean(SpinRate,
|
| 2387 |
-
`Avg IVB` = round(mean(InducedVertBreak,
|
| 2388 |
-
`Avg HB` = round(mean(HorzBreak,
|
| 2389 |
-
`VAA` = round(mean(VertApprAngle,
|
| 2390 |
-
`HAA` = round(mean(HorzApprAngle,
|
| 2391 |
-
`hRel` = round(mean(RelSide,
|
| 2392 |
-
`vRel` = round(mean(RelHeight,
|
| 2393 |
-
`Ext` = round(mean(Extension,
|
| 2394 |
-
`Strike%` = 100 * sum(!PitchCall %in% c("BallCalled","BallinDirt","BallIntentional")) / dplyr::n(),
|
| 2395 |
`Whiff%` = ifelse(sum(SwingIndicator, na.rm = TRUE) > 0,
|
| 2396 |
-
100 * sum(WhiffIndicator, na.rm = TRUE) / sum(SwingIndicator, na.rm = TRUE), 0),
|
| 2397 |
-
`Zone%` = 100 * sum(StrikeZoneIndicator, na.rm = TRUE) / dplyr::n(),
|
| 2398 |
`Stuff+` = round(mean(stuff_plus, na.rm = TRUE), 1),
|
| 2399 |
.groups = "drop"
|
| 2400 |
) %>%
|
| 2401 |
dplyr::arrange(dplyr::desc(`Usage%`))
|
| 2402 |
|
| 2403 |
-
#
|
| 2404 |
dup_mask <- duplicated(names(pitch_stats))
|
| 2405 |
if (any(dup_mask)) {
|
| 2406 |
message("Removing ", sum(dup_mask), " duplicate columns from pitch_stats")
|
| 2407 |
pitch_stats <- pitch_stats[, !dup_mask, drop = FALSE]
|
| 2408 |
}
|
| 2409 |
|
| 2410 |
-
# NOW
|
| 2411 |
-
num_cols <- ncol(pitch_stats)
|
| 2412 |
num_rows <- nrow(pitch_stats)
|
|
|
|
| 2413 |
color_matrix <- matrix("#FFFFFF", nrow = max(1, num_rows), ncol = max(1, num_cols))
|
| 2414 |
|
| 2415 |
-
|
| 2416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2417 |
c_avg_velo <- col_idx("Avg Velo")
|
| 2418 |
c_max_velo <- col_idx("Max Velo")
|
| 2419 |
c_spin <- col_idx("Avg Spin")
|
|
@@ -2423,6 +2437,11 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2423 |
c_stuff <- col_idx("Stuff+")
|
| 2424 |
c_ext <- col_idx("Ext")
|
| 2425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2426 |
for (i in seq_len(num_rows)) {
|
| 2427 |
pt <- pitch_stats$Pitch[i]
|
| 2428 |
|
|
@@ -2447,27 +2466,27 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2447 |
vb_r <- sec_ref$velo_r %||% NA_real_
|
| 2448 |
velo_bench <- if (identical(pitcher_hand, "Left")) vb_l else vb_r
|
| 2449 |
}
|
| 2450 |
-
if (!is.na(velo_bench) &&
|
| 2451 |
color_matrix[i, c_avg_velo] <- get_gradient_color(pitch_stats$`Avg Velo`[i], velo_bench, "higher_better", 0.05)
|
| 2452 |
-
if (!is.na(velo_bench) &&
|
| 2453 |
color_matrix[i, c_max_velo] <- get_gradient_color(pitch_stats$`Max Velo`[i], velo_bench, "higher_better", 0.05)
|
| 2454 |
}
|
| 2455 |
|
| 2456 |
-
if (!is.null(sec_ref) &&
|
| 2457 |
-
color_matrix[i, c_spin] <- get_gradient_color(pitch_stats$`Avg Spin`[i],
|
| 2458 |
-
if (!is.null(sec_ref) &&
|
| 2459 |
-
color_matrix[i, c_strk] <- get_gradient_color(pitch_stats$`Strike%`[i],
|
| 2460 |
-
if (!is.null(sec_ref) &&
|
| 2461 |
-
color_matrix[i, c_whiff] <- get_gradient_color(pitch_stats$`Whiff%`[i],
|
| 2462 |
-
if (!is.null(sec_ref) &&
|
| 2463 |
-
color_matrix[i, c_zone] <- get_gradient_color(pitch_stats$`Zone%`[i],
|
| 2464 |
|
| 2465 |
-
if (
|
| 2466 |
val <- pitch_stats$`Stuff+`[i]
|
| 2467 |
if (is.finite(val)) color_matrix[i, c_stuff] <- get_gradient_color(val, 100, "higher_better", 0.20)
|
| 2468 |
}
|
| 2469 |
|
| 2470 |
-
if (
|
| 2471 |
ext_bench <- sec_extension_benchmark(pt)
|
| 2472 |
if (is.finite(ext_bench)) {
|
| 2473 |
color_matrix[i, c_ext] <- get_gradient_color(pitch_stats$`Ext`[i], ext_bench, "higher_better", 0.08)
|
|
@@ -2477,7 +2496,6 @@ create_advanced_pitch_characteristics <- function(data, player_name) {
|
|
| 2477 |
|
| 2478 |
list(stats = pitch_stats, colors = color_matrix)
|
| 2479 |
}
|
| 2480 |
-
|
| 2481 |
|
| 2482 |
create_movement_plot <- create_pitcher_movement_plot
|
| 2483 |
|
|
@@ -2732,31 +2750,63 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2732 |
|
| 2733 |
.text_on_fill <- function(hex) {
|
| 2734 |
if (is.na(hex) || !nzchar(hex)) return("black")
|
| 2735 |
-
|
| 2736 |
-
|
| 2737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2738 |
}
|
| 2739 |
|
| 2740 |
pitcher_df <- dplyr::filter(game_df, Pitcher == pitcher_name)
|
| 2741 |
if (nrow(pitcher_df) == 0) {
|
| 2742 |
pdf(output_file, width = 11, height = 14)
|
| 2743 |
grid::grid.newpage()
|
| 2744 |
-
grid::grid.text("No data available for
|
| 2745 |
gp = grid::gpar(fontsize = 16, fontface = "bold"))
|
| 2746 |
dev.off()
|
| 2747 |
return(output_file)
|
| 2748 |
}
|
| 2749 |
|
| 2750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2751 |
summary_stats <- summary_result$stats
|
| 2752 |
summary_colors <- summary_result$colors
|
| 2753 |
|
| 2754 |
-
|
| 2755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2756 |
pitch_colors_matrix <- pitch_result$colors
|
| 2757 |
|
| 2758 |
-
|
| 2759 |
-
|
|
|
|
| 2760 |
Pitch = "-", Count = 0, `Usage%` = NA_real_,
|
| 2761 |
`Avg Velo` = NA_real_, `Max Velo` = NA_real_,
|
| 2762 |
`Avg Spin` = NA_real_,
|
|
@@ -2765,58 +2815,92 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2765 |
`hRel` = NA_real_, `vRel` = NA_real_, `Ext` = NA_real_,
|
| 2766 |
`Strike%` = NA_real_, `Whiff%` = NA_real_,
|
| 2767 |
`Zone%` = NA_real_,
|
| 2768 |
-
`Stuff+` = NA_real_
|
|
|
|
| 2769 |
)
|
|
|
|
| 2770 |
}
|
| 2771 |
|
|
|
|
| 2772 |
max_rows_to_show <- min(nrow(pitch_char), 9)
|
| 2773 |
if (nrow(pitch_char) > max_rows_to_show) {
|
| 2774 |
-
pitch_char <- pitch_char
|
| 2775 |
}
|
| 2776 |
-
pitch_colors_matrix <- sync_color_matrix_to_df(pitch_colors_matrix, pitch_char, fill = "#FFFFFF")
|
| 2777 |
|
| 2778 |
-
|
| 2779 |
-
|
| 2780 |
-
|
| 2781 |
-
location_rhb <- create_location_by_result_plot(pitcher_df, pitcher_name, "Right", pitch_colors)
|
| 2782 |
-
count_plot <- create_count_usage_plot(pitcher_df, pitcher_name, pitch_colors)
|
| 2783 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2784 |
pdf(output_file, width = 11, height = 14)
|
| 2785 |
on.exit(try(dev.off(), silent = TRUE), add = TRUE)
|
| 2786 |
grid::grid.newpage()
|
| 2787 |
|
| 2788 |
-
|
| 2789 |
-
|
| 2790 |
charts_y_top <- 0.85
|
| 2791 |
charts_height <- 0.30
|
| 2792 |
charts_y_bottom <- charts_y_top - charts_height
|
| 2793 |
-
|
| 2794 |
-
|
| 2795 |
-
|
| 2796 |
-
|
| 2797 |
-
|
| 2798 |
-
|
| 2799 |
-
|
| 2800 |
-
min_row_h <- 0.0125
|
| 2801 |
-
max_row_h <- 0.0180
|
| 2802 |
|
| 2803 |
y_top_char_orig <- count_y_bottom - 0.011
|
| 2804 |
-
rows_including_header <-
|
| 2805 |
available_for_table_orig <- y_top_char_orig - (base_loc_top + table_margin)
|
| 2806 |
-
row_h_char <- min(max_row_h, max(min_row_h, available_for_table_orig / rows_including_header))
|
| 2807 |
y_loc_top <- y_top_char_orig - rows_including_header * row_h_char - (table_margin * 0.5)
|
| 2808 |
|
| 2809 |
table_lower_offset <- 0.025
|
| 2810 |
y_top_char <- y_top_char_orig - table_lower_offset
|
| 2811 |
-
|
| 2812 |
available_for_table <- y_top_char - (base_loc_top + table_margin)
|
| 2813 |
-
row_h_char <- min(max_row_h, max(min_row_h, available_for_table / rows_including_header))
|
| 2814 |
|
|
|
|
| 2815 |
grid::pushViewport(grid::viewport(x = 0.5, y = header_y_top, width = 1, height = 0.04, just = c("center","top")))
|
| 2816 |
grid::grid.text(paste(pitcher_name, "- Advanced Pitcher Report"),
|
| 2817 |
gp = grid::gpar(fontface = "bold", cex = 1.8, col = "#006F71"))
|
| 2818 |
grid::popViewport()
|
| 2819 |
|
|
|
|
| 2820 |
grid::grid.text("Summary", x = 0.5, y = 0.94,
|
| 2821 |
gp = grid::gpar(fontface = "bold", cex = 1.1, col = "#006F71"))
|
| 2822 |
|
|
@@ -2844,29 +2928,44 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2844 |
gp = grid::gpar(cex = 0.62))
|
| 2845 |
}
|
| 2846 |
|
|
|
|
| 2847 |
grid::pushViewport(grid::viewport(x = 0.25, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
|
| 2848 |
-
print(movement_plot, newpage = FALSE)
|
| 2849 |
grid::popViewport()
|
| 2850 |
|
|
|
|
| 2851 |
grid::pushViewport(grid::viewport(x = 0.75, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
|
| 2852 |
-
print(velo_plot, newpage = FALSE)
|
| 2853 |
grid::popViewport()
|
| 2854 |
|
|
|
|
| 2855 |
grid::pushViewport(grid::viewport(x = 0.5, y = count_y_top, width = 0.92, height = count_height, just = c("center","top")))
|
| 2856 |
-
print(count_plot, newpage = FALSE)
|
| 2857 |
grid::popViewport()
|
| 2858 |
|
|
|
|
| 2859 |
grid::grid.text("Pitch Characteristics", x = 0.5, y = y_top_char + 0.015,
|
| 2860 |
gp = grid::gpar(fontface = "bold", cex = 1.1, col = "#006F71"))
|
| 2861 |
|
| 2862 |
char_headers <- names(pitch_char)
|
| 2863 |
num_char_cols <- length(char_headers)
|
| 2864 |
|
| 2865 |
-
|
| 2866 |
-
|
| 2867 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2868 |
|
|
|
|
| 2869 |
for (i in seq_along(char_headers)) {
|
|
|
|
| 2870 |
grid::grid.rect(x = x_pos_char[i], y = y_top_char, width = char_widths[i]*0.985, height = row_h_char,
|
| 2871 |
just = c("left","top"), gp = grid::gpar(fill = "#006F71", col = "black", lwd = 0.5))
|
| 2872 |
grid::grid.text(char_headers[i],
|
|
@@ -2874,34 +2973,38 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2874 |
gp = grid::gpar(col = "white", cex = 0.50, fontface = "bold"))
|
| 2875 |
}
|
| 2876 |
|
| 2877 |
-
|
| 2878 |
-
if (!nzchar(colname) || is.null(df) || !is.data.frame(df)) return(NA)
|
| 2879 |
-
if (!(colname %in% names(df))) return(NA)
|
| 2880 |
-
col <- df[[colname]]
|
| 2881 |
-
if (length(col) < r || r < 1) return(NA)
|
| 2882 |
-
col[r]
|
| 2883 |
-
}
|
| 2884 |
-
|
| 2885 |
i_col_pitch <- match("Pitch", char_headers)
|
| 2886 |
has_pitchcol <- !is.na(i_col_pitch) && i_col_pitch >= 1
|
| 2887 |
|
| 2888 |
-
|
|
|
|
| 2889 |
y_row <- y_top_char - r * row_h_char
|
| 2890 |
-
pitch_name <- if (has_pitchcol) as.character(
|
| 2891 |
|
| 2892 |
for (i in seq_along(char_headers)) {
|
|
|
|
|
|
|
|
|
|
| 2893 |
colname <- char_headers[i]
|
| 2894 |
|
| 2895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2896 |
|
| 2897 |
-
|
|
|
|
| 2898 |
bg <- pitch_colors[[pitch_name]]
|
| 2899 |
}
|
| 2900 |
|
| 2901 |
grid::grid.rect(x = x_pos_char[i], y = y_row, width = char_widths[i]*0.985, height = row_h_char,
|
| 2902 |
just = c("left","top"), gp = grid::gpar(fill = bg, col = "grey80", lwd = 0.3))
|
| 2903 |
|
| 2904 |
-
|
|
|
|
| 2905 |
|
| 2906 |
display_val <- if (is.numeric(val)) {
|
| 2907 |
if (is.na(val) || is.nan(val) || is.infinite(val)) "-" else sprintf("%.1f", val)
|
|
@@ -2919,14 +3022,16 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2919 |
}
|
| 2920 |
}
|
| 2921 |
|
|
|
|
| 2922 |
grid::pushViewport(grid::viewport(x = 0.25, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
|
| 2923 |
-
print(location_lhb, newpage = FALSE)
|
| 2924 |
grid::popViewport()
|
| 2925 |
|
| 2926 |
grid::pushViewport(grid::viewport(x = 0.75, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
|
| 2927 |
-
print(location_rhb, newpage = FALSE)
|
| 2928 |
grid::popViewport()
|
| 2929 |
|
|
|
|
| 2930 |
grid::grid.text("Red: Below SEC Avg, White: Near SEC Avg, Green: Above SEC Avg | Stuff+ predicts pitch effectiveness; 100 is average, 10 = one SD",
|
| 2931 |
x = 0.5, y = 0.04, gp = grid::gpar(cex = 0.82, col = "grey40", fontface = "italic"))
|
| 2932 |
grid::grid.text("Data: TrackMan | Report Generated: Coastal Carolina Baseball",
|
|
@@ -2935,6 +3040,7 @@ create_advanced_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
|
|
| 2935 |
invisible(output_file)
|
| 2936 |
}
|
| 2937 |
|
|
|
|
| 2938 |
# =====================================================================
|
| 2939 |
# =========================== UI ================================
|
| 2940 |
# =====================================================================
|
|
|
|
| 2316 |
list(stats = summary_stats, colors = colors)
|
| 2317 |
}
|
| 2318 |
|
| 2319 |
+
|
| 2320 |
create_advanced_pitch_characteristics <- function(data, player_name) {
|
| 2321 |
data <- normalize_columns(data)
|
| 2322 |
|
|
|
|
| 2330 |
} else "Right"
|
| 2331 |
|
| 2332 |
if (!is.null(stuffplus_model) && nrow(pitcher_data) > 0) {
|
|
|
|
| 2333 |
# If reference data exists, use the league-standardization path
|
| 2334 |
if (!is.null(reference_data_for_stuff)) {
|
| 2335 |
combined <- dplyr::bind_rows(
|
|
|
|
| 2338 |
reference_data_for_stuff$sbc
|
| 2339 |
)
|
| 2340 |
pitcher_data <- standardize_stuffplus_to_league(pitcher_data, combined)
|
|
|
|
| 2341 |
} else {
|
|
|
|
| 2342 |
message("WARNING: No reference data available. Using local standardization.")
|
|
|
|
| 2343 |
pitcher_data$raw_stuff <- tryCatch({
|
| 2344 |
predict(stuffplus_model, pitcher_data, type = "numeric")$.pred
|
| 2345 |
}, error = function(e) {
|
|
|
|
| 2354 |
q <- quantile(finite_raw, probs = c(0.01, 0.99), na.rm = TRUE)
|
| 2355 |
lo <- q[1]; hi <- q[2]
|
| 2356 |
pitcher_data$raw_stuff_winz <- pmin(pmax(pitcher_data$raw_stuff, lo), hi)
|
|
|
|
| 2357 |
raw_mean <- mean(pitcher_data$raw_stuff_winz, na.rm = TRUE)
|
| 2358 |
raw_sd <- sd(pitcher_data$raw_stuff_winz, na.rm = TRUE)
|
| 2359 |
if (!is.finite(raw_sd) || raw_sd == 0) raw_sd <- 1e-8
|
|
|
|
| 2360 |
pitcher_data$stuff_plus <- ((pitcher_data$raw_stuff_winz - raw_mean) / raw_sd) * 10 + 100
|
| 2361 |
}
|
| 2362 |
}
|
|
|
|
| 2363 |
} else {
|
|
|
|
| 2364 |
if (is.null(stuffplus_model)) message("Stuff+ model not loaded")
|
| 2365 |
if (nrow(pitcher_data) == 0) message("No pitcher data for Stuff+ prediction")
|
|
|
|
| 2366 |
pitcher_data$raw_stuff <- NA_real_
|
| 2367 |
pitcher_data$stuff_plus <- NA_real_
|
| 2368 |
}
|
| 2369 |
|
| 2370 |
+
# Handle case where pitcher_data is empty after filtering
|
| 2371 |
+
if (nrow(pitcher_data) == 0) {
|
| 2372 |
+
empty_df <- data.frame(
|
| 2373 |
+
Pitch = character(0), Count = integer(0), `Usage%` = numeric(0),
|
| 2374 |
+
`Avg Velo` = numeric(0), `Max Velo` = numeric(0), `Avg Spin` = numeric(0),
|
| 2375 |
+
`Avg IVB` = numeric(0), `Avg HB` = numeric(0), `VAA` = numeric(0),
|
| 2376 |
+
`HAA` = numeric(0), `hRel` = numeric(0), `vRel` = numeric(0),
|
| 2377 |
+
`Ext` = numeric(0), `Strike%` = numeric(0), `Whiff%` = numeric(0),
|
| 2378 |
+
`Zone%` = numeric(0), `Stuff+` = numeric(0),
|
| 2379 |
+
check.names = FALSE, stringsAsFactors = FALSE
|
| 2380 |
+
)
|
| 2381 |
+
return(list(stats = empty_df, colors = matrix("#FFFFFF", nrow = 0, ncol = 17)))
|
| 2382 |
+
}
|
| 2383 |
|
| 2384 |
pitch_stats <- pitcher_data %>%
|
| 2385 |
dplyr::group_by(Pitch = TaggedPitchType) %>%
|
| 2386 |
dplyr::summarise(
|
| 2387 |
Count = dplyr::n(),
|
| 2388 |
+
`Usage%` = round(100 * dplyr::n() / nrow(pitcher_data), 1),
|
| 2389 |
+
`Avg Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
|
| 2390 |
+
`Max Velo` = round(max(RelSpeed, na.rm = TRUE), 1),
|
| 2391 |
+
`Avg Spin` = round(mean(SpinRate, na.rm = TRUE), 0),
|
| 2392 |
+
`Avg IVB` = round(mean(InducedVertBreak, na.rm = TRUE), 1),
|
| 2393 |
+
`Avg HB` = round(mean(HorzBreak, na.rm = TRUE), 1),
|
| 2394 |
+
`VAA` = round(mean(VertApprAngle, na.rm = TRUE), 1),
|
| 2395 |
+
`HAA` = round(mean(HorzApprAngle, na.rm = TRUE), 1),
|
| 2396 |
+
`hRel` = round(mean(RelSide, na.rm = TRUE), 1),
|
| 2397 |
+
`vRel` = round(mean(RelHeight, na.rm = TRUE), 1),
|
| 2398 |
+
`Ext` = round(mean(Extension, na.rm = TRUE), 2),
|
| 2399 |
+
`Strike%` = round(100 * sum(!PitchCall %in% c("BallCalled","BallinDirt","BallIntentional")) / dplyr::n(), 1),
|
| 2400 |
`Whiff%` = ifelse(sum(SwingIndicator, na.rm = TRUE) > 0,
|
| 2401 |
+
round(100 * sum(WhiffIndicator, na.rm = TRUE) / sum(SwingIndicator, na.rm = TRUE), 1), 0),
|
| 2402 |
+
`Zone%` = round(100 * sum(StrikeZoneIndicator, na.rm = TRUE) / dplyr::n(), 1),
|
| 2403 |
`Stuff+` = round(mean(stuff_plus, na.rm = TRUE), 1),
|
| 2404 |
.groups = "drop"
|
| 2405 |
) %>%
|
| 2406 |
dplyr::arrange(dplyr::desc(`Usage%`))
|
| 2407 |
|
| 2408 |
+
# CRITICAL: Remove duplicate columns FIRST before creating color matrix
|
| 2409 |
dup_mask <- duplicated(names(pitch_stats))
|
| 2410 |
if (any(dup_mask)) {
|
| 2411 |
message("Removing ", sum(dup_mask), " duplicate columns from pitch_stats")
|
| 2412 |
pitch_stats <- pitch_stats[, !dup_mask, drop = FALSE]
|
| 2413 |
}
|
| 2414 |
|
| 2415 |
+
# NOW get final dimensions and create color matrix
|
|
|
|
| 2416 |
num_rows <- nrow(pitch_stats)
|
| 2417 |
+
num_cols <- ncol(pitch_stats)
|
| 2418 |
color_matrix <- matrix("#FFFFFF", nrow = max(1, num_rows), ncol = max(1, num_cols))
|
| 2419 |
|
| 2420 |
+
if (num_rows == 0) {
|
| 2421 |
+
return(list(stats = pitch_stats, colors = color_matrix))
|
| 2422 |
+
}
|
| 2423 |
+
|
| 2424 |
+
# Get column indices AFTER deduplication
|
| 2425 |
+
col_idx <- function(nm) {
|
| 2426 |
+
idx <- match(nm, names(pitch_stats))
|
| 2427 |
+
if (is.na(idx)) return(NA_integer_)
|
| 2428 |
+
idx
|
| 2429 |
+
}
|
| 2430 |
+
|
| 2431 |
c_avg_velo <- col_idx("Avg Velo")
|
| 2432 |
c_max_velo <- col_idx("Max Velo")
|
| 2433 |
c_spin <- col_idx("Avg Spin")
|
|
|
|
| 2437 |
c_stuff <- col_idx("Stuff+")
|
| 2438 |
c_ext <- col_idx("Ext")
|
| 2439 |
|
| 2440 |
+
# Helper to safely check index validity
|
| 2441 |
+
valid_idx <- function(idx) {
|
| 2442 |
+
!is.na(idx) && is.numeric(idx) && length(idx) == 1 && idx >= 1 && idx <= num_cols
|
| 2443 |
+
}
|
| 2444 |
+
|
| 2445 |
for (i in seq_len(num_rows)) {
|
| 2446 |
pt <- pitch_stats$Pitch[i]
|
| 2447 |
|
|
|
|
| 2466 |
vb_r <- sec_ref$velo_r %||% NA_real_
|
| 2467 |
velo_bench <- if (identical(pitcher_hand, "Left")) vb_l else vb_r
|
| 2468 |
}
|
| 2469 |
+
if (!is.na(velo_bench) && valid_idx(c_avg_velo))
|
| 2470 |
color_matrix[i, c_avg_velo] <- get_gradient_color(pitch_stats$`Avg Velo`[i], velo_bench, "higher_better", 0.05)
|
| 2471 |
+
if (!is.na(velo_bench) && valid_idx(c_max_velo))
|
| 2472 |
color_matrix[i, c_max_velo] <- get_gradient_color(pitch_stats$`Max Velo`[i], velo_bench, "higher_better", 0.05)
|
| 2473 |
}
|
| 2474 |
|
| 2475 |
+
if (!is.null(sec_ref) && valid_idx(c_spin) && !is.null(sec_ref$spin))
|
| 2476 |
+
color_matrix[i, c_spin] <- get_gradient_color(pitch_stats$`Avg Spin`[i], sec_ref$spin, "higher_better", 0.20)
|
| 2477 |
+
if (!is.null(sec_ref) && valid_idx(c_strk) && !is.null(sec_ref$strike))
|
| 2478 |
+
color_matrix[i, c_strk] <- get_gradient_color(pitch_stats$`Strike%`[i], sec_ref$strike, "higher_better", 0.15)
|
| 2479 |
+
if (!is.null(sec_ref) && valid_idx(c_whiff) && !is.null(sec_ref$whiff))
|
| 2480 |
+
color_matrix[i, c_whiff] <- get_gradient_color(pitch_stats$`Whiff%`[i], sec_ref$whiff, "higher_better", 0.30)
|
| 2481 |
+
if (!is.null(sec_ref) && valid_idx(c_zone) && !is.null(sec_ref$zone))
|
| 2482 |
+
color_matrix[i, c_zone] <- get_gradient_color(pitch_stats$`Zone%`[i], sec_ref$zone, "higher_better", 0.20)
|
| 2483 |
|
| 2484 |
+
if (valid_idx(c_stuff)) {
|
| 2485 |
val <- pitch_stats$`Stuff+`[i]
|
| 2486 |
if (is.finite(val)) color_matrix[i, c_stuff] <- get_gradient_color(val, 100, "higher_better", 0.20)
|
| 2487 |
}
|
| 2488 |
|
| 2489 |
+
if (valid_idx(c_ext)) {
|
| 2490 |
ext_bench <- sec_extension_benchmark(pt)
|
| 2491 |
if (is.finite(ext_bench)) {
|
| 2492 |
color_matrix[i, c_ext] <- get_gradient_color(pitch_stats$`Ext`[i], ext_bench, "higher_better", 0.08)
|
|
|
|
| 2496 |
|
| 2497 |
list(stats = pitch_stats, colors = color_matrix)
|
| 2498 |
}
|
|
|
|
| 2499 |
|
| 2500 |
create_movement_plot <- create_pitcher_movement_plot
|
| 2501 |
|
|
|
|
| 2750 |
|
| 2751 |
.text_on_fill <- function(hex) {
|
| 2752 |
if (is.na(hex) || !nzchar(hex)) return("black")
|
| 2753 |
+
tryCatch({
|
| 2754 |
+
rgb <- grDevices::col2rgb(hex) / 255
|
| 2755 |
+
L <- 0.2126*rgb[1] + 0.7152*rgb[2] + 0.0722*rgb[3]
|
| 2756 |
+
ifelse(L < 0.5, "white", "black")
|
| 2757 |
+
}, error = function(e) "black")
|
| 2758 |
+
}
|
| 2759 |
+
|
| 2760 |
+
# Safe cell accessor - handles missing columns gracefully
|
| 2761 |
+
get_cell_value <- function(df, colname, row_idx) {
|
| 2762 |
+
if (is.null(df) || !is.data.frame(df)) return(NA)
|
| 2763 |
+
if (is.null(colname) || !nzchar(colname)) return(NA)
|
| 2764 |
+
if (!(colname %in% names(df))) return(NA)
|
| 2765 |
+
if (row_idx < 1 || row_idx > nrow(df)) return(NA)
|
| 2766 |
+
tryCatch(df[[colname]][row_idx], error = function(e) NA)
|
| 2767 |
}
|
| 2768 |
|
| 2769 |
pitcher_df <- dplyr::filter(game_df, Pitcher == pitcher_name)
|
| 2770 |
if (nrow(pitcher_df) == 0) {
|
| 2771 |
pdf(output_file, width = 11, height = 14)
|
| 2772 |
grid::grid.newpage()
|
| 2773 |
+
grid::grid.text(paste("No data available for", pitcher_name),
|
| 2774 |
gp = grid::gpar(fontsize = 16, fontface = "bold"))
|
| 2775 |
dev.off()
|
| 2776 |
return(output_file)
|
| 2777 |
}
|
| 2778 |
|
| 2779 |
+
game_day <- tryCatch(parse_game_day(pitcher_df), error = function(e) Sys.Date())
|
| 2780 |
+
|
| 2781 |
+
# Get summary stats with error handling
|
| 2782 |
+
summary_result <- tryCatch(
|
| 2783 |
+
create_advanced_pitcher_summary(pitcher_df, pitcher_name),
|
| 2784 |
+
error = function(e) {
|
| 2785 |
+
message("Error in summary: ", e$message)
|
| 2786 |
+
list(stats = data.frame(IP=0, R=0, BF=0, K=0, BB=0, H=0, check.names=FALSE),
|
| 2787 |
+
colors = rep("#FFFFFF", 6))
|
| 2788 |
+
}
|
| 2789 |
+
)
|
| 2790 |
summary_stats <- summary_result$stats
|
| 2791 |
summary_colors <- summary_result$colors
|
| 2792 |
|
| 2793 |
+
# Get pitch characteristics with error handling
|
| 2794 |
+
pitch_result <- tryCatch(
|
| 2795 |
+
create_advanced_pitch_characteristics(pitcher_df, pitcher_name),
|
| 2796 |
+
error = function(e) {
|
| 2797 |
+
message("Error in pitch characteristics: ", e$message)
|
| 2798 |
+
list(
|
| 2799 |
+
stats = data.frame(Pitch = "-", Count = 0, `Usage%` = NA_real_, check.names = FALSE),
|
| 2800 |
+
colors = matrix("#FFFFFF", nrow = 1, ncol = 3)
|
| 2801 |
+
)
|
| 2802 |
+
}
|
| 2803 |
+
)
|
| 2804 |
+
pitch_char <- pitch_result$stats
|
| 2805 |
pitch_colors_matrix <- pitch_result$colors
|
| 2806 |
|
| 2807 |
+
# Handle empty or NULL pitch_char
|
| 2808 |
+
if (is.null(pitch_char) || nrow(pitch_char) == 0) {
|
| 2809 |
+
pitch_char <- data.frame(
|
| 2810 |
Pitch = "-", Count = 0, `Usage%` = NA_real_,
|
| 2811 |
`Avg Velo` = NA_real_, `Max Velo` = NA_real_,
|
| 2812 |
`Avg Spin` = NA_real_,
|
|
|
|
| 2815 |
`hRel` = NA_real_, `vRel` = NA_real_, `Ext` = NA_real_,
|
| 2816 |
`Strike%` = NA_real_, `Whiff%` = NA_real_,
|
| 2817 |
`Zone%` = NA_real_,
|
| 2818 |
+
`Stuff+` = NA_real_,
|
| 2819 |
+
check.names = FALSE
|
| 2820 |
)
|
| 2821 |
+
pitch_colors_matrix <- matrix("#FFFFFF", nrow = 1, ncol = ncol(pitch_char))
|
| 2822 |
}
|
| 2823 |
|
| 2824 |
+
# Limit rows to max 9
|
| 2825 |
max_rows_to_show <- min(nrow(pitch_char), 9)
|
| 2826 |
if (nrow(pitch_char) > max_rows_to_show) {
|
| 2827 |
+
pitch_char <- pitch_char[1:max_rows_to_show, , drop = FALSE]
|
| 2828 |
}
|
|
|
|
| 2829 |
|
| 2830 |
+
# Get final dimensions
|
| 2831 |
+
num_rows <- nrow(pitch_char)
|
| 2832 |
+
num_cols <- ncol(pitch_char)
|
|
|
|
|
|
|
| 2833 |
|
| 2834 |
+
# CRITICAL: Rebuild color matrix to EXACTLY match pitch_char dimensions
|
| 2835 |
+
new_color_matrix <- matrix("#FFFFFF", nrow = num_rows, ncol = num_cols)
|
| 2836 |
+
if (!is.null(pitch_colors_matrix) && is.matrix(pitch_colors_matrix)) {
|
| 2837 |
+
rows_to_copy <- min(nrow(pitch_colors_matrix), num_rows)
|
| 2838 |
+
cols_to_copy <- min(ncol(pitch_colors_matrix), num_cols)
|
| 2839 |
+
if (rows_to_copy > 0 && cols_to_copy > 0) {
|
| 2840 |
+
new_color_matrix[1:rows_to_copy, 1:cols_to_copy] <-
|
| 2841 |
+
pitch_colors_matrix[1:rows_to_copy, 1:cols_to_copy]
|
| 2842 |
+
}
|
| 2843 |
+
}
|
| 2844 |
+
pitch_colors_matrix <- new_color_matrix
|
| 2845 |
+
|
| 2846 |
+
# Create plots with error handling
|
| 2847 |
+
movement_plot <- tryCatch(
|
| 2848 |
+
create_movement_plot(pitcher_df, pitcher_name, pitch_colors),
|
| 2849 |
+
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Movement Plot Error")
|
| 2850 |
+
)
|
| 2851 |
+
velo_plot <- tryCatch(
|
| 2852 |
+
create_velocity_distribution_plot(pitcher_df, pitcher_name, pitch_colors),
|
| 2853 |
+
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Velocity Plot Error")
|
| 2854 |
+
)
|
| 2855 |
+
location_lhb <- tryCatch(
|
| 2856 |
+
create_location_by_result_plot(pitcher_df, pitcher_name, "Left", pitch_colors),
|
| 2857 |
+
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("LHB Location Error")
|
| 2858 |
+
)
|
| 2859 |
+
location_rhb <- tryCatch(
|
| 2860 |
+
create_location_by_result_plot(pitcher_df, pitcher_name, "Right", pitch_colors),
|
| 2861 |
+
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("RHB Location Error")
|
| 2862 |
+
)
|
| 2863 |
+
count_plot <- tryCatch(
|
| 2864 |
+
create_count_usage_plot(pitcher_df, pitcher_name, pitch_colors),
|
| 2865 |
+
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Count Usage Error")
|
| 2866 |
+
)
|
| 2867 |
+
|
| 2868 |
+
# Start PDF
|
| 2869 |
pdf(output_file, width = 11, height = 14)
|
| 2870 |
on.exit(try(dev.off(), silent = TRUE), add = TRUE)
|
| 2871 |
grid::grid.newpage()
|
| 2872 |
|
| 2873 |
+
# Layout calculations
|
| 2874 |
+
header_y_top <- 0.98
|
| 2875 |
charts_y_top <- 0.85
|
| 2876 |
charts_height <- 0.30
|
| 2877 |
charts_y_bottom <- charts_y_top - charts_height
|
| 2878 |
+
count_y_top <- charts_y_bottom - 0.02
|
| 2879 |
+
count_height <- 0.18
|
| 2880 |
+
count_y_bottom <- count_y_top - count_height
|
| 2881 |
+
base_loc_top <- 0.30
|
| 2882 |
+
table_margin <- 0.03
|
| 2883 |
+
min_row_h <- 0.0125
|
| 2884 |
+
max_row_h <- 0.0180
|
|
|
|
|
|
|
| 2885 |
|
| 2886 |
y_top_char_orig <- count_y_bottom - 0.011
|
| 2887 |
+
rows_including_header <- num_rows + 1
|
| 2888 |
available_for_table_orig <- y_top_char_orig - (base_loc_top + table_margin)
|
| 2889 |
+
row_h_char <- min(max_row_h, max(min_row_h, available_for_table_orig / max(1, rows_including_header)))
|
| 2890 |
y_loc_top <- y_top_char_orig - rows_including_header * row_h_char - (table_margin * 0.5)
|
| 2891 |
|
| 2892 |
table_lower_offset <- 0.025
|
| 2893 |
y_top_char <- y_top_char_orig - table_lower_offset
|
|
|
|
| 2894 |
available_for_table <- y_top_char - (base_loc_top + table_margin)
|
| 2895 |
+
row_h_char <- min(max_row_h, max(min_row_h, available_for_table / max(1, rows_including_header)))
|
| 2896 |
|
| 2897 |
+
# Header
|
| 2898 |
grid::pushViewport(grid::viewport(x = 0.5, y = header_y_top, width = 1, height = 0.04, just = c("center","top")))
|
| 2899 |
grid::grid.text(paste(pitcher_name, "- Advanced Pitcher Report"),
|
| 2900 |
gp = grid::gpar(fontface = "bold", cex = 1.8, col = "#006F71"))
|
| 2901 |
grid::popViewport()
|
| 2902 |
|
| 2903 |
+
# Summary section
|
| 2904 |
grid::grid.text("Summary", x = 0.5, y = 0.94,
|
| 2905 |
gp = grid::gpar(fontface = "bold", cex = 1.1, col = "#006F71"))
|
| 2906 |
|
|
|
|
| 2928 |
gp = grid::gpar(cex = 0.62))
|
| 2929 |
}
|
| 2930 |
|
| 2931 |
+
# Movement plot
|
| 2932 |
grid::pushViewport(grid::viewport(x = 0.25, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
|
| 2933 |
+
tryCatch(print(movement_plot, newpage = FALSE), error = function(e) NULL)
|
| 2934 |
grid::popViewport()
|
| 2935 |
|
| 2936 |
+
# Velocity plot
|
| 2937 |
grid::pushViewport(grid::viewport(x = 0.75, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
|
| 2938 |
+
tryCatch(print(velo_plot, newpage = FALSE), error = function(e) NULL)
|
| 2939 |
grid::popViewport()
|
| 2940 |
|
| 2941 |
+
# Count plot
|
| 2942 |
grid::pushViewport(grid::viewport(x = 0.5, y = count_y_top, width = 0.92, height = count_height, just = c("center","top")))
|
| 2943 |
+
tryCatch(print(count_plot, newpage = FALSE), error = function(e) NULL)
|
| 2944 |
grid::popViewport()
|
| 2945 |
|
| 2946 |
+
# Pitch Characteristics table
|
| 2947 |
grid::grid.text("Pitch Characteristics", x = 0.5, y = y_top_char + 0.015,
|
| 2948 |
gp = grid::gpar(fontface = "bold", cex = 1.1, col = "#006F71"))
|
| 2949 |
|
| 2950 |
char_headers <- names(pitch_char)
|
| 2951 |
num_char_cols <- length(char_headers)
|
| 2952 |
|
| 2953 |
+
# Calculate column widths safely
|
| 2954 |
+
if (num_char_cols > 1) {
|
| 2955 |
+
char_widths <- c(0.10, rep((1 - 0.10 - 0.06) / (num_char_cols - 1), num_char_cols - 1))
|
| 2956 |
+
} else {
|
| 2957 |
+
char_widths <- c(0.10)
|
| 2958 |
+
}
|
| 2959 |
+
|
| 2960 |
+
x_start_char <- 0.5 - sum(char_widths)/2
|
| 2961 |
+
x_pos_char <- c(x_start_char)
|
| 2962 |
+
if (length(char_widths) > 1) {
|
| 2963 |
+
x_pos_char <- c(x_start_char, x_start_char + cumsum(char_widths[-length(char_widths)]))
|
| 2964 |
+
}
|
| 2965 |
|
| 2966 |
+
# Draw header row
|
| 2967 |
for (i in seq_along(char_headers)) {
|
| 2968 |
+
if (i > length(x_pos_char) || i > length(char_widths)) break
|
| 2969 |
grid::grid.rect(x = x_pos_char[i], y = y_top_char, width = char_widths[i]*0.985, height = row_h_char,
|
| 2970 |
just = c("left","top"), gp = grid::gpar(fill = "#006F71", col = "black", lwd = 0.5))
|
| 2971 |
grid::grid.text(char_headers[i],
|
|
|
|
| 2973 |
gp = grid::gpar(col = "white", cex = 0.50, fontface = "bold"))
|
| 2974 |
}
|
| 2975 |
|
| 2976 |
+
# Find Pitch column index
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2977 |
i_col_pitch <- match("Pitch", char_headers)
|
| 2978 |
has_pitchcol <- !is.na(i_col_pitch) && i_col_pitch >= 1
|
| 2979 |
|
| 2980 |
+
# Draw data rows
|
| 2981 |
+
for (r in seq_len(num_rows)) {
|
| 2982 |
y_row <- y_top_char - r * row_h_char
|
| 2983 |
+
pitch_name <- if (has_pitchcol) as.character(get_cell_value(pitch_char, "Pitch", r)) else NA_character_
|
| 2984 |
|
| 2985 |
for (i in seq_along(char_headers)) {
|
| 2986 |
+
if (i > length(x_pos_char) || i > length(char_widths)) break
|
| 2987 |
+
if (i > num_cols) break
|
| 2988 |
+
|
| 2989 |
colname <- char_headers[i]
|
| 2990 |
|
| 2991 |
+
# Get background color safely - check bounds explicitly
|
| 2992 |
+
bg <- "#FFFFFF"
|
| 2993 |
+
if (r >= 1 && r <= nrow(pitch_colors_matrix) && i >= 1 && i <= ncol(pitch_colors_matrix)) {
|
| 2994 |
+
bg <- pitch_colors_matrix[r, i]
|
| 2995 |
+
if (is.na(bg) || !nzchar(bg)) bg <- "#FFFFFF"
|
| 2996 |
+
}
|
| 2997 |
|
| 2998 |
+
# Override for Pitch column with pitch type color
|
| 2999 |
+
if (has_pitchcol && identical(colname, "Pitch") && !is.na(pitch_name) && pitch_name %in% names(pitch_colors)) {
|
| 3000 |
bg <- pitch_colors[[pitch_name]]
|
| 3001 |
}
|
| 3002 |
|
| 3003 |
grid::grid.rect(x = x_pos_char[i], y = y_row, width = char_widths[i]*0.985, height = row_h_char,
|
| 3004 |
just = c("left","top"), gp = grid::gpar(fill = bg, col = "grey80", lwd = 0.3))
|
| 3005 |
|
| 3006 |
+
# Get value safely
|
| 3007 |
+
val <- get_cell_value(pitch_char, colname, r)
|
| 3008 |
|
| 3009 |
display_val <- if (is.numeric(val)) {
|
| 3010 |
if (is.na(val) || is.nan(val) || is.infinite(val)) "-" else sprintf("%.1f", val)
|
|
|
|
| 3022 |
}
|
| 3023 |
}
|
| 3024 |
|
| 3025 |
+
# Location plots
|
| 3026 |
grid::pushViewport(grid::viewport(x = 0.25, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
|
| 3027 |
+
tryCatch(print(location_lhb, newpage = FALSE), error = function(e) NULL)
|
| 3028 |
grid::popViewport()
|
| 3029 |
|
| 3030 |
grid::pushViewport(grid::viewport(x = 0.75, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
|
| 3031 |
+
tryCatch(print(location_rhb, newpage = FALSE), error = function(e) NULL)
|
| 3032 |
grid::popViewport()
|
| 3033 |
|
| 3034 |
+
# Footer
|
| 3035 |
grid::grid.text("Red: Below SEC Avg, White: Near SEC Avg, Green: Above SEC Avg | Stuff+ predicts pitch effectiveness; 100 is average, 10 = one SD",
|
| 3036 |
x = 0.5, y = 0.04, gp = grid::gpar(cex = 0.82, col = "grey40", fontface = "italic"))
|
| 3037 |
grid::grid.text("Data: TrackMan | Report Generated: Coastal Carolina Baseball",
|
|
|
|
| 3040 |
invisible(output_file)
|
| 3041 |
}
|
| 3042 |
|
| 3043 |
+
|
| 3044 |
# =====================================================================
|
| 3045 |
# =========================== UI ================================
|
| 3046 |
# =====================================================================
|