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, " ↑"), inning_arrow == "down" ~ paste0(inning_text, " ↓"), 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 ) }