DaveM2430 commited on
Commit
6c84288
Β·
verified Β·
1 Parent(s): a487355

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +89 -237
app.R CHANGED
@@ -5,7 +5,7 @@ library(DT)
5
  library(ggplot2)
6
  library(shinyjs)
7
 
8
- # ── Load data ──────────────────────────────────────────────────────────────────
9
  load_data <- function(level) {
10
  if (level == "All") {
11
  dplyr::bind_rows(lapply(c("D1","D2","D3","JUCO","NAIA"), function(l) {
@@ -16,15 +16,23 @@ load_data <- function(level) {
16
  }
17
  }
18
 
19
- load_scores <- function(level) {
20
  if (level == "All") {
21
  dplyr::bind_rows(lapply(c("D1","D2","D3","JUCO","NAIA"), function(l) {
22
- arrow::read_parquet(paste0("/app/", l, "_scores.parquet"))
23
- })) %>%
24
- distinct(Pitcher, .keep_all = TRUE)
25
  } else {
26
- arrow::read_parquet(paste0("/app/", level, "_scores.parquet")) %>%
27
- distinct(Pitcher, .keep_all = TRUE)
 
 
 
 
 
 
 
 
 
28
  }
29
  }
30
 
@@ -33,8 +41,7 @@ load_portal <- function() {
33
  df <- arrow::read_parquet("/app/transfer_portal.parquet")
34
  names(df) <- tolower(names(df))
35
  df %>%
36
- rename(Pitcher = pitcher, Portal = transfer,
37
- ERA = era, FIP = fip, WHIP = whip) %>%
38
  distinct(Pitcher, .keep_all = TRUE)
39
  }, error = function(e) {
40
  message("Portal load failed: ", e$message)
@@ -87,201 +94,49 @@ clean_pitch_type <- function(pt) {
87
  )
88
  }
89
 
90
- # ── Pitcher stats ──────────────────────────────────────────────────────────────
91
- build_pitcher_stats <- function(df, min_pitches, scores, portal) {
92
-
93
- team_lookup <- df %>%
94
- select(Pitcher, PitcherTeam) %>%
95
- distinct(Pitcher, .keep_all = TRUE)
96
-
97
- df %>%
98
- mutate(
99
- IsStrike = PitchCall %in% c("StrikeCalled","StrikeSwinging","FoulBall",
100
- "FoulBallNotFieldable","FoulTip","InPlay"),
101
- IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
102
- abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
103
- IsWhiff = PitchCall == "StrikeSwinging",
104
- IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
105
- "FoulBallNotFieldable","FoulTip","InPlay"),
106
- IsChase = IsSwing & !IsZone,
107
- IsOutZone = !IsZone,
108
- IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
109
- !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
110
- IsK = !is.na(KorBB) & KorBB == "Strikeout",
111
- IsBB = !is.na(KorBB) & KorBB == "Walk",
112
- IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
113
- (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch"))
114
- ) %>%
115
- group_by(Pitcher, Level) %>%
116
- summarise(
117
- Pitches = n(),
118
- K = sum(IsK, na.rm = TRUE),
119
- BB = sum(IsBB, na.rm = TRUE),
120
- PA = sum(IsPA, na.rm = TRUE),
121
- zone_n = sum(IsZone, na.rm = TRUE),
122
- strike_n = sum(IsStrike, na.rm = TRUE),
123
- whiff_n = sum(IsWhiff, na.rm = TRUE),
124
- swing_n = sum(IsSwing, na.rm = TRUE),
125
- chase_n = sum(IsChase, na.rm = TRUE),
126
- outzone_n = sum(IsOutZone, na.rm = TRUE),
127
- hh_n = sum(IsHardHit, na.rm = TRUE),
128
- bip_n = sum(!is.na(ExitSpeed) &
129
- !PitchCall %in% c("FoulBall","FoulBallNotFieldable",
130
- "FoulBallFieldable","FoulTip"), na.rm = TRUE),
131
- fb_velo_sum = sum(ifelse(!is.na(RelSpeed) &
132
- TaggedPitchType %in% c("Fastball","FourSeamFastBall",
133
- "Four-Seam","FourSeam",
134
- "Sinker","TwoSeamFastBall",
135
- "OneSeamFastball","Cutter"),
136
- RelSpeed, NA), na.rm = TRUE),
137
- fb_velo_n = sum(!is.na(RelSpeed) &
138
- TaggedPitchType %in% c("Fastball","FourSeamFastBall",
139
- "Four-Seam","FourSeam",
140
- "Sinker","TwoSeamFastBall",
141
- "OneSeamFastball","Cutter"),
142
- na.rm = TRUE),
143
- .groups = "drop"
144
- ) %>%
145
- left_join(team_lookup, by = "Pitcher") %>%
146
- left_join(
147
- scores %>% select(Pitcher, `Stuff+`, `Location+`) %>%
148
- distinct(Pitcher, .keep_all = TRUE),
149
- by = "Pitcher"
150
- ) %>%
151
  filter(Pitches >= min_pitches) %>%
152
- left_join(portal %>% select(Pitcher, Portal, ERA, FIP, WHIP), by = "Pitcher") %>%
153
  mutate(
154
- Portal = ifelse(is.na(Portal), "No", as.character(Portal)),
155
- ERA = ifelse(Portal == "No", NA_real_, as.numeric(ERA)),
156
- FIP = ifelse(Portal == "No", NA_real_, as.numeric(FIP)),
157
- WHIP = ifelse(Portal == "No", NA_real_, as.numeric(WHIP)),
158
- Name = sapply(Pitcher, format_name),
159
- Team = PitcherTeam,
160
- `Zone%` = round(zone_n / Pitches * 100, 1),
161
- `Strike%` = round(strike_n / Pitches * 100, 1),
162
- `K's` = K,
163
- `BB's` = BB,
164
- `K/BB` = case_when(
165
- K == 0 & BB == 0 ~ NA_real_,
166
- BB == 0 ~ NA_real_,
167
- K == 0 ~ 0,
168
- TRUE ~ round(K / BB, 2)
169
- ),
170
- `Whiff%` = round(whiff_n / pmax(swing_n, 1) * 100, 1),
171
- `Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
172
- `FB Velo` = ifelse(fb_velo_n == 0, NA_real_,
173
- round(fb_velo_sum / fb_velo_n, 1)),
174
- `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
175
- `Plot` = paste0('<button class="plot-btn" data-pitcher="',
176
- gsub('"', '&quot;', Pitcher),
177
- '">&#128202; View</button>'),
178
- `Twitter` = paste0('<a href="https://www.google.com/search?q=',
179
- gsub(" ", "+", sapply(Pitcher, format_name)),
180
- '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
181
- ) %>%
182
- filter(!is.na(Name), trimws(Name) != "", grepl(" ", trimws(Name))) %>%
183
- select(Name, Level, Team, Portal, `Plot`, `Twitter`, Pitches, ERA, FIP, WHIP,
184
- `FB Velo`, `Zone%`, `Strike%`, `K's`, `BB's`, `K/BB`,
185
- `Whiff%`, `Chase%`, `Hard Hit%`, `Stuff+`, `Location+`) %>%
186
  arrange(desc(Pitches))
187
  }
188
 
189
- # ── Hitter stats ───────────────────────────────────────────────────────────────
190
- build_hitter_stats <- function(df, min_pa, hitter_portal) {
191
-
192
- team_lookup <- df %>%
193
- select(Batter, BatterTeam) %>%
194
- distinct(Batter, .keep_all = TRUE)
195
-
196
- df %>%
197
- mutate(
198
- IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
199
- "FoulBallNotFieldable","FoulTip","InPlay"),
200
- IsContact = PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulTip","InPlay"),
201
- IsWhiff = PitchCall == "StrikeSwinging",
202
- IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
203
- abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
204
- IsChase = IsSwing & !IsZone,
205
- IsOutZone = !IsZone,
206
- IsZoneSwing = IsSwing & IsZone,
207
- IsZoneWhiff = IsWhiff & IsZone,
208
- IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
209
- !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
210
- IsBIP = !is.na(ExitSpeed) &
211
- !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
212
- IsGB = IsBIP & TaggedHitType %in% c("Groundball","GroundBall"),
213
- IsFB = IsBIP & TaggedHitType == "FlyBall",
214
- IsLD = IsBIP & TaggedHitType == "LineDrive",
215
- IsHit = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun"),
216
- IsAB = !is.na(PlayResult) & PlayResult %in% c("Single","Double","Triple","HomeRun",
217
- "Out","Error","FieldersChoice"),
218
- IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
219
- (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch")),
220
- IsHR = !is.na(PlayResult) & PlayResult == "HomeRun",
221
- IsBB = !is.na(KorBB) & KorBB == "Walk",
222
- IsK = !is.na(KorBB) & KorBB == "Strikeout"
223
- ) %>%
224
- group_by(Batter, Level) %>%
225
- summarise(
226
- Pitches = n(),
227
- PA = sum(IsPA, na.rm = TRUE),
228
- AB = sum(IsAB, na.rm = TRUE),
229
- H = sum(IsHit, na.rm = TRUE),
230
- HR = sum(IsHR, na.rm = TRUE),
231
- BB = sum(IsBB, na.rm = TRUE),
232
- K = sum(IsK, na.rm = TRUE),
233
- hh_n = sum(IsHardHit, na.rm = TRUE),
234
- bip_n = sum(IsBIP, na.rm = TRUE),
235
- gb_n = sum(IsGB, na.rm = TRUE),
236
- fb_n = sum(IsFB, na.rm = TRUE),
237
- ld_n = sum(IsLD, na.rm = TRUE),
238
- whiff_n = sum(IsWhiff, na.rm = TRUE),
239
- swing_n = sum(IsSwing, na.rm = TRUE),
240
- contact_n = sum(IsContact, na.rm = TRUE),
241
- chase_n = sum(IsChase, na.rm = TRUE),
242
- outzone_n = sum(IsOutZone, na.rm = TRUE),
243
- zone_swing_n = sum(IsZoneSwing, na.rm = TRUE),
244
- zone_whiff_n = sum(IsZoneWhiff, na.rm = TRUE),
245
- ev_sum = sum(ifelse(IsBIP, ExitSpeed, NA), na.rm = TRUE),
246
- .groups = "drop"
247
- ) %>%
248
- left_join(team_lookup, by = "Batter") %>%
249
  filter(PA >= min_pa) %>%
250
- left_join(
251
- hitter_portal %>% select(Batter, Portal, Position, BatSide, BA, OBP, SLG),
252
- by = "Batter"
253
- ) %>%
254
  mutate(
255
- Portal = ifelse(is.na(Portal), "No", as.character(Portal)),
256
- BA = ifelse(Portal == "No", NA_real_, as.numeric(BA)),
257
- OBP = ifelse(Portal == "No", NA_real_, as.numeric(OBP)),
258
- SLG = ifelse(Portal == "No", NA_real_, as.numeric(SLG)),
259
- Name = paste0('<button class="hitter-name-btn" data-batter="',
260
- gsub('"', '&quot;', Batter),
261
- '" style="background:none;border:none;color:#002147;',
262
- 'font-weight:700;cursor:pointer;padding:0;font-size:13px;">',
263
- sapply(Batter, format_name), '</button>'),
264
- Team = BatterTeam,
265
- AVG = round(H / pmax(AB, 1), 3),
266
- `BB%` = round(BB / pmax(PA, 1) * 100, 1),
267
- `K%` = round(K / pmax(PA, 1) * 100, 1),
268
- `Swing%` = round(swing_n / Pitches * 100, 1),
269
- `Contact%` = round(contact_n / pmax(swing_n, 1) * 100, 1),
270
- `IZ Whiff%` = round(zone_whiff_n / pmax(zone_swing_n, 1) * 100, 1),
271
- `Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
272
- `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1),
273
- `Avg EV` = round(ev_sum / pmax(bip_n, 1), 1),
274
- `GB%` = round(gb_n / pmax(bip_n, 1) * 100, 1),
275
- `FB%` = round(fb_n / pmax(bip_n, 1) * 100, 1),
276
- `LD%` = round(ld_n / pmax(bip_n, 1) * 100, 1),
277
- `Twitter` = paste0('<a href="https://www.google.com/search?q=',
278
- gsub(" ", "+", sapply(Batter, format_name)),
279
- '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
280
- ) %>%
281
- filter(!is.na(Name), grepl("data-batter", Name)) %>%
282
- select(Name, Level, Team, Portal, Position, BatSide, `Twitter`, PA,
283
- AVG, OBP, SLG, `BB%`, `K%`, `Swing%`, `Contact%`, `IZ Whiff%`,
284
- `Chase%`, `Hard Hit%`, `Avg EV`, `GB%`, `FB%`, `LD%`) %>%
285
  arrange(desc(PA))
286
  }
287
 
@@ -422,7 +277,6 @@ ui <- fluidPage(
422
  Shiny.addCustomMessageHandler('update_label', function(msg) {
423
  $('#min_label').text(msg);
424
  });
425
-
426
  });
427
  "))
428
  ),
@@ -523,14 +377,14 @@ ui <- fluidPage(
523
  # ── Server ─────────────────────────────────────────────────────────────────────
524
  server <- function(input, output, session) {
525
 
 
526
  raw_data <- reactive({
527
  req(input$level)
528
  tryCatch({
529
  list(
530
- df = load_data(input$level),
531
- scores = load_scores(input$level),
532
- portal = load_portal(),
533
- hitter_portal = load_hitter_portal()
534
  )
535
  }, error = function(e) {
536
  showNotification(paste("Failed to load:", e$message), type = "error")
@@ -538,6 +392,7 @@ server <- function(input, output, session) {
538
  })
539
  })
540
 
 
541
  observeEvent(input$position, {
542
  if (input$position == "Pitcher") {
543
  session$sendCustomMessage("update_label", "Min Pitches")
@@ -552,44 +407,39 @@ server <- function(input, output, session) {
552
  }
553
  })
554
 
 
555
  stats_table <- reactive({
556
  req(raw_data(), input$min_pitches)
557
  if (input$position == "Pitcher") {
558
- tryCatch({
559
- df <- build_pitcher_stats(
560
- raw_data()$df,
561
  input$min_pitches,
562
- raw_data()$scores,
563
- raw_data()$portal
564
- )
565
- if (input$portal_only) df <- df %>% filter(Portal == "Yes")
566
- df
567
- }, error = function(e) {
568
- message("build_pitcher_stats failed: ", e$message)
569
- showNotification(paste("Stats error:", e$message), type = "error")
570
- NULL
571
- })
572
  } else {
573
- tryCatch({
574
- df <- build_hitter_stats(
575
- raw_data()$df,
576
  input$min_pitches,
577
- raw_data()$hitter_portal
578
- )
579
- if (input$portal_only) df <- df %>% filter(Portal == "Yes")
580
- if (input$bat_side != "All")
581
- df <- df %>% filter(BatSide == input$bat_side)
582
- if (input$hitter_position != "All")
583
- df <- df %>% filter(Position == input$hitter_position)
584
- df
585
- }, error = function(e) {
586
- message("build_hitter_stats failed: ", e$message)
587
- showNotification(paste("Stats error:", e$message), type = "error")
588
- NULL
589
- })
590
  }
591
  })
592
 
 
593
  output$main_ui <- renderUI({
594
  if (input$position == "Pitcher") {
595
  tagList(
@@ -610,6 +460,7 @@ server <- function(input, output, session) {
610
  }
611
  })
612
 
 
613
  output$pitcher_table <- renderDT({
614
  req(stats_table(), nrow(stats_table()) > 0)
615
  dt <- stats_table()
@@ -635,12 +486,12 @@ server <- function(input, output, session) {
635
  pageLength = 25,
636
  lengthMenu = c(25, 50, 100),
637
  scrollX = TRUE,
638
- order = list(list(6, "desc")),
639
  orderMulti = FALSE,
640
  dom = "lfrtip",
641
  columnDefs = list(
642
  list(className = "dt-left", targets = 0:1),
643
- list(className = "dt-center", targets = 2:20),
644
  list(orderable = FALSE, targets = c(4, 5))
645
  ),
646
  initComplete = JS("function(settings, json) {
@@ -655,6 +506,7 @@ server <- function(input, output, session) {
655
  )
656
  })
657
 
 
658
  output$hitter_table <- renderDT({
659
  req(stats_table(), nrow(stats_table()) > 0)
660
  dt <- stats_table()
@@ -668,12 +520,12 @@ server <- function(input, output, session) {
668
  pageLength = 25,
669
  lengthMenu = c(25, 50, 100),
670
  scrollX = TRUE,
671
- order = list(list(7, "desc")),
672
  orderMulti = FALSE,
673
  dom = "lfrtip",
674
  columnDefs = list(
675
  list(className = "dt-left", targets = 0:1),
676
- list(className = "dt-center", targets = 2:21),
677
  list(orderable = FALSE, targets = 6)
678
  ),
679
  initComplete = JS("function(settings, json) {
@@ -703,7 +555,7 @@ server <- function(input, output, session) {
703
 
704
  pitcher_plot_data <- reactive({
705
  req(current_pitcher(), raw_data())
706
- raw_data()$df %>%
707
  filter(Pitcher == current_pitcher()) %>%
708
  mutate(PitchType = clean_pitch_type(TaggedPitchType)) %>%
709
  filter(!is.na(PitchType), !is.na(HorzBreak), !is.na(InducedVertBreak))
@@ -834,7 +686,7 @@ server <- function(input, output, session) {
834
 
835
  batter_data <- reactive({
836
  req(current_batter(), raw_data())
837
- raw_data()$df %>%
838
  filter(Batter == current_batter()) %>%
839
  mutate(
840
  PitchGroup = case_when(
 
5
  library(ggplot2)
6
  library(shinyjs)
7
 
8
+ # ── Load functions ─────────────────────────────────────────────────────────────
9
  load_data <- function(level) {
10
  if (level == "All") {
11
  dplyr::bind_rows(lapply(c("D1","D2","D3","JUCO","NAIA"), function(l) {
 
16
  }
17
  }
18
 
19
+ load_pitcher_leaderboard <- function(level) {
20
  if (level == "All") {
21
  dplyr::bind_rows(lapply(c("D1","D2","D3","JUCO","NAIA"), function(l) {
22
+ arrow::read_parquet(paste0("/app/", l, "_pitcher_leaderboard.parquet"))
23
+ }))
 
24
  } else {
25
+ arrow::read_parquet(paste0("/app/", level, "_pitcher_leaderboard.parquet"))
26
+ }
27
+ }
28
+
29
+ load_hitter_leaderboard <- function(level) {
30
+ if (level == "All") {
31
+ dplyr::bind_rows(lapply(c("D1","D2","D3","JUCO","NAIA"), function(l) {
32
+ arrow::read_parquet(paste0("/app/", l, "_hitter_leaderboard.parquet"))
33
+ }))
34
+ } else {
35
+ arrow::read_parquet(paste0("/app/", level, "_hitter_leaderboard.parquet"))
36
  }
37
  }
38
 
 
41
  df <- arrow::read_parquet("/app/transfer_portal.parquet")
42
  names(df) <- tolower(names(df))
43
  df %>%
44
+ rename(Pitcher=pitcher, Portal=transfer, ERA=era, FIP=fip, WHIP=whip) %>%
 
45
  distinct(Pitcher, .keep_all = TRUE)
46
  }, error = function(e) {
47
  message("Portal load failed: ", e$message)
 
94
  )
95
  }
96
 
97
+ # ── Build pitcher table from leaderboard ───────────────────────────────────────
98
+ build_pitcher_table <- function(lb, min_pitches, portal_only) {
99
+ df <- lb %>%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  filter(Pitches >= min_pitches) %>%
 
101
  mutate(
102
+ `Plot` = paste0('<button class="plot-btn" data-pitcher="',
103
+ gsub('"', '&quot;', Pitcher),
104
+ '">&#128202; View</button>'),
105
+ `Twitter` = paste0('<a href="https://www.google.com/search?q=',
106
+ gsub(" ", "+", Name),
107
+ '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
108
+ )
109
+ if (portal_only) df <- df %>% filter(Portal == "Yes")
110
+ df %>%
111
+ select(Name, Level, Team, Portal, `Plot`, `Twitter`, HomeState, Height, Weight,
112
+ Pitches, ERA, FIP, WHIP, `FB Velo`, `Zone%`, `Strike%`,
113
+ `K's`, `BB's`, `K/BB`, `Whiff%`, `Chase%`, `Hard Hit%`,
114
+ `Stuff+`, `Location+`) %>%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  arrange(desc(Pitches))
116
  }
117
 
118
+ # ── Build hitter table from leaderboard ────────────────────────────────────────
119
+ build_hitter_table <- function(lb, min_pa, portal_only, bat_side, hitter_pos) {
120
+ df <- lb %>%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  filter(PA >= min_pa) %>%
 
 
 
 
122
  mutate(
123
+ Name = paste0('<button class="hitter-name-btn" data-batter="',
124
+ gsub('"', '&quot;', Batter),
125
+ '" style="background:none;border:none;color:#002147;',
126
+ 'font-weight:700;cursor:pointer;padding:0;font-size:13px;">',
127
+ Name, '</button>'),
128
+ `Twitter` = paste0('<a href="https://www.google.com/search?q=',
129
+ gsub(" ", "+", sapply(Batter, format_name)),
130
+ '+baseball+twitter" target="_blank">&#x1F426; Search</a>')
131
+ )
132
+ if (portal_only) df <- df %>% filter(Portal == "Yes")
133
+ if (bat_side != "All") df <- df %>% filter(BatSide == bat_side)
134
+ if (hitter_pos != "All") df <- df %>% filter(Position == hitter_pos)
135
+ df %>%
136
+ select(Name, Level, Team, Portal, Position, BatSide, `Twitter`,
137
+ HomeState, Height, Weight, PA,
138
+ AVG, OBP, SLG, BA, `BB%`, `K%`, `Swing%`, `Contact%`,
139
+ `IZ Whiff%`, `Chase%`, `Hard Hit%`, `Avg EV`, `GB%`, `FB%`, `LD%`) %>%
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  arrange(desc(PA))
141
  }
142
 
 
277
  Shiny.addCustomMessageHandler('update_label', function(msg) {
278
  $('#min_label').text(msg);
279
  });
 
280
  });
281
  "))
282
  ),
 
377
  # ── Server ─────────────────────────────────────────────────────────────────────
378
  server <- function(input, output, session) {
379
 
380
+ # ── Load leaderboards once ──────────────────────────────────────────────────
381
  raw_data <- reactive({
382
  req(input$level)
383
  tryCatch({
384
  list(
385
+ pitcher_lb = load_pitcher_leaderboard(input$level),
386
+ hitter_lb = load_hitter_leaderboard(input$level),
387
+ raw_df = load_data(input$level)
 
388
  )
389
  }, error = function(e) {
390
  showNotification(paste("Failed to load:", e$message), type = "error")
 
392
  })
393
  })
394
 
395
+ # ── Position switch ─────────────────────────────────────────────────────────
396
  observeEvent(input$position, {
397
  if (input$position == "Pitcher") {
398
  session$sendCustomMessage("update_label", "Min Pitches")
 
407
  }
408
  })
409
 
410
+ # ── Stats table ─────────────────────────────────────────────────────────────
411
  stats_table <- reactive({
412
  req(raw_data(), input$min_pitches)
413
  if (input$position == "Pitcher") {
414
+ tryCatch(
415
+ build_pitcher_table(
416
+ raw_data()$pitcher_lb,
417
  input$min_pitches,
418
+ input$portal_only
419
+ ),
420
+ error = function(e) {
421
+ message("build_pitcher_table failed: ", e$message)
422
+ NULL
423
+ }
424
+ )
 
 
 
425
  } else {
426
+ tryCatch(
427
+ build_hitter_table(
428
+ raw_data()$hitter_lb,
429
  input$min_pitches,
430
+ input$portal_only,
431
+ input$bat_side,
432
+ input$hitter_position
433
+ ),
434
+ error = function(e) {
435
+ message("build_hitter_table failed: ", e$message)
436
+ NULL
437
+ }
438
+ )
 
 
 
 
439
  }
440
  })
441
 
442
+ # ── Main UI ─────────────────────────────────────────────────────────────────
443
  output$main_ui <- renderUI({
444
  if (input$position == "Pitcher") {
445
  tagList(
 
460
  }
461
  })
462
 
463
+ # ── Pitcher table ────────────────────────────────────────────────────────────
464
  output$pitcher_table <- renderDT({
465
  req(stats_table(), nrow(stats_table()) > 0)
466
  dt <- stats_table()
 
486
  pageLength = 25,
487
  lengthMenu = c(25, 50, 100),
488
  scrollX = TRUE,
489
+ order = list(list(9, "desc")),
490
  orderMulti = FALSE,
491
  dom = "lfrtip",
492
  columnDefs = list(
493
  list(className = "dt-left", targets = 0:1),
494
+ list(className = "dt-center", targets = 2:23),
495
  list(orderable = FALSE, targets = c(4, 5))
496
  ),
497
  initComplete = JS("function(settings, json) {
 
506
  )
507
  })
508
 
509
+ # ── Hitter table ─────────────────────────────────────────────────────────────
510
  output$hitter_table <- renderDT({
511
  req(stats_table(), nrow(stats_table()) > 0)
512
  dt <- stats_table()
 
520
  pageLength = 25,
521
  lengthMenu = c(25, 50, 100),
522
  scrollX = TRUE,
523
+ order = list(list(10, "desc")),
524
  orderMulti = FALSE,
525
  dom = "lfrtip",
526
  columnDefs = list(
527
  list(className = "dt-left", targets = 0:1),
528
+ list(className = "dt-center", targets = 2:25),
529
  list(orderable = FALSE, targets = 6)
530
  ),
531
  initComplete = JS("function(settings, json) {
 
555
 
556
  pitcher_plot_data <- reactive({
557
  req(current_pitcher(), raw_data())
558
+ raw_data()$raw_df %>%
559
  filter(Pitcher == current_pitcher()) %>%
560
  mutate(PitchType = clean_pitch_type(TaggedPitchType)) %>%
561
  filter(!is.na(PitchType), !is.na(HorzBreak), !is.na(InducedVertBreak))
 
686
 
687
  batter_data <- reactive({
688
  req(current_batter(), raw_data())
689
+ raw_data()$raw_df %>%
690
  filter(Batter == current_batter()) %>%
691
  mutate(
692
  PitchGroup = case_when(