igroffman commited on
Commit
0efb222
·
verified ·
1 Parent(s): 92bcc52

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +179 -165
app.R CHANGED
@@ -173,7 +173,7 @@ spring26_ncaa <- download_master_dataset("CoastalBaseball/2026MasterDataset", "n
173
  college_join <- download_private_csv("CoastalBaseball/DefenseAppDataset", "college_join.csv")
174
 
175
  # Load Catcher 2026 data
176
- Catcher2026 <- read_parquet("Catcher2026.parquet")
177
  Catcher2026 <- Catcher2026 %>%
178
  mutate(
179
  Date = as.Date(Date),
@@ -1044,11 +1044,14 @@ app_ui <- fluidPage(
1044
  uiOutput("throw_header_stats"),
1045
  hr(),
1046
  fluidRow(
1047
- column(6,
1048
- selectInput("throw_view_mode", "View Mode:",
1049
- choices = c("Points (by Result)","Heatmap","Color by Throw Speed",
1050
- "Release Point (Side)","Release Point (Overhead)"),
1051
- selected = "Points (by Result)", width = "100%"))
 
 
 
1052
  ),
1053
  fluidRow(
1054
  column(6,
@@ -1059,14 +1062,8 @@ app_ui <- fluidPage(
1059
  plotlyOutput("throw_safe_plot", height = "520px"))
1060
  ),
1061
  hr(),
1062
- fluidRow(
1063
- column(6,
1064
- h4("Release Point (Side View)", class = "section-header"),
1065
- plotlyOutput("throw_release_side", height = "420px")),
1066
- column(6,
1067
- h4("Release Point (Overhead)", class = "section-header"),
1068
- plotlyOutput("throw_release_overhead", height = "420px"))
1069
- ),
1070
  hr(),
1071
  h4("Throwing Log", class = "section-header"),
1072
  DTOutput("throw_log_table")
@@ -1711,65 +1708,87 @@ server <- function(input, output, session) {
1711
  })
1712
 
1713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1714
  make_recv_plot <- function(data, title_text, view_mode) {
1715
- if (nrow(data) == 0) return(ggplotly(ggplot() + theme_void() + ggtitle(title_text) +
1716
- theme(plot.title = element_text(hjust=.5, size=12, face="bold"))))
1717
-
 
 
 
1718
  if (view_mode == "Catch Position (Overhead)") {
1719
  data <- data %>% filter(!is.na(CatchPositionX) & !is.na(CatchPositionZ))
1720
- if (nrow(data) == 0) return(ggplotly(ggplot() + theme_void() + ggtitle("No catch position data")))
1721
- data <- data %>% mutate(row_num = row_number())
1722
- bb_l <- data.frame(x=c(-3.5,-3.5,-0.708,-0.708), y=c(-4.5,0.15,0.15,-4.5))
1723
- bb_r <- data.frame(x=c(0.708,0.708,3.5,3.5), y=c(-4.5,0.15,0.15,-4.5))
1724
- p <- ggplot(data, aes(x = as.numeric(CatchPositionX), y = as.numeric(CatchPositionZ))) +
1725
- geom_polygon(data=bb_l, aes(x=x,y=y), fill="gray90", color="black", linewidth=.4, inherit.aes=FALSE) +
1726
- geom_polygon(data=bb_r, aes(x=x,y=y), fill="gray90", color="black", linewidth=.4, inherit.aes=FALSE) +
1727
- geom_polygon(data=data.frame(x=c(-.708,.708,.708,0,-.708), y=c(.15,.15,.3,.5,.3)),
1728
- aes(x=x,y=y), fill="gray95", color="black", linewidth=.6, inherit.aes=FALSE) +
1729
- geom_point(aes(fill=TaggedPitchType, text=paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
1730
- "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,"<br>Date: ",Date)),
1731
- shape=21, size=4, color="black", stroke=.5, alpha=.9) +
1732
- scale_fill_manual(values=pitch_colors, na.value="grey60", name="Pitch Type") +
1733
- coord_equal() + xlim(-4, 4) + ylim(-5, 3) +
1734
- labs(title=paste(title_text, "- Catch Position (Overhead)"), x="X (ft)", y="Z (ft)") +
1735
- theme_classic() + theme(plot.title=element_text(hjust=.5,size=11,face="bold"), legend.position="bottom")
1736
- return(ggplotly(p, tooltip="text"))
 
 
 
 
 
 
1737
  }
1738
-
1739
  if (view_mode == "Heatmap") {
1740
  data <- data %>% filter(!is.na(PlateLocSide) & !is.na(PlateLocHeight))
1741
- if (nrow(data) < 3) return(ggplotly(ggplot() + theme_void() + ggtitle(paste(title_text, "- Not enough data"))))
1742
- p <- ggplot(data, aes(x=PlateLocSide, y=PlateLocHeight)) +
1743
- geom_density_2d_filled(alpha=0.7) +
1744
- scale_fill_viridis_d(option="inferno", name="Density") +
1745
- sz_segments() +
1746
- geom_polygon(data=hp_polygon, aes(x=x,y=y), fill=NA, color="white", linewidth=.8, inherit.aes=FALSE) +
1747
- coord_fixed(ratio=1) + xlim(-2,2) + ylim(0,4.5) + ggtitle(title_text) +
1748
- theme_void() + theme(legend.position="none", plot.margin=margin(3,3,3,3),
1749
- plot.title=element_text(hjust=0.5, size=12, face="bold"))
1750
- return(ggplotly(p))
1751
  }
1752
-
1753
- # Default: Points
1754
  data <- data %>% mutate(row_num = row_number())
1755
- apt <- unique(data$TaggedPitchType[!is.na(data$TaggedPitchType)])
1756
- p <- ggplot(data, aes(PlateLocSide, PlateLocHeight)) +
1757
- bz_segments() + sz_segments() +
1758
- geom_polygon(data=hp_polygon, aes(x=x,y=y), fill=NA, color="gray40", linewidth=.4, inherit.aes=FALSE) +
1759
- geom_point(aes(fill=TaggedPitchType, text=paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
1760
- "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,
1761
- "<br>Velo: ",round(RelSpeed,1)," mph","<br>Count: ",Balls,"-",Strikes,
1762
- "<br>RV: ",round(mean_DRE,3),"<br>Date: ",Date)),
1763
- shape=21, size=5, color="black", stroke=.6, alpha=.95) +
1764
- geom_text(aes(label=row_num), size=2.0, fontface="bold", color="white") +
1765
- scale_fill_manual(values=pitch_colors, na.value="grey60", name="Pitch Type", drop=FALSE, limits=apt) +
1766
- coord_equal() + scale_x_continuous(limits=c(-1.8,1.8)) + scale_y_continuous(limits=c(0,4.5)) +
1767
- labs(title=title_text) + theme_classic() +
1768
- theme(axis.title=element_blank(), axis.text=element_blank(), axis.ticks=element_blank(),
1769
- axis.line=element_blank(), panel.grid=element_blank(),
1770
- plot.title=element_text(hjust=.5,size=12,face="bold"), legend.position="bottom",
1771
- legend.title=element_text(size=8,face="bold"))
1772
- ggplotly(p, tooltip="text")
 
1773
  }
1774
 
1775
  output$recv_strikes_added <- renderPlotly({
@@ -1867,23 +1886,19 @@ server <- function(input, output, session) {
1867
 
1868
  if (view_mode == "Heatmap") {
1869
  data <- data %>% filter(!is.na(BasePositionZ) & !is.na(BasePositionY))
1870
- if (nrow(data) == 0) return(ggplotly(ggplot() + theme_void() + ggtitle("No data")))
1871
- p <- ggplot() +
1872
- geom_polygon(data=grass, aes(x,y), fill='darkcyan', color='darkcyan') +
1873
- geom_polygon(data=sky, aes(x,y), fill='yellow', color='yellow') +
1874
- geom_polygon(data=dirt, aes(x,y), fill='brown', color='brown') +
1875
- geom_polygon(data=ground, aes(x,y), fill='darkgreen', color='darkgreen') +
1876
- geom_polygon(data=base_w, aes(x,y), fill='white', color='black') +
1877
- geom_polygon(data=base_b, aes(x,y), fill='lightgrey', color='black') +
1878
- geom_density_2d_filled(data=data, aes(x=BasePositionZ,y=BasePositionY), alpha=0.6) +
1879
- scale_fill_viridis_d(option="inferno", name="Density") +
1880
- scale_x_continuous(limits=c(-10,10)) + scale_y_continuous(limits=c(-5,9)) +
1881
- theme_bw() + coord_fixed() +
1882
- theme(legend.position="bottom", axis.title=element_blank(), axis.text=element_blank(),
1883
- axis.ticks=element_blank(), panel.grid=element_blank(),
1884
- plot.title=element_text(size=11,face='bold',hjust=.5)) +
1885
- ggtitle(title_text)
1886
- return(ggplotly(p))
1887
  }
1888
 
1889
  if (view_mode == "Color by Throw Speed") {
@@ -1977,38 +1992,45 @@ server <- function(input, output, session) {
1977
  make_throw_base_plot(td, paste0(input$ct_catcher, " - Safe"), input$throw_view_mode)
1978
  })
1979
 
1980
- output$throw_release_side <- renderPlotly({
1981
  td <- throw_data() %>%
1982
  filter(!is.na(ThrowPositionX) & !is.na(ThrowPositionY))
1983
- if (nrow(td) == 0) return(ggplotly(ggplot() + theme_void() + ggtitle("No release point data") +
1984
- theme(plot.title=element_text(hjust=.5,size=11,face="bold"))))
1985
- throw_color_map <- c('2B Out'='#339a1d','2B Safe'='red','3B Out'='#1a5d1a','3B Safe'='#ff6b6b')
1986
- p <- ggplot(td, aes(x=as.numeric(ThrowPositionX), y=as.numeric(ThrowPositionY),
1987
- text=paste0("Result: ",throw_label,"<br>Speed: ",round(ThrowSpeed,1)," mph",
1988
- "<br>Pop: ",round(PopTime,3),"s","<br>Date: ",Date))) +
1989
- geom_point(aes(color=throw_label), size=4, alpha=.85) +
1990
- scale_color_manual(values=throw_color_map, name="Result") +
1991
- labs(title=paste0(input$ct_catcher," - Release Point (Side)"), x="X (ft)", y="Y (ft)") +
1992
- theme_minimal() +
1993
- theme(plot.title=element_text(hjust=.5,size=12,face='bold'), legend.position="bottom")
1994
- ggplotly(p, tooltip="text")
1995
- })
1996
-
1997
- output$throw_release_overhead <- renderPlotly({
1998
- td <- throw_data() %>%
1999
- filter(!is.na(ThrowPositionX) & !is.na(ThrowPositionZ))
2000
- if (nrow(td) == 0) return(ggplotly(ggplot() + theme_void() + ggtitle("No release point data") +
2001
- theme(plot.title=element_text(hjust=.5,size=11,face="bold"))))
2002
- throw_color_map <- c('2B Out'='#339a1d','2B Safe'='red','3B Out'='#1a5d1a','3B Safe'='#ff6b6b')
2003
- p <- ggplot(td, aes(x=as.numeric(ThrowPositionX), y=as.numeric(ThrowPositionZ),
2004
- text=paste0("Result: ",throw_label,"<br>Speed: ",round(ThrowSpeed,1)," mph",
2005
- "<br>Date: ",Date))) +
2006
- geom_point(aes(color=throw_label), size=4, alpha=.85) +
2007
- scale_color_manual(values=throw_color_map, name="Result") +
2008
- labs(title=paste0(input$ct_catcher," - Release Point (Overhead)"), x="X (ft)", y="Z (ft)") +
2009
- theme_minimal() +
2010
- theme(plot.title=element_text(hjust=.5,size=12,face='bold'), legend.position="bottom")
2011
- ggplotly(p, tooltip="text")
 
 
 
 
 
 
 
2012
  })
2013
 
2014
  output$throw_log_table <- renderDT({
@@ -2081,22 +2103,18 @@ server <- function(input, output, session) {
2081
  bi <- 1; bo <- 4; bymin <- -2.5; bymax <- 3.7
2082
 
2083
  if (view_mode == "Heatmap" && nrow(bd) >= 3) {
2084
- p <- ggplot() +
2085
- geom_segment(aes(x=bi,xend=bo,y=bymin,yend=bymin), color="black", linewidth=.8, inherit.aes=FALSE) +
2086
- geom_segment(aes(x=bi,xend=bo,y=bymax,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2087
- geom_segment(aes(x=bi,xend=bi,y=bymin,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2088
- geom_segment(aes(x=bo,xend=bo,y=bymin,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2089
- geom_segment(aes(x=-bi,xend=-bo,y=bymin,yend=bymin), color="black", linewidth=.8, inherit.aes=FALSE) +
2090
- geom_segment(aes(x=-bi,xend=-bo,y=bymax,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2091
- geom_segment(aes(x=-bi,xend=-bi,y=bymin,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2092
- geom_segment(aes(x=-bo,xend=-bo,y=bymin,yend=bymax), color="black", linewidth=.8, inherit.aes=FALSE) +
2093
- geom_polygon(data=plate, aes(x,y), fill=NA, color="black", linewidth=.8) +
2094
- geom_density_2d_filled(data=bd, aes(x=plot_x, y=plot_y), alpha=0.7) +
2095
- scale_fill_viridis_d(option="inferno", name="Density") +
2096
- coord_fixed() + labs(title="Blocking (Overhead) - Heatmap", x=NULL, y=NULL) +
2097
- theme_void(base_size=9) +
2098
- theme(plot.title=element_text(size=11,face="bold",hjust=.5), legend.position="bottom")
2099
- return(ggplotly(p))
2100
  }
2101
 
2102
  # Points mode
@@ -2134,48 +2152,44 @@ server <- function(input, output, session) {
2134
  bd <- block_data() %>%
2135
  filter(!is.na(PlateLocSide) & !is.na(PlateLocHeight)) %>%
2136
  mutate(row_num = row_number())
2137
-
2138
  view_mode <- input$block_view_mode
2139
-
2140
  if (nrow(bd) == 0) {
2141
- return(ggplotly(ggplot() + theme_void() + ggtitle("Blocking (Zone)") +
2142
- theme(plot.title=element_text(hjust=.5,size=11,face="bold"))))
2143
  }
2144
-
2145
  if (view_mode == "Heatmap" && nrow(bd) >= 3) {
2146
- p <- ggplot(bd, aes(x=PlateLocSide, y=PlateLocHeight)) +
2147
- geom_density_2d_filled(alpha=0.7) +
2148
- scale_fill_viridis_d(option="inferno", name="Density") +
2149
- sz_segments() +
2150
- geom_polygon(data=hp_polygon, aes(x=x,y=y), fill=NA, color="white", linewidth=.8, inherit.aes=FALSE) +
2151
- coord_fixed(ratio=1) + xlim(-2.5,2.5) + ylim(-1,4.5) +
2152
- ggtitle("Blocking (Zone) - Heatmap") +
2153
- theme_void() + theme(legend.position="none", plot.margin=margin(3,3,3,3),
2154
- plot.title=element_text(hjust=0.5, size=11, face="bold"))
2155
- return(ggplotly(p))
2156
  }
2157
-
2158
- sl <- unique(bd$block_type)
2159
- sv <- c("Block"=21,"PB/WP"=24)[sl]
2160
-
2161
- p <- ggplot(bd, aes(PlateLocSide, PlateLocHeight,
2162
- text=paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
2163
- "<br>Type: ",block_type,
2164
- "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,
2165
- "<br>Velo: ",round(RelSpeed,1)," mph",
2166
- "<br>RV: ",round(mean_DRE,3),
2167
- "<br>Date: ",Date))) +
2168
- bz_segments() + sz_segments() +
2169
- geom_polygon(data=hp_polygon, aes(x=x,y=y), fill=NA, color="gray40", linewidth=.5, inherit.aes=FALSE) +
2170
- geom_point(aes(fill=TaggedPitchType, shape=block_type), size=4, color="black", stroke=.6) +
2171
- geom_text(aes(label=row_num), size=2.0, fontface="bold", color="white") +
2172
- scale_fill_manual(values=pitch_colors, na.value="grey60", name="Pitch Type") +
2173
- scale_shape_manual(values=sv, name=NULL) +
2174
- coord_equal(xlim=c(-2.5,2.5), ylim=c(-1,4.5)) +
2175
- labs(title="Blocking (Zone)") + theme_void() +
2176
- theme(plot.title=element_text(hjust=.5,size=11,face="bold"),
2177
- legend.position="bottom", legend.box="vertical")
2178
- ggplotly(p, tooltip="text")
2179
  })
2180
 
2181
  output$block_log_table <- renderDT({
 
173
  college_join <- download_private_csv("CoastalBaseball/DefenseAppDataset", "college_join.csv")
174
 
175
  # Load Catcher 2026 data
176
+ Catcher2026 <- download_private_parquet("CoastalBaseball/DefenseAppDataset", "Catcher2026.parquet")
177
  Catcher2026 <- Catcher2026 %>%
178
  mutate(
179
  Date = as.Date(Date),
 
1044
  uiOutput("throw_header_stats"),
1045
  hr(),
1046
  fluidRow(
1047
+ column(4,
1048
+ selectInput("throw_view_mode", "Base Arrival View:",
1049
+ choices = c("Points (by Result)","Heatmap","Color by Throw Speed"),
1050
+ selected = "Points (by Result)", width = "100%")),
1051
+ column(4,
1052
+ selectInput("throw_release_color", "Release Point Color By:",
1053
+ choices = c("Result","Throw Speed"),
1054
+ selected = "Result", width = "100%"))
1055
  ),
1056
  fluidRow(
1057
  column(6,
 
1062
  plotlyOutput("throw_safe_plot", height = "520px"))
1063
  ),
1064
  hr(),
1065
+ h4("Catcher Release Point", class = "section-header"),
1066
+ plotlyOutput("throw_release_plot", height = "480px"),
 
 
 
 
 
 
1067
  hr(),
1068
  h4("Throwing Log", class = "section-header"),
1069
  DTOutput("throw_log_table")
 
1708
  })
1709
 
1710
 
1711
+ # Pure plotly zone plot helper — no ggplotly conversion issues
1712
+ plotly_zone_shapes <- function() {
1713
+ list(
1714
+ list(type="rect", x0=-.83083, x1=.83083, y0=1.5, y1=3.3775,
1715
+ line=list(color="black", width=2), fillcolor="rgba(0,0,0,0)", layer="above"),
1716
+ list(type="rect", x0=-.9975, x1=.9975, y0=1.3775, y1=3.5,
1717
+ line=list(color="gray", width=1, dash="dot"), fillcolor="rgba(0,0,0,0)", layer="above"),
1718
+ list(type="path",
1719
+ path="M -0.60 0.15 L 0.60 0.15 L 0.60 0.27 L 0 0.42 L -0.60 0.27 Z",
1720
+ line=list(color="gray40", width=1.5), fillcolor="rgba(0,0,0,0)", layer="above")
1721
+ )
1722
+ }
1723
+
1724
  make_recv_plot <- function(data, title_text, view_mode) {
1725
+ if (nrow(data) == 0) {
1726
+ return(plot_ly() %>% layout(title=list(text=title_text, font=list(size=14)),
1727
+ xaxis=list(visible=FALSE), yaxis=list(visible=FALSE),
1728
+ annotations=list(list(text="No data", showarrow=FALSE, font=list(size=16)))))
1729
+ }
1730
+
1731
  if (view_mode == "Catch Position (Overhead)") {
1732
  data <- data %>% filter(!is.na(CatchPositionX) & !is.na(CatchPositionZ))
1733
+ if (nrow(data) == 0) return(plot_ly() %>% layout(title="No catch position data"))
1734
+ data <- data %>% mutate(row_num = row_number(), cpx = as.numeric(CatchPositionX), cpz = as.numeric(CatchPositionZ))
1735
+ pt_colors <- sapply(data$TaggedPitchType, function(pt) {
1736
+ if (!is.na(pt) && pt %in% names(pitch_colors)) pitch_colors[pt] else "grey60"
1737
+ })
1738
+ p <- plot_ly(data, x=~cpx, y=~cpz, type="scatter", mode="markers",
1739
+ marker=list(size=10, color=pt_colors, line=list(color="black", width=1)),
1740
+ text=~paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
1741
+ "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,"<br>Date: ",Date),
1742
+ hoverinfo="text") %>%
1743
+ layout(title=list(text=paste(title_text,"- Catch Position"), font=list(size=14)),
1744
+ xaxis=list(title="X (ft)", range=c(-4,4), scaleanchor="y"),
1745
+ yaxis=list(title="Z (ft)", range=c(-5,3)),
1746
+ shapes=list(
1747
+ list(type="rect", x0=-3.5, x1=-0.708, y0=-4.5, y1=0.15,
1748
+ line=list(color="black",width=1), fillcolor="rgba(200,200,200,0.3)"),
1749
+ list(type="rect", x0=0.708, x1=3.5, y0=-4.5, y1=0.15,
1750
+ line=list(color="black",width=1), fillcolor="rgba(200,200,200,0.3)"),
1751
+ list(type="path",
1752
+ 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",
1753
+ line=list(color="black",width=1.5), fillcolor="rgba(240,240,240,0.5)"))
1754
+ )
1755
+ return(p)
1756
  }
1757
+
1758
  if (view_mode == "Heatmap") {
1759
  data <- data %>% filter(!is.na(PlateLocSide) & !is.na(PlateLocHeight))
1760
+ if (nrow(data) < 3) return(plot_ly() %>% layout(title=paste(title_text,"- Not enough data")))
1761
+ p <- plot_ly(data, x=~PlateLocSide, y=~PlateLocHeight, type="histogram2dcontour",
1762
+ colorscale=list(c(0,"white"),c(0.25,"#0551bc"),c(0.5,"#03ff00"),c(0.75,"#fbff00"),c(1,"#dc1100")),
1763
+ contours=list(showlabels=FALSE), showscale=FALSE) %>%
1764
+ layout(title=list(text=title_text, font=list(size=14)),
1765
+ xaxis=list(title="", range=c(-2,2), scaleanchor="y", showticklabels=FALSE),
1766
+ yaxis=list(title="", range=c(0,4.5), showticklabels=FALSE),
1767
+ shapes=plotly_zone_shapes())
1768
+ return(p)
 
1769
  }
1770
+
1771
+ # Default: Points mode — pure plotly
1772
  data <- data %>% mutate(row_num = row_number())
1773
+ pt_colors <- sapply(data$TaggedPitchType, function(pt) {
1774
+ if (!is.na(pt) && pt %in% names(pitch_colors)) pitch_colors[pt] else "grey60"
1775
+ })
1776
+ p <- plot_ly(data, x=~PlateLocSide, y=~PlateLocHeight, type="scatter", mode="markers+text",
1777
+ marker=list(size=14, color=pt_colors, line=list(color="black", width=1.2)),
1778
+ text=~as.character(row_num), textfont=list(size=8, color="white"),
1779
+ textposition="middle center",
1780
+ hovertext=~paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
1781
+ "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,
1782
+ "<br>Velo: ",round(RelSpeed,1)," mph",
1783
+ "<br>Count: ",Balls,"-",Strikes,
1784
+ "<br>RV: ",round(mean_DRE,3),"<br>Date: ",Date),
1785
+ hoverinfo="text") %>%
1786
+ layout(title=list(text=title_text, font=list(size=14)),
1787
+ xaxis=list(title="", range=c(-1.8,1.8), zeroline=FALSE, showticklabels=FALSE, scaleanchor="y"),
1788
+ yaxis=list(title="", range=c(0,4.5), zeroline=FALSE, showticklabels=FALSE),
1789
+ shapes=plotly_zone_shapes(),
1790
+ showlegend=FALSE)
1791
+ p
1792
  }
1793
 
1794
  output$recv_strikes_added <- renderPlotly({
 
1886
 
1887
  if (view_mode == "Heatmap") {
1888
  data <- data %>% filter(!is.na(BasePositionZ) & !is.na(BasePositionY))
1889
+ if (nrow(data) < 3) return(plot_ly() %>% layout(title="Not enough data for heatmap"))
1890
+ p <- plot_ly(data, x=~BasePositionZ, y=~BasePositionY, type="histogram2dcontour",
1891
+ colorscale=list(c(0,"rgba(0,0,0,0)"),c(0.25,"#02fbff"),c(0.5,"#03ff00"),c(0.75,"#fbff00"),c(1,"#dc1100")),
1892
+ showscale=FALSE) %>%
1893
+ layout(title=list(text=title_text, font=list(size=14)),
1894
+ xaxis=list(title="", range=c(-10,10)),
1895
+ yaxis=list(title="", range=c(-5,9), scaleanchor="x"),
1896
+ shapes=list(
1897
+ list(type="rect", x0=-10,x1=10,y0=0.25,y1=8, fillcolor="rgba(0,139,139,0.15)", line=list(width=0), layer="below"),
1898
+ list(type="rect", x0=-10,x1=10,y0=8,y1=9, fillcolor="rgba(255,255,0,0.2)", line=list(width=0), layer="below"),
1899
+ list(type="rect", x0=-10,x1=10,y0=-2,y1=0.25, fillcolor="rgba(139,69,19,0.2)", line=list(width=0), layer="below"),
1900
+ list(type="rect", x0=-1,x1=1,y0=0,y1=0.45, fillcolor="white", line=list(color="black",width=1), layer="above")))
1901
+ return(p)
 
 
 
 
1902
  }
1903
 
1904
  if (view_mode == "Color by Throw Speed") {
 
1992
  make_throw_base_plot(td, paste0(input$ct_catcher, " - Safe"), input$throw_view_mode)
1993
  })
1994
 
1995
+ output$throw_release_plot <- renderPlotly({
1996
  td <- throw_data() %>%
1997
  filter(!is.na(ThrowPositionX) & !is.na(ThrowPositionY))
1998
+ if (nrow(td) == 0) {
1999
+ return(plot_ly() %>% layout(title="No release point data",
2000
+ xaxis=list(visible=FALSE), yaxis=list(visible=FALSE)))
2001
+ }
2002
+ td <- td %>% mutate(tpx = as.numeric(ThrowPositionX), tpy = as.numeric(ThrowPositionY))
2003
+ color_by <- input$throw_release_color
2004
+
2005
+ if (color_by == "Throw Speed") {
2006
+ p <- plot_ly(td, x=~tpx, y=~tpy, type="scatter", mode="markers",
2007
+ marker=list(size=12, color=~ThrowSpeed,
2008
+ colorscale=list(c(0,"#0551bc"),c(0.25,"#02fbff"),c(0.5,"#03ff00"),c(0.75,"#fbff00"),c(1,"#ff1f02")),
2009
+ colorbar=list(title="Velo (mph)"),
2010
+ line=list(color="black", width=1)),
2011
+ text=~paste0("Result: ",throw_label,"<br>Speed: ",round(ThrowSpeed,1)," mph",
2012
+ "<br>Pop: ",round(PopTime,3),"s","<br>Exchange: ",round(ExchangeTime,3),"s",
2013
+ "<br>Date: ",Date),
2014
+ hoverinfo="text")
2015
+ } else {
2016
+ throw_color_map <- c('2B Out'='#339a1d','2B Safe'='red','3B Out'='#1a5d1a','3B Safe'='#ff6b6b')
2017
+ td$pt_color <- sapply(td$throw_label, function(tl) {
2018
+ if (!is.na(tl) && tl %in% names(throw_color_map)) throw_color_map[tl] else "gray"
2019
+ })
2020
+ p <- plot_ly(td, x=~tpx, y=~tpy, type="scatter", mode="markers",
2021
+ marker=list(size=12, color=~pt_color, line=list(color="black", width=1)),
2022
+ text=~paste0("Result: ",throw_label,"<br>Speed: ",round(ThrowSpeed,1)," mph",
2023
+ "<br>Pop: ",round(PopTime,3),"s","<br>Exchange: ",round(ExchangeTime,3),"s",
2024
+ "<br>Date: ",Date),
2025
+ hoverinfo="text", name=~throw_label)
2026
+ }
2027
+ p %>% layout(
2028
+ title=list(text=paste0(input$ct_catcher," - Catcher Release Point"), font=list(size=15)),
2029
+ xaxis=list(title="Release X (ft)", range=c(-8,8), zeroline=TRUE,
2030
+ zerolinecolor="gray80", gridcolor="gray90"),
2031
+ yaxis=list(title="Release Y (ft)", range=c(-8,8), zeroline=TRUE,
2032
+ zerolinecolor="gray80", gridcolor="gray90", scaleanchor="x"),
2033
+ showlegend=(color_by == "Result"))
2034
  })
2035
 
2036
  output$throw_log_table <- renderDT({
 
2103
  bi <- 1; bo <- 4; bymin <- -2.5; bymax <- 3.7
2104
 
2105
  if (view_mode == "Heatmap" && nrow(bd) >= 3) {
2106
+ p <- plot_ly(bd, x=~plot_x, y=~plot_y, type="histogram2dcontour",
2107
+ colorscale=list(c(0,"white"),c(0.25,"#0551bc"),c(0.5,"#03ff00"),c(0.75,"#fbff00"),c(1,"#dc1100")),
2108
+ showscale=FALSE) %>%
2109
+ layout(title=list(text="Blocking (Overhead) - Heatmap", font=list(size=14)),
2110
+ xaxis=list(title=""), yaxis=list(title="", scaleanchor="x"),
2111
+ shapes=list(
2112
+ list(type="rect", x0=bi,x1=bo,y0=bymin,y1=bymax, line=list(color="black",width=2), fillcolor="rgba(0,0,0,0)"),
2113
+ list(type="rect", x0=-bo,x1=-bi,y0=bymin,y1=bymax, line=list(color="black",width=2), fillcolor="rgba(0,0,0,0)"),
2114
+ list(type="path",
2115
+ path="M -0.708 1.417 L 0.708 1.417 L 0.708 0.708 L 0 0 L -0.708 0.708 Z",
2116
+ line=list(color="black",width=2), fillcolor="rgba(0,0,0,0)")))
2117
+ return(p)
 
 
 
 
2118
  }
2119
 
2120
  # Points mode
 
2152
  bd <- block_data() %>%
2153
  filter(!is.na(PlateLocSide) & !is.na(PlateLocHeight)) %>%
2154
  mutate(row_num = row_number())
2155
+
2156
  view_mode <- input$block_view_mode
2157
+
2158
  if (nrow(bd) == 0) {
2159
+ return(plot_ly() %>% layout(title="Blocking (Zone)", xaxis=list(visible=FALSE), yaxis=list(visible=FALSE)))
 
2160
  }
2161
+
2162
  if (view_mode == "Heatmap" && nrow(bd) >= 3) {
2163
+ p <- plot_ly(bd, x=~PlateLocSide, y=~PlateLocHeight, type="histogram2dcontour",
2164
+ colorscale=list(c(0,"white"),c(0.25,"#0551bc"),c(0.5,"#03ff00"),c(0.75,"#fbff00"),c(1,"#dc1100")),
2165
+ showscale=FALSE) %>%
2166
+ layout(title=list(text="Blocking (Zone) - Heatmap", font=list(size=14)),
2167
+ xaxis=list(title="", range=c(-2.5,2.5), scaleanchor="y", showticklabels=FALSE),
2168
+ yaxis=list(title="", range=c(-1,4.5), showticklabels=FALSE),
2169
+ shapes=plotly_zone_shapes())
2170
+ return(p)
 
 
2171
  }
2172
+
2173
+ # Points mode — pure plotly
2174
+ pt_colors <- sapply(bd$TaggedPitchType, function(pt) {
2175
+ if (!is.na(pt) && pt %in% names(pitch_colors)) pitch_colors[pt] else "grey60"
2176
+ })
2177
+ symbols <- ifelse(bd$block_type == "PB/WP", "triangle-up", "circle")
2178
+ p <- plot_ly(bd, x=~PlateLocSide, y=~PlateLocHeight, type="scatter", mode="markers+text",
2179
+ marker=list(size=14, color=pt_colors, symbol=symbols, line=list(color="black", width=1.2)),
2180
+ text=~as.character(row_num), textfont=list(size=8, color="white"),
2181
+ textposition="middle center",
2182
+ hovertext=~paste0("#",row_num,"<br>Pitch: ",TaggedPitchType,
2183
+ "<br>Type: ",block_type,
2184
+ "<br>Pitcher: ",Pitcher,"<br>Batter: ",Batter,
2185
+ "<br>Velo: ",round(RelSpeed,1)," mph",
2186
+ "<br>RV: ",round(mean_DRE,3),"<br>Date: ",Date),
2187
+ hoverinfo="text") %>%
2188
+ layout(title=list(text="Blocking (Zone)", font=list(size=14)),
2189
+ xaxis=list(title="", range=c(-2.5,2.5), scaleanchor="y", showticklabels=FALSE),
2190
+ yaxis=list(title="", range=c(-1,4.5), showticklabels=FALSE),
2191
+ shapes=plotly_zone_shapes(), showlegend=FALSE)
2192
+ p
 
2193
  })
2194
 
2195
  output$block_log_table <- renderDT({