App / Server.R
cdcavin's picture
Season-aware leaderboard minimums
2ddd0de
Raw History Blame Contribute Delete
132 kB
server <- function(input, output, session) {
user_auth <- reactiveVal(FALSE)
login_transition <- reactiveVal(FALSE)
login_success_pending <- reactiveVal(FALSE)
scores_data_rv <- reactiveVal(tibble::tibble())
scores_error_rv <- reactiveVal(NULL)
standings_data_rv <- reactiveVal(tibble::tibble())
standings_error_rv <- reactiveVal(NULL)
player_scope <- reactiveVal("wvu")
all_conferences <- get_all_conferences()
conference_game_choices <- stats::setNames(paste0("conference::", all_conferences), all_conferences)
conference_standings_choices <- stats::setNames(all_conferences, all_conferences)
empty_selection_lookup <- tibble::tibble(
selection_id = character(),
player_name = character(),
player_ids = list(),
team_name = character()
)
empty_selection_choices <- stats::setNames(character(), character())
get_global_value <- function(name, default = NULL, mode = "any") {
if (exists(name, mode = mode, inherits = TRUE)) {
get(name, mode = mode, inherits = TRUE)
} else {
default
}
}
default_from_choices <- function(choices) {
if (length(choices) == 0) {
character(0)
} else {
unname(choices[[1]])
}
}
get_wvu_role_bundle <- function(lookup_name, choices_name, default_name) {
choices <- get_global_value(choices_name, empty_selection_choices)
list(
lookup = get_global_value(lookup_name, empty_selection_lookup),
choices = choices,
default = get_global_value(default_name, default_from_choices(choices))
)
}
bind_app_cache <- function(renderable, ...) {
tryCatch(
shiny::bindCache(renderable, ..., cache = "app"),
error = function(e) renderable
)
}
initial_hitter_choices <- get_global_value("wvu_hitter_choices", empty_selection_choices)
initial_pitcher_choices <- get_global_value("wvu_pitcher_choices", empty_selection_choices)
initial_catcher_choices <- get_global_value("wvu_catcher_choices", empty_selection_choices)
initial_hitter_default <- get_global_value("default_hitter_selection", default_from_choices(initial_hitter_choices))
initial_pitcher_default <- get_global_value("default_pitcher_selection", default_from_choices(initial_pitcher_choices))
initial_catcher_default <- get_global_value("default_catcher_selection", default_from_choices(initial_catcher_choices))
player_fixed_datatable <- function(tbl, page_length = 20, filter = "top", rownames = FALSE) {
player_cols <- c("Batter", "Pitcher", "Name", "Player", "Catcher", "Fielder")
player_col <- intersect(player_cols, names(tbl))[1]
left_cols <- if (is.na(player_col)) 1L else match(player_col, names(tbl))
DT::datatable(
tbl,
extensions = c("FixedHeader", "FixedColumns"),
options = list(
scrollX = TRUE,
scrollY = "70vh",
scrollCollapse = TRUE,
pageLength = page_length,
fixedHeader = list(header = TRUE, headerOffset = 80),
fixedColumns = list(leftColumns = left_cols)
),
filter = filter,
rownames = rownames
)
}
defense_error_message <- function(err, fallback = "Defense output failed") {
msg <- conditionMessage(err)
if (is.null(msg) || !nzchar(msg)) {
fallback
} else {
msg
}
}
defense_error_plot <- function(message_text) {
ggplot2::ggplot() +
ggplot2::annotate(
"text",
x = 0.5,
y = 0.5,
label = message_text,
size = 5,
color = "#C94B32",
fontface = "bold"
) +
ggplot2::xlim(0, 1) +
ggplot2::ylim(0, 1) +
ggplot2::theme_void()
}
defense_error_plotly <- function(message_text) {
plotly::plot_ly(
x = 0.5,
y = 0.5,
type = "scatter",
mode = "text",
text = message_text,
hoverinfo = "skip",
textfont = list(color = "#C94B32", size = 16)
) %>%
plotly::layout(
xaxis = list(visible = FALSE, range = c(0, 1), fixedrange = TRUE),
yaxis = list(visible = FALSE, range = c(0, 1), fixedrange = TRUE),
margin = list(l = 20, r = 20, t = 20, b = 20),
plot_bgcolor = "white",
paper_bgcolor = "white",
showlegend = FALSE
)
}
defense_error_table <- function(message_text) {
tibble::tibble(Error = message_text)
}
hitter_summary_cols <- c(
"Batter", "Season", "Date", "Inning", "PitchNo", "PitchCall", "Distance", "Bearing", "event_type",
"ExitSpeed", "Angle", "xba", "xslg", "xwOBA", "woba", "wobacon",
"TaggedPitchType", "PitcherThrows", "is_plate_appearance", "is_walk", "is_k",
"is_hit", "is_barrel", "is_hard_hit", "is_whiff", "chase", "sd_rv_added", "in_zone_whiff",
"on_base", "PlayResult", "AutoHitType", "BatterSide", "KorBB", "ContactPositionX",
"ContactPositionY", "ContactPositionZ", "in_zone", "is_swing", "Balls", "Strikes"
)
hitter_game_cols <- unique(c(
hitter_summary_cols,
"Pitcher", "RelSpeed", "Date", "GameID", "PitcherTeam", "BatterTeam", "PlateLocSide", "PlateLocHeight", "PitchofPA", "PAofInning", "Inning"
))
hitter_zone_cols <- c(
"Batter", "PitcherThrows", "TaggedPitchType", "PlateLocSide", "PlateLocHeight",
"PitchCall", "ExitSpeed", "wobacon", "xwOBA", "is_whiff", "is_hard_hit",
"sd_rv_added", "correct_decision", "in_zone", "is_swing", "BatterSide"
)
hitter_table_cols <- c(
"Batter", "Pitcher", "PitcherThrows", "TaggedPitchType", "Top.Bottom", "BatterSide",
"Date", "PitchCall", "PlayResult", "AutoHitType", "ExitSpeed", "Angle", "Bearing",
"RelSpeed", "InducedVertBreak", "HorzBreak", "SpinRate", "RelHeight", "RelSide",
"VertApprAngle", "Extension", "Balls", "Strikes", "Outs", "Inning", "xba", "xslg",
"xwOBA", "is_plate_appearance", "is_barrel", "is_hard_hit", "is_hit", "on_base",
"slg", "chase", "sd_rv_added", "is_whiff", "in_zone_whiff", "in_zone", "is_swing",
"is_k", "is_walk", "ax0", "az0"
)
pitcher_summary_cols <- c(
"Pitcher", "Batter", "Season", "Date", "PitchNo", "PitchofPA", "PAofInning", "Inning", "BatterSide", "PitcherThrows",
"TaggedPitchType", "RelSide", "RelHeight", "Extension", "HorzBreak",
"InducedVertBreak", "VertBreak", "SpinRate", "SpinAxis", "VertApprAngle",
"HorzApprAngle", "RelSpeed", "PitchCall", "ExitSpeed", "AutoHitType", "PlayResult",
"is_plate_appearance", "is_hit", "is_walk", "is_k", "is_csw", "is_barrel", "is_hard_hit", "chase", "sd_rv_added", "in_zone",
"is_put_away", "in_zone_whiff", "is_whiff", "xba", "xwOBA", "OutsOnPlay",
"RunsScored", "stuff", "arm_angle", "PlateLocSide", "PlateLocHeight", "BatterTeam",
"Balls", "Strikes"
)
catcher_cols <- c(
"Season", "Catcher", "CatcherId", "PlateLocSide", "PlateLocHeight", "sz_bot", "sz_top",
"is_swing", "is_csw", "strike_prob", "BasePositionX", "BasePositionY", "BasePositionZ",
"PopTime", "TimeToBase", "ExchangeTime", "ThrowSpeed"
)
hitter_summary_cols <- unique(c(hitter_summary_cols, "BatterId"))
hitter_game_cols <- unique(c(hitter_game_cols, "BatterId"))
hitter_zone_cols <- unique(c(hitter_zone_cols, "BatterId"))
hitter_table_cols <- unique(c(hitter_table_cols, "BatterId", "PitcherId"))
pitcher_summary_cols <- unique(c(pitcher_summary_cols, "PitcherId"))
active_hitter_bundle <- reactive(get_wvu_role_bundle("wvu_hitter_lookup", "wvu_hitter_choices", "default_hitter_selection"))
active_pitcher_bundle <- reactive(get_wvu_role_bundle("wvu_pitcher_lookup", "wvu_pitcher_choices", "default_pitcher_selection"))
active_catcher_bundle <- reactive(get_wvu_role_bundle("wvu_catcher_lookup", "wvu_catcher_choices", "default_catcher_selection"))
active_hitter_lookup <- reactive(active_hitter_bundle()$lookup)
active_pitcher_lookup <- reactive(active_pitcher_bundle()$lookup)
active_catcher_lookup <- reactive(active_catcher_bundle()$lookup)
active_hitter_choices <- reactive(active_hitter_bundle()$choices)
active_pitcher_choices <- reactive(active_pitcher_bundle()$choices)
active_catcher_choices <- reactive(active_catcher_bundle()$choices)
active_hitter_default <- reactive(active_hitter_bundle()$default)
active_pitcher_default <- reactive(active_pitcher_bundle()$default)
active_catcher_default <- reactive(active_catcher_bundle()$default)
active_outfielder_choices <- reactive(get_global_value("outfielders", character()))
active_infielder_choices <- reactive(get_global_value("infielders", character()))
active_outfielder_default <- reactive(get_global_value("default_outfielder_selection", character(0)))
active_infielder_default <- reactive(get_global_value("default_infielder_selection", character(0)))
active_hitter_pool_ids <- reactive(get_global_value("wvu_hitter_ids", character()))
active_pitcher_pool_ids <- reactive(get_global_value("wvu_pitcher_ids", character()))
active_team_oaa_names <- reactive(get_global_value("wvu_hitters", character()))
valid_selection_id <- function(selection_id, lookup) {
!is.null(selection_id) && nzchar(selection_id) && selection_id %in% lookup$selection_id
}
safe_lookup <- function(active_lookup, fallback_name) {
tryCatch(
active_lookup(),
error = function(e) get_global_value(fallback_name, empty_selection_lookup)
)
}
safe_default <- function(active_default, choices_name, default_name) {
tryCatch(
active_default(),
error = function(e) get_global_value(default_name, default_from_choices(get_global_value(choices_name, empty_selection_choices)))
)
}
resolve_choice_selection <- function(current_value, choices, fallback = NULL) {
values <- as.character(unname(choices))
if (length(values) == 0) {
return(character(0))
}
if (!is.null(current_value) && length(current_value) == 1 && nzchar(current_value) && current_value %in% values) {
return(current_value)
}
if (!is.null(fallback) && length(fallback) == 1 && nzchar(fallback) && fallback %in% values) {
return(fallback)
}
values[[1]]
}
refresh_scope_inputs <- function(view) {
view <- view %||% "hitters_summary"
if (view %in% c("hitters_summary", "hitters_game", "hitters_zones")) {
hitter_choices <- active_hitter_choices()
for (input_id in intersect(
c("player", "hitter_by_game", "hitter_zone"),
switch(
view,
hitters_summary = "player",
hitters_game = "hitter_by_game",
hitters_zones = "hitter_zone",
c("player", "hitter_by_game", "hitter_zone")
)
)) {
hitter_selected <- resolve_choice_selection(
input[[input_id]],
hitter_choices,
current_hitter_selection()
)
updateSelectizeInput(session, input_id, choices = hitter_choices, selected = hitter_selected, server = FALSE)
}
}
if (view %in% c("pitchers_summary", "pitchers_by_date", "new_pitch", "stuffbymovement", "pens", "sims", "pitchers_similarity")) {
pitcher_choices <- active_pitcher_choices()
pitcher_input_ids <- switch(
view,
pitchers_summary = "pitcher",
pitchers_by_date = "Pitcher_dates",
new_pitch = "new_pitch_pitcher",
stuffbymovement = "stuff_pitcher",
pens = "pens_pitcher",
sims = "sim_pitcher",
pitchers_similarity = "similarity_pitcher",
c("pitcher", "Pitcher_dates", "new_pitch_pitcher", "stuff_pitcher", "pens_pitcher", "sim_pitcher", "similarity_pitcher")
)
for (input_id in pitcher_input_ids) {
pitcher_selected <- resolve_choice_selection(
input[[input_id]],
pitcher_choices,
current_pitcher_selection()
)
updateSelectizeInput(session, input_id, choices = pitcher_choices, selected = pitcher_selected, server = FALSE)
}
}
if (identical(view, "catching_defense")) {
catcher_choices <- active_catcher_choices()
catcher_selected <- resolve_choice_selection(input$catcher, catcher_choices, active_catcher_default())
updateSelectizeInput(session, "catcher", choices = catcher_choices, selected = catcher_selected, server = FALSE)
}
if (identical(view, "outfield_defense")) {
outfielder_choices <- active_outfielder_choices()
outfielder_selected <- resolve_choice_selection(input$outfielder, outfielder_choices, active_outfielder_default())
updateSelectInput(session, "outfielder", choices = outfielder_choices, selected = outfielder_selected)
}
if (identical(view, "infield_defense")) {
infielder_choices <- active_infielder_choices()
infielder_selected <- resolve_choice_selection(input$infielder, infielder_choices, active_infielder_default())
updateSelectInput(session, "infielder", choices = infielder_choices, selected = infielder_selected)
}
}
observeEvent(input$current_view, {
refresh_scope_inputs(input$current_view)
}, ignoreInit = TRUE)
selected_hitter_name <- function(selection_id) {
lookup_selection_name(selection_id, safe_lookup(active_hitter_lookup, "wvu_hitter_lookup"))
}
selected_hitter_ids <- function(selection_id) {
lookup_selection_ids(selection_id, safe_lookup(active_hitter_lookup, "wvu_hitter_lookup"))
}
selected_pitcher_name <- function(selection_id) {
lookup_selection_name(selection_id, safe_lookup(active_pitcher_lookup, "wvu_pitcher_lookup"))
}
selected_pitcher_ids <- function(selection_id) {
lookup_selection_ids(selection_id, safe_lookup(active_pitcher_lookup, "wvu_pitcher_lookup"))
}
selected_catcher_name <- function(selection_id) {
lookup_selection_name(selection_id, safe_lookup(active_catcher_lookup, "wvu_catcher_lookup"))
}
selected_catcher_ids <- function(selection_id) {
lookup_selection_ids(selection_id, safe_lookup(active_catcher_lookup, "wvu_catcher_lookup"))
}
current_hitter_selection <- function(input_id = NULL) {
if (!is.null(input_id) && isTruthy(input[[input_id]])) {
return(input[[input_id]])
}
for (candidate in c("player", "hitter_by_game", "hitter_zone")) {
if (valid_selection_id(input[[candidate]], safe_lookup(active_hitter_lookup, "wvu_hitter_lookup"))) {
return(input[[candidate]])
}
}
safe_default(active_hitter_default, "wvu_hitter_choices", "default_hitter_selection")
}
current_pitcher_selection <- function(input_id = NULL) {
if (!is.null(input_id) && isTruthy(input[[input_id]])) {
return(input[[input_id]])
}
for (candidate in c("pitcher", "Pitcher_dates", "new_pitch_pitcher", "stuff_pitcher", "pens_pitcher", "sim_pitcher", "similarity_pitcher")) {
if (valid_selection_id(input[[candidate]], safe_lookup(active_pitcher_lookup, "wvu_pitcher_lookup"))) {
return(input[[candidate]])
}
}
safe_default(active_pitcher_default, "wvu_pitcher_choices", "default_pitcher_selection")
}
current_hitter_name <- function(input_id = NULL) {
selected_hitter_name(current_hitter_selection(input_id))
}
current_hitter_ids <- function(input_id = NULL) {
selected_hitter_ids(current_hitter_selection(input_id))
}
current_pitcher_name <- function(input_id = NULL) {
selected_pitcher_name(current_pitcher_selection(input_id))
}
current_pitcher_ids <- function(input_id = NULL) {
selected_pitcher_ids(current_pitcher_selection(input_id))
}
exact_pitcher_selection <- function(input_id) {
if (is.null(input_id)) {
return(character(0))
}
selection_id <- input[[input_id]]
if (valid_selection_id(selection_id, safe_lookup(active_pitcher_lookup, "wvu_pitcher_lookup"))) {
return(selection_id)
}
character(0)
}
exact_pitcher_name <- function(input_id) {
selected_pitcher_name(exact_pitcher_selection(input_id))
}
exact_pitcher_ids <- function(input_id) {
selected_pitcher_ids(exact_pitcher_selection(input_id))
}
hitter_summary_data <- reactive({
req(current_hitter_selection("player"), input$season)
batter_ids <- current_hitter_ids("player")
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_summary_cols,
seasons = input$season,
batter_ids = batter_ids,
cache_prefix = "server_hitter_summary"
) %>%
filter_out_scrimmage_dates()
})
hitter_game_data <- reactive({
req(current_hitter_selection("hitter_by_game"))
batter_ids <- current_hitter_ids("hitter_by_game")
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_game_cols,
batter_ids = batter_ids,
cache_prefix = "server_hitter_game"
) %>%
filter_out_scrimmage_dates()
})
hitter_zone_data <- reactive({
req(current_hitter_selection("hitter_zone"))
batter_ids <- current_hitter_ids("hitter_zone")
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_zone_cols,
batter_ids = batter_ids,
pitch_types = if (isTruthy(input$type_zones) && input$type_zones != "All") input$type_zones else NULL,
pitcher_throws = if (isTruthy(input$side_zones) && input$side_zones != "Any") input$side_zones else NULL,
cache_prefix = "server_hitter_zones"
) %>%
filter_out_scrimmage_dates()
})
hitter_leaderboard_data <- reactive({
req(input$pthrows, input$date_range_hitters)
batter_ids <- active_hitter_pool_ids()
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_table_cols,
batter_ids = batter_ids,
pitcher_throws = if (input$pthrows %in% c("Left", "Right")) input$pthrows else NULL,
date_range = input$date_range_hitters,
cache_prefix = "server_hitter_leaderboard"
) %>%
filter_out_scrimmage_dates()
})
hitter_filter_data <- reactive({
top_bottom <- NULL
if (identical(input$loc_filter, "Home")) {
top_bottom <- "Bottom"
} else if (identical(input$loc_filter, "Away")) {
top_bottom <- "Top"
}
batter_ids <- active_hitter_pool_ids()
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_table_cols,
batter_ids = batter_ids,
pitcher_throws = if (isTruthy(input$side_filter) && input$side_filter != "Any") input$side_filter else NULL,
pitch_types = if (isTruthy(input$type_filter) && input$type_filter != "Any") input$type_filter else NULL,
top_bottom = top_bottom,
cache_prefix = "server_hitter_filter"
) %>%
filter_out_scrimmage_dates()
})
hitter_arsenal_data <- reactive({
top_bottom <- NULL
if (identical(input$loc_filter2, "Home")) {
top_bottom <- "Bottom"
} else if (identical(input$loc_filter2, "Away")) {
top_bottom <- "Top"
}
batter_ids <- active_hitter_pool_ids()
req(length(batter_ids) > 0)
query_tm_pbp(
columns = hitter_table_cols,
batter_ids = batter_ids,
pitcher_throws = if (isTruthy(input$side_filter2) && input$side_filter2 != "Any") input$side_filter2 else NULL,
pitch_types = if (isTruthy(input$type_filter2) && input$type_filter2 != "Any") input$type_filter2 else NULL,
top_bottom = top_bottom,
cache_prefix = "server_hitter_arsenal"
) %>%
filter_out_scrimmage_dates()
})
pitcher_summary_data <- reactive({
req(current_pitcher_selection("pitcher"), input$season_pitchers)
pitcher_ids <- current_pitcher_ids("pitcher")
req(length(pitcher_ids) > 0)
query_tm_pbp(
columns = pitcher_summary_cols,
seasons = input$season_pitchers,
pitcher_ids = pitcher_ids,
cache_prefix = "server_pitcher_summary"
) %>%
filter_out_scrimmage_dates()
})
pitcher_by_date_data <- reactive({
req(isTruthy(exact_pitcher_selection("Pitcher_dates")))
pitcher_ids <- exact_pitcher_ids("Pitcher_dates")
req(length(pitcher_ids) > 0)
query_tm_pbp(
columns = unique(c(pitcher_summary_cols, "GameID")),
pitcher_ids = pitcher_ids,
cache_prefix = "server_pitcher_by_date"
)
})
pitcher_leaderboard_data <- reactive({
req(input$date_range_pitchers)
pitcher_ids <- active_pitcher_pool_ids()
req(length(pitcher_ids) > 0)
query_tm_pbp(
columns = pitcher_summary_cols,
pitcher_ids = pitcher_ids,
batter_sides = if (input$side %in% c("Left", "Right")) input$side else NULL,
date_range = input$date_range_pitchers,
cache_prefix = "server_pitcher_leaderboard"
) %>%
filter_out_scrimmage_dates()
})
stuff_movement_data <- reactive({
req(current_pitcher_selection("stuff_pitcher"))
pitcher_ids <- current_pitcher_ids("stuff_pitcher")
req(length(pitcher_ids) > 0)
query_tm_pbp(
columns = pitcher_summary_cols,
pitcher_ids = pitcher_ids,
cache_prefix = "server_stuff_movement"
) %>%
filter_out_scrimmage_dates()
})
catcher_view_data <- reactive({
req(input$season_catchers, input$catcher)
catcher_ids <- selected_catcher_ids(input$catcher)
req(length(catcher_ids) > 0)
query_tm_pbp(
columns = catcher_cols,
seasons = input$season_catchers,
catcher_ids = catcher_ids,
cache_prefix = "server_catcher_view"
) %>%
filter_out_scrimmage_dates()
})
similarity_pitcher_data <- reactive({
req(current_pitcher_selection("similarity_pitcher"), input$similarity_pitch_type)
pitcher_ids <- current_pitcher_ids("similarity_pitcher")
req(length(pitcher_ids) > 0)
query_tm_pbp(
columns = c("Pitcher", "PitcherId", "TaggedPitchType", "Season", "RelSpeed", "HorzBreak", "InducedVertBreak", "arm_angle"),
pitcher_ids = pitcher_ids,
pitch_types = input$similarity_pitch_type,
seasons = if (isTruthy(input$similarity_season) && input$similarity_season != "All") input$similarity_season else NULL,
cache_prefix = "server_similarity_pitcher"
) %>%
filter_out_scrimmage_dates()
})
hitter_summary_context <- reactive({
req(current_hitter_selection("player"), input$season)
data <- hitter_summary_data()
if (is_spring_season_label(input$season)) {
data <- data[!is_wvu_vs_wvu_game(data), , drop = FALSE]
}
list(
data = data,
selection = current_hitter_selection("player"),
ids = current_hitter_ids("player"),
name = current_hitter_name("player"),
season = input$season
)
})
get_hitter_summary_bundle_cached <- function(selection, season, ids, name, data, scope = "wvu") {
ids <- as.character(ids)
ids <- ids[!is.na(ids) & nzchar(ids)]
cache_key <- make_cache_key(
"server_hitter_summary_bundle",
scope,
selection %||% "",
season %||% "",
cache_key_part(ids)
)
cache_get_or_set(cache_key, {
radial_plot <- radial(data, name)
spider_plot <- get_spider(hitter_stats, name, season, batter_ids = ids)
list(
val_percentiles = value_percentiles(hit_war, name, season, batter_ids = ids),
hit_percentiles = hitter_percentiles(data, name, season, batter_ids = ids),
field_percentiles = fielding_percentiles(defense, name, season, batter_ids = ids),
spray_chart = spray_chart(data, name),
rolling_xwoba = rolling_xwOBA(data, name),
radial = radial_plot,
hit_summary_overall = hitter_summary_overall(data, name, season, hitter_stats, batter_ids = ids),
hit_summary = hitter_summary(data, name),
bb_profile = batted_ball_profile(data, name),
discipline = plate_discipline(data, name),
row_1_hitter = gridExtra::arrangeGrob(spider_plot, radial_plot, ncol = 2),
contact = contact_pos(data, name, season)
)
}, namespace = "server_hitter_summary_bundle")
}
hitter_summary_bundle <- reactive({
ctx <- hitter_summary_context()
get_hitter_summary_bundle_cached(
selection = ctx$selection,
season = ctx$season,
ids = ctx$ids,
name = ctx$name,
data = ctx$data,
scope = player_scope()
)
})
prewarm_hitter_summary_bundle_cache <- local({
warmed <- FALSE
function(force = FALSE) {
if (warmed && !force) {
return(invisible(NULL))
}
if (tolower(Sys.getenv("WVU_APP_PREWARM_HITTER_RENDER_CACHE", "false")) != "true") {
warmed <<- TRUE
return(invisible(NULL))
}
seasons <- get_available_seasons()
lookup <- get_global_value("wvu_hitter_lookup", empty_selection_lookup)
if (nrow(lookup) == 0 || length(seasons) == 0) {
warmed <<- TRUE
return(invisible(NULL))
}
for (row_index in seq_len(nrow(lookup))) {
selection_id <- lookup$selection_id[[row_index]]
player_name <- lookup$player_name[[row_index]]
player_ids <- as.character(lookup$player_ids[[row_index]])
player_ids <- player_ids[!is.na(player_ids) & nzchar(player_ids)]
if (!nzchar(selection_id) || !nzchar(player_name) || length(player_ids) == 0) {
next
}
for (season_value in seasons) {
summary_data <- query_tm_pbp(
columns = hitter_summary_cols,
seasons = season_value,
batter_ids = player_ids,
cache_prefix = "server_hitter_summary"
)
invisible(get_hitter_summary_bundle_cached(
selection = selection_id,
season = season_value,
ids = player_ids,
name = player_name,
data = summary_data,
scope = "wvu"
))
}
}
warmed <<- TRUE
invisible(NULL)
}
})
prewarm_hitter_summary_bundle_cache()
prewarm_initial_hitter_summary_bundle <- function() {
default_selection <- initial_hitter_default
if (is.null(default_selection) || !nzchar(default_selection)) {
return(invisible(NULL))
}
season_value <- get_default_season()
player_ids <- selected_hitter_ids(default_selection)
player_ids <- as.character(player_ids)
player_ids <- player_ids[!is.na(player_ids) & nzchar(player_ids)]
player_name <- selected_hitter_name(default_selection)
if (!nzchar(player_name) || length(player_ids) == 0 || is.null(season_value) || !nzchar(season_value)) {
return(invisible(NULL))
}
summary_data <- query_tm_pbp(
columns = hitter_summary_cols,
seasons = season_value,
batter_ids = player_ids,
cache_prefix = "server_hitter_summary"
)
invisible(get_hitter_summary_bundle_cached(
selection = default_selection,
season = season_value,
ids = player_ids,
name = player_name,
data = summary_data,
scope = "wvu"
))
}
session$onFlushed(function() {
prewarm_initial_hitter_summary_bundle()
}, once = TRUE)
pitcher_summary_context <- reactive({
req(current_pitcher_selection("pitcher"), input$season_pitchers)
data <- pitcher_summary_data()
if (is_spring_season_label(input$season_pitchers)) {
data <- data[!is_wvu_vs_wvu_game(data), , drop = FALSE]
}
list(
data = data,
selection = current_pitcher_selection("pitcher"),
ids = current_pitcher_ids("pitcher"),
name = current_pitcher_name("pitcher"),
season = input$season_pitchers
)
})
hitter_game_choices <- reactive({
req(current_hitter_selection("hitter_by_game"))
build_hitter_game_choices(hitter_game_data(), current_hitter_ids("hitter_by_game"))
})
pitcher_by_date_game_choices <- reactive({
req(isTruthy(exact_pitcher_selection("Pitcher_dates")))
build_pitcher_game_choices(
pitcher_by_date_data(),
pitcher_ids = exact_pitcher_ids("Pitcher_dates")
)
})
pitcher_by_date_selected_data <- reactive({
req(isTruthy(exact_pitcher_selection("Pitcher_dates")), input$Pitcher_game_dates)
filter_pitcher_game_data(
pitcher_by_date_data(),
input$Pitcher_game_dates,
pitcher_ids = exact_pitcher_ids("Pitcher_dates")
)
})
bullpen_selected_data <- reactive({
req(current_pitcher_selection("pens_pitcher"), input$pens_dates)
selected_ids <- current_pitcher_ids("pens_pitcher")
bullpens %>%
filter(
as.character(PitcherId) %in% selected_ids,
Date == input$pens_dates
)
})
sim_selected_data <- reactive({
req(current_pitcher_selection("sim_pitcher"), input$sim_pitcher_dates)
selected_ids <- current_pitcher_ids("sim_pitcher")
sim_games %>%
mutate(label = as.character(Date)) %>%
filter(
as.character(PitcherId) %in% selected_ids,
label %in% input$sim_pitcher_dates
)
})
outfield_oaa_all <- reactive({
query_outfield_oaa()
})
outfield_oaa_season <- reactive({
req(input$season_outfield)
query_outfield_oaa(input$season_outfield)
})
infield_oaa_all <- reactive({
query_infield_oaa()
})
infield_oaa_season <- reactive({
req(input$season_infield)
query_infield_oaa(input$season_infield)
})
output$login_ui <- renderUI({
if (!user_auth() || login_transition()) {
div(
class = paste("login-container", if (login_transition()) "login-container-exit"),
tags$style(HTML("
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Logo expands smoothly in one motion while moving to center */
@keyframes logoExpand {
from {
transform: translate(0, 0) scale(3);
opacity: 1;
}
to {
transform: translate(var(--move-x), var(--move-y)) scale(50);
opacity: 0;
}
}
/* Login fades out as logo expands */
@keyframes loginFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.login-container {
position: fixed;
inset: 0;
display: flex;
background: var(--white);
overflow: hidden;
}
.login-hero {
flex: 1;
position: relative;
overflow: hidden;
display: none;
}
@media (min-width: 900px) {
.login-hero { display: block; }
}
.login-hero-bg {
position: absolute;
inset: 0;
background-image: url('https://images.sidearmdev.com/crop?url=https%3a%2f%2fdxbhsrqyrr690.cloudfront.net%2fsidearm.nextgen.sites%2fwvuni.sidearmsports.com%2fimages%2f2025%2f2%2f18%2fWVU02765_large.jpg&height=1200&width=1600&type=webp');
background-size: cover;
background-position: center;
transform: scale(1.5);
transition: transform 8s ease-out;
}
.login-container:hover .login-hero-bg {
transform: scale(1);
}
.login-hero::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(0, 40, 85, 0.85) 0%,
rgba(0, 26, 58, 0.92) 100%
);
}
.login-form-panel {
flex: 0 0 520px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
background: white;
position: relative;
overflow: hidden;
}
@media (max-width: 899px) {
.login-form-panel {
flex: 1;
background-image: linear-gradient(
rgba(255, 255, 255, 0.95),
rgba(255, 255, 255, 0.95)
), url('https://images.sidearmdev.com/crop?url=https%3a%2f%2fdxbhsrqyrr690.cloudfront.net%2fsidearm.nextgen.sites%2fwvuni.sidearmsports.com%2fimages%2f2025%2f2%2f18%2fWVU02765_large.jpg&height=563&width=1000&type=webp');
background-size: cover;
background-position: center;
}
}
.login-form-panel::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(0, 40, 85, 0.03) 0%, transparent 70%);
pointer-events: none;
}
.login-form-inner {
max-width: 360px;
margin: 0 auto;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.login-branding {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 40px;
animation: fadeInUp 0.6s ease-out;
width: 100%;
}
.login-logo-img {
height: 70px;
width: auto;
filter: drop-shadow(0 4px 12px rgba(0, 40, 85, 0.15));
}
.login-branding-text {
border-left: 3px solid #EAAA00;
padding-left: 20px;
}
.login-branding-text h1 {
font-size: 22px;
font-weight: 700;
color: #002855;
margin: 0;
line-height: 1.2;
}
.login-form-group {
margin-bottom: 24px;
animation: fadeInUp 0.6s ease-out 0.2s both;
width: 100%;
}
.login-form-group .control-label {
display: none !important;
}
.login-form-group .shiny-input-container,
.login-form-group > div {
width: 100% !important;
}
.login-form-group .form-control {
width: 100%;
padding: 16px 18px !important;
border: 2px solid #e2e8f0 !important;
border-radius: 12px !important;
font-size: 16px !important;
font-family: inherit !important;
background: #f8fafc !important;
transition: all 0.2s ease !important;
height: 56px !important;
}
.login-form-group .form-control:focus {
border-color: #002855 !important;
background: white !important;
box-shadow: 0 0 0 4px rgba(0, 40, 85, 0.1) !important;
outline: none !important;
}
.login-btn-wrapper {
animation: fadeInUp 0.6s ease-out 0.3s both;
width: 100%;
}
.login-submit-btn {
width: 100%;
padding: 18px 24px !important;
background: linear-gradient(135deg, #002855 0%, #003875 100%) !important;
border: none !important;
border-radius: 12px !important;
color: white !important;
font-family: inherit !important;
font-size: 16px !important;
font-weight: 600 !important;
cursor: pointer !important;
position: relative;
overflow: hidden;
transition: all 0.3s ease !important;
height: 56px !important;
}
.login-submit-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255,255,255,0.2),
transparent
);
background-size: 200% 100%;
opacity: 0;
transition: opacity 0.3s ease;
}
.login-submit-btn:hover {
background: #EAAA00 !important;
color: #002855 !important;
transform: translateY(-2px) !important;
box-shadow: 0 10px 30px rgba(234, 170, 0, 0.4) !important;
}
.login-submit-btn:hover::before {
opacity: 1;
animation: shimmer 1.5s infinite;
}
.login-submit-btn:active {
transform: translateY(0) !important;
}
/* Expanding logo layer */
.logo-expand-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 4000;
display: none;
}
.logo-expand-layer img {
position: absolute;
transform-origin: center center;
will-change: transform, opacity;
}
.login-container-exit {
pointer-events: none;
}
.login-container-exit .login-hero,
.login-container-exit .login-form-panel {
animation: loginFadeOut .7s ease-out forwards;
}
.login-container-exit .logo-expand-layer {
display: block;
}
.login-container-exit .logo-expand-layer img {
animation: logoExpand 1s ease-in-out forwards;
}
")),
# Left side - Hero image with overlay
div(
class = "login-hero",
div(class = "login-hero-bg")
),
# Right side - Login form
div(
class = "login-form-panel",
div(
class = "login-form-inner",
# Branding
div(
class = "login-branding",
tags$img(
class = "login-logo-img",
src = "https://upload.wikimedia.org/wikipedia/commons/5/5d/WVU_flying_WV_Gold124.svg"
),
div(
class = "login-branding-text",
tags$h1("WVU Baseball")
)
),
# Password input
div(
class = "login-form-group",
passwordInput("password", label = NULL, placeholder = "Enter password")
),
# Submit button
div(
class = "login-btn-wrapper",
actionButton(
"login_btn",
"Sign In",
class = "login-submit-btn"
)
),
)
),
div(class = "logo-expand-layer"),
# Enter key handler
tags$script(HTML("
$(document).on('keypress', function(e) {
if (e.which == 13) { $('#login_btn').click(); }
});
")),
if (login_transition()) {
tags$script(HTML("
(function() {
var logo = document.querySelector('.login-branding .login-logo-img');
var expandLayer = document.querySelector('.logo-expand-layer');
if (logo && expandLayer) {
var rect = logo.getBoundingClientRect();
var clone = logo.cloneNode(true);
// Position clone at logo's current location
clone.style.left = rect.left + 'px';
clone.style.top = rect.top + 'px';
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
// Calculate movement to center
var logoCenterX = rect.left + rect.width / 2;
var logoCenterY = rect.top + rect.height / 2;
var screenCenterX = window.innerWidth / 2;
var screenCenterY = window.innerHeight / 2;
var moveX = screenCenterX - logoCenterX;
var moveY = screenCenterY - logoCenterY;
clone.style.setProperty('--move-x', moveX + 'px');
clone.style.setProperty('--move-y', moveY + 'px');
expandLayer.appendChild(clone);
logo.style.visibility = 'hidden';
}
setTimeout(function() {
Shiny.setInputValue('login_exit_done', Date.now(), {priority: 'event'});
}, 600);
})();
"))
} else {
NULL
}
)
} else {
NULL
}
})
observeEvent(input$login_btn, {
if (input$password == PASSWORD) {
login_success_pending(TRUE)
login_transition(TRUE)
} else {
showModal(modalDialog("Incorrect password", easyClose = TRUE))
}
})
observeEvent(input$login_exit_done, {
req(login_success_pending())
user_auth(TRUE)
login_success_pending(FALSE)
login_transition(FALSE)
})
output$app_ui <- renderUI({
if (user_auth()) {
div(
class = "app-slide-in",
tags$style(HTML("
@keyframes appReveal {
from { opacity: 0; }
to { opacity: 1; }
}
.app-slide-in {
animation: appReveal 0.5s ease-out forwards;
}
.selectize-input { background: rgba(255,255,255,0.9)!important; border:1px solid #ccc!important; color:#002855!important; }
.selectize-dropdown { background:#fff!important; border:1px solid #ccc!important; }
.selectize-dropdown .option {
background: #fff !important;
color: #002855 !important;
}
.selectize-dropdown .option:hover,
.selectize-dropdown .option.active {
background: #002855 !important;
color: #ffffff !important;
}
.selectize-dropdown .option:hover {
background: #EAAA00 !important;
color: #002855 !important;
}
.selectize-dropdown .option.selected {
background: #002855 !important;
color: #ffffff !important;
}
.nav-dropdown button:hover { background: rgba(255,255,255,0.1)!important; }
.nav-dropdown div button {
background: white !important;
color: #002855 !important;
}
.nav-dropdown div button:hover {
background: #EAAA00 !important;
color: #002855 !important;
}
#selection_options .selectize-control,
#selection_options .form-group {
margin:0!important; padding:0!important; min-width:50px; max-width:250px;
}
.nav-dropdown > button { text-align:left!important; }
#selection_options .form-group .control-label {
text-align:center!important; display:block; width:100%;
color: #EAAA00 !important;
}
.topbar-logo-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 12px;
padding: 0 6px;
line-height: 1;
}
.topbar-logo-img {
height: 38px;
width: auto;
display: block;
}
/* kill Bootstrap gutters so full-width works */
html, body { margin:0; padding:0; }
.container-fluid { padding-left:0!important; padding-right:0!important; }
")),
div(
id = "selection_shell",
style = "background: #002855; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 1100;",
div(
style = "background: #002855;
padding: 0px 0px;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px; /* adjust spacing between tabs */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
",
div(
class = "nav-dropdown",
style = "position: relative;",
actionButton("hitters_main", "Hitting",
style = "background: transparent; color: white; border: none; font-size: 16px; padding: 10px 15px; cursor: pointer;"),
div(
id = "hitters_submenu",
style = "position: absolute; top: 100%; left: 0; background: white; min-width: 150px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 5px; display: none; z-index: 1000;",
actionButton("hitters_summary", "Summary",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("hitters_game", "By Game",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("hitters_zones", "Zones",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("hitters_leaderboards", "Leaderboards",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("hitters_pitch_level_filters", "Pitch Level Filters",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("hitters_pitcher_level_filters", "Pitcher Level Filters",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
tags$script("
$('#hitters_summary, #hitters_game, #hitters_zones, #hitters_leaderboards, #hitters_pitch_level_filters, #hitters_pitcher_level_filters').hover(function() {
$(this).css('background-color', '#EAAA00');
}, function() {
$(this).css('background-color', 'white');
});
")
)
),
div(
class = "nav-dropdown",
style = "position: relative;",
actionButton("pitchers_main", "Pitching",
style = "background: transparent; color: white; border: none; font-size: 16px; padding: 10px 15px; cursor: pointer;"),
div(
id = "pitchers_submenu",
style = "position: absolute; top: 100%; left: 0; background: white; min-width: 150px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 5px; display: none; z-index: 1000;",
actionButton("pitchers_summary", "Summary",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("pitchers_by_date", "By Date",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("new_pitch", "Pitch Addition",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("stuffbymovement", "Ideal Movement",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("pitchers_leaders", "Leaderboards",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("pens", "Bullpens",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("sims", "Sim Games",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("pitchers_similarity", "Similarity",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
tags$script("
$('#pitchers_summary, #pitchers_by_date, #new_pitch, #stuffbymovement, #pitchers_leaders, #pens, #sims, #pitchers_similarity').hover(function() {
$(this).css('background-color', '#EAAA00');
}, function() {
$(this).css('background-color', 'white');
});
")
)
),
div(
class = "nav-dropdown",
style = "position: relative;",
actionButton("fielding_main", "Fielding",
style = "background: transparent; color: white; border: none; font-size: 16px; padding: 10px 15px; cursor: pointer;"),
div(
id = "fielding_submenu",
style = "position: absolute; top: 100%; left: 0; background: white; min-width: 150px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 5px; display: none; z-index: 1000;",
actionButton("catching_defense", "Catching",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("infield_defense", "Infield",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("outfield_defense", "Outfield",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("team_oaa", "Team OAA",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
tags$script("
$('#catching_defense, #infield_defense, #outfield_defense, #team_oaa').hover(function() {
$(this).css('background-color', '#EAAA00');
}, function() {
$(this).css('background-color', 'white');
});
")
)
),
div(
class = "nav-dropdown",
style = "position: relative;",
actionButton("glossary", "Glossary",
style = "background: transparent; color: white; border: none; font-size: 16px; padding: 10px 15px; cursor: pointer;")
),
div(
class = "nav-dropdown",
style = "position: relative;",
actionButton("scores_main", "Scores",
style = "background: transparent; color: white; border: none; font-size: 16px; padding: 10px 15px; cursor: pointer;"),
div(
id = "scores_submenu",
style = "position: absolute; top: 100%; left: 0; background: white; min-width: 150px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 5px; display: none; z-index: 1000;",
actionButton("scores_games", "Games",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
actionButton("scores_standings", "Standings",
style = "display: block; width: 100%; text-align: left; background: white; border: none; padding: 10px 15px; color: #002855; cursor: pointer;"),
tags$script("
$('#scores_games, #scores_standings').hover(function() {
$(this).css('background-color', '#EAAA00');
}, function() {
$(this).css('background-color', 'white');
});
")
)
),
div(
class = "topbar-logo-wrap",
tags$img(
class = "topbar-logo-img",
src = "https://upload.wikimedia.org/wikipedia/commons/5/5d/WVU_flying_WV_Gold124.svg",
alt = "WVU logo"
)
)
),
div(
id = "selection_options",
style = "
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 0;
margin: 0;
padding: 0;
",
conditionalPanel(
condition = "input.current_view == 'hitters_summary'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("player", "Batter",
choices = initial_hitter_choices,
selected = initial_hitter_default,
multiple = FALSE,
width = "150px"),
selectizeInput("season", "Season",
choices = get_available_seasons(),
selected = get_default_season(),
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'hitters_game'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("hitter_by_game", "Batter",
choices = initial_hitter_choices,
selected = initial_hitter_default,
width = "150px"),
selectInput("hitter_game_dates", "Game Date",
choices = NULL,
selected = NULL,
multiple = TRUE,
selectize = TRUE)
)
),
conditionalPanel(
condition = "input.current_view == 'hitters_zones'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("hitter_zone", "Batter",
choices = initial_hitter_choices,
selected = initial_hitter_default,
width = "150px"),
selectInput("stat_zones", "Stat",
choices = c("Avg EV", "wOBAcon", "xwOBAcon", "Contact %", "Hard Hit %",
"Swing Decision Runs Gained / 100"), selected = "xwOBAcon", width = "200px"),
selectInput("type_zones", "Pitch Type", choices = c("All", get_pitch_type_choices()), selected = "All", width = "150px"),
selectInput("side_zones", "Pitcher Side", choices = c("Any", "Right", "Left"), selected = "Any", width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'hitters_leaderboards'",
div(
style = "display: flex; gap: 10px;",
dateRangeInput("date_range_hitters",
"Date Range",
start = get_default_start_date(),
end = Sys.Date(),
format = "yyyy-mm-dd",
width = "300px"),
selectInput("table_type", "Select Table Type",
choices = c("Hitter Stats", "Hitter Stats by Pitch Group"),
selected = "Hitter Stats", width = "150px"),
selectInput("pthrows", "By Pitcher Hand",
choices = c("Any", "Right", "Left"),
selected = "Any", width = "100px"
),
numericInput("sample_pa",
"Min PA",
value = get_leaderboard_default("hitter_pa"),
min = 1,
max = 400,
step = 1,
width = "50px"
),
numericInput("bbe_hit",
"Min BBE",
value = get_leaderboard_default("hitter_bbe"),
min = 0,
max = 100,
step = 1,
width = "50px"
)
)
),
conditionalPanel(
condition = "input.current_view == 'hitters_pitch_level_filters'",
div(
style = "display: flex; gap: 10px;",
selectInput("side_filter", "Pitch Hand", choices = c("Any", "Right", "Left"), selected = "Any", width = "100px"),
selectInput("type_filter", "Pitch Type", choices = c("Any", get_pitch_type_choices()), selected = "Any", width = "100px"),
selectInput("loc_filter", "H/A", choices = c("Any", "Home", "Away"), selected = "Any", width = "100px"),
selectInput("hitter_location", "Pitch Loc", choices = c("Any", "Inside", "Outside", "Up", "Down"), selected = "Any", width = "100px"),
selectInput("filter_col1", "Stat", choices = c("Balls", "Strikes", "Outs", "Inning", "Velocity", "IVB", "HB", "Spin","RelHeight", "RelSide", "VAA"), selected = "Balls", width = "100px"),
selectInput("filter_op1", "Operator", choices = c("==", "!=", "<", ">", "<=", ">="), width = "100px"),
numericInput("filter_val1", "Value", value = NA, width = "100px")
)
),
conditionalPanel(
condition = "input.current_view == 'hitters_pitcher_level_filters'",
div(
style = "display: flex; gap: 10px;",
selectInput("side_filter2", "Pitch Hand", choices = c("Any", "Right", "Left"), selected = "Any", width = "100px"),
selectInput("type_filter2", "Pitch Type", choices = c("Any", get_pitch_type_choices()), selected = "Any", width = "100px"),
selectInput("loc_filter2", "H/A", choices = c("Any", "Home", "Away"), selected = "Any", width = "100px"),
selectInput("hitter_location2", "Pitch Loc", choices = c("Any", "Inside", "Outside", "Up", "Down"), selected = "Any", width = "100px"),
selectInput("filter_col2", "Stat", choices = c("Balls", "Strikes", "Outs", "Inning", "Velocity", "IVB", "HB", "Spin","RelHeight", "RelSide", "VAA"), selected = "Velocity", width = "100px"),
selectInput("filter_op2", "Operator", choices = c("==", "!=", "<", ">", "<=", ">="), selected = ">=", width = "100px"),
numericInput("filter_val2", "Value", value = NA, width = "100px")
)
),
conditionalPanel(
condition = "input.current_view == 'pitchers_summary'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("pitcher", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default,
width = "150px"),
selectizeInput("season_pitchers", "Season",
choices = get_available_seasons(),
selected = get_default_season(),
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'pitchers_by_date'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("Pitcher_dates", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default,
width = "150px"),
selectizeInput("Pitcher_game_dates", "Game Date",
choices = NULL,
selected = NULL,
multiple = FALSE,
width = "250px")
)
),
conditionalPanel(
condition = "input.current_view == 'new_pitch'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("new_pitch_pitcher", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default),
selectInput("new_pitch_pitcher_pitch_type", "Pitch Type",
choices = unique(stuff_means$TaggedPitchType),
selected = "Fastball", width = "125px"
),
numericInput("hb",
"HB",
value = 0,
min = -25,
max = 25,
step = 1,
width = "80px"
),
numericInput("ivb",
"IVB",
value = 0,
min = -25,
max = 25,
step = 1,
width = "80px"
),
numericInput("velo",
"Velocity",
value = 85,
min = 70,
max = 100,
step = 1,
width = "80px"
)
)
),
conditionalPanel(
condition = "input.current_view == 'stuffbymovement'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("stuff_pitcher", "Pitcher", choices = initial_pitcher_choices, selected = initial_pitcher_default, width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'pitchers_leaders'",
div(
style = "display: flex; gap: 10px;",
dateRangeInput("date_range_pitchers",
"Date Range",
start = get_default_start_date(),
end = Sys.Date(),
format = "yyyy-mm-dd",
width = "300px"),
selectInput("table", "Select Table Type",
choices = c("Pitcher Stats", "Pitcher Stats by Pitch Type"),
selected = "Pitcher Stats", width = "150px"
),
selectInput("side", "By Batter Side",
choices = c("Any", "Right", "Left"),
selected = "Any", width = "100px"
),
numericInput("sample",
"Min P",
value = get_leaderboard_default("pitcher_p"),
min = 10,
max = 1000,
step = 1, width = "50px"
),
numericInput("bbe",
"Min BBE",
value = get_leaderboard_default("pitcher_bbe"),
min = 0,
max = 100,
step = 1, width = "50px"
)
)
),
conditionalPanel(
condition = "input.current_view == 'pens'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("pens_pitcher", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default, width = "150px"),
selectizeInput("pens_dates", "Game Date",
choices = NULL,
selected = NULL,
multiple = FALSE,
width = "150px"),
downloadButton("downloadPlot", "Download")
)
),
conditionalPanel(
condition = "input.current_view == 'sims'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("sim_pitcher", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default,
width = "150px"),
selectizeInput("sim_pitcher_dates", "Date",
choices = NULL,
selected = NULL,
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'pitchers_similarity'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("similarity_pitcher", "Pitcher",
choices = initial_pitcher_choices,
selected = initial_pitcher_default,
width = "150px"),
selectizeInput("similarity_season", "Season",
choices = NULL,
selected = NULL,
width = "150px"),
selectizeInput("similarity_pitch_type", "Pitch Type",
choices = NULL,
selected = NULL,
width = "150px"),
selectInput("similarity_type", "Compare To",
choices = c("MLB" = "mlb", "WVU" = "wvu"),
selected = "mlb",
width = "120px"),
numericInput("similarity_top_n", "Top N",
value = 15,
min = 5,
max = 50,
width = "80px")
)
),
conditionalPanel(
condition = "input.current_view == 'catching_defense'",
div(
style = "display: flex; gap: 10px;",
selectizeInput("catcher", "Catcher",
choices = initial_catcher_choices,
selected = initial_catcher_default,
width = "150px"),
selectizeInput("season_catchers", "Season",
choices = get_available_seasons(),
selected = get_default_season(),
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'outfield_defense'",
div(
style = "display: flex; gap: 10px;",
selectInput("outfielder", "Fielder",
choices = outfielders, selected = default_outfielder_selection, width = "250px"
),
selectizeInput("season_outfield", "Season",
choices = get_available_seasons(),
selected = get_default_season(),
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'infield_defense'",
div(
style = "display: flex; gap: 10px;",
selectInput(
"infielder", "Fielder",
choices = infielders,
selected = default_infielder_selection, width = "250px"
),
selectizeInput("season_infield", "Season",
choices = get_available_seasons(),
selected = get_default_season(),
multiple = FALSE,
width = "150px")
)
),
conditionalPanel(
condition = "input.current_view == 'scores'",
div(
id = "scores_controls",
style = "display:flex; gap:10px; align-items:flex-end;",
tags$style(HTML("
#scores_controls .form-group {
width: 160px !important;
margin-bottom: 0 !important;
}
#scores_controls .form-control,
#scores_controls .selectize-input,
#scores_controls .btn {
height: 40px !important;
min-height: 40px !important;
}
#scores_controls .selectize-input {
display: flex !important;
align-items: center !important;
}
#scores_controls .input-group-addon,
#scores_controls .input-group-btn .btn {
height: 40px !important;
}
#scores_controls .btn {
width: 160px !important;
}
")),
dateInput(
"scores_date", "Date",
value = Sys.Date(),
format = "yyyy-mm-dd",
width = "160px"
),
selectInput(
"scores_view", "Games",
choices = c(
"All Games" = "all",
"T25" = "t25",
conference_game_choices
),
selected = "all",
width = "160px"
),
div(
class = "form-group",
style = "width:160px; margin-bottom:0;",
tags$label("\u00A0"),
actionButton("refresh_scores", "Refresh", width = "160px")
)
)
),
conditionalPanel(
condition = "input.current_view == 'standings'",
div(
id = "standings_controls",
style = "display:flex; gap:10px; align-items:flex-end;",
tags$style(HTML("
#standings_controls .form-group {
width: 160px !important;
margin-bottom: 0 !important;
}
#standings_controls .form-control,
#standings_controls .selectize-input,
#standings_controls .btn {
height: 40px !important;
min-height: 40px !important;
}
#standings_controls .selectize-input {
display: flex !important;
align-items: center !important;
}
#standings_controls .btn {
width: 160px !important;
}
")),
selectInput(
"standings_conference", "Conference",
choices = c("All Conferences" = "all", conference_standings_choices),
selected = "Big 12",
width = "160px"
),
div(
class = "form-group",
style = "width:160px; margin-bottom:0;",
tags$label("\u00A0"),
actionButton("refresh_standings", "Refresh", width = "160px")
)
)
)
)
),
div(
id = "main_content",
style = "padding: 20px;",
conditionalPanel(
condition = "input.current_view == 'hitters_summary'",
fluidRow(
column(
width = 7,
plotOutput("val_percentiles", height = "180px"),
plotOutput("hit_percentiles", height = "560px"),
uiOutput("field_percentiles_ui")
),
column(
width = 5,
plotOutput("spray_chart", height = "410px"),
plotOutput("rolling_xwoba", height = "400px")
)
),
gt_output("hit_summary_overall"),
gt_output("hit_summary"),
gt_output("bb_profile"),
gt_output("discipline"),
plotOutput("row_1_hitter", height = "500px"),
plotOutput("contact", height = "400px")
),
conditionalPanel(
condition = "input.current_view == 'hitters_game'",
gt_output("hitter_stats_by_game")
),
conditionalPanel(
condition = "input.current_view == 'hitters_zones'",
plotOutput("hit_zones", height = "700px")
),
conditionalPanel(
condition = "input.current_view == 'hitters_leaderboards'",
DT::DTOutput("hitter_stats_table")
),
conditionalPanel(
condition = "input.current_view == 'hitters_pitch_level_filters'",
DT::DTOutput("filtered_hitter_table")
),
conditionalPanel(
condition = "input.current_view == 'hitters_pitcher_level_filters'",
DT::DTOutput("filtered_hitter_table_arsenal")
),
conditionalPanel(
condition = "input.current_view == 'pitchers_summary'",
fluidRow(
column(
width = 7,
plotOutput("val_percentiles_pitcher", height = "250px"),
plotOutput("pit_percentiles", height = "560px")
),
column(
width = 5,
plotOutput("short_form", height = "410px"),
plotOutput("rolling_xwoba_pit", height = "400px")
)
),
gt_output("summary_overall"),
gt_output("pitch_summary"),
plotOutput("row_1_pitcher", height = "400px"),
plotOutput("row_2_pitcher", height = "400px"),
plotOutput("pitch_locations", height = "410px"),
gt_output("pitch_summary_right"),
gt_output("pitch_summary_left"),
plotOutput("stuff_date", height = "400px")
),
conditionalPanel(
condition = "input.current_view == 'pitchers_by_date'",
gt_output("summary_dates"),
gt_output("summary_overall_dates"),
plotOutput("movement_dates"),
plotOutput("locs_game_l", height = "300px"),
plotOutput("locs_game_r", height = "300px"),
plotOutput("at_bats_game", height = "1200px")
),
conditionalPanel(
condition = "input.current_view == 'new_pitch'",
tagList(
fluidRow(
column(
width = 6,
gt_output("actual_pitch_stats")
),
column(
width = 6,
gt_output("new_pitch_stats"),
plotOutput("new_pitch_plot", height = "600px")
)
),
plotOutput("new_pitch_shap_plot", height = "500px")
)
),
conditionalPanel(
condition = "input.current_view == 'stuffbymovement'",
plotOutput("stuff_movement_plot", height = "900px")
),
conditionalPanel(
condition = "input.current_view == 'pitchers_leaders'",
DT::DTOutput("pitcher_stats_table")
),
conditionalPanel(
condition = "input.current_view == 'pens'",
gt_output("summary_dates_pen"),
plotOutput("movement_dates_pen"),
plotOutput("locs_pen", height = "300px")
),
conditionalPanel(
condition = "input.current_view == 'sims'",
gt_output("sim_summary"),
plotOutput("sim_movement"),
plotOutput("locs_sim_l", height = "300px"),
plotOutput("locs_sim_r", height = "300px")
),
conditionalPanel(
condition = "input.current_view == 'pitchers_similarity'",
h4("Pitch Comparables"),
gt_output("similarity_pitcher_info"),
DT::DTOutput("similarity_table")
),
conditionalPanel(
condition = "input.current_view == 'catching_defense'",
plotOutput("framing_plot", height = "900px")
),
conditionalPanel(
condition = "input.current_view == 'outfield_defense'",
fluidRow(
column(
width = 6,
plotOutput("star_plot", height = "580px")
),
column(
width = 6,
plotlyOutput("all_catches", height = "290px"),
plotOutput("direction", height = "290px")
)
),
fluidRow(
column(
width = 12,
gt_output("star_table")
)
),
DT::DTOutput("ind_plays_oaa")
),
conditionalPanel(
condition = "input.current_view == 'infield_defense'",
plotOutput("star_plot_inf", height = "600px", width = "950px"),
gt_output("star_table_inf"),
DT::DTOutput("ind_plays_oaa_inf")
),
conditionalPanel(
condition = "input.current_view == 'team_oaa'",
DT::DTOutput("team_oaa_table")
),
conditionalPanel(
condition = "input.current_view == 'scores'",
tags$style(HTML("
.scores-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-auto-flow: row;
align-content: start;
gap: 12px;
width: 100%;
}
.score-card {
background: #ffffff;
border: 1px solid #d7deea;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 2px rgba(0, 40, 85, 0.08);
}
.score-header {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
margin-bottom: 10px;
}
.score-status {
display: inline-flex;
align-items: center;
line-height: 1;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
border-radius: 999px;
padding: 3px 8px;
margin-bottom: 0;
}
.score-status--final {
background: #dcfce7;
color: #166534;
}
.score-status--live {
background: #fee2e2;
color: #b91c1c;
}
.score-status--upcoming {
background: #fff4cc;
color: #7a5a00;
}
.score-status--canceled {
background: #eceff3;
color: #4b5563;
}
.score-tag {
display: inline-flex;
align-items: center;
line-height: 1;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
border-radius: 999px;
padding: 3px 8px;
}
.score-tag--late {
background: #ffedd5;
color: #c2410c;
}
.score-tag--close {
background: #fef3c7;
color: #92400e;
}
.score-lines {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: stretch;
gap: 10px;
margin: 4px 0;
}
.score-lines__teams,
.score-lines__scores {
display: grid;
grid-template-rows: auto auto;
gap: 8px;
align-content: center;
}
.score-lines__inning {
display: flex;
align-items: center;
justify-content: center;
min-width: 38px;
}
.score-team, .score-score {
color: #111827;
font-size: 15px;
}
.score-inning {
color: #475569;
font-size: 15px;
font-weight: 700;
text-align: center;
white-space: nowrap;
}
.score-team-main {
display: inline-flex;
align-items: center;
gap: 6px;
}
.score-team-logo {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
}
.score-score {
font-weight: 700;
text-align: right;
}
.score-team--winner, .score-score--winner {
color: #15803d;
font-weight: 700;
}
.score-team--leader-live, .score-score--leader-live {
color: #1d4ed8;
font-weight: 700;
}
.score-detail {
margin-top: 8px;
color: #4b5563;
font-size: 12px;
}
.scores-empty {
background: #ffffff;
border: 1px solid #d7deea;
border-radius: 10px;
padding: 14px;
color: #334155;
}
.standings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
width: 100%;
}
.standings-card {
background: #ffffff;
border: 1px solid #d7deea;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 2px rgba(0, 40, 85, 0.08);
}
.standings-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
margin-bottom: 8px;
}
.standings-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.standings-table th,
.standings-table td {
padding: 6px 4px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
color: #0f172a;
vertical-align: middle;
}
.standings-table th {
font-weight: 700;
color: #334155;
}
.standings-table tr:last-child td {
border-bottom: none;
}
.standings-team-main {
display: inline-flex;
align-items: center;
gap: 6px;
}
.standings-team-logo {
width: 16px;
height: 16px;
object-fit: contain;
flex-shrink: 0;
}
@media (max-width: 1100px) {
.standings-grid {
grid-template-columns: minmax(0, 1fr);
}
}
")),
uiOutput("scores_cards")
),
conditionalPanel(
condition = "input.current_view == 'standings'",
uiOutput("standings_cards")
),
conditionalPanel(
condition = "input.current_view == 'glossary'",
DT::DTOutput("glossary_table")
)
),
tags$script("
$(document).ready(function() {
// Hover functionality for dropdowns
$('.nav-dropdown').hover(function() {
$(this).find('div[id$=\"_submenu\"]').show();
}, function() {
$(this).find('div[id$=\"_submenu\"]').hide();
});
// Initialize current view
Shiny.setInputValue('current_view', 'hitters_summary');
});
// Handle submenu clicks
$('#hitters_summary').click(function() {
Shiny.setInputValue('current_view', 'hitters_summary');
$('#hitters_submenu').hide();
});
$('#hitters_game').click(function() {
Shiny.setInputValue('current_view', 'hitters_game');
$('#hitters_submenu').hide();
});
$('#hitters_zones').click(function() {
Shiny.setInputValue('current_view', 'hitters_zones');
$('#hitters_submenu').hide();
});
$('#hitters_leaderboards').click(function() {
Shiny.setInputValue('current_view', 'hitters_leaderboards');
$('#hitters_submenu').hide();
});
$('#hitters_pitch_level_filters').click(function() {
Shiny.setInputValue('current_view', 'hitters_pitch_level_filters');
$('#hitters_submenu').hide();
});
$('#hitters_pitcher_level_filters').click(function() {
Shiny.setInputValue('current_view', 'hitters_pitcher_level_filters');
$('#hitters_submenu').hide();
});
$('#pitchers_summary').click(function() {
Shiny.setInputValue('current_view', 'pitchers_summary');
$('#pitchers_submenu').hide();
});
$('#pitchers_by_date').click(function() {
Shiny.setInputValue('current_view', 'pitchers_by_date');
$('#pitchers_submenu').hide();
});
$('#new_pitch').click(function() {
Shiny.setInputValue('current_view', 'new_pitch');
$('#pitchers_submenu').hide();
});
$('#stuffbymovement').click(function() {
Shiny.setInputValue('current_view', 'stuffbymovement');
$('#pitchers_submenu').hide();
});
$('#pitchers_leaders').click(function() {
Shiny.setInputValue('current_view', 'pitchers_leaders');
$('#pitchers_submenu').hide();
});
$('#pens').click(function() {
Shiny.setInputValue('current_view', 'pens');
$('#pitchers_submenu').hide();
});
$('#sims').click(function() {
Shiny.setInputValue('current_view', 'sims');
$('#pitchers_submenu').hide();
});
$('#pitchers_similarity').click(function() {
Shiny.setInputValue('current_view', 'pitchers_similarity');
$('#pitchers_submenu').hide();
});
$('#catching_defense').click(function() {
Shiny.setInputValue('current_view', 'catching_defense');
$('#fielding_submenu').hide();
});
$('#outfield_defense').click(function() {
Shiny.setInputValue('current_view', 'outfield_defense');
$('#fielding_submenu').hide();
});
$('#infield_defense').click(function() {
Shiny.setInputValue('current_view', 'infield_defense');
$('#fielding_submenu').hide();
});
$('#pens').click(function() {
Shiny.setInputValue('current_view', 'pens');
$('#fielding_submenu').hide();
});
$('#team_oaa').click(function() {
Shiny.setInputValue('current_view', 'team_oaa');
$('#fielding_submenu').hide();
});
$('#glossary').click(function() {
Shiny.setInputValue('current_view', 'glossary');
});
$('#scores_main').click(function() {
Shiny.setInputValue('current_view', 'scores');
});
$('#scores_games').click(function() {
Shiny.setInputValue('current_view', 'scores');
$('#scores_submenu').hide();
});
$('#scores_standings').click(function() {
Shiny.setInputValue('current_view', 'standings');
$('#scores_submenu').hide();
});
")
)
}
})
output$field_percentiles_ui <- renderUI({
height_px <- "70px"
if (!is.null(current_hitter_selection("player")) && !is.null(input$season)) {
selected_ids <- current_hitter_ids("player")
field_row <- defense %>%
dplyr::filter(id %in% selected_ids, Season == input$season)
has_oaa <- nrow(field_row) > 0 && any(!is.na(field_row$OAA))
has_frv <- nrow(field_row) > 0 && any(!is.na(field_row$frv))
if (has_oaa && has_frv) {
height_px <- "100px"
}
}
plotOutput("field_percentiles", height = height_px)
})
output$val_percentiles <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$val_percentiles
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$hit_percentiles <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$hit_percentiles
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$field_percentiles <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$field_percentiles
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$spray_chart <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$spray_chart
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$rolling_xwoba <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$rolling_xwoba
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$radial <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$radial
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$hit_summary_overall <- bind_app_cache(
render_gt({
hitter_summary_bundle()$hit_summary_overall
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$hit_summary <- bind_app_cache(
render_gt({
hitter_summary_bundle()$hit_summary
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$bb_profile <- bind_app_cache(
render_gt({
hitter_summary_bundle()$bb_profile
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$discipline <- bind_app_cache(
render_gt({
hitter_summary_bundle()$discipline
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$row_1_hitter <- bind_app_cache(
renderPlot({
grid::grid.newpage()
grid::grid.draw(hitter_summary_bundle()$row_1_hitter)
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$contact <- bind_app_cache(
renderPlot({
hitter_summary_bundle()$contact
}),
player_scope(),
current_hitter_selection("player"),
input$season
)
output$pit_percentiles <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
pitcher_percentiles(
ctx$data,
ctx$name,
ctx$season,
pitcher_ids = ctx$ids
)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$val_percentiles_pitcher <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
value_percentiles_pitcher(
ctx$name,
pit_war,
ctx$data,
ctx$season,
pitcher_ids = ctx$ids
)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$short_form <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
movement(ctx$data, ctx$name)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$row_1_pitcher <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
p1 <- long_form(ctx$data, ctx$name)
p2 <- release_points(ctx$data, ctx$name)
p3 <- velo(ctx$data, ctx$name)
grid.arrange(p1, p2, p3, ncol = 3)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$row_2_pitcher <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
p1 <- count_usage(ctx$data, ctx$name)
p2 <- plot_pitch_type_freq(ctx$data, ctx$name)
p3 <- tilt(ctx$data, ctx$name)
grid.arrange(p1, p2, p3, ncol = 3)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$pitch_locations <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
locations(ctx$data, ctx$name)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$stuff_date <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
stuff_by_date(ctx$data, ctx$name)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$rolling_xwoba_pit <- bind_app_cache(
renderPlot({
ctx <- pitcher_summary_context()
rolling_xwOBA_pitcher(ctx$data, ctx$name)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$summary_overall <- bind_app_cache(
render_gt({
ctx <- pitcher_summary_context()
pitch_summary_overall(
ctx$data,
ctx$name,
ctx$season,
pitcher_stats,
pitcher_ids = ctx$ids
)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$pitch_summary <- bind_app_cache(
render_gt({
ctx <- pitcher_summary_context()
pitch_summary(ctx$data, ctx$name)
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$pitch_summary_right <- bind_app_cache(
render_gt({
ctx <- pitcher_summary_context()
pitch_summary_side(ctx$data, ctx$name, "Right")
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
output$pitch_summary_left <- bind_app_cache(
render_gt({
ctx <- pitcher_summary_context()
pitch_summary_side(ctx$data, ctx$name, "Left")
}),
player_scope(),
current_pitcher_selection("pitcher"),
input$season_pitchers
)
observeEvent(input$Pitcher_dates, {
req(isTruthy(exact_pitcher_selection("Pitcher_dates")))
available_games <- pitcher_by_date_game_choices()
game_choices <- stats::setNames(available_games$game_key, available_games$game_label)
selected_game <- if (nrow(available_games) == 0) {
character(0)
} else if (isTruthy(input$Pitcher_game_dates) && input$Pitcher_game_dates %in% available_games$game_key) {
input$Pitcher_game_dates
} else {
available_games$game_key[[1]]
}
updateSelectInput(
session,
"Pitcher_game_dates",
choices = game_choices,
selected = selected_game
)
})
observeEvent(input$Pitcher_game_dates, {
req(isTruthy(exact_pitcher_selection("Pitcher_dates")), input$Pitcher_game_dates)
selected_name <- exact_pitcher_name("Pitcher_dates")
filtered_df <- pitcher_by_date_selected_data()
output$movement_dates <- bind_app_cache(
renderPlot({
p1 <- movement(filtered_df, selected_name, FALSE)
p2 <- release_points(filtered_df, selected_name)
p3 <- velo(filtered_df, selected_name)
grid.arrange(p1, p2, p3, ncol = 3)
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates
)
output$summary_dates <- bind_app_cache(
render_gt({
pitch_summary(filtered_df, selected_name)
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates
)
output$summary_overall_dates <- bind_app_cache(
render_gt({
pitch_summary_game(filtered_df, selected_name)
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates
)
output$at_bats_game <- bind_app_cache(
renderPlot({
at_bats(filtered_df, selected_name)
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates
)
output$locs_game_l <- bind_app_cache(
renderPlot({
locations_game(filtered_df, selected_name, "Left")
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates,
"Left"
)
output$locs_game_r <- bind_app_cache(
renderPlot({
locations_game(filtered_df, selected_name, "Right")
}),
player_scope(),
exact_pitcher_selection("Pitcher_dates"),
input$Pitcher_game_dates,
"Right"
)
})
observeEvent(input$hitter_by_game, {
req(current_hitter_selection("hitter_by_game"))
available_games_hitters <- hitter_game_choices()
if (is.data.frame(available_games_hitters)) {
game_choices <- stats::setNames(available_games_hitters$game_key, available_games_hitters$game_label)
selected_game <- if (nrow(available_games_hitters) == 0) {
character(0)
} else {
available_games_hitters$game_key[[1]]
}
} else {
game_choices <- available_games_hitters
selected_game <- if (length(game_choices) == 0) character(0) else unname(game_choices[[1]])
}
updateSelectInput(
session,
"hitter_game_dates",
choices = game_choices,
selected = selected_game
)
})
observeEvent(input$hitter_game_dates, {
req(current_hitter_selection("hitter_by_game"), input$hitter_game_dates)
output$hitter_stats_by_game <- bind_app_cache(
render_gt({
hitter_game(
hitter_game_data(),
current_hitter_name("hitter_by_game"),
input$hitter_game_dates,
batter_ids = current_hitter_ids("hitter_by_game")
)
}),
player_scope(),
current_hitter_selection("hitter_by_game"),
input$hitter_game_dates
)
})
output$hit_zones <- bind_app_cache(
renderPlot({
req(current_hitter_selection("hitter_zone"), input$stat_zones)
zone_data <- hitter_zone_data()
selected_name <- current_hitter_name("hitter_zone")
p1 <- ev_map(zone_data, selected_name, input$stat_zones)
p2 <- zone_map(zone_data, selected_name, input$type_zones)
grid.arrange(p1, p2, ncol = 2)
}),
player_scope(),
current_hitter_selection("hitter_zone"),
input$stat_zones,
input$type_zones,
input$side_zones
)
observeEvent(
list(input$current_view, input$scores_date, input$refresh_scores),
{
req(identical(input$current_view, "scores"), input$scores_date)
scores <- fetch_espn_scoreboard(input$scores_date)
scores_data_rv(scores)
scores_error_rv(attr(scores, "fetch_error"))
base_choices <- c(
"All Games" = "all",
"T25" = "t25"
)
view_choices <- c(base_choices, conference_game_choices)
current_view <- input$scores_view %||% "all"
if (!(current_view %in% unname(view_choices))) {
current_view <- "all"
}
updateSelectInput(session, "scores_view", choices = view_choices, selected = current_view)
},
ignoreInit = FALSE
)
observeEvent(
list(input$current_view, input$refresh_standings),
{
req(identical(input$current_view, "standings"))
season_value <- suppressWarnings(as.integer(format(Sys.Date(), "%Y")))
standings <- fetch_d1baseball_standings(season = season_value)
if (nrow(standings) == 0) {
standings <- fetch_local_standings(season = season_value)
}
standings_data_rv(standings)
standings_error_rv(attr(standings, "fetch_error"))
choices <- c("All Conferences" = "all", conference_standings_choices)
selected <- input$standings_conference %||% "all"
if (!(selected %in% unname(choices))) {
selected <- "all"
}
updateSelectInput(session, "standings_conference", choices = choices, selected = selected)
},
ignoreInit = FALSE
)
output$scores_cards <- bind_app_cache(renderUI({
req(input$current_view == "scores", input$scores_date, input$scores_view)
scores <- scores_data_rv()
fetch_error <- scores_error_rv()
view_key <- input$scores_view %||% "all"
if (nrow(scores) > 0) {
if (view_key %in% c("t25", "T25")) {
scores <- scores %>% filter(top25_game)
} else if (startsWith(view_key, "conference::")) {
conference_name <- sub("^conference::", "", view_key)
scores <- scores %>% filter(away_conference == conference_name | home_conference == conference_name)
} else if (!(view_key %in% c("all", "All Games"))) {
scores <- scores %>% filter(away_conference == view_key | home_conference == view_key)
}
}
if (!("start_datetime_utc" %in% names(scores))) {
scores$start_datetime_utc <- NA_character_
}
if (!("start_time" %in% names(scores))) {
scores$start_time <- NA_character_
}
if (!("start_sort_key" %in% names(scores))) {
scores$start_sort_key <- NA_real_
}
if (!("start_time_minutes" %in% names(scores))) {
scores$start_time_minutes <- NA_real_
}
if (nrow(scores) == 0) {
message_text <- if (!is.null(fetch_error)) {
fetch_error
} else {
paste0("No games found for ", as.character(as.Date(input$scores_date)), ".")
}
return(div(class = "scores-empty", message_text))
}
cards <- lapply(seq_len(nrow(scores)), function(i) {
game <- scores[i, ]
is_live <- identical(game$status_category[[1]], "live")
away_is_winner <- identical(game$winner[[1]], "away")
home_is_winner <- identical(game$winner[[1]], "home")
away_is_live_leader <- is_live && !is.na(game$away_score[[1]]) && !is.na(game$home_score[[1]]) && game$away_score[[1]] > game$home_score[[1]]
home_is_live_leader <- is_live && !is.na(game$away_score[[1]]) && !is.na(game$home_score[[1]]) && game$home_score[[1]] > game$away_score[[1]]
away_school <- if ("away_school" %in% names(game) && !is.na(game$away_school[[1]]) && nzchar(game$away_school[[1]])) {
game$away_school[[1]]
} else {
game$away[[1]]
}
home_school <- if ("home_school" %in% names(game) && !is.na(game$home_school[[1]]) && nzchar(game$home_school[[1]])) {
game$home_school[[1]]
} else {
game$home[[1]]
}
away_record_suffix <- if ("away_record" %in% names(game) && !is.na(game$away_record[[1]]) && nzchar(game$away_record[[1]])) {
paste0(" (", game$away_record[[1]], ")")
} else {
""
}
home_record_suffix <- if ("home_record" %in% names(game) && !is.na(game$home_record[[1]]) && nzchar(game$home_record[[1]])) {
paste0(" (", game$home_record[[1]], ")")
} else {
""
}
away_label <- if (!is.na(game$away_rank[[1]])) {
paste0("#", game$away_rank[[1]], " ", away_school, away_record_suffix)
} else {
paste0(away_school, away_record_suffix)
}
home_label <- if (!is.na(game$home_rank[[1]])) {
paste0("#", game$home_rank[[1]], " ", home_school, home_record_suffix)
} else {
paste0(home_school, home_record_suffix)
}
away_name_node <- tags$span(
class = "score-team-main",
if (!is.na(game$away_logo[[1]]) && nzchar(game$away_logo[[1]])) {
tags$img(src = game$away_logo[[1]], class = "score-team-logo", alt = game$away[[1]])
} else {
NULL
},
tags$span(class = "score-team-text", away_label)
)
home_name_node <- tags$span(
class = "score-team-main",
if (!is.na(game$home_logo[[1]]) && nzchar(game$home_logo[[1]])) {
tags$img(src = game$home_logo[[1]], class = "score-team-logo", alt = game$home[[1]])
} else {
NULL
},
tags$span(class = "score-team-text", home_label)
)
inning_text <- if (!is.na(game$inning_display[[1]]) && nzchar(game$inning_display[[1]])) game$inning_display[[1]] else "-"
inning_arrow <- game$inning_arrow[[1]] %||% ""
inning_markup <- dplyr::case_when(
inning_arrow == "up" ~ paste0(inning_text, " &uarr;"),
inning_arrow == "down" ~ paste0(inning_text, " &darr;"),
TRUE ~ inning_text
)
game_start_time <- if ("start_time" %in% names(game) && !is.na(game$start_time[[1]])) game$start_time[[1]] else ""
game_location <- if ("location" %in% names(game) && !is.na(game$location[[1]])) game$location[[1]] else ""
footer_text <- dplyr::case_when(
game$status_category[[1]] == "upcoming" && nzchar(game_start_time) && nzchar(game_location) ~ paste(game_start_time, game_location, sep = " | "),
game$status_category[[1]] == "upcoming" && nzchar(game_start_time) ~ game_start_time,
nzchar(game_location) ~ game_location,
TRUE ~ ""
)
status_class <- paste("score-status", paste0("score-status--", game$status_category[[1]]))
live_tags <- list()
if (isTRUE(game$live_late[[1]])) {
live_tags <- append(live_tags, list(tags$span(class = "score-tag score-tag--late", "Late")))
}
if (isTRUE(game$live_close[[1]])) {
live_tags <- append(live_tags, list(tags$span(class = "score-tag score-tag--close", "Close")))
}
header <- do.call(
tags$div,
c(
list(class = "score-header", tags$span(class = status_class, game$status_label[[1]])),
live_tags
)
)
away_team_class <- dplyr::case_when(
away_is_winner ~ "score-team score-team--winner",
away_is_live_leader ~ "score-team score-team--leader-live",
TRUE ~ "score-team"
)
away_score_class <- dplyr::case_when(
away_is_winner ~ "score-score score-score--winner",
away_is_live_leader ~ "score-score score-score--leader-live",
TRUE ~ "score-score"
)
home_team_class <- dplyr::case_when(
home_is_winner ~ "score-team score-team--winner",
home_is_live_leader ~ "score-team score-team--leader-live",
TRUE ~ "score-team"
)
home_score_class <- dplyr::case_when(
home_is_winner ~ "score-score score-score--winner",
home_is_live_leader ~ "score-score score-score--leader-live",
TRUE ~ "score-score"
)
tags$div(
class = "score-card",
header,
tags$div(
class = "score-lines",
tags$div(
class = "score-lines__teams",
tags$span(class = away_team_class, away_name_node),
tags$span(class = home_team_class, home_name_node)
),
tags$div(
class = "score-lines__inning",
tags$span(class = "score-inning", HTML(inning_markup))
),
tags$div(
class = "score-lines__scores",
tags$span(class = away_score_class, game$away_score_display[[1]]),
tags$span(class = home_score_class, game$home_score_display[[1]])
)
),
if (nzchar(footer_text)) {
tags$div(class = "score-detail", footer_text)
} else {
NULL
}
)
})
tags$div(class = "scores-grid", cards)
}),
input$current_view,
input$scores_date,
input$scores_view,
input$refresh_scores
)
output$standings_cards <- bind_app_cache(renderUI({
req(input$current_view == "standings", input$standings_conference)
standings <- standings_data_rv()
standings_error <- standings_error_rv()
if (nrow(standings) == 0) {
message_text <- if (!is.null(standings_error)) {
if (identical(standings_error, "No standings found.")) {
paste0("No standings found for ", format(Sys.Date(), "%Y"), ".")
} else {
standings_error
}
} else {
"No standings data available."
}
return(div(class = "scores-empty", message_text))
}
selected_conf <- input$standings_conference %||% "all"
if (!identical(selected_conf, "all")) {
standings <- standings %>%
dplyr::filter(conference == selected_conf)
}
if (nrow(standings) == 0) {
return(div(class = "scores-empty", "No standings data for selected conference."))
}
conferences <- unique(standings$conference)
conferences <- conferences[!is.na(conferences) & nzchar(conferences)]
standings_cards <- lapply(conferences, function(conf_name) {
conf_tbl <- standings %>%
dplyr::filter(conference == conf_name) %>%
dplyr::arrange(display_rank, team)
rows <- lapply(seq_len(nrow(conf_tbl)), function(i) {
team_logo <- conf_tbl$team_logo[[i]]
team_name <- conf_tbl$team[[i]]
team_key <- normalize_team_name(team_name)
is_wvu <- team_key %in% c("west virginia", "west virginia mountaineers", "wvu")
row_style <- if (is_wvu) "background-color:#EAAA00;color:#002855;font-weight:700;" else NULL
conf_record <- conf_tbl$conference_record[[i]]
overall_record <- conf_tbl$overall_record[[i]]
conf_record <- if (!is.na(conf_record) && nzchar(conf_record)) conf_record else "-"
overall_record <- if (!is.na(overall_record) && nzchar(overall_record)) overall_record else "-"
team_node <- tags$span(
class = "standings-team-main",
if (!is.na(team_logo) && nzchar(team_logo)) {
tags$img(src = team_logo, class = "standings-team-logo", alt = team_name)
} else {
NULL
},
tags$span(team_name)
)
tags$tr(
style = row_style,
tags$td(conf_tbl$display_rank[[i]]),
tags$td(team_node),
tags$td(conf_record),
tags$td(overall_record)
)
})
tags$div(
class = "standings-card",
tags$div(class = "standings-title", conf_name),
tags$table(
class = "standings-table",
tags$thead(
tags$tr(
tags$th("Rk"),
tags$th("Team"),
tags$th("Conf"),
tags$th("Overall")
)
),
do.call(tags$tbody, rows)
)
)
})
tags$div(class = "standings-grid", standings_cards)
}),
input$current_view,
input$standings_conference,
input$refresh_standings
)
output$hitter_stats_table <- bind_app_cache(
DT::renderDT({
req(input$pthrows, input$table_type, input$sample_pa, input$bbe_hit)
df <- hitter_leaderboard_data()
table <- if (input$table_type == "Hitter Stats") {
hitter_table(df) %>%
filter(PA >= input$sample_pa, `Batted Balls` >= input$bbe_hit)
} else {
hitter_table_by_hand(df) %>%
filter(PA >= input$sample_pa, `Batted Balls` >= input$bbe_hit)
}
player_fixed_datatable(table, page_length = 20, filter = "top")
}),
player_scope(),
input$pthrows,
input$date_range_hitters,
input$table_type,
input$sample_pa,
input$bbe_hit
)
output$filtered_hitter_table <- bind_app_cache(
DT::renderDT({
req(input$filter_col1, input$filter_op1, input$filter_val1)
df <- hitter_filter_data()
col_map <- c(
"Balls" = "Balls",
"Strikes" = "Strikes",
"Outs" = "Outs",
"Inning" = "Inning",
"Velocity" = "RelSpeed",
"IVB" = "InducedVertBreak",
"HB" = "HorzBreak",
"Spin" = "SpinRate",
"RelHeight" = "RelHeight",
"RelSide" = "RelSide",
"VAA" = "VertApprAngle"
)
col <- col_map[[input$filter_col1]]
op <- input$filter_op1
val <- as.numeric(input$filter_val1)
expr_text <- paste0("`", col, "`", op,val)
expr <- parse(text = expr_text)
df_filtered <- df %>%
filter(eval(expr))
df_filtered <- df_filtered %>%
filter({
locs <- input$hitter_location
if (is.null(locs) || "Any" %in% locs) {
TRUE
} else {
conds <- list()
if ("Inside" %in% locs) {
conds <- append(conds, list(
(PlateLocSide > 0 & BatterSide == "Right") | (PlateLocSide < 0 & BatterSide == "Left")
))
}
if ("Outside" %in% locs) {
conds <- append(conds, list(
(PlateLocSide < 0 & BatterSide == "Right") | (PlateLocSide > 0 & BatterSide == "Left")
))
}
if ("Up" %in% locs) {
conds <- append(conds, list(PlateLocHeight > 30))
}
if ("Down" %in% locs) {
conds <- append(conds, list(PlateLocHeight <= 30))
}
Reduce(`&`, conds)
}
})
table_filt <- hitter_table_filter(df_filtered)
player_fixed_datatable(table_filt, page_length = 20, filter = "top")
}),
player_scope(),
input$side_filter,
input$type_filter,
input$loc_filter,
input$filter_col1,
input$filter_op1,
input$filter_val1,
input$hitter_location
)
output$filtered_hitter_table_arsenal <- bind_app_cache(
DT::renderDT({
req(input$filter_col2, input$filter_op2, input$filter_val2)
df <- hitter_arsenal_data()
col_map2 <- c(
"Velocity" = "RelSpeed",
"IVB" = "InducedVertBreak",
"HB" = "HorzBreak",
"Spin" = "SpinRate",
"RelHeight" = "RelHeight",
"RelSide" = "RelSide",
"VAA" = "VertApprAngle"
)
col <- col_map2[["Velocity"]]
op <- ">="
val <- as.numeric(90)
arsenal_summary <- df %>%
group_by(Pitcher, TaggedPitchType) %>%
summarise(across(all_of(unname(col_map2)), ~ mean(.x, na.rm = TRUE)), .groups = "drop")
expr_text <- paste0("`", col, "`", op,val)
expr <- parse(text = expr_text)
arsenal_summary <- arsenal_summary %>%
filter(eval(expr))
arsenal_pitchers <- unique(arsenal_summary$Pitcher)
df_filtered <- df %>%
filter(Pitcher %in% arsenal_pitchers)
df_filtered <- df_filtered %>%
filter({
locs <- input$hitter_location2
if (is.null(locs) || "Any" %in% locs) {
TRUE
} else {
conds <- list()
if ("Inside" %in% locs) {
conds <- append(conds, list(
(PlateLocSide > 0 & BatterSide == "Right") | (PlateLocSide < 0 & BatterSide == "Left")
))
}
if ("Outside" %in% locs) {
conds <- append(conds, list(
(PlateLocSide < 0 & BatterSide == "Right") | (PlateLocSide > 0 & BatterSide == "Left")
))
}
if ("Up" %in% locs) {
conds <- append(conds, list(PlateLocHeight > 30))
}
if ("Down" %in% locs) {
conds <- append(conds, list(PlateLocHeight <= 30))
}
Reduce(`&`, conds)
}
})
table_filt <- hitter_table_filter(df_filtered)
player_fixed_datatable(table_filt, page_length = 20, filter = "top")
}),
player_scope(),
input$side_filter2,
input$type_filter2,
input$loc_filter2,
input$filter_col2,
input$filter_op2,
input$filter_val2,
input$hitter_location2
)
output$actual_pitch_stats <- bind_app_cache(
render_gt({
actual_pitches_stuff(current_pitcher_name("new_pitch_pitcher"))
}),
player_scope(),
current_pitcher_selection("new_pitch_pitcher")
)
observeEvent(input$new_pitch_pitcher, {
req(current_pitcher_selection("new_pitch_pitcher"))
defaults <- get_new_pitch_defaults(current_pitcher_name("new_pitch_pitcher"))
updateSelectInput(
session,
"new_pitch_pitcher_pitch_type",
selected = defaults$pitch_type
)
updateNumericInput(session, "hb", value = defaults$hb)
updateNumericInput(session, "ivb", value = defaults$ivb)
updateNumericInput(session, "velo", value = defaults$velo)
}, ignoreInit = FALSE)
output$new_pitch_stats <- bind_app_cache(
render_gt({
new_pitch_stuff(current_pitcher_name("new_pitch_pitcher"), input$new_pitch_pitcher_pitch_type,
input$hb, input$ivb, input$velo)
}),
player_scope(),
current_pitcher_selection("new_pitch_pitcher"),
input$new_pitch_pitcher_pitch_type,
input$hb,
input$ivb,
input$velo
)
output$new_pitch_plot <- bind_app_cache(
renderPlot({
new_pitch_plot(current_pitcher_name("new_pitch_pitcher"), input$new_pitch_pitcher_pitch_type,
input$hb, input$ivb)
}),
player_scope(),
current_pitcher_selection("new_pitch_pitcher"),
input$new_pitch_pitcher_pitch_type,
input$hb,
input$ivb
)
output$new_pitch_shap_plot <- bind_app_cache(
renderPlot({
new_pitch_shap_plot(
current_pitcher_name("new_pitch_pitcher"),
input$new_pitch_pitcher_pitch_type,
input$hb,
input$ivb,
input$velo
)
}),
player_scope(),
current_pitcher_selection("new_pitch_pitcher"),
input$new_pitch_pitcher_pitch_type,
input$hb,
input$ivb,
input$velo
)
output$stuff_movement_plot <- bind_app_cache(
renderPlot({
stuff_movement(stuff_movement_data(), current_pitcher_name("stuff_pitcher"))
}),
player_scope(),
current_pitcher_selection("stuff_pitcher")
)
output$pitcher_stats_table <- bind_app_cache(
renderDT({
filtered_df <- pitcher_leaderboard_data()
if (input$table == "Pitcher Stats") {
table <- pitcher_table(filtered_df) %>%
filter(P >= input$sample & BBE >= input$bbe) %>%
mutate(IP = case_when(
abs(IP %% 1 - 0.33) < 0.01 ~ floor(IP) + 0.1,
abs(IP %% 1 - 0.67) < 0.01 ~ floor(IP) + 0.2,
TRUE ~ IP
))
player_fixed_datatable(table, page_length = 20, filter = "top")
} else {
table2 <- pitcher_table_by_hand(filtered_df) %>%
filter(P >= input$sample & BBE >= input$bbe) %>%
mutate(IP = case_when(
abs(IP %% 1 - 0.33) < 0.01 ~ floor(IP) + 0.1,
abs(IP %% 1 - 0.67) < 0.01 ~ floor(IP) + 0.2,
TRUE ~ IP
))
player_fixed_datatable(table2, page_length = 20, filter = "top")
}
}),
player_scope(),
input$date_range_pitchers,
input$side,
input$table,
input$sample,
input$bbe
)
output$framing_plot <- bind_app_cache(renderPlot({
req(input$season_catchers, input$catcher)
selected_season <- input$season_catchers
selected_catcher <- selected_catcher_name(input$catcher)
catcher_df <- catcher_view_data()
empty_plot <- function(msg = "No data available") {
ggplot() +
annotate("text", x = 0.5, y = 0.5, label = msg,
size = 6, color = wvu_blue, fontface = "bold") +
theme_void() +
theme(plot.background = element_rect(fill = "white", color = NA))
}
if (nrow(catcher_df) == 0) {
return(empty_plot(paste0("No data available for ", selected_catcher, "\nin ", selected_season)))
}
p1 <- tryCatch(framing_by_zone(catcher_df, selected_catcher), error = function(e) empty_plot("Framing data unavailable"))
p2 <- tryCatch(framing_by_zone(catcher_df, selected_catcher, TRUE), error = function(e) empty_plot("Framing data unavailable"))
p3 <- tryCatch(plot_catcher_times(catcher_df, selected_catcher), error = function(e) empty_plot("Pop time data unavailable"))
p4 <- tryCatch(plot_catcher_throws(catcher_df, selected_catcher), error = function(e) empty_plot("Throwing data unavailable"))
grid.arrange(p1, p2, p3, p4, ncol = 2)
}),
player_scope(),
input$season_catchers,
input$catcher
)
output$ind_plays_oaa <- bind_app_cache(DT::renderDT({
req(input$outfielder, input$season_outfield)
outfield_data <- outfield_oaa_season()
tryCatch(
player_fixed_datatable(get_oaa_table(outfield_data, input$outfielder), page_length = 20, filter = "top"),
error = function(e) {
player_fixed_datatable(
defense_error_table(defense_error_message(e)),
page_length = 1,
filter = "none"
)
}
)
}),
player_scope(),
input$outfielder,
input$season_outfield
)
output$star_plot <- bind_app_cache(renderPlot({
req(input$outfielder, input$season_outfield)
outfield_data <- outfield_oaa_season()
tryCatch(
star_graph(outfield_data, outfield_oaa_all(), input$outfielder),
error = function(e) defense_error_plot(defense_error_message(e))
)
}),
player_scope(),
input$outfielder,
input$season_outfield
)
output$star_table <- bind_app_cache(render_gt({
req(input$outfielder, input$season_outfield)
outfield_data <- outfield_oaa_season()
tryCatch(
oaa_table(outfield_data, input$outfielder),
error = function(e) gt::gt(defense_error_table(defense_error_message(e)))
)
}),
player_scope(),
input$outfielder,
input$season_outfield
)
output$direction <- bind_app_cache(renderPlot({
req(input$outfielder, input$season_outfield)
outfield_data <- outfield_oaa_season()
tryCatch(
direction_oaa(outfield_data, input$outfielder),
error = function(e) defense_error_plot(defense_error_message(e))
)
}),
player_scope(),
input$outfielder,
input$season_outfield
)
output$all_catches <- bind_app_cache(renderPlotly({
req(input$outfielder, input$season_outfield)
outfield_data <- outfield_oaa_season()
tryCatch(
show_all_catches(outfield_data, input$outfielder),
error = function(e) defense_error_plotly(defense_error_message(e))
)
}),
player_scope(),
input$outfielder,
input$season_outfield
)
output$star_plot_inf <- bind_app_cache(renderPlot({
req(input$infielder, input$season_infield)
infield_data <- infield_oaa_season()
tryCatch(
star_graph_inf(infield_data, infield_oaa_all(), input$infielder),
error = function(e) defense_error_plot(defense_error_message(e))
)
}),
player_scope(),
input$infielder,
input$season_infield
)
output$star_table_inf <- bind_app_cache(render_gt({
req(input$infielder, input$season_infield)
infield_data <- infield_oaa_season()
tryCatch(
oaa_table_inf(infield_data, input$infielder),
error = function(e) gt::gt(defense_error_table(defense_error_message(e)))
)
}),
player_scope(),
input$infielder,
input$season_infield
)
output$ind_plays_oaa_inf <- bind_app_cache(DT::renderDT({
req(input$infielder, input$season_infield)
infield_data <- infield_oaa_season()
tryCatch(
player_fixed_datatable(get_oaa_table_inf(infield_data, input$infielder), page_length = 20, filter = "top"),
error = function(e) {
player_fixed_datatable(
defense_error_table(defense_error_message(e)),
page_length = 1,
filter = "none"
)
}
)
}),
player_scope(),
input$infielder,
input$season_infield
)
output$team_oaa_table <- bind_app_cache(DT::renderDT({
t <- defense_summary %>%
filter(Name %in% active_team_oaa_names()) %>%
select(-any_of("Team")) %>%
mutate(
succ_rate = round(succ_rate, 2),
x_succ_rate = round(x_succ_rate, 2)
) %>%
rename(FRV = frv,
OAA = oaa,
`Success Rate` = succ_rate,
`X Success Rate` = x_succ_rate,
Position = position,
Plays = plays,
`Strikes Added` = strikes_added) %>%
select(-any_of("id"))
player_fixed_datatable(t, page_length = 20, filter = "top")
}),
player_scope()
)
output$glossary_table <- DT::renderDT({
player_fixed_datatable(glossary_df, page_length = nrow(glossary_df), filter = "none", rownames = FALSE)
})
observeEvent(input$pens_pitcher, {
req(current_pitcher_selection("pens_pitcher"))
selected_ids <- current_pitcher_ids("pens_pitcher")
available_dates_pens <- bullpens %>%
filter(as.character(PitcherId) %in% selected_ids) %>%
arrange(desc(Date)) %>%
pull(Date) %>%
unique()
updateSelectInput(
session,
"pens_dates",
choices = available_dates_pens,
selected = available_dates_pens[1]
)
})
observeEvent(input$pens_dates, {
req(current_pitcher_selection("pens_pitcher"), input$pens_dates)
selected_name <- current_pitcher_name("pens_pitcher")
filtered_pens <- bullpen_selected_data()
output$movement_dates_pen <- bind_app_cache(
renderPlot({
p1 <- movement(filtered_pens, selected_name, FALSE)
p2 <- release_points(filtered_pens, selected_name)
p3 <- velo(filtered_pens, selected_name)
grid.arrange(p1, p2, p3, ncol = 3)
}),
player_scope(),
current_pitcher_selection("pens_pitcher"),
input$pens_dates
)
output$summary_dates_pen <- bind_app_cache(
render_gt({
pitch_summary_bullpens(filtered_pens, selected_name, 1200)
}),
player_scope(),
current_pitcher_selection("pens_pitcher"),
input$pens_dates
)
output$locs_pen <- bind_app_cache(
renderPlot({
locations_game(filtered_pens, selected_name)
}),
player_scope(),
current_pitcher_selection("pens_pitcher"),
input$pens_dates
)
})
output$downloadPlot <- downloadHandler(
filename = function() {
player <- gsub(" ", "_", current_pitcher_name("pens_pitcher"))
dates <- gsub("[^0-9]", "", paste(input$pens_dates, collapse = "_"))
paste0(player, "_", dates, ".pdf")
},
content = function(file) {
pdf(file, width = 11, height = 8.5)
selected_ids <- current_pitcher_ids("pens_pitcher")
selected_name <- current_pitcher_name("pens_pitcher")
filteredpens <- bullpens %>%
filter(
as.character(PitcherId) %in% selected_ids,
Date == input$pens_dates
)
p1 <- movement(filteredpens, selected_name, FALSE)
p2 <- release_points(filteredpens, selected_name)
p3 <- velo(filteredpens, selected_name)
p4 <- locations_game(filteredpens, selected_name)
tbl <- pitch_summary_bullpens(filteredpens, selected_name, 900)
tmp <- tempfile(fileext = ".png")
gt::gtsave(tbl, tmp, expand = 6)
tbl_img <- png::readPNG(tmp)
tbl_g <- rasterGrob(tbl_img, interpolate = FALSE)
grid.arrange(tbl_g,
arrangeGrob(p1, p2, p3, ncol = 3),
p4,
nrow = 3,
heights = c(1, 2, 2)
)
dev.off()
}
)
observeEvent(input$sim_pitcher, {
req(current_pitcher_selection("sim_pitcher"))
selected_ids <- current_pitcher_ids("sim_pitcher")
available_dates_sims <- sim_games %>%
filter(as.character(PitcherId) %in% selected_ids) %>%
mutate(label = as.character(Date)) %>%
arrange(desc(Date)) %>%
pull(label) %>%
unique()
updateSelectizeInput(
session,
"sim_pitcher_dates",
choices = available_dates_sims,
selected = if (length(available_dates_sims)) available_dates_sims[1] else NULL,
server = TRUE
)
})
observeEvent(input$sim_pitcher_dates, ignoreInit = FALSE, {
req(current_pitcher_selection("sim_pitcher"), input$sim_pitcher_dates)
selected_name <- current_pitcher_name("sim_pitcher")
filtered_df_sims <- sim_selected_data()
output$sim_movement <- bind_app_cache(
renderPlot({
p1 <- movement(filtered_df_sims, selected_name, FALSE)
p2 <- release_points(filtered_df_sims, selected_name)
p3 <- velo(filtered_df_sims, selected_name)
grid.arrange(p1, p2, p3, ncol = 3)
}),
player_scope(),
current_pitcher_selection("sim_pitcher"),
input$sim_pitcher_dates
)
output$sim_summary <- bind_app_cache(
render_gt({
pitch_summary(filtered_df_sims, selected_name, sim = TRUE)
}),
player_scope(),
current_pitcher_selection("sim_pitcher"),
input$sim_pitcher_dates
)
output$locs_sim_l <- bind_app_cache(
renderPlot({
locations_game(filtered_df_sims, selected_name, "Left")
}),
player_scope(),
current_pitcher_selection("sim_pitcher"),
input$sim_pitcher_dates,
"Left"
)
output$locs_sim_r <- bind_app_cache(
renderPlot({
locations_game(filtered_df_sims, selected_name, "Right")
}),
player_scope(),
current_pitcher_selection("sim_pitcher"),
input$sim_pitcher_dates,
"Right"
)
})
# =============================================================================
# Pitch Similarity Tab
# =============================================================================
# Update seasons and pitch types when pitcher changes
observeEvent(input$similarity_pitcher, {
req(current_pitcher_selection("similarity_pitcher"))
selected_name <- current_pitcher_name("similarity_pitcher")
selected_ids <- current_pitcher_ids("similarity_pitcher")
seasons <- get_pitcher_seasons(selected_ids)
seasons_with_all <- c("All", seasons)
selected_season <- if (length(seasons) > 0) seasons[length(seasons)] else "All"
updateSelectizeInput(session, "similarity_season",
choices = seasons_with_all,
selected = selected_season)
# Also update pitch types directly (don't rely on season change cascade)
season_filter <- if (selected_season == "All") NULL else selected_season
pitch_types <- get_pitcher_pitch_types(selected_ids, season_filter)
updateSelectizeInput(session, "similarity_pitch_type",
choices = pitch_types,
selected = if (length(pitch_types) > 0) pitch_types[1] else NULL)
})
# Update pitch types when season changes
observeEvent(input$similarity_season, {
req(current_pitcher_selection("similarity_pitcher"), input$similarity_season)
selected_ids <- current_pitcher_ids("similarity_pitcher")
season_filter <- if (input$similarity_season == "All") NULL else input$similarity_season
pitch_types <- get_pitcher_pitch_types(selected_ids, season_filter)
updateSelectizeInput(session, "similarity_pitch_type",
choices = pitch_types,
selected = if (length(pitch_types) > 0) pitch_types[1] else NULL)
}, ignoreInit = TRUE)
# Selected pitcher info table
output$similarity_pitcher_info <- bind_app_cache(
render_gt({
req(current_pitcher_selection("similarity_pitcher"), input$similarity_pitch_type)
data <- similarity_pitcher_data()
data %>%
dplyr::summarise(
Pitcher = dplyr::first(Pitcher),
`Pitch Type` = dplyr::first(TaggedPitchType),
Season = if (!is.null(input$similarity_season) && input$similarity_season != "All") input$similarity_season else "All",
Pitches = dplyr::n(),
Velo = round(mean(RelSpeed, na.rm = TRUE), 1),
HB = round(mean(HorzBreak, na.rm = TRUE), 1),
IVB = round(mean(InducedVertBreak, na.rm = TRUE), 1),
`Arm Angle` = round(mean(arm_angle, na.rm = TRUE), 1)
) %>%
gt() %>%
wvu_gt_theme()
}),
player_scope(),
current_pitcher_selection("similarity_pitcher"),
input$similarity_pitch_type,
input$similarity_season
)
# Similarity results table
output$similarity_table <- bind_app_cache(
DT::renderDT({
req(current_pitcher_selection("similarity_pitcher"), input$similarity_pitch_type)
season_val <- if (is.null(input$similarity_season) || input$similarity_season == "All") NULL else input$similarity_season
result <- tryCatch({
if (input$similarity_type == "mlb") {
get_mlb_comparables(current_pitcher_name("similarity_pitcher"), input$similarity_pitch_type, season_val, input$similarity_top_n)
} else {
get_wvu_comparables(current_pitcher_name("similarity_pitcher"), input$similarity_pitch_type, season_val, input$similarity_top_n)
}
}, error = function(e) {
showNotification(paste("Error:", e$message), type = "error")
NULL
})
req(result)
DT::datatable(result, options = list(pageLength = 15, scrollX = TRUE, dom = 'tip'), rownames = FALSE)
}),
player_scope(),
current_pitcher_selection("similarity_pitcher"),
input$similarity_pitch_type,
input$similarity_season,
input$similarity_type,
input$similarity_top_n
)
}