TimStats commited on
Commit
6cff671
·
verified ·
1 Parent(s): ce28028

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +29 -3
  2. README.md +1 -1
  3. app.R +576 -48
  4. gitattributes +34 -0
  5. gitignore +1 -0
Dockerfile CHANGED
@@ -2,13 +2,39 @@ FROM rocker/r-base:latest
2
 
3
  WORKDIR /code
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  RUN install2.r --error \
 
 
 
6
  shiny \
7
  dplyr \
8
  ggplot2 \
9
  readr \
10
- ggExtra
11
-
 
 
 
 
 
 
 
 
 
 
12
  COPY . .
13
 
14
- CMD ["R", "--quiet", "-e", "shiny::runApp(host='0.0.0.0', port=7860)"]
 
2
 
3
  WORKDIR /code
4
 
5
+ # Install system dependencies for magick and curl
6
+ RUN apt-get update && apt-get install -y \
7
+ libmagick++-dev \
8
+ imagemagick \
9
+ libcurl4-openssl-dev \
10
+ libssl-dev \
11
+ libfontconfig1-dev \
12
+ libfreetype6-dev \
13
+ libharfbuzz-dev \
14
+ libfribidi-dev \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Install R packages from CRAN - added httr
18
  RUN install2.r --error \
19
+ remotes \
20
+ curl \
21
+ httr \
22
  shiny \
23
  dplyr \
24
  ggplot2 \
25
  readr \
26
+ ggExtra \
27
+ patchwork \
28
+ gridExtra \
29
+ gtable
30
+
31
+ # Install magick separately to better handle any errors
32
+ RUN R -e 'install.packages("magick", repos="https://cloud.r-project.org")'
33
+
34
+ # Install showtext from GitHub after other dependencies are in place
35
+ RUN R -e 'remotes::install_github("yixuan/showtext")'
36
+
37
+ RUN apt-get update && apt-get install -y fonts-roboto
38
  COPY . .
39
 
40
+ CMD ["R", "--quiet", "-e", "shiny::runApp(host='0.0.0.0', port=7860)"]
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: MiLBSavant
3
  emoji: 📚
4
  colorFrom: blue
5
  colorTo: yellow
 
1
  ---
2
+ title: SavantGraphs
3
  emoji: 📚
4
  colorFrom: blue
5
  colorTo: yellow
app.R CHANGED
@@ -1,58 +1,586 @@
 
1
  library(shiny)
2
- library(bslib)
3
- library(dplyr)
4
  library(ggplot2)
 
 
 
 
 
 
 
 
5
 
6
- df <- readr::read_csv("penguins.csv")
7
- # Find subset of columns that are suitable for scatter plot
8
- df_num <- df |> select(where(is.numeric), -Year)
9
-
10
- ui <- page_sidebar(
11
- theme = bs_theme(bootswatch = "minty"),
12
- title = "Penguins explorer",
13
- sidebar = sidebar(
14
- varSelectInput("xvar", "X variable", df_num, selected = "Bill Length (mm)"),
15
- varSelectInput("yvar", "Y variable", df_num, selected = "Bill Depth (mm)"),
16
- checkboxGroupInput("species", "Filter by species",
17
- choices = unique(df$Species), selected = unique(df$Species)
18
- ),
19
- hr(), # Add a horizontal rule
20
- checkboxInput("by_species", "Show species", TRUE),
21
- checkboxInput("show_margins", "Show marginal plots", TRUE),
22
- checkboxInput("smooth", "Add smoother"),
23
- ),
24
- plotOutput("scatter")
25
- )
26
 
27
- server <- function(input, output, session) {
28
- subsetted <- reactive({
29
- req(input$species)
30
- df |> filter(Species %in% input$species)
31
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- output$scatter <- renderPlot(
34
- {
35
- p <- ggplot(subsetted(), aes(!!input$xvar, !!input$yvar)) +
36
- theme_light() +
37
- list(
38
- theme(legend.position = "bottom"),
39
- if (input$by_species) aes(color = Species),
40
- geom_point(),
41
- if (input$smooth) geom_smooth()
42
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- if (input$show_margins) {
45
- margin_type <- if (input$by_species) "density" else "histogram"
46
- p <- p |> ggExtra::ggMarginal(
47
- type = margin_type, margins = "both",
48
- size = 8, groupColour = input$by_species, groupFill = input$by_species
49
- )
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- p
53
- },
54
- res = 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
- shinyApp(ui, server)
 
 
1
+ # app.R
2
  library(shiny)
3
+ #library(tidyverse)
 
4
  library(ggplot2)
5
+ library(dplyr)
6
+ library(patchwork)
7
+ library(showtext)
8
+ library(magick)
9
+ library(grid)
10
+ library(gridExtra)
11
+ library(gtable)
12
+ library(httr)
13
 
14
+ # showtext_opts(dpi = 300) # Match plot DPI
15
+ # showtext_auto(enable = TRUE)
16
+ # font_add_google("Roboto Condensed", "roboto")
17
+ font_add_google("Roboto Condensed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ is_barrel <- function(df) {
20
+ df$hit_speedr <- round(df$hit_speed)
21
+ df <- df |>
22
+ mutate(barrel = ifelse((hit_speedr >= 124) &
23
+ (hit_angle >= 0 & hit_angle <= 50),1,0)) |>
24
+ mutate(barrel = ifelse((hit_speedr == 123) &
25
+ (hit_angle >= 1 & hit_angle <= 50),1,barrel)) |>
26
+ mutate(barrel = ifelse((hit_speedr == 122) &
27
+ (hit_angle >= 2 & hit_angle <= 50),1,barrel)) |>
28
+ mutate(barrel = ifelse((hit_speedr == 121) &
29
+ (hit_angle >= 3 & hit_angle <= 50),1,barrel)) |>
30
+ mutate(barrel = ifelse((hit_speedr == 120) &
31
+ (hit_angle >= 4 & hit_angle <= 50),1,barrel)) |>
32
+ mutate(barrel = ifelse((hit_speedr == 119) &
33
+ (hit_angle >= 5 & hit_angle <= 50),1,barrel)) |>
34
+ mutate(barrel = ifelse((hit_speedr == 118) &
35
+ (hit_angle >= 6 & hit_angle <= 50),1,barrel)) |>
36
+ mutate(barrel = ifelse((hit_speedr == 117) &
37
+ (hit_angle >= 7 & hit_angle <= 50),1,barrel)) |>
38
+ mutate(barrel = ifelse((hit_speedr == 116) &
39
+ (hit_angle >= 8 & hit_angle <= 50),1,barrel)) |>
40
+ mutate(barrel = ifelse((hit_speedr == 115) &
41
+ (hit_angle >= 9 & hit_angle <= 50),1,barrel)) |>
42
+ mutate(barrel = ifelse((hit_speedr == 114) &
43
+ (hit_angle >= 10 & hit_angle <= 50),1,barrel)) |>
44
+ mutate(barrel = ifelse((hit_speedr == 113) &
45
+ (hit_angle >= 11 & hit_angle <= 50),1,barrel)) |>
46
+ mutate(barrel = ifelse((hit_speedr == 112) &
47
+ (hit_angle >= 12 & hit_angle <= 50),1,barrel)) |>
48
+ mutate(barrel = ifelse((hit_speedr == 111) &
49
+ (hit_angle >= 13 & hit_angle <= 50),1,barrel)) |>
50
+ mutate(barrel = ifelse((hit_speedr == 110) &
51
+ (hit_angle >= 14 & hit_angle <= 48),1,barrel)) |>
52
+ mutate(barrel = ifelse((hit_speedr == 109) &
53
+ (hit_angle >= 15 & hit_angle <= 46),1,barrel)) |>
54
+ mutate(barrel = ifelse((hit_speedr == 108) &
55
+ (hit_angle >= 16 & hit_angle <= 45),1,barrel)) |>
56
+ mutate(barrel = ifelse((hit_speedr == 107) &
57
+ (hit_angle >= 17 & hit_angle <= 43),1,barrel)) |>
58
+ mutate(barrel = ifelse((hit_speedr == 106) &
59
+ (hit_angle >= 18 & hit_angle <= 42),1,barrel)) |>
60
+ mutate(barrel = ifelse((hit_speedr == 105) &
61
+ (hit_angle >= 19 & hit_angle <= 40),1,barrel)) |>
62
+ mutate(barrel = ifelse((hit_speedr == 104) &
63
+ (hit_angle >= 20 & hit_angle <= 39),1,barrel)) |>
64
+ mutate(barrel = ifelse((hit_speedr == 103) &
65
+ (hit_angle >= 21 & hit_angle <= 37),1,barrel)) |>
66
+ mutate(barrel = ifelse((hit_speedr == 102) &
67
+ (hit_angle >= 22 & hit_angle <= 36),1,barrel)) |>
68
+ mutate(barrel = ifelse((hit_speedr == 101) &
69
+ (hit_angle >= 23 & hit_angle <= 34),1,barrel)) |>
70
+ mutate(barrel = ifelse((hit_speedr == 100) &
71
+ (hit_angle >= 24 & hit_angle <= 33),1,barrel)) |>
72
+ mutate(barrel = ifelse((hit_speedr == 99) &
73
+ (hit_angle >= 25 & hit_angle <= 31),1,barrel)) |>
74
+ mutate(barrel = ifelse((hit_speedr == 98) &
75
+ (hit_angle >= 26 & hit_angle <= 30),1,barrel)) |>
76
+ select(-hit_speedr)
77
+ return(df)
78
+ }
79
 
80
+ apply_percentile_calcs <- function(data) {
81
+ # List of columns to apply percent_rank
82
+ percent_rank_cols <- c("Z-Con%", "Z-Swing%", "O-Con%", "Avg EV", "Max EV", "EV90", "Barrel%", "Swing%", "wOBA",
83
+ "wOBACON","xwOBA","xDamage")
84
+
85
+ # List of columns to apply inverse percent_rank
86
+ inverse_percent_rank_cols <- c("Chase%", "Whiff%", "stdev(LA)", "SwStr%")
87
+
88
+ # Create an empty list to store results
89
+ percentile_list <- list()
90
+
91
+ # Calculate regular percentiles
92
+ for(col in percent_rank_cols) {
93
+ percentile_list[[col]] <- data.frame(
94
+ `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name
95
+ `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name
96
+ metric = col,
97
+ percentile = round(percent_rank(data[[col]]) * 100),
98
+ value = data[[col]],
99
+ stringsAsFactors = FALSE
100
+ )
101
+ }
102
+
103
+ # Calculate inverse percentiles
104
+ for(col in inverse_percent_rank_cols) {
105
+ percentile_list[[col]] <- data.frame(
106
+ `Batter Name` = data[["Batter Name"]], # Using [[ ]] to preserve exact column name
107
+ `Batter ID` = data[["Batter ID"]], # Using [[ ]] to preserve exact column name
108
+ metric = col,
109
+ percentile = round((1 - percent_rank(data[[col]])) * 100),
110
+ value = data[[col]],
111
+ stringsAsFactors = FALSE
112
+ )
113
+ }
114
+
115
+ # Combine all results into one data frame
116
+ result <- do.call(rbind, percentile_list)
117
+
118
+ # Reset row names
119
+ rownames(result) <- NULL
120
+
121
+ return(result)
122
+ }
123
 
124
+ get_player_image <- function(player_id) {
125
+ # Try MLB silo image first
126
+ silo_url <- sprintf("https://img.mlbstatic.com/mlb-photos/image/upload/w_200,q_auto:best/v1/people/%s/headshot/silo/current", player_id)
127
+
128
+ # Check if silo works
129
+ silo_result <- tryCatch({
130
+ response <- httr::HEAD(silo_url)
131
+ httr::status_code(response) == 200
132
+ }, error = function(e) FALSE)
133
+
134
+ # If silo fails, use MiLB with correct formatting
135
+ if (!silo_result) {
136
+ 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))
137
+ }
138
+
139
+ # Return silo if it worked
140
+ return(silo_url)
141
+ }
142
+
143
+ get_player_info <- function(player_id, season, level = "MLB") {
144
+ # Initialize return values
145
+ team <- "MLB"
146
+ position <- NA
147
+
148
+ # If MLB level, use original endpoint
149
+ if(level == "MLB") {
150
+ url <- paste0("https://statsapi.mlb.com/api/v1/people/", player_id,
151
+ "/stats?stats=season&season=", season, "&group=hitting")
152
+
153
+ response <- httr::GET(url)
154
+ data <- httr::content(response, "parsed")
155
+
156
+ if(length(data$stats) > 0 && length(data$stats[[1]]$splits) > 0) {
157
+ team <- data$stats[[1]]$splits[[length(data$stats[[1]]$splits)]]$team$name
158
+ }
159
+ } else {
160
+ # For minor leagues, get player info from sports endpoint
161
+ sport_code <- if(level == "AAA") "11" else "14" # 11 for AAA, 14 for FSL
162
+ url <- paste0("https://statsapi.mlb.com/api/v1/sports/", sport_code, "/players?season=", season)
163
+
164
+ response <- httr::GET(url)
165
+ # Convert response to data frame
166
+ players_df <- jsonlite::fromJSON(rawToChar(response$content), flatten = TRUE)$people
167
+
168
+ # Find player directly
169
+ found_player <- players_df[players_df$id == player_id, ]
170
+
171
+ if(nrow(found_player) > 0) {
172
+ team_id <- found_player$currentTeam.id
173
+
174
+ # Get parent org using team id
175
+ team_url <- paste0("https://statsapi.mlb.com/api/v1/teams/", team_id, "?season=", season)
176
+ team_response <- httr::GET(team_url)
177
+ team_data <- jsonlite::fromJSON(rawToChar(team_response$content))
178
+
179
+ team <- team_data$teams$parentOrgName
180
+ }
181
+ }
182
+
183
+ # Get position info (same for all levels)
184
+ url2 <- paste0("https://statsapi.mlb.com/api/v1/people/", player_id)
185
+ response2 <- httr::GET(url2)
186
+ data2 <- httr::content(response2, "parsed")
187
+
188
+ if(length(data2$people) > 0) {
189
+ full_position <- data2$people[[1]]$primaryPosition$name
190
+ position <- case_when(
191
+ full_position == "First Base" ~ "1B",
192
+ full_position == "Second Base" ~ "2B",
193
+ full_position == "Third Base" ~ "3B",
194
+ full_position == "Shortstop" ~ "SS",
195
+ full_position == "Catcher" ~ "C",
196
+ full_position == "Left Field" ~ "LF",
197
+ full_position == "Center Field" ~ "CF",
198
+ full_position == "Right Field" ~ "RF",
199
+ full_position == "Outfielder" ~ "OF",
200
+ full_position == "Outfield" ~ "OF",
201
+ full_position == "Designated Hitter" ~ "DH",
202
+ full_position == "Pitcher" ~ "P",
203
+ full_position == "Two-Way Player" ~ "TWP",
204
+ TRUE ~ as.character(full_position)
205
+ )
206
+ }
207
+
208
+ return(list(
209
+ team = team,
210
+ position = position
211
+ ))
212
+ }
213
+ download_private_csv <- function(repo_id, filename) {
214
+ url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename)
215
+ response <- GET(url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
216
+
217
+ if (status_code(response) == 200) {
218
+ content <- content(response, "text")
219
+ con <- textConnection(content)
220
+
221
+ # Try different read options
222
+ data <- read.csv(con,
223
+ header = TRUE,
224
+ check.names = FALSE, # This prevents R from modifying column names
225
+ fileEncoding = "UTF-8",
226
+ stringsAsFactors = FALSE)
227
+ close(con)
228
+ return(data)
229
+ } else {
230
+ stop("Failed to download dataset")
231
+ }
232
+ }
233
+ MLB <- download_private_csv("TimStats/StatcastDataAll", "MLB.csv")
234
+ AAA <- download_private_csv("TimStats/StatcastDataAll", "AAA.csv")
235
+ FSLAll <- download_private_csv("TimStats/StatcastDataAll", "FSL.csv")
236
+
237
+ temp_players <- MLB %>% filter(season == 2024)
238
+ MLBC <- rbind(MLB,AAA,FSLAll)
239
+ data <- is_barrel(MLBC) %>%
240
+ mutate(
241
+ BBE = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE),
242
+ Swing = case_when(description %in% c('Foul','Foul Bunt','Foul Pitchout','Foul Tip',
243
+ 'In play, run(s)','In play, out(s)','In play, no out',
244
+ 'Swinging Strike','Swinging Strike (Blocked)',
245
+ 'Missed Bunt') ~ TRUE, TRUE ~ FALSE),
246
+ Contact = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out',
247
+ 'Foul','Foul Bunt','Foul Pitchout') ~ TRUE, TRUE ~ FALSE),
248
+ Whiff = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)',
249
+ 'Missed Bunt','Foul Tip') ~ TRUE, TRUE ~ FALSE),
250
+ IZ = ifelse(zone <= 9, TRUE, FALSE),
251
+ Single = case_when(result == "Single" & BBE == TRUE ~ TRUE, TRUE ~ FALSE),
252
+ Double = case_when(result == "Double" & BBE == TRUE ~ TRUE, TRUE ~ FALSE),
253
+ Triple = case_when(result == "Triple" & BBE == TRUE ~ TRUE, TRUE ~ FALSE),
254
+ `Home Run` = case_when(result == "Home Run" & BBE == TRUE ~ TRUE, TRUE ~ FALSE),
255
+ Walk = case_when(balls >= 4 & result == "Walk" ~ TRUE, TRUE ~ FALSE),
256
+ HBP = case_when(description == "Hit By Pitch" & result == "Hit By Pitch" ~ TRUE, TRUE ~ FALSE),
257
+ Strikeout = case_when(strikes >= 3 & result %in% c("Strikeout",'Stikeout Double Play') ~ TRUE, TRUE ~ FALSE),
258
+ Sac = case_when(BBE == TRUE & result %in% c('Sac Fly','Sac Bunt',
259
+ 'Sac Fly Double Play','Sac Bunt Double Play') ~ TRUE, TRUE ~ FALSE),
260
+ IBB = case_when(pitchNum == 1 & result == "Intent Walk" ~ TRUE, TRUE ~ FALSE),
261
+ AB = Strikeout + BBE - Sac,
262
+ PA = AB + Walk + HBP + IBB
263
+ ) %>%
264
+ group_by(`Batter Name`,`Batter ID`,season,level) %>%
265
+ summarise(
266
+ BIP = sum(BBE,na.rm = TRUE),
267
+ wOBA = round((sum(Single,na.rm = TRUE) * .882 + sum(Double, na.rm = TRUE) * 1.254 +
268
+ sum(Triple,na.rm = TRUE) * 1.59 + sum(`Home Run`,na.rm = TRUE) * 2.05 +
269
+ sum(Walk,na.rm = TRUE) * .689 + sum(HBP,na.rm = TRUE) * .720) /
270
+ (sum(PA,na.rm = TRUE) - sum(IBB,na.rm = TRUE)), 3),
271
+ wOBACON = round((sum(Single,na.rm = TRUE) * .882 + sum(Double, na.rm = TRUE) * 1.254 +
272
+ sum(Triple,na.rm = TRUE) * 1.59 + sum(`Home Run`,na.rm = TRUE) * 2.05 )/
273
+ sum(BBE,na.rm = TRUE), 3),
274
+ xwOBA = round(mean(expected_woba,na.rm = TRUE), 3),
275
+ xDamage = round(mean(expected_woba[BBE == TRUE],na.rm = TRUE), 3),
276
+ `Avg EV` = round(mean(hit_speed,na.rm = TRUE), 1),
277
+ EV90 = round(quantile(hit_speed,0.9,na.rm = TRUE), 1),
278
+ `Max EV` = round(max(hit_speed,na.rm = TRUE), 1),
279
+ 'stdev(LA)' = round(sd(hit_angle,na.rm = TRUE), 1),
280
+ 'Barrel%' = round(100 * mean(barrel[Swing == TRUE],na.rm = TRUE), 1),
281
+ "Z-Con%" = round(100 * mean(Contact[IZ == TRUE & Swing == TRUE],na.rm = TRUE), 1),
282
+ "Z-Swing%" = round(100 * mean(Swing[IZ == TRUE],na.rm = TRUE), 1),
283
+ "O-Con%" = round(100 * mean(Contact[IZ == FALSE & Swing == TRUE],na.rm = TRUE), 1),
284
+ "Chase%" = round(100 * mean(Swing[IZ == FALSE],na.rm = TRUE), 1),
285
+ "Whiff%" = round(100 * mean(Whiff[Swing == TRUE],na.rm = TRUE), 1),
286
+ "Swing%" = round(100 * mean(Swing,na.rm = TRUE), 1),
287
+ "SwStr%" = round(100 * mean(Whiff,na.rm = TRUE), 1)
288
+ )
289
 
290
+ # UI definition
291
+ ui <- fluidPage(
292
+ tags$head(
293
+ # Load Google Fonts via CSS
294
+ tags$link(href = "https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap",
295
+ rel = "stylesheet"),
296
+ tags$style(HTML("
297
+ /* Apply Roboto Condensed as default font */
298
+ * {
299
+ font-family: 'Roboto Condensed', sans-serif !important;
300
+ }
301
+ "))
302
+ ),
303
+ titlePanel(NULL, windowTitle = "Baseball Stats Visualization"),
304
+
305
+ sidebarLayout(
306
+ sidebarPanel(
307
+ selectInput("szn", "Season:", c(2024, 2023, 2022, 2021, 2021)),
308
+ selectInput("level", "Level:", c("MLB", "AAA", "FSL")),
309
+ selectInput("type", "Player Type:", c("Batter", "Pitcher")),
310
+ selectInput("player", "Player:", choices = unique(temp_players$`Batter Name`)),
311
+ # Add toggle for custom team
312
+ checkboxInput("use_custom_team", "Use Custom Team", FALSE),
313
+ # Conditional panel for team selection
314
+ conditionalPanel(
315
+ condition = "input.use_custom_team == true",
316
+ selectInput(
317
+ inputId = "team",
318
+ label = "Select Team",
319
+ choices = c(
320
+ # Regular teams (sorted alphabetically)
321
+ "Angels" = "LAA",
322
+ "Astros" = "HOU",
323
+ "Athletics" = "OAK",
324
+ "Blue Jays" = "TOR",
325
+ "Braves" = "ATL",
326
+ "Brewers" = "MIL",
327
+ "Cardinals" = "STL",
328
+ "Cubs" = "CHC",
329
+ "D-backs" = "ARI",
330
+ "Dodgers" = "LAD",
331
+ "Giants" = "SF",
332
+ "Guardians" = "CLE",
333
+ "Mariners" = "SEA",
334
+ "Marlins" = "MIA",
335
+ "Mets" = "NYM",
336
+ "Nationals" = "WSH",
337
+ "Orioles" = "BAL",
338
+ "Padres" = "SD",
339
+ "Phillies" = "PHI",
340
+ "Pirates" = "PIT",
341
+ "Rangers" = "TEX",
342
+ "Rays" = "TB",
343
+ "Red Sox" = "BOS",
344
+ "Reds" = "CIN",
345
+ "Rockies" = "COL",
346
+ "Royals" = "KC",
347
+ "Tigers" = "DET",
348
+ "Twins" = "MIN",
349
+ "White Sox" = "CHW",
350
+ "Yankees" = "NYY",
351
+ # MLB option at the top
352
+ "MLB" = "MLB"
353
+ ),
354
+ selected = "MLB"
355
+ )
356
+ ),
357
+ ),
358
+
359
+ mainPanel(
360
+ div(class = "plot-container",
361
+ plotOutput("statsPlot")
362
+ )
363
+ )
364
  )
365
+ )
366
+
367
+ # Server logic
368
+ server <- function(input, output,session) {
369
+
370
+ observeEvent(c(input$szn,input$level), {
371
+ # Filter data based on selected season
372
+ filtered_data <- MLBC[MLBC$season == input$szn & MLBC$level == input$level,]
373
+
374
+ updateSelectInput(session,
375
+ inputId = "player",
376
+ choices = unique(filtered_data$`Batter Name`))
377
+ })
378
+ # Create reactive value to store team
379
+ team_value <- reactiveVal("MLB")
380
+ position_value <- reactiveVal("")
381
+ # Watch for player or season changes to update team
382
+ observeEvent(c(input$player, input$szn), {
383
+ if (!input$use_custom_team && !is.null(input$player)) {
384
+
385
+ player_id <- MLBC %>%
386
+ filter(`Batter Name` == input$player) %>%
387
+ pull(`Batter ID`) %>%
388
+ unique() %>%
389
+ first()
390
+
391
+ if (!is.null(player_id)) {
392
+ player_info <- get_player_info(player_id, input$szn, input$level)
393
+
394
+ team_abb <- switch(player_info$team,
395
+ "Los Angeles Angels" = "LAA",
396
+ "Houston Astros" = "HOU",
397
+ "Oakland Athletics" = "OAK",
398
+ "Toronto Blue Jays" = "TOR",
399
+ "Atlanta Braves" = "ATL",
400
+ "Milwaukee Brewers" = "MIL",
401
+ "St. Louis Cardinals" = "STL",
402
+ "Chicago Cubs" = "CHC",
403
+ "Arizona Diamondbacks" = "ARI",
404
+ "Los Angeles Dodgers" = "LAD",
405
+ "San Francisco Giants" = "SF",
406
+ "Cleveland Guardians" = "CLE",
407
+ "Seattle Mariners" = "SEA",
408
+ "Miami Marlins" = "MIA",
409
+ "New York Mets" = "NYM",
410
+ "Washington Nationals" = "WSH",
411
+ "Baltimore Orioles" = "BAL",
412
+ "San Diego Padres" = "SD",
413
+ "Philadelphia Phillies" = "PHI",
414
+ "Pittsburgh Pirates" = "PIT",
415
+ "Texas Rangers" = "TEX",
416
+ "Tampa Bay Rays" = "TB",
417
+ "Boston Red Sox" = "BOS",
418
+ "Cincinnati Reds" = "CIN",
419
+ "Colorado Rockies" = "COL",
420
+ "Kansas City Royals" = "KC",
421
+ "Detroit Tigers" = "DET",
422
+ "Minnesota Twins" = "MIN",
423
+ "Chicago White Sox" = "CHW",
424
+ "New York Yankees" = "NYY",
425
+ "MLB")
426
+ if(is.na(team_abb)){
427
+ team_abb <- "MLB"
428
+ }
429
+ team_value(team_abb)
430
+ position_value(player_info$position)
431
+ }
432
+ }
433
+ })
434
+
435
+ current_team <- reactive({
436
+ if (input$use_custom_team) {
437
+ return(input$team)
438
+ } else {
439
+ return(team_value())
440
+ }
441
+ })
442
+
443
+ output$statsPlot <- renderPlot({
444
+ req(position_value())
445
+
446
+ # showtext::showtext_begin()
447
+ # on.exit(showtext::showtext_end())
448
+
449
+ data <- data %>% filter(season == input$szn,level == input$level)
450
+
451
+ BBE <- MLBC %>%
452
+ filter(season == input$szn) %>%
453
+ filter(level == input$level) %>%
454
+ filter(`Batter Name` == input$player) %>%
455
+ mutate(
456
+ BBE = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE)
457
+ )
458
+
459
+
460
+ indv <- data %>% filter(`Batter Name` == input$player,level == input$level)
461
+ #if(indv[1,5] >= 149){
462
+ qual <- data %>% filter(BIP > 249)
463
+ data <- rbind(indv,qual)
464
+ data <- unique(data)
465
+ #}
466
+ current_data <- apply_percentile_calcs(data %>% select(-BIP)) %>%
467
+ filter(`Batter.Name` == input$player) %>%
468
+ mutate(metric = factor(metric, levels = c(
469
+ "wOBA", "wOBACON", "xwOBA", "xDamage",
470
+ "Avg EV", "EV90", "Max EV",
471
+ "stdev(LA)", "Barrel%",
472
+ "Z-Con%", "Z-Swing%", "O-Con%",
473
+ "Chase%", "Whiff%", "Swing%", "SwStr%"
474
+ ))) %>%
475
+ arrange(metric)
476
+ #current_data <- data
477
+ pos <- position_value()
478
+
479
+ BBE <- sum(BBE$BBE,na.rm = TRUE)
480
+
481
+ # Add Roboto Condensed Condensed font
482
+ #font_add_google("Roboto Condensed Condensed", "Roboto Condensed")
483
+ #showtext_auto()
484
+
485
+ # Color function
486
+ current_data$color <- scales::gradient_n_pal(c("#325aa1","#90A4AE", "#D82129"))(current_data$percentile/100)
487
+
488
+ # Labels plot
489
+ labels_plot <- ggplot() +
490
+ annotate("text", x = c(10, 50, 90), y = 1.2,
491
+ label = c("Poor", "Average", "Great"),
492
+ color = c("#3661ad", "#90A4AE", "#DC3545"),
493
+ family = "Roboto Condensed", size = 6) +
494
+ annotate("text", x = c(10, 50, 90), y = .5,
495
+ label = "▲",
496
+ color = c("#3661ad", "#90A4AE", "#DC3545"), size = 12) +
497
+ scale_x_continuous(limits = c(-16, 113), expand = c(0, 0)) +
498
+ scale_y_continuous(limits = c(0.5, 1.5)) +
499
+ theme_void()
500
+
501
+ # Main plot
502
+ main_plot <- ggplot(current_data, aes(y = factor(metric, levels = rev(metric)))) +
503
+ geom_tile(aes(x = 50, width = 100),
504
+ fill = "#c7dcdc", alpha = 0.3, height = 0.25) +
505
+ geom_tile(aes(x = percentile/2, width = percentile, fill = color),
506
+ height = 0.7) +
507
+ annotate("segment", x = c(10, 50, 90), xend = c(10, 50, 90),
508
+ y = 0, yend = 16.35,
509
+ color = c("white"),
510
+ linewidth = 1.5, alpha = 0.5) +
511
+ geom_segment(aes(x = -2, xend = -16,
512
+ y = as.numeric(factor(metric, levels = rev(metric))) - 0.3,
513
+ yend = as.numeric(factor(metric, levels = rev(metric))) - 0.3),
514
+ linetype = "longdash", color = "#399098", size = 1) +
515
+ geom_segment(aes(x = 102, xend = 113,
516
+ y = as.numeric(factor(metric, levels = rev(metric))) - 0.3,
517
+ yend = as.numeric(factor(metric, levels = rev(metric))) - 0.3),
518
+ linetype = "longdash", color = "#399098", size = 1) +
519
+ geom_text(aes(x = -3, label = metric),
520
+ hjust = 1, size = 6, family = "Roboto Condensed") +
521
+ geom_text(aes(x = 103, label = value),
522
+ hjust = 0, size = 6, family = "Roboto Condensed") +
523
+ geom_point(aes(x = percentile, color = "white", fill = color),
524
+ size = 12, shape = 21, stroke = 3) +
525
+ geom_text(aes(x = percentile, label = percentile),
526
+ size = 5, color = "white", fontface = "bold", family = "Roboto Condensed") +
527
+ scale_x_continuous(limits = c(-16, 113), expand = c(0, 0)) +
528
+ scale_fill_identity() +
529
+ scale_color_identity() +
530
+ theme_minimal() +
531
+ theme(
532
+ axis.text = element_blank(),
533
+ axis.title = element_blank(),
534
+ panel.grid = element_blank(),
535
+ plot.margin = margin(t = 0, r = 0, b = -20, l = 0), # Reduced bottom margin
536
+ text = element_text(family = "Roboto Condensed")
537
+ )
538
+
539
+ # Load and process team logo
540
+ if(current_team() == "MLB"){
541
+ logo_url <- "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=400&h=400&transparent=true"
542
+ } else {
543
+ logo_url <- sprintf("https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/%s.png&h=200&w=200",
544
+ current_team())
545
+ }
546
+ logo_img <- image_read(logo_url)
547
+ logo_raster <- as.raster(logo_img)
548
+
549
+
550
+ #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"))
551
+ player_url <- get_player_image(current_data[1,2])
552
+ player_img <- image_read(player_url)
553
+ player_raster <- as.raster(player_img)
554
+ # Create title with logo
555
+ title_grob <- textGrob(paste0(input$player, " - ", pos,
556
+ "\n BBE - ", BBE, "\n",input$level,
557
+ " Percentile Rankings - ",input$szn),
558
+ gp = gpar(fontsize = 25, fontface = "bold",
559
+ fontfamily = "Roboto Condensed"))
560
+ logo_grob <- rasterGrob(logo_raster, x = 0, width = unit(.5, "npc"),hjust = 0)
561
+ player_grob <- rasterGrob(player_raster, x = 0.5, width = unit(.5, "npc"),hjust = 0)
562
+ title_with_logo <- arrangeGrob(logo_grob, title_grob,player_grob, ncol = 3,
563
+ widths = c(.25,.5,.25))
564
+ caption_grob <- textGrob("Viz by: @TimStats | tim-stats.com | Data: MLB",gp = gpar(fontsize = 15, fontface = "bold",
565
+ fontfamily = "Roboto Condensed"))
566
+
567
+ # Final arrangement with logo in title
568
+ final_plot <- grid.arrange(
569
+ title_with_logo,
570
+ labels_plot,
571
+ main_plot,
572
+ caption_grob,
573
+ heights = c(0.15, 0.05, 0.75, 0.05)
574
+ )
575
+
576
+ grid.arrange(
577
+ gtable_add_padding(
578
+ final_plot,
579
+ padding = unit(c(20, 20, 20, 20), "points") # top, right, bottom, left margins
580
+ )
581
+ )
582
+ }, height = 1000, width = 1000, res = 97,pointsize = 12)
583
  }
584
 
585
+ # Run the app
586
+ shinyApp(ui = ui, server = server)
gitattributes ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tflite filter=lfs diff=lfs merge=lfs -text
29
+ *.tgz filter=lfs diff=lfs merge=lfs -text
30
+ *.wasm filter=lfs diff=lfs merge=lfs -text
31
+ *.xz filter=lfs diff=lfs merge=lfs -text
32
+ *.zip filter=lfs diff=lfs merge=lfs -text
33
+ *.zst filter=lfs diff=lfs merge=lfs -text
34
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .DS_Store