Overview
sdvplotR provides a family of helpers for embedding team
logos, wordmarks, player headshots, and team-colored bars in reactable
tables. These complement the existing gt helpers
(gt_sdv_logos() et al.) and are designed for interactive /
web-first dashboards.
All functions follow the same sport / variant / size
contract as their ggplot2 and gt
counterparts:
| Argument | Purpose |
|---|---|
sport |
One of supported_sports()
|
variant |
"primary", "dark", "light",
"alt", "classic", "helmet"
(NFL) |
height |
Image height in pixels |
team_col, value_col
|
Column names for color / bar resolution |
The image helpers (reactable_sdv_logos(),
reactable_sdv_headshots(), …) return HTML, so their columns
need html = TRUE.
Basic: Team logos in a standings table
library(reactable)
library(sdvplotR)
library(dplyr)
# Regular-season records from nflreadr's schedules, one row per team per game,
# for the last completed regular season (NFL seasons are named for the year they
# start and end in early January)
season <- as.integer(format(Sys.Date(), "%Y")) - 1 -
(format(Sys.Date(), "%m-%d") < "01-15")
standings <- nflreadr::load_schedules(season) |>
filter(game_type == "REG", !is.na(result)) |>
nflreadr::clean_homeaway() |>
group_by(team_abbr = team) |>
summarise(
wins = sum(team_score > opponent_score),
losses = sum(team_score < opponent_score),
pct = mean(team_score > opponent_score) + 0.5 * mean(team_score == opponent_score),
.groups = "drop"
) |>
arrange(desc(pct)) |>
slice_head(n = 16)
reactable(
standings,
columns = list(
team_abbr = colDef(
name = "Team",
cell = reactable_sdv_logos(sport = "nfl", height = 28),
html = TRUE
),
pct = colDef(
format = colFormat(digits = 3),
style = reactable_sdv_team_color_bar(
standings,
team_col = "team_abbr",
sport = "nfl",
max_value = 1
)
)
)
)Dark mode variants
reactable tables often sit inside dark-themed
dashboards. Use variant = "dark" for high-contrast logos on
dark backgrounds:
reactable(
standings,
theme = reactableTheme(
color = "#e0e0e0",
backgroundColor = "#1a1a1a",
borderColor = "#333"
),
columns = list(
team_abbr = colDef(
cell = reactable_sdv_logos(sport = "nfl", variant = "dark", height = 28),
html = TRUE
)
)
)Logo column headers
Replace column headers (e.g., per-team stat columns) with the team’s logo:
# A wide table with one column per team (example values)
wide_df <- data.frame(
week = 1:4,
KC = c(0.21, 0.05, 0.14, 0.30), BUF = c(0.12, 0.25, 0.08, 0.18),
SF = c(0.09, 0.16, 0.22, 0.04), DAL = c(-0.03, 0.11, 0.02, 0.07),
PHI = c(0.15, 0.19, 0.12, 0.23), MIA = c(0.01, -0.06, 0.10, 0.03)
)
reactable(
wide_df,
columns = reactable_sdv_cols_label(
wide_df,
sport = "nfl",
variant = "primary",
height = 26
)
)Team-colored rows
Use reactable_sdv_team_color_bg() to tint entire rows
with the team’s primary color at low opacity — useful for scanning
standings quickly:
reactable(
standings,
columns = list(
team_abbr = colDef(
cell = reactable_sdv_logos(sport = "nfl", height = 28),
html = TRUE
),
wins = colDef(style = reactable_sdv_team_color_bg(standings, "team_abbr", sport = "nfl")),
losses = colDef(style = reactable_sdv_team_color_bg(standings, "team_abbr", sport = "nfl")),
pct = colDef(style = reactable_sdv_team_color_bg(standings, "team_abbr", sport = "nfl"))
)
)Player headshots
Headshots use the same reactable_sdv_headshots() helper
across all sports. NFL headshots take GSIS IDs, the gsis_id
in nflreadr’s rosters:
roster <- nflreadr::load_rosters(seasons = season) |>
filter(team %in% c("KC", "BUF", "SF"), position == "QB", !is.na(gsis_id)) |>
select(full_name, team, position, gsis_id)
reactable(
roster,
columns = list(
full_name = colDef(name = "Player"),
team = colDef(cell = reactable_sdv_logos(sport = "nfl", height = 24), html = TRUE),
gsis_id = colDef(
name = "Headshot",
cell = reactable_sdv_headshots(sport = "nfl", height = 40),
html = TRUE
)
)
)Cross-sport examples
All helpers work identically across sports — only the
sport argument changes.
NBA standings (hoopR)
# the last completed regular season (named for the year it ends, in mid-April)
nba_season <- as.integer(format(Sys.Date(), "%Y")) -
(format(Sys.Date(), "%m-%d") < "04-20")
nba_standings <- hoopR::load_nba_standings(seasons = nba_season) |>
filter(stat_name %in% c("wins", "losses", "winPercent")) |>
select(team = team_abbreviation, conference = group_abbreviation, stat_name, value) |>
tidyr::pivot_wider(names_from = stat_name, values_from = value) |>
arrange(desc(winPercent)) |>
slice_head(n = 10)
reactable(
nba_standings,
columns = list(
team = colDef(cell = reactable_sdv_logos(sport = "nba", height = 28), html = TRUE),
winPercent = colDef(name = "Pct", format = colFormat(digits = 3))
)
)CFB power ratings (cfbfastR)
ESPN’s Football Power Index, which needs no API key:
# the last completed college football regular season (it ends in mid-December)
cfb_season <- as.integer(format(Sys.Date(), "%Y")) -
(format(Sys.Date(), "%m-%d") < "12-15")
cfb_fpi <- cfbfastR::espn_ratings_fpi(year = cfb_season) |>
# FPI arrives as text; sort it as a number
mutate(fpi = as.numeric(fpi)) |>
arrange(desc(fpi)) |>
select(team = team_abbreviation, fpi, w, l) |>
slice_head(n = 25)
reactable(
cfb_fpi,
columns = list(
team = colDef(cell = reactable_sdv_logos(sport = "cfb", height = 28), html = TRUE),
fpi = colDef(name = "FPI", format = colFormat(digits = 1))
)
)NHL skater stats (fastRhockey)
# the last completed regular season (named for the year it ends, in mid-April)
nhl_season <- as.integer(format(Sys.Date(), "%Y")) -
(format(Sys.Date(), "%m-%d") < "04-20")
nhl_skaters <- fastRhockey::nhl_stats_skaters(
season = paste0(nhl_season - 1, nhl_season),
limit = 20
) |>
# a player traded mid-season lists every team; keep his last
mutate(team = sub(".*,\\s*", "", team_abbrevs)) |>
select(skater_full_name, team, player_id, goals, assists, points)
reactable(
nhl_skaters,
columns = list(
team = colDef(cell = reactable_sdv_logos(sport = "nhl", height = 28), html = TRUE),
player_id = colDef(
name = "Headshot",
# NHL API player ids: the NHL's own image CDN
cell = reactable_sdv_headshots(sport = "nhl", id_type = "league", height = 40),
html = TRUE
)
)
)Integration with reactablefmtr
These helpers compose cleanly with reactablefmtr,
whose embed_img() draws a column that already holds image
URLs:
library(reactablefmtr)
standings |>
left_join(select(team_reference("nfl"), team_abbr, logo = logo_url), by = "team_abbr") |>
select(logo, team_abbr, wins, losses, pct) |>
reactable(
columns = list(
logo = colDef(name = "", cell = embed_img(height = 28))
)
)team_reference() holds every team’s logo URLs. For
sdvplotR’s variant resolution (dark/light/alt/classic) and its aliases
and historical team mappings inside the table itself, prefer the
reactable_sdv_* helpers.
Best practices
- Cache reactable outputs in Shiny apps — image-heavy tables can be slow to render on first load.
-
Use
variant = "dark"when the table sits on a dark background. -
Pair logos with team-colored bars
(
reactable_sdv_team_color_bar) for scannable leaderboards. -
Set
heightconsistently across a single table to keep rows aligned. -
Pre-validate teams with
clean_team_abbrs()before passing to reactable — it avoids silent fallback images.
