igroffman commited on
Commit
19c17c7
·
verified ·
1 Parent(s): 77e76d2

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +391 -2
app.R CHANGED
@@ -127,6 +127,304 @@ process_dataset <- function(df) {
127
  df
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  parse_game_day <- function(df, tz = "America/New_York") {
131
  stopifnot("Date" %in% names(df))
132
  if (inherits(df$Date, "Date")) {
@@ -2622,7 +2920,8 @@ radioButtons("report_type", "Report Type",
2622
  "Pitcher"="pitcher",
2623
  "Advanced Pitcher"="advanced_pitcher",
2624
  "Catcher"="catcher",
2625
- "Umpire"="umpire"),
 
2626
  selected = "hitter", inline = TRUE),
2627
  hr(),
2628
  h4("Optional: Bio CSV", style = "color: #006F71; font-size: 1em;"),
@@ -2664,7 +2963,8 @@ server <- function(input, output, session) {
2664
  bio_hitter <- reactiveVal(NULL)
2665
  bio_catch <- reactiveVal(NULL)
2666
  data_umpire <- reactiveVal(NULL)
2667
-
 
2668
  observe({
2669
  df <- data_hitter()
2670
  if (!is.null(df) && input$report_type == "umpire") {
@@ -2696,6 +2996,25 @@ server <- function(input, output, session) {
2696
  data_hitter(NULL); data_catcher(NULL)
2697
  })
2698
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2699
 
2700
  observeEvent(input$bio_csv_hitter, {
2701
  req(input$bio_csv_hitter)
@@ -2746,6 +3065,14 @@ server <- function(input, output, session) {
2746
  catchers <- sort(unique(na.omit(df$Catcher)))
2747
  if (!length(catchers)) return(div(p("No catchers found in uploaded data", style="color:#cc6600;font-weight:bold;")))
2748
  selectInput("catcher_name", "Select Catcher", choices = catchers, selected = catchers[1], width = "100%")
 
 
 
 
 
 
 
 
2749
  } else if (input$report_type == "advanced_pitcher") {
2750
  df <- data_pitcher()
2751
  if (is.null(df)) return(div(p("Please upload a CSV to begin", style = "color:#666;font-style:italic;text-align:center;")))
@@ -2788,6 +3115,8 @@ server <- function(input, output, session) {
2788
  downloadButton("download_advanced_pitcher", "Download Advanced Pitcher PDF", class = "btn-primary")
2789
  } else if (input$report_type == "umpire") {
2790
  downloadButton("download_umpire", "Download Umpire PDF", class = "btn-primary")
 
 
2791
  }
2792
  })
2793
 
@@ -2866,6 +3195,25 @@ server <- function(input, output, session) {
2866
  },
2867
  contentType = "application/pdf"
2868
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2869
 
2870
  output$download_all_coastal_pitchers <- downloadHandler(
2871
  filename = function() {
@@ -2970,6 +3318,24 @@ server <- function(input, output, session) {
2970
  strong("Strikes Lost: "), receiving_stats$strikes_lost),
2971
  p(strong("Throws Recorded: "), throwing_stats$throws))
2972
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2973
  })
2974
 
2975
  output$preview_content <- renderUI({
@@ -3016,6 +3382,19 @@ server <- function(input, output, session) {
3016
  plotOutput("preview_throwing", height = "400px")
3017
  )
3018
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
3019
  })
3020
 
3021
  output$preview_plot_hitter <- renderPlot({
@@ -3045,6 +3424,16 @@ server <- function(input, output, session) {
3045
  pitch_colors <- c("Fastball"="#FA8072","Four-Seam"="#FA8072","FourSeamFastBall" = "#FA8072","Sinker"="#fdae61","Slider"="#A020F0","Sweeper"="magenta","Curveball"="#2c7bb6","ChangeUp"="#90EE90","Splitter"="#90EE32","Cutter"="red")
3046
  create_pitcher_location_plot(pitcher_df, pitch_colors)
3047
  }, res=120)
 
 
 
 
 
 
 
 
 
 
3048
 
3049
  output$preview_release <- renderPlot({
3050
  df <- data_pitcher(); req(df, input$pitcher_name)
 
127
  df
128
  }
129
 
130
+ process_bp_dataset <- function(df) {
131
+ # Process BP data with different structure
132
+ if ("Batter" %in% names(df)) {
133
+ df <- df %>% mutate(Batter = stringr::str_replace(Batter, "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
134
+ }
135
+
136
+ df <- df %>% distinct()
137
+ if ("PitchUID" %in% names(df)) df <- df %>% distinct(PitchUID, .keep_all = TRUE)
138
+
139
+ # Date processing
140
+ if ("Date" %in% names(df)) {
141
+ df$Date <- suppressWarnings(as.Date(df$Date, format = "%m/%d/%y"))
142
+ if (all(is.na(df$Date))) df$Date <- suppressWarnings(as.Date(df$Date, format = "%m/%d/%Y"))
143
+ if (all(is.na(df$Date))) df$Date <- suppressWarnings(as.Date(df$Date))
144
+ }
145
+
146
+ # Convert numeric columns
147
+ if ("PlateLocSide" %in% names(df)) df$PlateLocSide <- as.numeric(df$PlateLocSide)
148
+ if ("PlateLocHeight" %in% names(df)) df$PlateLocHeight <- as.numeric(df$PlateLocHeight)
149
+ if ("ExitSpeed" %in% names(df)) df$ExitSpeed <- as.numeric(df$ExitSpeed)
150
+ if ("Angle" %in% names(df)) df$Angle <- as.numeric(df$Angle)
151
+ if ("Distance" %in% names(df)) df$Distance <- as.numeric(df$Distance)
152
+ if ("Bearing" %in% names(df)) df$Bearing <- as.numeric(df$Bearing)
153
+ if ("ContactPositionX" %in% names(df)) df$ContactPositionX <- as.numeric(df$ContactPositionX)
154
+ if ("ContactPositionY" %in% names(df)) df$ContactPositionY <- as.numeric(df$ContactPositionY)
155
+ if ("ContactPositionZ" %in% names(df)) df$ContactPositionZ <- as.numeric(df$ContactPositionZ)
156
+
157
+ # Create BP-specific indicators
158
+ df <- df %>%
159
+ mutate(
160
+ # Filter out bad exit velo data
161
+ ExitSpeed = ifelse(!is.na(ExitSpeed) & !is.na(Angle) &
162
+ (ExitSpeed > 120 & Angle < -10 | ExitSpeed < 70), NA, ExitSpeed),
163
+
164
+ # Ball in play indicator
165
+ BIPind = ifelse(!is.na(ExitSpeed) | !is.na(Angle) | !is.na(Distance), 1, 0),
166
+
167
+ # Launch angle zones
168
+ LA1030ind = ifelse(BIPind == 1 & !is.na(Angle) & Angle >= 10 & Angle <= 30, 1, 0),
169
+
170
+ # Barrels
171
+ Barrelind = ifelse(BIPind == 1 & !is.na(ExitSpeed) & !is.na(Angle) &
172
+ ExitSpeed >= 95 & Angle >= 10 & Angle <= 32, 1, 0),
173
+
174
+ # Hard hits
175
+ HHind = ifelse(BIPind == 1 & !is.na(ExitSpeed) & ExitSpeed >= 95, 1, 0),
176
+
177
+ # Solid contact
178
+ SCind = ifelse(BIPind == 1 & !is.na(ExitSpeed) & !is.na(Angle) &
179
+ ((ExitSpeed > 95 & Angle >= 0 & Angle <= 35) |
180
+ (ExitSpeed > 92 & Angle >= 8 & Angle <= 35)), 1, 0),
181
+
182
+ # Hit type indicators
183
+ GBindicator = ifelse(BIPind == 1 & !is.na(TaggedHitType) & TaggedHitType == "GroundBall", 1, 0),
184
+ LDind = ifelse(BIPind == 1 & !is.na(TaggedHitType) & TaggedHitType == "LineDrive", 1, 0),
185
+ FBind = ifelse(BIPind == 1 & !is.na(TaggedHitType) & TaggedHitType == "FlyBall", 1, 0),
186
+ Popind = ifelse(BIPind == 1 & !is.na(TaggedHitType) & TaggedHitType == "Popup", 1, 0)
187
+ )
188
+
189
+ df
190
+ }
191
+
192
+
193
+ create_bp_spray_chart <- function(batter_name, bp_data) {
194
+ chart_data <- bp_data %>%
195
+ filter(Batter == batter_name, BIPind == 1, !is.na(Distance), !is.na(Bearing)) %>%
196
+ mutate(
197
+ Bearing2 = Bearing * pi/180,
198
+ x = Distance * sin(Bearing2),
199
+ y = Distance * cos(Bearing2)
200
+ )
201
+
202
+ if (!nrow(chart_data)) {
203
+ return(
204
+ ggplot() + theme_void() +
205
+ coord_fixed(xlim = c(-360, 360), ylim = c(-20, 410), expand = FALSE) +
206
+ annotate("segment", x = 0, y = 0, xend = 247.487, yend = 247.487, color = "gray70") +
207
+ annotate("segment", x = 0, y = 0, xend = -247.487, yend = 247.487, color = "gray70") +
208
+ ggtitle(paste("BP Spray Chart:", batter_name)) +
209
+ annotate("text", x = 0, y = 200, label = "No spray data available", size = 5, color = "gray50") +
210
+ theme(plot.title = element_text(hjust=0.5, size=12, face="bold"))
211
+ )
212
+ }
213
+
214
+ ggplot(chart_data, aes(x, y)) +
215
+ coord_fixed(xlim = c(-360, 360), ylim = c(-20, 410), expand = FALSE) +
216
+ annotate("segment", x = 0, y = 0, xend = 247.487, yend = 247.487, color = "black") +
217
+ annotate("segment", x = 0, y = 0, xend = -247.487, yend = 247.487, color = "black") +
218
+ annotate("segment", x = 63.6396, y = 63.6396, xend = 0, yend = 127.279, color = "black") +
219
+ annotate("segment", x = -63.6396, y = 63.6396, xend = 0, yend = 127.279, color = "black") +
220
+ annotate("curve", x = 89.095, y = 89.095, xend = 0, yend = 160, curvature = 0.36, linewidth = 0.5, color = "black") +
221
+ annotate("curve", x = -89.095, y = 89.095, xend = 0, yend = 160, curvature = -0.36, linewidth = 0.5, color = "black") +
222
+ annotate("curve", x = -247.487, y = 247.487, xend = 247.487, yend = 247.487, curvature = -0.65, linewidth = 0.5, color = "black") +
223
+ geom_point(aes(fill = ExitSpeed), size = 3.5, shape = 21, color = "black", stroke = 0.5, alpha = 0.85) +
224
+ scale_fill_gradient(low = "#E1463E", high = "#00840D", name = "Exit Velo", na.value = "grey50") +
225
+ theme_void() +
226
+ ggtitle(paste("BP Spray Chart:", batter_name)) +
227
+ theme(
228
+ legend.position = "right",
229
+ plot.title = element_text(hjust = 0.5, size = 12, face = "bold"),
230
+ plot.margin = margin(5, 5, 5, 5)
231
+ )
232
+ }
233
+
234
+ create_bp_zone_plot <- function(batter_name, bp_data) {
235
+ zone_data <- bp_data %>%
236
+ filter(Batter == batter_name, BIPind == 1, !is.na(PlateLocSide), !is.na(PlateLocHeight))
237
+
238
+ if (!nrow(zone_data)) {
239
+ return(
240
+ ggplot() +
241
+ annotate("rect", xmin = -0.8303, xmax = 0.8303, ymin = 1.5, ymax = 3.38,
242
+ alpha = 0, size = .5, color = "gray70") +
243
+ annotate("path",
244
+ x = c(-0.708, 0.708, 0.708, 0, -0.708, -0.708),
245
+ y = c(0.15, 0.15, 0.3, 0.5, 0.3, 0.15),
246
+ color = "gray70", linewidth = 0.5) +
247
+ coord_fixed(ratio = 1, xlim = c(-2, 2), ylim = c(0, 4.5)) +
248
+ theme_void() +
249
+ ggtitle(paste("BP Zone Plot:", batter_name)) +
250
+ annotate("text", x = 0, y = 2.5, label = "No zone data available", size = 5, color = "gray50") +
251
+ theme(plot.title = element_text(hjust = 0.5, size = 12, face = "bold"))
252
+ )
253
+ }
254
+
255
+ ggplot(zone_data, aes(x = PlateLocSide, y = PlateLocHeight)) +
256
+ geom_point(aes(fill = ExitSpeed), size = 3.5, shape = 21, color = "black", stroke = 0.5, alpha = 0.8) +
257
+ scale_fill_gradient(low = "#E1463E", high = "#00840D", name = "Exit Velo", na.value = "grey50") +
258
+ annotate("rect", xmin = -0.8303, xmax = 0.8303, ymin = 1.5, ymax = 3.38,
259
+ fill = NA, color = "black", linewidth = 1) +
260
+ annotate("path",
261
+ x = c(-0.708, 0.708, 0.708, 0, -0.708, -0.708),
262
+ y = c(0.15, 0.15, 0.3, 0.5, 0.3, 0.15),
263
+ color = "black", linewidth = 0.8) +
264
+ coord_fixed(ratio = 1, xlim = c(-2, 2), ylim = c(0, 4.5)) +
265
+ ggtitle(paste("BP Zone Plot:", batter_name)) +
266
+ theme_void() +
267
+ theme(
268
+ legend.position = "right",
269
+ plot.title = element_text(hjust = 0.5, size = 12, face = "bold"),
270
+ plot.margin = margin(5, 5, 5, 5)
271
+ )
272
+ }
273
+
274
+ create_bp_contact_map <- function(batter_name, bp_data) {
275
+ contact_data <- bp_data %>%
276
+ filter(Batter == batter_name, BIPind == 1, !is.na(ExitSpeed),
277
+ !is.na(ContactPositionZ), !is.na(ContactPositionX), !is.na(ContactPositionY)) %>%
278
+ mutate(
279
+ ContactPositionX = ContactPositionX * 12,
280
+ ContactPositionY = ContactPositionY * 12,
281
+ ContactPositionZ = ContactPositionZ * 12
282
+ )
283
+
284
+ if (!nrow(contact_data)) {
285
+ return(
286
+ ggplot() +
287
+ annotate("segment", x = -8.5, y = 17, xend = 8.5, yend = 17, color = "gray70", linewidth = 0.5) +
288
+ annotate("segment", x = 8.5, y = 8.5, xend = 8.5, yend = 17, color = "gray70", linewidth = 0.5) +
289
+ annotate("segment", x = -8.5, y = 8.5, xend = -8.5, yend = 17, color = "gray70", linewidth = 0.5) +
290
+ annotate("rect", xmin = 20, xmax = 48, ymin = -20, ymax = 40, fill = NA, color = "gray70", linewidth = 0.5) +
291
+ annotate("rect", xmin = -48, xmax = -20, ymin = -20, ymax = 40, fill = NA, color = "gray70", linewidth = 0.5) +
292
+ xlim(-50, 50) + ylim(-20, 50) +
293
+ coord_fixed() +
294
+ theme_void() +
295
+ ggtitle(paste("BP Contact Points:", batter_name)) +
296
+ annotate("text", x = 0, y = 20, label = "No contact data available", size = 5, color = "gray50") +
297
+ theme(plot.title = element_text(hjust = 0.5, size = 12, face = "bold"))
298
+ )
299
+ }
300
+
301
+ batter_side <- unique(contact_data$BatterSide)[1]
302
+ if (is.na(batter_side)) batter_side <- "Right"
303
+
304
+ ggplot(contact_data, aes(x = ContactPositionZ, y = ContactPositionX)) +
305
+ annotate("segment", x = -8.5, y = 17, xend = 8.5, yend = 17, color = "black", linewidth = 0.5) +
306
+ annotate("segment", x = 8.5, y = 8.5, xend = 8.5, yend = 17, color = "black", linewidth = 0.5) +
307
+ annotate("segment", x = -8.5, y = 8.5, xend = -8.5, yend = 17, color = "black", linewidth = 0.5) +
308
+ annotate("segment", x = -8.5, y = 8.5, xend = 0, yend = 0, color = "black", linewidth = 0.5) +
309
+ annotate("segment", x = 8.5, y = 8.5, xend = 0, yend = 0, color = "black", linewidth = 0.5) +
310
+ annotate("rect", xmin = 20, xmax = 48, ymin = -20, ymax = 40, fill = NA, color = "black", linewidth = 0.5) +
311
+ annotate("rect", xmin = -48, xmax = -20, ymin = -20, ymax = 40, fill = NA, color = "black", linewidth = 0.5) +
312
+ annotate("text", x = ifelse(batter_side == "Right", -34, 34), y = 10,
313
+ label = ifelse(batter_side == "Right", "R", "L"), size = 8, fontface = "bold") +
314
+ xlim(-50, 50) + ylim(-20, 50) +
315
+ geom_point(aes(fill = ExitSpeed), color = "black", stroke = 0.5, shape = 21, alpha = 0.85, size = 3) +
316
+ geom_smooth(aes(color = "Optimal Contact"), method = "lm", level = 0, se = FALSE, linewidth = 0.8) +
317
+ scale_fill_gradient(name = "Exit Velo", low = "#E1463E", high = "#00840D") +
318
+ scale_color_manual(name = NULL, values = c("Optimal Contact" = "black")) +
319
+ coord_fixed() +
320
+ ggtitle(paste("BP Contact Points:", batter_name)) +
321
+ theme_void() +
322
+ theme(
323
+ legend.position = "right",
324
+ plot.title = element_text(hjust = 0.5, size = 12, face = "bold"),
325
+ plot.margin = margin(5, 5, 5, 5)
326
+ )
327
+ }
328
+
329
+ create_bp_pdf <- function(bp_data, batter_name, output_file) {
330
+ if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
331
+
332
+ batter_df <- filter(bp_data, Batter == batter_name)
333
+
334
+ # Calculate stats in the order: BBE, Avg EV, Avg LA, Max EV, SC%, 10-30%, HH%, Barrel%
335
+ stats <- batter_df %>%
336
+ summarise(
337
+ BBE = sum(BIPind, na.rm = TRUE),
338
+ `Avg EV` = round(mean(ExitSpeed[BIPind == 1], na.rm = TRUE), 1),
339
+ `Avg LA` = round(mean(Angle[BIPind == 1], na.rm = TRUE), 1),
340
+ `Max EV` = round(max(ExitSpeed[BIPind == 1], na.rm = TRUE), 1),
341
+ `SC%` = round(sum(SCind, na.rm = TRUE) / sum(BIPind, na.rm = TRUE) * 100, 1),
342
+ `10-30%` = round(sum(LA1030ind, na.rm = TRUE) / sum(BIPind, na.rm = TRUE) * 100, 1),
343
+ `HH%` = round(sum(HHind, na.rm = TRUE) / sum(BIPind, na.rm = TRUE) * 100, 1),
344
+ `Barrel%` = round(sum(Barrelind, na.rm = TRUE) / sum(BIPind, na.rm = TRUE) * 100, 1),
345
+ .groups = "drop"
346
+ )
347
+
348
+ # Create plots
349
+ spray_plot <- create_bp_spray_chart(batter_name, bp_data)
350
+ zone_plot <- create_bp_zone_plot(batter_name, bp_data)
351
+ contact_plot <- create_bp_contact_map(batter_name, bp_data)
352
+
353
+ # Create PDF
354
+ pdf(output_file, width = 11, height = 8.5)
355
+ on.exit(try(dev.off(), silent = TRUE), add = TRUE)
356
+
357
+ grid::grid.newpage()
358
+
359
+ # Title section
360
+ grid::pushViewport(grid::viewport(x = 0.5, y = 0.97, width = 1, height = 0.06, just = c("center", "top")))
361
+ grid::grid.text("BP Report",
362
+ gp = grid::gpar(fontface = "bold", cex = 1.5, col = "#006F71"))
363
+ grid::popViewport()
364
+
365
+ grid::pushViewport(grid::viewport(x = 0.5, y = 0.92, width = 1, height = 0.05, just = c("center", "top")))
366
+ grid::grid.text(batter_name,
367
+ gp = grid::gpar(fontface = "bold", cex = 1.8, col = "black"))
368
+ grid::popViewport()
369
+
370
+ # Stats table with teal header
371
+ headers <- c("BBE", "Avg EV", "Avg LA", "Max EV", "SC%", "10-30%", "HH%", "Barrel%")
372
+ values <- c(stats$BBE, stats$`Avg EV`, stats$`Avg LA`, stats$`Max EV`,
373
+ stats$`SC%`, stats$`10-30%`, stats$`HH%`, stats$`Barrel%`)
374
+
375
+ col_w <- 0.09
376
+ x0 <- 0.5 - (length(headers) * col_w) / 2
377
+ yh <- 0.84
378
+ yv <- 0.82
379
+
380
+ for (i in seq_along(headers)) {
381
+ xi <- x0 + (i - 1) * col_w
382
+
383
+ # Header with teal background
384
+ grid::grid.rect(x = xi, y = yh, width = col_w * 0.985, height = 0.018,
385
+ just = c("left", "top"),
386
+ gp = grid::gpar(fill = "#006F71", col = "black", lwd = 0.5))
387
+ grid::grid.text(headers[i],
388
+ x = xi + col_w * 0.49, y = yh - 0.009,
389
+ gp = grid::gpar(col = "white", cex = 0.70, fontface = "bold"))
390
+
391
+ # Value cell with color coding based on performance
392
+ val <- values[i]
393
+ cell_color <- if (headers[i] %in% c("Avg EV", "Max EV", "SC%", "10-30%", "HH%", "Barrel%")) {
394
+ if (is.na(val)) "#FFFFFF"
395
+ else if (headers[i] == "Avg EV" && val >= 90) "#00840D"
396
+ else if (headers[i] == "Max EV" && val >= 100) "#00840D"
397
+ else if (headers[i] %in% c("SC%", "10-30%", "HH%", "Barrel%") && val >= 50) "#00840D"
398
+ else if (val >= 40) "lightgreen"
399
+ else "#FFFFFF"
400
+ } else "#FFFFFF"
401
+
402
+ grid::grid.rect(x = xi, y = yv, width = col_w * 0.985, height = 0.018,
403
+ just = c("left", "top"),
404
+ gp = grid::gpar(fill = cell_color, col = "black", lwd = 0.4))
405
+ grid::grid.text(ifelse(is.finite(val), as.character(val), "-"),
406
+ x = xi + col_w * 0.49, y = yv - 0.009,
407
+ gp = grid::gpar(cex = 0.70))
408
+ }
409
+
410
+ # Spray chart (left)
411
+ grid::pushViewport(grid::viewport(x = 0.25, y = 0.72, width = 0.45, height = 0.50, just = c("center", "top")))
412
+ print(spray_plot, newpage = FALSE)
413
+ grid::popViewport()
414
+
415
+ # Zone plot (right)
416
+ grid::pushViewport(grid::viewport(x = 0.75, y = 0.72, width = 0.45, height = 0.50, just = c("center", "top")))
417
+ print(zone_plot, newpage = FALSE)
418
+ grid::popViewport()
419
+
420
+ # Contact map (bottom center)
421
+ grid::pushViewport(grid::viewport(x = 0.5, y = 0.20, width = 0.50, height = 0.24, just = c("center", "top")))
422
+ print(contact_plot, newpage = FALSE)
423
+ grid::popViewport()
424
+
425
+ invisible(output_file)
426
+ }
427
+
428
  parse_game_day <- function(df, tz = "America/New_York") {
429
  stopifnot("Date" %in% names(df))
430
  if (inherits(df$Date, "Date")) {
 
2920
  "Pitcher"="pitcher",
2921
  "Advanced Pitcher"="advanced_pitcher",
2922
  "Catcher"="catcher",
2923
+ "Umpire"="umpire",
2924
+ "BP Report"="bp"),
2925
  selected = "hitter", inline = TRUE),
2926
  hr(),
2927
  h4("Optional: Bio CSV", style = "color: #006F71; font-size: 1em;"),
 
2963
  bio_hitter <- reactiveVal(NULL)
2964
  bio_catch <- reactiveVal(NULL)
2965
  data_umpire <- reactiveVal(NULL)
2966
+ data_bp <- reactiveVal(NULL)
2967
+
2968
  observe({
2969
  df <- data_hitter()
2970
  if (!is.null(df) && input$report_type == "umpire") {
 
2996
  data_hitter(NULL); data_catcher(NULL)
2997
  })
2998
  })
2999
+
3000
+ observeEvent(input$game_csv, {
3001
+ req(input$game_csv)
3002
+ tryCatch({
3003
+ df <- read.csv(input$game_csv$datapath, stringsAsFactors = FALSE)
3004
+
3005
+ # Existing code for hitter/pitcher/catcher/umpire...
3006
+ data_hitter(process_dataset(df))
3007
+ data_catcher(catcher_process_dataset(df))
3008
+
3009
+ # ADD THIS NEW LINE:
3010
+ data_bp(process_bp_dataset(df))
3011
+
3012
+ showNotification("Game data loaded successfully!", type = "message", duration = 3)
3013
+ }, error = function(e) {
3014
+ showNotification(paste("Error loading CSV:", e$message), type = "error", duration = 6)
3015
+ data_hitter(NULL); data_catcher(NULL); data_bp(NULL)
3016
+ })
3017
+ })
3018
 
3019
  observeEvent(input$bio_csv_hitter, {
3020
  req(input$bio_csv_hitter)
 
3065
  catchers <- sort(unique(na.omit(df$Catcher)))
3066
  if (!length(catchers)) return(div(p("No catchers found in uploaded data", style="color:#cc6600;font-weight:bold;")))
3067
  selectInput("catcher_name", "Select Catcher", choices = catchers, selected = catchers[1], width = "100%")
3068
+ } else if (input$report_type == "bp") {
3069
+ df <- data_bp()
3070
+ if (is.null(df)) return(div(p("Please upload a BP CSV to begin",
3071
+ style = "color:#666;font-style:italic;text-align:center;")))
3072
+ players <- sort(unique(na.omit(df$Batter)))
3073
+ if (!length(players)) return(div(p("No players found in BP data",
3074
+ style="color:#cc6600;font-weight:bold;")))
3075
+ selectInput("bp_player_name", "Select Player", choices = players, selected = players[1], width = "100%")
3076
  } else if (input$report_type == "advanced_pitcher") {
3077
  df <- data_pitcher()
3078
  if (is.null(df)) return(div(p("Please upload a CSV to begin", style = "color:#666;font-style:italic;text-align:center;")))
 
3115
  downloadButton("download_advanced_pitcher", "Download Advanced Pitcher PDF", class = "btn-primary")
3116
  } else if (input$report_type == "umpire") {
3117
  downloadButton("download_umpire", "Download Umpire PDF", class = "btn-primary")
3118
+ } else if (input$report_type == "bp") {
3119
+ downloadButton("download_bp", "Download BP Report PDF", class = "btn-primary")
3120
  }
3121
  })
3122
 
 
3195
  },
3196
  contentType = "application/pdf"
3197
  )
3198
+
3199
+ output$download_bp <- downloadHandler(
3200
+ filename = function() {
3201
+ df <- data_bp(); req(df, input$bp_player_name)
3202
+ player_clean <- gsub(" ", "_", input$bp_player_name)
3203
+ paste0(player_clean, "_BP_Report.pdf")
3204
+ },
3205
+ content = function(file) {
3206
+ df <- data_bp(); req(df, input$bp_player_name)
3207
+ withProgress(message='Generating BP Report PDF', value=0, {
3208
+ incProgress(.3, detail="Processing data...")
3209
+ incProgress(.4, detail="Creating visualizations...")
3210
+ create_bp_pdf(df, input$bp_player_name, file)
3211
+ incProgress(.3, detail="Finalizing report...")
3212
+ })
3213
+ showNotification("BP Report generated!", type="message", duration=3)
3214
+ },
3215
+ contentType = "application/pdf"
3216
+ )
3217
 
3218
  output$download_all_coastal_pitchers <- downloadHandler(
3219
  filename = function() {
 
3318
  strong("Strikes Lost: "), receiving_stats$strikes_lost),
3319
  p(strong("Throws Recorded: "), throwing_stats$throws))
3320
  }
3321
+ } else if (input$report_type == "bp") {
3322
+ df <- data_bp(); req(df, input$bp_player_name)
3323
+ player_df <- df %>% filter(Batter == input$bp_player_name)
3324
+ if (!nrow(player_df)) return(NULL)
3325
+
3326
+ stats <- player_df %>%
3327
+ summarise(
3328
+ bbe = sum(BIPind, na.rm = TRUE),
3329
+ avg_ev = round(mean(ExitSpeed[BIPind == 1], na.rm = TRUE), 1),
3330
+ max_ev = round(max(ExitSpeed[BIPind == 1], na.rm = TRUE), 1)
3331
+ )
3332
+
3333
+ div(class = "status-box",
3334
+ h4("✓ Ready to Generate BP Report", style = "margin-top: 0; color: #006F71;"),
3335
+ p(strong("Player: "), input$bp_player_name),
3336
+ p(strong("Batted Ball Events: "), stats$bbe),
3337
+ p(strong("Avg Exit Velo: "), stats$avg_ev, " mph"),
3338
+ p(strong("Max Exit Velo: "), stats$max_ev, " mph"))
3339
  })
3340
 
3341
  output$preview_content <- renderUI({
 
3382
  plotOutput("preview_throwing", height = "400px")
3383
  )
3384
  }
3385
+ } else if (input$report_type == "bp") {
3386
+ df <- data_bp()
3387
+ if (is.null(df)) return(div(style = "text-align:center;padding:60px;color:#999;",
3388
+ h4("No data to preview")))
3389
+ req(input$bp_player_name)
3390
+
3391
+ tagList(
3392
+ h4("BP Spray Chart", style = "color: #006F71;"),
3393
+ plotOutput("preview_bp_spray", height = "400px"),
3394
+ br(),
3395
+ h4("BP Zone Plot", style = "color: #006F71;"),
3396
+ plotOutput("preview_bp_zone", height = "400px")
3397
+ )
3398
  })
3399
 
3400
  output$preview_plot_hitter <- renderPlot({
 
3424
  pitch_colors <- c("Fastball"="#FA8072","Four-Seam"="#FA8072","FourSeamFastBall" = "#FA8072","Sinker"="#fdae61","Slider"="#A020F0","Sweeper"="magenta","Curveball"="#2c7bb6","ChangeUp"="#90EE90","Splitter"="#90EE32","Cutter"="red")
3425
  create_pitcher_location_plot(pitcher_df, pitch_colors)
3426
  }, res=120)
3427
+
3428
+ output$preview_bp_spray <- renderPlot({
3429
+ df <- data_bp(); req(df, input$bp_player_name)
3430
+ create_bp_spray_chart(input$bp_player_name, df)
3431
+ }, res = 96)
3432
+
3433
+ output$preview_bp_zone <- renderPlot({
3434
+ df <- data_bp(); req(df, input$bp_player_name)
3435
+ create_bp_zone_plot(input$bp_player_name, df)
3436
+ }, res = 96)
3437
 
3438
  output$preview_release <- renderPlot({
3439
  df <- data_pitcher(); req(df, input$pitcher_name)