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

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +270 -223
app.R CHANGED
@@ -163,24 +163,6 @@ app_css <- "
163
  # ====================== HITTER CODE (VERBATIM) ======================
164
  # =====================================================================
165
 
166
- tableau_pitch_colors <- c(
167
- # Primary pitches from screenshots
168
- "Sinker" = "#5DADE2", # Teal/Cyan
169
- "Slider" = "#E67E22", # Orange
170
- "Cutter" = "#F4D03F", # Yellow/Gold
171
- "Sweeper" = "#E67E22", # Same as Slider
172
-
173
- # Other pitch types (standard colors)
174
- "Fastball" = "#E74C3C", # Red
175
- "Four-Seam" = "#E74C3C",
176
- "FourSeamFastBall" = "#E74C3C",
177
- "Curveball" = "#9B59B6", # Purple
178
- "ChangeUp" = "#2ECC71", # Green
179
- "Splitter" = "#1ABC9C", # Teal-green
180
- "Knuckle Curve" = "#8E44AD",
181
- "Other" = "#95A5A6" # Gray
182
- )
183
-
184
  process_dataset <- function(df) {
185
  if ("Batter" %in% names(df)) {
186
  df <- df %>% mutate(Batter = stringr::str_replace(Batter, "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
@@ -1086,10 +1068,45 @@ create_postgame_pdf <- function(game_df, player_name, output_file, bio_data = NU
1086
  }
1087
  }
1088
 
 
 
 
1089
  # =====================================================================
1090
- # TABLEAU-STYLE PITCHER REPORT FUNCTIONS
 
 
 
 
 
1091
  # =====================================================================
1092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1093
  process_tableau_pitcher_data <- function(df) {
1094
  # Standardize Pitcher name
1095
  if (!"Pitcher" %in% names(df)) {
@@ -1100,10 +1117,25 @@ process_tableau_pitcher_data <- function(df) {
1100
  df <- df %>%
1101
  mutate(Pitcher = str_replace(coalesce(Pitcher, ""), "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
1102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1103
  # Calculate all indicators
1104
  df <- df %>%
1105
  mutate(
1106
- # Strike Zone Indicator (standard zone)
1107
  StrikeZoneIndicator = ifelse(PlateLocSide >= -0.8333 & PlateLocSide <= 0.8333 &
1108
  PlateLocHeight >= 1.5 & PlateLocHeight <= 3.5, 1, 0),
1109
 
@@ -1117,50 +1149,40 @@ process_tableau_pitcher_data <- function(df) {
1117
  EdgeIndicator = ifelse((EdgeHeightIndicator == 1 & EdgeZoneWIndicator == 1) |
1118
  (EdgeWidthIndicator == 1 & EdgeZoneHtIndicator == 1), 1, 0),
1119
 
1120
- # Quality pitch (zone or edge)
1121
  QualityPitchIndicator = ifelse(StrikeZoneIndicator == 1 | EdgeIndicator == 1, 1, 0),
1122
 
1123
- # Strike/Ball indicators
1124
  StrikeIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "StrikeCalled",
1125
  "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1126
  WhiffIndicator = ifelse(PitchCall == "StrikeSwinging", 1, 0),
1127
  SwingIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "FoulBallNotFieldable",
1128
  "FoulBall", "InPlay"), 1, 0),
1129
 
1130
- # First pitch indicators
1131
  FPindicator = ifelse(Balls == 0 & Strikes == 0, 1, 0),
1132
  FPSindicator = ifelse(PitchCall %in% c("StrikeCalled", "StrikeSwinging",
1133
  "FoulBallNotFieldable", "FoulBall", "InPlay") &
1134
  FPindicator == 1, 1, 0),
1135
 
1136
- # Ahead indicator (0-1 or 1-1 count, got strike)
1137
  AheadIndicator = ifelse(((Balls == 0 & Strikes == 1) | (Balls == 1 & Strikes == 1)) &
1138
  StrikeIndicator == 1, 1, 0),
1139
 
1140
- # At-bat indicator
1141
  ABindicator = ifelse(PlayResult %in% c("Error", "FieldersChoice", "Out",
1142
  "Single", "Double", "Triple", "HomeRun") |
1143
  KorBB == "Strikeout", 1, 0),
1144
 
1145
- # Hit indicator
1146
  HitIndicator = ifelse(PlayResult %in% c("Single", "Double", "Triple", "HomeRun"), 1, 0),
1147
 
1148
- # Plate appearance indicator
1149
  PAindicator = ifelse(PitchCall %in% c("InPlay", "HitByPitch", "CatchersInterference") |
1150
  KorBB %in% c("Walk", "Strikeout"), 1, 0),
1151
 
1152
- # Leadoff indicators
1153
  LeadOffIndicator = ifelse((PAofInning == 1 & (PlayResult != "Undefined" | KorBB != "Undefined")) |
1154
  PitchCall == "HitByPitch", 1, 0),
1155
  OutIndicator = ifelse((PlayResult %in% c("Out", "FieldersChoice") | KorBB == "Strikeout") &
1156
  PitchCall != "HitByPitch", 1, 0),
1157
  LOOindicator = ifelse(LeadOffIndicator == 1 & OutIndicator == 1, 1, 0),
1158
 
1159
- # Walk/HBP indicators
1160
  HBPIndicator = ifelse(PitchCall == "HitByPitch", 1, 0),
1161
  WalkIndicator = ifelse(KorBB == "Walk", 1, 0),
1162
 
1163
- # Batter side indicators
1164
  LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1165
  RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1166
  )
@@ -1168,8 +1190,10 @@ process_tableau_pitcher_data <- function(df) {
1168
  df
1169
  }
1170
 
 
 
 
1171
  calculate_tableau_header_stats <- function(pitcher_df) {
1172
- # At-bats (unique AB-ending events)
1173
  ab_data <- pitcher_df %>%
1174
  filter(ABindicator == 1) %>%
1175
  group_by(Inning, Batter, PAofInning) %>%
@@ -1181,7 +1205,6 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1181
  xbh <- sum(ab_data$PlayResult %in% c("Double", "Triple", "HomeRun"), na.rm = TRUE)
1182
  runs <- sum(pitcher_df$RunsScored, na.rm = TRUE)
1183
 
1184
- # Plate appearances
1185
  pa_data <- pitcher_df %>%
1186
  filter(PAindicator == 1) %>%
1187
  group_by(Inning, Batter, PAofInning) %>%
@@ -1192,10 +1215,8 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1192
  hbp <- sum(pa_data$HBPIndicator, na.rm = TRUE)
1193
  so <- sum(pa_data$KorBB == "Strikeout", na.rm = TRUE)
1194
 
1195
- # Batting average
1196
  avg <- ifelse(at_bats > 0, round(hits / at_bats, 3), 0)
1197
 
1198
- # Percentage stats
1199
  total_pitches <- nrow(pitcher_df)
1200
  strike_pct <- ifelse(total_pitches > 0,
1201
  round(100 * sum(pitcher_df$StrikeIndicator, na.rm = TRUE) / total_pitches, 0), 0)
@@ -1204,16 +1225,13 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1204
  fp_k_pct <- ifelse(fp_pitches > 0,
1205
  round(100 * sum(pitcher_df$FPSindicator, na.rm = TRUE) / fp_pitches, 0), 0)
1206
 
1207
- # E+A% = (FPS + Ahead pitches) / First pitches
1208
  ea_pct <- ifelse(fp_pitches > 0,
1209
  round(100 * (sum(pitcher_df$FPSindicator, na.rm = TRUE) +
1210
  sum(pitcher_df$AheadIndicator & pitcher_df$Balls == 0, na.rm = TRUE)) / fp_pitches, 0), 0)
1211
 
1212
- # Comp% = Quality pitches / Total pitches
1213
  comp_pct <- ifelse(total_pitches > 0,
1214
  round(100 * sum(pitcher_df$QualityPitchIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1215
 
1216
- # LOO% = Leadoff outs / Leadoff opportunities
1217
  leadoff_opps <- sum(pitcher_df$LeadOffIndicator, na.rm = TRUE)
1218
  loo_pct <- ifelse(leadoff_opps > 0,
1219
  round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
@@ -1234,9 +1252,27 @@ calculate_tableau_header_stats <- function(pitcher_df) {
1234
  )
1235
  }
1236
 
 
 
 
1237
  calculate_tableau_location_data <- function(pitcher_df) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1238
  pitcher_df %>%
1239
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other") %>%
1240
  group_by(TaggedPitchType) %>%
1241
  summarise(
1242
  `Zone%` = paste0(round(100 * sum(StrikeZoneIndicator, na.rm = TRUE) / n(), 0), "%"),
@@ -1253,8 +1289,21 @@ calculate_tableau_pitch_usage <- function(pitcher_df) {
1253
  lhh_pitches <- sum(pitcher_df$LHHindicator, na.rm = TRUE)
1254
  rhh_pitches <- sum(pitcher_df$RHHindicator, na.rm = TRUE)
1255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
  pitcher_df %>%
1257
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other") %>%
1258
  group_by(TaggedPitchType) %>%
1259
  summarise(
1260
  `Pitch Count` = n(),
@@ -1265,8 +1314,21 @@ calculate_tableau_pitch_usage <- function(pitcher_df) {
1265
  }
1266
 
1267
  calculate_tableau_velo_movement <- function(pitcher_df) {
 
 
 
 
 
 
 
 
 
 
 
 
 
1268
  pitcher_df %>%
1269
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other") %>%
1270
  group_by(TaggedPitchType) %>%
1271
  summarise(
1272
  `Avg. Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
@@ -1280,14 +1342,26 @@ calculate_tableau_velo_movement <- function(pitcher_df) {
1280
  }
1281
 
1282
  calculate_tableau_release_data <- function(pitcher_df) {
1283
- # Get primary fastball for comparison
1284
  primary <- pitcher_df %>%
1285
  filter(TaggedPitchType %in% c("Fastball", "Sinker", "Four-Seam", "FourSeamFastBall"))
1286
  fb_ht <- if (nrow(primary) > 0) mean(primary$RelHeight, na.rm = TRUE) else NA
1287
  fb_side <- if (nrow(primary) > 0) mean(primary$RelSide, na.rm = TRUE) else NA
1288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289
  pitcher_df %>%
1290
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other") %>%
1291
  group_by(TaggedPitchType) %>%
1292
  summarise(
1293
  `Avg. Rel Height (ft.)` = round(mean(RelHeight, na.rm = TRUE), 2),
@@ -1300,11 +1374,25 @@ calculate_tableau_release_data <- function(pitcher_df) {
1300
  }
1301
 
1302
  # =====================================================================
1303
- # LOCATION PLOT - WITH PROPER ZONE GRAPHICS
1304
  # =====================================================================
1305
  create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1306
  df <- pitcher_df %>%
1307
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other",
1308
  !is.na(PlateLocSide), !is.na(PlateLocHeight)) %>%
1309
  mutate(
1310
  ResultDisplay = case_when(
@@ -1326,94 +1414,56 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1326
  if (nrow(df) == 0) {
1327
  return(ggplot() + theme_void() +
1328
  labs(title = "Location Report") +
1329
- theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5)))
1330
  }
1331
 
1332
- # Define zone boundaries
1333
- zone_left <- -0.8333
1334
- zone_right <- 0.8333
1335
- zone_bottom <- 1.5
1336
- zone_top <- 3.5
1337
-
1338
- # Shadow zone (outer dashed) - about 0.25 wider on each side
1339
- shadow_left <- -1.1
1340
- shadow_right <- 1.1
1341
- shadow_bottom <- 1.2
1342
- shadow_top <- 3.8
1343
-
1344
- # Inner zone thirds
1345
  zone_width <- (zone_right - zone_left) / 3
1346
  zone_height <- (zone_top - zone_bottom) / 3
1347
 
1348
  p <- ggplot(df, aes(x = PlateLocSide, y = PlateLocHeight)) +
1349
-
1350
- # Shadow zone (outer dashed rectangle)
1351
- annotate("rect",
1352
- xmin = shadow_left, xmax = shadow_right,
1353
  ymin = shadow_bottom, ymax = shadow_top,
1354
  fill = NA, color = "gray30", linetype = "dashed", linewidth = 0.6) +
1355
-
1356
- # Strike zone (solid RED rectangle)
1357
- annotate("rect",
1358
- xmin = zone_left, xmax = zone_right,
1359
  ymin = zone_bottom, ymax = zone_top,
1360
  fill = NA, color = "#E74C3C", linewidth = 1.2) +
1361
-
1362
- # Inner zone divisions (dashed gray lines)
1363
- # Vertical lines
1364
  annotate("segment", x = zone_left + zone_width, xend = zone_left + zone_width,
1365
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.4) +
1366
  annotate("segment", x = zone_left + 2*zone_width, xend = zone_left + 2*zone_width,
1367
  y = zone_bottom, yend = zone_top, color = "gray50", linetype = "dashed", linewidth = 0.4) +
1368
- # Horizontal lines
1369
  annotate("segment", x = zone_left, xend = zone_right,
1370
  y = zone_bottom + zone_height, yend = zone_bottom + zone_height,
1371
  color = "gray50", linetype = "dashed", linewidth = 0.4) +
1372
  annotate("segment", x = zone_left, xend = zone_right,
1373
  y = zone_bottom + 2*zone_height, yend = zone_bottom + 2*zone_height,
1374
  color = "gray50", linetype = "dashed", linewidth = 0.4) +
1375
-
1376
- # Home plate
1377
  annotate("segment", x = -0.708, y = 0.15, xend = 0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1378
  annotate("segment", x = -0.708, y = 0.30, xend = -0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1379
  annotate("segment", x = 0.708, y = 0.30, xend = 0.708, yend = 0.15, color = "black", linewidth = 0.6) +
1380
  annotate("segment", x = -0.708, y = 0.30, xend = 0, yend = 0.50, color = "black", linewidth = 0.6) +
1381
  annotate("segment", x = 0.708, y = 0.30, xend = 0, yend = 0.50, color = "black", linewidth = 0.6) +
1382
-
1383
- # Center line (dashed, vertical through zone)
1384
  annotate("segment", x = 0, xend = 0, y = 0.5, yend = shadow_top + 0.3,
1385
  color = "gray60", linetype = "dotted", linewidth = 0.4) +
1386
-
1387
- # Pitch points
1388
- geom_point(aes(color = TaggedPitchType, shape = ResultDisplay), size = 3.5, stroke = 1.2) +
1389
-
1390
  scale_color_manual(values = pitch_colors, name = "Pitch Type") +
1391
  scale_shape_manual(
1392
- values = c(
1393
- "BallCalled" = 1, # Open circle
1394
- "Double" = 18, # Diamond
1395
- "FoulBall" = 2, # Open triangle
1396
- "HitByPitch" = 8, # Asterisk
1397
- "Sacrifice" = 3, # Plus
1398
- "Single" = 19, # Half-filled circle
1399
- "StrikeCalled" = 4, # X
1400
- "StrikeSwinging" = 8, # Asterisk
1401
- "Triple" = 17, # Filled triangle
1402
- "HomeRun" = 18, # Diamond
1403
- "Out" = 4, # X
1404
- "Other" = 16 # Filled circle
1405
- ),
1406
  name = "Play Result"
1407
  ) +
1408
-
1409
- coord_fixed(xlim = c(-2.2, 2.2), ylim = c(-0.2, 4.5)) +
1410
  labs(title = "Location Report") +
1411
  theme_minimal() +
1412
  theme(
1413
- plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
1414
  legend.position = "left",
1415
- legend.title = element_text(size = 8, face = "bold"),
1416
- legend.text = element_text(size = 7),
1417
  axis.text = element_blank(),
1418
  axis.title = element_blank(),
1419
  axis.ticks = element_blank(),
@@ -1423,15 +1473,31 @@ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1423
  p
1424
  }
1425
 
 
 
 
1426
  create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
 
 
 
 
 
 
 
 
 
 
 
 
 
1427
  df <- pitcher_df %>%
1428
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other",
1429
  !is.na(HorzBreak), !is.na(InducedVertBreak))
1430
 
1431
  if (nrow(df) == 0) {
1432
  return(ggplot() + theme_void() +
1433
  labs(title = "Movement Profile") +
1434
- theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5)))
1435
  }
1436
 
1437
  ggplot(df, aes(x = HorzBreak, y = InducedVertBreak, color = TaggedPitchType)) +
@@ -1443,71 +1509,79 @@ create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1443
  labs(title = "Movement Profile", x = "Horz Break", y = "Induced Vert Break") +
1444
  theme_minimal() +
1445
  theme(
1446
- plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
1447
  legend.position = "none",
1448
- axis.title = element_text(size = 9),
1449
- axis.text = element_text(size = 8)
1450
  )
1451
  }
1452
 
 
 
 
1453
  create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
 
 
 
 
 
 
 
 
 
 
 
 
 
1454
  df <- pitcher_df %>%
1455
- filter(!is.na(TaggedPitchType), TaggedPitchType != "", TaggedPitchType != "Other",
1456
  !is.na(RelSide), !is.na(RelHeight))
1457
 
1458
  if (nrow(df) == 0) {
1459
  return(ggplot() + theme_void() +
1460
  labs(title = "Release Plot") +
1461
- theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5)))
1462
  }
1463
 
1464
- # Create mound arc data (terracotta half-circle)
1465
  mound_theta <- seq(0, pi, length.out = 100)
1466
  mound_radius <- 3
1467
  mound_df <- data.frame(
1468
  x = mound_radius * cos(mound_theta),
1469
- y = mound_radius * sin(mound_theta) * 0.35 # Flatten the arc
1470
  )
1471
 
1472
  ggplot(df, aes(x = RelSide, y = RelHeight)) +
1473
- # Mound (terracotta half-circle)
1474
  geom_polygon(data = mound_df, aes(x = x, y = y),
1475
  fill = "#C0392B", color = NA, inherit.aes = FALSE) +
1476
-
1477
- # Pitching rubber (white rectangle)
1478
  annotate("rect", xmin = -0.5, xmax = 0.5, ymin = 0.85, ymax = 1.05,
1479
  fill = "white", color = "gray40", linewidth = 0.3) +
1480
-
1481
- # Center line (dashed vertical)
1482
  geom_vline(xintercept = 0, color = "gray60", linetype = "dashed", linewidth = 0.4) +
1483
-
1484
- # Release points
1485
  geom_point(aes(color = TaggedPitchType), size = 3, alpha = 0.8) +
1486
-
1487
  scale_color_manual(values = pitch_colors) +
1488
  coord_cartesian(xlim = c(-4, 4), ylim = c(0, 7)) +
1489
  labs(title = "Release Plot", x = "Rel Side", y = "Rel Height") +
1490
  theme_minimal() +
1491
  theme(
1492
- plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
1493
  legend.position = "none",
1494
- axis.title = element_text(size = 9),
1495
- axis.text = element_text(size = 8)
1496
  )
1497
  }
1498
 
 
 
 
1499
  draw_tableau_table <- function(title, data, metrics, pitch_types, pitch_colors,
1500
- x_start, y_start, col_w = 0.12, row_h = 0.018) {
1501
 
1502
- # Title
1503
- grid.text(title, x = x_start + 0.2, y = y_start + 0.03,
1504
- gp = gpar(fontface = "bold", cex = 0.9))
1505
 
1506
- # Column headers (pitch types)
1507
  header_y <- y_start
1508
  for (i in seq_along(pitch_types)) {
1509
  pt <- pitch_types[i]
1510
- col_color <- ifelse(pt %in% names(pitch_colors), pitch_colors[pt], "#95A5A6")
1511
 
1512
  grid.rect(x = x_start + (i-1)*col_w, y = header_y,
1513
  width = col_w * 0.95, height = row_h * 1.1,
@@ -1515,19 +1589,16 @@ draw_tableau_table <- function(title, data, metrics, pitch_types, pitch_colors,
1515
  gp = gpar(fill = col_color, col = "black", lwd = 0.5))
1516
 
1517
  grid.text(pt, x = x_start + (i-1)*col_w + col_w/2, y = header_y,
1518
- gp = gpar(col = "white", cex = 0.45, fontface = "bold"))
1519
  }
1520
 
1521
- # Data rows
1522
  for (m in seq_along(metrics)) {
1523
  metric_name <- metrics[m]
1524
  y_pos <- header_y - m * row_h
1525
 
1526
- # Row label
1527
- grid.text(metric_name, x = x_start - 0.02, y = y_pos, just = "right",
1528
- gp = gpar(cex = 0.45, fontface = "bold"))
1529
 
1530
- # Values for each pitch type
1531
  for (i in seq_along(pitch_types)) {
1532
  pt <- pitch_types[i]
1533
  idx <- which(data$TaggedPitchType == pt)
@@ -1543,16 +1614,17 @@ draw_tableau_table <- function(title, data, metrics, pitch_types, pitch_colors,
1543
  gp = gpar(fill = "white", col = "gray80", lwd = 0.3))
1544
 
1545
  grid.text(val, x = x_start + (i-1)*col_w + col_w/2, y = y_pos,
1546
- gp = gpar(cex = 0.42))
1547
  }
1548
  }
1549
  }
1550
-
 
 
 
1551
  create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1552
- # Close any open devices
1553
  if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1554
 
1555
- # Process and filter data
1556
  pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1557
  filter(Pitcher == pitcher_name)
1558
 
@@ -1575,19 +1647,20 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1575
  if (is.na(pitcher_team)) pitcher_team <- "Unknown"
1576
 
1577
  batter_teams <- unique(pitcher_df$BatterTeam)
1578
- batter_team_str <- if (length(batter_teams) > 1) "All" else batter_teams[1]
1579
  if (is.na(batter_team_str)) batter_team_str <- "Unknown"
1580
 
1581
- # Calculate all stats
1582
  stats <- calculate_tableau_header_stats(pitcher_df)
1583
  loc_data <- calculate_tableau_location_data(pitcher_df)
1584
  usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1585
  velo_data <- calculate_tableau_velo_movement(pitcher_df)
1586
  rel_data <- calculate_tableau_release_data(pitcher_df)
1587
 
1588
- # Get pitch types used
1589
  pitch_types <- unique(c(loc_data$TaggedPitchType, usage_data$TaggedPitchType))
1590
  pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
 
1591
 
1592
  # Create plots
1593
  loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
@@ -1603,79 +1676,65 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1603
  # =====================================================================
1604
  # HEADER BAR (Teal)
1605
  # =====================================================================
1606
- grid.rect(x = 0, y = 0.94, width = 1, height = 0.06,
1607
  just = c("left", "bottom"),
1608
  gp = gpar(fill = "#006F71", col = NA))
1609
 
1610
- grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.97, just = "left",
1611
- gp = gpar(col = "white", fontface = "bold", cex = 1.4))
1612
 
1613
  # =====================================================================
1614
- # INFO ROW
1615
  # =====================================================================
1616
- info_y <- 0.91
1617
- info_labels <- c("Date", "Pitcher Team", "Pitcher", "Batter Team", "vs. L/R")
1618
- info_values <- c(game_date, pitcher_team, pitcher_name, batter_team_str, "All")
1619
-
1620
- for (i in 1:5) {
1621
- x_pos <- 0.02 + (i-1) * 0.18
1622
- grid.text(info_labels[i], x = x_pos, y = info_y + 0.015, just = "left",
1623
- gp = gpar(cex = 0.6, col = "gray50"))
1624
- grid.text(info_values[i], x = x_pos, y = info_y - 0.012, just = "left",
1625
  gp = gpar(cex = 0.7, fontface = "bold"))
1626
  }
1627
 
1628
  # =====================================================================
1629
- # STAT BOXES (Two rows)
1630
  # =====================================================================
1631
- # Row 1: At Bats, H, XBH, R, BB/HBP, SO, AVG
1632
- stat_labels_1 <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG")
1633
- stat_values_1 <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs, stats$bb_hbp, stats$so, stats$avg)
1634
- stat_colors_1 <- c("#006F71", "#006F71", "#006F71", "#E74C3C", "#006F71", "#006F71", "#006F71")
1635
-
1636
- box_y1 <- 0.865
1637
- box_w <- 0.085
1638
- box_h <- 0.04
1639
-
1640
- for (i in 1:7) {
1641
- x_pos <- 0.02 + (i-1) * (box_w + 0.01)
1642
- grid.rect(x = x_pos, y = box_y1, width = box_w, height = box_h,
1643
- just = c("left", "center"),
1644
- gp = gpar(fill = stat_colors_1[i], col = "black", lwd = 0.5))
1645
- grid.text(stat_labels_1[i], x = x_pos + box_w/2, y = box_y1 + 0.01,
1646
- gp = gpar(col = "white", cex = 0.5, fontface = "bold"))
1647
- grid.text(as.character(stat_values_1[i]), x = x_pos + box_w/2, y = box_y1 - 0.008,
1648
- gp = gpar(col = "white", cex = 0.7, fontface = "bold"))
1649
- }
1650
-
1651
- # Row 2: Strike%, 1st P K%, E+A%, Comp%, LOO%
1652
- stat_labels_2 <- c("Strike%", "1st P K%", "E+A%", "Comp%", "LOO%")
1653
- stat_values_2 <- c(stats$strike_pct, stats$fp_k_pct, stats$ea_pct, stats$comp_pct, stats$loo_pct)
1654
-
1655
- box_y2 <- 0.82
1656
-
1657
- for (i in 1:5) {
1658
- x_pos <- 0.02 + (i-1) * (box_w + 0.01)
1659
- grid.rect(x = x_pos, y = box_y2, width = box_w, height = box_h,
1660
  just = c("left", "center"),
1661
- gp = gpar(fill = "#006F71", col = "black", lwd = 0.5))
1662
- grid.text(stat_labels_2[i], x = x_pos + box_w/2, y = box_y2 + 0.01,
1663
- gp = gpar(col = "white", cex = 0.5, fontface = "bold"))
1664
- grid.text(stat_values_2[i], x = x_pos + box_w/2, y = box_y2 - 0.008,
1665
- gp = gpar(col = "white", cex = 0.7, fontface = "bold"))
 
 
 
 
1666
  }
1667
 
1668
- # PDF button
1669
- grid.rect(x = 0.92, y = 0.84, width = 0.06, height = 0.06,
1670
- just = c("left", "center"),
1671
- gp = gpar(fill = "#E74C3C", col = "black", lwd = 0.5))
1672
- grid.text("PDF", x = 0.95, y = 0.84,
1673
- gp = gpar(col = "white", cex = 0.7, fontface = "bold"))
1674
-
1675
  # =====================================================================
1676
- # LOCATION PLOT (Left side, smaller)
1677
  # =====================================================================
1678
- pushViewport(viewport(x = 0.25, y = 0.60, width = 0.45, height = 0.32))
1679
  print(loc_plot, newpage = FALSE)
1680
  popViewport()
1681
 
@@ -1688,13 +1747,13 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1688
  metrics = c("Zone%", "Edge%", "Strike%", "Whiff%"),
1689
  pitch_types = pitch_types,
1690
  pitch_colors = tableau_pitch_colors,
1691
- x_start = 0.55, y_start = 0.73,
1692
- col_w = min(0.12, 0.4 / max(1, length(pitch_types))),
1693
- row_h = 0.018
1694
  )
1695
 
1696
  # =====================================================================
1697
- # PITCH USAGE TABLE (Right side, below location data)
1698
  # =====================================================================
1699
  draw_tableau_table(
1700
  title = "Pitch Usage",
@@ -1702,26 +1761,20 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1702
  metrics = c("Usage vs. LHH", "Usage vs. RHH", "Pitch Count"),
1703
  pitch_types = pitch_types,
1704
  pitch_colors = tableau_pitch_colors,
1705
- x_start = 0.55, y_start = 0.58,
1706
- col_w = min(0.12, 0.4 / max(1, length(pitch_types))),
1707
- row_h = 0.018
1708
  )
1709
 
1710
- # =====================================================================
1711
- # DIVIDER
1712
- # =====================================================================
1713
- grid.lines(x = c(0.02, 0.98), y = c(0.42, 0.42),
1714
- gp = gpar(col = "gray70", lty = "dotted"))
1715
-
1716
  # =====================================================================
1717
  # MOVEMENT PLOT (Left side)
1718
  # =====================================================================
1719
- pushViewport(viewport(x = 0.25, y = 0.28, width = 0.45, height = 0.26))
1720
  print(mov_plot, newpage = FALSE)
1721
  popViewport()
1722
 
1723
  # =====================================================================
1724
- # VELO & MOVEMENT TABLE (Right side)
1725
  # =====================================================================
1726
  draw_tableau_table(
1727
  title = "Velo & Movement",
@@ -1730,26 +1783,20 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1730
  "Avg. Vert Break", "Avg. Horz Break"),
1731
  pitch_types = pitch_types,
1732
  pitch_colors = tableau_pitch_colors,
1733
- x_start = 0.55, y_start = 0.38,
1734
- col_w = min(0.12, 0.4 / max(1, length(pitch_types))),
1735
- row_h = 0.016
1736
  )
1737
 
1738
  # =====================================================================
1739
- # DIVIDER
1740
  # =====================================================================
1741
- grid.lines(x = c(0.02, 0.98), y = c(0.14, 0.14),
1742
- gp = gpar(col = "gray70", lty = "dotted"))
1743
-
1744
- # =====================================================================
1745
- # RELEASE PLOT (Left side)
1746
- # =====================================================================
1747
- pushViewport(viewport(x = 0.25, y = 0.07, width = 0.45, height = 0.13))
1748
  print(rel_plot, newpage = FALSE)
1749
  popViewport()
1750
 
1751
  # =====================================================================
1752
- # RELEASE DATA TABLE (Right side)
1753
  # =====================================================================
1754
  draw_tableau_table(
1755
  title = "Release Data",
@@ -1759,14 +1806,14 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1759
  "Avg. Extension (ft.)"),
1760
  pitch_types = pitch_types,
1761
  pitch_colors = tableau_pitch_colors,
1762
- x_start = 0.55, y_start = 0.12,
1763
- col_w = min(0.12, 0.4 / max(1, length(pitch_types))),
1764
  row_h = 0.014
1765
  )
1766
 
1767
  invisible(output_file)
1768
  }
1769
-
1770
  # =====================================================================
1771
  # ===================== CATCHER CODE (wrapped) =======================
1772
  # =====================================================================
 
163
  # ====================== HITTER CODE (VERBATIM) ======================
164
  # =====================================================================
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  process_dataset <- function(df) {
167
  if ("Batter" %in% names(df)) {
168
  df <- df %>% mutate(Batter = stringr::str_replace(Batter, "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
 
1068
  }
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
+ # 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",
1091
+ "Sweeper" = "#E67E22",
1092
+ "Fastball" = "#E74C3C",
1093
+ "Four-Seam" = "#E74C3C",
1094
+ "FourSeamFastBall" = "#E74C3C",
1095
+ "4-Seam Fastball" = "#E74C3C",
1096
+ "Curveball" = "#9B59B6",
1097
+ "ChangeUp" = "#2ECC71",
1098
+ "Changeup" = "#2ECC71",
1099
+ "Splitter" = "#1ABC9C",
1100
+ "Knuckle Curve" = "#8E44AD",
1101
+ "Two-Seam" = "#F2A541",
1102
+ "TwoSeamFastBall" = "#F2A541",
1103
+ "Other" = "#95A5A6",
1104
+ "Undefined" = "#95A5A6"
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)) {
 
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
 
 
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
  )
 
1190
  df
1191
  }
1192
 
1193
+ # =====================================================================
1194
+ # HEADER STATISTICS
1195
+ # =====================================================================
1196
  calculate_tableau_header_stats <- function(pitcher_df) {
 
1197
  ab_data <- pitcher_df %>%
1198
  filter(ABindicator == 1) %>%
1199
  group_by(Inning, Batter, PAofInning) %>%
 
1205
  xbh <- sum(ab_data$PlayResult %in% c("Double", "Triple", "HomeRun"), na.rm = TRUE)
1206
  runs <- sum(pitcher_df$RunsScored, na.rm = TRUE)
1207
 
 
1208
  pa_data <- pitcher_df %>%
1209
  filter(PAindicator == 1) %>%
1210
  group_by(Inning, Batter, PAofInning) %>%
 
1215
  hbp <- sum(pa_data$HBPIndicator, na.rm = TRUE)
1216
  so <- sum(pa_data$KorBB == "Strikeout", na.rm = TRUE)
1217
 
 
1218
  avg <- ifelse(at_bats > 0, round(hits / at_bats, 3), 0)
1219
 
 
1220
  total_pitches <- nrow(pitcher_df)
1221
  strike_pct <- ifelse(total_pitches > 0,
1222
  round(100 * sum(pitcher_df$StrikeIndicator, na.rm = TRUE) / total_pitches, 0), 0)
 
1225
  fp_k_pct <- ifelse(fp_pitches > 0,
1226
  round(100 * sum(pitcher_df$FPSindicator, na.rm = TRUE) / fp_pitches, 0), 0)
1227
 
 
1228
  ea_pct <- ifelse(fp_pitches > 0,
1229
  round(100 * (sum(pitcher_df$FPSindicator, na.rm = TRUE) +
1230
  sum(pitcher_df$AheadIndicator & pitcher_df$Balls == 0, na.rm = TRUE)) / fp_pitches, 0), 0)
1231
 
 
1232
  comp_pct <- ifelse(total_pitches > 0,
1233
  round(100 * sum(pitcher_df$QualityPitchIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1234
 
 
1235
  leadoff_opps <- sum(pitcher_df$LeadOffIndicator, na.rm = TRUE)
1236
  loo_pct <- ifelse(leadoff_opps > 0,
1237
  round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
 
1252
  )
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) %>%
1276
  group_by(TaggedPitchType) %>%
1277
  summarise(
1278
  `Zone%` = paste0(round(100 * sum(StrikeZoneIndicator, na.rm = TRUE) / n(), 0), "%"),
 
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) %>%
1307
  group_by(TaggedPitchType) %>%
1308
  summarise(
1309
  `Pitch Count` = n(),
 
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) %>%
1332
  group_by(TaggedPitchType) %>%
1333
  summarise(
1334
  `Avg. Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
 
1342
  }
1343
 
1344
  calculate_tableau_release_data <- function(pitcher_df) {
 
1345
  primary <- 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),
 
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(
 
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
1421
+ zone_bottom <- 1.5; zone_top <- 3.5
1422
+ shadow_left <- -1.1; shadow_right <- 1.1
1423
+ shadow_bottom <- 1.2; shadow_top <- 3.8
 
 
 
 
 
 
 
 
 
1424
  zone_width <- (zone_right - zone_left) / 3
1425
  zone_height <- (zone_top - zone_bottom) / 3
1426
 
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(),
 
1473
  p
1474
  }
1475
 
1476
+ # =====================================================================
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,
1495
  !is.na(HorzBreak), !is.na(InducedVertBreak))
1496
 
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)) +
 
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,
1538
  !is.na(RelSide), !is.na(RelHeight))
1539
 
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(
1549
  x = mound_radius * cos(mound_theta),
1550
+ y = mound_radius * sin(mound_theta) * 0.35
1551
  )
1552
 
1553
  ggplot(df, aes(x = RelSide, y = RelHeight)) +
 
1554
  geom_polygon(data = mound_df, aes(x = x, y = y),
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,
 
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)
 
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)
1627
 
 
1628
  pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1629
  filter(Pitcher == pitcher_name)
1630
 
 
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)
1655
  loc_data <- calculate_tableau_location_data(pitcher_df)
1656
  usage_data <- calculate_tableau_pitch_usage(pitcher_df)
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"
1664
 
1665
  # Create plots
1666
  loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
 
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
 
 
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",
 
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",
 
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",
 
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)
1815
  }
1816
+
1817
  # =====================================================================
1818
  # ===================== CATCHER CODE (wrapped) =======================
1819
  # =====================================================================