Spaces:
Running
Running
Update app.R
Browse files
app.R
CHANGED
|
@@ -789,6 +789,30 @@ app_ui <- fluidPage(
|
|
| 789 |
padding: 20px;
|
| 790 |
margin-bottom: 15px;
|
| 791 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
"))
|
| 793 |
),
|
| 794 |
|
|
@@ -957,7 +981,7 @@ app_ui <- fluidPage(
|
|
| 957 |
)
|
| 958 |
),
|
| 959 |
|
| 960 |
-
# ββ
|
| 961 |
hr(),
|
| 962 |
fluidRow(
|
| 963 |
column(12,
|
|
@@ -1043,6 +1067,70 @@ app_ui <- fluidPage(
|
|
| 1043 |
)
|
| 1044 |
),
|
| 1045 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
# Download Tab
|
| 1047 |
tabPanel(
|
| 1048 |
"Download",
|
|
@@ -1168,9 +1256,21 @@ server <- function(input, output, session) {
|
|
| 1168 |
merge_result <- reactiveVal(NULL)
|
| 1169 |
scraped_data <- reactiveVal(NULL)
|
| 1170 |
scrape_polling <- reactiveVal(FALSE)
|
| 1171 |
-
|
| 1172 |
scrape_status_msg <- reactiveVal("Ready.")
|
| 1173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
# Handle column selection buttons
|
| 1175 |
observeEvent(input$select_all_cols, {
|
| 1176 |
updateCheckboxGroupInput(session, "columns_to_remove",
|
|
@@ -1210,9 +1310,28 @@ server <- function(input, output, session) {
|
|
| 1210 |
processed_data(processed_df)
|
| 1211 |
plot_data(processed_df)
|
| 1212 |
|
|
|
|
|
|
|
|
|
|
| 1213 |
return(processed_df)
|
| 1214 |
}
|
| 1215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1216 |
# Re-process data when date format changes
|
| 1217 |
observeEvent(input$date_format, {
|
| 1218 |
req(input$file)
|
|
@@ -1528,6 +1647,13 @@ server <- function(input, output, session) {
|
|
| 1528 |
"\u25CB Bat tracking: Not uploaded"
|
| 1529 |
}
|
| 1530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1531 |
summary_text <- paste(
|
| 1532 |
paste0("\u2713 ", format_label, " file processed successfully!"),
|
| 1533 |
paste("\u2713 Original columns:", ncol(original_df)),
|
|
@@ -1535,6 +1661,7 @@ server <- function(input, output, session) {
|
|
| 1535 |
paste("\u2713 Rows processed:", nrow(df)),
|
| 1536 |
removed_cols_text,
|
| 1537 |
bat_tracking_text,
|
|
|
|
| 1538 |
"\u2713 Duplicates removed",
|
| 1539 |
paste("\u2713 Date format:", if (input$date_format == "mdyy") "M/D/YY" else "YYYY-MM-DD"),
|
| 1540 |
sep = "\n"
|
|
@@ -2107,6 +2234,272 @@ server <- function(input, output, session) {
|
|
| 2107 |
# End Rule-Based Retagging
|
| 2108 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2110 |
# Click info output
|
| 2111 |
output$click_info <- renderText({
|
| 2112 |
if (!is.null(selected_pitch())) {
|
|
@@ -2124,6 +2517,7 @@ server <- function(input, output, session) {
|
|
| 2124 |
req(processed_data())
|
| 2125 |
df <- processed_data()
|
| 2126 |
result <- merge_result()
|
|
|
|
| 2127 |
|
| 2128 |
bat_tracking_summary <- if (!is.null(result) && result$matched > 0) {
|
| 2129 |
paste("Bat tracking data:", result$matched, "pitches with swing metrics")
|
|
@@ -2131,6 +2525,13 @@ server <- function(input, output, session) {
|
|
| 2131 |
"Bat tracking data: None"
|
| 2132 |
}
|
| 2133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2134 |
summary_text <- paste(
|
| 2135 |
paste("Total rows:", nrow(df)),
|
| 2136 |
paste("Total columns:", ncol(df)),
|
|
@@ -2153,6 +2554,7 @@ server <- function(input, output, session) {
|
|
| 2153 |
"TaggedPitchType column not available"
|
| 2154 |
}),
|
| 2155 |
bat_tracking_summary,
|
|
|
|
| 2156 |
paste("Source format:", toupper(uploaded_file_type())),
|
| 2157 |
paste("Date format:", if (input$date_format == "mdyy") "M/D/YY" else "YYYY-MM-DD"),
|
| 2158 |
sep = "\n"
|
|
@@ -2161,7 +2563,7 @@ server <- function(input, output, session) {
|
|
| 2161 |
return(summary_text)
|
| 2162 |
})
|
| 2163 |
|
| 2164 |
-
# Download handler: CSV or Parquet with custom filename
|
| 2165 |
output$downloadData <- downloadHandler(
|
| 2166 |
filename = function() {
|
| 2167 |
base_name <- gsub("[^A-Za-z0-9_\\-]", "_", input$download_filename)
|
|
@@ -2171,10 +2573,12 @@ server <- function(input, output, session) {
|
|
| 2171 |
paste0(base_name, ".", ext)
|
| 2172 |
},
|
| 2173 |
content = function(file) {
|
|
|
|
|
|
|
| 2174 |
if (input$download_format == "parquet") {
|
| 2175 |
-
arrow::write_parquet(
|
| 2176 |
} else {
|
| 2177 |
-
write.csv(
|
| 2178 |
}
|
| 2179 |
}
|
| 2180 |
)
|
|
|
|
| 789 |
padding: 20px;
|
| 790 |
margin-bottom: 15px;
|
| 791 |
}
|
| 792 |
+
|
| 793 |
+
/* Catcher notes styling */
|
| 794 |
+
.catcher-notes-input-box {
|
| 795 |
+
background: linear-gradient(135deg, #e8f4f8 0%, #f0e6d3 100%);
|
| 796 |
+
border: 2px solid darkcyan;
|
| 797 |
+
border-radius: 15px;
|
| 798 |
+
padding: 20px;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
.catcher-note-entry {
|
| 802 |
+
background: #fff;
|
| 803 |
+
border: 1px solid rgba(0,139,139,.15);
|
| 804 |
+
border-radius: 10px;
|
| 805 |
+
padding: 12px 16px;
|
| 806 |
+
margin-bottom: 8px;
|
| 807 |
+
display: flex;
|
| 808 |
+
justify-content: space-between;
|
| 809 |
+
align-items: center;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
.catcher-note-entry:hover {
|
| 813 |
+
border-color: darkcyan;
|
| 814 |
+
box-shadow: 0 2px 8px rgba(0,139,139,.12);
|
| 815 |
+
}
|
| 816 |
"))
|
| 817 |
),
|
| 818 |
|
|
|
|
| 981 |
)
|
| 982 |
),
|
| 983 |
|
| 984 |
+
# ββ Rule-Based Pitch Retagging Panel ββ
|
| 985 |
hr(),
|
| 986 |
fluidRow(
|
| 987 |
column(12,
|
|
|
|
| 1067 |
)
|
| 1068 |
),
|
| 1069 |
|
| 1070 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1071 |
+
# Catcher Notes Tab
|
| 1072 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1073 |
+
tabPanel(
|
| 1074 |
+
"Catcher Notes",
|
| 1075 |
+
fluidRow(
|
| 1076 |
+
column(5,
|
| 1077 |
+
div(class = "catcher-notes-input-box",
|
| 1078 |
+
h3("Add Catcher Note", style = "margin-top: 0; color: darkcyan; border-bottom: 2px solid darkcyan; padding-bottom: 8px;"),
|
| 1079 |
+
p(style = "color: #666; font-size: 13px; margin-bottom: 15px;",
|
| 1080 |
+
"Log catcher events (throws, wild pitches, passed balls). ",
|
| 1081 |
+
"Each note is matched to the pitch row by Catcher, Batter, Inning, and Count, ",
|
| 1082 |
+
"then written into a CatcherNotes column on download."),
|
| 1083 |
+
|
| 1084 |
+
fluidRow(
|
| 1085 |
+
column(6, selectInput("cn_catcher", "Catcher:", choices = NULL)),
|
| 1086 |
+
column(6, selectInput("cn_batter", "Batter:", choices = NULL))
|
| 1087 |
+
),
|
| 1088 |
+
|
| 1089 |
+
fluidRow(
|
| 1090 |
+
column(4, numericInput("cn_inning", "Inning:", value = 1, min = 1, max = 20, step = 1)),
|
| 1091 |
+
column(4, numericInput("cn_balls", "Balls:", value = 0, min = 0, max = 3, step = 1)),
|
| 1092 |
+
column(4, numericInput("cn_strikes", "Strikes:", value = 0, min = 0, max = 2, step = 1))
|
| 1093 |
+
),
|
| 1094 |
+
|
| 1095 |
+
selectInput("cn_result", "Result:",
|
| 1096 |
+
choices = c("2B Out", "2B Safe", "3B Out", "3B Safe",
|
| 1097 |
+
"Wild Pitch", "Passed Ball",
|
| 1098 |
+
"Pickoff Attempt", "Pickoff Out",
|
| 1099 |
+
"Blocked Ball", "Other"),
|
| 1100 |
+
selected = "2B Out"),
|
| 1101 |
+
|
| 1102 |
+
conditionalPanel(
|
| 1103 |
+
condition = "input.cn_result == 'Other'",
|
| 1104 |
+
textInput("cn_custom_result", "Custom Note:", placeholder = "Describe the event...")
|
| 1105 |
+
),
|
| 1106 |
+
|
| 1107 |
+
br(),
|
| 1108 |
+
actionButton("cn_add_btn", "Add Note", class = "btn-success",
|
| 1109 |
+
style = "width: 100%; font-weight: bold; font-size: 15px;"),
|
| 1110 |
+
br(), br(),
|
| 1111 |
+
uiOutput("cn_match_feedback")
|
| 1112 |
+
)
|
| 1113 |
+
),
|
| 1114 |
+
|
| 1115 |
+
column(7,
|
| 1116 |
+
h3("Logged Catcher Notes"),
|
| 1117 |
+
p(style = "color: #666; font-size: 13px;",
|
| 1118 |
+
"These notes will be merged into the CatcherNotes column when you download the data."),
|
| 1119 |
+
DT::dataTableOutput("cn_notes_table"),
|
| 1120 |
+
br(),
|
| 1121 |
+
fluidRow(
|
| 1122 |
+
column(6,
|
| 1123 |
+
actionButton("cn_clear_all_btn", "Clear All Notes", class = "btn-danger",
|
| 1124 |
+
style = "width: 100%;")
|
| 1125 |
+
),
|
| 1126 |
+
column(6,
|
| 1127 |
+
verbatimTextOutput("cn_summary")
|
| 1128 |
+
)
|
| 1129 |
+
)
|
| 1130 |
+
)
|
| 1131 |
+
)
|
| 1132 |
+
),
|
| 1133 |
+
|
| 1134 |
# Download Tab
|
| 1135 |
tabPanel(
|
| 1136 |
"Download",
|
|
|
|
| 1256 |
merge_result <- reactiveVal(NULL)
|
| 1257 |
scraped_data <- reactiveVal(NULL)
|
| 1258 |
scrape_polling <- reactiveVal(FALSE)
|
|
|
|
| 1259 |
scrape_status_msg <- reactiveVal("Ready.")
|
| 1260 |
|
| 1261 |
+
# Catcher Notes: stored as a list of data frames, each row is one note
|
| 1262 |
+
catcher_notes_list <- reactiveVal(data.frame(
|
| 1263 |
+
NoteID = integer(0),
|
| 1264 |
+
Catcher = character(0),
|
| 1265 |
+
Batter = character(0),
|
| 1266 |
+
Inning = integer(0),
|
| 1267 |
+
Balls = integer(0),
|
| 1268 |
+
Strikes = integer(0),
|
| 1269 |
+
Result = character(0),
|
| 1270 |
+
MatchedRow = integer(0),
|
| 1271 |
+
stringsAsFactors = FALSE
|
| 1272 |
+
))
|
| 1273 |
+
|
| 1274 |
# Handle column selection buttons
|
| 1275 |
observeEvent(input$select_all_cols, {
|
| 1276 |
updateCheckboxGroupInput(session, "columns_to_remove",
|
|
|
|
| 1310 |
processed_data(processed_df)
|
| 1311 |
plot_data(processed_df)
|
| 1312 |
|
| 1313 |
+
# Update catcher notes dropdowns
|
| 1314 |
+
update_catcher_note_choices(processed_df)
|
| 1315 |
+
|
| 1316 |
return(processed_df)
|
| 1317 |
}
|
| 1318 |
|
| 1319 |
+
# Helper to populate Catcher Notes dropdowns from current data
|
| 1320 |
+
update_catcher_note_choices <- function(df) {
|
| 1321 |
+
if (!is.null(df)) {
|
| 1322 |
+
if ("Catcher" %in% names(df)) {
|
| 1323 |
+
catchers <- sort(unique(df$Catcher[!is.na(df$Catcher) & df$Catcher != ""]))
|
| 1324 |
+
updateSelectInput(session, "cn_catcher", choices = catchers,
|
| 1325 |
+
selected = if (length(catchers) > 0) catchers[1] else NULL)
|
| 1326 |
+
}
|
| 1327 |
+
if ("Batter" %in% names(df)) {
|
| 1328 |
+
batters <- sort(unique(df$Batter[!is.na(df$Batter) & df$Batter != ""]))
|
| 1329 |
+
updateSelectInput(session, "cn_batter", choices = batters,
|
| 1330 |
+
selected = if (length(batters) > 0) batters[1] else NULL)
|
| 1331 |
+
}
|
| 1332 |
+
}
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
# Re-process data when date format changes
|
| 1336 |
observeEvent(input$date_format, {
|
| 1337 |
req(input$file)
|
|
|
|
| 1647 |
"\u25CB Bat tracking: Not uploaded"
|
| 1648 |
}
|
| 1649 |
|
| 1650 |
+
notes <- catcher_notes_list()
|
| 1651 |
+
notes_text <- if (nrow(notes) > 0) {
|
| 1652 |
+
paste("\u2713 Catcher notes:", nrow(notes), "logged")
|
| 1653 |
+
} else {
|
| 1654 |
+
"\u25CB Catcher notes: None"
|
| 1655 |
+
}
|
| 1656 |
+
|
| 1657 |
summary_text <- paste(
|
| 1658 |
paste0("\u2713 ", format_label, " file processed successfully!"),
|
| 1659 |
paste("\u2713 Original columns:", ncol(original_df)),
|
|
|
|
| 1661 |
paste("\u2713 Rows processed:", nrow(df)),
|
| 1662 |
removed_cols_text,
|
| 1663 |
bat_tracking_text,
|
| 1664 |
+
notes_text,
|
| 1665 |
"\u2713 Duplicates removed",
|
| 1666 |
paste("\u2713 Date format:", if (input$date_format == "mdyy") "M/D/YY" else "YYYY-MM-DD"),
|
| 1667 |
sep = "\n"
|
|
|
|
| 2234 |
# End Rule-Based Retagging
|
| 2235 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2236 |
|
| 2237 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2238 |
+
# Catcher Notes β Server Logic
|
| 2239 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2240 |
+
|
| 2241 |
+
# Add a catcher note
|
| 2242 |
+
observeEvent(input$cn_add_btn, {
|
| 2243 |
+
req(processed_data(), input$cn_catcher, input$cn_batter)
|
| 2244 |
+
|
| 2245 |
+
df <- processed_data()
|
| 2246 |
+
|
| 2247 |
+
# Determine the result text
|
| 2248 |
+
result_text <- input$cn_result
|
| 2249 |
+
if (result_text == "Other" && !is.null(input$cn_custom_result) && nzchar(trimws(input$cn_custom_result))) {
|
| 2250 |
+
result_text <- trimws(input$cn_custom_result)
|
| 2251 |
+
}
|
| 2252 |
+
|
| 2253 |
+
# Find the matching row(s): Catcher + Batter + Inning + Balls + Strikes
|
| 2254 |
+
# We match the LAST pitch in that count for that matchup in that inning
|
| 2255 |
+
# (the event most likely happened on the final pitch of that count)
|
| 2256 |
+
has_catcher <- "Catcher" %in% names(df)
|
| 2257 |
+
has_batter <- "Batter" %in% names(df)
|
| 2258 |
+
has_inning <- "Inning" %in% names(df)
|
| 2259 |
+
has_balls <- "Balls" %in% names(df)
|
| 2260 |
+
has_strikes <- "Strikes" %in% names(df)
|
| 2261 |
+
|
| 2262 |
+
candidates <- df
|
| 2263 |
+
if (has_catcher) candidates <- candidates %>% filter(Catcher == input$cn_catcher)
|
| 2264 |
+
if (has_batter) candidates <- candidates %>% filter(Batter == input$cn_batter)
|
| 2265 |
+
if (has_inning) candidates <- candidates %>% filter(Inning == input$cn_inning)
|
| 2266 |
+
if (has_balls) candidates <- candidates %>% filter(Balls == input$cn_balls)
|
| 2267 |
+
if (has_strikes) candidates <- candidates %>% filter(Strikes == input$cn_strikes)
|
| 2268 |
+
|
| 2269 |
+
# Get the row index in the full dataframe for the last matching pitch
|
| 2270 |
+
if (nrow(candidates) > 0) {
|
| 2271 |
+
# Find which rows in the full df match
|
| 2272 |
+
match_idx <- which(
|
| 2273 |
+
(if (has_catcher) df$Catcher == input$cn_catcher else TRUE) &
|
| 2274 |
+
(if (has_batter) df$Batter == input$cn_batter else TRUE) &
|
| 2275 |
+
(if (has_inning) df$Inning == input$cn_inning else TRUE) &
|
| 2276 |
+
(if (has_balls) df$Balls == input$cn_balls else TRUE) &
|
| 2277 |
+
(if (has_strikes) df$Strikes == input$cn_strikes else TRUE)
|
| 2278 |
+
)
|
| 2279 |
+
matched_row <- max(match_idx) # last pitch at that count
|
| 2280 |
+
} else {
|
| 2281 |
+
matched_row <- NA_integer_
|
| 2282 |
+
}
|
| 2283 |
+
|
| 2284 |
+
# Build the new note
|
| 2285 |
+
notes <- catcher_notes_list()
|
| 2286 |
+
new_id <- if (nrow(notes) == 0) 1L else max(notes$NoteID) + 1L
|
| 2287 |
+
|
| 2288 |
+
new_note <- data.frame(
|
| 2289 |
+
NoteID = new_id,
|
| 2290 |
+
Catcher = input$cn_catcher,
|
| 2291 |
+
Batter = input$cn_batter,
|
| 2292 |
+
Inning = as.integer(input$cn_inning),
|
| 2293 |
+
Balls = as.integer(input$cn_balls),
|
| 2294 |
+
Strikes = as.integer(input$cn_strikes),
|
| 2295 |
+
Result = result_text,
|
| 2296 |
+
MatchedRow = matched_row,
|
| 2297 |
+
stringsAsFactors = FALSE
|
| 2298 |
+
)
|
| 2299 |
+
|
| 2300 |
+
catcher_notes_list(bind_rows(notes, new_note))
|
| 2301 |
+
|
| 2302 |
+
# Show match feedback
|
| 2303 |
+
output$cn_match_feedback <- renderUI({
|
| 2304 |
+
if (!is.na(matched_row)) {
|
| 2305 |
+
pitch_info <- df[matched_row, ]
|
| 2306 |
+
detail_parts <- c()
|
| 2307 |
+
if ("PitchNo" %in% names(pitch_info) && !is.na(pitch_info$PitchNo))
|
| 2308 |
+
detail_parts <- c(detail_parts, paste("Pitch #", pitch_info$PitchNo))
|
| 2309 |
+
if ("PitchCall" %in% names(pitch_info) && !is.na(pitch_info$PitchCall))
|
| 2310 |
+
detail_parts <- c(detail_parts, pitch_info$PitchCall)
|
| 2311 |
+
if ("Pitcher" %in% names(pitch_info) && !is.na(pitch_info$Pitcher))
|
| 2312 |
+
detail_parts <- c(detail_parts, paste("vs", pitch_info$Pitcher))
|
| 2313 |
+
|
| 2314 |
+
div(class = "merge-status-box merge-success",
|
| 2315 |
+
style = "margin-top: 10px;",
|
| 2316 |
+
p(style = "margin: 0; font-weight: 600; color: #155724;",
|
| 2317 |
+
paste0("\u2713 Matched to row ", matched_row)),
|
| 2318 |
+
if (length(detail_parts) > 0)
|
| 2319 |
+
p(style = "margin: 4px 0 0 0; color: #155724; font-size: 13px;",
|
| 2320 |
+
paste(detail_parts, collapse = " | "))
|
| 2321 |
+
)
|
| 2322 |
+
} else {
|
| 2323 |
+
div(class = "merge-status-box merge-warning",
|
| 2324 |
+
style = "margin-top: 10px;",
|
| 2325 |
+
p(style = "margin: 0; font-weight: 600; color: #856404;",
|
| 2326 |
+
paste0("\u26A0 No matching pitch found for ", input$cn_catcher,
|
| 2327 |
+
" / ", input$cn_batter, " / Inn ", input$cn_inning,
|
| 2328 |
+
" / ", input$cn_balls, "-", input$cn_strikes)),
|
| 2329 |
+
p(style = "margin: 4px 0 0 0; color: #856404; font-size: 13px;",
|
| 2330 |
+
"Note saved anyway \u2014 it will appear in CatcherNotes column as unmatched.")
|
| 2331 |
+
)
|
| 2332 |
+
}
|
| 2333 |
+
})
|
| 2334 |
+
|
| 2335 |
+
showNotification(
|
| 2336 |
+
paste0("Added: ", result_text, " (", input$cn_catcher, " / ", input$cn_batter,
|
| 2337 |
+
" / Inn ", input$cn_inning, " / ", input$cn_balls, "-", input$cn_strikes, ")"),
|
| 2338 |
+
type = "message", duration = 3
|
| 2339 |
+
)
|
| 2340 |
+
})
|
| 2341 |
+
|
| 2342 |
+
# Render the notes table
|
| 2343 |
+
output$cn_notes_table <- DT::renderDataTable({
|
| 2344 |
+
notes <- catcher_notes_list()
|
| 2345 |
+
if (nrow(notes) == 0) return(NULL)
|
| 2346 |
+
|
| 2347 |
+
display_notes <- notes %>%
|
| 2348 |
+
mutate(
|
| 2349 |
+
Count = paste0(Balls, "-", Strikes),
|
| 2350 |
+
Match = ifelse(is.na(MatchedRow), "\u2717 No match", paste0("\u2713 Row ", MatchedRow))
|
| 2351 |
+
) %>%
|
| 2352 |
+
select(NoteID, Catcher, Batter, Inning, Count, Result, Match)
|
| 2353 |
+
|
| 2354 |
+
DT::datatable(
|
| 2355 |
+
display_notes,
|
| 2356 |
+
options = list(
|
| 2357 |
+
scrollX = TRUE, pageLength = 15, dom = "tip",
|
| 2358 |
+
columnDefs = list(list(className = "dt-center", targets = "_all"))
|
| 2359 |
+
),
|
| 2360 |
+
rownames = FALSE,
|
| 2361 |
+
selection = "single",
|
| 2362 |
+
callback = DT::JS("
|
| 2363 |
+
table.on('click', 'tr', function() {
|
| 2364 |
+
var data = table.row(this).data();
|
| 2365 |
+
if (data) {
|
| 2366 |
+
Shiny.setInputValue('cn_delete_row', data[0], {priority: 'event'});
|
| 2367 |
+
}
|
| 2368 |
+
});
|
| 2369 |
+
")
|
| 2370 |
+
) %>%
|
| 2371 |
+
DT::formatStyle("Match",
|
| 2372 |
+
color = DT::styleEqual(c("\u2717 No match"), c("#dc3545")),
|
| 2373 |
+
fontWeight = "bold"
|
| 2374 |
+
)
|
| 2375 |
+
})
|
| 2376 |
+
|
| 2377 |
+
# Delete a single note by clicking its row
|
| 2378 |
+
observeEvent(input$cn_delete_row, {
|
| 2379 |
+
notes <- catcher_notes_list()
|
| 2380 |
+
note_id <- as.integer(input$cn_delete_row)
|
| 2381 |
+
|
| 2382 |
+
if (note_id %in% notes$NoteID) {
|
| 2383 |
+
showModal(modalDialog(
|
| 2384 |
+
title = "Delete Catcher Note?",
|
| 2385 |
+
p(paste("Remove note #", note_id, "?")),
|
| 2386 |
+
footer = tagList(
|
| 2387 |
+
actionButton("cn_confirm_delete", "Delete", class = "btn-danger"),
|
| 2388 |
+
modalButton("Cancel")
|
| 2389 |
+
),
|
| 2390 |
+
size = "s", easyClose = TRUE
|
| 2391 |
+
))
|
| 2392 |
+
}
|
| 2393 |
+
})
|
| 2394 |
+
|
| 2395 |
+
observeEvent(input$cn_confirm_delete, {
|
| 2396 |
+
notes <- catcher_notes_list()
|
| 2397 |
+
note_id <- as.integer(input$cn_delete_row)
|
| 2398 |
+
catcher_notes_list(notes %>% filter(NoteID != note_id))
|
| 2399 |
+
removeModal()
|
| 2400 |
+
showNotification(paste("Deleted note #", note_id), type = "message", duration = 2)
|
| 2401 |
+
})
|
| 2402 |
+
|
| 2403 |
+
# Clear all notes
|
| 2404 |
+
observeEvent(input$cn_clear_all_btn, {
|
| 2405 |
+
showModal(modalDialog(
|
| 2406 |
+
title = "Clear All Catcher Notes?",
|
| 2407 |
+
p("This will remove all logged catcher notes. This cannot be undone."),
|
| 2408 |
+
footer = tagList(
|
| 2409 |
+
actionButton("cn_confirm_clear_all", "Clear All", class = "btn-danger"),
|
| 2410 |
+
modalButton("Cancel")
|
| 2411 |
+
),
|
| 2412 |
+
size = "s", easyClose = TRUE
|
| 2413 |
+
))
|
| 2414 |
+
})
|
| 2415 |
+
|
| 2416 |
+
observeEvent(input$cn_confirm_clear_all, {
|
| 2417 |
+
catcher_notes_list(data.frame(
|
| 2418 |
+
NoteID = integer(0), Catcher = character(0), Batter = character(0),
|
| 2419 |
+
Inning = integer(0), Balls = integer(0), Strikes = integer(0),
|
| 2420 |
+
Result = character(0), MatchedRow = integer(0), stringsAsFactors = FALSE
|
| 2421 |
+
))
|
| 2422 |
+
removeModal()
|
| 2423 |
+
output$cn_match_feedback <- renderUI({ NULL })
|
| 2424 |
+
showNotification("All catcher notes cleared.", type = "message", duration = 2)
|
| 2425 |
+
})
|
| 2426 |
+
|
| 2427 |
+
# Catcher notes summary
|
| 2428 |
+
output$cn_summary <- renderText({
|
| 2429 |
+
notes <- catcher_notes_list()
|
| 2430 |
+
if (nrow(notes) == 0) return("No notes logged yet.")
|
| 2431 |
+
|
| 2432 |
+
n_matched <- sum(!is.na(notes$MatchedRow))
|
| 2433 |
+
n_unmatched <- sum(is.na(notes$MatchedRow))
|
| 2434 |
+
result_counts <- table(notes$Result)
|
| 2435 |
+
result_str <- paste(names(result_counts), "(", result_counts, ")", collapse = ", ")
|
| 2436 |
+
|
| 2437 |
+
paste(
|
| 2438 |
+
paste("Total notes:", nrow(notes)),
|
| 2439 |
+
paste("Matched:", n_matched, "| Unmatched:", n_unmatched),
|
| 2440 |
+
paste("Results:", result_str),
|
| 2441 |
+
sep = "\n"
|
| 2442 |
+
)
|
| 2443 |
+
})
|
| 2444 |
+
|
| 2445 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2446 |
+
# Helper: merge catcher notes into data for download
|
| 2447 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2448 |
+
|
| 2449 |
+
build_download_data <- function() {
|
| 2450 |
+
df <- processed_data()
|
| 2451 |
+
if (is.null(df)) return(NULL)
|
| 2452 |
+
|
| 2453 |
+
notes <- catcher_notes_list()
|
| 2454 |
+
|
| 2455 |
+
if (nrow(notes) == 0) return(df)
|
| 2456 |
+
|
| 2457 |
+
# Initialize CatcherNotes column if not present
|
| 2458 |
+
if (!"CatcherNotes" %in% names(df)) {
|
| 2459 |
+
df$CatcherNotes <- NA_character_
|
| 2460 |
+
}
|
| 2461 |
+
|
| 2462 |
+
# For each note, write the result into the matched row
|
| 2463 |
+
# If multiple notes match the same row, concatenate with " | "
|
| 2464 |
+
for (i in seq_len(nrow(notes))) {
|
| 2465 |
+
row_idx <- notes$MatchedRow[i]
|
| 2466 |
+
result <- notes$Result[i]
|
| 2467 |
+
|
| 2468 |
+
if (!is.na(row_idx) && row_idx >= 1 && row_idx <= nrow(df)) {
|
| 2469 |
+
existing <- df$CatcherNotes[row_idx]
|
| 2470 |
+
if (is.na(existing) || existing == "") {
|
| 2471 |
+
df$CatcherNotes[row_idx] <- result
|
| 2472 |
+
} else {
|
| 2473 |
+
df$CatcherNotes[row_idx] <- paste(existing, result, sep = " | ")
|
| 2474 |
+
}
|
| 2475 |
+
}
|
| 2476 |
+
}
|
| 2477 |
+
|
| 2478 |
+
# Also add unmatched notes as a summary attribute (append to last row as a note)
|
| 2479 |
+
unmatched <- notes %>% filter(is.na(MatchedRow))
|
| 2480 |
+
if (nrow(unmatched) > 0) {
|
| 2481 |
+
unmatched_texts <- paste0(
|
| 2482 |
+
unmatched$Result, " (", unmatched$Catcher, "/", unmatched$Batter,
|
| 2483 |
+
" Inn", unmatched$Inning, " ", unmatched$Balls, "-", unmatched$Strikes, ")"
|
| 2484 |
+
)
|
| 2485 |
+
# Put unmatched notes in the last row's CatcherNotes with a prefix
|
| 2486 |
+
last_row <- nrow(df)
|
| 2487 |
+
existing <- df$CatcherNotes[last_row]
|
| 2488 |
+
unmatched_str <- paste0("[UNMATCHED] ", paste(unmatched_texts, collapse = "; "))
|
| 2489 |
+
if (is.na(existing) || existing == "") {
|
| 2490 |
+
df$CatcherNotes[last_row] <- unmatched_str
|
| 2491 |
+
} else {
|
| 2492 |
+
df$CatcherNotes[last_row] <- paste(existing, unmatched_str, sep = " | ")
|
| 2493 |
+
}
|
| 2494 |
+
}
|
| 2495 |
+
|
| 2496 |
+
return(df)
|
| 2497 |
+
}
|
| 2498 |
+
|
| 2499 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2500 |
+
# End Catcher Notes
|
| 2501 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2502 |
+
|
| 2503 |
# Click info output
|
| 2504 |
output$click_info <- renderText({
|
| 2505 |
if (!is.null(selected_pitch())) {
|
|
|
|
| 2517 |
req(processed_data())
|
| 2518 |
df <- processed_data()
|
| 2519 |
result <- merge_result()
|
| 2520 |
+
notes <- catcher_notes_list()
|
| 2521 |
|
| 2522 |
bat_tracking_summary <- if (!is.null(result) && result$matched > 0) {
|
| 2523 |
paste("Bat tracking data:", result$matched, "pitches with swing metrics")
|
|
|
|
| 2525 |
"Bat tracking data: None"
|
| 2526 |
}
|
| 2527 |
|
| 2528 |
+
notes_summary <- if (nrow(notes) > 0) {
|
| 2529 |
+
n_matched <- sum(!is.na(notes$MatchedRow))
|
| 2530 |
+
paste0("Catcher notes: ", nrow(notes), " total (", n_matched, " matched to rows)")
|
| 2531 |
+
} else {
|
| 2532 |
+
"Catcher notes: None"
|
| 2533 |
+
}
|
| 2534 |
+
|
| 2535 |
summary_text <- paste(
|
| 2536 |
paste("Total rows:", nrow(df)),
|
| 2537 |
paste("Total columns:", ncol(df)),
|
|
|
|
| 2554 |
"TaggedPitchType column not available"
|
| 2555 |
}),
|
| 2556 |
bat_tracking_summary,
|
| 2557 |
+
notes_summary,
|
| 2558 |
paste("Source format:", toupper(uploaded_file_type())),
|
| 2559 |
paste("Date format:", if (input$date_format == "mdyy") "M/D/YY" else "YYYY-MM-DD"),
|
| 2560 |
sep = "\n"
|
|
|
|
| 2563 |
return(summary_text)
|
| 2564 |
})
|
| 2565 |
|
| 2566 |
+
# Download handler: CSV or Parquet with custom filename β NOW includes catcher notes
|
| 2567 |
output$downloadData <- downloadHandler(
|
| 2568 |
filename = function() {
|
| 2569 |
base_name <- gsub("[^A-Za-z0-9_\\-]", "_", input$download_filename)
|
|
|
|
| 2573 |
paste0(base_name, ".", ext)
|
| 2574 |
},
|
| 2575 |
content = function(file) {
|
| 2576 |
+
download_df <- build_download_data()
|
| 2577 |
+
|
| 2578 |
if (input$download_format == "parquet") {
|
| 2579 |
+
arrow::write_parquet(download_df, file)
|
| 2580 |
} else {
|
| 2581 |
+
write.csv(download_df, file, row.names = FALSE)
|
| 2582 |
}
|
| 2583 |
}
|
| 2584 |
)
|