igroffman commited on
Commit
61f598c
·
verified ·
1 Parent(s): 38a4a20

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +460 -0
app.R CHANGED
@@ -1068,6 +1068,412 @@ create_postgame_pdf <- function(game_df, player_name, output_file, bio_data = NU
1068
  }
1069
  }
1070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1071
  # =====================================================================
1072
  # ===================== CATCHER CODE (wrapped) =======================
1073
  # =====================================================================
@@ -3527,6 +3933,7 @@ radioButtons("report_type", "Report Type",
3527
  c("Hitter"="hitter",
3528
  "Pitcher"="pitcher",
3529
  "Advanced Pitcher"="advanced_pitcher",
 
3530
  "Catcher"="catcher",
3531
  "Umpire"="umpire",
3532
  "BP Report"="bp"),
@@ -3688,6 +4095,13 @@ output$selector_ui <- renderUI({
3688
  if (!length(players)) return(div(p("No players found in BP data",
3689
  style="color:#cc6600;font-weight:bold;")))
3690
  selectInput("bp_player_name", "Select Player", choices = players, selected = players[1], width = "100%")
 
 
 
 
 
 
 
3691
 
3692
  } else if (input$report_type == "advanced_pitcher") {
3693
  df <- data_pitcher()
@@ -3736,6 +4150,9 @@ output$download_ui <- renderUI({
3736
 
3737
  } else if (input$report_type == "umpire") {
3738
  downloadButton("download_umpire", "Download Umpire PDF", class = "btn-primary")
 
 
 
3739
 
3740
  } else if (input$report_type == "bp") {
3741
  downloadButton("download_bp", "Download BP Report PDF", class = "btn-primary")
@@ -4051,6 +4468,18 @@ output$leaderboard_ui <- renderUI({
4051
  h4("Count Usage (Ahead/Behind)", style="color:#006F71;"),
4052
  plotOutput("preview_advanced_count", height="380px")
4053
  )
 
 
 
 
 
 
 
 
 
 
 
 
4054
 
4055
  } else if (input$report_type == "catcher") {
4056
  df <- data_catcher()
@@ -4113,6 +4542,18 @@ output$leaderboard_ui <- renderUI({
4113
  create_bp_spray_chart(input$bp_player_name, df)
4114
  }, res = 96)
4115
 
 
 
 
 
 
 
 
 
 
 
 
 
4116
  output$preview_bp_zone <- renderPlot({
4117
  df <- data_bp(); req(df, input$bp_player_name)
4118
  create_bp_zone_plot(input$bp_player_name, df)
@@ -4277,6 +4718,25 @@ output$preview_bp_zone <- renderPlot({
4277
  },
4278
  contentType = "application/zip"
4279
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4280
 
4281
  output$download_all_coastal_hitters <- downloadHandler(
4282
  filename = function() {
 
1068
  }
1069
  }
1070
 
1071
+ # =====================================================================
1072
+ # TABLEAU-STYLE PITCHER REPORT FUNCTIONS
1073
+ # =====================================================================
1074
+
1075
+ tableau_pitch_colors <- c(
1076
+ "Fastball" = "#FA8072", "FourSeamFastBall" = "#FA8072", "Four-Seam" = "#FA8072",
1077
+ "Sinker" = "#F2A541", "Slider" = "#9B59B6", "Sweeper" = "#9B59B6",
1078
+ "Curveball" = "#2c7bb6", "ChangeUp" = "#90EE90", "Splitter" = "#90EE32",
1079
+ "Cutter" = "#1ABC9C"
1080
+ )
1081
+
1082
+ process_tableau_pitcher_data <- function(df) {
1083
+ if (!"Pitcher" %in% names(df)) {
1084
+ alt <- intersect(c("PitcherName","pitcher","Pitcher_LastFirst","PlayerName"), names(df))
1085
+ if (length(alt)) df$Pitcher <- df[[alt[1]]] else df$Pitcher <- NA_character_
1086
+ }
1087
+ df <- df %>%
1088
+ mutate(Pitcher = stringr::str_replace(coalesce(Pitcher, ""), "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
1089
+
1090
+ df <- df %>%
1091
+ mutate(
1092
+ StrikeZoneIndicator = ifelse(PlateLocSide >= -0.8333 & PlateLocSide <= 0.8333 &
1093
+ PlateLocHeight >= 1.5 & PlateLocHeight <= 3.37467, 1, 0),
1094
+ EdgeHeightIndicator = ifelse((PlateLocHeight > 14/12 & PlateLocHeight < 22/12) |
1095
+ (PlateLocHeight > 38/12 & PlateLocHeight < 46/12), 1, 0),
1096
+ EdgeZoneHtIndicator = ifelse(PlateLocHeight > 16/12 & PlateLocHeight < 45.2/12, 1, 0),
1097
+ EdgeZoneWIndicator = ifelse(PlateLocSide > -13.4/12 & PlateLocSide < 13.4/12, 1, 0),
1098
+ EdgeWidthIndicator = ifelse((PlateLocSide > -13.3/12 & PlateLocSide < -6.7/12) |
1099
+ (PlateLocSide < 13.3/12 & PlateLocSide > 6.7/12), 1, 0),
1100
+ EdgeIndicator = ifelse((EdgeHeightIndicator == 1 & EdgeZoneWIndicator == 1) |
1101
+ (EdgeWidthIndicator == 1 & EdgeZoneHtIndicator == 1), 1, 0),
1102
+ QualityPitchIndicator = ifelse(StrikeZoneIndicator == 1 | EdgeIndicator == 1, 1, 0),
1103
+ StrikeIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "StrikeCalled", "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1104
+ WhiffIndicator = ifelse(PitchCall == "StrikeSwinging", 1, 0),
1105
+ SwingIndicator = ifelse(PitchCall %in% c("StrikeSwinging", "FoulBallNotFieldable", "FoulBall", "InPlay"), 1, 0),
1106
+ FPindicator = ifelse(Balls == 0 & Strikes == 0, 1, 0),
1107
+ FPSindicator = ifelse(PitchCall %in% c("StrikeCalled", "StrikeSwinging", "FoulBallNotFieldable", "FoulBall", "InPlay") & FPindicator == 1, 1, 0),
1108
+ AheadIndicator = ifelse(((Balls == 0 & Strikes == 1) | (Balls == 1 & Strikes == 1)) & StrikeIndicator == 1, 1, 0),
1109
+ ABindicator = ifelse(PlayResult %in% c("Error", "FieldersChoice", "Out", "Single", "Double", "Triple", "HomeRun") | KorBB == "Strikeout", 1, 0),
1110
+ HitIndicator = ifelse(PlayResult %in% c("Single", "Double", "Triple", "HomeRun"), 1, 0),
1111
+ PAindicator = ifelse(PitchCall %in% c("InPlay", "HitByPitch", "CatchersInterference") | KorBB %in% c("Walk", "Strikeout"), 1, 0),
1112
+ LeadOffIndicator = ifelse((PAofInning == 1 & (PlayResult != "Undefined" | KorBB != "Undefined")) | PitchCall == "HitByPitch", 1, 0),
1113
+ OutIndicator = ifelse((PlayResult %in% c("Out", "FieldersChoice") | KorBB == "Strikeout") & PitchCall != "HitByPitch", 1, 0),
1114
+ LOOindicator = ifelse(LeadOffIndicator == 1 & OutIndicator == 1, 1, 0),
1115
+ HBPIndicator = ifelse(PitchCall == "HitByPitch", 1, 0),
1116
+ WalkIndicator = ifelse(KorBB == "Walk", 1, 0),
1117
+ LHHindicator = ifelse(BatterSide == "Left", 1, 0),
1118
+ RHHindicator = ifelse(BatterSide == "Right", 1, 0)
1119
+ )
1120
+ df
1121
+ }
1122
+
1123
+ calculate_tableau_header_stats <- function(pitcher_df) {
1124
+ ab_data <- pitcher_df %>% filter(ABindicator == 1) %>%
1125
+ group_by(Inning, Batter, PAofInning) %>% slice_tail(n = 1) %>% ungroup()
1126
+
1127
+ at_bats <- nrow(ab_data)
1128
+ hits <- sum(ab_data$HitIndicator, na.rm = TRUE)
1129
+ xbh <- sum(ab_data$PlayResult %in% c("Double", "Triple", "HomeRun"), na.rm = TRUE)
1130
+ runs <- sum(pitcher_df$RunsScored, na.rm = TRUE)
1131
+
1132
+ pa_data <- pitcher_df %>% filter(PAindicator == 1) %>%
1133
+ group_by(Inning, Batter, PAofInning) %>% slice_tail(n = 1) %>% ungroup()
1134
+
1135
+ bb <- sum(pa_data$WalkIndicator, na.rm = TRUE)
1136
+ hbp <- sum(pa_data$HBPIndicator, na.rm = TRUE)
1137
+ so <- sum(pa_data$KorBB == "Strikeout", na.rm = TRUE)
1138
+ avg <- ifelse(at_bats > 0, round(hits / at_bats, 3), 0)
1139
+
1140
+ total_pitches <- nrow(pitcher_df)
1141
+ strike_pct <- ifelse(total_pitches > 0, round(100 * sum(pitcher_df$StrikeIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1142
+ fp_pitches <- sum(pitcher_df$FPindicator, na.rm = TRUE)
1143
+ fp_k_pct <- ifelse(fp_pitches > 0, round(100 * sum(pitcher_df$FPSindicator, na.rm = TRUE) / fp_pitches, 0), 0)
1144
+ ea_pct <- ifelse(fp_pitches > 0, round(100 * (sum(pitcher_df$FPSindicator, na.rm = TRUE) + sum(pitcher_df$AheadIndicator & pitcher_df$Balls == 0, na.rm = TRUE)) / fp_pitches, 0), 0)
1145
+ comp_pct <- ifelse(total_pitches > 0, round(100 * sum(pitcher_df$QualityPitchIndicator, na.rm = TRUE) / total_pitches, 0), 0)
1146
+ leadoff_opps <- sum(pitcher_df$LeadOffIndicator, na.rm = TRUE)
1147
+ loo_pct <- ifelse(leadoff_opps > 0, round(100 * sum(pitcher_df$LOOindicator, na.rm = TRUE) / leadoff_opps, 0), 0)
1148
+
1149
+ list(at_bats = at_bats, hits = hits, xbh = xbh, runs = runs, bb_hbp = bb + hbp, so = so,
1150
+ avg = sprintf("%.3f", avg), strike_pct = paste0(strike_pct, "%"), fp_k_pct = paste0(fp_k_pct, "%"),
1151
+ ea_pct = paste0(ea_pct, "%"), comp_pct = paste0(comp_pct, "%"), loo_pct = paste0(loo_pct, "%"))
1152
+ }
1153
+
1154
+ calculate_tableau_location_data <- function(pitcher_df) {
1155
+ pitcher_df %>%
1156
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other") %>%
1157
+ group_by(TaggedPitchType) %>%
1158
+ summarise(
1159
+ `Zone%` = round(100 * sum(StrikeZoneIndicator, na.rm = TRUE) / n(), 0),
1160
+ `Edge%` = round(100 * sum(EdgeIndicator, na.rm = TRUE) / n(), 0),
1161
+ `Strike%` = round(100 * sum(StrikeIndicator, na.rm = TRUE) / n(), 0),
1162
+ `Whiff%` = ifelse(sum(SwingIndicator, na.rm = TRUE) > 0,
1163
+ round(100 * sum(WhiffIndicator, na.rm = TRUE) / sum(SwingIndicator, na.rm = TRUE), 0), 0),
1164
+ .groups = "drop"
1165
+ )
1166
+ }
1167
+
1168
+ calculate_tableau_pitch_usage <- function(pitcher_df) {
1169
+ lhh_pitches <- sum(pitcher_df$LHHindicator, na.rm = TRUE)
1170
+ rhh_pitches <- sum(pitcher_df$RHHindicator, na.rm = TRUE)
1171
+
1172
+ pitcher_df %>%
1173
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other") %>%
1174
+ group_by(TaggedPitchType) %>%
1175
+ summarise(
1176
+ `Pitch Count` = n(),
1177
+ `Usage vs. LHH` = paste0(round(100 * sum(LHHindicator, na.rm = TRUE) / max(1, lhh_pitches), 0), "%"),
1178
+ `Usage vs. RHH` = paste0(round(100 * sum(RHHindicator, na.rm = TRUE) / max(1, rhh_pitches), 0), "%"),
1179
+ .groups = "drop"
1180
+ )
1181
+ }
1182
+
1183
+ calculate_tableau_velo_movement <- function(pitcher_df) {
1184
+ pitcher_df %>%
1185
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other") %>%
1186
+ group_by(TaggedPitchType) %>%
1187
+ summarise(
1188
+ `Avg. Velo` = round(mean(RelSpeed, na.rm = TRUE), 1),
1189
+ `Max. Velo` = round(max(RelSpeed, na.rm = TRUE), 1),
1190
+ `Avg. Spin Rate` = format(round(mean(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1191
+ `Max. Spin Rate` = format(round(max(SpinRate, na.rm = TRUE), 0), big.mark = ","),
1192
+ `Avg. Vert Break` = round(mean(InducedVertBreak, na.rm = TRUE), 0),
1193
+ `Avg. Horz Break` = round(mean(HorzBreak, na.rm = TRUE), 0),
1194
+ .groups = "drop"
1195
+ )
1196
+ }
1197
+
1198
+ calculate_tableau_release_data <- function(pitcher_df) {
1199
+ primary <- pitcher_df %>% filter(TaggedPitchType %in% c("Fastball", "Sinker", "Four-Seam", "FourSeamFastBall"))
1200
+ fb_ht <- if (nrow(primary) > 0) mean(primary$RelHeight, na.rm = TRUE) else NA
1201
+ fb_side <- if (nrow(primary) > 0) mean(primary$RelSide, na.rm = TRUE) else NA
1202
+
1203
+ pitcher_df %>%
1204
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other") %>%
1205
+ group_by(TaggedPitchType) %>%
1206
+ summarise(
1207
+ `Avg. Rel Height (ft.)` = round(mean(RelHeight, na.rm = TRUE), 2),
1208
+ `Rel Ht vs. FB (in.)` = round((mean(RelHeight, na.rm = TRUE) - fb_ht) * 12, 0),
1209
+ `Avg. Rel Side (ft.)` = round(mean(RelSide, na.rm = TRUE), 2),
1210
+ `Rel Side vs. FB (in.)` = round((mean(RelSide, na.rm = TRUE) - fb_side) * 12, 0),
1211
+ `Avg. Extension (ft.)` = round(mean(Extension, na.rm = TRUE), 2),
1212
+ .groups = "drop"
1213
+ )
1214
+ }
1215
+
1216
+ create_tableau_location_plot <- function(pitcher_df, pitch_colors) {
1217
+ df <- pitcher_df %>%
1218
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other",
1219
+ !is.na(PlateLocSide), !is.na(PlateLocHeight)) %>%
1220
+ mutate(ResultDisplay = case_when(
1221
+ PitchCall %in% c("BallCalled", "BallinDirt") ~ "BallCalled",
1222
+ PlayResult == "Double" ~ "Double",
1223
+ PitchCall %in% c("FoulBall", "FoulBallNotFieldable") ~ "FoulBall",
1224
+ PitchCall == "HitByPitch" ~ "HitByPitch",
1225
+ PlayResult %in% c("Sacrifice", "SacrificeFly") ~ "Sacrifice",
1226
+ PlayResult == "Single" ~ "Single",
1227
+ PitchCall == "StrikeCalled" ~ "StrikeCalled",
1228
+ PitchCall == "StrikeSwinging" ~ "StrikeSwinging",
1229
+ PlayResult == "Triple" ~ "Triple",
1230
+ TRUE ~ "Other"
1231
+ ))
1232
+
1233
+ if (nrow(df) == 0) return(ggplot() + theme_void() + ggtitle("Location Report"))
1234
+
1235
+ ggplot(df, aes(x = PlateLocSide, y = PlateLocHeight)) +
1236
+ annotate("rect", xmin = -0.8333, xmax = 0.8333, ymin = 1.5, ymax = 3.37467, fill = NA, color = "black", linewidth = 0.8) +
1237
+ annotate("segment", x = -0.708, y = 0.15, xend = 0.708, yend = 0.15, color = "black") +
1238
+ annotate("segment", x = -0.708, y = 0.30, xend = -0.708, yend = 0.15, color = "black") +
1239
+ annotate("segment", x = 0.708, y = 0.30, xend = 0.708, yend = 0.15, color = "black") +
1240
+ annotate("segment", x = -0.708, y = 0.30, xend = 0, yend = 0.50, color = "black") +
1241
+ annotate("segment", x = 0.708, y = 0.30, xend = 0, yend = 0.50, color = "black") +
1242
+ geom_point(aes(color = TaggedPitchType, shape = ResultDisplay), size = 3.5, stroke = 1.2) +
1243
+ scale_color_manual(values = pitch_colors, name = "Pitch Type") +
1244
+ scale_shape_manual(values = c("BallCalled" = 1, "Double" = 18, "FoulBall" = 2, "HitByPitch" = 8,
1245
+ "Sacrifice" = 3, "Single" = 19, "StrikeCalled" = 4,
1246
+ "StrikeSwinging" = 8, "Triple" = 17, "Other" = 16),
1247
+ name = "Play Result") +
1248
+ coord_fixed(xlim = c(-2, 2), ylim = c(0, 4.5)) +
1249
+ labs(title = "Location Report") +
1250
+ theme_minimal() +
1251
+ theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
1252
+ legend.position = "left", axis.text = element_blank(),
1253
+ axis.ticks = element_blank(), panel.grid = element_blank())
1254
+ }
1255
+
1256
+ create_tableau_movement_plot <- function(pitcher_df, pitch_colors) {
1257
+ df <- pitcher_df %>%
1258
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other",
1259
+ !is.na(HorzBreak), !is.na(InducedVertBreak))
1260
+
1261
+ if (nrow(df) == 0) return(ggplot() + theme_void() + ggtitle("Movement Profile"))
1262
+
1263
+ ggplot(df, aes(x = HorzBreak, y = InducedVertBreak, color = TaggedPitchType)) +
1264
+ geom_vline(xintercept = 0, color = "black") +
1265
+ geom_hline(yintercept = 0, color = "black") +
1266
+ geom_point(size = 3, alpha = 0.8) +
1267
+ scale_color_manual(values = pitch_colors) +
1268
+ coord_cartesian(xlim = c(-30, 30), ylim = c(-30, 30)) +
1269
+ labs(title = "Movement Profile", x = "Horz Break", y = "Induced Vert Break") +
1270
+ theme_minimal() +
1271
+ theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5), legend.position = "none")
1272
+ }
1273
+
1274
+ create_tableau_release_plot <- function(pitcher_df, pitch_colors) {
1275
+ df <- pitcher_df %>%
1276
+ filter(!is.na(TaggedPitchType), TaggedPitchType != "Other",
1277
+ !is.na(RelSide), !is.na(RelHeight))
1278
+
1279
+ if (nrow(df) == 0) return(ggplot() + theme_void() + ggtitle("Release Plot"))
1280
+
1281
+ ggplot(df, aes(x = RelSide, y = RelHeight, color = TaggedPitchType)) +
1282
+ geom_point(size = 3, alpha = 0.8) +
1283
+ scale_color_manual(values = pitch_colors) +
1284
+ coord_cartesian(xlim = c(-4, 4), ylim = c(0, 6)) +
1285
+ labs(title = "Release Plot", x = "Rel Side", y = "Rel Height") +
1286
+ theme_minimal() +
1287
+ theme(plot.title = element_text(size = 14, face = "bold", hjust = 0.5), legend.position = "none")
1288
+ }
1289
+
1290
+ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1291
+ if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1292
+
1293
+ pitcher_df <- process_tableau_pitcher_data(game_df) %>% filter(Pitcher == pitcher_name)
1294
+
1295
+ if (nrow(pitcher_df) == 0) {
1296
+ pdf(output_file, width = 11, height = 8.5)
1297
+ grid::grid.newpage()
1298
+ grid::grid.text(paste("No data for", pitcher_name), gp = grid::gpar(fontsize = 16, fontface = "bold"))
1299
+ dev.off()
1300
+ return(output_file)
1301
+ }
1302
+
1303
+ game_date <- tryCatch({
1304
+ d <- unique(pitcher_df$Date)[1]
1305
+ if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
1306
+ }, error = function(e) "Unknown")
1307
+
1308
+ pitcher_team <- unique(pitcher_df$PitcherTeam)[1]
1309
+ if (is.na(pitcher_team)) pitcher_team <- "Multiple values"
1310
+ batter_team_str <- if (length(unique(pitcher_df$BatterTeam)) > 1) "All" else unique(pitcher_df$BatterTeam)[1]
1311
+
1312
+ stats <- calculate_tableau_header_stats(pitcher_df)
1313
+ loc_data <- calculate_tableau_location_data(pitcher_df)
1314
+ usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1315
+ velo_data <- calculate_tableau_velo_movement(pitcher_df)
1316
+ rel_data <- calculate_tableau_release_data(pitcher_df)
1317
+
1318
+ loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
1319
+ mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1320
+ rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1321
+
1322
+ pitch_types <- loc_data$TaggedPitchType
1323
+
1324
+ pdf(output_file, width = 11, height = 8.5)
1325
+ on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1326
+ grid::grid.newpage()
1327
+
1328
+ # Header
1329
+ grid::grid.rect(x = 0, y = 0.92, width = 1, height = 0.08, just = c("left", "bottom"),
1330
+ gp = grid::gpar(fill = "#006F71", col = NA))
1331
+ grid::grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.96, just = "left",
1332
+ gp = grid::gpar(col = "white", fontface = "bold", cex = 1.5))
1333
+
1334
+ # Info row
1335
+ info_labels <- c("Date", "Pitcher Team", "Pitcher", "Batter Team", "vs. L/R")
1336
+ info_values <- c(game_date, pitcher_team, pitcher_name, batter_team_str, "All")
1337
+ for (i in 1:5) {
1338
+ grid::grid.text(info_labels[i], x = 0.02 + (i-1)*0.18, y = 0.90, just = "left", gp = grid::gpar(cex = 0.7, col = "gray50"))
1339
+ grid::grid.text(info_values[i], x = 0.02 + (i-1)*0.18, y = 0.87, just = "left", gp = grid::gpar(cex = 0.8, fontface = "bold"))
1340
+ }
1341
+
1342
+ # Stat boxes
1343
+ stat_labels <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG")
1344
+ stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs, stats$bb_hbp, stats$so, stats$avg)
1345
+ stat_colors <- c("#006F71", "#006F71", "#006F71", "#E74C3C", "#006F71", "#006F71", "#006F71")
1346
+
1347
+ for (i in 1:7) {
1348
+ x <- 0.02 + (i-1)*0.07
1349
+ grid::grid.rect(x = x, y = 0.82, width = 0.065, height = 0.055, just = c("left", "center"),
1350
+ gp = grid::gpar(fill = stat_colors[i], col = "black"))
1351
+ grid::grid.text(stat_labels[i], x = x + 0.0325, y = 0.835, gp = grid::gpar(col = "white", cex = 0.55, fontface = "bold"))
1352
+ grid::grid.text(stat_values[i], x = x + 0.0325, y = 0.805, gp = grid::gpar(col = "white", cex = 0.8, fontface = "bold"))
1353
+ }
1354
+
1355
+ # Percentage boxes
1356
+ pct_labels <- c("Strike%", "1st P K%", "E+A%", "Comp%", "LOO%")
1357
+ pct_values <- c(stats$strike_pct, stats$fp_k_pct, stats$ea_pct, stats$comp_pct, stats$loo_pct)
1358
+ for (i in 1:5) {
1359
+ x <- 0.55 + (i-1)*0.075
1360
+ grid::grid.rect(x = x, y = 0.82, width = 0.07, height = 0.055, just = c("left", "center"),
1361
+ gp = grid::gpar(fill = "#006F71", col = "black"))
1362
+ grid::grid.text(pct_labels[i], x = x + 0.035, y = 0.835, gp = grid::gpar(col = "white", cex = 0.5, fontface = "bold"))
1363
+ grid::grid.text(pct_values[i], x = x + 0.035, y = 0.805, gp = grid::gpar(col = "white", cex = 0.75, fontface = "bold"))
1364
+ }
1365
+
1366
+ # PDF button
1367
+ grid::grid.rect(x = 0.94, y = 0.82, width = 0.05, height = 0.055, just = c("left", "center"),
1368
+ gp = grid::gpar(fill = "#E74C3C", col = "black"))
1369
+ grid::grid.text("PDF", x = 0.965, y = 0.82, gp = grid::gpar(col = "white", cex = 0.7, fontface = "bold"))
1370
+
1371
+ # Location Plot
1372
+ grid::pushViewport(grid::viewport(x = 0.22, y = 0.52, width = 0.40, height = 0.28))
1373
+ print(loc_plot, newpage = FALSE)
1374
+ grid::popViewport()
1375
+
1376
+ # Location Data Table
1377
+ grid::grid.text("Location Data", x = 0.75, y = 0.74, gp = grid::gpar(fontface = "bold", cex = 1.0))
1378
+ col_w <- 0.07; row_h <- 0.025
1379
+ for (i in seq_along(pitch_types)) {
1380
+ grid::grid.rect(x = 0.58 + i*col_w, y = 0.70, width = col_w*0.95, height = row_h,
1381
+ just = c("left", "center"), gp = grid::gpar(fill = tableau_pitch_colors[pitch_types[i]], col = "black"))
1382
+ grid::grid.text(pitch_types[i], x = 0.58 + i*col_w + col_w/2, y = 0.70,
1383
+ gp = grid::gpar(col = "white", cex = 0.5, fontface = "bold"))
1384
+ }
1385
+ metrics <- c("Zone%", "Edge%", "Strike%", "Whiff%")
1386
+ for (m in seq_along(metrics)) {
1387
+ y <- 0.70 - m*row_h
1388
+ grid::grid.text(metrics[m], x = 0.58, y = y, just = "left", gp = grid::gpar(cex = 0.55, fontface = "bold"))
1389
+ for (i in seq_along(pitch_types)) {
1390
+ val <- loc_data[[metrics[m]]][i]
1391
+ grid::grid.rect(x = 0.58 + i*col_w, y = y, width = col_w*0.95, height = row_h*0.9,
1392
+ just = c("left", "center"), gp = grid::gpar(fill = "white", col = "gray80"))
1393
+ grid::grid.text(paste0(val, "%"), x = 0.58 + i*col_w + col_w/2, y = y, gp = grid::gpar(cex = 0.5))
1394
+ }
1395
+ }
1396
+
1397
+ # Pitch Usage Table
1398
+ grid::grid.text("Pitch Usage", x = 0.75, y = 0.55, gp = grid::gpar(fontface = "bold", cex = 1.0))
1399
+ for (i in seq_along(pitch_types)) {
1400
+ grid::grid.rect(x = 0.58 + i*col_w, y = 0.52, width = col_w*0.95, height = row_h,
1401
+ just = c("left", "center"), gp = grid::gpar(fill = tableau_pitch_colors[pitch_types[i]], col = "black"))
1402
+ grid::grid.text(pitch_types[i], x = 0.58 + i*col_w + col_w/2, y = 0.52, gp = grid::gpar(col = "white", cex = 0.5, fontface = "bold"))
1403
+ }
1404
+ usage_metrics <- c("Usage vs. LHH", "Usage vs. RHH", "Pitch Count")
1405
+ for (m in seq_along(usage_metrics)) {
1406
+ y <- 0.52 - m*row_h
1407
+ grid::grid.text(usage_metrics[m], x = 0.58, y = y, just = "left", gp = grid::gpar(cex = 0.55, fontface = "bold"))
1408
+ for (i in seq_along(pitch_types)) {
1409
+ idx <- which(usage_data$TaggedPitchType == pitch_types[i])
1410
+ val <- if (length(idx) > 0) usage_data[[usage_metrics[m]]][idx] else "-"
1411
+ grid::grid.rect(x = 0.58 + i*col_w, y = y, width = col_w*0.95, height = row_h*0.9,
1412
+ just = c("left", "center"), gp = grid::gpar(fill = "white", col = "gray80"))
1413
+ grid::grid.text(as.character(val), x = 0.58 + i*col_w + col_w/2, y = y, gp = grid::gpar(cex = 0.5))
1414
+ }
1415
+ }
1416
+
1417
+ # Divider
1418
+ grid::grid.lines(x = c(0, 1), y = c(0.38, 0.38), gp = grid::gpar(col = "gray80", lty = "dotted"))
1419
+
1420
+ # Movement Plot
1421
+ grid::pushViewport(grid::viewport(x = 0.22, y = 0.22, width = 0.38, height = 0.16))
1422
+ print(mov_plot, newpage = FALSE)
1423
+ grid::popViewport()
1424
+
1425
+ # Velo & Movement Table
1426
+ grid::grid.text("Velo & Movement", x = 0.75, y = 0.36, gp = grid::gpar(fontface = "bold", cex = 1.0))
1427
+ for (i in seq_along(pitch_types)) {
1428
+ grid::grid.rect(x = 0.58 + i*col_w, y = 0.33, width = col_w*0.95, height = row_h,
1429
+ just = c("left", "center"), gp = grid::gpar(fill = tableau_pitch_colors[pitch_types[i]], col = "black"))
1430
+ grid::grid.text(pitch_types[i], x = 0.58 + i*col_w + col_w/2, y = 0.33, gp = grid::gpar(col = "white", cex = 0.5, fontface = "bold"))
1431
+ }
1432
+ velo_metrics <- c("Avg. Velo", "Max. Velo", "Avg. Spin Rate", "Max. Spin Rate", "Avg. Vert Break", "Avg. Horz Break")
1433
+ for (m in seq_along(velo_metrics)) {
1434
+ y <- 0.33 - m*row_h
1435
+ grid::grid.text(velo_metrics[m], x = 0.58, y = y, just = "left", gp = grid::gpar(cex = 0.5, fontface = "bold"))
1436
+ for (i in seq_along(pitch_types)) {
1437
+ idx <- which(velo_data$TaggedPitchType == pitch_types[i])
1438
+ val <- if (length(idx) > 0) velo_data[[velo_metrics[m]]][idx] else "-"
1439
+ grid::grid.rect(x = 0.58 + i*col_w, y = y, width = col_w*0.95, height = row_h*0.9,
1440
+ just = c("left", "center"), gp = grid::gpar(fill = "white", col = "gray80"))
1441
+ grid::grid.text(as.character(val), x = 0.58 + i*col_w + col_w/2, y = y, gp = grid::gpar(cex = 0.45))
1442
+ }
1443
+ }
1444
+
1445
+ # Divider
1446
+ grid::grid.lines(x = c(0, 1), y = c(0.14, 0.14), gp = grid::gpar(col = "gray80", lty = "dotted"))
1447
+
1448
+ # Release Plot
1449
+ grid::pushViewport(grid::viewport(x = 0.22, y = 0.07, width = 0.38, height = 0.12))
1450
+ print(rel_plot, newpage = FALSE)
1451
+ grid::popViewport()
1452
+
1453
+ # Release Data Table
1454
+ grid::grid.text("Release Data", x = 0.75, y = 0.13, gp = grid::gpar(fontface = "bold", cex = 1.0))
1455
+ row_h_rel <- 0.018
1456
+ for (i in seq_along(pitch_types)) {
1457
+ grid::grid.rect(x = 0.58 + i*col_w, y = 0.10, width = col_w*0.95, height = row_h_rel,
1458
+ just = c("left", "center"), gp = grid::gpar(fill = tableau_pitch_colors[pitch_types[i]], col = "black"))
1459
+ grid::grid.text(pitch_types[i], x = 0.58 + i*col_w + col_w/2, y = 0.10, gp = grid::gpar(col = "white", cex = 0.45, fontface = "bold"))
1460
+ }
1461
+ rel_metrics <- c("Avg. Rel Height (ft.)", "Rel Ht vs. FB (in.)", "Avg. Rel Side (ft.)", "Rel Side vs. FB (in.)", "Avg. Extension (ft.)")
1462
+ for (m in seq_along(rel_metrics)) {
1463
+ y <- 0.10 - m*row_h_rel
1464
+ grid::grid.text(rel_metrics[m], x = 0.58, y = y, just = "left", gp = grid::gpar(cex = 0.45, fontface = "bold"))
1465
+ for (i in seq_along(pitch_types)) {
1466
+ idx <- which(rel_data$TaggedPitchType == pitch_types[i])
1467
+ val <- if (length(idx) > 0) rel_data[[rel_metrics[m]]][idx] else "-"
1468
+ grid::grid.rect(x = 0.58 + i*col_w, y = y, width = col_w*0.95, height = row_h_rel*0.9,
1469
+ just = c("left", "center"), gp = grid::gpar(fill = "white", col = "gray80"))
1470
+ grid::grid.text(as.character(val), x = 0.58 + i*col_w + col_w/2, y = y, gp = grid::gpar(cex = 0.42))
1471
+ }
1472
+ }
1473
+
1474
+ invisible(output_file)
1475
+ }
1476
+
1477
  # =====================================================================
1478
  # ===================== CATCHER CODE (wrapped) =======================
1479
  # =====================================================================
 
3933
  c("Hitter"="hitter",
3934
  "Pitcher"="pitcher",
3935
  "Advanced Pitcher"="advanced_pitcher",
3936
+ "Tableau Pitcher"="tableau_pitcher",
3937
  "Catcher"="catcher",
3938
  "Umpire"="umpire",
3939
  "BP Report"="bp"),
 
4095
  if (!length(players)) return(div(p("No players found in BP data",
4096
  style="color:#cc6600;font-weight:bold;")))
4097
  selectInput("bp_player_name", "Select Player", choices = players, selected = players[1], width = "100%")
4098
+
4099
+ } else if (input$report_type == "tableau_pitcher") {
4100
+ df <- data_pitcher()
4101
+ if (is.null(df)) return(div(p("Please upload a CSV to begin", style = "color:#666;font-style:italic;text-align:center;")))
4102
+ pitchers <- sort(unique(na.omit(df$Pitcher)))
4103
+ if (!length(pitchers)) return(div(p("No pitchers found", style="color:#cc6600;font-weight:bold;")))
4104
+ selectInput("tableau_pitcher_name", "Select Pitcher", choices = pitchers, selected = pitchers[1], width = "100%")
4105
 
4106
  } else if (input$report_type == "advanced_pitcher") {
4107
  df <- data_pitcher()
 
4150
 
4151
  } else if (input$report_type == "umpire") {
4152
  downloadButton("download_umpire", "Download Umpire PDF", class = "btn-primary")
4153
+
4154
+ } else if (input$report_type == "tableau_pitcher") {
4155
+ downloadButton("download_tableau_pitcher", "Download Tableau Pitcher PDF", class = "btn-primary")
4156
 
4157
  } else if (input$report_type == "bp") {
4158
  downloadButton("download_bp", "Download BP Report PDF", class = "btn-primary")
 
4468
  h4("Count Usage (Ahead/Behind)", style="color:#006F71;"),
4469
  plotOutput("preview_advanced_count", height="380px")
4470
  )
4471
+
4472
+ } else if (input$report_type == "tableau_pitcher") {
4473
+ df <- data_pitcher()
4474
+ if (is.null(df)) return(div(style = "text-align:center;padding:60px;color:#999;", h4("No data to preview")))
4475
+ req(input$tableau_pitcher_name)
4476
+ tagList(
4477
+ h4("Location Report Preview", style = "color:#006F71;"),
4478
+ plotOutput("preview_tableau_location", height = "380px"),
4479
+ br(),
4480
+ h4("Movement Profile Preview", style = "color:#006F71;"),
4481
+ plotOutput("preview_tableau_movement", height = "380px")
4482
+ )
4483
 
4484
  } else if (input$report_type == "catcher") {
4485
  df <- data_catcher()
 
4542
  create_bp_spray_chart(input$bp_player_name, df)
4543
  }, res = 96)
4544
 
4545
+ output$preview_tableau_location <- renderPlot({
4546
+ df <- data_pitcher(); req(df, input$tableau_pitcher_name)
4547
+ pitcher_df <- process_tableau_pitcher_data(df) %>% filter(Pitcher == input$tableau_pitcher_name)
4548
+ create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
4549
+ }, res = 120)
4550
+
4551
+ output$preview_tableau_movement <- renderPlot({
4552
+ df <- data_pitcher(); req(df, input$tableau_pitcher_name)
4553
+ pitcher_df <- process_tableau_pitcher_data(df) %>% filter(Pitcher == input$tableau_pitcher_name)
4554
+ create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
4555
+ }, res = 120)
4556
+
4557
  output$preview_bp_zone <- renderPlot({
4558
  df <- data_bp(); req(df, input$bp_player_name)
4559
  create_bp_zone_plot(input$bp_player_name, df)
 
4718
  },
4719
  contentType = "application/zip"
4720
  )
4721
+
4722
+ output$download_tableau_pitcher <- downloadHandler(
4723
+ filename = function() {
4724
+ df <- data_pitcher(); req(df, input$tableau_pitcher_name)
4725
+ pitcher_clean <- gsub(" ", "_", input$tableau_pitcher_name)
4726
+ date_str <- format(parse_game_day(df %>% filter(Pitcher == input$tableau_pitcher_name)), "%Y%m%d")
4727
+ paste0(pitcher_clean, "_", date_str, "_Tableau_Report.pdf")
4728
+ },
4729
+ content = function(file) {
4730
+ df <- data_pitcher(); req(df, input$tableau_pitcher_name)
4731
+ withProgress(message = 'Generating Tableau Pitcher PDF', value = 0, {
4732
+ incProgress(.5, detail = "Creating visualizations...")
4733
+ create_tableau_pitcher_pdf(df, input$tableau_pitcher_name, file)
4734
+ incProgress(.5, detail = "Finalizing...")
4735
+ })
4736
+ showNotification("Tableau Pitcher report generated!", type = "message", duration = 3)
4737
+ },
4738
+ contentType = "application/pdf"
4739
+ )
4740
 
4741
  output$download_all_coastal_hitters <- downloadHandler(
4742
  filename = function() {