igroffman commited on
Commit
56f1b66
·
verified ·
1 Parent(s): cc62536

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +117 -26
app.R CHANGED
@@ -5296,8 +5296,9 @@ create_count_usage_plot <- function(data, pitcher_name, pitch_colors) {
5296
  create_location_by_result_plot <- function(data, player_name, batter_side, pitch_colors) {
5297
  data <- normalize_columns(data)
5298
 
5299
- # Fixed facet levels
5300
  result_levels <- c(
 
5301
  "Whiffs",
5302
  "Balls Called",
5303
  "Strikes Called",
@@ -5305,7 +5306,7 @@ create_location_by_result_plot <- function(data, player_name, batter_side, pitch
5305
  "2 Strikes"
5306
  )
5307
 
5308
- pitcher_data <- data %>%
5309
  dplyr::filter(
5310
  Pitcher == player_name,
5311
  BatterSide == batter_side,
@@ -5313,7 +5314,10 @@ create_location_by_result_plot <- function(data, player_name, batter_side, pitch
5313
  TaggedPitchType != "Other",
5314
  !is.na(PlateLocSide),
5315
  !is.na(PlateLocHeight)
5316
- ) %>%
 
 
 
5317
  dplyr::mutate(
5318
  ResultType = dplyr::case_when(
5319
  PitchCall == "StrikeSwinging" ~ "Whiffs",
@@ -5326,8 +5330,13 @@ create_location_by_result_plot <- function(data, player_name, batter_side, pitch
5326
  ) %>%
5327
  dplyr::filter(!is.na(ResultType))
5328
 
 
 
 
 
 
5329
  if (nrow(pitcher_data) == 0) {
5330
- # Dummy data frame so we still draw 5 facets + zones
5331
  plot_df <- data.frame(
5332
  PlateLocSide = NA_real_,
5333
  PlateLocHeight = NA_real_,
@@ -5346,7 +5355,7 @@ create_location_by_result_plot <- function(data, player_name, batter_side, pitch
5346
  ) +
5347
  facet_wrap(
5348
  ~ ResultType,
5349
- ncol = 5,
5350
  labeller = labeller(ResultType = label_value),
5351
  drop = FALSE # <- keep empty facets
5352
  ) +
@@ -5380,9 +5389,78 @@ create_location_by_side_plot <- function(data, player_name, batter_side, pitch_c
5380
  create_location_by_result_plot(data, player_name, batter_side, pitch_colors)
5381
  }
5382
 
 
 
5383
 
5384
- create_location_by_side_plot <- function(data, player_name, batter_side, pitch_colors) {
5385
- create_location_by_result_plot(data, player_name, batter_side, pitch_colors)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5386
  }
5387
 
5388
  get_team_logo_path <- function(team_name, logo_dir = "logos") {
@@ -5625,9 +5703,9 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5625
  create_movement_plot(pitcher_df, pitcher_name, pitch_colors),
5626
  error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Movement Plot Error")
5627
  )
5628
- velo_plot <- tryCatch(
5629
- create_velocity_distribution_plot(pitcher_df, pitcher_name, pitch_colors),
5630
- error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Velocity Plot Error")
5631
  )
5632
  location_lhb <- tryCatch(
5633
  create_location_by_result_plot(pitcher_df, pitcher_name, "Left", pitch_colors),
@@ -5652,27 +5730,27 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5652
  })
5653
 
5654
  # Start PDF
5655
- pdf(output_file, width = 11, height = 15)
5656
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
5657
  grid::grid.newpage()
5658
 
5659
  header_y_top <- 0.98
5660
- charts_y_top <- 0.85
5661
- charts_height <- 0.30
5662
  charts_y_bottom <- charts_y_top - charts_height
5663
 
5664
  # --- BIGGER TABLES: taller rows, larger text ---
5665
  row_h_char <- 0.0235 # pitch characteristics row height
5666
  hdr_cex <- 0.72 # table header text size
5667
  cell_cex <- 0.72 # table cell text size
5668
- summary_cex <- 1.15 # summary/header block text size
5669
  section_cex <- 1.35 # section title size
5670
 
5671
  rows_including_header <- num_rows + 1
5672
 
5673
  # ----- Row 2: Count Leverage TABLE (left) | Release plot (right) -----
5674
- row2_y_top <- charts_y_bottom - 0.02
5675
- release_height <- 0.24
5676
 
5677
  lev_rows <- if (!is.null(lev_result)) nrow(lev_result$by_pitch) + 1L else 0L # +1 overall row
5678
  lev_row_h <- row_h_char
@@ -5681,13 +5759,15 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5681
  lev_block_bottom <- y_top_lev - (lev_rows + 1) * lev_row_h # +1 for header row
5682
 
5683
  row2_bottom <- min(lev_block_bottom, row2_y_top - release_height)
 
 
5684
 
5685
  # Pitch characteristics table below row 2
5686
- y_top_char <- row2_bottom - 0.045
5687
  char_block_bottom <- y_top_char - rows_including_header * row_h_char
5688
 
5689
  # Location plots at the BOTTOM of the report
5690
- y_loc_top <- char_block_bottom - 0.030
5691
 
5692
  # ===== HEADER WITH LOGO =====
5693
  grid::pushViewport(grid::viewport(x = 0.5, y = header_y_top, width = 1, height = 0.06, just = c("center","top")))
@@ -5699,7 +5779,7 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5699
 
5700
  grid::grid.text(paste(pitcher_name, "- Advanced Pitcher Report"),
5701
  x = 0.5, y = 0.5,
5702
- gp = grid::gpar(fontface = "bold", cex = 2.2, col = report_color_primary))
5703
 
5704
 
5705
  # Add team logo on the right if available (mirror)
@@ -5717,12 +5797,12 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5717
 
5718
  summary_headers <- names(summary_stats)
5719
  summary_values <- as.numeric(summary_stats[1, ])
5720
- summary_widths <- rep(0.095, length(summary_headers))
5721
 
5722
  x_start <- 0.5 - sum(summary_widths)/2
5723
  x_pos <- c(x_start, x_start + cumsum(summary_widths[-length(summary_widths)]))
5724
- y_top <- 0.912
5725
- row_h <- 0.032
5726
 
5727
  for (i in seq_along(summary_headers)) {
5728
  grid::grid.rect(x = x_pos[i], y = y_top, width = summary_widths[i]*0.985, height = row_h,
@@ -5745,7 +5825,7 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5745
  grid::popViewport()
5746
 
5747
  grid::pushViewport(grid::viewport(x = 0.75, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
5748
- tryCatch(print(velo_plot, newpage = FALSE), error = function(e) NULL)
5749
  grid::popViewport()
5750
 
5751
  # Row 2: Count Leverage TABLE (left) | Release side plot (right)
@@ -5922,12 +6002,23 @@ if (!is.null(team_master) && !is.na(pitcher_team_abbr)) {
5922
  }
5923
  }
5924
 
5925
- # Location plots
5926
- grid::pushViewport(grid::viewport(x = 0.25, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
 
 
 
 
 
 
 
 
 
5927
  tryCatch(print(location_lhb, newpage = FALSE), error = function(e) NULL)
5928
  grid::popViewport()
5929
 
5930
- grid::pushViewport(grid::viewport(x = 0.75, y = y_loc_top, width = 0.45, height = 0.24, just = c("center","top")))
 
 
5931
  tryCatch(print(location_rhb, newpage = FALSE), error = function(e) NULL)
5932
  grid::popViewport()
5933
 
 
5296
  create_location_by_result_plot <- function(data, player_name, batter_side, pitch_colors) {
5297
  data <- normalize_columns(data)
5298
 
5299
+ # Fixed facet levels ("Overall" = every pitch to that side)
5300
  result_levels <- c(
5301
+ "Overall",
5302
  "Whiffs",
5303
  "Balls Called",
5304
  "Strikes Called",
 
5306
  "2 Strikes"
5307
  )
5308
 
5309
+ base_data <- data %>%
5310
  dplyr::filter(
5311
  Pitcher == player_name,
5312
  BatterSide == batter_side,
 
5314
  TaggedPitchType != "Other",
5315
  !is.na(PlateLocSide),
5316
  !is.na(PlateLocHeight)
5317
+ )
5318
+
5319
+ # Result-specific facets
5320
+ result_data <- base_data %>%
5321
  dplyr::mutate(
5322
  ResultType = dplyr::case_when(
5323
  PitchCall == "StrikeSwinging" ~ "Whiffs",
 
5330
  ) %>%
5331
  dplyr::filter(!is.na(ResultType))
5332
 
5333
+ # Overall facet = every pitch, uncategorized
5334
+ overall_data <- base_data %>% dplyr::mutate(ResultType = "Overall")
5335
+
5336
+ pitcher_data <- dplyr::bind_rows(overall_data, result_data)
5337
+
5338
  if (nrow(pitcher_data) == 0) {
5339
+ # Dummy data frame so we still draw all facets + zones
5340
  plot_df <- data.frame(
5341
  PlateLocSide = NA_real_,
5342
  PlateLocHeight = NA_real_,
 
5355
  ) +
5356
  facet_wrap(
5357
  ~ ResultType,
5358
+ ncol = 6,
5359
  labeller = labeller(ResultType = label_value),
5360
  drop = FALSE # <- keep empty facets
5361
  ) +
 
5389
  create_location_by_result_plot(data, player_name, batter_side, pitch_colors)
5390
  }
5391
 
5392
+ create_release_consistency_plot <- function(data, player_name, pitch_colors) {
5393
+ data <- normalize_columns(data)
5394
 
5395
+ df <- data %>%
5396
+ dplyr::filter(
5397
+ Pitcher == player_name,
5398
+ !is.na(RelSide), !is.na(RelHeight),
5399
+ !is.na(TaggedPitchType), TaggedPitchType != "Other"
5400
+ )
5401
+
5402
+ # Ring geometry (inches)
5403
+ circ <- function(r) {
5404
+ th <- seq(0, 2 * pi, length.out = 100)
5405
+ data.frame(x = r * cos(th), y = r * sin(th), r = r)
5406
+ }
5407
+ ring_df <- do.call(rbind, lapply(c(3, 6, 9, 12), circ))
5408
+ lim <- 13
5409
+
5410
+ base_plot <- ggplot() +
5411
+ geom_path(data = ring_df, aes(x, y, group = r),
5412
+ color = "gray75", linewidth = 0.4) +
5413
+ geom_path(data = circ(3), aes(x, y), color = "#c0392b",
5414
+ linewidth = 0.9, linetype = "longdash") +
5415
+ annotate("text", x = 0, y = 3.6, label = "3\" = hitter-detectable",
5416
+ size = 3, color = "#c0392b") +
5417
+ geom_hline(yintercept = 0, color = "gray80", linewidth = 0.3) +
5418
+ geom_vline(xintercept = 0, color = "gray80", linewidth = 0.3)
5419
+
5420
+ if (nrow(df) == 0) {
5421
+ return(
5422
+ base_plot +
5423
+ coord_fixed(xlim = c(-lim, lim), ylim = c(-lim, lim)) +
5424
+ labs(x = NULL, y = NULL) +
5425
+ ggtitle("Release Consistency") +
5426
+ theme_void(base_size = 12) +
5427
+ theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5))
5428
+ )
5429
+ }
5430
+
5431
+ # Anchor = average release of the fastball if present, else most-thrown pitch
5432
+ counts <- df %>% dplyr::count(TaggedPitchType, sort = TRUE)
5433
+ anchor_type <- if (any(counts$TaggedPitchType == "Fastball")) {
5434
+ "Fastball"
5435
+ } else {
5436
+ as.character(counts$TaggedPitchType[1])
5437
+ }
5438
+ anchor <- df %>%
5439
+ dplyr::filter(TaggedPitchType == anchor_type) %>%
5440
+ dplyr::summarise(RelSide = mean(RelSide, na.rm = TRUE),
5441
+ RelHeight = mean(RelHeight, na.rm = TRUE))
5442
+
5443
+ # Per-pitch averages, in INCHES relative to the anchor
5444
+ avg <- df %>%
5445
+ dplyr::group_by(TaggedPitchType) %>%
5446
+ dplyr::summarise(RelSide = mean(RelSide, na.rm = TRUE),
5447
+ RelHeight = mean(RelHeight, na.rm = TRUE),
5448
+ .groups = "drop") %>%
5449
+ dplyr::mutate(dx = (RelSide - anchor$RelSide) * 12,
5450
+ dy = (RelHeight - anchor$RelHeight) * 12)
5451
+
5452
+ base_plot +
5453
+ geom_point(data = avg, aes(dx, dy, fill = TaggedPitchType),
5454
+ shape = 21, size = 7, color = "black", stroke = 0.6) +
5455
+ scale_fill_manual(values = pitch_colors, name = "Pitch") +
5456
+ coord_fixed(xlim = c(-lim, lim), ylim = c(-lim, lim)) +
5457
+ labs(x = NULL, y = NULL,
5458
+ caption = sprintf("Center = avg %s release", anchor_type)) +
5459
+ ggtitle("Release Consistency") +
5460
+ theme_void(base_size = 12) +
5461
+ theme(legend.position = "bottom",
5462
+ plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
5463
+ plot.caption = element_text(hjust = 0.5, color = "gray45"))
5464
  }
5465
 
5466
  get_team_logo_path <- function(team_name, logo_dir = "logos") {
 
5703
  create_movement_plot(pitcher_df, pitcher_name, pitch_colors),
5704
  error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Movement Plot Error")
5705
  )
5706
+ release_consistency_plot <- tryCatch(
5707
+ create_release_consistency_plot(pitcher_df, pitcher_name, pitch_colors),
5708
+ error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("Release Consistency Error")
5709
  )
5710
  location_lhb <- tryCatch(
5711
  create_location_by_result_plot(pitcher_df, pitcher_name, "Left", pitch_colors),
 
5730
  })
5731
 
5732
  # Start PDF
5733
+ pdf(output_file, width = 12, height = 18)
5734
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
5735
  grid::grid.newpage()
5736
 
5737
  header_y_top <- 0.98
5738
+ charts_y_top <- 0.86
5739
+ charts_height <- 0.22
5740
  charts_y_bottom <- charts_y_top - charts_height
5741
 
5742
  # --- BIGGER TABLES: taller rows, larger text ---
5743
  row_h_char <- 0.0235 # pitch characteristics row height
5744
  hdr_cex <- 0.72 # table header text size
5745
  cell_cex <- 0.72 # table cell text size
5746
+ summary_cex <- 0.78 # summary/header block text size
5747
  section_cex <- 1.35 # section title size
5748
 
5749
  rows_including_header <- num_rows + 1
5750
 
5751
  # ----- Row 2: Count Leverage TABLE (left) | Release plot (right) -----
5752
+ row2_y_top <- charts_y_bottom - 0.018
5753
+ release_height <- 0.18
5754
 
5755
  lev_rows <- if (!is.null(lev_result)) nrow(lev_result$by_pitch) + 1L else 0L # +1 overall row
5756
  lev_row_h <- row_h_char
 
5759
  lev_block_bottom <- y_top_lev - (lev_rows + 1) * lev_row_h # +1 for header row
5760
 
5761
  row2_bottom <- min(lev_block_bottom, row2_y_top - release_height)
5762
+ # Never let the tables push the location charts off the page
5763
+ row2_bottom <- max(row2_bottom, 0.40)
5764
 
5765
  # Pitch characteristics table below row 2
5766
+ y_top_char <- row2_bottom - 0.038
5767
  char_block_bottom <- y_top_char - rows_including_header * row_h_char
5768
 
5769
  # Location plots at the BOTTOM of the report
5770
+ y_loc_top <- char_block_bottom - 0.026
5771
 
5772
  # ===== HEADER WITH LOGO =====
5773
  grid::pushViewport(grid::viewport(x = 0.5, y = header_y_top, width = 1, height = 0.06, just = c("center","top")))
 
5779
 
5780
  grid::grid.text(paste(pitcher_name, "- Advanced Pitcher Report"),
5781
  x = 0.5, y = 0.5,
5782
+ gp = grid::gpar(fontface = "bold", cex = 1.5, col = report_color_primary))
5783
 
5784
 
5785
  # Add team logo on the right if available (mirror)
 
5797
 
5798
  summary_headers <- names(summary_stats)
5799
  summary_values <- as.numeric(summary_stats[1, ])
5800
+ summary_widths <- rep(0.070, length(summary_headers))
5801
 
5802
  x_start <- 0.5 - sum(summary_widths)/2
5803
  x_pos <- c(x_start, x_start + cumsum(summary_widths[-length(summary_widths)]))
5804
+ y_top <- 0.905
5805
+ row_h <- 0.022
5806
 
5807
  for (i in seq_along(summary_headers)) {
5808
  grid::grid.rect(x = x_pos[i], y = y_top, width = summary_widths[i]*0.985, height = row_h,
 
5825
  grid::popViewport()
5826
 
5827
  grid::pushViewport(grid::viewport(x = 0.75, y = charts_y_top, width = 0.45, height = charts_height, just = c("center","top")))
5828
+ tryCatch(print(release_consistency_plot, newpage = FALSE), error = function(e) NULL)
5829
  grid::popViewport()
5830
 
5831
  # Row 2: Count Leverage TABLE (left) | Release side plot (right)
 
6002
  }
6003
  }
6004
 
6005
+ # Location plots (stacked full-width so all 6 facets stay legible).
6006
+ # Height is derived from whatever vertical space is actually left so the
6007
+ # bottom panel never runs off the page on deep pitch mixes.
6008
+ footer_y <- 0.02
6009
+ loc_gap <- 0.010
6010
+ loc_avail <- y_loc_top - (footer_y + 0.022) - loc_gap
6011
+ loc_row_h <- max(0.085, min(0.150, loc_avail / 2))
6012
+
6013
+ grid::pushViewport(grid::viewport(x = 0.5, y = y_loc_top,
6014
+ width = 0.96, height = loc_row_h,
6015
+ just = c("center","top")))
6016
  tryCatch(print(location_lhb, newpage = FALSE), error = function(e) NULL)
6017
  grid::popViewport()
6018
 
6019
+ grid::pushViewport(grid::viewport(x = 0.5, y = y_loc_top - loc_row_h - loc_gap,
6020
+ width = 0.96, height = loc_row_h,
6021
+ just = c("center","top")))
6022
  tryCatch(print(location_rhb, newpage = FALSE), error = function(e) NULL)
6023
  grid::popViewport()
6024