Spaces:
Running
Running
Update app.R
Browse files
app.R
CHANGED
|
@@ -173,7 +173,7 @@ ui <- fluidPage(
|
|
| 173 |
h3("Pitch Metrics Summary"),
|
| 174 |
DTOutput("movement_stats"),
|
| 175 |
h3("Location Plot (Editable)"),
|
| 176 |
-
plotlyOutput("location_plot", height="600px")
|
| 177 |
)
|
| 178 |
)
|
| 179 |
),
|
|
@@ -197,7 +197,7 @@ server <- function(input, output, session){
|
|
| 197 |
processed_data <- reactiveVal(NULL)
|
| 198 |
plot_data <- reactiveVal(NULL)
|
| 199 |
selected_pitch <- reactiveVal(NULL)
|
| 200 |
-
selected_keys <- reactiveVal(integer(0))
|
| 201 |
|
| 202 |
# quick-select buttons
|
| 203 |
observeEvent(input$select_all_cols, { updateCheckboxGroupInput(session,"columns_to_remove", selected = columns_to_remove) })
|
|
@@ -231,13 +231,13 @@ server <- function(input, output, session){
|
|
| 231 |
have <- intersect(names(df), num_cols)
|
| 232 |
df[have] <- lapply(df[have], function(x) suppressWarnings(as.numeric(x)))
|
| 233 |
|
| 234 |
-
df <- df %>% distinct()
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
processed_data(df)
|
| 239 |
-
plot_data(df)
|
| 240 |
|
|
|
|
|
|
|
| 241 |
|
| 242 |
if ("Pitcher" %in% names(df)) {
|
| 243 |
choices <- sort(unique(df$Pitcher[!is.na(df$Pitcher)]))
|
|
@@ -275,85 +275,91 @@ plot_data(df)
|
|
| 275 |
datatable(processed_data(), options=list(scrollX=TRUE, pageLength=10), filter="top")
|
| 276 |
})
|
| 277 |
|
| 278 |
-
pitcher_df <- reactive({
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
) |>
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
"
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
shapes = list(
|
| 342 |
-
list(type="rect", x0=-0.8303, x1=0.8303, y0=1.6, y1=3.5,
|
| 343 |
-
line=list(color="black", width=1), fillcolor="rgba(0,0,0,0)"),
|
| 344 |
-
list(type="path",
|
| 345 |
-
path="M -0.708 0.15 L 0.708 0.15 L 0.708 0.3 L 0 0.5 L -0.708 0.3 Z",
|
| 346 |
-
line=list(color="black", width=0.8))
|
| 347 |
-
)
|
| 348 |
) |>
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
# ---- Click handlers (single mode) for both plots ----
|
| 353 |
show_pitch_modal <- function(hit_row){
|
| 354 |
selected_pitch(list(
|
| 355 |
pitcher = input$pitcher_select,
|
| 356 |
-
|
| 357 |
data = hit_row[1,],
|
| 358 |
original_type = hit_row$TaggedPitchType[1]
|
| 359 |
))
|
|
@@ -377,16 +383,21 @@ output$location_plot <- renderPlotly({
|
|
| 377 |
|
| 378 |
observeEvent(event_data("plotly_click", source="mv"), {
|
| 379 |
req(input$selection_mode == "single")
|
| 380 |
-
clk <- event_data("plotly_click", source="mv")
|
|
|
|
| 381 |
key <- clk$key[[1]]
|
| 382 |
-
d <- pitcher_df()
|
|
|
|
| 383 |
if (nrow(hit)==1) show_pitch_modal(hit)
|
| 384 |
})
|
|
|
|
| 385 |
observeEvent(event_data("plotly_click", source="loc"), {
|
| 386 |
req(input$selection_mode == "single")
|
| 387 |
-
clk <- event_data("plotly_click", source="loc")
|
|
|
|
| 388 |
key <- clk$key[[1]]
|
| 389 |
-
d <- pitcher_df()
|
|
|
|
| 390 |
if (nrow(hit)==1) show_pitch_modal(hit)
|
| 391 |
})
|
| 392 |
|
|
@@ -407,41 +418,31 @@ output$location_plot <- renderPlotly({
|
|
| 407 |
)
|
| 408 |
})
|
| 409 |
|
| 410 |
-
observeEvent(input$update_pitch, {
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
| 413 |
|
| 414 |
-
|
| 415 |
-
|
|
|
|
| 416 |
|
| 417 |
-
|
| 418 |
-
|
| 419 |
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
showNotification("Updated pitch tag.", type="message", duration=3)
|
| 425 |
-
selected_pitch(NULL)
|
| 426 |
-
} else {
|
| 427 |
-
showNotification("Could not map selection back to dataset.", type="error")
|
| 428 |
-
}
|
| 429 |
-
})
|
| 430 |
-
|
| 431 |
-
# map row_key back to absolute index for this pitcher slice
|
| 432 |
-
idx_pitcher <- which(cur$Pitcher == info$pitcher)
|
| 433 |
-
if (length(idx_pitcher) >= info$row_key && info$row_key > 0) {
|
| 434 |
-
abs_row <- idx_pitcher[info$row_key]
|
| 435 |
-
cur$TaggedPitchType[abs_row] <- new_type
|
| 436 |
-
plot_data(cur); processed_data(cur)
|
| 437 |
removeModal()
|
| 438 |
-
showNotification(
|
| 439 |
-
type="message", duration=3)
|
| 440 |
selected_pitch(NULL)
|
| 441 |
} else {
|
| 442 |
showNotification("Could not map selection back to dataset.", type="error")
|
| 443 |
}
|
| 444 |
-
})
|
|
|
|
| 445 |
observeEvent(input$cancel_edit, { removeModal(); selected_pitch(NULL) })
|
| 446 |
|
| 447 |
# ---- Drag select (either chart) ----
|
|
@@ -450,6 +451,7 @@ observeEvent(input$update_pitch, {
|
|
| 450 |
ev <- event_data("plotly_selected", source="mv")
|
| 451 |
if (!is.null(ev) && length(ev$key)) selected_keys(unique(as.integer(ev$key)))
|
| 452 |
})
|
|
|
|
| 453 |
observeEvent(event_data("plotly_selected", source="loc"), {
|
| 454 |
req(input$selection_mode == "drag")
|
| 455 |
ev <- event_data("plotly_selected", source="loc")
|
|
@@ -460,33 +462,38 @@ observeEvent(input$update_pitch, {
|
|
| 460 |
output$selection_info <- renderText({
|
| 461 |
keys <- selected_keys()
|
| 462 |
if (!length(keys)) return("No points selected.")
|
| 463 |
-
d <- pitcher_df()
|
|
|
|
| 464 |
cnt <- sort(table(sel$TaggedPitchType), decreasing = TRUE)
|
| 465 |
paste(nrow(sel), "points selected:", paste(names(cnt), "(", cnt, ")", collapse=", "))
|
| 466 |
})
|
| 467 |
|
| 468 |
-
observeEvent(input$apply_bulk_change, {
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
| 472 |
|
| 473 |
-
|
| 474 |
-
|
|
|
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
hit_idx <- which(cur$.uid %in% uids & cur$Pitcher == input$pitcher_select)
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
|
|
|
| 490 |
|
| 491 |
# click info small text
|
| 492 |
output$click_info <- renderText({
|
|
@@ -544,10 +551,11 @@ observeEvent(input$apply_bulk_change, {
|
|
| 544 |
datatable(summary_stats, options=list(pageLength=15, dom='t', scrollX=TRUE), rownames=FALSE) %>%
|
| 545 |
formatStyle(columns = names(summary_stats), fontSize='12px')
|
| 546 |
})
|
| 547 |
-
|
| 548 |
# data summary + download
|
| 549 |
output$data_summary <- renderText({
|
| 550 |
-
req(processed_data())
|
|
|
|
| 551 |
paste(
|
| 552 |
paste("Total rows:", nrow(df)),
|
| 553 |
paste("Total columns:", ncol(df)),
|
|
@@ -555,11 +563,16 @@ observeEvent(input$apply_bulk_change, {
|
|
| 555 |
if ("Date" %in% names(df) && !all(is.na(df$Date))) {
|
| 556 |
paste(min(as.Date(df$Date), na.rm=TRUE), "to", max(as.Date(df$Date), na.rm=TRUE))
|
| 557 |
} else "Date column not available"),
|
| 558 |
-
paste("Unique pitchers:",
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
sep = "\n"
|
| 561 |
)
|
| 562 |
})
|
|
|
|
| 563 |
output$downloadData <- downloadHandler(
|
| 564 |
filename = function(){ paste0("app_ready_COA_", Sys.Date(), ".csv") },
|
| 565 |
content = function(file){ write.csv(processed_data(), file, row.names=FALSE) }
|
|
|
|
| 173 |
h3("Pitch Metrics Summary"),
|
| 174 |
DTOutput("movement_stats"),
|
| 175 |
h3("Location Plot (Editable)"),
|
| 176 |
+
plotlyOutput("location_plot", height="600px")
|
| 177 |
)
|
| 178 |
)
|
| 179 |
),
|
|
|
|
| 197 |
processed_data <- reactiveVal(NULL)
|
| 198 |
plot_data <- reactiveVal(NULL)
|
| 199 |
selected_pitch <- reactiveVal(NULL)
|
| 200 |
+
selected_keys <- reactiveVal(integer(0))
|
| 201 |
|
| 202 |
# quick-select buttons
|
| 203 |
observeEvent(input$select_all_cols, { updateCheckboxGroupInput(session,"columns_to_remove", selected = columns_to_remove) })
|
|
|
|
| 231 |
have <- intersect(names(df), num_cols)
|
| 232 |
df[have] <- lapply(df[have], function(x) suppressWarnings(as.numeric(x)))
|
| 233 |
|
| 234 |
+
df <- df %>% distinct()
|
| 235 |
|
| 236 |
+
# Create unique identifiers
|
| 237 |
+
if (!".uid" %in% names(df)) df$.uid <- seq_len(nrow(df))
|
|
|
|
|
|
|
| 238 |
|
| 239 |
+
processed_data(df)
|
| 240 |
+
plot_data(df)
|
| 241 |
|
| 242 |
if ("Pitcher" %in% names(df)) {
|
| 243 |
choices <- sort(unique(df$Pitcher[!is.na(df$Pitcher)]))
|
|
|
|
| 275 |
datatable(processed_data(), options=list(scrollX=TRUE, pageLength=10), filter="top")
|
| 276 |
})
|
| 277 |
|
| 278 |
+
pitcher_df <- reactive({
|
| 279 |
+
req(plot_data(), input$pitcher_select)
|
| 280 |
+
d <- plot_data() %>%
|
| 281 |
+
filter(Pitcher == input$pitcher_select) %>%
|
| 282 |
+
filter(!is.na(HorzBreak), !is.na(InducedVertBreak), !is.na(RelSpeed))
|
| 283 |
+
|
| 284 |
+
# Add row_key for this pitcher's subset
|
| 285 |
+
if (nrow(d) > 0) {
|
| 286 |
+
d$row_key <- seq_len(nrow(d))
|
| 287 |
+
}
|
| 288 |
+
d
|
| 289 |
+
})
|
| 290 |
+
|
| 291 |
+
output$movement_plot <- renderPlotly({
|
| 292 |
+
d <- pitcher_df()
|
| 293 |
+
validate(need(nrow(d) > 0, "No data for selected pitcher"))
|
| 294 |
+
|
| 295 |
+
plot_ly(
|
| 296 |
+
data = d, source = "mv", type = "scatter", mode = "markers",
|
| 297 |
+
x = ~HorzBreak, y = ~InducedVertBreak,
|
| 298 |
+
text = ~paste0(
|
| 299 |
+
"<b>", TaggedPitchType, "</b>",
|
| 300 |
+
"<br>Velo: ", round(RelSpeed,1), " mph",
|
| 301 |
+
"<br>IVB: ", round(InducedVertBreak,1), " in",
|
| 302 |
+
"<br>HB: ", round(HorzBreak,1), " in",
|
| 303 |
+
if ("SpinRate" %in% names(d)) paste0("<br>Spin: ", round(SpinRate), " rpm") else ""
|
| 304 |
+
),
|
| 305 |
+
hoverinfo = "text",
|
| 306 |
+
key = ~.uid,
|
| 307 |
+
color = ~factor(TaggedPitchType),
|
| 308 |
+
colors = pitch_colors,
|
| 309 |
+
marker = list(size = 10)
|
| 310 |
) |>
|
| 311 |
+
layout(
|
| 312 |
+
title = paste("Pitch Movement Chart -", input$pitcher_select),
|
| 313 |
+
xaxis = list(title="Horizontal Break (in)", range=c(-25,25), zeroline=TRUE),
|
| 314 |
+
yaxis = list(title="Induced Vertical Break (in)", range=c(-25,25), zeroline=TRUE),
|
| 315 |
+
dragmode = if (input$selection_mode == "drag") "select" else "zoom"
|
| 316 |
+
) |>
|
| 317 |
+
config(displaylogo = FALSE)
|
| 318 |
+
})
|
| 319 |
+
|
| 320 |
+
output$location_plot <- renderPlotly({
|
| 321 |
+
d <- pitcher_df()
|
| 322 |
+
validate(need(nrow(d) > 0, "No data for selected pitcher"))
|
| 323 |
+
|
| 324 |
+
plot_ly(
|
| 325 |
+
data = d, source = "loc", type = "scatter", mode = "markers",
|
| 326 |
+
x = ~PlateLocSide, y = ~PlateLocHeight,
|
| 327 |
+
text = ~paste0(
|
| 328 |
+
"<b>", TaggedPitchType, "</b>",
|
| 329 |
+
"<br>Velo: ", round(RelSpeed,1), " mph",
|
| 330 |
+
"<br>X: ", round(PlateLocSide,2),
|
| 331 |
+
"<br>Z: ", round(PlateLocHeight,2),
|
| 332 |
+
"<br>IVB: ", round(InducedVertBreak,1), " in",
|
| 333 |
+
"<br>HB: ", round(HorzBreak,1), " in",
|
| 334 |
+
if ("SpinRate" %in% names(d)) paste0("<br>Spin: ", round(SpinRate), " rpm") else ""
|
| 335 |
+
),
|
| 336 |
+
hoverinfo = "text",
|
| 337 |
+
key = ~.uid,
|
| 338 |
+
color = ~factor(TaggedPitchType),
|
| 339 |
+
colors = pitch_colors,
|
| 340 |
+
marker = list(size = 9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
) |>
|
| 342 |
+
layout(
|
| 343 |
+
title = "Pitch Location (Editable)",
|
| 344 |
+
xaxis = list(title="Plate X (ft)", range=c(-2,2), zeroline=TRUE),
|
| 345 |
+
yaxis = list(title="Plate Z (ft)", range=c(0,4.5), zeroline=TRUE),
|
| 346 |
+
dragmode = if (input$selection_mode == "drag") "select" else "zoom",
|
| 347 |
+
shapes = list(
|
| 348 |
+
list(type="rect", x0=-0.8303, x1=0.8303, y0=1.6, y1=3.5,
|
| 349 |
+
line=list(color="black", width=1), fillcolor="rgba(0,0,0,0)"),
|
| 350 |
+
list(type="path",
|
| 351 |
+
path="M -0.708 0.15 L 0.708 0.15 L 0.708 0.3 L 0 0.5 L -0.708 0.3 Z",
|
| 352 |
+
line=list(color="black", width=0.8))
|
| 353 |
+
)
|
| 354 |
+
) |>
|
| 355 |
+
config(displaylogo = FALSE)
|
| 356 |
+
})
|
| 357 |
|
| 358 |
# ---- Click handlers (single mode) for both plots ----
|
| 359 |
show_pitch_modal <- function(hit_row){
|
| 360 |
selected_pitch(list(
|
| 361 |
pitcher = input$pitcher_select,
|
| 362 |
+
uid = hit_row$.uid[1],
|
| 363 |
data = hit_row[1,],
|
| 364 |
original_type = hit_row$TaggedPitchType[1]
|
| 365 |
))
|
|
|
|
| 383 |
|
| 384 |
observeEvent(event_data("plotly_click", source="mv"), {
|
| 385 |
req(input$selection_mode == "single")
|
| 386 |
+
clk <- event_data("plotly_click", source="mv")
|
| 387 |
+
req(nrow(as.data.frame(clk))>0)
|
| 388 |
key <- clk$key[[1]]
|
| 389 |
+
d <- pitcher_df()
|
| 390 |
+
hit <- d[d$.uid == key, ]
|
| 391 |
if (nrow(hit)==1) show_pitch_modal(hit)
|
| 392 |
})
|
| 393 |
+
|
| 394 |
observeEvent(event_data("plotly_click", source="loc"), {
|
| 395 |
req(input$selection_mode == "single")
|
| 396 |
+
clk <- event_data("plotly_click", source="loc")
|
| 397 |
+
req(nrow(as.data.frame(clk))>0)
|
| 398 |
key <- clk$key[[1]]
|
| 399 |
+
d <- pitcher_df()
|
| 400 |
+
hit <- d[d$.uid == key, ]
|
| 401 |
if (nrow(hit)==1) show_pitch_modal(hit)
|
| 402 |
})
|
| 403 |
|
|
|
|
| 418 |
)
|
| 419 |
})
|
| 420 |
|
| 421 |
+
observeEvent(input$update_pitch, {
|
| 422 |
+
info <- selected_pitch()
|
| 423 |
+
req(info)
|
| 424 |
+
new_type <- input$modal_new_pitch_type
|
| 425 |
+
req(new_type)
|
| 426 |
|
| 427 |
+
cur <- plot_data()
|
| 428 |
+
req("Pitcher" %in% names(cur))
|
| 429 |
+
cur$TaggedPitchType <- as.character(cur$TaggedPitchType)
|
| 430 |
|
| 431 |
+
uid <- info$uid
|
| 432 |
+
hit_idx <- which(cur$.uid == uid)
|
| 433 |
|
| 434 |
+
if (length(hit_idx) == 1) {
|
| 435 |
+
cur$TaggedPitchType[hit_idx] <- new_type
|
| 436 |
+
plot_data(cur)
|
| 437 |
+
processed_data(cur)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
removeModal()
|
| 439 |
+
showNotification("Updated pitch tag.", type="message", duration=3)
|
|
|
|
| 440 |
selected_pitch(NULL)
|
| 441 |
} else {
|
| 442 |
showNotification("Could not map selection back to dataset.", type="error")
|
| 443 |
}
|
| 444 |
+
})
|
| 445 |
+
|
| 446 |
observeEvent(input$cancel_edit, { removeModal(); selected_pitch(NULL) })
|
| 447 |
|
| 448 |
# ---- Drag select (either chart) ----
|
|
|
|
| 451 |
ev <- event_data("plotly_selected", source="mv")
|
| 452 |
if (!is.null(ev) && length(ev$key)) selected_keys(unique(as.integer(ev$key)))
|
| 453 |
})
|
| 454 |
+
|
| 455 |
observeEvent(event_data("plotly_selected", source="loc"), {
|
| 456 |
req(input$selection_mode == "drag")
|
| 457 |
ev <- event_data("plotly_selected", source="loc")
|
|
|
|
| 462 |
output$selection_info <- renderText({
|
| 463 |
keys <- selected_keys()
|
| 464 |
if (!length(keys)) return("No points selected.")
|
| 465 |
+
d <- pitcher_df()
|
| 466 |
+
sel <- d[d$.uid %in% keys, ]
|
| 467 |
cnt <- sort(table(sel$TaggedPitchType), decreasing = TRUE)
|
| 468 |
paste(nrow(sel), "points selected:", paste(names(cnt), "(", cnt, ")", collapse=", "))
|
| 469 |
})
|
| 470 |
|
| 471 |
+
observeEvent(input$apply_bulk_change, {
|
| 472 |
+
req(input$selection_mode == "drag")
|
| 473 |
+
keys <- selected_keys()
|
| 474 |
+
req(length(keys) > 0)
|
| 475 |
+
new_type <- input$bulk_pitch_type
|
| 476 |
+
req(new_type)
|
| 477 |
|
| 478 |
+
cur <- plot_data()
|
| 479 |
+
req("Pitcher" %in% names(cur))
|
| 480 |
+
cur$TaggedPitchType <- as.character(cur$TaggedPitchType)
|
| 481 |
|
| 482 |
+
uids <- as.integer(keys)
|
| 483 |
+
hit_idx <- which(cur$.uid %in% uids & cur$Pitcher == input$pitcher_select)
|
|
|
|
| 484 |
|
| 485 |
+
if (length(hit_idx) == 0) {
|
| 486 |
+
showNotification("No matching rows found for bulk edit.", type="warning")
|
| 487 |
+
return()
|
| 488 |
+
}
|
| 489 |
|
| 490 |
+
cur$TaggedPitchType[hit_idx] <- new_type
|
| 491 |
+
plot_data(cur)
|
| 492 |
+
processed_data(cur)
|
| 493 |
+
selected_keys(integer(0))
|
| 494 |
+
showNotification(paste("Updated", length(hit_idx), "pitches to", new_type),
|
| 495 |
+
type="message", duration=3)
|
| 496 |
+
})
|
| 497 |
|
| 498 |
# click info small text
|
| 499 |
output$click_info <- renderText({
|
|
|
|
| 551 |
datatable(summary_stats, options=list(pageLength=15, dom='t', scrollX=TRUE), rownames=FALSE) %>%
|
| 552 |
formatStyle(columns = names(summary_stats), fontSize='12px')
|
| 553 |
})
|
| 554 |
+
|
| 555 |
# data summary + download
|
| 556 |
output$data_summary <- renderText({
|
| 557 |
+
req(processed_data())
|
| 558 |
+
df <- processed_data()
|
| 559 |
paste(
|
| 560 |
paste("Total rows:", nrow(df)),
|
| 561 |
paste("Total columns:", ncol(df)),
|
|
|
|
| 563 |
if ("Date" %in% names(df) && !all(is.na(df$Date))) {
|
| 564 |
paste(min(as.Date(df$Date), na.rm=TRUE), "to", max(as.Date(df$Date), na.rm=TRUE))
|
| 565 |
} else "Date column not available"),
|
| 566 |
+
paste("Unique pitchers:",
|
| 567 |
+
if ("Pitcher" %in% names(df)) length(unique(df$Pitcher[!is.na(df$Pitcher)]))
|
| 568 |
+
else "Pitcher column not available"),
|
| 569 |
+
paste("Pitch types:",
|
| 570 |
+
if ("TaggedPitchType" %in% names(df)) paste(sort(unique(df$TaggedPitchType[!is.na(df$TaggedPitchType)])), collapse=", ")
|
| 571 |
+
else "TaggedPitchType column not available"),
|
| 572 |
sep = "\n"
|
| 573 |
)
|
| 574 |
})
|
| 575 |
+
|
| 576 |
output$downloadData <- downloadHandler(
|
| 577 |
filename = function(){ paste0("app_ready_COA_", Sys.Date(), ".csv") },
|
| 578 |
content = function(file){ write.csv(processed_data(), file, row.names=FALSE) }
|