igroffman commited on
Commit
ce417e4
·
verified ·
1 Parent(s): 64d3ce0

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +276 -162
app.R CHANGED
@@ -1069,28 +1069,6 @@ create_postgame_pdf <- function(game_df, player_name, output_file, bio_data = NU
1069
  }
1070
 
1071
 
1072
- # =====================================================================
1073
- # TABLEAU-STYLE PITCHER POST-GAME REPORT - FIXED VERSION
1074
- # =====================================================================
1075
- # Fixes:
1076
- # - Pitch type detection (handles "Undefined", empty, NA)
1077
- # - Correct header colors matching screenshot
1078
- # - Single row stats layout
1079
- # - No labels in info row (just values)
1080
- # - Smaller location chart, bigger release chart
1081
- # =====================================================================
1082
-
1083
- # =====================================================================
1084
- # TABLEAU-STYLE PITCHER POST-GAME REPORT - VERSION 3
1085
- # =====================================================================
1086
- # Changes from v2:
1087
- # - Charts moved left and higher
1088
- # - Location plot legend much smaller, zone positioned higher
1089
- # - Tables farther right, bigger text, more compact
1090
- # - Header stats: colored label text on white, white value background
1091
- # - Only away team shown in header (no home team/pitcher team)
1092
- # =====================================================================
1093
-
1094
  library(ggplot2)
1095
  library(dplyr)
1096
  library(grid)
@@ -1127,29 +1105,38 @@ process_tableau_pitcher_data <- function(df) {
1127
  alt <- intersect(c("PitcherName","pitcher","Pitcher_LastFirst","PlayerName"), names(df))
1128
  if (length(alt)) df$Pitcher <- df[[alt[1]]] else df$Pitcher <- NA_character_
1129
  }
1130
-
1131
  df <- df %>%
1132
  mutate(Pitcher = str_replace(coalesce(Pitcher, ""), "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
1133
-
1134
  # Clean up TaggedPitchType
1135
  if ("TaggedPitchType" %in% names(df)) {
1136
  df <- df %>%
1137
  mutate(
1138
  TaggedPitchType = case_when(
1139
- is.na(TaggedPitchType) | TaggedPitchType == "" | TaggedPitchType == "Undefined" ~
1140
- ifelse("AutoPitchType" %in% names(df) & !is.na(AutoPitchType) & AutoPitchType != "" & AutoPitchType != "Undefined",
1141
- AutoPitchType,
1142
  ifelse("PitchType" %in% names(df) & !is.na(PitchType) & PitchType != "" & PitchType != "Undefined",
1143
  PitchType, "Undefined")),
1144
  TRUE ~ TaggedPitchType
1145
  )
1146
  )
1147
  }
1148
-
 
 
 
 
 
 
 
 
 
1149
  # Calculate indicators
1150
  df <- df %>%
1151
  mutate(
1152
- StrikeZoneIndicator = ifelse(PlateLocSide >= -0.8333 & PlateLocSide <= 0.8333 &
1153
  PlateLocHeight >= 1.5 & PlateLocHeight <= 3.5, 1, 0),
1154
  EdgeHeightIndicator = ifelse((PlateLocHeight > 14/12 & PlateLocHeight < 22/12) |
1155
  (PlateLocHeight > 38/12 & PlateLocHeight < 46/12), 1, 0),
@@ -1157,29 +1144,29 @@ process_tableau_pitcher_data <- function(df) {
1157
  EdgeZoneWIndicator = ifelse(PlateLocSide > -13.4/12 & PlateLocSide < 13.4/12, 1, 0),
1158
  EdgeWidthIndicator = ifelse((PlateLocSide > -13.3/12 & PlateLocSide < -6.7/12) |
1159
  (PlateLocSide < 13.3/12 & PlateLocSide > 6.7/12), 1, 0),
1160
- EdgeIndicator = ifelse((EdgeHeightIndicator == 1 & EdgeZoneWIndicator == 1) |
1161
  (EdgeWidthIndicator == 1 & EdgeZoneHtIndicator == 1), 1, 0),
1162
  QualityPitchIndicator = ifelse(StrikeZoneIndicator == 1 | EdgeIndicator == 1, 1, 0),
1163
- StrikeIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "StrikeCalled",
1164
- "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1165
  WhiffIndicator = ifelse(PitchCall == "StrikeSwinging", 1, 0),
1166
- SwingIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "FoulBallNotFieldable",
1167
- "FoulBall", "InPlay"), 1, 0),
1168
  FPindicator = ifelse(Balls == 0 & Strikes == 0, 1, 0),
1169
- FPSindicator = ifelse(PitchCall %in% c("StrikeCalled", "StrikeSwinging",
1170
- "FoulBallNotFieldable", "FoulBall", "InPlay") &
1171
  FPindicator == 1, 1, 0),
1172
- AheadIndicator = ifelse(((Balls == 0 & Strikes == 1) | (Balls == 1 & Strikes == 1)) &
1173
  StrikeIndicator == 1, 1, 0),
1174
- ABindicator = ifelse(PlayResult %in% c("Error", "FieldersChoice", "Out",
1175
- "Single", "Double", "Triple", "HomeRun") |
1176
  KorBB == "Strikeout", 1, 0),
1177
  HitIndicator = ifelse(PlayResult %in% c("Single", "Double", "Triple", "HomeRun"), 1, 0),
1178
- PAindicator = ifelse(PitchCall %in% c("InPlay", "HitByPitch", "CatchersInterference") |
1179
  KorBB %in% c("Walk", "Strikeout"), 1, 0),
1180
- LeadOffIndicator = ifelse((PAofInning == 1 & (PlayResult != "Undefined" | KorBB != "Undefined")) |
1181
  PitchCall == "HitByPitch", 1, 0),
1182
- OutIndicator = ifelse((PlayResult %in% c("Out", "FieldersChoice") | KorBB == "Strikeout") &
1183
  PitchCall != "HitByPitch", 1, 0),
1184
  LOOindicator = ifelse(LeadOffIndicator == 1 & OutIndicator == 1, 1, 0),
1185
  HBPIndicator = ifelse(PitchCall == "HitByPitch", 1, 0),
@@ -1187,7 +1174,7 @@ process_tableau_pitcher_data <- function(df) {
1187
  LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1188
  RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1189
  )
1190
-
1191
  df
1192
  }
1193
 
@@ -1195,48 +1182,48 @@ process_tableau_pitcher_data <- function(df) {
1195
  # HEADER STATISTICS
1196
  # =====================================================================
1197
  calculate_tableau_header_stats <- function(pitcher_df) {
1198
- ab_data <- pitcher_df %>%
1199
  filter(ABindicator == 1) %>%
1200
- group_by(Inning, Batter, PAofInning) %>%
1201
- slice_tail(n = 1) %>%
1202
  ungroup()
1203
-
1204
  at_bats <- nrow(ab_data)
1205
  hits <- sum(ab_data$HitIndicator, na.rm = TRUE)
1206
  xbh <- sum(ab_data$PlayResult %in% c("Double", "Triple", "HomeRun"), na.rm = TRUE)
1207
  runs <- sum(pitcher_df$RunsScored, na.rm = TRUE)
1208
-
1209
- pa_data <- pitcher_df %>%
1210
  filter(PAindicator == 1) %>%
1211
- group_by(Inning, Batter, PAofInning) %>%
1212
- slice_tail(n = 1) %>%
1213
  ungroup()
1214
-
1215
  bb <- sum(pa_data$WalkIndicator, na.rm = TRUE)
1216
  hbp <- sum(pa_data$HBPIndicator, na.rm = TRUE)
1217
  so <- sum(pa_data$KorBB == "Strikeout", na.rm = TRUE)
1218
-
1219
  avg <- ifelse(at_bats > 0, round(hits / at_bats, 3), 0)
1220
-
1221
  total_pitches <- nrow(pitcher_df)
1222
- strike_pct <- ifelse(total_pitches > 0,
1223
  round(100 * sum(pitcher_df$StrikeIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1224
-
1225
  fp_pitches <- sum(pitcher_df$FPindicator, na.rm = TRUE)
1226
- fp_k_pct <- ifelse(fp_pitches > 0,
1227
  round(100 * sum(pitcher_df$FPSindicator, na.rm = TRUE) / fp_pitches, 0), 0)
1228
-
1229
- ea_pct <- ifelse(fp_pitches > 0,
1230
- round(100 * (sum(pitcher_df$FPSindicator, na.rm = TRUE) +
1231
  sum(pitcher_df$AheadIndicator & pitcher_df$Balls == 0, na.rm = TRUE)) / fp_pitches, 0), 0)
1232
-
1233
- comp_pct <- ifelse(total_pitches > 0,
1234
  round(100 * sum(pitcher_df$QualityPitchIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1235
-
1236
  leadoff_opps <- sum(pitcher_df$LeadOffIndicator, na.rm = TRUE)
1237
- loo_pct <- ifelse(leadoff_opps > 0,
1238
  round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
1239
-
1240
  list(
1241
  at_bats = at_bats, hits = hits, xbh = xbh, runs = runs,
1242
  bb_hbp = bb + hbp, so = so,
@@ -1257,11 +1244,11 @@ get_valid_pitch_types <- function(pitcher_df) {
1257
  filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1258
  pull(TaggedPitchType) %>%
1259
  unique()
1260
-
1261
  if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1262
  return(valid_types)
1263
  }
1264
-
1265
  filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1266
  if (length(filter_types) == 0) return("Undefined")
1267
  filter_types
@@ -1269,7 +1256,7 @@ get_valid_pitch_types <- function(pitcher_df) {
1269
 
1270
  calculate_tableau_location_data <- function(pitcher_df) {
1271
  filter_types <- get_valid_pitch_types(pitcher_df)
1272
-
1273
  pitcher_df %>%
1274
  filter(TaggedPitchType %in% filter_types) %>%
1275
  group_by(TaggedPitchType) %>%
@@ -1278,8 +1265,8 @@ calculate_tableau_location_data <- function(pitcher_df) {
1278
  `Edge%` = paste0(round(100 * sum(EdgeIndicator, na.rm = TRUE) / n(), 0), "%"),
1279
  `Strike%` = paste0(round(100 * sum(StrikeIndicator, na.rm = TRUE) / n(), 0), "%"),
1280
  `Whiff%` = ifelse(sum(SwingIndicator, na.rm = TRUE) > 0,
1281
- paste0(round(100 * sum(WhiffIndicator, na.rm = TRUE) / sum(SwingIndicator, na.rm = TRUE), 0), "%"),
1282
- "0%"),
1283
  .groups = "drop"
1284
  )
1285
  }
@@ -1288,7 +1275,7 @@ calculate_tableau_pitch_usage <- function(pitcher_df) {
1288
  lhh_pitches <- sum(pitcher_df$LHHindicator, na.rm = TRUE)
1289
  rhh_pitches <- sum(pitcher_df$RHHindicator, na.rm = TRUE)
1290
  filter_types <- get_valid_pitch_types(pitcher_df)
1291
-
1292
  pitcher_df %>%
1293
  filter(TaggedPitchType %in% filter_types) %>%
1294
  group_by(TaggedPitchType) %>%
@@ -1302,7 +1289,7 @@ calculate_tableau_pitch_usage <- function(pitcher_df) {
1302
 
1303
  calculate_tableau_velo_movement <- function(pitcher_df) {
1304
  filter_types <- get_valid_pitch_types(pitcher_df)
1305
-
1306
  pitcher_df %>%
1307
  filter(TaggedPitchType %in% filter_types) %>%
1308
  group_by(TaggedPitchType) %>%
@@ -1318,12 +1305,12 @@ calculate_tableau_velo_movement <- function(pitcher_df) {
1318
  }
1319
 
1320
  calculate_tableau_release_data <- function(pitcher_df) {
1321
- primary <- pitcher_df %>%
1322
  filter(TaggedPitchType %in% c("Fastball", "Sinker", "Four-Seam", "FourSeamFastBall"))
1323
  fb_ht <- if (nrow(primary) > 0) mean(primary$RelHeight, na.rm = TRUE) else NA
1324
  fb_side <- if (nrow(primary) > 0) mean(primary$RelSide, na.rm = TRUE) else NA
1325
  filter_types <- get_valid_pitch_types(pitcher_df)
1326
-
1327
  pitcher_df %>%
1328
  filter(TaggedPitchType %in% filter_types) %>%
1329
  group_by(TaggedPitchType) %>%
@@ -1338,11 +1325,11 @@ calculate_tableau_release_data <- function(pitcher_df) {
1338
  }
1339
 
1340
  # =====================================================================
1341
- # LOCATION PLOT - Smaller legend, zone higher
1342
  # =====================================================================
1343
  create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1344
  filter_types <- get_valid_pitch_types(pitcher_df)
1345
-
1346
  df <- pitcher_df %>%
1347
  filter(TaggedPitchType %in% filter_types,
1348
  !is.na(PlateLocSide), !is.na(PlateLocHeight)) %>%
@@ -1362,42 +1349,40 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1362
  TRUE ~ "Other"
1363
  )
1364
  )
1365
-
1366
  if (nrow(df) == 0) {
1367
- return(ggplot() + theme_void() +
1368
  labs(title = "Location Report") +
1369
  theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5)))
1370
  }
1371
-
1372
  zone_left <- -0.8333; zone_right <- 0.8333
1373
  zone_bottom <- 1.5; zone_top <- 3.5
1374
  shadow_left <- -1.1; shadow_right <- 1.1
1375
  shadow_bottom <- 1.2; shadow_top <- 3.8
1376
  zone_width <- (zone_right - zone_left) / 3
1377
  zone_height <- (zone_top - zone_bottom) / 3
1378
-
1379
- p <- ggplot(df, aes(x = PlateLocSide, y = PlateLocHeight)) +
1380
- annotate("rect", xmin = shadow_left, xmax = shadow_right,
1381
- ymin = shadow_bottom, ymax = shadow_top,
1382
  fill = NA, color = "gray30", linetype = "dashed", linewidth = 0.5) +
1383
- annotate("rect", xmin = zone_left, xmax = zone_right,
1384
- ymin = zone_bottom, ymax = zone_top,
1385
  fill = NA, color = "#E74C3C", linewidth = 1) +
1386
  annotate("segment", x = zone_left + zone_width, xend = zone_left + zone_width,
1387
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.3) +
1388
  annotate("segment", x = zone_left + 2*zone_width, xend = zone_left + 2*zone_width,
1389
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.3) +
1390
  annotate("segment", x = zone_left, xend = zone_right,
1391
- y = zone_bottom + zone_height, yend = zone_bottom + zone_height,
1392
  color = "gray50", linetype = "dashed", linewidth = 0.3) +
1393
  annotate("segment", x = zone_left, xend = zone_right,
1394
- y = zone_bottom + 2*zone_height, yend = zone_bottom + 2*zone_height,
1395
  color = "gray50", linetype = "dashed", linewidth = 0.3) +
1396
- # Home plate
1397
- annotate("polygon", x = c(-0.708, 0.708, 0.708, 0, -0.708),
1398
  y = c(0.15, 0.15, 0.30, 0.50, 0.30), fill = NA, color = "black", linewidth = 0.5) +
1399
- # Center line
1400
- annotate("segment", x = 0, xend = 0, y = 0.5, yend = shadow_top + 0.2,
1401
  color = "gray60", linetype = "dotted", linewidth = 0.3) +
1402
  geom_point(aes(color = TaggedPitchType, shape = ResultDisplay), size = 2.5, stroke = 0.8) +
1403
  scale_color_manual(values = pitch_colors, name = "Pitch") +
@@ -1411,14 +1396,14 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1411
  labs(title = "Location Report") +
1412
  theme_minimal() +
1413
  theme(
1414
- plot.title = element_text(size = 9, face = "bold", hjust = 0.5),
1415
  legend.position = "left",
1416
- legend.title = element_text(size = 5, face = "bold"),
1417
- legend.text = element_text(size = 4),
1418
- legend.key.size = unit(0.3, "cm"),
1419
- legend.spacing.y = unit(0.05, "cm"),
1420
  legend.margin = margin(0, 0, 0, 0),
1421
- legend.box.margin = margin(0, -5, 0, -10),
1422
  axis.text = element_blank(),
1423
  axis.title = element_blank(),
1424
  axis.ticks = element_blank(),
@@ -1426,15 +1411,13 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1426
  plot.margin = margin(2, 2, 2, 2)
1427
  ) +
1428
  guides(
1429
- color = guide_legend(override.aes = list(size = 2), ncol = 1),
1430
- shape = guide_legend(override.aes = list(size = 2), ncol = 1)
1431
  )
1432
-
1433
- p
1434
  }
1435
 
1436
  # =====================================================================
1437
- # MOVEMENT PLOT
1438
  # =====================================================================
1439
  create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1440
  filter_types <- get_valid_pitch_types(pitcher_df)
@@ -1452,7 +1435,6 @@ create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1452
  ggplot(df, aes(x = HorzBreak, y = InducedVertBreak)) +
1453
  geom_vline(xintercept = 0, color = "black", linewidth = 0.4) +
1454
  geom_hline(yintercept = 0, color = "black", linewidth = 0.4) +
1455
- # shape 21 uses fill for the inside color
1456
  geom_point(aes(fill = TaggedPitchType), size = 4, alpha = 1,
1457
  shape = 21, color = "black", stroke = 0.9) +
1458
  scale_fill_manual(values = pitch_colors, drop = FALSE) +
@@ -1469,7 +1451,7 @@ create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1469
  }
1470
 
1471
  # =====================================================================
1472
- # RELEASE PLOT
1473
  # =====================================================================
1474
  create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1475
  filter_types <- get_valid_pitch_types(pitcher_df)
@@ -1513,57 +1495,47 @@ create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1513
  }
1514
 
1515
  # =====================================================================
1516
- # TABLE DRAWING (BIG + AUTO-SCALED TO FILL A RECTANGLE)
1517
  # =====================================================================
1518
  draw_tableau_table_fill <- function(
1519
  title,
1520
  data,
1521
- rows, # a named vector: names = display label, values = column name in `data`
1522
  pitch_types,
1523
  pitch_colors,
1524
- x, y, # TOP-LEFT anchor of the table block (npc)
1525
- width, height # size of the table block (npc)
1526
  ) {
1527
  if (is.null(pitch_types) || length(pitch_types) == 0) pitch_types <- "Undefined"
1528
 
1529
  n_cols <- length(pitch_types)
1530
- n_rows <- length(rows) + 1 # +1 header row
1531
 
1532
- # Leave some space for the title
1533
- title_h <- min(0.06, height * 0.20)
1534
  table_top <- y - title_h
1535
  table_h <- height - title_h
1536
 
1537
  col_w <- width / n_cols
1538
  row_h <- table_h / n_rows
1539
 
1540
- # Scale font to row height (tuned for letter portrait)
1541
- header_cex <- max(0.9, min(1.6, row_h * 28))
1542
- body_cex <- max(0.85, min(1.5, row_h * 26))
1543
- label_cex <- max(0.85, min(1.5, row_h * 26))
1544
- title_cex <- max(1.1, min(1.9, row_h * 30))
1545
-
1546
- # Title
1547
- grid.text(
1548
- title,
1549
- x = x + width/2,
1550
- y = y,
1551
- gp = gpar(fontface = "bold", cex = title_cex, col = "#006F71")
1552
- )
1553
 
1554
- # Column headers (pitch types)
 
 
 
1555
  for (i in seq_along(pitch_types)) {
1556
  pt <- pitch_types[i]
1557
  col_color <- if (pt %in% names(pitch_colors)) pitch_colors[[pt]] else "#95A5A6"
1558
 
1559
- grid.rect(
1560
- x = x + (i-1)*col_w,
1561
- y = table_top,
1562
- width = col_w * 0.98,
1563
- height = row_h * 0.95,
1564
- just = c("left", "top"),
1565
- gp = gpar(fill = col_color, col = "gray30", lwd = 0.6)
1566
- )
1567
 
1568
  pt_short <- pt
1569
  pt_short <- gsub("ChangeUp|Changeup", "CH", pt_short)
@@ -1575,15 +1547,13 @@ draw_tableau_table_fill <- function(
1575
  pt_short <- gsub("Splitter", "SP", pt_short)
1576
  pt_short <- gsub("Sweeper", "SW", pt_short)
1577
 
1578
- grid.text(
1579
- pt_short,
1580
- x = x + (i-1)*col_w + col_w/2,
1581
- y = table_top - row_h*0.48,
1582
- gp = gpar(col = "white", cex = header_cex, fontface = "bold")
1583
- )
1584
  }
1585
 
1586
- # Data rows
1587
  row_names <- names(rows)
1588
  col_names <- as.character(rows)
1589
 
@@ -1592,14 +1562,11 @@ draw_tableau_table_fill <- function(
1592
  coln <- col_names[r]
1593
  y_row_top <- table_top - r*row_h
1594
 
1595
- # Row label on the left (slightly outside the block)
1596
- grid.text(
1597
- disp,
1598
- x = x - 0.010,
1599
- y = y_row_top - row_h*0.55,
1600
- just = "right",
1601
- gp = gpar(cex = label_cex, fontface = "bold")
1602
- )
1603
 
1604
  for (i in seq_along(pitch_types)) {
1605
  pt <- pitch_types[i]
@@ -1610,25 +1577,172 @@ draw_tableau_table_fill <- function(
1610
  val <- as.character(data[[coln]][idx[1]])
1611
  }
1612
 
1613
- grid.rect(
1614
- x = x + (i-1)*col_w,
1615
- y = y_row_top,
1616
- width = col_w * 0.98,
1617
- height = row_h * 0.95,
1618
- just = c("left", "top"),
1619
- gp = gpar(fill = "white", col = "gray40", lwd = 0.6)
1620
- )
1621
 
1622
- grid.text(
1623
- val,
1624
- x = x + (i-1)*col_w + col_w/2,
1625
- y = y_row_top - row_h*0.55,
1626
- gp = gpar(cex = body_cex, fontface = "plain")
1627
- )
1628
  }
1629
  }
1630
  }
1631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1632
 
1633
  # =====================================================================
1634
  # MAIN PDF FUNCTION
 
1069
  }
1070
 
1071
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
  library(ggplot2)
1073
  library(dplyr)
1074
  library(grid)
 
1105
  alt <- intersect(c("PitcherName","pitcher","Pitcher_LastFirst","PlayerName"), names(df))
1106
  if (length(alt)) df$Pitcher <- df[[alt[1]]] else df$Pitcher <- NA_character_
1107
  }
1108
+
1109
  df <- df %>%
1110
  mutate(Pitcher = str_replace(coalesce(Pitcher, ""), "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
1111
+
1112
  # Clean up TaggedPitchType
1113
  if ("TaggedPitchType" %in% names(df)) {
1114
  df <- df %>%
1115
  mutate(
1116
  TaggedPitchType = case_when(
1117
+ is.na(TaggedPitchType) | TaggedPitchType == "" | TaggedPitchType == "Undefined" ~
1118
+ ifelse("AutoPitchType" %in% names(df) & !is.na(AutoPitchType) & AutoPitchType != "" & AutoPitchType != "Undefined",
1119
+ AutoPitchType,
1120
  ifelse("PitchType" %in% names(df) & !is.na(PitchType) & PitchType != "" & PitchType != "Undefined",
1121
  PitchType, "Undefined")),
1122
  TRUE ~ TaggedPitchType
1123
  )
1124
  )
1125
  }
1126
+
1127
+ # Normalize a few common fastball spellings to improve color mapping consistency
1128
+ df <- df %>%
1129
+ mutate(
1130
+ TaggedPitchType = case_when(
1131
+ TaggedPitchType %in% c("FourSeamFastball","FourSeamFastBall","4-Seam","4-Seam Fast Ball","Four-Seam Fastball","4-Seam Fastball") ~ "Four-Seam",
1132
+ TRUE ~ TaggedPitchType
1133
+ )
1134
+ )
1135
+
1136
  # Calculate indicators
1137
  df <- df %>%
1138
  mutate(
1139
+ StrikeZoneIndicator = ifelse(PlateLocSide >= -0.8333 & PlateLocSide <= 0.8333 &
1140
  PlateLocHeight >= 1.5 & PlateLocHeight <= 3.5, 1, 0),
1141
  EdgeHeightIndicator = ifelse((PlateLocHeight > 14/12 & PlateLocHeight < 22/12) |
1142
  (PlateLocHeight > 38/12 & PlateLocHeight < 46/12), 1, 0),
 
1144
  EdgeZoneWIndicator = ifelse(PlateLocSide > -13.4/12 & PlateLocSide < 13.4/12, 1, 0),
1145
  EdgeWidthIndicator = ifelse((PlateLocSide > -13.3/12 & PlateLocSide < -6.7/12) |
1146
  (PlateLocSide < 13.3/12 & PlateLocSide > 6.7/12), 1, 0),
1147
+ EdgeIndicator = ifelse((EdgeHeightIndicator == 1 & EdgeZoneWIndicator == 1) |
1148
  (EdgeWidthIndicator == 1 & EdgeZoneHtIndicator == 1), 1, 0),
1149
  QualityPitchIndicator = ifelse(StrikeZoneIndicator == 1 | EdgeIndicator == 1, 1, 0),
1150
+ StrikeIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "StrikeCalled",
1151
+ "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1152
  WhiffIndicator = ifelse(PitchCall == "StrikeSwinging", 1, 0),
1153
+ SwingIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "FoulBallNotFieldable",
1154
+ "FoulBall", "InPlay"), 1, 0),
1155
  FPindicator = ifelse(Balls == 0 & Strikes == 0, 1, 0),
1156
+ FPSindicator = ifelse(PitchCall %in% c("StrikeCalled", "StrikeSwinging",
1157
+ "FoulBallNotFieldable", "FoulBall", "InPlay") &
1158
  FPindicator == 1, 1, 0),
1159
+ AheadIndicator = ifelse(((Balls == 0 & Strikes == 1) | (Balls == 1 & Strikes == 1)) &
1160
  StrikeIndicator == 1, 1, 0),
1161
+ ABindicator = ifelse(PlayResult %in% c("Error", "FieldersChoice", "Out",
1162
+ "Single", "Double", "Triple", "HomeRun") |
1163
  KorBB == "Strikeout", 1, 0),
1164
  HitIndicator = ifelse(PlayResult %in% c("Single", "Double", "Triple", "HomeRun"), 1, 0),
1165
+ PAindicator = ifelse(PitchCall %in% c("InPlay", "HitByPitch", "CatchersInterference") |
1166
  KorBB %in% c("Walk", "Strikeout"), 1, 0),
1167
+ LeadOffIndicator = ifelse((PAofInning == 1 & (PlayResult != "Undefined" | KorBB != "Undefined")) |
1168
  PitchCall == "HitByPitch", 1, 0),
1169
+ OutIndicator = ifelse((PlayResult %in% c("Out", "FieldersChoice") | KorBB == "Strikeout") &
1170
  PitchCall != "HitByPitch", 1, 0),
1171
  LOOindicator = ifelse(LeadOffIndicator == 1 & OutIndicator == 1, 1, 0),
1172
  HBPIndicator = ifelse(PitchCall == "HitByPitch", 1, 0),
 
1174
  LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1175
  RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1176
  )
1177
+
1178
  df
1179
  }
1180
 
 
1182
  # HEADER STATISTICS
1183
  # =====================================================================
1184
  calculate_tableau_header_stats <- function(pitcher_df) {
1185
+ ab_data <- pitcher_df %>%
1186
  filter(ABindicator == 1) %>%
1187
+ group_by(Inning, Batter, PAofInning) %>%
1188
+ slice_tail(n = 1) %>%
1189
  ungroup()
1190
+
1191
  at_bats <- nrow(ab_data)
1192
  hits <- sum(ab_data$HitIndicator, na.rm = TRUE)
1193
  xbh <- sum(ab_data$PlayResult %in% c("Double", "Triple", "HomeRun"), na.rm = TRUE)
1194
  runs <- sum(pitcher_df$RunsScored, na.rm = TRUE)
1195
+
1196
+ pa_data <- pitcher_df %>%
1197
  filter(PAindicator == 1) %>%
1198
+ group_by(Inning, Batter, PAofInning) %>%
1199
+ slice_tail(n = 1) %>%
1200
  ungroup()
1201
+
1202
  bb <- sum(pa_data$WalkIndicator, na.rm = TRUE)
1203
  hbp <- sum(pa_data$HBPIndicator, na.rm = TRUE)
1204
  so <- sum(pa_data$KorBB == "Strikeout", na.rm = TRUE)
1205
+
1206
  avg <- ifelse(at_bats > 0, round(hits / at_bats, 3), 0)
1207
+
1208
  total_pitches <- nrow(pitcher_df)
1209
+ strike_pct <- ifelse(total_pitches > 0,
1210
  round(100 * sum(pitcher_df$StrikeIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1211
+
1212
  fp_pitches <- sum(pitcher_df$FPindicator, na.rm = TRUE)
1213
+ fp_k_pct <- ifelse(fp_pitches > 0,
1214
  round(100 * sum(pitcher_df$FPSindicator, na.rm = TRUE) / fp_pitches, 0), 0)
1215
+
1216
+ ea_pct <- ifelse(fp_pitches > 0,
1217
+ round(100 * (sum(pitcher_df$FPSindicator, na.rm = TRUE) +
1218
  sum(pitcher_df$AheadIndicator & pitcher_df$Balls == 0, na.rm = TRUE)) / fp_pitches, 0), 0)
1219
+
1220
+ comp_pct <- ifelse(total_pitches > 0,
1221
  round(100 * sum(pitcher_df$QualityPitchIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1222
+
1223
  leadoff_opps <- sum(pitcher_df$LeadOffIndicator, na.rm = TRUE)
1224
+ loo_pct <- ifelse(leadoff_opps > 0,
1225
  round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
1226
+
1227
  list(
1228
  at_bats = at_bats, hits = hits, xbh = xbh, runs = runs,
1229
  bb_hbp = bb + hbp, so = so,
 
1244
  filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1245
  pull(TaggedPitchType) %>%
1246
  unique()
1247
+
1248
  if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1249
  return(valid_types)
1250
  }
1251
+
1252
  filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1253
  if (length(filter_types) == 0) return("Undefined")
1254
  filter_types
 
1256
 
1257
  calculate_tableau_location_data <- function(pitcher_df) {
1258
  filter_types <- get_valid_pitch_types(pitcher_df)
1259
+
1260
  pitcher_df %>%
1261
  filter(TaggedPitchType %in% filter_types) %>%
1262
  group_by(TaggedPitchType) %>%
 
1265
  `Edge%` = paste0(round(100 * sum(EdgeIndicator, na.rm = TRUE) / n(), 0), "%"),
1266
  `Strike%` = paste0(round(100 * sum(StrikeIndicator, na.rm = TRUE) / n(), 0), "%"),
1267
  `Whiff%` = ifelse(sum(SwingIndicator, na.rm = TRUE) > 0,
1268
+ paste0(round(100 * sum(WhiffIndicator, na.rm = TRUE) / sum(SwingIndicator, na.rm = TRUE), 0), "%"),
1269
+ "0%"),
1270
  .groups = "drop"
1271
  )
1272
  }
 
1275
  lhh_pitches <- sum(pitcher_df$LHHindicator, na.rm = TRUE)
1276
  rhh_pitches <- sum(pitcher_df$RHHindicator, na.rm = TRUE)
1277
  filter_types <- get_valid_pitch_types(pitcher_df)
1278
+
1279
  pitcher_df %>%
1280
  filter(TaggedPitchType %in% filter_types) %>%
1281
  group_by(TaggedPitchType) %>%
 
1289
 
1290
  calculate_tableau_velo_movement <- function(pitcher_df) {
1291
  filter_types <- get_valid_pitch_types(pitcher_df)
1292
+
1293
  pitcher_df %>%
1294
  filter(TaggedPitchType %in% filter_types) %>%
1295
  group_by(TaggedPitchType) %>%
 
1305
  }
1306
 
1307
  calculate_tableau_release_data <- function(pitcher_df) {
1308
+ primary <- pitcher_df %>%
1309
  filter(TaggedPitchType %in% c("Fastball", "Sinker", "Four-Seam", "FourSeamFastBall"))
1310
  fb_ht <- if (nrow(primary) > 0) mean(primary$RelHeight, na.rm = TRUE) else NA
1311
  fb_side <- if (nrow(primary) > 0) mean(primary$RelSide, na.rm = TRUE) else NA
1312
  filter_types <- get_valid_pitch_types(pitcher_df)
1313
+
1314
  pitcher_df %>%
1315
  filter(TaggedPitchType %in% filter_types) %>%
1316
  group_by(TaggedPitchType) %>%
 
1325
  }
1326
 
1327
  # =====================================================================
1328
+ # LOCATION PLOT
1329
  # =====================================================================
1330
  create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1331
  filter_types <- get_valid_pitch_types(pitcher_df)
1332
+
1333
  df <- pitcher_df %>%
1334
  filter(TaggedPitchType %in% filter_types,
1335
  !is.na(PlateLocSide), !is.na(PlateLocHeight)) %>%
 
1349
  TRUE ~ "Other"
1350
  )
1351
  )
1352
+
1353
  if (nrow(df) == 0) {
1354
+ return(ggplot() + theme_void() +
1355
  labs(title = "Location Report") +
1356
  theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5)))
1357
  }
1358
+
1359
  zone_left <- -0.8333; zone_right <- 0.8333
1360
  zone_bottom <- 1.5; zone_top <- 3.5
1361
  shadow_left <- -1.1; shadow_right <- 1.1
1362
  shadow_bottom <- 1.2; shadow_top <- 3.8
1363
  zone_width <- (zone_right - zone_left) / 3
1364
  zone_height <- (zone_top - zone_bottom) / 3
1365
+
1366
+ ggplot(df, aes(x = PlateLocSide, y = PlateLocHeight)) +
1367
+ annotate("rect", xmin = shadow_left, xmax = shadow_right,
1368
+ ymin = shadow_bottom, ymax = shadow_top,
1369
  fill = NA, color = "gray30", linetype = "dashed", linewidth = 0.5) +
1370
+ annotate("rect", xmin = zone_left, xmax = zone_right,
1371
+ ymin = zone_bottom, ymax = zone_top,
1372
  fill = NA, color = "#E74C3C", linewidth = 1) +
1373
  annotate("segment", x = zone_left + zone_width, xend = zone_left + zone_width,
1374
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.3) +
1375
  annotate("segment", x = zone_left + 2*zone_width, xend = zone_left + 2*zone_width,
1376
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.3) +
1377
  annotate("segment", x = zone_left, xend = zone_right,
1378
+ y = zone_bottom + zone_height, yend = zone_bottom + zone_height,
1379
  color = "gray50", linetype = "dashed", linewidth = 0.3) +
1380
  annotate("segment", x = zone_left, xend = zone_right,
1381
+ y = zone_bottom + 2*zone_height, yend = zone_bottom + 2*zone_height,
1382
  color = "gray50", linetype = "dashed", linewidth = 0.3) +
1383
+ annotate("polygon", x = c(-0.708, 0.708, 0.708, 0, -0.708),
 
1384
  y = c(0.15, 0.15, 0.30, 0.50, 0.30), fill = NA, color = "black", linewidth = 0.5) +
1385
+ annotate("segment", x = 0, xend = 0, y = 0.5, yend = shadow_top + 0.2,
 
1386
  color = "gray60", linetype = "dotted", linewidth = 0.3) +
1387
  geom_point(aes(color = TaggedPitchType, shape = ResultDisplay), size = 2.5, stroke = 0.8) +
1388
  scale_color_manual(values = pitch_colors, name = "Pitch") +
 
1396
  labs(title = "Location Report") +
1397
  theme_minimal() +
1398
  theme(
1399
+ plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
1400
  legend.position = "left",
1401
+ legend.title = element_text(size = 7, face = "bold"),
1402
+ legend.text = element_text(size = 6),
1403
+ legend.key.size = unit(0.45, "cm"),
1404
+ legend.spacing.y = unit(0.08, "cm"),
1405
  legend.margin = margin(0, 0, 0, 0),
1406
+ legend.box.margin = margin(0, -4, 0, -6),
1407
  axis.text = element_blank(),
1408
  axis.title = element_blank(),
1409
  axis.ticks = element_blank(),
 
1411
  plot.margin = margin(2, 2, 2, 2)
1412
  ) +
1413
  guides(
1414
+ color = guide_legend(override.aes = list(size = 3), ncol = 1),
1415
+ shape = guide_legend(override.aes = list(size = 3), ncol = 1)
1416
  )
 
 
1417
  }
1418
 
1419
  # =====================================================================
1420
+ # MOVEMENT PLOT (FILLED POINTS = PITCH COLORS)
1421
  # =====================================================================
1422
  create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1423
  filter_types <- get_valid_pitch_types(pitcher_df)
 
1435
  ggplot(df, aes(x = HorzBreak, y = InducedVertBreak)) +
1436
  geom_vline(xintercept = 0, color = "black", linewidth = 0.4) +
1437
  geom_hline(yintercept = 0, color = "black", linewidth = 0.4) +
 
1438
  geom_point(aes(fill = TaggedPitchType), size = 4, alpha = 1,
1439
  shape = 21, color = "black", stroke = 0.9) +
1440
  scale_fill_manual(values = pitch_colors, drop = FALSE) +
 
1451
  }
1452
 
1453
  # =====================================================================
1454
+ # RELEASE PLOT (FILLED POINTS = PITCH COLORS)
1455
  # =====================================================================
1456
  create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1457
  filter_types <- get_valid_pitch_types(pitcher_df)
 
1495
  }
1496
 
1497
  # =====================================================================
1498
+ # TABLE DRAWING (AUTO-SCALED, BUT NOT TOO HUGE)
1499
  # =====================================================================
1500
  draw_tableau_table_fill <- function(
1501
  title,
1502
  data,
1503
+ rows, # named vector: display label -> column name in `data`
1504
  pitch_types,
1505
  pitch_colors,
1506
+ x, y, # TOP-LEFT anchor (npc)
1507
+ width, height # size (npc)
1508
  ) {
1509
  if (is.null(pitch_types) || length(pitch_types) == 0) pitch_types <- "Undefined"
1510
 
1511
  n_cols <- length(pitch_types)
1512
+ n_rows <- length(rows) + 1
1513
 
1514
+ title_h <- min(0.05, height * 0.18)
 
1515
  table_top <- y - title_h
1516
  table_h <- height - title_h
1517
 
1518
  col_w <- width / n_cols
1519
  row_h <- table_h / n_rows
1520
 
1521
+ # Slightly reduced scaling so it doesn't blow up
1522
+ header_cex <- max(0.75, min(1.25, row_h * 24))
1523
+ body_cex <- max(0.70, min(1.15, row_h * 22))
1524
+ label_cex <- max(0.70, min(1.15, row_h * 22))
1525
+ title_cex <- max(0.95, min(1.35, row_h * 26))
 
 
 
 
 
 
 
 
1526
 
1527
+ grid.text(title, x = x + width/2, y = y,
1528
+ gp = gpar(fontface = "bold", cex = title_cex, col = "#006F71"))
1529
+
1530
+ # headers
1531
  for (i in seq_along(pitch_types)) {
1532
  pt <- pitch_types[i]
1533
  col_color <- if (pt %in% names(pitch_colors)) pitch_colors[[pt]] else "#95A5A6"
1534
 
1535
+ grid.rect(x = x + (i-1)*col_w, y = table_top,
1536
+ width = col_w * 0.98, height = row_h * 0.95,
1537
+ just = c("left", "top"),
1538
+ gp = gpar(fill = col_color, col = "gray30", lwd = 0.6))
 
 
 
 
1539
 
1540
  pt_short <- pt
1541
  pt_short <- gsub("ChangeUp|Changeup", "CH", pt_short)
 
1547
  pt_short <- gsub("Splitter", "SP", pt_short)
1548
  pt_short <- gsub("Sweeper", "SW", pt_short)
1549
 
1550
+ grid.text(pt_short,
1551
+ x = x + (i-1)*col_w + col_w/2,
1552
+ y = table_top - row_h*0.55,
1553
+ gp = gpar(col = "white", cex = header_cex, fontface = "bold"))
 
 
1554
  }
1555
 
1556
+ # rows
1557
  row_names <- names(rows)
1558
  col_names <- as.character(rows)
1559
 
 
1562
  coln <- col_names[r]
1563
  y_row_top <- table_top - r*row_h
1564
 
1565
+ grid.text(disp,
1566
+ x = x - 0.010,
1567
+ y = y_row_top - row_h*0.55,
1568
+ just = "right",
1569
+ gp = gpar(cex = label_cex, fontface = "bold"))
 
 
 
1570
 
1571
  for (i in seq_along(pitch_types)) {
1572
  pt <- pitch_types[i]
 
1577
  val <- as.character(data[[coln]][idx[1]])
1578
  }
1579
 
1580
+ grid.rect(x = x + (i-1)*col_w, y = y_row_top,
1581
+ width = col_w * 0.98, height = row_h * 0.95,
1582
+ just = c("left", "top"),
1583
+ gp = gpar(fill = "white", col = "gray40", lwd = 0.6))
 
 
 
 
1584
 
1585
+ grid.text(val,
1586
+ x = x + (i-1)*col_w + col_w/2,
1587
+ y = y_row_top - row_h*0.55,
1588
+ gp = gpar(cex = body_cex))
 
 
1589
  }
1590
  }
1591
  }
1592
 
1593
+ # =====================================================================
1594
+ # MAIN PDF FUNCTION
1595
+ # =====================================================================
1596
+ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1597
+ if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1598
+
1599
+ pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1600
+ filter(Pitcher == pitcher_name)
1601
+
1602
+ if (nrow(pitcher_df) == 0) {
1603
+ pdf(output_file, width = 8.5, height = 11)
1604
+ grid.newpage()
1605
+ grid.text(paste("No data found for", pitcher_name),
1606
+ gp = gpar(fontsize = 16, fontface = "bold"))
1607
+ dev.off()
1608
+ return(output_file)
1609
+ }
1610
+
1611
+ game_date <- tryCatch({
1612
+ d <- unique(pitcher_df$Date)[1]
1613
+ if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
1614
+ }, error = function(e) "NA")
1615
+
1616
+ batter_teams <- unique(pitcher_df$BatterTeam)
1617
+ batter_teams <- batter_teams[!is.na(batter_teams)]
1618
+ away_team <- if (length(batter_teams) > 0) batter_teams[1] else "Unknown"
1619
+
1620
+ stats <- calculate_tableau_header_stats(pitcher_df)
1621
+ loc_data <- calculate_tableau_location_data(pitcher_df)
1622
+ usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1623
+ velo_data <- calculate_tableau_velo_movement(pitcher_df)
1624
+ rel_data <- calculate_tableau_release_data(pitcher_df)
1625
+
1626
+ pitch_types <- unique(c(loc_data$TaggedPitchType, usage_data$TaggedPitchType))
1627
+ pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
1628
+ if (length(pitch_types) == 0) pitch_types <- "Undefined"
1629
+
1630
+ loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
1631
+ mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1632
+ rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1633
+
1634
+ pdf(output_file, width = 8.5, height = 11)
1635
+ on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1636
+
1637
+ grid.newpage()
1638
+
1639
+ # HEADER BAR
1640
+ grid.rect(x = 0, y = 0.955, width = 1, height = 0.045,
1641
+ just = c("left", "bottom"),
1642
+ gp = gpar(fill = "#006F71", col = NA))
1643
+
1644
+ grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.977, just = "left",
1645
+ gp = gpar(col = "white", fontface = "bold", cex = 1.2))
1646
+
1647
+ # INFO ROW
1648
+ info_y <- 0.935
1649
+ grid.text(paste0(game_date, " | ", pitcher_name, " vs ", away_team),
1650
+ x = 0.02, y = info_y, just = "left",
1651
+ gp = gpar(cex = 0.8, fontface = "bold"))
1652
+
1653
+ # STAT BOXES (BIGGER)
1654
+ stat_labels <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG",
1655
+ "Strike%", "1st P K%", "E+A%", "Comp%", "LOO%")
1656
+ stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs,
1657
+ stats$bb_hbp, stats$so, stats$avg,
1658
+ stats$strike_pct, stats$fp_k_pct, stats$ea_pct,
1659
+ stats$comp_pct, stats$loo_pct)
1660
+
1661
+ label_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60", "#00BCD4",
1662
+ "#E67E22", "#E67E22", "#E67E22", "#E67E22", "#E67E22")
1663
+
1664
+ box_y <- 0.895
1665
+ box_w <- 0.072
1666
+ box_h <- 0.050
1667
+ x_start <- 0.02
1668
+ x_gap <- 0.010
1669
+
1670
+ for (i in 1:12) {
1671
+ x_pos <- x_start + (i-1) * (box_w + x_gap)
1672
+
1673
+ grid.rect(x = x_pos, y = box_y, width = box_w, height = box_h,
1674
+ just = c("left", "center"),
1675
+ gp = gpar(fill = "white", col = label_colors[i], lwd = 2))
1676
+
1677
+ grid.text(stat_labels[i], x = x_pos + box_w/2, y = box_y + 0.015,
1678
+ gp = gpar(col = label_colors[i], cex = 0.55, fontface = "bold"))
1679
+
1680
+ grid.text(as.character(stat_values[i]), x = x_pos + box_w/2, y = box_y - 0.010,
1681
+ gp = gpar(col = "black", cex = 0.80, fontface = "bold"))
1682
+ }
1683
+
1684
+ # CHARTS (Location directly under stats; similar sizing)
1685
+ pushViewport(viewport(x = 0.23, y = 0.66, width = 0.42, height = 0.30))
1686
+ print(loc_plot, newpage = FALSE)
1687
+ popViewport()
1688
+
1689
+ pushViewport(viewport(x = 0.23, y = 0.37, width = 0.40, height = 0.24))
1690
+ print(mov_plot, newpage = FALSE)
1691
+ popViewport()
1692
+
1693
+ pushViewport(viewport(x = 0.23, y = 0.13, width = 0.42, height = 0.24))
1694
+ print(rel_plot, newpage = FALSE)
1695
+ popViewport()
1696
+
1697
+ # TABLES (Top third = Location + Usage; middle = Velo; bottom = Release)
1698
+ table_x <- 0.56
1699
+ table_w <- 0.41
1700
+
1701
+ draw_tableau_table_fill(
1702
+ title = "Location Data",
1703
+ data = loc_data,
1704
+ rows = c("Zone%"="Zone%", "Edge%"="Edge%", "Strike%"="Strike%", "Whiff%"="Whiff%"),
1705
+ pitch_types = pitch_types,
1706
+ pitch_colors = tableau_pitch_colors,
1707
+ x = table_x, y = 0.78, width = table_w, height = 0.16
1708
+ )
1709
+
1710
+ draw_tableau_table_fill(
1711
+ title = "Pitch Usage",
1712
+ data = usage_data,
1713
+ rows = c("Usage vs. LHH"="Usage vs. LHH", "Usage vs. RHH"="Usage vs. RHH", "Pitch Count"="Pitch Count"),
1714
+ pitch_types = pitch_types,
1715
+ pitch_colors = tableau_pitch_colors,
1716
+ x = table_x, y = 0.60, width = table_w, height = 0.14
1717
+ )
1718
+
1719
+ draw_tableau_table_fill(
1720
+ title = "Velo & Movement",
1721
+ data = velo_data,
1722
+ rows = c("Avg. Velo"="Avg. Velo", "Max Velo"="Max. Velo",
1723
+ "Avg. Spin"="Avg. Spin", "Max Spin"="Max. Spin",
1724
+ "Avg. IVB"="Avg. IVB", "Avg. HB"="Avg. HB"),
1725
+ pitch_types = pitch_types,
1726
+ pitch_colors = tableau_pitch_colors,
1727
+ x = table_x, y = 0.42, width = table_w, height = 0.20
1728
+ )
1729
+
1730
+ draw_tableau_table_fill(
1731
+ title = "Release Data",
1732
+ data = rel_data,
1733
+ rows = c("Rel Ht"="Avg. Rel Ht",
1734
+ "Rel Ht vs FB (in)"="Rel Ht vs. FB",
1735
+ "Rel Side"="Avg. Rel Side",
1736
+ "Rel Side vs FB (in)"="Rel Side vs. FB",
1737
+ "Ext"="Avg. Ext"),
1738
+ pitch_types = pitch_types,
1739
+ pitch_colors = tableau_pitch_colors,
1740
+ x = table_x, y = 0.20, width = table_w, height = 0.22
1741
+ )
1742
+
1743
+ invisible(output_file)
1744
+ }
1745
+
1746
 
1747
  # =====================================================================
1748
  # MAIN PDF FUNCTION