Spaces:
Running
Running
Update app.R
Browse files
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 |
-
|
| 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
|
| 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 =
|
| 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 |
-
|
| 5385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 5629 |
-
|
| 5630 |
-
error = function(e) ggplot2::ggplot() + ggplot2::theme_void() + ggplot2::ggtitle("
|
| 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 =
|
| 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.
|
| 5661 |
-
charts_height <- 0.
|
| 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 <-
|
| 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.
|
| 5675 |
-
release_height <- 0.
|
| 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.
|
| 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.
|
| 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 =
|
| 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.
|
| 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.
|
| 5725 |
-
row_h <- 0.
|
| 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(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5927 |
tryCatch(print(location_lhb, newpage = FALSE), error = function(e) NULL)
|
| 5928 |
grid::popViewport()
|
| 5929 |
|
| 5930 |
-
grid::pushViewport(grid::viewport(x = 0.
|
|
|
|
|
|
|
| 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 |
|