DaveM2430 commited on
Commit
6ba012a
Β·
verified Β·
1 Parent(s): 03c52a2

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +172 -63
app.R CHANGED
@@ -1,73 +1,134 @@
1
  library(shiny)
2
  library(dplyr)
 
 
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  ui <- fluidPage(
5
  tags$head(
6
  tags$style(HTML("
7
  body { margin:0; font-family:'Segoe UI',Arial,sans-serif; background:#f5f5f5; }
 
8
  .header { background:#002147; padding:14px 24px;
9
  display:flex; align-items:center; justify-content:space-between; }
10
  .header h1 { color:white; margin:0; font-size:26px; font-weight:700; }
11
  .header p { color:#aabbcc; margin:0; font-size:13px; }
 
12
  .filters { display:flex; gap:24px; align-items:flex-end;
13
  padding:16px 24px; background:white;
14
  border-bottom:1px solid #e0e0e0; flex-wrap:wrap; }
15
  .filter-label { font-weight:600; font-size:11px; text-transform:uppercase;
16
  letter-spacing:0.4px; color:#555; margin-bottom:4px; }
 
17
  .main { padding:20px 24px; }
18
  .form-control { border:1px solid #d0d0d0; border-radius:4px; }
19
 
20
- /* ── Splash ─────────────────────────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  #splash {
22
  position:fixed; top:0; left:0; width:100%; height:100%;
23
- background:#002147;
24
- display:flex; flex-direction:column;
25
  align-items:center; justify-content:center;
26
- z-index:9999;
27
- opacity:1;
28
- transition:opacity 0.8s ease;
29
- }
30
- #splash.fade-out {
31
- opacity:0;
32
- pointer-events:none;
33
- }
34
- #splash img {
35
- width:140px;
36
- animation:logoIn 0.7s ease forwards;
37
- opacity:0;
38
- }
39
- #splash h1 {
40
- color:white;
41
- font-size:28px;
42
- font-weight:700;
43
- margin:18px 0 6px 0;
44
- animation:textIn 0.7s ease 0.3s forwards;
45
- opacity:0;
46
- letter-spacing:0.5px;
47
- }
48
- #splash p {
49
- color:#aabbcc;
50
- font-size:14px;
51
- margin:0;
52
- animation:textIn 0.7s ease 0.5s forwards;
53
- opacity:0;
54
- }
55
- #splash .bar-track {
56
- width:200px;
57
- height:3px;
58
- background:rgba(255,255,255,0.2);
59
- border-radius:2px;
60
- margin-top:28px;
61
- animation:textIn 0.7s ease 0.6s forwards;
62
- opacity:0;
63
- }
64
- #splash .bar-fill {
65
- height:3px;
66
- width:0%;
67
- background:white;
68
- border-radius:2px;
69
- animation:barFill 1.8s ease 0.8s forwards;
70
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  @keyframes logoIn {
73
  from { opacity:0; transform:translateY(-20px); }
@@ -87,25 +148,21 @@ ui <- fluidPage(
87
  $(document).ready(function() {
88
  setTimeout(function() {
89
  $('#splash').addClass('fade-out');
90
- setTimeout(function() {
91
- $('#splash').remove();
92
- }, 800);
93
  }, 2800);
94
  });
95
  "))
96
  ),
97
 
98
- # ── Splash screen ───────────────────────────────────────────────────────────
99
  tags$div(id="splash",
100
  tags$img(src="Butler-Bulldogs-Logo-1990.png"),
101
- tags$h1("TPD"),
102
  tags$p("Butler Baseball Analytics"),
103
- tags$div(class="bar-track",
104
- tags$div(class="bar-fill")
105
- )
106
  ),
107
 
108
- # ── Header ──────────────────────────────────────────────────────────────────
109
  tags$div(class="header",
110
  tags$div(
111
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
@@ -114,13 +171,13 @@ ui <- fluidPage(
114
  tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
115
  ),
116
 
117
- # ── Filters ─────────────────────────────────────────────────────────────────
118
  tags$div(class="filters",
119
  tags$div(
120
  tags$div("Level", class="filter-label"),
121
  selectInput("level", label=NULL,
122
- choices = c("All","D1","D2","D3","JUCO","Other"),
123
- selected = "All",
124
  width = "160px")
125
  ),
126
  tags$div(
@@ -133,20 +190,72 @@ ui <- fluidPage(
133
  tags$div(
134
  tags$div("Min Pitches", class="filter-label"),
135
  numericInput("min_pitches", label=NULL,
136
- value = 100, min = 1, max = 1000, step = 25,
137
- width = "160px")
138
  )
139
  ),
140
 
141
- # ── Main content ─────────────────────────────────────────────────────────────
142
  tags$div(class="main",
143
  uiOutput("main_ui")
144
  )
145
  )
146
 
 
147
  server <- function(input, output, session) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  output$main_ui <- renderUI({
149
- tags$p("Content will load here.", style="color:#888;")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  })
151
  }
152
 
 
1
  library(shiny)
2
  library(dplyr)
3
+ library(arrow)
4
+ library(DT)
5
 
6
+ # ── Load data ──────────────────────────────────────────────────────────────────
7
+ load_data <- function(level) {
8
+ path <- paste0("/app/", gsub("[^A-Za-z0-9]","_",level), ".parquet")
9
+ arrow::read_parquet(path)
10
+ }
11
+
12
+ build_pitcher_stats <- function(df, min_pitches) {
13
+
14
+ format_name <- function(name) {
15
+ parts <- strsplit(name, ", ")[[1]]
16
+ if (length(parts) == 2) paste(trimws(parts[2]), trimws(parts[1])) else name
17
+ }
18
+
19
+ df %>%
20
+ mutate(
21
+ IsStrike = PitchCall %in% c("StrikeCalled","StrikeSwinging","FoulBall",
22
+ "FoulBallNotFieldable","FoulTip","InPlay"),
23
+ IsZone = !is.na(PlateLocSide) & !is.na(PlateLocHeight) &
24
+ abs(PlateLocSide) <= 0.8303 & PlateLocHeight >= 1.5 & PlateLocHeight <= 3.3775,
25
+ IsWhiff = PitchCall == "StrikeSwinging",
26
+ IsSwing = PitchCall %in% c("StrikeSwinging","FoulBall",
27
+ "FoulBallNotFieldable","FoulTip","InPlay"),
28
+ IsChase = IsSwing & !IsZone,
29
+ IsOutZone = !IsZone,
30
+ IsHardHit = !is.na(ExitSpeed) & ExitSpeed >= 95 &
31
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable","FoulBallFieldable","FoulTip"),
32
+ IsK = !is.na(KorBB) & KorBB == "Strikeout",
33
+ IsBB = !is.na(KorBB) & KorBB == "Walk",
34
+ IsPA = (!is.na(KorBB) & KorBB %in% c("Strikeout","Walk")) |
35
+ (!is.na(PitchCall) & PitchCall %in% c("InPlay","HitByPitch"))
36
+ ) %>%
37
+ group_by(Pitcher, Level) %>%
38
+ summarise(
39
+ Pitches = n(),
40
+ K = sum(IsK, na.rm=TRUE),
41
+ BB = sum(IsBB, na.rm=TRUE),
42
+ PA = sum(IsPA, na.rm=TRUE),
43
+ zone_n = sum(IsZone, na.rm=TRUE),
44
+ strike_n = sum(IsStrike, na.rm=TRUE),
45
+ whiff_n = sum(IsWhiff, na.rm=TRUE),
46
+ swing_n = sum(IsSwing, na.rm=TRUE),
47
+ chase_n = sum(IsChase, na.rm=TRUE),
48
+ outzone_n = sum(IsOutZone, na.rm=TRUE),
49
+ hh_n = sum(IsHardHit, na.rm=TRUE),
50
+ bip_n = sum(!is.na(ExitSpeed) &
51
+ !PitchCall %in% c("FoulBall","FoulBallNotFieldable",
52
+ "FoulBallFieldable","FoulTip"), na.rm=TRUE),
53
+ .groups = "drop"
54
+ ) %>%
55
+ filter(Pitches >= min_pitches) %>%
56
+ mutate(
57
+ Name = sapply(Pitcher, format_name),
58
+ `Zone%` = round(zone_n / Pitches * 100, 1),
59
+ `Strike%` = round(strike_n / Pitches * 100, 1),
60
+ `K's` = K,
61
+ `BB's` = BB,
62
+ `K-BB%` = round((K - BB) / pmax(PA, 1) * 100, 1),
63
+ `Whiff%` = round(whiff_n / pmax(swing_n, 1) * 100, 1),
64
+ `Chase%` = round(chase_n / pmax(outzone_n, 1) * 100, 1),
65
+ `Hard Hit%` = round(hh_n / pmax(bip_n, 1) * 100, 1)
66
+ ) %>%
67
+ select(Name, Level, Pitches, `Zone%`, `Strike%`, `K's`, `BB's`,
68
+ `K-BB%`, `Whiff%`, `Chase%`, `Hard Hit%`) %>%
69
+ arrange(desc(Pitches))
70
+ }
71
+
72
+ # ── UI ─────────────────────────────────────────────────────────────────────────
73
  ui <- fluidPage(
74
  tags$head(
75
  tags$style(HTML("
76
  body { margin:0; font-family:'Segoe UI',Arial,sans-serif; background:#f5f5f5; }
77
+
78
  .header { background:#002147; padding:14px 24px;
79
  display:flex; align-items:center; justify-content:space-between; }
80
  .header h1 { color:white; margin:0; font-size:26px; font-weight:700; }
81
  .header p { color:#aabbcc; margin:0; font-size:13px; }
82
+
83
  .filters { display:flex; gap:24px; align-items:flex-end;
84
  padding:16px 24px; background:white;
85
  border-bottom:1px solid #e0e0e0; flex-wrap:wrap; }
86
  .filter-label { font-weight:600; font-size:11px; text-transform:uppercase;
87
  letter-spacing:0.4px; color:#555; margin-bottom:4px; }
88
+
89
  .main { padding:20px 24px; }
90
  .form-control { border:1px solid #d0d0d0; border-radius:4px; }
91
 
92
+ /* table */
93
+ .dataTables_wrapper { background:white; border-radius:8px;
94
+ padding:16px; box-shadow:0 1px 4px rgba(0,0,0,0.08); }
95
+ .dataTables_filter input { border:1px solid #d0d0d0; border-radius:4px;
96
+ padding:4px 8px; }
97
+ .dataTables_length select { border:1px solid #d0d0d0; border-radius:4px; }
98
+ table.dataTable thead th {
99
+ background:#002147; color:white; font-weight:600;
100
+ font-size:12px; text-transform:uppercase; letter-spacing:0.3px;
101
+ border-bottom:none !important; cursor:pointer;
102
+ }
103
+ table.dataTable thead th:hover { background:#003366; }
104
+ table.dataTable tbody tr { background:white; }
105
+ table.dataTable tbody tr:nth-child(even) { background:#f7f9fc; }
106
+ table.dataTable tbody tr:hover { background:#eef2f7 !important; }
107
+ table.dataTable tbody td { font-size:13px; padding:10px 12px;
108
+ border-bottom:1px solid #f0f0f0; }
109
+ table.dataTable tbody td:first-child { font-weight:600; color:#002147; }
110
+
111
+ /* splash */
112
  #splash {
113
  position:fixed; top:0; left:0; width:100%; height:100%;
114
+ background:#002147; display:flex; flex-direction:column;
 
115
  align-items:center; justify-content:center;
116
+ z-index:9999; opacity:1; transition:opacity 0.8s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
+ #splash.fade-out { opacity:0; pointer-events:none; }
119
+ #splash img { width:140px; animation:logoIn 0.7s ease forwards; opacity:0; }
120
+ #splash h1 { color:white; font-size:28px; font-weight:700;
121
+ margin:18px 0 6px 0; animation:textIn 0.7s ease 0.3s forwards;
122
+ opacity:0; letter-spacing:0.5px; }
123
+ #splash p { color:#aabbcc; font-size:14px; margin:0;
124
+ animation:textIn 0.7s ease 0.5s forwards; opacity:0; }
125
+ #splash .bar-track { width:200px; height:3px;
126
+ background:rgba(255,255,255,0.2); border-radius:2px;
127
+ margin-top:28px;
128
+ animation:textIn 0.7s ease 0.6s forwards; opacity:0; }
129
+ #splash .bar-fill { height:3px; width:0%; background:white;
130
+ border-radius:2px;
131
+ animation:barFill 1.8s ease 0.8s forwards; }
132
 
133
  @keyframes logoIn {
134
  from { opacity:0; transform:translateY(-20px); }
 
148
  $(document).ready(function() {
149
  setTimeout(function() {
150
  $('#splash').addClass('fade-out');
151
+ setTimeout(function() { $('#splash').remove(); }, 800);
 
 
152
  }, 2800);
153
  });
154
  "))
155
  ),
156
 
157
+ # splash
158
  tags$div(id="splash",
159
  tags$img(src="Butler-Bulldogs-Logo-1990.png"),
160
+ tags$h1("Butler Baseball TPD"),
161
  tags$p("Butler Baseball Analytics"),
162
+ tags$div(class="bar-track", tags$div(class="bar-fill"))
 
 
163
  ),
164
 
165
+ # header
166
  tags$div(class="header",
167
  tags$div(
168
  tags$h1("Butler Baseball Transfer Portal Dashboard"),
 
171
  tags$img(src="Butler-Bulldogs-Logo-1990.png", height="52px")
172
  ),
173
 
174
+ # filters
175
  tags$div(class="filters",
176
  tags$div(
177
  tags$div("Level", class="filter-label"),
178
  selectInput("level", label=NULL,
179
+ choices = c("D1","D2","D3","JUCO","NAIA"),
180
+ selected = "D1",
181
  width = "160px")
182
  ),
183
  tags$div(
 
190
  tags$div(
191
  tags$div("Min Pitches", class="filter-label"),
192
  numericInput("min_pitches", label=NULL,
193
+ value=100, min=1, max=2000, step=25,
194
+ width="160px")
195
  )
196
  ),
197
 
198
+ # main
199
  tags$div(class="main",
200
  uiOutput("main_ui")
201
  )
202
  )
203
 
204
+ # ── Server ─────────────────────────────────────────────────────────────────────
205
  server <- function(input, output, session) {
206
+
207
+ # load data reactively when level changes
208
+ raw_data <- reactive({
209
+ req(input$level)
210
+ load_data(input$level)
211
+ })
212
+
213
+ # build stats table
214
+ stats_table <- reactive({
215
+ req(raw_data(), input$min_pitches)
216
+ if (input$position == "Pitcher") {
217
+ build_pitcher_stats(raw_data(), input$min_pitches)
218
+ }
219
+ # hitter branch coming soon
220
+ })
221
+
222
  output$main_ui <- renderUI({
223
+ if (input$position == "Pitcher") {
224
+ DTOutput("pitcher_table")
225
+ } else {
226
+ tags$p("Hitter view coming soon.", style="color:#888;")
227
+ }
228
+ })
229
+
230
+ output$pitcher_table <- renderDT({
231
+ req(stats_table())
232
+ dt <- stats_table()
233
+
234
+ # ── Color ranges (edit these when ready) ──────────────────────────────────
235
+ # zone_pct_range <- c(42, 53) # bottom, top
236
+ # strike_pct_range <- c(58, 67)
237
+ # kbb_range <- c(5, 20)
238
+ # whiff_range <- c(15, 35)
239
+ # chase_range <- c(25, 40)
240
+ # hardhit_range <- c(25, 42)
241
+ # ──────────────────────────────────────────────────────────────────────────
242
+
243
+ datatable(
244
+ dt,
245
+ rownames = FALSE,
246
+ selection = "single",
247
+ options = list(
248
+ pageLength = 25,
249
+ lengthMenu = c(25, 50, 100),
250
+ scrollX = TRUE,
251
+ order = list(list(2, "desc")),
252
+ dom = "lfrtip",
253
+ columnDefs = list(
254
+ list(className="dt-center", targets=1:10),
255
+ list(className="dt-left", targets=0)
256
+ )
257
+ )
258
+ )
259
  })
260
  }
261