TimStats commited on
Commit
7e1e22e
·
verified ·
1 Parent(s): 435d655

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +374 -49
app.R CHANGED
@@ -1,58 +1,383 @@
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
  library(shiny)
2
+ library(tidyverse)
3
+
4
+ data1 <- read.csv("data20-24.csv", header = TRUE, check.names = FALSE, fileEncoding = "UTF-8")
5
+
6
+ AAaveragePT <- data1 %>%
7
+ mutate(arm_angle = round(arm_angle, digits = 0)) %>%
8
+ group_by(arm_angle, phand, pitch_name) %>%
9
+ summarise(
10
+ AvgIVB = mean(IVB, na.rm = TRUE),
11
+ AvgHB = mean(HB, na.rm = TRUE),
12
+ .groups = 'drop'
13
+ )
14
+
15
+ break_plot_Szn <- function(game,data1,sdate,edate) {
16
+ game <- game %>% left_join(pitch_type_lookup, by = c("pitch_name"))
17
+
18
+ title <- paste0(unique(game$`Pitcher Name`)," ",sdate," to ",edate)
19
+ # Get pitcher's handedness
20
+ pitcher_hand <- unique(game$phand)
21
+
22
+ angle_degrees <- round(mean(game$arm_angle,na.rm = TRUE),digits = 1)
23
+ # Calculate average movement by arm angle and pitch type from season data
24
+ # AAaveragePT <- data1 %>%
25
+ # mutate(arm_angle = round(arm_angle, digits = 0)) %>%
26
+ # group_by(arm_angle, phand, pitch_name) %>%
27
+ # summarise(
28
+ # AvgIVB = mean(IVB, na.rm = TRUE),
29
+ # AvgHB = mean(HB, na.rm = TRUE),
30
+ # .groups = 'drop'
31
+ # )
32
+
33
+ pitch_colors <- c(
34
+ "FF" = "#FF4136",
35
+ "SI" = "#FF851B",
36
+ "FC" = "#FFDC00",
37
+ "CH" = "#2ECC40",
38
+ "SL" = "#0074D9",
39
+ "ST" = "#ED68ED",
40
+ "CU" = "#B10DC9",
41
+ "FS" = "#01FF70",
42
+ "KC" = "#85144b",
43
+ "SV" = "#3D9970",
44
+ "KN" = "#39CCCC",
45
+ "FO" = "#F012BE",
46
+ "EP" = "#AAAAAA",
47
+ "FA" = "#7FDBFF",
48
+ "SC" = "#FF69B4"
49
+ )
50
+
51
+ # Convert angle to radians
52
+ angle_radians <- angle_degrees * (pi / 180)
53
+
54
+ # Legend location based on handedness
55
+ if(pitcher_hand == "L") {
56
+ leg <- c(0.08, .22)
57
+ factor <- -1
58
+ } else {
59
+ leg <- c(0.92, .22)
60
+ factor <- 1
61
+ }
62
+
63
+ # Calculate the endpoint coordinates
64
+ x_end <- 50 * cos(angle_radians) * factor
65
+ y_end <- 50 * sin(angle_radians)
66
+
67
+ avg_locations <- game %>%
68
+ group_by(pitch_abbr) %>%
69
+ summarize(
70
+ avg_HB = mean(HB, na.rm = TRUE),
71
+ avg_IVB = mean(IVB, na.rm = TRUE)
72
+ )
73
+
74
+ # Get unique pitch types from the game data
75
+ game_pitches <- unique(game$pitch_abbr)
76
+
77
+ # Filter season data for the current arm angle, handedness, and only pitches in the game
78
+ arm_angle_data <- data1 %>%
79
+ filter(abs(round(arm_angle) - angle_degrees) <= 2,
80
+ phand == pitcher_hand) %>%
81
+ left_join(pitch_type_lookup, by = c("pitch_name")) %>%
82
+ filter(pitch_abbr %in% game_pitches)
83
+
84
+ ggplot(game, aes(x = HB, y = IVB)) +
85
+ # Base layers
86
+ geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
87
+ geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
88
+
89
+ # Add ellipses from season data for this arm angle
90
+ stat_ellipse(
91
+ data = arm_angle_data,
92
+ aes(x = HB, y = IVB, fill = pitch_abbr),
93
+ geom = "polygon",
94
+ alpha = 0.2,
95
+ level = 0.68,
96
+ show.legend = FALSE
97
+ ) +
98
+
99
+ # Individual pitch points from the game
100
+ # geom_point(aes(fill = pitch_abbr), color = "#c6c6c6", size = 3, stroke = .5, shape = 21) +
101
+
102
+ # # Average location points from season data
103
+ # geom_point(
104
+ # data = AAaveragePT %>%
105
+ # filter(arm_angle == angle_degrees,
106
+ # phand == pitcher_hand) %>%
107
+ # left_join(pitch_type_lookup, by = c("pitch_name")) %>%
108
+ # filter(pitch_abbr %in% game_pitches),
109
+ # aes(x = AvgHB, y = AvgIVB, fill = pitch_abbr),
110
+ # color = "black",
111
+ # size = 6,
112
+ # stroke = .5,
113
+ # shape = 21
114
+ # ) +
115
+ geom_point(data = avg_locations, aes(x = avg_HB, y = avg_IVB, fill = pitch_abbr),
116
+ color = "black", size = 6, stroke = .5, shape = 21) +
117
+ # Arm angle line
118
+ geom_segment(x = 0, y = 0, xend = x_end, yend = y_end, color = "red", linewidth = 1, linetype = 5) +
119
+
120
+ # Aesthetics
121
+ scale_fill_manual(values = pitch_colors) +
122
+ scale_color_manual(values = pitch_colors) +
123
+ labs(
124
+ x = "Horizontal Break (in)",
125
+ y = "Induced Vertical Break (in)",
126
+ title = title,
127
+ subtitle = paste0("Arm Angle: ", angle_degrees, "\u00b0"),
128
+ caption = "Data: MLB | Viz: @TimStats\nEllipses show 1σ of movement for same-handed pitchers at similar arm angles"
129
+ ) +
130
+ xlim(-25, 25) +
131
+ ylim(-25, 25) +
132
+ theme_minimal() +
133
+ theme(
134
+ legend.position = leg,
135
+ plot.title = element_text(hjust = 0.5, face = "bold", color = "white"),
136
+ plot.subtitle = element_text(hjust = 0.5, face = "italic", color = "white"),
137
+ aspect.ratio = 1,
138
+ plot.background = element_rect(fill = "#333333", color = NA),
139
+ panel.background = element_rect(fill = "#333333", color = NA),
140
+ axis.text = element_text(color = "white"),
141
+ axis.title = element_text(color = "white"),
142
+ legend.background = element_rect(fill = "#333333"),
143
+ legend.text = element_text(color = "white"),
144
+ legend.title = element_blank(),
145
+ plot.margin = margin(10, 5, 10, 5),
146
+ axis.line = element_blank(),
147
+ axis.ticks = element_line(color = "white"),
148
+ panel.grid = element_blank(),
149
+ legend.key = element_blank(),
150
+ plot.caption = element_text(
151
+ color = "white",
152
+ hjust = 0.5
153
+ ),
154
+ panel.border = element_blank()
155
+ )
156
+ }
157
+ break_plot_tot <- function(game,data1,sdate,edate) {
158
+ game <- game %>% left_join(pitch_type_lookup, by = c("pitch_name"))
159
+ title <- paste0(unique(game$`Pitcher Name`)," ",sdate," to ",edate)
160
+ # Get pitcher's handedness
161
+ pitcher_hand <- unique(game$phand)
162
+
163
+ angle_degrees <- round(mean(game$arm_angle,na.rm = TRUE),digits = 1)
164
+ # Calculate average movement by arm angle and pitch type from season data
165
+ # AAaveragePT <- data1 %>%
166
+ # mutate(arm_angle = round(arm_angle, digits = 0)) %>%
167
+ # group_by(arm_angle, phand, pitch_name) %>%
168
+ # summarise(
169
+ # AvgIVB = mean(IVB, na.rm = TRUE),
170
+ # AvgHB = mean(HB, na.rm = TRUE),
171
+ # .groups = 'drop'
172
+ # )
173
+
174
+ pitch_colors <- c(
175
+ "FF" = "#FF4136",
176
+ "SI" = "#FF851B",
177
+ "FC" = "#FFDC00",
178
+ "CH" = "#2ECC40",
179
+ "SL" = "#0074D9",
180
+ "ST" = "#ED68ED",
181
+ "CU" = "#B10DC9",
182
+ "FS" = "#01FF70",
183
+ "KC" = "#85144b",
184
+ "SV" = "#3D9970",
185
+ "KN" = "#39CCCC",
186
+ "FO" = "#F012BE",
187
+ "EP" = "#AAAAAA",
188
+ "FA" = "#7FDBFF",
189
+ "SC" = "#FF69B4"
190
+ )
191
+
192
+ # Convert angle to radians
193
+ angle_radians <- angle_degrees * (pi / 180)
194
+
195
+ # Legend location based on handedness
196
+ if(pitcher_hand == "L") {
197
+ leg <- c(0.08, .22)
198
+ factor <- -1
199
+ } else {
200
+ leg <- c(0.92, .22)
201
+ factor <- 1
202
+ }
203
+
204
+ # Calculate the endpoint coordinates
205
+ x_end <- 50 * cos(angle_radians) * factor
206
+ y_end <- 50 * sin(angle_radians)
207
+
208
+ avg_locations <- game %>%
209
+ group_by(pitch_abbr) %>%
210
+ summarize(
211
+ avg_HB = mean(HB, na.rm = TRUE),
212
+ avg_IVB = mean(IVB, na.rm = TRUE)
213
+ )
214
+
215
+ # Get unique pitch types from the game data
216
+ game_pitches <- unique(game$pitch_abbr)
217
+
218
+ # Filter season data for the current arm angle, handedness, and only pitches in the game
219
+ arm_angle_data <- data1 %>%
220
+ filter(abs(round(arm_angle) - angle_degrees) <= 2,
221
+ phand == pitcher_hand) %>%
222
+ left_join(pitch_type_lookup, by = c("pitch_name")) %>%
223
+ filter(pitch_abbr %in% game_pitches)
224
+
225
+ ggplot(game, aes(x = HB, y = IVB)) +
226
+ # Base layers
227
+ geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
228
+ geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
229
+
230
+ # Add ellipses from season data for this arm angle
231
+ stat_ellipse(
232
+ data = arm_angle_data,
233
+ aes(x = HB, y = IVB, fill = pitch_abbr),
234
+ geom = "polygon",
235
+ alpha = 0.2,
236
+ level = 0.68,
237
+ show.legend = FALSE
238
+ ) +
239
+
240
+ # Individual pitch points from the game
241
+ geom_point(aes(fill = pitch_abbr), color = "#c6c6c6", size = 3, stroke = .5, shape = 21) +
242
+
243
+ # # Average location points from season data
244
+ # geom_point(
245
+ # data = AAaveragePT %>%
246
+ # filter(arm_angle == angle_degrees,
247
+ # phand == pitcher_hand) %>%
248
+ # left_join(pitch_type_lookup, by = c("pitch_name")) %>%
249
+ # filter(pitch_abbr %in% game_pitches),
250
+ # aes(x = AvgHB, y = AvgIVB, fill = pitch_abbr),
251
+ # color = "black",
252
+ # size = 6,
253
+ # stroke = .5,
254
+ # shape = 21
255
+ # ) +
256
+ geom_point(data = avg_locations, aes(x = avg_HB, y = avg_IVB, fill = pitch_abbr),
257
+ color = "black", size = 6, stroke = .5, shape = 21) +
258
+ # Arm angle line
259
+ geom_segment(x = 0, y = 0, xend = x_end, yend = y_end, color = "red", linewidth = 1, linetype = 5) +
260
+
261
+ # Aesthetics
262
+ scale_fill_manual(values = pitch_colors) +
263
+ scale_color_manual(values = pitch_colors) +
264
+ labs(
265
+ x = "Horizontal Break (in)",
266
+ y = "Induced Vertical Break (in)",
267
+ title = title,
268
+ subtitle = paste0("Arm Angle: ", angle_degrees, "\u00b0"),
269
+ caption = "Data: MLB | Viz: @TimStats\nEllipses show 1σ of movement for same-handed pitchers at similar arm angles"
270
+ ) +
271
+ xlim(-25, 25) +
272
+ ylim(-25, 25) +
273
+ theme_minimal() +
274
+ theme(
275
+ legend.position = leg,
276
+ plot.title = element_text(hjust = 0.5, face = "bold", color = "white"),
277
+ plot.subtitle = element_text(hjust = 0.5, face = "italic", color = "white"),
278
+ aspect.ratio = 1,
279
+ plot.background = element_rect(fill = "#333333", color = NA),
280
+ panel.background = element_rect(fill = "#333333", color = NA),
281
+ axis.text = element_text(color = "white"),
282
+ axis.title = element_text(color = "white"),
283
+ legend.background = element_rect(fill = "#333333"),
284
+ legend.text = element_text(color = "white"),
285
+ legend.title = element_blank(),
286
+ plot.margin = margin(10, 5, 10, 5),
287
+ axis.line = element_blank(),
288
+ axis.ticks = element_line(color = "white"),
289
+ panel.grid = element_blank(),
290
+ legend.key = element_blank(),
291
+ plot.caption = element_text(
292
+ color = "white",
293
+ hjust = 0.5
294
+ ),
295
+ panel.border = element_blank()
296
+ )
297
+ }
298
+
299
+ ui <- fluidPage(
300
+ # Application title
301
+ titlePanel("2020-2024 MLB Pitch Plots"),
302
+
303
+ sidebarLayout(
304
+ sidebarPanel(
305
+ width = 3,
306
+ selectInput("player", "Select Player:", choices = unique(data1$`Pitcher Name`),
307
+ width = "100%"),
308
+ dateRangeInput("date1", "Date Range:",
309
+ start = "2024-02-09",
310
+ end = "2024-09-05",
311
+ width = "100%"),
312
+ radioButtons("type", "Plot Type",
313
+ choices = c("Season Average","All Pitches")),
314
+ actionButton("submit", "Update Plot",
315
+ class = "btn btn-primary btn-block",
316
+ style = "margin-bottom: 10px"),
317
+ downloadButton("download", "Download Plot",
318
+ class = "btn btn-success btn-block")
319
  ),
320
+
321
+
322
+ mainPanel(
323
+ width = 9,
324
+ plotOutput("Plot")
325
+ )
326
+ )
327
  )
328
 
329
+ # Define server
330
+ server <- function(input, output) {
331
+ filtered_data <- eventReactive(input$submit, {
332
+ data1 %>%
333
+ filter(`Pitcher Name` == input$player,
334
+ between(as.Date(date), input$date1[1], input$date1[2]))
335
  })
336
+
337
+ current_plot <- reactive({
338
+ game <- filtered_data()
339
+ season_data <- data1
340
+
341
+ if(nrow(game) == 0) {
342
+ return(ggplot() +
343
+ annotate("text", x = 0.5, y = 0.5,
344
+ label = "No data available for selected date range",
345
+ color = "white") +
346
+ theme_void() +
347
+ theme(plot.background = element_rect(fill = "#333333", color = NA)))
348
+ }
349
+
350
+ if(input$type == "Season Average"){
351
+ break_plot_Szn(game, season_data, input$date1[1], input$date1[2])
352
+ } else{
353
+ break_plot_tot(game, season_data, input$date1[1], input$date1[2])
354
+ }
355
+ })
356
+
357
+ output$Plot <- renderPlot({
358
+ current_plot()
359
+ }, width = 1000, height = 1000)
360
+
361
+ output$download <- downloadHandler(
362
+ filename = function() {
363
+
364
+ paste0(
365
+ gsub(" ", "_", input$player), "_",
366
+ format(input$date1[1], "%Y%m%d"), "_to_",
367
+ format(input$date1[2], "%Y%m%d"), ".png"
368
+ )
369
  },
370
+ content = function(file) {
371
+
372
+ ggsave(file,
373
+ plot = current_plot(),
374
+ width = 7,
375
+ height = 7,
376
+ dpi = 300,
377
+ bg = "#333333")
378
+ }
379
  )
380
  }
381
 
382
+ # Run the application
383
+ shinyApp(ui = ui, server = server)