Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -24,12 +24,12 @@ load_portal <- function() {
|
|
| 24 |
rename(Pitcher = pitcher, Portal = portal) %>%
|
| 25 |
distinct(Pitcher, .keep_all = TRUE)
|
| 26 |
},
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
)
|
| 32 |
}
|
|
|
|
| 33 |
# ── Pitch colors ───────────────────────────────────────────────────────────────
|
| 34 |
pitch_colors <- c(
|
| 35 |
"Fastball" = "red", "Sinker" = "orange",
|
|
@@ -63,7 +63,7 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
|
|
| 63 |
|
| 64 |
team_lookup <- df %>%
|
| 65 |
select(Pitcher, PitcherTeam) %>%
|
| 66 |
-
distinct(Pitcher, .keep_all=TRUE)
|
| 67 |
|
| 68 |
df %>%
|
| 69 |
mutate(
|
|
@@ -86,39 +86,39 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
|
|
| 86 |
group_by(Pitcher, Level) %>%
|
| 87 |
summarise(
|
| 88 |
Pitches = n(),
|
| 89 |
-
K = sum(IsK, na.rm=TRUE),
|
| 90 |
-
BB = sum(IsBB, na.rm=TRUE),
|
| 91 |
-
PA = sum(IsPA, na.rm=TRUE),
|
| 92 |
-
zone_n = sum(IsZone, na.rm=TRUE),
|
| 93 |
-
strike_n = sum(IsStrike, na.rm=TRUE),
|
| 94 |
-
whiff_n = sum(IsWhiff, na.rm=TRUE),
|
| 95 |
-
swing_n = sum(IsSwing, na.rm=TRUE),
|
| 96 |
-
chase_n = sum(IsChase, na.rm=TRUE),
|
| 97 |
-
outzone_n = sum(IsOutZone, na.rm=TRUE),
|
| 98 |
-
hh_n = sum(IsHardHit, na.rm=TRUE),
|
| 99 |
bip_n = sum(!is.na(ExitSpeed) &
|
| 100 |
!PitchCall %in% c("FoulBall","FoulBallNotFieldable",
|
| 101 |
-
"FoulBallFieldable","FoulTip"), na.rm=TRUE),
|
| 102 |
fb_velo_sum = sum(ifelse(!is.na(RelSpeed) &
|
| 103 |
TaggedPitchType %in% c("Fastball","FourSeamFastBall",
|
| 104 |
"Four-Seam","FourSeam",
|
| 105 |
"Sinker","TwoSeamFastBall",
|
| 106 |
"OneSeamFastball","Cutter"),
|
| 107 |
-
RelSpeed, NA), na.rm=TRUE),
|
| 108 |
fb_velo_n = sum(!is.na(RelSpeed) &
|
| 109 |
TaggedPitchType %in% c("Fastball","FourSeamFastBall",
|
| 110 |
"Four-Seam","FourSeam",
|
| 111 |
"Sinker","TwoSeamFastBall",
|
| 112 |
"OneSeamFastball","Cutter"),
|
| 113 |
-
na.rm=TRUE),
|
| 114 |
.groups = "drop"
|
| 115 |
) %>%
|
| 116 |
-
left_join(team_lookup, by="Pitcher") %>%
|
| 117 |
-
left_join(scores %>% select(Pitcher, `Stuff+`, `Location+`), by="Pitcher") %>%
|
| 118 |
filter(Pitches >= min_pitches) %>%
|
| 119 |
-
left_join(portal, by="Pitcher") %>%
|
| 120 |
mutate(
|
| 121 |
-
Portal
|
| 122 |
Name = sapply(Pitcher, format_name),
|
| 123 |
Team = PitcherTeam,
|
| 124 |
`Zone%` = round(zone_n / Pitches * 100, 1),
|
|
@@ -137,13 +137,13 @@ build_pitcher_stats <- function(df, min_pitches, scores, portal) {
|
|
| 137 |
round(fb_velo_sum / fb_velo_n, 1)),
|
| 138 |
`Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
|
| 139 |
`Plot` = paste0('<button class="plot-btn" data-pitcher="',
|
| 140 |
-
gsub('"','"',Pitcher),
|
| 141 |
'">📊 View</button>'),
|
| 142 |
`Twitter` = paste0('<a href="https://www.google.com/search?q=',
|
| 143 |
-
gsub(" ","+",sapply(Pitcher, format_name)),
|
| 144 |
'+baseball+twitter" target="_blank">🐦 Search</a>')
|
| 145 |
) %>%
|
| 146 |
-
filter(!is.na(Name), trimws(Name)!="", grepl(" ",trimws(Name))) %>%
|
| 147 |
select(Name, Level, Team, Pitches, Portal, `FB Velo`, `Zone%`, `Strike%`,
|
| 148 |
`K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
|
| 149 |
`Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
|
|
@@ -199,6 +199,7 @@ ui <- fluidPage(
|
|
| 199 |
background:#e8f0fe; border-radius:4px; padding:3px 8px; display:inline-block;
|
| 200 |
}
|
| 201 |
table.dataTable tbody td a:hover { background:#002147; color:white; }
|
|
|
|
| 202 |
.portal-yes { color:#00840D; font-weight:700; }
|
| 203 |
.portal-no { color:#aaa; font-weight:400; }
|
| 204 |
|
|
@@ -279,57 +280,57 @@ ui <- fluidPage(
|
|
| 279 |
),
|
| 280 |
|
| 281 |
# splash
|
| 282 |
-
tags$div(id="splash",
|
| 283 |
-
tags$img(src="Butler-Bulldogs-Logo-1990.png"),
|
| 284 |
tags$h1("Butler Baseball Transfer Portal"),
|
| 285 |
tags$p("Butler Baseball Analytics"),
|
| 286 |
-
tags$div(class="bar-track", tags$div(class="bar-fill"))
|
| 287 |
),
|
| 288 |
|
| 289 |
# header
|
| 290 |
-
tags$div(class="header",
|
| 291 |
tags$div(
|
| 292 |
tags$h1("Butler Baseball Transfer Portal Dashboard"),
|
| 293 |
tags$p("Butler Baseball Analytics")
|
| 294 |
),
|
| 295 |
-
tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
|
| 296 |
),
|
| 297 |
|
| 298 |
# filters
|
| 299 |
-
tags$div(class="filters",
|
| 300 |
tags$div(
|
| 301 |
-
tags$div("Level", class="filter-label"),
|
| 302 |
-
selectInput("level", label=NULL,
|
| 303 |
-
choices=c("D1","D2","D3","JUCO","NAIA"),
|
| 304 |
-
selected="D1", width="160px")
|
| 305 |
),
|
| 306 |
tags$div(
|
| 307 |
-
tags$div("Position", class="filter-label"),
|
| 308 |
-
selectInput("position", label=NULL,
|
| 309 |
-
choices=c("Pitcher","Hitter"),
|
| 310 |
-
selected="Pitcher", width="160px")
|
| 311 |
),
|
| 312 |
tags$div(
|
| 313 |
-
tags$div("Min Pitches", class="filter-label"),
|
| 314 |
-
numericInput("min_pitches", label=NULL,
|
| 315 |
-
value=100, min=1, max=2000, step=25, width="160px")
|
| 316 |
)
|
| 317 |
),
|
| 318 |
|
| 319 |
# main
|
| 320 |
-
tags$div(class="main",
|
| 321 |
uiOutput("main_ui")
|
| 322 |
),
|
| 323 |
|
| 324 |
# modal
|
| 325 |
tags$div(
|
| 326 |
-
id="modal_wrapper", class="modal-overlay", style="display:none;",
|
| 327 |
tags$div(
|
| 328 |
-
class="modal-box",
|
| 329 |
-
tags$button("✕", id="modal_close_btn", class="modal-close"),
|
| 330 |
uiOutput("modal_header"),
|
| 331 |
-
plotOutput("movement_plot", height="400px"),
|
| 332 |
-
tags$div("Pitch Specifications", class="section-label"),
|
| 333 |
DTOutput("pitch_type_table")
|
| 334 |
)
|
| 335 |
)
|
|
@@ -347,7 +348,7 @@ server <- function(input, output, session) {
|
|
| 347 |
portal = load_portal()
|
| 348 |
)
|
| 349 |
}, error = function(e) {
|
| 350 |
-
showNotification(paste("Failed to load:", e$message), type="error")
|
| 351 |
NULL
|
| 352 |
})
|
| 353 |
})
|
|
@@ -355,23 +356,30 @@ server <- function(input, output, session) {
|
|
| 355 |
stats_table <- reactive({
|
| 356 |
req(raw_data(), input$min_pitches)
|
| 357 |
if (input$position == "Pitcher")
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
)
|
| 364 |
})
|
| 365 |
|
| 366 |
output$main_ui <- renderUI({
|
| 367 |
if (input$position == "Pitcher") {
|
| 368 |
tagList(
|
| 369 |
-
tags$p(id="loading_msg", "Loading data...",
|
| 370 |
-
style="color:#888; font-size:14px; padding:10px 0;"),
|
| 371 |
DTOutput("pitcher_table")
|
| 372 |
)
|
| 373 |
} else {
|
| 374 |
-
tags$p("Hitter view coming soon.", style="color:#888;")
|
| 375 |
}
|
| 376 |
})
|
| 377 |
|
|
@@ -404,9 +412,9 @@ server <- function(input, output, session) {
|
|
| 404 |
orderMulti = FALSE,
|
| 405 |
dom = "lfrtip",
|
| 406 |
columnDefs = list(
|
| 407 |
-
list(className="dt-left", targets=0:1),
|
| 408 |
-
list(className="dt-center", targets=2:17),
|
| 409 |
-
list(orderable=FALSE, targets=c(16,17))
|
| 410 |
),
|
| 411 |
initComplete = JS("function(settings, json) {
|
| 412 |
$('#loading_msg').hide();
|
|
@@ -443,12 +451,12 @@ server <- function(input, output, session) {
|
|
| 443 |
output$modal_header <- renderUI({
|
| 444 |
req(current_pitcher())
|
| 445 |
parts <- strsplit(current_pitcher(), ", ")[[1]]
|
| 446 |
-
display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
|
| 447 |
pdata <- pitcher_plot_data()
|
| 448 |
-
team <- if (nrow(pdata)>0) pdata$PitcherTeam[1] else ""
|
| 449 |
tagList(
|
| 450 |
-
tags$p(display_name, class="modal-title"),
|
| 451 |
-
tags$p(team, class="modal-subtitle")
|
| 452 |
)
|
| 453 |
})
|
| 454 |
|
|
@@ -456,40 +464,40 @@ server <- function(input, output, session) {
|
|
| 456 |
req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
|
| 457 |
pdata <- pitcher_plot_data()
|
| 458 |
parts <- strsplit(current_pitcher(), ", ")[[1]]
|
| 459 |
-
display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
|
| 460 |
|
| 461 |
avg_data <- pdata %>%
|
| 462 |
group_by(PitchType) %>%
|
| 463 |
summarise(
|
| 464 |
-
HB = mean(HorzBreak, na.rm=TRUE),
|
| 465 |
-
iVB = mean(InducedVertBreak, na.rm=TRUE),
|
| 466 |
-
Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
|
| 467 |
-
.groups="drop"
|
| 468 |
)
|
| 469 |
|
| 470 |
ggplot() +
|
| 471 |
-
geom_vline(xintercept=0, color="black") +
|
| 472 |
-
geom_hline(yintercept=0, color="black") +
|
| 473 |
-
geom_point(data=pdata,
|
| 474 |
-
aes(x=HorzBreak, y=InducedVertBreak, fill=PitchType),
|
| 475 |
-
size=3, alpha=0.6, shape=21, color="black", stroke=0.4) +
|
| 476 |
-
geom_point(data=avg_data,
|
| 477 |
-
aes(x=HB, y=iVB, color=PitchType),
|
| 478 |
-
size=9, alpha=0.9) +
|
| 479 |
-
geom_text(data=avg_data,
|
| 480 |
-
aes(x=HB, y=iVB, label=Velo),
|
| 481 |
-
color="white", size=3, fontface="bold") +
|
| 482 |
-
scale_fill_manual(values=pitch_colors, name=NULL, drop=TRUE) +
|
| 483 |
-
scale_color_manual(values=pitch_colors, name=NULL, drop=TRUE) +
|
| 484 |
-
xlim(-25,25) + ylim(-25,25) +
|
| 485 |
-
labs(title=paste(display_name, "— Pitch Movement"),
|
| 486 |
-
x="Horizontal Break (in)", y="Induced Vertical Break (in)") +
|
| 487 |
theme_minimal() +
|
| 488 |
theme(
|
| 489 |
-
plot.title = element_text(hjust=0.5, size=13, face="bold", color="#002147"),
|
| 490 |
legend.position = "bottom",
|
| 491 |
-
legend.text = element_text(size=9),
|
| 492 |
-
legend.key.size = unit(0.4,"cm")
|
| 493 |
)
|
| 494 |
})
|
| 495 |
|
|
@@ -511,26 +519,26 @@ server <- function(input, output, session) {
|
|
| 511 |
IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
|
| 512 |
!PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
|
| 513 |
) %>%
|
| 514 |
-
group_by(`Pitch Type`=PitchType) %>%
|
| 515 |
summarise(
|
| 516 |
`#` = n(),
|
| 517 |
-
`Usage%` = round(n()/nrow(pdata)*100, 1),
|
| 518 |
-
Velo = round(mean(RelSpeed, na.rm=TRUE), 1),
|
| 519 |
-
iVB = round(mean(InducedVertBreak, na.rm=TRUE), 1),
|
| 520 |
-
HB = round(mean(HorzBreak, na.rm=TRUE), 1),
|
| 521 |
-
Spin = round(mean(SpinRate, na.rm=TRUE), 0),
|
| 522 |
-
RelH = round(mean(RelHeight, na.rm=TRUE), 1),
|
| 523 |
-
RelS = round(mean(RelSide, na.rm=TRUE), 1),
|
| 524 |
-
VAA = round(mean(VertApprAngle, na.rm=TRUE), 1),
|
| 525 |
-
HAA = round(mean(HorzApprAngle, na.rm=TRUE), 1),
|
| 526 |
-
`Zone%` = round(sum(IsZone,
|
| 527 |
-
`Strike%` = round(sum(IsStrike,
|
| 528 |
-
`Whiff%` = round(ifelse(sum(IsSwing)==0, NA,
|
| 529 |
-
sum(IsWhiff)/sum(IsSwing)*100), 1),
|
| 530 |
-
`Chase%` = round(ifelse(sum(IsOutZone)==0, NA,
|
| 531 |
-
sum(IsChase)/sum(IsOutZone)*100), 1),
|
| 532 |
-
`Hard Hit%` = round(ifelse(sum(!is.na(ExitSpeed))==0, NA,
|
| 533 |
-
sum(IsHardHit)/sum(!is.na(ExitSpeed))*100), 1),
|
| 534 |
.groups = "drop"
|
| 535 |
) %>%
|
| 536 |
arrange(desc(`#`)) %>%
|
|
@@ -546,8 +554,8 @@ server <- function(input, output, session) {
|
|
| 546 |
scrollX = TRUE,
|
| 547 |
ordering = TRUE,
|
| 548 |
columnDefs = list(
|
| 549 |
-
list(className="dt-center", targets=1:15),
|
| 550 |
-
list(className="dt-left", targets=0)
|
| 551 |
)
|
| 552 |
)
|
| 553 |
)
|
|
|
|
| 24 |
rename(Pitcher = pitcher, Portal = portal) %>%
|
| 25 |
distinct(Pitcher, .keep_all = TRUE)
|
| 26 |
},
|
| 27 |
+
error = function(e) {
|
| 28 |
+
message("Portal load failed: ", e$message)
|
| 29 |
+
tibble(Pitcher = character(), Portal = character())
|
| 30 |
+
})
|
|
|
|
| 31 |
}
|
| 32 |
+
|
| 33 |
# ── Pitch colors ───────────────────────────────────────────────────────────────
|
| 34 |
pitch_colors <- c(
|
| 35 |
"Fastball" = "red", "Sinker" = "orange",
|
|
|
|
| 63 |
|
| 64 |
team_lookup <- df %>%
|
| 65 |
select(Pitcher, PitcherTeam) %>%
|
| 66 |
+
distinct(Pitcher, .keep_all = TRUE)
|
| 67 |
|
| 68 |
df %>%
|
| 69 |
mutate(
|
|
|
|
| 86 |
group_by(Pitcher, Level) %>%
|
| 87 |
summarise(
|
| 88 |
Pitches = n(),
|
| 89 |
+
K = sum(IsK, na.rm = TRUE),
|
| 90 |
+
BB = sum(IsBB, na.rm = TRUE),
|
| 91 |
+
PA = sum(IsPA, na.rm = TRUE),
|
| 92 |
+
zone_n = sum(IsZone, na.rm = TRUE),
|
| 93 |
+
strike_n = sum(IsStrike, na.rm = TRUE),
|
| 94 |
+
whiff_n = sum(IsWhiff, na.rm = TRUE),
|
| 95 |
+
swing_n = sum(IsSwing, na.rm = TRUE),
|
| 96 |
+
chase_n = sum(IsChase, na.rm = TRUE),
|
| 97 |
+
outzone_n = sum(IsOutZone, na.rm = TRUE),
|
| 98 |
+
hh_n = sum(IsHardHit, na.rm = TRUE),
|
| 99 |
bip_n = sum(!is.na(ExitSpeed) &
|
| 100 |
!PitchCall %in% c("FoulBall","FoulBallNotFieldable",
|
| 101 |
+
"FoulBallFieldable","FoulTip"), na.rm = TRUE),
|
| 102 |
fb_velo_sum = sum(ifelse(!is.na(RelSpeed) &
|
| 103 |
TaggedPitchType %in% c("Fastball","FourSeamFastBall",
|
| 104 |
"Four-Seam","FourSeam",
|
| 105 |
"Sinker","TwoSeamFastBall",
|
| 106 |
"OneSeamFastball","Cutter"),
|
| 107 |
+
RelSpeed, NA), na.rm = TRUE),
|
| 108 |
fb_velo_n = sum(!is.na(RelSpeed) &
|
| 109 |
TaggedPitchType %in% c("Fastball","FourSeamFastBall",
|
| 110 |
"Four-Seam","FourSeam",
|
| 111 |
"Sinker","TwoSeamFastBall",
|
| 112 |
"OneSeamFastball","Cutter"),
|
| 113 |
+
na.rm = TRUE),
|
| 114 |
.groups = "drop"
|
| 115 |
) %>%
|
| 116 |
+
left_join(team_lookup, by = "Pitcher") %>%
|
| 117 |
+
left_join(scores %>% select(Pitcher, `Stuff+`, `Location+`), by = "Pitcher") %>%
|
| 118 |
filter(Pitches >= min_pitches) %>%
|
| 119 |
+
left_join(portal %>% select(Pitcher, Portal), by = "Pitcher") %>%
|
| 120 |
mutate(
|
| 121 |
+
Portal = ifelse(is.na(Portal), "No", as.character(Portal)),
|
| 122 |
Name = sapply(Pitcher, format_name),
|
| 123 |
Team = PitcherTeam,
|
| 124 |
`Zone%` = round(zone_n / Pitches * 100, 1),
|
|
|
|
| 137 |
round(fb_velo_sum / fb_velo_n, 1)),
|
| 138 |
`Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
|
| 139 |
`Plot` = paste0('<button class="plot-btn" data-pitcher="',
|
| 140 |
+
gsub('"', '"', Pitcher),
|
| 141 |
'">📊 View</button>'),
|
| 142 |
`Twitter` = paste0('<a href="https://www.google.com/search?q=',
|
| 143 |
+
gsub(" ", "+", sapply(Pitcher, format_name)),
|
| 144 |
'+baseball+twitter" target="_blank">🐦 Search</a>')
|
| 145 |
) %>%
|
| 146 |
+
filter(!is.na(Name), trimws(Name) != "", grepl(" ", trimws(Name))) %>%
|
| 147 |
select(Name, Level, Team, Pitches, Portal, `FB Velo`, `Zone%`, `Strike%`,
|
| 148 |
`K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
|
| 149 |
`Stuff+`, `Location+`, `Plot`, `Twitter`) %>%
|
|
|
|
| 199 |
background:#e8f0fe; border-radius:4px; padding:3px 8px; display:inline-block;
|
| 200 |
}
|
| 201 |
table.dataTable tbody td a:hover { background:#002147; color:white; }
|
| 202 |
+
|
| 203 |
.portal-yes { color:#00840D; font-weight:700; }
|
| 204 |
.portal-no { color:#aaa; font-weight:400; }
|
| 205 |
|
|
|
|
| 280 |
),
|
| 281 |
|
| 282 |
# splash
|
| 283 |
+
tags$div(id = "splash",
|
| 284 |
+
tags$img(src = "Butler-Bulldogs-Logo-1990.png"),
|
| 285 |
tags$h1("Butler Baseball Transfer Portal"),
|
| 286 |
tags$p("Butler Baseball Analytics"),
|
| 287 |
+
tags$div(class = "bar-track", tags$div(class = "bar-fill"))
|
| 288 |
),
|
| 289 |
|
| 290 |
# header
|
| 291 |
+
tags$div(class = "header",
|
| 292 |
tags$div(
|
| 293 |
tags$h1("Butler Baseball Transfer Portal Dashboard"),
|
| 294 |
tags$p("Butler Baseball Analytics")
|
| 295 |
),
|
| 296 |
+
tags$img(src = "Butler-Bulldogs-Logo-1990.png", height = "52px")
|
| 297 |
),
|
| 298 |
|
| 299 |
# filters
|
| 300 |
+
tags$div(class = "filters",
|
| 301 |
tags$div(
|
| 302 |
+
tags$div("Level", class = "filter-label"),
|
| 303 |
+
selectInput("level", label = NULL,
|
| 304 |
+
choices = c("D1","D2","D3","JUCO","NAIA"),
|
| 305 |
+
selected = "D1", width = "160px")
|
| 306 |
),
|
| 307 |
tags$div(
|
| 308 |
+
tags$div("Position", class = "filter-label"),
|
| 309 |
+
selectInput("position", label = NULL,
|
| 310 |
+
choices = c("Pitcher","Hitter"),
|
| 311 |
+
selected = "Pitcher", width = "160px")
|
| 312 |
),
|
| 313 |
tags$div(
|
| 314 |
+
tags$div("Min Pitches", class = "filter-label"),
|
| 315 |
+
numericInput("min_pitches", label = NULL,
|
| 316 |
+
value = 100, min = 1, max = 2000, step = 25, width = "160px")
|
| 317 |
)
|
| 318 |
),
|
| 319 |
|
| 320 |
# main
|
| 321 |
+
tags$div(class = "main",
|
| 322 |
uiOutput("main_ui")
|
| 323 |
),
|
| 324 |
|
| 325 |
# modal
|
| 326 |
tags$div(
|
| 327 |
+
id = "modal_wrapper", class = "modal-overlay", style = "display:none;",
|
| 328 |
tags$div(
|
| 329 |
+
class = "modal-box",
|
| 330 |
+
tags$button("✕", id = "modal_close_btn", class = "modal-close"),
|
| 331 |
uiOutput("modal_header"),
|
| 332 |
+
plotOutput("movement_plot", height = "400px"),
|
| 333 |
+
tags$div("Pitch Specifications", class = "section-label"),
|
| 334 |
DTOutput("pitch_type_table")
|
| 335 |
)
|
| 336 |
)
|
|
|
|
| 348 |
portal = load_portal()
|
| 349 |
)
|
| 350 |
}, error = function(e) {
|
| 351 |
+
showNotification(paste("Failed to load:", e$message), type = "error")
|
| 352 |
NULL
|
| 353 |
})
|
| 354 |
})
|
|
|
|
| 356 |
stats_table <- reactive({
|
| 357 |
req(raw_data(), input$min_pitches)
|
| 358 |
if (input$position == "Pitcher")
|
| 359 |
+
tryCatch(
|
| 360 |
+
build_pitcher_stats(
|
| 361 |
+
raw_data()$df,
|
| 362 |
+
input$min_pitches,
|
| 363 |
+
raw_data()$scores,
|
| 364 |
+
raw_data()$portal
|
| 365 |
+
),
|
| 366 |
+
error = function(e) {
|
| 367 |
+
message("build_pitcher_stats failed: ", e$message)
|
| 368 |
+
showNotification(paste("Stats error:", e$message), type = "error")
|
| 369 |
+
NULL
|
| 370 |
+
}
|
| 371 |
)
|
| 372 |
})
|
| 373 |
|
| 374 |
output$main_ui <- renderUI({
|
| 375 |
if (input$position == "Pitcher") {
|
| 376 |
tagList(
|
| 377 |
+
tags$p(id = "loading_msg", "Loading data...",
|
| 378 |
+
style = "color:#888; font-size:14px; padding:10px 0;"),
|
| 379 |
DTOutput("pitcher_table")
|
| 380 |
)
|
| 381 |
} else {
|
| 382 |
+
tags$p("Hitter view coming soon.", style = "color:#888;")
|
| 383 |
}
|
| 384 |
})
|
| 385 |
|
|
|
|
| 412 |
orderMulti = FALSE,
|
| 413 |
dom = "lfrtip",
|
| 414 |
columnDefs = list(
|
| 415 |
+
list(className = "dt-left", targets = 0:1),
|
| 416 |
+
list(className = "dt-center", targets = 2:17),
|
| 417 |
+
list(orderable = FALSE, targets = c(16, 17))
|
| 418 |
),
|
| 419 |
initComplete = JS("function(settings, json) {
|
| 420 |
$('#loading_msg').hide();
|
|
|
|
| 451 |
output$modal_header <- renderUI({
|
| 452 |
req(current_pitcher())
|
| 453 |
parts <- strsplit(current_pitcher(), ", ")[[1]]
|
| 454 |
+
display_name <- if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else current_pitcher()
|
| 455 |
pdata <- pitcher_plot_data()
|
| 456 |
+
team <- if (nrow(pdata) > 0) pdata$PitcherTeam[1] else ""
|
| 457 |
tagList(
|
| 458 |
+
tags$p(display_name, class = "modal-title"),
|
| 459 |
+
tags$p(team, class = "modal-subtitle")
|
| 460 |
)
|
| 461 |
})
|
| 462 |
|
|
|
|
| 464 |
req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
|
| 465 |
pdata <- pitcher_plot_data()
|
| 466 |
parts <- strsplit(current_pitcher(), ", ")[[1]]
|
| 467 |
+
display_name <- if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else current_pitcher()
|
| 468 |
|
| 469 |
avg_data <- pdata %>%
|
| 470 |
group_by(PitchType) %>%
|
| 471 |
summarise(
|
| 472 |
+
HB = mean(HorzBreak, na.rm = TRUE),
|
| 473 |
+
iVB = mean(InducedVertBreak, na.rm = TRUE),
|
| 474 |
+
Velo = round(mean(RelSpeed, na.rm = TRUE), 1),
|
| 475 |
+
.groups = "drop"
|
| 476 |
)
|
| 477 |
|
| 478 |
ggplot() +
|
| 479 |
+
geom_vline(xintercept = 0, color = "black") +
|
| 480 |
+
geom_hline(yintercept = 0, color = "black") +
|
| 481 |
+
geom_point(data = pdata,
|
| 482 |
+
aes(x = HorzBreak, y = InducedVertBreak, fill = PitchType),
|
| 483 |
+
size = 3, alpha = 0.6, shape = 21, color = "black", stroke = 0.4) +
|
| 484 |
+
geom_point(data = avg_data,
|
| 485 |
+
aes(x = HB, y = iVB, color = PitchType),
|
| 486 |
+
size = 9, alpha = 0.9) +
|
| 487 |
+
geom_text(data = avg_data,
|
| 488 |
+
aes(x = HB, y = iVB, label = Velo),
|
| 489 |
+
color = "white", size = 3, fontface = "bold") +
|
| 490 |
+
scale_fill_manual(values = pitch_colors, name = NULL, drop = TRUE) +
|
| 491 |
+
scale_color_manual(values = pitch_colors, name = NULL, drop = TRUE) +
|
| 492 |
+
xlim(-25, 25) + ylim(-25, 25) +
|
| 493 |
+
labs(title = paste(display_name, "— Pitch Movement"),
|
| 494 |
+
x = "Horizontal Break (in)", y = "Induced Vertical Break (in)") +
|
| 495 |
theme_minimal() +
|
| 496 |
theme(
|
| 497 |
+
plot.title = element_text(hjust = 0.5, size = 13, face = "bold", color = "#002147"),
|
| 498 |
legend.position = "bottom",
|
| 499 |
+
legend.text = element_text(size = 9),
|
| 500 |
+
legend.key.size = unit(0.4, "cm")
|
| 501 |
)
|
| 502 |
})
|
| 503 |
|
|
|
|
| 519 |
IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
|
| 520 |
!PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip")
|
| 521 |
) %>%
|
| 522 |
+
group_by(`Pitch Type` = PitchType) %>%
|
| 523 |
summarise(
|
| 524 |
`#` = n(),
|
| 525 |
+
`Usage%` = round(n() / nrow(pdata) * 100, 1),
|
| 526 |
+
Velo = round(mean(RelSpeed, na.rm = TRUE), 1),
|
| 527 |
+
iVB = round(mean(InducedVertBreak, na.rm = TRUE), 1),
|
| 528 |
+
HB = round(mean(HorzBreak, na.rm = TRUE), 1),
|
| 529 |
+
Spin = round(mean(SpinRate, na.rm = TRUE), 0),
|
| 530 |
+
RelH = round(mean(RelHeight, na.rm = TRUE), 1),
|
| 531 |
+
RelS = round(mean(RelSide, na.rm = TRUE), 1),
|
| 532 |
+
VAA = round(mean(VertApprAngle, na.rm = TRUE), 1),
|
| 533 |
+
HAA = round(mean(HorzApprAngle, na.rm = TRUE), 1),
|
| 534 |
+
`Zone%` = round(sum(IsZone, na.rm = TRUE) / n() * 100, 1),
|
| 535 |
+
`Strike%` = round(sum(IsStrike, na.rm = TRUE) / n() * 100, 1),
|
| 536 |
+
`Whiff%` = round(ifelse(sum(IsSwing) == 0, NA,
|
| 537 |
+
sum(IsWhiff) / sum(IsSwing) * 100), 1),
|
| 538 |
+
`Chase%` = round(ifelse(sum(IsOutZone) == 0, NA,
|
| 539 |
+
sum(IsChase) / sum(IsOutZone) * 100), 1),
|
| 540 |
+
`Hard Hit%` = round(ifelse(sum(!is.na(ExitSpeed)) == 0, NA,
|
| 541 |
+
sum(IsHardHit) / sum(!is.na(ExitSpeed)) * 100), 1),
|
| 542 |
.groups = "drop"
|
| 543 |
) %>%
|
| 544 |
arrange(desc(`#`)) %>%
|
|
|
|
| 554 |
scrollX = TRUE,
|
| 555 |
ordering = TRUE,
|
| 556 |
columnDefs = list(
|
| 557 |
+
list(className = "dt-center", targets = 1:15),
|
| 558 |
+
list(className = "dt-left", targets = 0)
|
| 559 |
)
|
| 560 |
)
|
| 561 |
)
|