Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -2,6 +2,7 @@ library(shiny)
|
|
| 2 |
library(dplyr)
|
| 3 |
library(arrow)
|
| 4 |
library(DT)
|
|
|
|
| 5 |
|
| 6 |
# ββ Load data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
load_data <- function(level) {
|
|
@@ -9,6 +10,29 @@ load_data <- function(level) {
|
|
| 9 |
arrow::read_parquet(path)
|
| 10 |
}
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
build_pitcher_stats <- function(df, min_pitches) {
|
| 13 |
|
| 14 |
format_name <- function(name) {
|
|
@@ -16,7 +40,6 @@ build_pitcher_stats <- function(df, min_pitches) {
|
|
| 16 |
if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else name
|
| 17 |
}
|
| 18 |
|
| 19 |
-
# grab team lookup separately before any grouping
|
| 20 |
team_lookup <- df %>%
|
| 21 |
select(Pitcher, PitcherTeam) %>%
|
| 22 |
distinct(Pitcher, .keep_all = TRUE)
|
|
@@ -88,7 +111,9 @@ build_pitcher_stats <- function(df, min_pitches) {
|
|
| 88 |
`Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
|
| 89 |
`FB Velo` = ifelse(fb_velo_n == 0, NA_real_,
|
| 90 |
round(fb_velo_sum / fb_velo_n, 1)),
|
| 91 |
-
`Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1)
|
|
|
|
|
|
|
| 92 |
) %>%
|
| 93 |
filter(
|
| 94 |
!is.na(Name),
|
|
@@ -96,11 +121,11 @@ build_pitcher_stats <- function(df, min_pitches) {
|
|
| 96 |
grepl(" ", trimws(Name))
|
| 97 |
) %>%
|
| 98 |
select(Name, Level, Team, Pitches, `FB Velo`, `Zone%`, `Strike%`,
|
| 99 |
-
`K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`) %>%
|
| 100 |
arrange(desc(Pitches))
|
| 101 |
}
|
| 102 |
|
| 103 |
-
#
|
| 104 |
ui <- fluidPage(
|
| 105 |
tags$head(
|
| 106 |
tags$style(HTML("
|
|
@@ -120,18 +145,17 @@ ui <- fluidPage(
|
|
| 120 |
.main { padding:20px 24px; }
|
| 121 |
.form-control { border:1px solid #d0d0d0; border-radius:4px; }
|
| 122 |
|
| 123 |
-
/* table */
|
| 124 |
.dataTables_wrapper { background:white; border-radius:8px;
|
| 125 |
padding:16px; box-shadow:0 1px 4px rgba(0,0,0,0.08); }
|
| 126 |
.dataTables_filter input { border:1px solid #d0d0d0; border-radius:4px;
|
| 127 |
padding:4px 8px; }
|
| 128 |
.dataTables_length select { border:1px solid #d0d0d0; border-radius:4px; }
|
| 129 |
table.dataTable thead th {
|
| 130 |
-
background:#002147; color:white; font-weight:600;
|
| 131 |
font-size:12px; text-transform:uppercase; letter-spacing:0.3px;
|
| 132 |
border-bottom:none !important; cursor:pointer;
|
| 133 |
}
|
| 134 |
-
table.dataTable thead th:hover { background:#003366; }
|
| 135 |
table.dataTable tbody tr { background:white; }
|
| 136 |
table.dataTable tbody tr:nth-child(even) { background:#f7f9fc; }
|
| 137 |
table.dataTable tbody tr:hover { background:#eef2f7 !important; }
|
|
@@ -139,7 +163,25 @@ ui <- fluidPage(
|
|
| 139 |
border-bottom:1px solid #f0f0f0; }
|
| 140 |
table.dataTable tbody td:first-child { font-weight:600; color:#002147; }
|
| 141 |
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
#splash {
|
| 144 |
position:fixed; top:0; left:0; width:100%; height:100%;
|
| 145 |
background:#002147; display:flex; flex-direction:column;
|
|
@@ -177,10 +219,23 @@ ui <- fluidPage(
|
|
| 177 |
|
| 178 |
tags$script(HTML("
|
| 179 |
$(document).ready(function() {
|
|
|
|
|
|
|
| 180 |
setTimeout(function() {
|
| 181 |
$('#splash').addClass('fade-out');
|
| 182 |
setTimeout(function() { $('#splash').remove(); }, 800);
|
| 183 |
}, 2800);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
});
|
| 185 |
"))
|
| 186 |
),
|
|
@@ -188,7 +243,7 @@ ui <- fluidPage(
|
|
| 188 |
# splash
|
| 189 |
tags$div(id="splash",
|
| 190 |
tags$img(src="Butler-Bulldogs-Logo-1990.png"),
|
| 191 |
-
tags$h1("Butler Baseball
|
| 192 |
tags$p("Butler Baseball Analytics"),
|
| 193 |
tags$div(class="bar-track", tags$div(class="bar-fill"))
|
| 194 |
),
|
|
@@ -228,44 +283,56 @@ ui <- fluidPage(
|
|
| 228 |
|
| 229 |
# main
|
| 230 |
tags$div(class="main",
|
| 231 |
-
uiOutput("main_ui")
|
|
|
|
| 232 |
)
|
| 233 |
)
|
| 234 |
|
| 235 |
# ββ Server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 236 |
server <- function(input, output, session) {
|
| 237 |
|
| 238 |
-
# load data reactively when level changes
|
| 239 |
raw_data <- reactive({
|
| 240 |
req(input$level)
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
})
|
| 243 |
|
| 244 |
-
# build stats table
|
| 245 |
stats_table <- reactive({
|
| 246 |
req(raw_data(), input$min_pitches)
|
| 247 |
if (input$position == "Pitcher") {
|
| 248 |
build_pitcher_stats(raw_data(), input$min_pitches)
|
| 249 |
}
|
| 250 |
-
# hitter branch coming soon
|
| 251 |
})
|
| 252 |
|
| 253 |
output$main_ui <- renderUI({
|
| 254 |
if (input$position == "Pitcher") {
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
} else {
|
| 257 |
tags$p("Hitter view coming soon.", style="color:#888;")
|
| 258 |
}
|
| 259 |
})
|
| 260 |
|
| 261 |
-
output$pitcher_table <- renderDT({
|
|
|
|
| 262 |
req(stats_table())
|
|
|
|
|
|
|
| 263 |
dt <- stats_table()
|
| 264 |
|
| 265 |
-
# ββ Color ranges (edit these when ready) ββββββββββββββββββββββββββββββββββ
|
| 266 |
# zone_pct_range <- c(42, 53)
|
| 267 |
# strike_pct_range <- c(58, 67)
|
| 268 |
-
#
|
|
|
|
| 269 |
# whiff_range <- c(15, 35)
|
| 270 |
# chase_range <- c(25, 40)
|
| 271 |
# hardhit_range <- c(25, 42)
|
|
@@ -274,20 +341,148 @@ output$pitcher_table <- renderDT({
|
|
| 274 |
datatable(
|
| 275 |
dt,
|
| 276 |
rownames = FALSE,
|
| 277 |
-
|
|
|
|
| 278 |
options = list(
|
| 279 |
-
pageLength
|
| 280 |
-
lengthMenu
|
| 281 |
-
scrollX
|
| 282 |
-
order
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
list(className="dt-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
)
|
| 289 |
)
|
| 290 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
shinyApp(ui, server)
|
|
|
|
| 2 |
library(dplyr)
|
| 3 |
library(arrow)
|
| 4 |
library(DT)
|
| 5 |
+
library(ggplot2)
|
| 6 |
|
| 7 |
# ββ Load data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
load_data <- function(level) {
|
|
|
|
| 10 |
arrow::read_parquet(path)
|
| 11 |
}
|
| 12 |
|
| 13 |
+
# ββ Pitch colors βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
pitch_colors <- c(
|
| 15 |
+
"Fastball" = "red", "Sinker" = "orange",
|
| 16 |
+
"Slider" = "gold", "Sweeper" = "pink",
|
| 17 |
+
"Curveball" = "blue", "Changeup" = "green3",
|
| 18 |
+
"Cutter" = "#8B4513", "Splitter" = "mediumpurple3"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
clean_pitch_type <- function(pt) {
|
| 22 |
+
dplyr::case_when(
|
| 23 |
+
pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
|
| 24 |
+
pt %in% c("Sinker","TwoSeamFastBall","OneSeamFastball") ~ "Sinker",
|
| 25 |
+
pt == "Cutter" ~ "Cutter",
|
| 26 |
+
pt %in% c("Curveball","CurveBall") ~ "Curveball",
|
| 27 |
+
pt == "Slider" ~ "Slider",
|
| 28 |
+
pt == "Sweeper" ~ "Sweeper",
|
| 29 |
+
pt %in% c("ChangeUp","Changeup") ~ "Changeup",
|
| 30 |
+
pt == "Splitter" ~ "Splitter",
|
| 31 |
+
TRUE ~ NA_character_
|
| 32 |
+
)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
# ββ Pitcher stats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
build_pitcher_stats <- function(df, min_pitches) {
|
| 37 |
|
| 38 |
format_name <- function(name) {
|
|
|
|
| 40 |
if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else name
|
| 41 |
}
|
| 42 |
|
|
|
|
| 43 |
team_lookup <- df %>%
|
| 44 |
select(Pitcher, PitcherTeam) %>%
|
| 45 |
distinct(Pitcher, .keep_all = TRUE)
|
|
|
|
| 111 |
`Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
|
| 112 |
`FB Velo` = ifelse(fb_velo_n == 0, NA_real_,
|
| 113 |
round(fb_velo_sum / fb_velo_n, 1)),
|
| 114 |
+
`Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
|
| 115 |
+
`Plot` = paste0('<button class="plot-btn" data-pitcher="',
|
| 116 |
+
Pitcher, '">π View</button>')
|
| 117 |
) %>%
|
| 118 |
filter(
|
| 119 |
!is.na(Name),
|
|
|
|
| 121 |
grepl(" ", trimws(Name))
|
| 122 |
) %>%
|
| 123 |
select(Name, Level, Team, Pitches, `FB Velo`, `Zone%`, `Strike%`,
|
| 124 |
+
`K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`, `Plot`) %>%
|
| 125 |
arrange(desc(Pitches))
|
| 126 |
}
|
| 127 |
|
| 128 |
+
# ββ UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 129 |
ui <- fluidPage(
|
| 130 |
tags$head(
|
| 131 |
tags$style(HTML("
|
|
|
|
| 145 |
.main { padding:20px 24px; }
|
| 146 |
.form-control { border:1px solid #d0d0d0; border-radius:4px; }
|
| 147 |
|
|
|
|
| 148 |
.dataTables_wrapper { background:white; border-radius:8px;
|
| 149 |
padding:16px; box-shadow:0 1px 4px rgba(0,0,0,0.08); }
|
| 150 |
.dataTables_filter input { border:1px solid #d0d0d0; border-radius:4px;
|
| 151 |
padding:4px 8px; }
|
| 152 |
.dataTables_length select { border:1px solid #d0d0d0; border-radius:4px; }
|
| 153 |
table.dataTable thead th {
|
| 154 |
+
background:#002147 !important; color:white !important; font-weight:600;
|
| 155 |
font-size:12px; text-transform:uppercase; letter-spacing:0.3px;
|
| 156 |
border-bottom:none !important; cursor:pointer;
|
| 157 |
}
|
| 158 |
+
table.dataTable thead th:hover { background:#003366 !important; }
|
| 159 |
table.dataTable tbody tr { background:white; }
|
| 160 |
table.dataTable tbody tr:nth-child(even) { background:#f7f9fc; }
|
| 161 |
table.dataTable tbody tr:hover { background:#eef2f7 !important; }
|
|
|
|
| 163 |
border-bottom:1px solid #f0f0f0; }
|
| 164 |
table.dataTable tbody td:first-child { font-weight:600; color:#002147; }
|
| 165 |
|
| 166 |
+
.plot-btn {
|
| 167 |
+
background:#002147; color:white; border:none; border-radius:4px;
|
| 168 |
+
padding:4px 10px; font-size:12px; cursor:pointer; font-weight:600;
|
| 169 |
+
}
|
| 170 |
+
.plot-btn:hover { background:#003366; }
|
| 171 |
+
|
| 172 |
+
/* modal */
|
| 173 |
+
.modal-backdrop { position:fixed; top:0; left:0; width:100%; height:100%;
|
| 174 |
+
background:rgba(0,0,0,0.5); z-index:1000;
|
| 175 |
+
display:flex; align-items:center; justify-content:center; }
|
| 176 |
+
.modal-box { background:white; border-radius:10px; padding:24px;
|
| 177 |
+
width:700px; max-width:95vw; position:relative;
|
| 178 |
+
box-shadow:0 4px 20px rgba(0,0,0,0.3);
|
| 179 |
+
max-height:90vh; overflow-y:auto; }
|
| 180 |
+
.modal-close { position:absolute; top:12px; right:16px; font-size:20px;
|
| 181 |
+
cursor:pointer; color:#555; background:none; border:none; }
|
| 182 |
+
.modal-title { font-size:18px; font-weight:700; color:#002147;
|
| 183 |
+
margin:0 0 16px 0; }
|
| 184 |
+
|
| 185 |
#splash {
|
| 186 |
position:fixed; top:0; left:0; width:100%; height:100%;
|
| 187 |
background:#002147; display:flex; flex-direction:column;
|
|
|
|
| 219 |
|
| 220 |
tags$script(HTML("
|
| 221 |
$(document).ready(function() {
|
| 222 |
+
|
| 223 |
+
// splash
|
| 224 |
setTimeout(function() {
|
| 225 |
$('#splash').addClass('fade-out');
|
| 226 |
setTimeout(function() { $('#splash').remove(); }, 800);
|
| 227 |
}, 2800);
|
| 228 |
+
|
| 229 |
+
// plot button click β delegate since table renders after doc ready
|
| 230 |
+
$(document).on('click', '.plot-btn', function() {
|
| 231 |
+
var pitcher = $(this).data('pitcher');
|
| 232 |
+
Shiny.setInputValue('selected_pitcher', pitcher, {priority: 'event'});
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
// close modal
|
| 236 |
+
$(document).on('click', '#modal_close', function() {
|
| 237 |
+
$('#pitcher_modal').remove();
|
| 238 |
+
});
|
| 239 |
});
|
| 240 |
"))
|
| 241 |
),
|
|
|
|
| 243 |
# splash
|
| 244 |
tags$div(id="splash",
|
| 245 |
tags$img(src="Butler-Bulldogs-Logo-1990.png"),
|
| 246 |
+
tags$h1("Butler Baseball Transfer Portal"),
|
| 247 |
tags$p("Butler Baseball Analytics"),
|
| 248 |
tags$div(class="bar-track", tags$div(class="bar-fill"))
|
| 249 |
),
|
|
|
|
| 283 |
|
| 284 |
# main
|
| 285 |
tags$div(class="main",
|
| 286 |
+
uiOutput("main_ui"),
|
| 287 |
+
uiOutput("pitcher_modal")
|
| 288 |
)
|
| 289 |
)
|
| 290 |
|
| 291 |
# ββ Server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 292 |
server <- function(input, output, session) {
|
| 293 |
|
|
|
|
| 294 |
raw_data <- reactive({
|
| 295 |
req(input$level)
|
| 296 |
+
tryCatch(
|
| 297 |
+
load_data(input$level),
|
| 298 |
+
error = function(e) {
|
| 299 |
+
showNotification(paste("Failed to load data:", e$message), type="error")
|
| 300 |
+
NULL
|
| 301 |
+
}
|
| 302 |
+
)
|
| 303 |
})
|
| 304 |
|
|
|
|
| 305 |
stats_table <- reactive({
|
| 306 |
req(raw_data(), input$min_pitches)
|
| 307 |
if (input$position == "Pitcher") {
|
| 308 |
build_pitcher_stats(raw_data(), input$min_pitches)
|
| 309 |
}
|
|
|
|
| 310 |
})
|
| 311 |
|
| 312 |
output$main_ui <- renderUI({
|
| 313 |
if (input$position == "Pitcher") {
|
| 314 |
+
tagList(
|
| 315 |
+
tags$p(id="loading_msg", "Loading data...",
|
| 316 |
+
style="color:#888; font-size:14px; padding:10px 0;"),
|
| 317 |
+
DTOutput("pitcher_table")
|
| 318 |
+
)
|
| 319 |
} else {
|
| 320 |
tags$p("Hitter view coming soon.", style="color:#888;")
|
| 321 |
}
|
| 322 |
})
|
| 323 |
|
| 324 |
+
output$pitcher_table <- renderDT({
|
| 325 |
+
req(raw_data())
|
| 326 |
req(stats_table())
|
| 327 |
+
req(nrow(stats_table()) > 0)
|
| 328 |
+
|
| 329 |
dt <- stats_table()
|
| 330 |
|
| 331 |
+
# ββ Color ranges (edit these when ready) ββββββββββββββββββββββββββββββββββ
|
| 332 |
# zone_pct_range <- c(42, 53)
|
| 333 |
# strike_pct_range <- c(58, 67)
|
| 334 |
+
# fb_velo_range <- c(88, 95)
|
| 335 |
+
# kb_range <- c(1.5, 4.0)
|
| 336 |
# whiff_range <- c(15, 35)
|
| 337 |
# chase_range <- c(25, 40)
|
| 338 |
# hardhit_range <- c(25, 42)
|
|
|
|
| 341 |
datatable(
|
| 342 |
dt,
|
| 343 |
rownames = FALSE,
|
| 344 |
+
escape = FALSE,
|
| 345 |
+
selection = "none",
|
| 346 |
options = list(
|
| 347 |
+
pageLength = 25,
|
| 348 |
+
lengthMenu = c(25, 50, 100),
|
| 349 |
+
scrollX = TRUE,
|
| 350 |
+
order = list(list(3, "desc")),
|
| 351 |
+
orderMulti = FALSE,
|
| 352 |
+
dom = "lfrtip",
|
| 353 |
+
columnDefs = list(
|
| 354 |
+
list(className="dt-center", targets=2:13),
|
| 355 |
+
list(className="dt-left", targets=0:1),
|
| 356 |
+
list(orderable=FALSE, targets=13)
|
| 357 |
+
),
|
| 358 |
+
initComplete = JS("function(settings, json) {
|
| 359 |
+
$('#loading_msg').hide();
|
| 360 |
+
}")
|
| 361 |
)
|
| 362 |
)
|
| 363 |
})
|
| 364 |
+
|
| 365 |
+
# movement plot modal
|
| 366 |
+
observeEvent(input$selected_pitcher, {
|
| 367 |
+
req(input$selected_pitcher, raw_data())
|
| 368 |
+
|
| 369 |
+
pitcher_raw <- input$selected_pitcher
|
| 370 |
+
|
| 371 |
+
pdata <- raw_data() %>%
|
| 372 |
+
filter(Pitcher == pitcher_raw) %>%
|
| 373 |
+
mutate(PitchType = clean_pitch_type(TaggedPitchType)) %>%
|
| 374 |
+
filter(!is.na(PitchType), !is.na(HorzBreak), !is.na(InducedVertBreak))
|
| 375 |
+
|
| 376 |
+
req(nrow(pdata) > 0)
|
| 377 |
+
|
| 378 |
+
# format display name
|
| 379 |
+
parts <- strsplit(pitcher_raw, ", ")[[1]]
|
| 380 |
+
display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else pitcher_raw
|
| 381 |
+
|
| 382 |
+
avg_data <- pdata %>%
|
| 383 |
+
group_by(PitchType) %>%
|
| 384 |
+
summarise(
|
| 385 |
+
HB = mean(HorzBreak, na.rm=TRUE),
|
| 386 |
+
iVB = mean(InducedVertBreak,na.rm=TRUE),
|
| 387 |
+
Velo = round(mean(RelSpeed, na.rm=TRUE),1),
|
| 388 |
+
.groups="drop"
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
p <- ggplot() +
|
| 392 |
+
geom_vline(xintercept=0, color="black") +
|
| 393 |
+
geom_hline(yintercept=0, color="black") +
|
| 394 |
+
geom_point(data=pdata,
|
| 395 |
+
aes(x=HorzBreak, y=InducedVertBreak, fill=PitchType),
|
| 396 |
+
size=3, alpha=0.6, shape=21, color="black", stroke=0.4) +
|
| 397 |
+
geom_point(data=avg_data,
|
| 398 |
+
aes(x=HB, y=iVB, color=PitchType),
|
| 399 |
+
size=9, alpha=0.9) +
|
| 400 |
+
geom_text(data=avg_data,
|
| 401 |
+
aes(x=HB, y=iVB, label=Velo),
|
| 402 |
+
color="white", size=3, fontface="bold") +
|
| 403 |
+
scale_fill_manual(values=pitch_colors, name=NULL, drop=TRUE) +
|
| 404 |
+
scale_color_manual(values=pitch_colors, name=NULL, drop=TRUE) +
|
| 405 |
+
xlim(-25,25) + ylim(-25,25) +
|
| 406 |
+
labs(title=paste(display_name, "β Pitch Movement"),
|
| 407 |
+
x="Horizontal Break (in)", y="Induced Vertical Break (in)") +
|
| 408 |
+
theme_minimal() +
|
| 409 |
+
theme(
|
| 410 |
+
plot.title = element_text(hjust=0.5, size=13, face="bold", color="#002147"),
|
| 411 |
+
legend.position = "bottom",
|
| 412 |
+
legend.text = element_text(size=9),
|
| 413 |
+
legend.key.size = unit(0.4,"cm")
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
pitch_table <- pdata %>%
|
| 417 |
+
mutate(
|
| 418 |
+
IsStrike = PitchCall %in% c("StrikeCalled","StrikeSwinging","FoulBall",
|
| 419 |
+
"FoulBallNotFieldable","FoulTip","InPlay"),
|
| 420 |
+
IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
|
| 421 |
+
abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
|
| 422 |
+
IsWhiff = PitchCall == "StrikeSwinging",
|
| 423 |
+
IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
|
| 424 |
+
"FoulBallNotFieldable","FoulTip","InPlay"),
|
| 425 |
+
IsChase = IsSwing & !IsZone,
|
| 426 |
+
IsOutZone = !IsZone,
|
| 427 |
+
IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
|
| 428 |
+
!PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
|
| 429 |
+
) %>%
|
| 430 |
+
group_by(`Pitch Type`=PitchType) %>%
|
| 431 |
+
summarise(
|
| 432 |
+
`#` = n(),
|
| 433 |
+
`Usage%` = round(n() / nrow(pdata) * 100, 1),
|
| 434 |
+
Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
|
| 435 |
+
iVB = round(mean(InducedVertBreak, na.rm=TRUE), 1),
|
| 436 |
+
HB = round(mean(HorzBreak, na.rm=TRUE), 1),
|
| 437 |
+
Spin = round(mean(SpinRate, na.rm=TRUE), 0),
|
| 438 |
+
RelH = round(mean(RelHeight, na.rm=TRUE), 1),
|
| 439 |
+
RelS = round(mean(RelSide, na.rm=TRUE), 1),
|
| 440 |
+
VAA = round(mean(VertApprAngle, na.rm=TRUE), 1),
|
| 441 |
+
HAA = round(mean(HorzApprAngle, na.rm=TRUE), 1),
|
| 442 |
+
`Zone%` = round(sum(IsZone, na.rm=TRUE) / n() * 100, 1),
|
| 443 |
+
`Strike%` = round(sum(IsStrike, na.rm=TRUE) / n() * 100, 1),
|
| 444 |
+
`Whiff%` = round(ifelse(sum(IsSwing)==0, NA,
|
| 445 |
+
sum(IsWhiff)/sum(IsSwing)*100), 1),
|
| 446 |
+
`Chase%` = round(ifelse(sum(IsOutZone)==0, NA,
|
| 447 |
+
sum(IsChase)/sum(IsOutZone)*100), 1),
|
| 448 |
+
`Hard Hit%`= round(ifelse(sum(!is.na(ExitSpeed))==0, NA,
|
| 449 |
+
sum(IsHardHit)/sum(!is.na(ExitSpeed))*100), 1),
|
| 450 |
+
.groups = "drop"
|
| 451 |
+
) %>%
|
| 452 |
+
arrange(desc(`#`)) %>%
|
| 453 |
+
mutate(across(where(is.numeric), ~ifelse(is.nan(.), NA, .)))
|
| 454 |
+
|
| 455 |
+
output$pitch_type_table <- renderDT({
|
| 456 |
+
datatable(
|
| 457 |
+
pitch_table,
|
| 458 |
+
rownames = FALSE,
|
| 459 |
+
selection = "none",
|
| 460 |
+
options = list(
|
| 461 |
+
pageLength = 10,
|
| 462 |
+
dom = "t",
|
| 463 |
+
scrollX = TRUE,
|
| 464 |
+
ordering = TRUE,
|
| 465 |
+
columnDefs = list(
|
| 466 |
+
list(className="dt-center", targets=1:11),
|
| 467 |
+
list(className="dt-left", targets=0)
|
| 468 |
+
)
|
| 469 |
+
)
|
| 470 |
+
)
|
| 471 |
+
})
|
| 472 |
+
|
| 473 |
+
output$pitcher_modal <- renderUI({
|
| 474 |
+
tags$div(class="modal-backdrop",
|
| 475 |
+
tags$div(class="modal-box",
|
| 476 |
+
tags$button("β", id="modal_close", class="modal-close"),
|
| 477 |
+
tags$p(paste(display_name, "β", pdata$PitcherTeam[1]),
|
| 478 |
+
class="modal-title"),
|
| 479 |
+
plotOutput("movement_plot", height="420px")
|
| 480 |
+
)
|
| 481 |
+
)
|
| 482 |
+
})
|
| 483 |
+
|
| 484 |
+
output$movement_plot <- renderPlot({ p })
|
| 485 |
+
})
|
| 486 |
}
|
| 487 |
|
| 488 |
shinyApp(ui, server)
|