igroffman commited on
Commit
c118bfb
·
verified ·
1 Parent(s): 3f542c1

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +225 -278
app.R CHANGED
@@ -1081,10 +1081,25 @@ create_postgame_pdf <- function(game_df, player_name, output_file, bio_data = NU
1081
  # =====================================================================
1082
 
1083
  # =====================================================================
1084
- # PITCH COLORS - Extended to handle more variations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  # =====================================================================
1086
  tableau_pitch_colors <- c(
1087
- # Primary pitches
1088
  "Sinker" = "#5DADE2",
1089
  "Slider" = "#E67E22",
1090
  "Cutter" = "#F4D03F",
@@ -1105,10 +1120,9 @@ tableau_pitch_colors <- c(
1105
  )
1106
 
1107
  # =====================================================================
1108
- # IMPROVED DATA PROCESSING - Better pitch type handling
1109
  # =====================================================================
1110
  process_tableau_pitcher_data <- function(df) {
1111
- # Standardize Pitcher name
1112
  if (!"Pitcher" %in% names(df)) {
1113
  alt <- intersect(c("PitcherName","pitcher","Pitcher_LastFirst","PlayerName"), names(df))
1114
  if (length(alt)) df$Pitcher <- df[[alt[1]]] else df$Pitcher <- NA_character_
@@ -1117,29 +1131,26 @@ process_tableau_pitcher_data <- function(df) {
1117
  df <- df %>%
1118
  mutate(Pitcher = str_replace(coalesce(Pitcher, ""), "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
1119
 
1120
- # CRITICAL: Clean up TaggedPitchType - replace empty/NA/Undefined with actual pitch type if available
1121
  if ("TaggedPitchType" %in% names(df)) {
1122
  df <- df %>%
1123
  mutate(
1124
  TaggedPitchType = case_when(
1125
  is.na(TaggedPitchType) | TaggedPitchType == "" | TaggedPitchType == "Undefined" ~
1126
- ifelse(!is.na(AutoPitchType) & AutoPitchType != "" & AutoPitchType != "Undefined",
1127
  AutoPitchType,
1128
- ifelse(!is.na(PitchType) & PitchType != "" & PitchType != "Undefined",
1129
  PitchType, "Undefined")),
1130
  TRUE ~ TaggedPitchType
1131
  )
1132
  )
1133
  }
1134
 
1135
- # Calculate all indicators
1136
  df <- df %>%
1137
  mutate(
1138
- # Strike Zone Indicator
1139
  StrikeZoneIndicator = ifelse(PlateLocSide >= -0.8333 & PlateLocSide <= 0.8333 &
1140
  PlateLocHeight >= 1.5 & PlateLocHeight <= 3.5, 1, 0),
1141
-
1142
- # Edge calculations
1143
  EdgeHeightIndicator = ifelse((PlateLocHeight > 14/12 & PlateLocHeight < 22/12) |
1144
  (PlateLocHeight > 38/12 & PlateLocHeight < 46/12), 1, 0),
1145
  EdgeZoneHtIndicator = ifelse(PlateLocHeight > 16/12 & PlateLocHeight < 45.2/12, 1, 0),
@@ -1148,41 +1159,31 @@ process_tableau_pitcher_data <- function(df) {
1148
  (PlateLocSide < 13.3/12 & PlateLocSide > 6.7/12), 1, 0),
1149
  EdgeIndicator = ifelse((EdgeHeightIndicator == 1 & EdgeZoneWIndicator == 1) |
1150
  (EdgeWidthIndicator == 1 & EdgeZoneHtIndicator == 1), 1, 0),
1151
-
1152
  QualityPitchIndicator = ifelse(StrikeZoneIndicator == 1 | EdgeIndicator == 1, 1, 0),
1153
-
1154
  StrikeIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "StrikeCalled",
1155
  "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1156
  WhiffIndicator = ifelse(PitchCall == "StrikeSwinging", 1, 0),
1157
  SwingIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "FoulBallNotFieldable",
1158
  "FoulBall", "InPlay"), 1, 0),
1159
-
1160
  FPindicator = ifelse(Balls == 0 & Strikes == 0, 1, 0),
1161
  FPSindicator = ifelse(PitchCall %in% c("StrikeCalled", "StrikeSwinging",
1162
  "FoulBallNotFieldable", "FoulBall", "InPlay") &
1163
  FPindicator == 1, 1, 0),
1164
-
1165
  AheadIndicator = ifelse(((Balls == 0 & Strikes == 1) | (Balls == 1 & Strikes == 1)) &
1166
  StrikeIndicator == 1, 1, 0),
1167
-
1168
  ABindicator = ifelse(PlayResult %in% c("Error", "FieldersChoice", "Out",
1169
  "Single", "Double", "Triple", "HomeRun") |
1170
  KorBB == "Strikeout", 1, 0),
1171
-
1172
  HitIndicator = ifelse(PlayResult %in% c("Single", "Double", "Triple", "HomeRun"), 1, 0),
1173
-
1174
  PAindicator = ifelse(PitchCall %in% c("InPlay", "HitByPitch", "CatchersInterference") |
1175
  KorBB %in% c("Walk", "Strikeout"), 1, 0),
1176
-
1177
  LeadOffIndicator = ifelse((PAofInning == 1 & (PlayResult != "Undefined" | KorBB != "Undefined")) |
1178
  PitchCall == "HitByPitch", 1, 0),
1179
  OutIndicator = ifelse((PlayResult %in% c("Out", "FieldersChoice") | KorBB == "Strikeout") &
1180
  PitchCall != "HitByPitch", 1, 0),
1181
  LOOindicator = ifelse(LeadOffIndicator == 1 & OutIndicator == 1, 1, 0),
1182
-
1183
  HBPIndicator = ifelse(PitchCall == "HitByPitch", 1, 0),
1184
  WalkIndicator = ifelse(KorBB == "Walk", 1, 0),
1185
-
1186
  LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1187
  RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1188
  )
@@ -1237,12 +1238,8 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1237
  round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
1238
 
1239
  list(
1240
- at_bats = at_bats,
1241
- hits = hits,
1242
- xbh = xbh,
1243
- runs = runs,
1244
- bb_hbp = bb + hbp,
1245
- so = so,
1246
  avg = sprintf("%.3f", avg),
1247
  strike_pct = paste0(strike_pct, "%"),
1248
  fp_k_pct = paste0(fp_k_pct, "%"),
@@ -1253,23 +1250,25 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1253
  }
1254
 
1255
  # =====================================================================
1256
- # TABLE CALCULATIONS - Now handles "Undefined" pitch types
1257
  # =====================================================================
1258
- calculate_tableau_location_data <- function(pitcher_df) {
1259
- # Get all pitch types including "Undefined" if that's all we have
1260
  valid_types <- pitcher_df %>%
1261
  filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1262
  pull(TaggedPitchType) %>%
1263
  unique()
1264
 
1265
- # If only "Undefined" exists, use it; otherwise exclude it
1266
  if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1267
- filter_types <- valid_types
1268
- } else {
1269
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1270
  }
1271
 
1272
- if (length(filter_types) == 0) filter_types <- "Undefined"
 
 
 
 
 
 
1273
 
1274
  pitcher_df %>%
1275
  filter(TaggedPitchType %in% filter_types) %>%
@@ -1288,19 +1287,7 @@ calculate_tableau_location_data <- function(pitcher_df) {
1288
  calculate_tableau_pitch_usage <- function(pitcher_df) {
1289
  lhh_pitches <- sum(pitcher_df$LHHindicator, na.rm = TRUE)
1290
  rhh_pitches <- sum(pitcher_df$RHHindicator, na.rm = TRUE)
1291
-
1292
- valid_types <- pitcher_df %>%
1293
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1294
- pull(TaggedPitchType) %>%
1295
- unique()
1296
-
1297
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1298
- filter_types <- valid_types
1299
- } else {
1300
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1301
- }
1302
-
1303
- if (length(filter_types) == 0) filter_types <- "Undefined"
1304
 
1305
  pitcher_df %>%
1306
  filter(TaggedPitchType %in% filter_types) %>%
@@ -1314,18 +1301,7 @@ calculate_tableau_pitch_usage <- function(pitcher_df) {
1314
  }
1315
 
1316
  calculate_tableau_velo_movement <- function(pitcher_df) {
1317
- valid_types <- pitcher_df %>%
1318
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1319
- pull(TaggedPitchType) %>%
1320
- unique()
1321
-
1322
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1323
- filter_types <- valid_types
1324
- } else {
1325
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1326
- }
1327
-
1328
- if (length(filter_types) == 0) filter_types <- "Undefined"
1329
 
1330
  pitcher_df %>%
1331
  filter(TaggedPitchType %in% filter_types) %>%
@@ -1333,10 +1309,10 @@ calculate_tableau_velo_movement <- function(pitcher_df) {
1333
  summarise(
1334
  `Avg. Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
1335
  `Max. Velo` = round(max(RelSpeed, na.rm = TRUE), 1),
1336
- `Avg. Spin Rate` = format(round(mean(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1337
- `Max. Spin Rate` = format(round(max(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1338
- `Avg. Vert Break` = round(mean(InducedVertBreak, na.rm = TRUE), 0),
1339
- `Avg. Horz Break` = round(mean(HorzBreak, na.rm = TRUE), 0),
1340
  .groups = "drop"
1341
  )
1342
  }
@@ -1346,66 +1322,42 @@ calculate_tableau_release_data <- function(pitcher_df) {
1346
  filter(TaggedPitchType %in% c("Fastball", "Sinker", "Four-Seam", "FourSeamFastBall"))
1347
  fb_ht <- if (nrow(primary) > 0) mean(primary$RelHeight, na.rm = TRUE) else NA
1348
  fb_side <- if (nrow(primary) > 0) mean(primary$RelSide, na.rm = TRUE) else NA
1349
-
1350
- valid_types <- pitcher_df %>%
1351
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1352
- pull(TaggedPitchType) %>%
1353
- unique()
1354
-
1355
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1356
- filter_types <- valid_types
1357
- } else {
1358
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1359
- }
1360
-
1361
- if (length(filter_types) == 0) filter_types <- "Undefined"
1362
 
1363
  pitcher_df %>%
1364
  filter(TaggedPitchType %in% filter_types) %>%
1365
  group_by(TaggedPitchType) %>%
1366
  summarise(
1367
- `Avg. Rel Height (ft.)` = round(mean(RelHeight, na.rm = TRUE), 2),
1368
- `Rel Ht vs. FB (in.)` = ifelse(is.na(fb_ht), NA, round((mean(RelHeight, na.rm = TRUE) - fb_ht) * 12, 0)),
1369
- `Avg. Rel Side (ft.)` = round(mean(RelSide, na.rm = TRUE), 2),
1370
- `Rel Side vs. FB (in.)` = ifelse(is.na(fb_side), NA, round((mean(RelSide, na.rm = TRUE) - fb_side) * 12, 0)),
1371
- `Avg. Extension (ft.)` = round(mean(Extension, na.rm = TRUE), 2),
1372
  .groups = "drop"
1373
  )
1374
  }
1375
 
1376
  # =====================================================================
1377
- # LOCATION PLOT
1378
  # =====================================================================
1379
  create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1380
- # Get valid pitch types
1381
- valid_types <- pitcher_df %>%
1382
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1383
- pull(TaggedPitchType) %>%
1384
- unique()
1385
-
1386
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1387
- filter_types <- valid_types
1388
- } else {
1389
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1390
- }
1391
-
1392
- if (length(filter_types) == 0) filter_types <- "Undefined"
1393
 
1394
  df <- pitcher_df %>%
1395
  filter(TaggedPitchType %in% filter_types,
1396
  !is.na(PlateLocSide), !is.na(PlateLocHeight)) %>%
1397
  mutate(
1398
  ResultDisplay = case_when(
1399
- PitchCall %in% c("BallCalled", "BallinDirt") ~ "BallCalled",
1400
- PlayResult == "Double" ~ "Double",
1401
- PitchCall %in% c("FoulBall", "FoulBallNotFieldable") ~ "FoulBall",
1402
- PitchCall == "HitByPitch" ~ "HitByPitch",
1403
- PlayResult %in% c("Sacrifice", "SacrificeFly") ~ "Sacrifice",
1404
- PlayResult == "Single" ~ "Single",
1405
- PitchCall == "StrikeCalled" ~ "StrikeCalled",
1406
- PitchCall == "StrikeSwinging" ~ "StrikeSwinging",
1407
- PlayResult == "Triple" ~ "Triple",
1408
- PlayResult == "HomeRun" ~ "HomeRun",
1409
  PlayResult == "Out" ~ "Out",
1410
  TRUE ~ "Other"
1411
  )
@@ -1414,7 +1366,7 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1414
  if (nrow(df) == 0) {
1415
  return(ggplot() + theme_void() +
1416
  labs(title = "Location Report") +
1417
- theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5)))
1418
  }
1419
 
1420
  zone_left <- -0.8333; zone_right <- 0.8333
@@ -1427,47 +1379,55 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1427
  p <- ggplot(df, aes(x = PlateLocSide, y = PlateLocHeight)) +
1428
  annotate("rect", xmin = shadow_left, xmax = shadow_right,
1429
  ymin = shadow_bottom, ymax = shadow_top,
1430
- fill = NA, color = "gray30", linetype = "dashed", linewidth = 0.6) +
1431
  annotate("rect", xmin = zone_left, xmax = zone_right,
1432
  ymin = zone_bottom, ymax = zone_top,
1433
- fill = NA, color = "#E74C3C", linewidth = 1.2) +
1434
  annotate("segment", x = zone_left + zone_width, xend = zone_left + zone_width,
1435
- y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.4) +
1436
  annotate("segment", x = zone_left + 2*zone_width, xend = zone_left + 2*zone_width,
1437
- y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.4) +
1438
  annotate("segment", x = zone_left, xend = zone_right,
1439
  y = zone_bottom + zone_height, yend = zone_bottom + zone_height,
1440
- color = "gray50", linetype = "dashed", linewidth = 0.4) +
1441
  annotate("segment", x = zone_left, xend = zone_right,
1442
  y = zone_bottom + 2*zone_height, yend = zone_bottom + 2*zone_height,
1443
- color = "gray50", linetype = "dashed", linewidth = 0.4) +
1444
- annotate("segment", x = -0.708, y = 0.15, xend = 0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1445
- annotate("segment", x = -0.708, y = 0.30, xend = -0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1446
- annotate("segment", x = 0.708, y = 0.30, xend = 0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1447
- annotate("segment", x = -0.708, y = 0.30, xend = 0, yend = 0.50, color = "black", linewidth = 0.6) +
1448
- annotate("segment", x = 0.708, y = 0.30, xend = 0, yend = 0.50, color = "black", linewidth = 0.6) +
1449
- annotate("segment", x = 0, xend = 0, y = 0.5, yend = shadow_top + 0.3,
1450
- color = "gray60", linetype = "dotted", linewidth = 0.4) +
1451
- geom_point(aes(color = TaggedPitchType, shape = ResultDisplay), size = 3, stroke = 1) +
1452
- scale_color_manual(values = pitch_colors, name = "Pitch Type") +
1453
  scale_shape_manual(
1454
- values = c("BallCalled" = 1, "Double" = 18, "FoulBall" = 2, "HitByPitch" = 8,
1455
- "Sacrifice" = 3, "Single" = 19, "StrikeCalled" = 4, "StrikeSwinging" = 8,
1456
- "Triple" = 17, "HomeRun" = 18, "Out" = 4, "Other" = 16),
1457
- name = "Play Result"
1458
  ) +
1459
- coord_fixed(xlim = c(-2, 2), ylim = c(-0.2, 4.2)) +
1460
  labs(title = "Location Report") +
1461
  theme_minimal() +
1462
  theme(
1463
- plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
1464
  legend.position = "left",
1465
- legend.title = element_text(size = 7, face = "bold"),
1466
- legend.text = element_text(size = 6),
 
 
 
 
1467
  axis.text = element_blank(),
1468
  axis.title = element_blank(),
1469
  axis.ticks = element_blank(),
1470
- panel.grid = element_blank()
 
 
 
 
 
1471
  )
1472
 
1473
  p
@@ -1477,18 +1437,7 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1477
  # MOVEMENT PLOT
1478
  # =====================================================================
1479
  create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1480
- valid_types <- pitcher_df %>%
1481
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1482
- pull(TaggedPitchType) %>%
1483
- unique()
1484
-
1485
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1486
- filter_types <- valid_types
1487
- } else {
1488
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1489
- }
1490
-
1491
- if (length(filter_types) == 0) filter_types <- "Undefined"
1492
 
1493
  df <- pitcher_df %>%
1494
  filter(TaggedPitchType %in% filter_types,
@@ -1497,41 +1446,31 @@ create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1497
  if (nrow(df) == 0) {
1498
  return(ggplot() + theme_void() +
1499
  labs(title = "Movement Profile") +
1500
- theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5)))
1501
  }
1502
 
1503
  ggplot(df, aes(x = HorzBreak, y = InducedVertBreak, color = TaggedPitchType)) +
1504
- geom_vline(xintercept = 0, color = "black", linewidth = 0.5) +
1505
- geom_hline(yintercept = 0, color = "black", linewidth = 0.5) +
1506
- geom_point(size = 3, alpha = 0.8) +
1507
  scale_color_manual(values = pitch_colors) +
1508
  coord_cartesian(xlim = c(-25, 25), ylim = c(-25, 25)) +
1509
- labs(title = "Movement Profile", x = "Horz Break", y = "Induced Vert Break") +
1510
  theme_minimal() +
1511
  theme(
1512
- plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
1513
  legend.position = "none",
1514
- axis.title = element_text(size = 8),
1515
- axis.text = element_text(size = 7)
 
1516
  )
1517
  }
1518
 
1519
  # =====================================================================
1520
- # RELEASE PLOT - WITH MOUND
1521
  # =====================================================================
1522
  create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1523
- valid_types <- pitcher_df %>%
1524
- filter(!is.na(TaggedPitchType), TaggedPitchType != "") %>%
1525
- pull(TaggedPitchType) %>%
1526
- unique()
1527
-
1528
- if (length(valid_types) == 1 && valid_types[1] == "Undefined") {
1529
- filter_types <- valid_types
1530
- } else {
1531
- filter_types <- valid_types[valid_types != "Undefined" & valid_types != "Other"]
1532
- }
1533
-
1534
- if (length(filter_types) == 0) filter_types <- "Undefined"
1535
 
1536
  df <- pitcher_df %>%
1537
  filter(TaggedPitchType %in% filter_types,
@@ -1540,9 +1479,10 @@ create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1540
  if (nrow(df) == 0) {
1541
  return(ggplot() + theme_void() +
1542
  labs(title = "Release Plot") +
1543
- theme(plot.title = element_text(size = 12, face = "bold", hjust = 0.5)))
1544
  }
1545
 
 
1546
  mound_theta <- seq(0, pi, length.out = 100)
1547
  mound_radius <- 3
1548
  mound_df <- data.frame(
@@ -1555,50 +1495,72 @@ create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1555
  fill = "#C0392B", color = NA, inherit.aes = FALSE) +
1556
  annotate("rect", xmin = -0.5, xmax = 0.5, ymin = 0.85, ymax = 1.05,
1557
  fill = "white", color = "gray40", linewidth = 0.3) +
1558
- geom_vline(xintercept = 0, color = "gray60", linetype = "dashed", linewidth = 0.4) +
1559
- geom_point(aes(color = TaggedPitchType), size = 3, alpha = 0.8) +
1560
  scale_color_manual(values = pitch_colors) +
1561
  coord_cartesian(xlim = c(-4, 4), ylim = c(0, 7)) +
1562
- labs(title = "Release Plot", x = "Rel Side", y = "Rel Height") +
1563
  theme_minimal() +
1564
  theme(
1565
- plot.title = element_text(size = 12, face = "bold", hjust = 0.5),
1566
  legend.position = "none",
1567
- axis.title = element_text(size = 8),
1568
- axis.text = element_text(size = 7)
 
1569
  )
1570
  }
1571
 
1572
  # =====================================================================
1573
- # TABLE DRAWING FUNCTION
1574
  # =====================================================================
1575
- draw_tableau_table <- function(title, data, metrics, pitch_types, pitch_colors,
1576
- x_start, y_start, col_w = 0.10, row_h = 0.016) {
1577
 
1578
- grid.text(title, x = x_start + length(pitch_types) * col_w / 2, y = y_start + 0.025,
1579
- gp = gpar(fontface = "bold", cex = 0.8))
 
1580
 
 
1581
  header_y <- y_start
1582
  for (i in seq_along(pitch_types)) {
1583
  pt <- pitch_types[i]
1584
  col_color <- if (pt %in% names(pitch_colors)) pitch_colors[pt] else "#95A5A6"
1585
 
1586
  grid.rect(x = x_start + (i-1)*col_w, y = header_y,
1587
- width = col_w * 0.95, height = row_h * 1.1,
1588
  just = c("left", "center"),
1589
- gp = gpar(fill = col_color, col = "black", lwd = 0.5))
 
 
 
 
 
 
 
 
 
 
1590
 
1591
- grid.text(pt, x = x_start + (i-1)*col_w + col_w/2, y = header_y,
1592
- gp = gpar(col = "white", cex = 0.38, fontface = "bold"))
1593
  }
1594
 
 
1595
  for (m in seq_along(metrics)) {
1596
  metric_name <- metrics[m]
1597
  y_pos <- header_y - m * row_h
1598
 
1599
- grid.text(metric_name, x = x_start - 0.015, y = y_pos, just = "right",
1600
- gp = gpar(cex = 0.38, fontface = "bold"))
 
 
 
 
 
 
 
1601
 
 
1602
  for (i in seq_along(pitch_types)) {
1603
  pt <- pitch_types[i]
1604
  idx <- which(data$TaggedPitchType == pt)
@@ -1609,18 +1571,18 @@ draw_tableau_table <- function(title, data, metrics, pitch_types, pitch_colors,
1609
  }
1610
 
1611
  grid.rect(x = x_start + (i-1)*col_w, y = y_pos,
1612
- width = col_w * 0.95, height = row_h * 0.9,
1613
  just = c("left", "center"),
1614
- gp = gpar(fill = "white", col = "gray80", lwd = 0.3))
1615
 
1616
  grid.text(val, x = x_start + (i-1)*col_w + col_w/2, y = y_pos,
1617
- gp = gpar(cex = 0.36))
1618
  }
1619
  }
1620
  }
1621
 
1622
  # =====================================================================
1623
- # MAIN PDF CREATION FUNCTION - PORTRAIT LAYOUT
1624
  # =====================================================================
1625
  create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1626
  if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
@@ -1637,18 +1599,16 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1637
  return(output_file)
1638
  }
1639
 
1640
- # Get metadata
1641
  game_date <- tryCatch({
1642
  d <- unique(pitcher_df$Date)[1]
1643
  if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
1644
  }, error = function(e) "NA")
1645
 
1646
- pitcher_team <- unique(pitcher_df$PitcherTeam)[1]
1647
- if (is.na(pitcher_team)) pitcher_team <- "Unknown"
1648
-
1649
  batter_teams <- unique(pitcher_df$BatterTeam)
1650
- batter_team_str <- if (length(batter_teams) > 1) paste(batter_teams, collapse = " / ") else batter_teams[1]
1651
- if (is.na(batter_team_str)) batter_team_str <- "Unknown"
1652
 
1653
  # Calculate stats
1654
  stats <- calculate_tableau_header_stats(pitcher_df)
@@ -1657,7 +1617,6 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1657
  velo_data <- calculate_tableau_velo_movement(pitcher_df)
1658
  rel_data <- calculate_tableau_release_data(pitcher_df)
1659
 
1660
- # Get pitch types
1661
  pitch_types <- unique(c(loc_data$TaggedPitchType, usage_data$TaggedPitchType))
1662
  pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
1663
  if (length(pitch_types) == 0) pitch_types <- "Undefined"
@@ -1667,148 +1626,136 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1667
  mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1668
  rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1669
 
1670
- # Create PDF - PORTRAIT (8.5 x 11)
1671
  pdf(output_file, width = 8.5, height = 11)
1672
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1673
 
1674
  grid.newpage()
1675
 
1676
  # =====================================================================
1677
- # HEADER BAR (Teal)
1678
  # =====================================================================
1679
- grid.rect(x = 0, y = 0.95, width = 1, height = 0.05,
1680
  just = c("left", "bottom"),
1681
  gp = gpar(fill = "#006F71", col = NA))
1682
 
1683
- grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.975, just = "left",
1684
- gp = gpar(col = "white", fontface = "bold", cex = 1.3))
1685
 
1686
  # =====================================================================
1687
- # INFO ROW - NO LABELS, just values
1688
  # =====================================================================
1689
- info_y <- 0.925
1690
- info_values <- c(game_date, pitcher_team, pitcher_name, batter_team_str)
1691
- info_x_positions <- c(0.05, 0.22, 0.42, 0.70)
1692
-
1693
- for (i in 1:4) {
1694
- grid.text(info_values[i], x = info_x_positions[i], y = info_y, just = "left",
1695
- gp = gpar(cex = 0.7, fontface = "bold"))
1696
- }
1697
 
1698
  # =====================================================================
1699
- # STAT BOXES - SINGLE ROW matching screenshot colors
1700
  # =====================================================================
1701
- # Colors from screenshot:
1702
- # At Bats = Red, H/XBH = Blue, R/BB+HBP/SO = Green, AVG = Cyan
1703
- # Strike%/1st P K%/E+A%/Comp%/LOO% = Orange text on white
1704
-
1705
- stat_labels <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG", "Strike%", "1st P K..", "E+A%", "Comp..", "LOO%")
1706
- stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs, stats$bb_hbp, stats$so, stats$avg,
1707
- stats$strike_pct, stats$fp_k_pct, stats$ea_pct, stats$comp_pct, stats$loo_pct)
1708
- stat_bg_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60", "#00BCD4",
1709
- "white", "white", "white", "white", "white")
1710
- stat_text_colors <- c("white", "white", "white", "white", "white", "white", "white",
1711
- "#E67E22", "#E67E22", "#E67E22", "#E67E22", "#E67E22")
1712
-
1713
- box_y <- 0.885
1714
- box_w <- 0.072
1715
- box_h <- 0.045
1716
  x_start <- 0.02
1717
 
1718
  for (i in 1:12) {
1719
- x_pos <- x_start + (i-1) * (box_w + 0.005)
1720
 
 
1721
  grid.rect(x = x_pos, y = box_y, width = box_w, height = box_h,
1722
  just = c("left", "center"),
1723
- gp = gpar(fill = stat_bg_colors[i], col = "black", lwd = 0.5))
1724
 
1725
- # Label at top
1726
- grid.text(stat_labels[i], x = x_pos + box_w/2, y = box_y + 0.012,
1727
- gp = gpar(col = stat_text_colors[i], cex = 0.42, fontface = "bold"))
1728
 
1729
- # Value below
1730
- grid.text(as.character(stat_values[i]), x = x_pos + box_w/2, y = box_y - 0.008,
1731
- gp = gpar(col = stat_text_colors[i], cex = 0.55, fontface = "bold"))
1732
  }
1733
 
1734
  # =====================================================================
1735
- # LOCATION PLOT (Left side - SMALLER)
1736
  # =====================================================================
1737
- pushViewport(viewport(x = 0.25, y = 0.62, width = 0.42, height = 0.26))
 
 
1738
  print(loc_plot, newpage = FALSE)
1739
  popViewport()
1740
 
 
 
 
 
 
 
 
 
 
 
1741
  # =====================================================================
1742
- # LOCATION DATA TABLE (Right side)
1743
  # =====================================================================
1744
- draw_tableau_table(
 
 
 
 
 
 
 
1745
  title = "Location Data",
1746
  data = loc_data,
1747
  metrics = c("Zone%", "Edge%", "Strike%", "Whiff%"),
1748
  pitch_types = pitch_types,
1749
  pitch_colors = tableau_pitch_colors,
1750
- x_start = 0.52, y_start = 0.76,
1751
- col_w = min(0.10, 0.45 / max(1, length(pitch_types))),
1752
- row_h = 0.016
1753
  )
1754
 
1755
- # =====================================================================
1756
- # PITCH USAGE TABLE
1757
- # =====================================================================
1758
- draw_tableau_table(
1759
  title = "Pitch Usage",
1760
  data = usage_data,
1761
  metrics = c("Usage vs. LHH", "Usage vs. RHH", "Pitch Count"),
1762
  pitch_types = pitch_types,
1763
  pitch_colors = tableau_pitch_colors,
1764
- x_start = 0.52, y_start = 0.62,
1765
- col_w = min(0.10, 0.45 / max(1, length(pitch_types))),
1766
- row_h = 0.016
1767
  )
1768
 
1769
- # =====================================================================
1770
- # MOVEMENT PLOT (Left side)
1771
- # =====================================================================
1772
- pushViewport(viewport(x = 0.25, y = 0.36, width = 0.42, height = 0.24))
1773
- print(mov_plot, newpage = FALSE)
1774
- popViewport()
1775
-
1776
- # =====================================================================
1777
- # VELO & MOVEMENT TABLE
1778
- # =====================================================================
1779
- draw_tableau_table(
1780
  title = "Velo & Movement",
1781
  data = velo_data,
1782
- metrics = c("Avg. Velo", "Max. Velo", "Avg. Spin Rate", "Max. Spin Rate",
1783
- "Avg. Vert Break", "Avg. Horz Break"),
1784
  pitch_types = pitch_types,
1785
  pitch_colors = tableau_pitch_colors,
1786
- x_start = 0.52, y_start = 0.46,
1787
- col_w = min(0.10, 0.45 / max(1, length(pitch_types))),
1788
- row_h = 0.014
1789
  )
1790
 
1791
- # =====================================================================
1792
- # RELEASE PLOT (Left side - BIGGER)
1793
- # =====================================================================
1794
- pushViewport(viewport(x = 0.25, y = 0.10, width = 0.42, height = 0.22))
1795
- print(rel_plot, newpage = FALSE)
1796
- popViewport()
1797
-
1798
- # =====================================================================
1799
- # RELEASE DATA TABLE
1800
- # =====================================================================
1801
- draw_tableau_table(
1802
  title = "Release Data",
1803
  data = rel_data,
1804
- metrics = c("Avg. Rel Height (ft.)", "Rel Ht vs. FB (in.)",
1805
- "Avg. Rel Side (ft.)", "Rel Side vs. FB (in.)",
1806
- "Avg. Extension (ft.)"),
1807
  pitch_types = pitch_types,
1808
  pitch_colors = tableau_pitch_colors,
1809
- x_start = 0.52, y_start = 0.22,
1810
- col_w = min(0.10, 0.45 / max(1, length(pitch_types))),
1811
- row_h = 0.014
1812
  )
1813
 
1814
  invisible(output_file)
 
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)
1097
+ library(stringr)
1098
+
1099
+ # =====================================================================
1100
+ # PITCH COLORS
1101
  # =====================================================================
1102
  tableau_pitch_colors <- c(
 
1103
  "Sinker" = "#5DADE2",
1104
  "Slider" = "#E67E22",
1105
  "Cutter" = "#F4D03F",
 
1120
  )
1121
 
1122
  # =====================================================================
1123
+ # DATA PROCESSING
1124
  # =====================================================================
1125
  process_tableau_pitcher_data <- function(df) {
 
1126
  if (!"Pitcher" %in% names(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_
 
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),
1156
  EdgeZoneHtIndicator = ifelse(PlateLocHeight > 16/12 & PlateLocHeight < 45.2/12, 1, 0),
 
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),
1186
  WalkIndicator = ifelse(KorBB == "Walk", 1, 0),
 
1187
  LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1188
  RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1189
  )
 
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,
 
 
 
 
1243
  avg = sprintf("%.3f", avg),
1244
  strike_pct = paste0(strike_pct, "%"),
1245
  fp_k_pct = paste0(fp_k_pct, "%"),
 
1250
  }
1251
 
1252
  # =====================================================================
1253
+ # TABLE CALCULATIONS
1254
  # =====================================================================
1255
+ get_valid_pitch_types <- function(pitcher_df) {
 
1256
  valid_types <- 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
1268
+ }
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) %>%
 
1287
  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) %>%
 
1301
  }
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) %>%
 
1309
  summarise(
1310
  `Avg. Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
1311
  `Max. Velo` = round(max(RelSpeed, na.rm = TRUE), 1),
1312
+ `Avg. Spin` = format(round(mean(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1313
+ `Max. Spin` = format(round(max(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1314
+ `Avg. IVB` = round(mean(InducedVertBreak, na.rm = TRUE), 0),
1315
+ `Avg. HB` = round(mean(HorzBreak, na.rm = TRUE), 0),
1316
  .groups = "drop"
1317
  )
1318
  }
 
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) %>%
1330
  summarise(
1331
+ `Rel Ht` = round(mean(RelHeight, na.rm = TRUE), 2),
1332
+ `vs FB` = ifelse(is.na(fb_ht), NA, round((mean(RelHeight, na.rm = TRUE) - fb_ht) * 12, 0)),
1333
+ `Rel Side` = round(mean(RelSide, na.rm = TRUE), 2),
1334
+ `vs FB (S)` = ifelse(is.na(fb_side), NA, round((mean(RelSide, na.rm = TRUE) - fb_side) * 12, 0)),
1335
+ `Ext` = round(mean(Extension, na.rm = TRUE), 2),
1336
  .groups = "drop"
1337
  )
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)) %>%
1349
  mutate(
1350
  ResultDisplay = case_when(
1351
+ PitchCall %in% c("BallCalled", "BallinDirt") ~ "Ball",
1352
+ PlayResult == "Double" ~ "2B",
1353
+ PitchCall %in% c("FoulBall", "FoulBallNotFieldable") ~ "Foul",
1354
+ PitchCall == "HitByPitch" ~ "HBP",
1355
+ PlayResult %in% c("Sacrifice", "SacrificeFly") ~ "Sac",
1356
+ PlayResult == "Single" ~ "1B",
1357
+ PitchCall == "StrikeCalled" ~ "Called",
1358
+ PitchCall == "StrikeSwinging" ~ "Whiff",
1359
+ PlayResult == "Triple" ~ "3B",
1360
+ PlayResult == "HomeRun" ~ "HR",
1361
  PlayResult == "Out" ~ "Out",
1362
  TRUE ~ "Other"
1363
  )
 
1366
  if (nrow(df) == 0) {
1367
  return(ggplot() + theme_void() +
1368
  labs(title = "Location Report") +
1369
+ theme(plot.title = element_text(size = 10, face = "bold", hjust = 0.5)))
1370
  }
1371
 
1372
  zone_left <- -0.8333; zone_right <- 0.8333
 
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") +
 
1404
  scale_shape_manual(
1405
+ values = c("Ball" = 1, "2B" = 18, "Foul" = 2, "HBP" = 8,
1406
+ "Sac" = 3, "1B" = 19, "Called" = 4, "Whiff" = 8,
1407
+ "3B" = 17, "HR" = 18, "Out" = 4, "Other" = 16),
1408
+ name = "Result"
1409
  ) +
1410
+ coord_fixed(xlim = c(-1.8, 1.8), ylim = c(0, 4.2)) +
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(),
1425
+ panel.grid = element_blank(),
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
 
1437
  # MOVEMENT PLOT
1438
  # =====================================================================
1439
  create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1440
+ filter_types <- get_valid_pitch_types(pitcher_df)
 
 
 
 
 
 
 
 
 
 
 
1441
 
1442
  df <- pitcher_df %>%
1443
  filter(TaggedPitchType %in% filter_types,
 
1446
  if (nrow(df) == 0) {
1447
  return(ggplot() + theme_void() +
1448
  labs(title = "Movement Profile") +
1449
+ theme(plot.title = element_text(size = 10, face = "bold", hjust = 0.5)))
1450
  }
1451
 
1452
  ggplot(df, aes(x = HorzBreak, y = InducedVertBreak, color = TaggedPitchType)) +
1453
+ geom_vline(xintercept = 0, color = "black", linewidth = 0.4) +
1454
+ geom_hline(yintercept = 0, color = "black", linewidth = 0.4) +
1455
+ geom_point(size = 2.5, alpha = 0.8) +
1456
  scale_color_manual(values = pitch_colors) +
1457
  coord_cartesian(xlim = c(-25, 25), ylim = c(-25, 25)) +
1458
+ labs(title = "Movement Profile", x = "HB", y = "IVB") +
1459
  theme_minimal() +
1460
  theme(
1461
+ plot.title = element_text(size = 9, face = "bold", hjust = 0.5),
1462
  legend.position = "none",
1463
+ axis.title = element_text(size = 7),
1464
+ axis.text = element_text(size = 6),
1465
+ plot.margin = margin(2, 2, 2, 2)
1466
  )
1467
  }
1468
 
1469
  # =====================================================================
1470
+ # RELEASE PLOT
1471
  # =====================================================================
1472
  create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1473
+ filter_types <- get_valid_pitch_types(pitcher_df)
 
 
 
 
 
 
 
 
 
 
 
1474
 
1475
  df <- pitcher_df %>%
1476
  filter(TaggedPitchType %in% filter_types,
 
1479
  if (nrow(df) == 0) {
1480
  return(ggplot() + theme_void() +
1481
  labs(title = "Release Plot") +
1482
+ theme(plot.title = element_text(size = 10, face = "bold", hjust = 0.5)))
1483
  }
1484
 
1485
+ # Mound arc
1486
  mound_theta <- seq(0, pi, length.out = 100)
1487
  mound_radius <- 3
1488
  mound_df <- data.frame(
 
1495
  fill = "#C0392B", color = NA, inherit.aes = FALSE) +
1496
  annotate("rect", xmin = -0.5, xmax = 0.5, ymin = 0.85, ymax = 1.05,
1497
  fill = "white", color = "gray40", linewidth = 0.3) +
1498
+ geom_vline(xintercept = 0, color = "gray60", linetype = "dashed", linewidth = 0.3) +
1499
+ geom_point(aes(color = TaggedPitchType), size = 2.5, alpha = 0.8) +
1500
  scale_color_manual(values = pitch_colors) +
1501
  coord_cartesian(xlim = c(-4, 4), ylim = c(0, 7)) +
1502
+ labs(title = "Release Plot", x = "Side", y = "Height") +
1503
  theme_minimal() +
1504
  theme(
1505
+ plot.title = element_text(size = 9, face = "bold", hjust = 0.5),
1506
  legend.position = "none",
1507
+ axis.title = element_text(size = 7),
1508
+ axis.text = element_text(size = 6),
1509
+ plot.margin = margin(2, 2, 2, 2)
1510
  )
1511
  }
1512
 
1513
  # =====================================================================
1514
+ # TABLE DRAWING - Compact with bigger text
1515
  # =====================================================================
1516
+ draw_tableau_table_v3 <- function(title, data, metrics, pitch_types, pitch_colors,
1517
+ x_start, y_start, col_w = 0.075, row_h = 0.014) {
1518
 
1519
+ # Title
1520
+ grid.text(title, x = x_start + length(pitch_types) * col_w / 2, y = y_start + 0.018,
1521
+ gp = gpar(fontface = "bold", cex = 0.65, col = "#006F71"))
1522
 
1523
+ # Column headers (pitch types)
1524
  header_y <- y_start
1525
  for (i in seq_along(pitch_types)) {
1526
  pt <- pitch_types[i]
1527
  col_color <- if (pt %in% names(pitch_colors)) pitch_colors[pt] else "#95A5A6"
1528
 
1529
  grid.rect(x = x_start + (i-1)*col_w, y = header_y,
1530
+ width = col_w * 0.96, height = row_h,
1531
  just = c("left", "center"),
1532
+ gp = gpar(fill = col_color, col = "gray60", lwd = 0.3))
1533
+
1534
+ # Abbreviate long pitch names
1535
+ pt_short <- gsub("ChangeUp", "CH", pt)
1536
+ pt_short <- gsub("Fastball", "FB", pt_short)
1537
+ pt_short <- gsub("Curveball", "CB", pt_short)
1538
+ pt_short <- gsub("Slider", "SL", pt_short)
1539
+ pt_short <- gsub("Sinker", "SI", pt_short)
1540
+ pt_short <- gsub("Cutter", "CT", pt_short)
1541
+ pt_short <- gsub("Splitter", "SP", pt_short)
1542
+ pt_short <- gsub("Sweeper", "SW", pt_short)
1543
 
1544
+ grid.text(pt_short, x = x_start + (i-1)*col_w + col_w/2, y = header_y,
1545
+ gp = gpar(col = "white", cex = 0.42, fontface = "bold"))
1546
  }
1547
 
1548
+ # Data rows
1549
  for (m in seq_along(metrics)) {
1550
  metric_name <- metrics[m]
1551
  y_pos <- header_y - m * row_h
1552
 
1553
+ # Row label - abbreviated
1554
+ metric_short <- gsub("Usage vs. ", "", metric_name)
1555
+ metric_short <- gsub("Avg. ", "", metric_short)
1556
+ metric_short <- gsub("Max. ", "Max ", metric_short)
1557
+ metric_short <- gsub("Pitch Count", "#", metric_short)
1558
+ metric_short <- gsub(" Rate", "", metric_short)
1559
+
1560
+ grid.text(metric_short, x = x_start - 0.008, y = y_pos, just = "right",
1561
+ gp = gpar(cex = 0.42, fontface = "bold"))
1562
 
1563
+ # Values for each pitch type
1564
  for (i in seq_along(pitch_types)) {
1565
  pt <- pitch_types[i]
1566
  idx <- which(data$TaggedPitchType == pt)
 
1571
  }
1572
 
1573
  grid.rect(x = x_start + (i-1)*col_w, y = y_pos,
1574
+ width = col_w * 0.96, height = row_h * 0.92,
1575
  just = c("left", "center"),
1576
+ gp = gpar(fill = "white", col = "gray80", lwd = 0.2))
1577
 
1578
  grid.text(val, x = x_start + (i-1)*col_w + col_w/2, y = y_pos,
1579
+ gp = gpar(cex = 0.40))
1580
  }
1581
  }
1582
  }
1583
 
1584
  # =====================================================================
1585
+ # MAIN PDF FUNCTION
1586
  # =====================================================================
1587
  create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1588
  if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
 
1599
  return(output_file)
1600
  }
1601
 
1602
+ # Metadata - only get away team (batter team)
1603
  game_date <- tryCatch({
1604
  d <- unique(pitcher_df$Date)[1]
1605
  if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
1606
  }, error = function(e) "NA")
1607
 
1608
+ # Get opponent team (batter team, not pitcher team)
 
 
1609
  batter_teams <- unique(pitcher_df$BatterTeam)
1610
+ batter_teams <- batter_teams[!is.na(batter_teams)]
1611
+ away_team <- if (length(batter_teams) > 0) batter_teams[1] else "Unknown"
1612
 
1613
  # Calculate stats
1614
  stats <- calculate_tableau_header_stats(pitcher_df)
 
1617
  velo_data <- calculate_tableau_velo_movement(pitcher_df)
1618
  rel_data <- calculate_tableau_release_data(pitcher_df)
1619
 
 
1620
  pitch_types <- unique(c(loc_data$TaggedPitchType, usage_data$TaggedPitchType))
1621
  pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
1622
  if (length(pitch_types) == 0) pitch_types <- "Undefined"
 
1626
  mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1627
  rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1628
 
1629
+ # Create PDF
1630
  pdf(output_file, width = 8.5, height = 11)
1631
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1632
 
1633
  grid.newpage()
1634
 
1635
  # =====================================================================
1636
+ # HEADER BAR
1637
  # =====================================================================
1638
+ grid.rect(x = 0, y = 0.955, width = 1, height = 0.045,
1639
  just = c("left", "bottom"),
1640
  gp = gpar(fill = "#006F71", col = NA))
1641
 
1642
+ grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.977, just = "left",
1643
+ gp = gpar(col = "white", fontface = "bold", cex = 1.2))
1644
 
1645
  # =====================================================================
1646
+ # INFO ROW - Date, Pitcher Name, vs Away Team only
1647
  # =====================================================================
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.7, fontface = "bold"))
 
 
 
 
1652
 
1653
  # =====================================================================
1654
+ # STAT BOXES - Colored labels, white value backgrounds
1655
  # =====================================================================
1656
+ stat_labels <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG",
1657
+ "Strike%", "1st P K%", "E+A%", "Comp%", "LOO%")
1658
+ stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs,
1659
+ stats$bb_hbp, stats$so, stats$avg,
1660
+ stats$strike_pct, stats$fp_k_pct, stats$ea_pct,
1661
+ stats$comp_pct, stats$loo_pct)
1662
+
1663
+ # Label colors (text color for the stat name)
1664
+ label_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60", "#00BCD4",
1665
+ "#E67E22", "#E67E22", "#E67E22", "#E67E22", "#E67E22")
1666
+
1667
+ box_y <- 0.905
1668
+ box_w <- 0.065
1669
+ box_h <- 0.038
 
1670
  x_start <- 0.02
1671
 
1672
  for (i in 1:12) {
1673
+ x_pos <- x_start + (i-1) * (box_w + 0.012)
1674
 
1675
+ # White background box with border matching label color
1676
  grid.rect(x = x_pos, y = box_y, width = box_w, height = box_h,
1677
  just = c("left", "center"),
1678
+ gp = gpar(fill = "white", col = label_colors[i], lwd = 1.5))
1679
 
1680
+ # Colored label at top
1681
+ grid.text(stat_labels[i], x = x_pos + box_w/2, y = box_y + 0.010,
1682
+ gp = gpar(col = label_colors[i], cex = 0.40, fontface = "bold"))
1683
 
1684
+ # Black value below
1685
+ grid.text(as.character(stat_values[i]), x = x_pos + box_w/2, y = box_y - 0.007,
1686
+ gp = gpar(col = "black", cex = 0.52, fontface = "bold"))
1687
  }
1688
 
1689
  # =====================================================================
1690
+ # CHARTS - Moved left and higher
1691
  # =====================================================================
1692
+
1693
+ # Location plot (top left) - moved left and up
1694
+ pushViewport(viewport(x = 0.22, y = 0.66, width = 0.40, height = 0.28))
1695
  print(loc_plot, newpage = FALSE)
1696
  popViewport()
1697
 
1698
+ # Movement plot (middle left)
1699
+ pushViewport(viewport(x = 0.22, y = 0.38, width = 0.40, height = 0.24))
1700
+ print(mov_plot, newpage = FALSE)
1701
+ popViewport()
1702
+
1703
+ # Release plot (bottom left) - bigger
1704
+ pushViewport(viewport(x = 0.22, y = 0.12, width = 0.40, height = 0.22))
1705
+ print(rel_plot, newpage = FALSE)
1706
+ popViewport()
1707
+
1708
  # =====================================================================
1709
+ # TABLES - Farther right, more compact, bigger text
1710
  # =====================================================================
1711
+
1712
+ # Adjust table position based on number of pitch types
1713
+ n_types <- length(pitch_types)
1714
+ col_w <- min(0.085, 0.42 / max(1, n_types))
1715
+ table_x <- 0.54
1716
+
1717
+ # Location Data table
1718
+ draw_tableau_table_v3(
1719
  title = "Location Data",
1720
  data = loc_data,
1721
  metrics = c("Zone%", "Edge%", "Strike%", "Whiff%"),
1722
  pitch_types = pitch_types,
1723
  pitch_colors = tableau_pitch_colors,
1724
+ x_start = table_x, y_start = 0.78,
1725
+ col_w = col_w, row_h = 0.014
 
1726
  )
1727
 
1728
+ # Pitch Usage table
1729
+ draw_tableau_table_v3(
 
 
1730
  title = "Pitch Usage",
1731
  data = usage_data,
1732
  metrics = c("Usage vs. LHH", "Usage vs. RHH", "Pitch Count"),
1733
  pitch_types = pitch_types,
1734
  pitch_colors = tableau_pitch_colors,
1735
+ x_start = table_x, y_start = 0.66,
1736
+ col_w = col_w, row_h = 0.014
 
1737
  )
1738
 
1739
+ # Velo & Movement table
1740
+ draw_tableau_table_v3(
 
 
 
 
 
 
 
 
 
1741
  title = "Velo & Movement",
1742
  data = velo_data,
1743
+ metrics = c("Avg. Velo", "Max. Velo", "Avg. Spin", "Max. Spin", "Avg. IVB", "Avg. HB"),
 
1744
  pitch_types = pitch_types,
1745
  pitch_colors = tableau_pitch_colors,
1746
+ x_start = table_x, y_start = 0.52,
1747
+ col_w = col_w, row_h = 0.013
 
1748
  )
1749
 
1750
+ # Release Data table
1751
+ draw_tableau_table_v3(
 
 
 
 
 
 
 
 
 
1752
  title = "Release Data",
1753
  data = rel_data,
1754
+ metrics = c("Rel Ht", "vs FB", "Rel Side", "vs FB (S)", "Ext"),
 
 
1755
  pitch_types = pitch_types,
1756
  pitch_colors = tableau_pitch_colors,
1757
+ x_start = table_x, y_start = 0.36,
1758
+ col_w = col_w, row_h = 0.013
 
1759
  )
1760
 
1761
  invisible(output_file)