Skip to content

Instantly share code, notes, and snippets.

@Henryjean
Created October 18, 2022 13:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Henryjean/9d6c624ce1768f479c6046cb08803a56 to your computer and use it in GitHub Desktop.
Save Henryjean/9d6c624ce1768f479c6046cb08803a56 to your computer and use it in GitHub Desktop.
Make a diamond plot from EPA per play data (colorblind friendly)
library(tidyverse)
library(nflplotR)
library(nflreadr)
# need this for rotation and saving
library(grid)
# need this for labels
library(ggtext)
# data loading and wrangling copied from: https://www.nflfastr.com/articles/beginners_guide.html#figures-with-qb-stats
# get pbp and filter to regular season rush and pass plays
pbp <- nflreadr::load_pbp(2022) %>%
dplyr::filter(season_type == "REG") %>%
dplyr::filter(!is.na(posteam) & (rush == 1 | pass == 1))
# offense epa
offense <- pbp %>%
dplyr::group_by(team = posteam) %>%
dplyr::summarise(off_epa = mean(epa, na.rm = TRUE))
# defense epa
defense <- pbp %>%
dplyr::group_by(team = defteam) %>%
dplyr::summarise(def_epa = mean(epa, na.rm = TRUE))
# join offense and defense
df <- offense %>%
inner_join(defense, by = "team")
# set max and min for off and def epa (want them to be symmetric for chart)
off_epa_min <- -.2
off_epa_max <- .2
def_epa_min <- -.2
def_epa_max <- .2
# set rotation to 45 degrees
rotation <- 45
# Colorblind friendly
p_cb <- df %>%
ggplot(aes(x = off_epa, y = def_epa)) +
# add color blocking
annotate("rect", xmin = (off_epa_max + off_epa_min) / 2, xmax = off_epa_max,
ymin = def_epa_min, ymax = (def_epa_max + def_epa_min) / 2, fill= "#a6dba0", alpha = .5, color = 'transparent') +
annotate("rect", xmin = off_epa_min, xmax = (off_epa_max + off_epa_min) / 2,
ymin = (def_epa_max + def_epa_min) / 2, ymax = def_epa_max, fill= "#c2a5cf", alpha = .5, color = 'transparent') +
annotate("rect", xmin = (off_epa_max + off_epa_min) / 2, xmax = off_epa_max,
ymin = (def_epa_max + def_epa_min) / 2, ymax = def_epa_max, fill= "#f7f7f7", alpha = .5, color = 'transparent') +
annotate("rect", xmin = off_epa_min, xmax = (off_epa_max + off_epa_min) / 2,
ymin = def_epa_min, ymax = (def_epa_max + def_epa_min) / 2, fill= "#f7f7f7", alpha = .5, color = 'transparent') +
# add team logos
geom_nfl_logos(aes(team_abbr = team), width = 0.08, alpha = 0.75, angle = -1*rotation) +
scale_y_reverse(limits = c(def_epa_max, def_epa_min), breaks = seq(.20, -.20, -.05),
labels = scales::number_format(style_positive = "plus", accuracy = .01)) +
scale_x_continuous(limits = c(off_epa_min, off_epa_max), breaks = seq(-.2, .2, .05),
labels = scales::number_format(style_positive = "plus", accuracy = .01)) +
coord_equal() +
# add axis labels
labs(
x = "Offensive EPA/play",
y = "Defensive EPA/play"
) +
# thematic stuff
theme_minimal(base_family = 'Roboto') +
theme(axis.text.x = element_text(angle=(-1 * rotation), hjust = 0.5, margin = margin(t = -9.5)),
axis.text.y = element_text(angle=(-1 * rotation), hjust = 0.5, margin = margin(r = -5)),
axis.title.x = element_text(size = 12,
#angle=(-1 * rotation + 45),
vjust = 0.5,
margin = margin(t = 10),
face = 'bold',
color = "black"),
axis.title.y = element_text(size = 12,
angle=(-1 * rotation - 45),
hjust = 0.5,
margin = margin(r = 10),
color = "black",
face = 'bold'),
plot.margin = margin(1.15, .5, .5, -.25, unit = 'in'),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = 'floralwhite', color = "floralwhite")) +
# hack together a title and subtitle
annotate(geom = 'text', x = .2, y = -.2, label = "2022 NFL Offensive and Defensive EPA per Play", angle = -1 * rotation, vjust = -3.5, fontface = 'bold', size = 4, family = 'Roboto') +
annotate(geom = 'text', x = .2, y = -.2, label = paste0("As of ", format.Date(Sys.Date(), "%B %d, %Y"), ""), angle = -1 * rotation, vjust = -2.5, size = 3, family = 'Roboto') +
annotate(geom = 'text', x = -.19, y = .19, label = "Data: @nflfastR | Chart: @owenlhjphillips", angle = -1 * rotation, hjust = -0.55, vjust = 5, size = 2.5, family = 'Roboto') +
# hack together a few chart guides (ie, 'Good D, Bad D')
geom_richtext(aes(x = .175, y = .175, label = "Good O, Bad D"), angle = -1 * rotation, size = 2.75, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#f7f7f7") +
geom_richtext(aes(x = -.18, y = -.18, label = "Bad O, Good D"), angle = -1 * rotation, size = 2.75, family = 'Roboto', fontface = 'bold', color = 'black', fill = "#f7f7f7") +
geom_richtext(aes(x = -.18, y = .18, label = "Bad O, Bad D"), angle = -1 * rotation, size = 2.75, family = 'Roboto', fontface = 'bold', color = 'white', fill = "#762a83", label.colour = 'black') +
geom_richtext(aes(x = .175, y = -.175, label = "Good O, Good D"), angle = -1 * rotation, size = 2.75, family = 'Roboto', fontface = 'bold', color = 'white', fill = "#1b7837", label.colour = 'black')
# save plot
png("epa_diamond_plot_cb.png", res = 300, width = 6, height = 6, units = 'in', bg = 'floralwhite')
print(p_cb, vp=viewport(angle=rotation,
width = unit(6, "in"),
height = unit(6, "in")))
dev.off()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment