Spaces:
Runtime error
Runtime error
| # app.R | |
| library(shiny) | |
| #library(tidyverse) | |
| library(ggplot2) | |
| library(dplyr) | |
| library(patchwork) | |
| library(showtext) | |
| library(magick) | |
| library(grid) | |
| library(gridExtra) | |
| library(gtable) | |
| library(httr) | |
| showtext_opts(dpi = 300) # Match plot DPI | |
| showtext_auto(enable = TRUE) | |
| font_add_google("Roboto Condensed", "roboto") | |
| is_barrel <- function(df) { | |
| df$hit_speedr <- round(df$hit_speed) | |
| df <- df |> | |
| mutate(barrel = ifelse((hit_speedr >= 124) & | |
| (hit_angle >= 0 & hit_angle <= 50),1,0)) |> | |
| mutate(barrel = ifelse((hit_speedr == 123) & | |
| (hit_angle >= 1 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 122) & | |
| (hit_angle >= 2 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 121) & | |
| (hit_angle >= 3 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 120) & | |
| (hit_angle >= 4 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 119) & | |
| (hit_angle >= 5 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 118) & | |
| (hit_angle >= 6 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 117) & | |
| (hit_angle >= 7 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 116) & | |
| (hit_angle >= 8 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 115) & | |
| (hit_angle >= 9 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 114) & | |
| (hit_angle >= 10 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 113) & | |
| (hit_angle >= 11 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 112) & | |
| (hit_angle >= 12 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 111) & | |
| (hit_angle >= 13 & hit_angle <= 50),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 110) & | |
| (hit_angle >= 14 & hit_angle <= 48),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 109) & | |
| (hit_angle >= 15 & hit_angle <= 46),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 108) & | |
| (hit_angle >= 16 & hit_angle <= 45),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 107) & | |
| (hit_angle >= 17 & hit_angle <= 43),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 106) & | |
| (hit_angle >= 18 & hit_angle <= 42),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 105) & | |
| (hit_angle >= 19 & hit_angle <= 40),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 104) & | |
| (hit_angle >= 20 & hit_angle <= 39),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 103) & | |
| (hit_angle >= 21 & hit_angle <= 37),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 102) & | |
| (hit_angle >= 22 & hit_angle <= 36),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 101) & | |
| (hit_angle >= 23 & hit_angle <= 34),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 100) & | |
| (hit_angle >= 24 & hit_angle <= 33),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 99) & | |
| (hit_angle >= 25 & hit_angle <= 31),1,barrel)) |> | |
| mutate(barrel = ifelse((hit_speedr == 98) & | |
| (hit_angle >= 26 & hit_angle <= 30),1,barrel)) |> | |
| select(-hit_speedr) | |
| return(df) | |
| } | |
| apply_percentile_calcs <- function(data) { | |
| # List of columns to apply percent_rank | |
| percent_rank_cols <- c("Z-Con%", "Z-Swing%", "O-Con%", "Avg EV", "Max EV", "EV90", "Barrel%", "Swing%", "wOBA", | |
| "wOBACON","xwOBA","xDamage") | |
| # List of columns to apply inverse percent_rank | |
| inverse_percent_rank_cols <- c("Chase%", "Whiff%", "stdev(LA)", "SwStr%") | |
| # Create an empty list to store results | |
| percentile_list <- list() | |
| # Calculate regular percentiles | |
| for(col in percent_rank_cols) { | |
| percentile_list[[col]] <- data.frame( | |
| `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name | |
| `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name | |
| metric = col, | |
| percentile = round(percent_rank(data[[col]]) * 100), | |
| value = data[[col]], | |
| stringsAsFactors = FALSE | |
| ) | |
| } | |
| # Calculate inverse percentiles | |
| for(col in inverse_percent_rank_cols) { | |
| percentile_list[[col]] <- data.frame( | |
| `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name | |
| `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name | |
| metric = col, | |
| percentile = round((1 - percent_rank(data[[col]])) * 100), | |
| value = data[[col]], | |
| stringsAsFactors = FALSE | |
| ) | |
| } | |
| # Combine all results into one data frame | |
| result <- do.call(rbind, percentile_list) | |
| # Reset row names | |
| rownames(result) <- NULL | |
| return(result) | |
| } | |
| get_player_image <- function(player_id) { | |
| # Try MLB silo image first | |
| silo_url <- sprintf("https://img.mlbstatic.com/mlb-photos/image/upload/w_200,q_auto:best/v1/people/%s/headshot/silo/current", player_id) | |
| # Check if silo works | |
| silo_result <- tryCatch({ | |
| response <- httr::HEAD(silo_url) | |
| httr::status_code(response) == 200 | |
| }, error = function(e) FALSE) | |
| # If silo fails, use MiLB with correct formatting | |
| if (!silo_result) { | |
| return(sprintf("https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto,b_white,ar_1:1/w_180/v1/people/%s/headshot/milb/current", player_id)) | |
| } | |
| # Return silo if it worked | |
| return(silo_url) | |
| } | |
| get_player_info <- function(player_id, season, level = "MLB") { | |
| # Initialize return values | |
| team <- "MLB" | |
| position <- NA | |
| # If MLB level, use original endpoint | |
| if(level == "MLB") { | |
| url <- paste0("https://statsapi.mlb.com/api/v1/people/", player_id, | |
| "/stats?stats=season&season=", season, "&group=hitting") | |
| response <- httr::GET(url) | |
| data <- httr::content(response, "parsed") | |
| if(length(data$stats) > 0 && length(data$stats[[1]]$splits) > 0) { | |
| team <- data$stats[[1]]$splits[[length(data$stats[[1]]$splits)]]$team$name | |
| } | |
| } else { | |
| # For minor leagues, get player info from sports endpoint | |
| sport_code <- if(level == "AAA") "11" else "14" # 11 for AAA, 14 for FSL | |
| url <- paste0("https://statsapi.mlb.com/api/v1/sports/", sport_code, "/players?season=", season) | |
| response <- httr::GET(url) | |
| # Convert response to data frame | |
| players_df <- jsonlite::fromJSON(rawToChar(response$content), flatten = TRUE)$people | |
| # Find player directly | |
| found_player <- players_df[players_df$id == player_id, ] | |
| if(nrow(found_player) > 0) { | |
| team_id <- found_player$currentTeam.id | |
| # Get parent org using team id | |
| team_url <- paste0("https://statsapi.mlb.com/api/v1/teams/", team_id, "?season=", season) | |
| team_response <- httr::GET(team_url) | |
| team_data <- jsonlite::fromJSON(rawToChar(team_response$content)) | |
| team <- team_data$teams$parentOrgName | |
| } | |
| } | |
| # Get position info (same for all levels) | |
| url2 <- paste0("https://statsapi.mlb.com/api/v1/people/", player_id) | |
| response2 <- httr::GET(url2) | |
| data2 <- httr::content(response2, "parsed") | |
| if(length(data2$people) > 0) { | |
| full_position <- data2$people[[1]]$primaryPosition$name | |
| position <- case_when( | |
| full_position == "First Base" ~ "1B", | |
| full_position == "Second Base" ~ "2B", | |
| full_position == "Third Base" ~ "3B", | |
| full_position == "Shortstop" ~ "SS", | |
| full_position == "Catcher" ~ "C", | |
| full_position == "Left Field" ~ "LF", | |
| full_position == "Center Field" ~ "CF", | |
| full_position == "Right Field" ~ "RF", | |
| full_position == "Outfielder" ~ "OF", | |
| full_position == "Outfield" ~ "OF", | |
| full_position == "Designated Hitter" ~ "DH", | |
| full_position == "Pitcher" ~ "P", | |
| full_position == "Two-Way Player" ~ "TWP", | |
| TRUE ~ as.character(full_position) | |
| ) | |
| } | |
| return(list( | |
| team = team, | |
| position = position | |
| )) | |
| } | |
| download_private_csv <- function(repo_id, filename) { | |
| url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename) | |
| response <- GET(url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV")))) | |
| if (status_code(response) == 200) { | |
| content <- content(response, "text") | |
| con <- textConnection(content) | |
| # Try different read options | |
| data <- read.csv(con, | |
| header = TRUE, | |
| check.names = FALSE, # This prevents R from modifying column names | |
| fileEncoding = "UTF-8", | |
| stringsAsFactors = FALSE) | |
| close(con) | |
| return(data) | |
| } else { | |
| stop("Failed to download dataset") | |
| } | |
| } | |
| MLB <- download_private_csv("TimStats/StatcastDataAll", "MLB.csv") | |
| AAA <- download_private_csv("TimStats/StatcastDataAll", "AAA.csv") | |
| FSLAll <- download_private_csv("TimStats/StatcastDataAll", "FSL.csv") | |
| temp_players <- MLB %>% filter(season == 2024) | |
| MLBC <- rbind(MLB,AAA,FSLAll) | |
| data <- is_barrel(MLBC) %>% | |
| mutate( | |
| BBE = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE), | |
| Swing = case_when(description %in% c('Foul','Foul Bunt','Foul Pitchout','Foul Tip', | |
| 'In play, run(s)','In play, out(s)','In play, no out', | |
| 'Swinging Strike','Swinging Strike (Blocked)', | |
| 'Missed Bunt') ~ TRUE, TRUE ~ FALSE), | |
| Contact = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out', | |
| 'Foul','Foul Bunt','Foul Pitchout') ~ TRUE, TRUE ~ FALSE), | |
| Whiff = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)', | |
| 'Missed Bunt','Foul Tip') ~ TRUE, TRUE ~ FALSE), | |
| IZ = ifelse(zone <= 9, TRUE, FALSE), | |
| Single = case_when(result == "Single" & BBE == TRUE ~ TRUE, TRUE ~ FALSE), | |
| Double = case_when(result == "Double" & BBE == TRUE ~ TRUE, TRUE ~ FALSE), | |
| Triple = case_when(result == "Triple" & BBE == TRUE ~ TRUE, TRUE ~ FALSE), | |
| `Home Run` = case_when(result == "Home Run" & BBE == TRUE ~ TRUE, TRUE ~ FALSE), | |
| Walk = case_when(balls >= 4 & result == "Walk" ~ TRUE, TRUE ~ FALSE), | |
| HBP = case_when(description == "Hit By Pitch" & result == "Hit By Pitch" ~ TRUE, TRUE ~ FALSE), | |
| Strikeout = case_when(strikes >= 3 & result %in% c("Strikeout",'Stikeout Double Play') ~ TRUE, TRUE ~ FALSE), | |
| Sac = case_when(BBE == TRUE & result %in% c('Sac Fly','Sac Bunt', | |
| 'Sac Fly Double Play','Sac Bunt Double Play') ~ TRUE, TRUE ~ FALSE), | |
| IBB = case_when(pitchNum == 1 & result == "Intent Walk" ~ TRUE, TRUE ~ FALSE), | |
| AB = Strikeout + BBE - Sac, | |
| PA = AB + Walk + HBP + IBB | |
| ) %>% | |
| group_by(`Batter Name`,`Batter ID`,season,level) %>% | |
| summarise( | |
| BIP = sum(BBE,na.rm = TRUE), | |
| wOBA = round((sum(Single,na.rm = TRUE) * .882 + sum(Double, na.rm = TRUE) * 1.254 + | |
| sum(Triple,na.rm = TRUE) * 1.59 + sum(`Home Run`,na.rm = TRUE) * 2.05 + | |
| sum(Walk,na.rm = TRUE) * .689 + sum(HBP,na.rm = TRUE) * .720) / | |
| (sum(PA,na.rm = TRUE) - sum(IBB,na.rm = TRUE)), 3), | |
| wOBACON = round((sum(Single,na.rm = TRUE) * .882 + sum(Double, na.rm = TRUE) * 1.254 + | |
| sum(Triple,na.rm = TRUE) * 1.59 + sum(`Home Run`,na.rm = TRUE) * 2.05 )/ | |
| sum(BBE,na.rm = TRUE), 3), | |
| xwOBA = round(mean(expected_woba,na.rm = TRUE), 3), | |
| xDamage = round(mean(expected_woba[BBE == TRUE],na.rm = TRUE), 3), | |
| `Avg EV` = round(mean(hit_speed,na.rm = TRUE), 1), | |
| EV90 = round(quantile(hit_speed,0.9,na.rm = TRUE), 1), | |
| `Max EV` = round(max(hit_speed,na.rm = TRUE), 1), | |
| 'stdev(LA)' = round(sd(hit_angle,na.rm = TRUE), 1), | |
| 'Barrel%' = round(100 * mean(barrel[Swing == TRUE],na.rm = TRUE), 1), | |
| "Z-Con%" = round(100 * mean(Contact[IZ == TRUE & Swing == TRUE],na.rm = TRUE), 1), | |
| "Z-Swing%" = round(100 * mean(Swing[IZ == TRUE],na.rm = TRUE), 1), | |
| "O-Con%" = round(100 * mean(Contact[IZ == FALSE & Swing == TRUE],na.rm = TRUE), 1), | |
| "Chase%" = round(100 * mean(Swing[IZ == FALSE],na.rm = TRUE), 1), | |
| "Whiff%" = round(100 * mean(Whiff[Swing == TRUE],na.rm = TRUE), 1), | |
| "Swing%" = round(100 * mean(Swing,na.rm = TRUE), 1), | |
| "SwStr%" = round(100 * mean(Whiff,na.rm = TRUE), 1) | |
| ) | |
| # UI definition | |
| ui <- fluidPage( | |
| tags$head( | |
| tags$style(HTML(" | |
| .plot-container { | |
| width: 1000px !important; | |
| height: 1000px !important; | |
| } | |
| ")) | |
| ), | |
| titlePanel(NULL, windowTitle = "Baseball Stats Visualization"), | |
| sidebarLayout( | |
| sidebarPanel( | |
| selectInput("szn", "Season:", c(2024, 2023, 2022, 2021, 2021)), | |
| selectInput("level", "Level:", c("MLB", "AAA", "FSL")), | |
| selectInput("type", "Player Type:", c("Batter", "Pitcher")), | |
| selectInput("player", "Player:", choices = unique(temp_players$`Batter Name`)), | |
| # Add toggle for custom team | |
| checkboxInput("use_custom_team", "Use Custom Team", FALSE), | |
| # Conditional panel for team selection | |
| conditionalPanel( | |
| condition = "input.use_custom_team == true", | |
| selectInput( | |
| inputId = "team", | |
| label = "Select Team", | |
| choices = c( | |
| # Regular teams (sorted alphabetically) | |
| "Angels" = "LAA", | |
| "Astros" = "HOU", | |
| "Athletics" = "OAK", | |
| "Blue Jays" = "TOR", | |
| "Braves" = "ATL", | |
| "Brewers" = "MIL", | |
| "Cardinals" = "STL", | |
| "Cubs" = "CHC", | |
| "D-backs" = "ARI", | |
| "Dodgers" = "LAD", | |
| "Giants" = "SF", | |
| "Guardians" = "CLE", | |
| "Mariners" = "SEA", | |
| "Marlins" = "MIA", | |
| "Mets" = "NYM", | |
| "Nationals" = "WSH", | |
| "Orioles" = "BAL", | |
| "Padres" = "SD", | |
| "Phillies" = "PHI", | |
| "Pirates" = "PIT", | |
| "Rangers" = "TEX", | |
| "Rays" = "TB", | |
| "Red Sox" = "BOS", | |
| "Reds" = "CIN", | |
| "Rockies" = "COL", | |
| "Royals" = "KC", | |
| "Tigers" = "DET", | |
| "Twins" = "MIN", | |
| "White Sox" = "CHW", | |
| "Yankees" = "NYY", | |
| # MLB option at the top | |
| "MLB" = "MLB" | |
| ), | |
| selected = "MLB" | |
| ) | |
| ), | |
| ), | |
| mainPanel( | |
| div(class = "plot-container", | |
| plotOutput("statsPlot") | |
| ) | |
| ) | |
| ) | |
| ) | |
| # Server logic | |
| server <- function(input, output,session) { | |
| observeEvent(c(input$szn,input$level), { | |
| # Filter data based on selected season | |
| filtered_data <- MLBC[MLBC$season == input$szn & MLBC$level == input$level,] | |
| updateSelectInput(session, | |
| inputId = "player", | |
| choices = unique(filtered_data$`Batter Name`)) | |
| }) | |
| # Create reactive value to store team | |
| team_value <- reactiveVal("MLB") | |
| position_value <- reactiveVal("") | |
| # Watch for player or season changes to update team | |
| observeEvent(c(input$player, input$szn), { | |
| if (!input$use_custom_team && !is.null(input$player)) { | |
| player_id <- MLBC %>% | |
| filter(`Batter Name` == input$player) %>% | |
| pull(`Batter ID`) %>% | |
| unique() %>% | |
| first() | |
| if (!is.null(player_id)) { | |
| player_info <- get_player_info(player_id, input$szn, input$level) | |
| team_abb <- switch(player_info$team, | |
| "Los Angeles Angels" = "LAA", | |
| "Houston Astros" = "HOU", | |
| "Oakland Athletics" = "OAK", | |
| "Toronto Blue Jays" = "TOR", | |
| "Atlanta Braves" = "ATL", | |
| "Milwaukee Brewers" = "MIL", | |
| "St. Louis Cardinals" = "STL", | |
| "Chicago Cubs" = "CHC", | |
| "Arizona Diamondbacks" = "ARI", | |
| "Los Angeles Dodgers" = "LAD", | |
| "San Francisco Giants" = "SF", | |
| "Cleveland Guardians" = "CLE", | |
| "Seattle Mariners" = "SEA", | |
| "Miami Marlins" = "MIA", | |
| "New York Mets" = "NYM", | |
| "Washington Nationals" = "WSH", | |
| "Baltimore Orioles" = "BAL", | |
| "San Diego Padres" = "SD", | |
| "Philadelphia Phillies" = "PHI", | |
| "Pittsburgh Pirates" = "PIT", | |
| "Texas Rangers" = "TEX", | |
| "Tampa Bay Rays" = "TB", | |
| "Boston Red Sox" = "BOS", | |
| "Cincinnati Reds" = "CIN", | |
| "Colorado Rockies" = "COL", | |
| "Kansas City Royals" = "KC", | |
| "Detroit Tigers" = "DET", | |
| "Minnesota Twins" = "MIN", | |
| "Chicago White Sox" = "CHW", | |
| "New York Yankees" = "NYY", | |
| "MLB") | |
| if(is.na(team_abb)){ | |
| team_abb <- "MLB" | |
| } | |
| team_value(team_abb) | |
| position_value(player_info$position) | |
| } | |
| } | |
| }) | |
| current_team <- reactive({ | |
| if (input$use_custom_team) { | |
| return(input$team) | |
| } else { | |
| return(team_value()) | |
| } | |
| }) | |
| output$statsPlot <- renderPlot({ | |
| req(position_value()) | |
| # showtext::showtext_begin() | |
| # on.exit(showtext::showtext_end()) | |
| data <- data %>% filter(season == input$szn,level == input$level) | |
| BBE <- MLBC %>% | |
| filter(season == input$szn) %>% | |
| filter(level == input$level) %>% | |
| filter(`Batter Name` == input$player) %>% | |
| mutate( | |
| BBE = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE) | |
| ) | |
| indv <- data %>% filter(`Batter Name` == input$player,level == input$level) | |
| #if(indv[1,5] >= 149){ | |
| qual <- data %>% filter(BIP > 249) | |
| data <- rbind(indv,qual) | |
| data <- unique(data) | |
| #} | |
| current_data <- apply_percentile_calcs(data %>% select(-BIP)) %>% | |
| filter(`Batter.Name` == input$player) %>% | |
| mutate(metric = factor(metric, levels = c( | |
| "wOBA", "wOBACON", "xwOBA", "xDamage", | |
| "Avg EV", "EV90", "Max EV", | |
| "stdev(LA)", "Barrel%", | |
| "Z-Con%", "Z-Swing%", "O-Con%", | |
| "Chase%", "Whiff%", "Swing%", "SwStr%" | |
| ))) %>% | |
| arrange(metric) | |
| #current_data <- data | |
| pos <- position_value() | |
| BBE <- sum(BBE$BBE,na.rm = TRUE) | |
| # Add Roboto Condensed font | |
| #font_add_google("Roboto Condensed", "roboto") | |
| #showtext_auto() | |
| # Color function | |
| current_data$color <- scales::gradient_n_pal(c("#325aa1","#90A4AE", "#D82129"))(current_data$percentile/100) | |
| # Labels plot | |
| labels_plot <- ggplot() + | |
| annotate("text", x = c(10, 50, 90), y = 1.2, | |
| label = c("Poor", "Average", "Great"), | |
| color = c("#3661ad", "#90A4AE", "#DC3545"), | |
| family = "roboto", size = 6) + | |
| annotate("text", x = c(10, 50, 90), y = .5, | |
| label = "▲", | |
| color = c("#3661ad", "#90A4AE", "#DC3545"), size = 12) + | |
| scale_x_continuous(limits = c(-16, 113), expand = c(0, 0)) + | |
| scale_y_continuous(limits = c(0.5, 1.5)) + | |
| theme_void() | |
| # Main plot | |
| main_plot <- ggplot(current_data, aes(y = factor(metric, levels = rev(metric)))) + | |
| geom_tile(aes(x = 50, width = 100), | |
| fill = "#c7dcdc", alpha = 0.3, height = 0.25) + | |
| geom_tile(aes(x = percentile/2, width = percentile, fill = color), | |
| height = 0.7) + | |
| annotate("segment", x = c(10, 50, 90), xend = c(10, 50, 90), | |
| y = 0, yend = 16.35, | |
| color = c("white"), | |
| linewidth = 1.5, alpha = 0.5) + | |
| geom_segment(aes(x = -2, xend = -16, | |
| y = as.numeric(factor(metric, levels = rev(metric))) - 0.3, | |
| yend = as.numeric(factor(metric, levels = rev(metric))) - 0.3), | |
| linetype = "longdash", color = "#399098", size = 1) + | |
| geom_segment(aes(x = 102, xend = 113, | |
| y = as.numeric(factor(metric, levels = rev(metric))) - 0.3, | |
| yend = as.numeric(factor(metric, levels = rev(metric))) - 0.3), | |
| linetype = "longdash", color = "#399098", size = 1) + | |
| geom_text(aes(x = -3, label = metric), | |
| hjust = 1, size = 5, family = "roboto") + | |
| geom_text(aes(x = 103, label = value), | |
| hjust = 0, size = 5, family = "roboto") + | |
| geom_point(aes(x = percentile, color = "white", fill = color), | |
| size = 12, shape = 21, stroke = 3) + | |
| geom_text(aes(x = percentile, label = percentile), | |
| size = 5, color = "white", fontface = "bold", family = "roboto") + | |
| scale_x_continuous(limits = c(-16, 113), expand = c(0, 0)) + | |
| scale_fill_identity() + | |
| scale_color_identity() + | |
| theme_minimal() + | |
| theme( | |
| axis.text = element_blank(), | |
| axis.title = element_blank(), | |
| panel.grid = element_blank(), | |
| plot.margin = margin(t = 0, r = 0, b = -20, l = 0), # Reduced bottom margin | |
| text = element_text(family = "roboto") | |
| ) | |
| # Load and process team logo | |
| if(current_team() == "MLB"){ | |
| logo_url <- "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=400&h=400&transparent=true" | |
| } else { | |
| logo_url <- sprintf("https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/%s.png&h=200&w=200", | |
| current_team()) | |
| } | |
| logo_img <- image_read(logo_url) | |
| logo_raster <- as.raster(logo_img) | |
| #player_url <- sprintf(paste0("https://img.mlbstatic.com/mlb-photos/image/upload/d_headshot_silo_generic.png,ar_1:1,b_auto:border,c_pad,q_auto:best/w_60/v1/people/427012/headshot/milb/current")) | |
| player_url <- get_player_image(current_data[1,2]) | |
| player_img <- image_read(player_url) | |
| player_raster <- as.raster(player_img) | |
| # Create title with logo | |
| title_grob <- textGrob(paste0(input$player, " - ", pos, | |
| "\n BBE - ", BBE, "\n",input$level, | |
| " Percentile Rankings - ",input$szn), | |
| gp = gpar(fontsize = 25, fontface = "bold", | |
| fontfamily = "roboto")) | |
| logo_grob <- rasterGrob(logo_raster, x = 0, width = unit(.5, "npc"),hjust = 0) | |
| player_grob <- rasterGrob(player_raster, x = 0.5, width = unit(.5, "npc"),hjust = 0) | |
| title_with_logo <- arrangeGrob(logo_grob, title_grob,player_grob, ncol = 3, | |
| widths = c(.25,.5,.25)) | |
| caption_grob <- textGrob("Viz by: @TimStats | tim-stats.com | Data: MLB",gp = gpar(fontsize = 15, fontface = "bold", | |
| fontfamily = "roboto")) | |
| # Final arrangement with logo in title | |
| final_plot <- grid.arrange( | |
| title_with_logo, | |
| labels_plot, | |
| main_plot, | |
| caption_grob, | |
| heights = c(0.15, 0.05, 0.75, 0.05) | |
| ) | |
| grid.arrange( | |
| gtable_add_padding( | |
| final_plot, | |
| padding = unit(c(20, 20, 20, 20), "points") # top, right, bottom, left margins | |
| ) | |
| ) | |
| }, height = 1000, width = 1000, res = 97,pointsize = 12) | |
| } | |
| # Run the app | |
| shinyApp(ui = ui, server = server) |