igroffman commited on
Commit
f98075b
·
verified ·
1 Parent(s): b329816

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +200 -94
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, na.rm = TRUE), 1),
2385
- `Max Velo` = round(max(RelSpeed, na.rm = TRUE), 1),
2386
- `Avg Spin` = round(mean(SpinRate, na.rm = TRUE), 0),
2387
- `Avg IVB` = round(mean(InducedVertBreak, na.rm = TRUE), 1),
2388
- `Avg HB` = round(mean(HorzBreak, na.rm = TRUE), 1),
2389
- `VAA` = round(mean(VertApprAngle, na.rm = TRUE), 1),
2390
- `HAA` = round(mean(HorzApprAngle, na.rm = TRUE), 1),
2391
- `hRel` = round(mean(RelSide, na.rm = TRUE), 1),
2392
- `vRel` = round(mean(RelHeight, na.rm = TRUE), 1),
2393
- `Ext` = round(mean(Extension, na.rm = TRUE), 2),
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
- # IMPORTANT: Remove duplicate columns FIRST, before creating color matrix
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 create the color matrix with the FINAL dimensions
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
- # Define col_idx AFTER deduplication so indices are correct
2416
- col_idx <- function(nm) match(nm, names(pitch_stats))
 
 
 
 
 
 
 
 
 
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) && has_col_index(c_avg_velo) && c_avg_velo <= num_cols)
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) && has_col_index(c_max_velo) && c_max_velo <= num_cols)
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) && has_col_index(c_spin) && c_spin <= num_cols && !is.null(sec_ref$spin))
2457
- color_matrix[i, c_spin] <- get_gradient_color(pitch_stats$`Avg Spin`[i], sec_ref$spin, "higher_better", 0.20)
2458
- if (!is.null(sec_ref) && has_col_index(c_strk) && c_strk <= num_cols && !is.null(sec_ref$strike))
2459
- color_matrix[i, c_strk] <- get_gradient_color(pitch_stats$`Strike%`[i], sec_ref$strike, "higher_better", 0.15)
2460
- if (!is.null(sec_ref) && has_col_index(c_whiff) && c_whiff <= num_cols && !is.null(sec_ref$whiff))
2461
- color_matrix[i, c_whiff] <- get_gradient_color(pitch_stats$`Whiff%`[i], sec_ref$whiff, "higher_better", 0.30)
2462
- if (!is.null(sec_ref) && has_col_index(c_zone) && c_zone <= num_cols && !is.null(sec_ref$zone))
2463
- color_matrix[i, c_zone] <- get_gradient_color(pitch_stats$`Zone%`[i], sec_ref$zone, "higher_better", 0.20)
2464
 
2465
- if (has_col_index(c_stuff) && c_stuff <= num_cols) {
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 (has_col_index(c_ext) && c_ext <= num_cols) {
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
- rgb <- grDevices::col2rgb(hex) / 255
2736
- L <- 0.2126*rgb[1] + 0.7152*rgb[2] + 0.0722*rgb[3]
2737
- ifelse(L < 0.5, "white", "black")
 
 
 
 
 
 
 
 
 
 
 
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 this pitcher",
2745
  gp = grid::gpar(fontsize = 16, fontface = "bold"))
2746
  dev.off()
2747
  return(output_file)
2748
  }
2749
 
2750
- summary_result <- create_advanced_pitcher_summary(pitcher_df, pitcher_name)
 
 
 
 
 
 
 
 
 
 
2751
  summary_stats <- summary_result$stats
2752
  summary_colors <- summary_result$colors
2753
 
2754
- pitch_result <- create_advanced_pitch_characteristics(pitcher_df, pitcher_name)
2755
- pitch_char <- pitch_result$stats
 
 
 
 
 
 
 
 
 
 
2756
  pitch_colors_matrix <- pitch_result$colors
2757
 
2758
- if (nrow(pitch_char) == 0) {
2759
- pitch_char <- tibble::tibble(
 
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 %>% dplyr::slice(1:max_rows_to_show)
2775
  }
2776
- pitch_colors_matrix <- sync_color_matrix_to_df(pitch_colors_matrix, pitch_char, fill = "#FFFFFF")
2777
 
2778
- movement_plot <- create_movement_plot(pitcher_df, pitcher_name, pitch_colors)
2779
- velo_plot <- create_velocity_distribution_plot(pitcher_df, pitcher_name, pitch_colors)
2780
- location_lhb <- create_location_by_result_plot(pitcher_df, pitcher_name, "Left", pitch_colors)
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
- header_y_top <- 0.98
2789
-
2790
  charts_y_top <- 0.85
2791
  charts_height <- 0.30
2792
  charts_y_bottom <- charts_y_top - charts_height
2793
-
2794
- count_y_top <- charts_y_bottom - 0.02
2795
- count_height <- 0.18
2796
- count_y_bottom <- count_y_top - count_height
2797
-
2798
- base_loc_top <- 0.30
2799
- table_margin <- 0.03
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 <- nrow(pitch_char) + 1
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
- char_widths <- c(0.10, rep((1 - 0.10 - 0.06) / max(1, (num_char_cols - 1)), max(0, num_char_cols - 1)))
2866
- x_start_char <- 0.5 - sum(char_widths)/2
2867
- x_pos_char <- c(x_start_char, x_start_char + cumsum(char_widths[-length(char_widths)]))
 
 
 
 
 
 
 
 
 
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
- get_cell_scalar <- function(df, colname, r) {
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
- for (r in seq_len(nrow(pitch_char))) {
 
2889
  y_row <- y_top_char - r * row_h_char
2890
- pitch_name <- if (has_pitchcol) as.character(get_cell_scalar(pitch_char, "Pitch", r)) else NA_character_
2891
 
2892
  for (i in seq_along(char_headers)) {
 
 
 
2893
  colname <- char_headers[i]
2894
 
2895
- bg <- safe_color_at(pitch_colors_matrix, r, i, default = "#FFFFFF")
 
 
 
 
 
2896
 
2897
- if (has_pitchcol && identical(colname, "Pitch") && !is.na(pitch_name) && !is.null(pitch_colors[[pitch_name]])) {
 
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
- val <- get_cell_scalar(pitch_char, colname, r)
 
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
  # =====================================================================