Spaces:
Sleeping
Sleeping
Update app.R
Browse files
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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 118 |
tags$div(class="filters",
|
| 119 |
tags$div(
|
| 120 |
tags$div("Level", class="filter-label"),
|
| 121 |
selectInput("level", label=NULL,
|
| 122 |
-
choices = c("
|
| 123 |
-
selected = "
|
| 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
|
| 137 |
-
width
|
| 138 |
)
|
| 139 |
),
|
| 140 |
|
| 141 |
-
#
|
| 142 |
tags$div(class="main",
|
| 143 |
uiOutput("main_ui")
|
| 144 |
)
|
| 145 |
)
|
| 146 |
|
|
|
|
| 147 |
server <- function(input, output, session) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
output$main_ui <- renderUI({
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|