Spaces:
Running
Running
Update app.R
Browse files
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)
|