On this page
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 throughsdv_resolve(). - 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 2025 standings. nflseedR::nfl_standings()
computes them from nflverse’s schedule, read with nflreadr,
NFL tiebreakers included; its conf_rank is each team’s
place in its conference, so ranks 1-7 are the playoff seeds. The seeds
come first in seed order, followed by the rest.
standings <- nflseedR::nfl_standings(nflreadr::load_schedules(2025), verbosity = "NONE")
conference_standings <- function(conference) {
s <- standings[standings$conf == conference, ]
s <- s[order(s$conf_rank), ]
data.frame(
team = s$team,
seed = ifelse(s$conf_rank <= 7, s$conf_rank, NA),
w = s$true_wins,
l = s$losses,
pct = s$win_pct
)
}
afc <- conference_standings("AFC")Step 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", league = "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 2025 standings")
}
base(afc, "AFC")| AFC playoff picture | ||||
| Final 2025 standings | ||||
| Seed | W | L | Win pct | |
|---|---|---|---|---|
![]() |
1 | 14 | 3 | .824 |
![]() |
2 | 14 | 3 | .824 |
![]() |
3 | 13 | 4 | .765 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 12 | 5 | .706 |
![]() |
6 | 12 | 5 | .706 |
![]() |
7 | 11 | 6 | .647 |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
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 2025 standings | ||||
| Seed | W | L | Win pct | |
|---|---|---|---|---|
![]() |
1 | 14 | 3 | .824 |
![]() |
2 | 14 | 3 | .824 |
![]() |
3 | 13 | 4 | .765 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 12 | 5 | .706 |
![]() |
6 | 12 | 5 | .706 |
![]() |
7 | 11 | 6 | .647 |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
gt_color_ranks() colors a column by its values, which
shows where the wins cluster 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 via nflreadr and nflseedR"
) |>
gt_theme_sdv(density = density)
}
playoff_table(afc, "AFC")| AFC playoff picture | ||||
| Final 2025 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 14 | 3 | .824 |
![]() |
2 | 14 | 3 | .824 |
![]() |
3 | 13 | 4 | .765 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 12 | 5 | .706 |
![]() |
6 | 12 | 5 | .706 |
![]() |
7 | 11 | 6 | .647 |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse via nflreadr and nflseedR | ||||
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 <- conference_standings("NFC")
gt_grid(
list(playoff_table(afc, "AFC"), playoff_table(nfc, "NFC")),
ncol = 2,
title = "The 2025 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 2025 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 14 | 3 | .824 |
![]() |
2 | 14 | 3 | .824 |
![]() |
3 | 13 | 4 | .765 |
![]() |
4 | 10 | 7 | .588 |
![]() |
5 | 12 | 5 | .706 |
![]() |
6 | 12 | 5 | .706 |
![]() |
7 | 11 | 6 | .647 |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 10 | .412 | |
![]() |
6 | 11 | .353 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
![]() |
3 | 14 | .176 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse via nflreadr and nflseedR | ||||
| NFC playoff picture | ||||
| Final 2025 standings | ||||
| 1 | Seed1 | W1 | L1 | Win pct1 |
|---|---|---|---|---|
![]() |
1 | 14 | 3 | .824 |
![]() |
2 | 11 | 6 | .647 |
![]() |
3 | 11 | 6 | .647 |
![]() |
4 | 8 | 9 | .471 |
![]() |
5 | 12 | 5 | .706 |
![]() |
6 | 12 | 5 | .706 |
![]() |
7 | 9 | 7 | .559 |
![]() |
9 | 8 | .529 | |
![]() |
9 | 8 | .529 | |
![]() |
8 | 9 | .471 | |
![]() |
8 | 9 | .471 | |
![]() |
7 | 9 | .441 | |
![]() |
6 | 11 | .353 | |
![]() |
5 | 12 | .294 | |
![]() |
4 | 13 | .235 | |
![]() |
3 | 14 | .176 | |
| 1 Above the line: playoff teams. | ||||
| Data: nflverse via nflreadr and nflseedR | ||||
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 2025 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 rest of the Tables cookbook goes deeper on single tools: styling headers, legends and captions, percentile bars and cut lines, faceted tables, tier lists, border bars, schedule matrices, rolling windows and saving and posting.
- Smaller helpers each have an example on their reference page:
gt_bold_rows(),gt_row_accent(),gt_group_stripes(),gt_highlight_na(),gt_color_results(),gt_fmt_rank(),gt_indicator_boxes(),gt_outliers(),gt_column_subheaders(),gt_marginalia(),gt_scale_note(),gt_set_font()andgt_wrap_labels();gt_sdv_cols_label()puts logos in the column labels.gt_theme_preview()shows every theme that needs only the table on your own data.
Related SportsDataverse packages
- sdvplot - the Python plotting package
- sdvplot-js - the JavaScript / TypeScript plotting packages
- cfbfastR - college football
- hoopR - men’s basketball
- wehoop - women’s basketball
- baseballr - baseball
- fastRhockey - hockey
- oddsapiR - betting odds
- sportyR - playing surfaces
- sportsdataverse-py - the Python package
- sportsdataverse-R - the R meta-package
































