sdvplotR has two kinds of gt helpers, and this article
uses both on one table.
-
The team layer turns team keys into logos,
headshots and team colors (
gt_sdv_logos(),gt_sdv_headshots(),gt_merge_stack_team_color()). It resolves every abbreviation, alias and relocated franchise throughclean_team_abbrs(). - The table toolkit, ported from Andrew Weatherman’s gtUtils, handles the editorial side: cut lines, colored ranks, captions, legends, grids of tables, themes, and saving images sized for posting.
Every one of these functions takes a gt table and
returns one, so they chain in any order. Having both in one package
means one install, and one set of conventions for how a sports table is
built.
The data
The final 2023 AFC standings, typed in so the article builds offline. The seven playoff seeds come first in seed order, followed by the rest by record.
afc <- data.frame(
team = c(
"BAL", "BUF", "KC", "HOU", "CLE", "MIA", "PIT",
"CIN", "IND", "JAX", "DEN", "LV", "NYJ", "TEN", "LAC", "NE"
),
seed = c(1:7, rep(NA, 9)),
w = c(13, 11, 11, 10, 11, 11, 10, 9, 9, 9, 8, 8, 7, 6, 5, 4)
)
afc$l <- 17 - afc$w
afc$pct <- afc$w / 17Step by step
Start with logos, labels and a box-score win percentage
(.765, not 0.765).
base <- function(df, conference) {
gt(df) |>
gt_sdv_logos(columns = "team", sport = "nfl", height = 24) |>
cols_label(team = "", seed = "Seed", w = "W", l = "L", pct = "Win pct") |>
sub_missing(columns = "seed", missing_text = "") |>
fmt(columns = "pct", fns = function(x) sub("^0", "", sprintf("%.3f", x))) |>
tab_header(paste(conference, "playoff picture"), "Final 2023 standings")
}
base(afc, "AFC")| AFC playoff picture | ||||
| Final 2023 standings | ||||
| Seed | W | L | Win pct | |
|---|---|---|---|---|
![]() |
1 | 13 | 4 | .765 |
![]() |
2 | 11 | 6 | .647 |
![]() |
3 | 11 | 6 | .647 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 11 | 6 | .647 |
![]() |
6 | 11 | 6 | .647 |
![]() |
7 | 10 | 7 | .588 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
4 | 13 | .235 | |
A standings table is read around one question: who is in?
gt_cutline() draws the answer as a line after the seventh
row, with a label, so readers don’t have to count seeds.
base(afc, "AFC") |>
gt_cutline(after = 7, label = "Playoff line")| AFC playoff picture | ||||
| Final 2023 standings | ||||
| Seed | W | L | Win pct | |
|---|---|---|---|---|
![]() |
1 | 13 | 4 | .765 |
![]() |
2 | 11 | 6 | .647 |
![]() |
3 | 11 | 6 | .647 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 11 | 6 | .647 |
![]() |
6 | 11 | 6 | .647 |
![]() |
7 | 10 | 7 | .588 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
4 | 13 | .235 | |
gt_color_ranks() colors a column by rank, which shows
where the wins cluster (four teams at 11 wins, three at 9) faster than
reading the numbers; reverse = TRUE puts the most wins at
the green end. The cut line’s label sits in the row below the line and
clears that row’s cell colors, so with colored cells, keep the line
unlabeled and say what it means in the caption.
gt_538_caption() separates that note from the source line.
gt_theme_sdv() goes last so it styles everything added
before it, and density is passed through for the saved
version below.
playoff_table <- function(df, conference, density = "comfortable") {
base(df, conference) |>
gt_cutline(after = 7) |>
gt_color_ranks(columns = "w", reverse = TRUE) |>
gt_538_caption(
top_caption = "Above the line: playoff teams.",
bottom_caption = "Data: nflverse"
) |>
gt_theme_sdv(density = density)
}
playoff_table(afc, "AFC")| AFC playoff picture | ||||
| Final 2023 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 13 | 4 | .765 |
![]() |
2 | 11 | 6 | .647 |
![]() |
3 | 11 | 6 | .647 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 11 | 6 | .647 |
![]() |
6 | 11 | 6 | .647 |
![]() |
7 | 10 | 7 | .588 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
4 | 13 | .235 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse | ||||
Both conferences at once
gt_grid() lays several finished tables out under one
shared title and source line. Each table keeps its own theme and cut
line; a note that applies to both goes in the grid’s
caption, so each table’s own caption stays short and the
tables stay narrow enough to sit side by side.
nfc <- data.frame(
team = c(
"SF", "DAL", "DET", "TB", "PHI", "LA", "GB",
"NO", "SEA", "ATL", "CHI", "MIN", "NYG", "ARI", "WAS", "CAR"
),
seed = c(1:7, rep(NA, 9)),
w = c(12, 12, 12, 9, 11, 10, 9, 9, 9, 7, 7, 7, 6, 4, 4, 2)
)
nfc$l <- 17 - nfc$w
nfc$pct <- nfc$w / 17
gt_grid(
list(playoff_table(afc, "AFC"), playoff_table(nfc, "NFC")),
ncol = 2,
title = "The 2023 NFL playoff field",
subtitle = "Final regular-season standings",
caption = "Seeds 1-4 won their division; 5-7 are wild cards.",
title_style = list(font = "Chivo", weight = 800)
)| AFC playoff picture | ||||
| Final 2023 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 13 | 4 | .765 |
![]() |
2 | 11 | 6 | .647 |
![]() |
3 | 11 | 6 | .647 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 11 | 6 | .647 |
![]() |
6 | 11 | 6 | .647 |
![]() |
7 | 10 | 7 | .588 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
4 | 13 | .235 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse | ||||
| NFC playoff picture | ||||
| Final 2023 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 12 | 5 | .706 |
![]() |
2 | 12 | 5 | .706 |
![]() |
3 | 12 | 5 | .706 |
![]() |
4 | 9 | 8 | .529 |
![]() |
5 | 11 | 6 | .647 |
![]() |
6 | 10 | 7 | .588 |
![]() |
7 | 9 | 8 | .529 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
7 | 10 | .412 | |
![]() |
7 | 10 | .412 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
4 | 13 | .235 | |
![]() |
4 | 13 | .235 | |
![]() |
2 | 15 | .118 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse | ||||
On a narrow screen the grid scrolls sideways inside its own box, so the page around it stays in place.
Posting it
gt_save_crop() saves a single table, trimmed and padded;
gt_social_crop() pads it to a platform’s aspect ratio. Use
the theme’s background from theme_bg so the padding blends
with the table. gt_grid() saves the whole grid when you
give it a file. Saving drives a headless Chrome through
webshot2, so these lines are not run here.
bg <- theme_bg$bg[theme_bg$theme == "gt_theme_sdv" & theme_bg$has_style == "light"]
playoff_table(afc, "AFC", density = "social") |>
gt_social_crop("afc-playoffs.png", aspect_ratio = "4:5", bg = bg)
gt_grid(
list(playoff_table(afc, "AFC"), playoff_table(nfc, "NFC")),
title = "The 2023 NFL playoff field",
file = "playoff-field.png"
)Where to go next
- SportsDataverse Table Themes covers the house theme, the dark style and team colors.
- The gt table cookbooks go deeper on single tools: styling headers, legends and captions, percentile bars and cut lines, faceted tables, tier lists and saving and posting.
































