igroffman commited on
Commit
4831df6
·
verified ·
1 Parent(s): 4cf674c

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +99 -259
app.R CHANGED
@@ -159,10 +159,6 @@ app_css <- "
159
  }
160
  "
161
 
162
- # =====================================================================
163
- # ====================== HITTER CODE (VERBATIM) ======================
164
- # =====================================================================
165
-
166
  process_dataset <- function(df) {
167
  if ("Batter" %in% names(df)) {
168
  df <- df %>% mutate(Batter = stringr::str_replace(Batter, "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
@@ -1590,14 +1586,12 @@ draw_tableau_table_fill <- function(
1590
  }
1591
  }
1592
 
1593
- # =====================================================================
1594
- # MAIN PDF FUNCTION
1595
- # =====================================================================
1596
  create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1597
  if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1598
 
1599
  pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1600
- filter(Pitcher == pitcher_name)
1601
 
1602
  if (nrow(pitcher_df) == 0) {
1603
  pdf(output_file, width = 8.5, height = 11)
@@ -1608,6 +1602,7 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1608
  return(output_file)
1609
  }
1610
 
 
1611
  game_date <- tryCatch({
1612
  d <- unique(pitcher_df$Date)[1]
1613
  if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
@@ -1617,26 +1612,40 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1617
  batter_teams <- batter_teams[!is.na(batter_teams)]
1618
  away_team <- if (length(batter_teams) > 0) batter_teams[1] else "Unknown"
1619
 
1620
- stats <- calculate_tableau_header_stats(pitcher_df)
1621
- loc_data <- calculate_tableau_location_data(pitcher_df)
 
1622
  usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1623
- velo_data <- calculate_tableau_velo_movement(pitcher_df)
1624
- rel_data <- calculate_tableau_release_data(pitcher_df)
 
 
 
 
 
 
 
 
 
 
1625
 
1626
- pitch_types <- unique(c(loc_data$TaggedPitchType, usage_data$TaggedPitchType))
1627
- pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
1628
  if (length(pitch_types) == 0) pitch_types <- "Undefined"
1629
 
 
1630
  loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
1631
  mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1632
  rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1633
 
 
1634
  pdf(output_file, width = 8.5, height = 11)
1635
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1636
 
1637
  grid.newpage()
1638
 
 
1639
  # HEADER BAR
 
1640
  grid.rect(x = 0, y = 0.955, width = 1, height = 0.045,
1641
  just = c("left", "bottom"),
1642
  gp = gpar(fill = "#006F71", col = NA))
@@ -1648,85 +1657,98 @@ create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1648
  info_y <- 0.935
1649
  grid.text(paste0(game_date, " | ", pitcher_name, " vs ", away_team),
1650
  x = 0.02, y = info_y, just = "left",
1651
- gp = gpar(cex = 0.8, fontface = "bold"))
1652
 
 
 
 
 
 
 
 
 
 
1653
 
1654
- stat_labels <- c("At Bats","H","XBH","R","BB/HBP","SO","AVG","Strike%","1st P K%","E+A%","Comp%","LOO%")
1655
- stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs,
1656
- stats$bb_hbp, stats$so, stats$avg,
1657
- stats$strike_pct, stats$fp_k_pct, stats$ea_pct,
1658
- stats$comp_pct, stats$loo_pct)
1659
-
1660
- # Same palette vibe as the PDF row (red/blue/green/cyan/orange)
1661
- label_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60",
1662
- "#00BCD4", "#F39C12", "#F39C12", "#F39C12", "#F39C12", "#F39C12")
1663
 
1664
- # Layout: make the tiles taller and a bit wider, like the PDF
1665
- tiles_y <- 0.895
1666
- tile_w <- 0.073
1667
- tile_h <- 0.055
1668
- tile_gap <- 0.010
1669
- x_start <- 0.02
1670
 
1671
- band_h <- tile_h * 0.55
 
1672
 
1673
- for (i in seq_along(stat_labels)) {
1674
- x_pos <- x_start + (i - 1) * (tile_w + tile_gap)
1675
 
1676
- # Outer border
1677
- grid.rect(
1678
- x = x_pos, y = tiles_y, width = tile_w, height = tile_h,
1679
- just = c("left", "center"),
1680
- gp = gpar(fill = NA, col = label_colors[i], lwd = 2.2)
1681
- )
1682
 
1683
- # Top colored band
1684
- grid.rect(
1685
- x = x_pos, y = tiles_y + (tile_h/2) - (band_h/2),
1686
- width = tile_w, height = band_h,
1687
- just = c("left", "center"),
1688
- gp = gpar(fill = label_colors[i], col = NA)
1689
- )
 
1690
 
1691
- # Bottom white area (optional subtle border line)
1692
- grid.rect(
1693
- x = x_pos, y = tiles_y - (tile_h/2) + ((tile_h - band_h)/2),
1694
- width = tile_w, height = (tile_h - band_h),
1695
- just = c("left", "center"),
1696
- gp = gpar(fill = "white", col = "black", lwd = 0.8)
1697
- )
 
1698
 
1699
- # Label (white, centered in colored band)
1700
- grid.text(
1701
- stat_labels[i],
1702
- x = x_pos + tile_w/2,
1703
- y = tiles_y + (tile_h/2) - (band_h/2),
1704
- gp = gpar(col = "white", cex = 0.72, fontface = "bold")
1705
- )
1706
 
1707
- # Value (big, centered in white area)
1708
- grid.text(
1709
- as.character(stat_values[i]),
1710
- x = x_pos + tile_w/2,
1711
- y = tiles_y - (tile_h/2) + ((tile_h - band_h)/2),
1712
- gp = gpar(col = "black", cex = 1.05, fontface = "bold")
1713
- )
1714
- }
1715
 
1716
- # CHARTS (Location directly under stats; similar sizing)
1717
- pushViewport(viewport(x = 0.23, y = 0.67, width = 0.42, height = 0.30))
 
 
 
1718
  print(loc_plot, newpage = FALSE)
1719
  popViewport()
1720
 
1721
- pushViewport(viewport(x = 0.23, y = 0.37, width = 0.40, height = 0.24))
1722
  print(mov_plot, newpage = FALSE)
1723
  popViewport()
1724
 
1725
- pushViewport(viewport(x = 0.23, y = 0.13, width = 0.42, height = 0.24))
1726
  print(rel_plot, newpage = FALSE)
1727
  popViewport()
1728
 
1729
- # TABLES (Top third = Location + Usage; middle = Velo; bottom = Release)
 
 
 
 
 
1730
  table_x <- 0.56
1731
  table_w <- 0.41
1732
 
@@ -1736,7 +1758,7 @@ for (i in seq_along(stat_labels)) {
1736
  rows = c("Zone%"="Zone%", "Edge%"="Edge%", "Strike%"="Strike%", "Whiff%"="Whiff%"),
1737
  pitch_types = pitch_types,
1738
  pitch_colors = tableau_pitch_colors,
1739
- x = table_x, y = 0.78, width = table_w, height = 0.16
1740
  )
1741
 
1742
  draw_tableau_table_fill(
@@ -1745,7 +1767,7 @@ for (i in seq_along(stat_labels)) {
1745
  rows = c("Usage vs. LHH"="Usage vs. LHH", "Usage vs. RHH"="Usage vs. RHH", "Pitch Count"="Pitch Count"),
1746
  pitch_types = pitch_types,
1747
  pitch_colors = tableau_pitch_colors,
1748
- x = table_x, y = 0.60, width = table_w, height = 0.14
1749
  )
1750
 
1751
  draw_tableau_table_fill(
@@ -1756,7 +1778,7 @@ for (i in seq_along(stat_labels)) {
1756
  "Avg. IVB"="Avg. IVB", "Avg. HB"="Avg. HB"),
1757
  pitch_types = pitch_types,
1758
  pitch_colors = tableau_pitch_colors,
1759
- x = table_x, y = 0.42, width = table_w, height = 0.20
1760
  )
1761
 
1762
  draw_tableau_table_fill(
@@ -1769,195 +1791,13 @@ for (i in seq_along(stat_labels)) {
1769
  "Ext"="Avg. Ext"),
1770
  pitch_types = pitch_types,
1771
  pitch_colors = tableau_pitch_colors,
1772
- x = table_x, y = 0.20, width = table_w, height = 0.22
1773
  )
1774
 
1775
  invisible(output_file)
1776
  }
1777
 
1778
 
1779
- # =====================================================================
1780
- # MAIN PDF FUNCTION
1781
- # =====================================================================
1782
- create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1783
- if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1784
-
1785
- pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1786
- filter(Pitcher == pitcher_name)
1787
-
1788
- if (nrow(pitcher_df) == 0) {
1789
- pdf(output_file, width = 8.5, height = 11)
1790
- grid.newpage()
1791
- grid.text(paste("No data found for", pitcher_name),
1792
- gp = gpar(fontsize = 16, fontface = "bold"))
1793
- dev.off()
1794
- return(output_file)
1795
- }
1796
-
1797
- # Metadata - only get away team (batter team)
1798
- game_date <- tryCatch({
1799
- d <- unique(pitcher_df$Date)[1]
1800
- if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
1801
- }, error = function(e) "NA")
1802
-
1803
- # Get opponent team (batter team, not pitcher team)
1804
- batter_teams <- unique(pitcher_df$BatterTeam)
1805
- batter_teams <- batter_teams[!is.na(batter_teams)]
1806
- away_team <- if (length(batter_teams) > 0) batter_teams[1] else "Unknown"
1807
-
1808
- # Calculate stats
1809
- stats <- calculate_tableau_header_stats(pitcher_df)
1810
- loc_data <- calculate_tableau_location_data(pitcher_df)
1811
- usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1812
- velo_data <- calculate_tableau_velo_movement(pitcher_df)
1813
- rel_data <- calculate_tableau_release_data(pitcher_df)
1814
-
1815
- # Pitch types ordered by usage (Pitch Count desc)
1816
- pitch_types <- usage_data %>%
1817
- arrange(desc(`Pitch Count`)) %>%
1818
- pull(TaggedPitchType) %>%
1819
- unique()
1820
-
1821
- pitch_types <- pitch_types[!is.na(pitch_types) & pitch_types != ""]
1822
- if (length(pitch_types) == 0) pitch_types <- "Undefined"
1823
- # Create plots
1824
- loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
1825
- mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1826
- rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1827
-
1828
- # Create PDF
1829
- pdf(output_file, width = 8.5, height = 11)
1830
- on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1831
-
1832
- grid.newpage()
1833
-
1834
- # =====================================================================
1835
- # HEADER BAR
1836
- # =====================================================================
1837
- grid.rect(x = 0, y = 0.955, width = 1, height = 0.045,
1838
- just = c("left", "bottom"),
1839
- gp = gpar(fill = "#006F71", col = NA))
1840
-
1841
- grid.text("Pitcher Post-Game Report", x = 0.02, y = 0.977, just = "left",
1842
- gp = gpar(col = "white", fontface = "bold", cex = 1.2))
1843
-
1844
- # =====================================================================
1845
- # INFO ROW - Date, Pitcher Name, vs Away Team only
1846
- # =====================================================================
1847
- info_y <- 0.935
1848
- grid.text(paste0(game_date, " | ", pitcher_name, " vs ", away_team),
1849
- x = 0.02, y = info_y, just = "left",
1850
- gp = gpar(cex = 0.7, fontface = "bold"))
1851
-
1852
- # =====================================================================
1853
- # STAT BOXES - Colored labels, white value backgrounds
1854
- # =====================================================================
1855
- stat_labels <- c("At Bats", "H", "XBH", "R", "BB/HBP", "SO", "AVG",
1856
- "Strike%", "1st P K%", "E+A%", "Comp%", "LOO%")
1857
- stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs,
1858
- stats$bb_hbp, stats$so, stats$avg,
1859
- stats$strike_pct, stats$fp_k_pct, stats$ea_pct,
1860
- stats$comp_pct, stats$loo_pct)
1861
-
1862
- # Label colors (text color for the stat name)
1863
- label_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60", "#00BCD4",
1864
- "#E67E22", "#E67E22", "#E67E22", "#E67E22", "#E67E22")
1865
-
1866
- box_y <- 0.905
1867
- box_w <- 0.065
1868
- box_h <- 0.038
1869
- x_start <- 0.02
1870
-
1871
- for (i in 1:12) {
1872
- x_pos <- x_start + (i-1) * (box_w + 0.012)
1873
-
1874
- # White background box with border matching label color
1875
- grid.rect(x = x_pos, y = box_y, width = box_w, height = box_h,
1876
- just = c("left", "center"),
1877
- gp = gpar(fill = "white", col = label_colors[i], lwd = 1.5))
1878
-
1879
- # Colored label at top
1880
- grid.text(stat_labels[i], x = x_pos + box_w/2, y = box_y + 0.010,
1881
- gp = gpar(col = label_colors[i], cex = 0.40, fontface = "bold"))
1882
-
1883
- # Black value below
1884
- grid.text(as.character(stat_values[i]), x = x_pos + box_w/2, y = box_y - 0.007,
1885
- gp = gpar(col = "black", cex = 0.52, fontface = "bold"))
1886
- }
1887
-
1888
- # =====================================================================
1889
- # CHARTS - Moved left and higher
1890
- # =====================================================================
1891
-
1892
- # Location plot (top left) - moved left and up
1893
- pushViewport(viewport(x = 0.22, y = 0.69, width = 0.40, height = 0.28))
1894
- print(loc_plot, newpage = FALSE)
1895
- popViewport()
1896
-
1897
- # Movement plot (middle left)
1898
- pushViewport(viewport(x = 0.22, y = 0.41, width = 0.38, height = 0.24))
1899
- print(mov_plot, newpage = FALSE)
1900
- popViewport()
1901
-
1902
- # Release plot (bottom left) - bigger
1903
- pushViewport(viewport(x = 0.22, y = 0.14, width = 0.40, height = 0.24))
1904
- print(rel_plot, newpage = FALSE)
1905
- popViewport()
1906
-
1907
-
1908
- # =====================================================================
1909
- # TABLES (moved UP to avoid cutoff + stacked like Tableau)
1910
- # =====================================================================
1911
- table_x <- 0.58
1912
- table_w <- 0.41
1913
-
1914
- # Top third (Location + Usage)
1915
- draw_tableau_table_fill(
1916
- title = "Location Data",
1917
- data = loc_data,
1918
- rows = c("Zone%"="Zone%", "Edge%"="Edge%", "Strike%"="Strike%", "Whiff%"="Whiff%"),
1919
- pitch_types = pitch_types,
1920
- pitch_colors = tableau_pitch_colors,
1921
- x = table_x, y = 0.83, width = table_w, height = 0.16
1922
- )
1923
-
1924
- draw_tableau_table_fill(
1925
- title = "Pitch Usage",
1926
- data = usage_data,
1927
- rows = c("Usage vs. LHH"="Usage vs. LHH", "Usage vs. RHH"="Usage vs. RHH", "Pitch Count"="Pitch Count"),
1928
- pitch_types = pitch_types,
1929
- pitch_colors = tableau_pitch_colors,
1930
- x = table_x, y = 0.64, width = table_w, height = 0.15
1931
- )
1932
-
1933
- # Middle block (Velo & Movement)
1934
- draw_tableau_table_fill(
1935
- title = "Velo & Movement",
1936
- data = velo_data,
1937
- rows = c("Avg. Velo"="Avg. Velo", "Max Velo"="Max. Velo",
1938
- "Avg. Spin"="Avg. Spin", "Max Spin"="Max. Spin",
1939
- "Avg. IVB"="Avg. IVB", "Avg. HB"="Avg. HB"),
1940
- pitch_types = pitch_types,
1941
- pitch_colors = tableau_pitch_colors,
1942
- x = table_x, y = 0.46, width = table_w, height = 0.20
1943
- )
1944
-
1945
- # Bottom block (Release Data) – lifted so it always fits
1946
- draw_tableau_table_fill(
1947
- title = "Release Data",
1948
- data = rel_data,
1949
- rows = c("Rel Ht"="Avg. Rel Ht",
1950
- "Rel Ht vs FB (in)"="Rel Ht vs. FB",
1951
- "Rel Side"="Avg. Rel Side",
1952
- "Rel Side vs FB (in)"="Rel Side vs. FB",
1953
- "Ext"="Avg. Ext"),
1954
- pitch_types = pitch_types,
1955
- pitch_colors = tableau_pitch_colors,
1956
- x = table_x, y = 0.24, width = table_w, height = 0.20
1957
- )
1958
-
1959
- invisible(output_file)
1960
- }
1961
 
1962
  # =====================================================================
1963
  # ===================== CATCHER CODE (wrapped) =======================
 
159
  }
160
  "
161
 
 
 
 
 
162
  process_dataset <- function(df) {
163
  if ("Batter" %in% names(df)) {
164
  df <- df %>% mutate(Batter = stringr::str_replace(Batter, "^\\s*(\\w+)\\s*,\\s*(\\w+)\\s*$", "\\2 \\1"))
 
1586
  }
1587
  }
1588
 
1589
+
 
 
1590
  create_tableau_pitcher_pdf <- function(game_df, pitcher_name, output_file) {
1591
  if (length(dev.list()) > 0) try(dev.off(), silent = TRUE)
1592
 
1593
  pitcher_df <- process_tableau_pitcher_data(game_df) %>%
1594
+ dplyr::filter(Pitcher == pitcher_name)
1595
 
1596
  if (nrow(pitcher_df) == 0) {
1597
  pdf(output_file, width = 8.5, height = 11)
 
1602
  return(output_file)
1603
  }
1604
 
1605
+ # ---- metadata ----
1606
  game_date <- tryCatch({
1607
  d <- unique(pitcher_df$Date)[1]
1608
  if (inherits(d, "Date")) format(d, "%m/%d/%Y") else as.character(d)
 
1612
  batter_teams <- batter_teams[!is.na(batter_teams)]
1613
  away_team <- if (length(batter_teams) > 0) batter_teams[1] else "Unknown"
1614
 
1615
+ # ---- calculations ----
1616
+ stats <- calculate_tableau_header_stats(pitcher_df)
1617
+ loc_data <- calculate_tableau_location_data(pitcher_df)
1618
  usage_data <- calculate_tableau_pitch_usage(pitcher_df)
1619
+ velo_data <- calculate_tableau_velo_movement(pitcher_df)
1620
+ rel_data <- calculate_tableau_release_data(pitcher_df)
1621
+
1622
+ # Pitch types ordered by usage (Pitch Count desc), then append any that appear elsewhere
1623
+ usage_order <- usage_data %>%
1624
+ dplyr::arrange(dplyr::desc(`Pitch Count`)) %>%
1625
+ dplyr::pull(TaggedPitchType) %>%
1626
+ unique()
1627
+
1628
+ other_types <- unique(c(loc_data$TaggedPitchType, velo_data$TaggedPitchType, rel_data$TaggedPitchType))
1629
+ other_types <- other_types[!is.na(other_types) & other_types != ""]
1630
+ usage_order <- usage_order[!is.na(usage_order) & usage_order != ""]
1631
 
1632
+ pitch_types <- unique(c(usage_order, setdiff(other_types, usage_order)))
 
1633
  if (length(pitch_types) == 0) pitch_types <- "Undefined"
1634
 
1635
+ # ---- plots ----
1636
  loc_plot <- create_tableau_location_plot(pitcher_df, tableau_pitch_colors)
1637
  mov_plot <- create_tableau_movement_plot(pitcher_df, tableau_pitch_colors)
1638
  rel_plot <- create_tableau_release_plot(pitcher_df, tableau_pitch_colors)
1639
 
1640
+ # ---- render ----
1641
  pdf(output_file, width = 8.5, height = 11)
1642
  on.exit(try(dev.off(), silent = TRUE), add = TRUE)
1643
 
1644
  grid.newpage()
1645
 
1646
+ # =====================================================================
1647
  # HEADER BAR
1648
+ # =====================================================================
1649
  grid.rect(x = 0, y = 0.955, width = 1, height = 0.045,
1650
  just = c("left", "bottom"),
1651
  gp = gpar(fill = "#006F71", col = NA))
 
1657
  info_y <- 0.935
1658
  grid.text(paste0(game_date, " | ", pitcher_name, " vs ", away_team),
1659
  x = 0.02, y = info_y, just = "left",
1660
+ gp = gpar(cex = 0.82, fontface = "bold"))
1661
 
1662
+ # =====================================================================
1663
+ # STAT TILES (Tableau-style: top color band + bottom white; BIG + readable)
1664
+ # =====================================================================
1665
+ stat_labels <- c("At Bats","H","XBH","R","BB/HBP","SO","AVG",
1666
+ "Strike%","1st P K%","E+A%","Comp%","LOO%")
1667
+ stat_values <- c(stats$at_bats, stats$hits, stats$xbh, stats$runs,
1668
+ stats$bb_hbp, stats$so, stats$avg,
1669
+ stats$strike_pct, stats$fp_k_pct, stats$ea_pct,
1670
+ stats$comp_pct, stats$loo_pct)
1671
 
1672
+ label_colors <- c("#E74C3C", "#3498DB", "#3498DB", "#27AE60", "#27AE60", "#27AE60",
1673
+ "#00BCD4", "#F39C12", "#F39C12", "#F39C12", "#F39C12", "#F39C12")
 
 
 
 
 
 
 
1674
 
1675
+ # Fit math (prevents clipping)
1676
+ tiles_y <- 0.905
1677
+ x_start <- 0.02
1678
+ tile_w <- 0.0715
1679
+ tile_h <- 0.060
1680
+ tile_gap <- 0.0075
1681
 
1682
+ band_h <- tile_h * 0.52
1683
+ bottom_h <- tile_h - band_h
1684
 
1685
+ for (i in seq_along(stat_labels)) {
1686
+ x_pos <- x_start + (i - 1) * (tile_w + tile_gap)
1687
 
1688
+ # Outer border
1689
+ grid.rect(
1690
+ x = x_pos, y = tiles_y, width = tile_w, height = tile_h,
1691
+ just = c("left", "center"),
1692
+ gp = gpar(fill = NA, col = label_colors[i], lwd = 2.4)
1693
+ )
1694
 
1695
+ # Top colored band
1696
+ grid.rect(
1697
+ x = x_pos,
1698
+ y = tiles_y + (tile_h/2) - (band_h/2),
1699
+ width = tile_w, height = band_h,
1700
+ just = c("left", "center"),
1701
+ gp = gpar(fill = label_colors[i], col = NA)
1702
+ )
1703
 
1704
+ # Bottom white area
1705
+ grid.rect(
1706
+ x = x_pos,
1707
+ y = tiles_y - (tile_h/2) + (bottom_h/2),
1708
+ width = tile_w, height = bottom_h,
1709
+ just = c("left", "center"),
1710
+ gp = gpar(fill = "white", col = "black", lwd = 0.9)
1711
+ )
1712
 
1713
+ # Label (white, centered in color band)
1714
+ grid.text(
1715
+ stat_labels[i],
1716
+ x = x_pos + tile_w/2,
1717
+ y = tiles_y + (tile_h/2) - (band_h/2),
1718
+ gp = gpar(col = "white", cex = 0.88, fontface = "bold")
1719
+ )
1720
 
1721
+ # Value (BIG)
1722
+ grid.text(
1723
+ as.character(stat_values[i]),
1724
+ x = x_pos + tile_w/2,
1725
+ y = tiles_y - (tile_h/2) + (bottom_h/2),
1726
+ gp = gpar(col = "black", cex = 1.35, fontface = "bold")
1727
+ )
1728
+ }
1729
 
1730
+ # =====================================================================
1731
+ # CHARTS (left column)
1732
+ # =====================================================================
1733
+ # Location directly under header stats
1734
+ pushViewport(viewport(x = 0.23, y = 0.675, width = 0.42, height = 0.30))
1735
  print(loc_plot, newpage = FALSE)
1736
  popViewport()
1737
 
1738
+ pushViewport(viewport(x = 0.23, y = 0.375, width = 0.40, height = 0.24))
1739
  print(mov_plot, newpage = FALSE)
1740
  popViewport()
1741
 
1742
+ pushViewport(viewport(x = 0.23, y = 0.135, width = 0.42, height = 0.24))
1743
  print(rel_plot, newpage = FALSE)
1744
  popViewport()
1745
 
1746
+ # =====================================================================
1747
+ # TABLES (right column) - lifted + guaranteed fit
1748
+ # Top third: Location + Usage
1749
+ # Middle: Velo & Movement
1750
+ # Bottom: Release Data
1751
+ # =====================================================================
1752
  table_x <- 0.56
1753
  table_w <- 0.41
1754
 
 
1758
  rows = c("Zone%"="Zone%", "Edge%"="Edge%", "Strike%"="Strike%", "Whiff%"="Whiff%"),
1759
  pitch_types = pitch_types,
1760
  pitch_colors = tableau_pitch_colors,
1761
+ x = table_x, y = 0.84, width = table_w, height = 0.16
1762
  )
1763
 
1764
  draw_tableau_table_fill(
 
1767
  rows = c("Usage vs. LHH"="Usage vs. LHH", "Usage vs. RHH"="Usage vs. RHH", "Pitch Count"="Pitch Count"),
1768
  pitch_types = pitch_types,
1769
  pitch_colors = tableau_pitch_colors,
1770
+ x = table_x, y = 0.66, width = table_w, height = 0.15
1771
  )
1772
 
1773
  draw_tableau_table_fill(
 
1778
  "Avg. IVB"="Avg. IVB", "Avg. HB"="Avg. HB"),
1779
  pitch_types = pitch_types,
1780
  pitch_colors = tableau_pitch_colors,
1781
+ x = table_x, y = 0.48, width = table_w, height = 0.22
1782
  )
1783
 
1784
  draw_tableau_table_fill(
 
1791
  "Ext"="Avg. Ext"),
1792
  pitch_types = pitch_types,
1793
  pitch_colors = tableau_pitch_colors,
1794
+ x = table_x, y = 0.285, width = table_w, height = 0.22
1795
  )
1796
 
1797
  invisible(output_file)
1798
  }
1799
 
1800
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1801
 
1802
  # =====================================================================
1803
  # ===================== CATCHER CODE (wrapped) =======================