igroffman commited on
Commit
73e80cd
·
verified ·
1 Parent(s): ebaf56b

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +146 -133
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") # <— new location plot
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)) # works for either chart
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
- if (!".uid" %in% names(df)) df$.uid <- seq_len(nrow(df))
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
- req(plot_data(), input$pitcher_select)
280
- plot_data() %>%
281
- filter(Pitcher == input$pitcher_select) %>%
282
- filter(!is.na(HorzBreak), !is.na(InducedVertBreak), !is.na(RelSpeed))
283
- })
284
-
285
- output$movement_plot <- renderPlotly({
286
- d <- pitcher_df()
287
- validate(need(nrow(d) > 0, "No data for selected pitcher"))
288
-
289
- plot_ly(
290
- data = d, source = "mv", type = "scatter", mode = "markers",
291
- x = ~HorzBreak, y = ~InducedVertBreak,
292
- text = ~paste0(
293
- "<b>", TaggedPitchType, "</b>",
294
- "<br>Velo: ", round(RelSpeed,1), " mph",
295
- "<br>IVB: ", round(InducedVertBreak,1), " in",
296
- "<br>HB: ", round(HorzBreak,1), " in",
297
- if ("SpinRate" %in% names(d)) paste0("<br>Spin: ", round(SpinRate), " rpm") else ""
298
- ),
299
- hoverinfo = "text",
300
- key = ~row_key,
301
- color = ~factor(TaggedPitchType),
302
- colors = pitch_colors,
303
- marker = list(size = 10)
304
- ) |>
305
- layout(
306
- title = paste("Pitch Movement Chart -", input$pitcher_select),
307
- xaxis = list(title="Horizontal Break (in)", range=c(-25,25), zeroline=TRUE),
308
- yaxis = list(title="Induced Vertical Break (in)", range=c(-25,25), zeroline=TRUE),
309
- dragmode = if (input$selection_mode == "drag") "select" else "zoom"
310
  ) |>
311
- config(displaylogo = FALSE)
312
- })
313
-
314
- output$location_plot <- renderPlotly({
315
- d <- pitcher_df()
316
- validate(need(nrow(d) > 0, "No data for selected pitcher"))
317
-
318
- plot_ly(
319
- data = d, source = "loc", type = "scatter", mode = "markers",
320
- x = ~PlateLocSide, y = ~PlateLocHeight,
321
- text = ~paste0(
322
- "<b>", TaggedPitchType, "</b>",
323
- "<br>Velo: ", round(RelSpeed,1), " mph",
324
- "<br>X: ", round(PlateLocSide,2),
325
- "<br>Z: ", round(PlateLocHeight,2),
326
- "<br>IVB: ", round(InducedVertBreak,1), " in",
327
- "<br>HB: ", round(HorzBreak,1), " in",
328
- if ("SpinRate" %in% names(d)) paste0("<br>Spin: ", round(SpinRate), " rpm") else ""
329
- ),
330
- hoverinfo = "text",
331
- key = ~row_key,
332
- color = ~factor(TaggedPitchType), # <-- data-driven
333
- colors = pitch_colors, # <-- your palette
334
- marker = list(size = 9)
335
- ) |>
336
- layout(
337
- title = "Pitch Location (Editable)",
338
- xaxis = list(title="Plate X (ft)", range=c(-2,2), zeroline=TRUE),
339
- yaxis = list(title="Plate Z (ft)", range=c(0,4.5), zeroline=TRUE),
340
- dragmode = if (input$selection_mode == "drag") "select" else "zoom",
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
- config(displaylogo = FALSE)
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
- row_key = hit_row$row_key[1],
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"); req(nrow(as.data.frame(clk))>0)
 
381
  key <- clk$key[[1]]
382
- d <- pitcher_df(); hit <- d[d$row_key == key, ]
 
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"); req(nrow(as.data.frame(clk))>0)
 
388
  key <- clk$key[[1]]
389
- d <- pitcher_df(); hit <- d[d$row_key == key, ]
 
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
- info <- selected_pitch(); req(info)
412
- new_type <- input$modal_new_pitch_type; req(new_type)
 
 
413
 
414
- cur <- plot_data(); req("Pitcher" %in% names(cur))
415
- cur$TaggedPitchType <- as.character(cur$TaggedPitchType) # ensure character
 
416
 
417
- uid <- info$uid
418
- hit_idx <- which(cur$.uid == uid)
419
 
420
- if (length(hit_idx) == 1) {
421
- cur$TaggedPitchType[hit_idx] <- new_type
422
- plot_data(cur); processed_data(cur)
423
- removeModal()
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(paste("Updated pitch from", info$original_type, "to", new_type),
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(); sel <- d[d$row_key %in% keys, ]
 
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
- req(input$selection_mode == "drag")
470
- keys <- selected_keys(); req(length(keys) > 0)
471
- new_type <- input$bulk_pitch_type; req(new_type)
 
 
472
 
473
- cur <- plot_data(); req("Pitcher" %in% names(cur))
474
- cur$TaggedPitchType <- as.character(cur$TaggedPitchType) # ensure character
 
475
 
476
- # keys from plotly are .uid values
477
- uids <- as.integer(keys)
478
- hit_idx <- which(cur$.uid %in% uids & cur$Pitcher == input$pitcher_select)
479
 
480
- if (length(hit_idx) == 0) {
481
- showNotification("No matching rows found for bulk edit.", type="warning"); return()
482
- }
 
483
 
484
- cur$TaggedPitchType[hit_idx] <- new_type
485
- plot_data(cur); processed_data(cur)
486
- selected_keys(integer(0))
487
- showNotification(paste("Updated", length(hit_idx), "pitches to", new_type),
488
- type="message", duration=3)
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()); df <- 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:", if ("Pitcher" %in% names(df)) length(unique(df$Pitcher[!is.na(df$Pitcher)])) else "Pitcher column not available"),
559
- paste("Pitch types:", if ("TaggedPitchType" %in% names(df)) paste(sort(unique(df$TaggedPitchType[!is.na(df$TaggedPitchType)])), collapse=", ") else "TaggedPitchType column not available"),
 
 
 
 
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) }