Skip to contents
On this page

A schedule matrix puts one team per row and one week per column, and draws each opponent’s logo in the cell where they met. This article builds two:

  1. A conference’s season: every SEC football team’s 2025 schedule, each cell shaded by whether the game was at home, away or on a neutral field, with the byes marked.
  2. One team’s season: the 2025-26 NBA team with the best record, laid out as a calendar, each game shaded by whether it was won or lost.

The catch in both is the coloring. A cell’s fill is a decision made cell by cell, not column by column. gt_highlight_cells() fills individual cells by a condition and gt_legend_discrete() draws the matching key; gt_sdv_logos() turns the opponents into logos. The SEC schedule comes from cfbfastR (CollegeFootballData.com, which needs a free API key) and the NBA games from hoopR’s ESPN team box scores.

gt_highlight_cells() and gt_legend_discrete() come from Andrew Weatherman’s gtUtils, and the SEC matrix follows his schedule-matrix example.

1. A conference’s season, one row per team

load_cfb_schedules() holds one row per game. nflreadr::clean_homeaway() doubles it into one row per team per game, renaming the home_* / away_* pairs to team_* / opponent_* and adding a location of "home" or "away". We fold the schedule’s neutral_site flag into that column as a third value, then keep the SEC. Week 15 holds only the conference title games, so the matrix stops at week 14.

schedule <- cfbfastR::load_cfb_schedules(2025)

The output below uses a snapshot of that call taken on November 09, 2026, the one the college football tutorial reads, because CollegeFootballData.com caps a key’s calls each month and the site build cannot rely on one; data-raw/article_fixtures.R takes it with the same code.

sec <- schedule |>
  filter(season_type == "regular", week <= 14) |>
  select(week, home_team, away_team, home_conference, away_conference, neutral_site) |>
  nflreadr::clean_homeaway() |>
  filter(team_conference == "SEC") |>
  mutate(location = if_else(neutral_site %in% TRUE, "neutral", location)) |>
  select(team, week, location, opponent) |>
  arrange(team, week)

The cells will hold the opponents’ school names, not image URLs. gt_sdv_logos() resolves a name like "Texas A&M" or "Central Arkansas" to the school’s logo when the table renders, FBS and FCS alike.

opponents <- sec |>
  pivot_wider(id_cols = team, names_from = week, values_from = opponent, names_sort = TRUE)

wk <- setdiff(names(opponents), "team")

names_sort = TRUE sorts the week columns. Because week is numeric they come out 1 through 14 in order rather than 1, 10, 11, ....

2. Carrying the location as a parallel mask

pivot_wider() on the opponent loses the location. Pivot it into its own frame of the same shape and turn each state into a logical mask.

locations <- sec |>
  pivot_wider(id_cols = team, names_from = week, values_from = location, names_sort = TRUE)
locations <- locations[match(opponents$team, locations$team), wk]

home_mask <- locations == "home"
away_mask <- locations == "away"
neutral_mask <- locations == "neutral"

The match() line holds locations to the row order of opponents, so the two frames line up cell for cell. Each mask is a TRUE/FALSE grid the same shape as the week columns, which is what gt_highlight_cells() takes. A bye is a missing opponent, so it needs no mask: its cells are NA in every mask, which gt_highlight_cells() reads as no match.

3. Filling cells by condition

gt_highlight_cells() fills the cells in a block of columns that meet a condition: either a formula tested against each column, like ~ is.na(.x), or a logical matrix the same shape as the columns, which is what the masks are. It is the per-cell counterpart to gtExtras::gt_highlight_cols() and gt_highlight_rows(), which fill whole columns or rows. Anything in ... goes to gt::cell_text() for the filled cells, which is how the byes get small grey type.

# ... |>
gt_highlight_cells(all_of(wk), condition = home_mask, fill = "#FBE7C6") |>
  gt_highlight_cells(all_of(wk), condition = away_mask, fill = "#FFFFFF") |>
  gt_highlight_cells(all_of(wk), condition = neutral_mask, fill = "#CFE0C3") |>
  gt_highlight_cells(
    all_of(wk),
    condition = ~ is.na(.x), fill = "#E4E4E4",
    text_color = "#565C64", size = px(9), weight = 600
  )

4. Logos for the opponents and the team

gt_sdv_logos() takes a whole block of columns, so one call draws every opponent; each logo’s alt text is the school’s full name. On the team column, include_name = TRUE keeps the name beside its logo. The byes are filled in with sub_missing(); a cell that names no team passes through gt_sdv_logos() as text.

# ... |>
sub_missing(columns = all_of(wk), missing_text = "BYE") |>
  gt_sdv_logos(columns = all_of(wk), league = "cfb", height = 24) |>
  gt_sdv_logos(columns = team, league = "cfb", height = 22, include_name = TRUE)

5. The legend

gt_legend_discrete() draws the key. Pass it a named vector of label = color and it renders the swatches, using the same fills as the cells so the key matches the table. label_placement = "inside" prints each label on its swatch, with the text color chosen per swatch for contrast. The heading_style and label_style lists take the same keys as every other style list in the package (see Styling Headers, Legends and Captions).

The SEC matrix

location_key <- c(
  "Home game" = "#FBE7C6", "Away game" = "#FFFFFF",
  "Neutral site" = "#CFE0C3", "Bye week" = "#E4E4E4"
)

opponents |>
  gt(id = "sec-schedule") |>
  gt_theme_scoreboard() |>
  gt_highlight_cells(all_of(wk), condition = home_mask, fill = "#FBE7C6") |>
  gt_highlight_cells(all_of(wk), condition = away_mask, fill = "#FFFFFF") |>
  gt_highlight_cells(all_of(wk), condition = neutral_mask, fill = "#CFE0C3") |>
  gt_highlight_cells(
    all_of(wk),
    condition = ~ is.na(.x), fill = "#E4E4E4",
    text_color = "#565C64", size = px(9), weight = 600
  ) |>
  sub_missing(columns = all_of(wk), missing_text = "BYE") |>
  gt_sdv_logos(columns = all_of(wk), league = "cfb", height = 24) |>
  gt_sdv_logos(columns = team, league = "cfb", height = 22, include_name = TRUE) |>
  cols_align(columns = all_of(wk), "center") |>
  cols_width(team ~ px(136), all_of(wk) ~ px(36)) |>
  cols_label(team = "") |>
  gt_border_grid(weight = 2) |>
  tab_options(table_body.hlines.width = px(2), data_row.padding = px(3)) |>
  tab_style(locations = cells_column_labels(), cell_text(size = px(13))) |>
  tab_style(locations = cells_body(columns = team), cell_text(size = px(13), weight = 600)) |>
  gt_legend_discrete(
    location_key,
    location = "top", label_placement = "inside", shape = "square",
    gap = 8, swatch_size = 20, border_width = 1.5, border_color = "black",
    heading = "2025 SEC football schedules",
    heading_style = list(
      font = "Oswald", size = 32, weight = 600,
      transform = "uppercase", margin_bottom = -2
    ),
    label_style = list(font = "Oswald", size = 13, margin_bottom = 6)
  ) |>
  gt_538_caption(
    "Regular season, weeks 1-14. The SEC title game (week 15) is left out.",
    "Data: CollegeFootballData.com via cfbfastR"
  )
2025 SEC football schedules
Home gameAway gameNeutral siteBye week
1 11 21 31 41 51 61 71 81 91 101 111 121 131 141
Alabama Florida State Seminoles UL Monroe Warhawks Wisconsin Badgers BYE Georgia Bulldogs Vanderbilt Commodores Missouri Tigers Tennessee Volunteers South Carolina Gamecocks BYE LSU Tigers Oklahoma Sooners Eastern Illinois Panthers Auburn Tigers
Arkansas Alabama A&M Bulldogs Arkansas State Red Wolves Ole Miss Rebels Memphis Tigers Notre Dame Fighting Irish BYE Tennessee Volunteers Texas A&M Aggies Auburn Tigers Mississippi State Bulldogs BYE LSU Tigers Texas Longhorns Missouri Tigers
Auburn Baylor Bears Ball State Cardinals South Alabama Jaguars Oklahoma Sooners Texas A&M Aggies BYE Georgia Bulldogs Missouri Tigers Arkansas Razorbacks Kentucky Wildcats Vanderbilt Commodores BYE Mercer Bears Alabama Crimson Tide
Florida Long Island University Sharks South Florida Bulls LSU Tigers Miami Hurricanes BYE Texas Longhorns Texas A&M Aggies Mississippi State Bulldogs BYE Georgia Bulldogs Kentucky Wildcats Ole Miss Rebels Tennessee Volunteers Florida State Seminoles
Georgia Marshall Thundering Herd Austin Peay Governors Tennessee Volunteers BYE Alabama Crimson Tide Kentucky Wildcats Auburn Tigers Ole Miss Rebels BYE Florida Gators Mississippi State Bulldogs Texas Longhorns Charlotte 49ers Georgia Tech Yellow Jackets
Kentucky Toledo Rockets Ole Miss Rebels Eastern Michigan Eagles BYE South Carolina Gamecocks Georgia Bulldogs BYE Texas Longhorns Tennessee Volunteers Auburn Tigers Florida Gators Tennessee Tech Golden Eagles Vanderbilt Commodores Louisville Cardinals
LSU Clemson Tigers Louisiana Tech Bulldogs Florida Gators SE Louisiana Lions Ole Miss Rebels BYE South Carolina Gamecocks Vanderbilt Commodores Texas A&M Aggies BYE Alabama Crimson Tide Arkansas Razorbacks Western Kentucky Hilltoppers Oklahoma Sooners
Mississippi State Southern Miss Golden Eagles Arizona State Sun Devils Alcorn State Braves Northern Illinois Huskies Tennessee Volunteers Texas A&M Aggies BYE Florida Gators Texas Longhorns Arkansas Razorbacks Georgia Bulldogs Missouri Tigers BYE Ole Miss Rebels
Missouri Central Arkansas Bears Kansas Jayhawks Louisiana Ragin' Cajuns South Carolina Gamecocks Massachusetts Minutemen BYE Alabama Crimson Tide Auburn Tigers Vanderbilt Commodores BYE Texas A&M Aggies Mississippi State Bulldogs Oklahoma Sooners Arkansas Razorbacks
Oklahoma Illinois State Redbirds Michigan Wolverines Temple Owls Auburn Tigers BYE Kent State Golden Flashes Texas Longhorns South Carolina Gamecocks Ole Miss Rebels Tennessee Volunteers BYE Alabama Crimson Tide Missouri Tigers LSU Tigers
Ole Miss Georgia State Panthers Kentucky Wildcats Arkansas Razorbacks Tulane Green Wave LSU Tigers BYE Washington State Cougars Georgia Bulldogs Oklahoma Sooners South Carolina Gamecocks The Citadel Bulldogs Florida Gators BYE Mississippi State Bulldogs
South Carolina Virginia Tech Hokies South Carolina State Bulldogs Vanderbilt Commodores Missouri Tigers Kentucky Wildcats BYE LSU Tigers Oklahoma Sooners Alabama Crimson Tide Ole Miss Rebels BYE Texas A&M Aggies Coastal Carolina Chanticleers Clemson Tigers
Tennessee Syracuse Orange East Tennessee State Buccaneers Georgia Bulldogs UAB Blazers Mississippi State Bulldogs BYE Arkansas Razorbacks Alabama Crimson Tide Kentucky Wildcats Oklahoma Sooners BYE New Mexico State Aggies Florida Gators Vanderbilt Commodores
Texas Ohio State Buckeyes San José State Spartans UTEP Miners Sam Houston Bearkats BYE Florida Gators Oklahoma Sooners Kentucky Wildcats Mississippi State Bulldogs Vanderbilt Commodores BYE Georgia Bulldogs Arkansas Razorbacks Texas A&M Aggies
Texas A&M UTSA Roadrunners Utah State Aggies Notre Dame Fighting Irish BYE Auburn Tigers Mississippi State Bulldogs Florida Gators Arkansas Razorbacks LSU Tigers BYE Missouri Tigers South Carolina Gamecocks Samford Bulldogs Texas Longhorns
Vanderbilt Charleston Southern Buccaneers Virginia Tech Hokies South Carolina Gamecocks Georgia State Panthers Utah State Aggies Alabama Crimson Tide BYE LSU Tigers Missouri Tigers Texas Longhorns Auburn Tigers BYE Kentucky Wildcats Tennessee Volunteers
1 Regular season, weeks 1-14. The SEC title game (week 15) is left out.
Data: CollegeFootballData.com via cfbfastR

gt_border_grid() draws the black rules between every cell, which is what makes the fills read as a grid of games rather than a striped list.

6. One team’s season as a calendar

The second matrix turns a single season into a calendar: one row per week, one column per weekday, the opponent’s logo on each game day. We find the team with the most regular-season wins (alphabetical order breaks a tie, so a rebuild picks the same team), then keep its games.

# the NBA Cup final is filed as a regular-season game but counts in no record
cup_final <- hoopR::load_nba_schedule(2026) |>
  filter(type_abbreviation == "CC") |>
  pull(id)

nba <- hoopR::load_nba_team_box(2026) |>
  # regular season; the box scores also hold the All-Star games
  filter(season_type == 2, team_abbreviation %in% sdv_teams("nba")$abbr, !game_id %in% cup_final)

best <- nba |>
  summarise(wins = sum(team_winner), losses = sum(!team_winner), .by = team_abbreviation) |>
  arrange(desc(wins), team_abbreviation) |>
  slice(1)

days <- c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")

games <- nba |>
  filter(team_abbreviation == best$team_abbreviation) |>
  mutate(
    weekday = as.integer(format(game_date, "%u")),
    day = days[weekday],
    week_of = game_date - (weekday - 1)
  ) |>
  select(week_of, day, opponent = opponent_team_abbreviation, won = team_winner)

format(game_date, "%u") gives the weekday as a number, Monday 1 to Sunday 7, whatever language the computer runs in, and subtracting it walks each game back to the Monday of its week. Two pivots from the same frame give the logos and the results, and complete() adds any week without a game (an All-Star break can leave one), so the weeks run down the calendar without a gap.

all_weeks <- seq(min(games$week_of), max(games$week_of), by = 7)

calendar <- games |>
  pivot_wider(id_cols = week_of, names_from = day, values_from = opponent) |>
  complete(week_of = all_weeks)
results <- games |>
  pivot_wider(id_cols = week_of, names_from = day, values_from = won) |>
  complete(week_of = all_weeks)

# every weekday as a column, in calendar order
calendar <- calendar[c("week_of", days)]
results <- results[days]

calendar <- calendar |>
  mutate(week = paste(month.abb[as.integer(format(week_of, "%m"))], as.integer(format(week_of, "%d")))) |>
  select(week, all_of(days))

win_mask <- results == TRUE
loss_mask <- results == FALSE

month.abb names the week in English whatever the system locale is, so the labels match on every machine that builds this page.

gt_theme_sdv_team() dresses the calendar in the team’s own colors: the title block in its primary color and the line under the labels in its secondary. The fills sit under the logos, so a glance down the grid shows the streaks.

team_name <- sdv_teams("nba")$name[sdv_teams("nba")$abbr == best$team_abbreviation]

calendar |>
  gt(id = "team-calendar") |>
  gt_highlight_cells(all_of(days), condition = win_mask, fill = "#CDEBD3") |>
  gt_highlight_cells(all_of(days), condition = loss_mask, fill = "#F6CFCB") |>
  sub_missing(columns = all_of(days), missing_text = "") |>
  gt_sdv_logos(columns = all_of(days), league = "nba", height = 26) |>
  cols_label(week = "Week of") |>
  cols_align(columns = all_of(days), "center") |>
  cols_width(week ~ px(78), all_of(days) ~ px(52)) |>
  tab_header(
    title = paste0(team_name, ", ", best$wins, "-", best$losses),
    subtitle = "The 2025-26 regular season, game by game"
  ) |>
  gt_theme_sdv_team(team = best$team_abbreviation, league = "nba", density = "compact") |>
  gt_border_grid(color = "#FFFFFF", weight = 2) |>
  gt_legend_discrete(
    c("Win" = "#CDEBD3", "Loss" = "#F6CFCB"),
    location = "bottom", label_placement = "inside", shape = "rounded",
    swatch_size = 18, gap = 6
  ) |>
  tab_source_note("Data: ESPN box scores via hoopR")
Oklahoma City Thunder, 64-18
The 2025-26 regular season, game by game
Week of Mon Tue Wed Thu Fri Sat Sun
Oct 20
Houston Rockets
Indiana Pacers
Atlanta Hawks
Oct 27 Dallas Mavericks Sacramento Kings
Washington Wizards

New Orleans Pelicans
Nov 3
LA Clippers Portland Trail Blazers
Sacramento Kings
Memphis Grizzlies
Nov 10
Golden State Warriors Los Angeles Lakers

Charlotte Hornets
Nov 17 New Orleans Pelicans
Sacramento Kings
Utah Jazz
Portland Trail Blazers
Nov 24

Minnesota Timberwolves
Phoenix Suns
Portland Trail Blazers
Dec 1
Golden State Warriors

Dallas Mavericks
Utah Jazz
Dec 8

Phoenix Suns

San Antonio Spurs
Dec 15


LA Clippers Minnesota Timberwolves

Dec 22 Memphis Grizzlies San Antonio Spurs
San Antonio Spurs

Philadelphia 76ers
Dec 29 Atlanta Hawks
Portland Trail Blazers
Golden State Warriors
Phoenix Suns
Jan 5 Charlotte Hornets
Utah Jazz
Memphis Grizzlies
Miami Heat
Jan 12
San Antonio Spurs
Houston Rockets
Miami Heat
Jan 19 Cleveland Cavaliers
Milwaukee Bucks
Indiana Pacers
Toronto Raptors
Jan 26
New Orleans Pelicans
Minnesota Timberwolves

Denver Nuggets
Feb 2
Orlando Magic San Antonio Spurs

Houston Rockets
Feb 9 Los Angeles Lakers
Phoenix Suns Milwaukee Bucks


Feb 16



Brooklyn Nets
Cleveland Cavaliers
Feb 23
Toronto Raptors Detroit Pistons
Denver Nuggets
Dallas Mavericks
Mar 2
Chicago Bulls New York Knicks

Golden State Warriors
Mar 9 Denver Nuggets

Boston Celtics

Minnesota Timberwolves
Mar 16
Orlando Magic Brooklyn Nets

Washington Wizards
Mar 23 Philadelphia 76ers
Boston Celtics
Chicago Bulls
New York Knicks
Mar 30 Detroit Pistons

Los Angeles Lakers

Utah Jazz
Apr 6
Los Angeles Lakers LA Clippers
Denver Nuggets
Phoenix Suns
WinLoss
Data: ESPN box scores via hoopR

gt_legend_discrete(location = "bottom") sets the key under the table, where it does not compete with the team-colored title block. A white gt_border_grid() separates the fills without drawing a hard rule through the logos.

Citation

To cite the sdvplotR R package in publications, use:

BibTeX Citation

@misc{gilani_carl_lee_kay_weatherman_sdvplotR,
  author = {Gilani, Saiem and Carl, Sebastian and Lee, Jared and Kay, Camden and Weatherman, Andrew},
  title = {sdvplotR: The SportsDataverse's R Package for Multi-League Sports Plotting.},
  url = {https://sdvplotR.sportsdataverse.org},
  year = {2026}
}