Skip to contents

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 through clean_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 / 17

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", 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
The BAL logo 1 13 4 .765
The BUF logo 2 11 6 .647
The KC logo 3 11 6 .647
The HOU logo 4 10 7 .588
The CLE logo 5 11 6 .647
The MIA logo 6 11 6 .647
The PIT logo 7 10 7 .588
The CIN logo
9 8 .529
The IND logo
9 8 .529
The JAX logo
9 8 .529
The DEN logo
8 9 .471
The LV logo
8 9 .471
The NYJ logo
7 10 .412
The TEN logo
6 11 .353
The LAC logo
5 12 .294
The NE logo
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
The BAL logo 1 13 4 .765
The BUF logo 2 11 6 .647
The KC logo 3 11 6 .647
The HOU logo 4 10 7 .588
The CLE logo 5 11 6 .647
The MIA logo 6 11 6 .647
The PIT logo 7 10 7 .588
The CIN logo
9 8 .529
The IND logo
9 8 .529
The JAX logo
9 8 .529
The DEN logo
8 9 .471
The LV logo
8 9 .471
The NYJ logo
7 10 .412
The TEN logo
6 11 .353
The LAC logo
5 12 .294
The NE logo
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
The BAL logo 1 13 4 .765
The BUF logo 2 11 6 .647
The KC logo 3 11 6 .647
The HOU logo 4 10 7 .588
The CLE logo 5 11 6 .647
The MIA logo 6 11 6 .647
The PIT logo 7 10 7 .588
The CIN logo
9 8 .529
The IND logo
9 8 .529
The JAX logo
9 8 .529
The DEN logo
8 9 .471
The LV logo
8 9 .471
The NYJ logo
7 10 .412
The TEN logo
6 11 .353
The LAC logo
5 12 .294
The NE logo
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)
)
The 2023 NFL playoff field
Final regular-season standings
AFC playoff picture
Final 2023 standings
1 Seed1 W1 L1 Win pct1
The BAL logo 1 13 4 .765
The BUF logo 2 11 6 .647
The KC logo 3 11 6 .647
The HOU logo 4 10 7 .588
The CLE logo 5 11 6 .647
The MIA logo 6 11 6 .647
The PIT logo 7 10 7 .588
The CIN logo
9 8 .529
The IND logo
9 8 .529
The JAX logo
9 8 .529
The DEN logo
8 9 .471
The LV logo
8 9 .471
The NYJ logo
7 10 .412
The TEN logo
6 11 .353
The LAC logo
5 12 .294
The NE logo
4 13 .235
1 Above the line: playoff teams.
Data: nflverse
NFC playoff picture
Final 2023 standings
1 Seed1 W1 L1 Win pct1
The SF logo 1 12 5 .706
The DAL logo 2 12 5 .706
The DET logo 3 12 5 .706
The TB logo 4 9 8 .529
The PHI logo 5 11 6 .647
The LA logo 6 10 7 .588
The GB logo 7 9 8 .529
The NO logo
9 8 .529
The SEA logo
9 8 .529
The ATL logo
7 10 .412
The CHI logo
7 10 .412
The MIN logo
7 10 .412
The NYG logo
6 11 .353
The ARI logo
4 13 .235
The WAS logo
4 13 .235
The CAR logo
2 15 .118
1 Above the line: playoff teams.
Data: nflverse
Seeds 1-4 won their division; 5-7 are wild cards.

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