Skip to contents

Introduction

This vignette demonstrates how to create rich NBA visualizations by combining hoopR for player and team data with sdvplotR for team logos, headshots, colors, and gt tables.

Setup

library(sdvplotR)
library(ggplot2)
library(hoopR)
library(dplyr)
library(gt)

# Get valid NBA team abbreviations
nba_teams <- valid_team_names("nba")
head(nba_teams)

Loading NBA Data

Use hoopR to load this season’s ESPN team and player box scores:

# The last completed regular season (hoopR names a season for the year it
# ends; the regular season ends in mid-April)
season <- as.integer(format(Sys.Date(), "%Y")) -
  (format(Sys.Date(), "%m-%d") < "04-20")

# One row per team per game (ESPN box scores), regular season only. ESPN tags
# the All-Star games as regular season too; keeping the league's own teams
# drops them
league_teams <- team_reference("nba")$team_abbr
team_stats <- hoopR::load_nba_team_box(seasons = season) |>
  filter(season_type == 2, team_abbreviation %in% league_teams)

# One row per player per game, with ESPN athlete IDs; players who did not
# play are dropped so games played counts real games
player_stats <- hoopR::load_nba_player_box(seasons = season) |>
  filter(season_type == 2, !did_not_play, team_abbreviation %in% league_teams)

NBA Team Performance

Visualize team performance with team logos:

# Calculate team metrics
team_perf <- team_stats |>
  filter(!is.na(team_abbreviation)) |>
  group_by(team_abbreviation) |>
  summarise(
    avg_points = mean(team_score, na.rm = TRUE),
    avg_rebounds = mean(total_rebounds, na.rm = TRUE),
    games = n(),
    .groups = "drop"
  ) |>
  filter(games >= 10)

ggplot(team_perf, aes(x = avg_points, y = avg_rebounds)) +
  geom_sdv_logos(
    aes(team = team_abbreviation),
    sport = "nba",
    width = 0.075
  ) +
  labs(
    title = "NBA Team Performance",
    subtitle = paste("Season", season),
    x = "Average Points per Game",
    y = "Average Rebounds per Game",
    caption = "Data: hoopR | Viz: sdvplotR"
  ) +
  theme_minimal()

NBA Team Colors

Use team colors to visualize win percentages:

# Calculate win percentage
team_wins <- team_stats |>
  filter(!is.na(team_abbreviation)) |>
  group_by(team_abbreviation) |>
  summarise(
    wins = sum(team_winner, na.rm = TRUE),
    games = n(),
    .groups = "drop"
  ) |>
  filter(games >= 10) |>
  mutate(win_pct = wins / games) |>
  arrange(desc(win_pct)) |>
  head(16)

ggplot(team_wins, aes(x = reorder(team_abbreviation, win_pct), y = win_pct)) +
  geom_col(aes(fill = team_abbreviation), width = 0.7) +
  scale_fill_sdv(sport = "nba", alpha = 0.8) +
  scale_y_continuous(labels = scales::percent) +
  labs(
    title = "Top 16 NBA Teams by Win Percentage",
    x = NULL,
    y = "Win Percentage"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "none"
  )

Player Headshots

Visualize top performers with player headshots:

# Top scorers
top_scorers <- player_stats |>
  filter(!is.na(athlete_id)) |>
  group_by(athlete_id, athlete_display_name) |>
  summarise(
    avg_points = mean(points, na.rm = TRUE),
    games = n(),
    .groups = "drop"
  ) |>
  filter(games >= 20) |>
  arrange(desc(avg_points)) |>
  head(8)

ggplot(top_scorers, aes(x = games, y = avg_points)) +
  geom_sdv_headshots(
    aes(player_id = athlete_id),
    sport = "nba",
    height = 0.15
  ) +
  geom_label(
    aes(label = athlete_display_name),
    nudge_y = -1.5,
    size = 3,
    alpha = 0.7
  ) +
  labs(
    title = "Top 8 NBA Scorers",
    subtitle = paste("Season", season),
    x = "Games Played",
    y = "Average Points per Game"
  ) +
  theme_minimal()

NBA Stats player IDs

The headshots above use ESPN athlete IDs, the athlete_id in hoopR’s load_nba_*() data. hoopR’s nba_*() functions read stats.nba.com, which numbers players differently (PLAYER_ID). Pass id_type = "league" to draw those from the NBA’s own image CDN. Both are plain numbers, so without it an NBA Stats ID is read as an ESPN ID and draws someone else or nothing: Dirk Nowitzki’s NBA Stats ID, 1717, is ESPN’s Jared Jeffries.

leaders <- hoopR::nba_leagueleaders(season = hoopR::year_to_season(season - 1), stat_category = "PTS")$LeagueLeaders |>
  mutate(across(c(GP, PTS), as.numeric)) |>
  head(8)

ggplot(leaders, aes(x = GP, y = PTS)) +
  geom_sdv_headshots(
    aes(player_id = PLAYER_ID),
    sport = "nba",
    id_type = "league",
    height = 0.15
  ) +
  labs(title = "NBA scoring leaders", x = "Games Played", y = "Points")

id_type works the same in gt_sdv_headshots(), reactable_sdv_headshots(), the headshot axis scales and element_sdv_headshot(). The NBA’s image CDN refuses requests from datacenter IPs, so a plot rendered on CI or a server can come back without these headshots; tables are unaffected, because the reader’s browser loads the images.

NBA Team Tiers

Create a tier plot ranking NBA teams:

# Sample tier assignments
tier_data <- data.frame(
  tier_no = c(1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5),
  team = c("BOS", "DEN", "MIL", "PHX", "LAL", "GSW", "MIA",
           "DAL", "PHI", "NYK", "CLE",
           "SAC", "MIN", "MEM", "BKN",
           "ATL")
)

sdv_team_tiers(
  tier_data,
  sport = "nba",
  title = "NBA Power Rankings",
  subtitle = paste("As of", Sys.Date()),
  tier_desc = c(
    "1" = "Championship Contenders",
    "2" = "Playoff Favorites",
    "3" = "Solid Playoffs",
    "4" = "Bubble Teams",
    "5" = "Rebuilding"
  )
)

NBA Conference Map

Show each conference’s teams in a column, using the conferences sdvplotR keeps for every team:

conference_map <- team_reference("nba") |>
  arrange(conference, team_name) |>
  group_by(conference) |>
  mutate(team_rank = row_number()) |>
  ungroup() |>
  mutate(conference_num = as.numeric(factor(conference)))

ggplot(conference_map, aes(x = conference_num, y = team_rank)) +
  geom_sdv_logos(
    aes(team = team_abbr),
    sport = "nba",
    width = 0.08
  ) +
  scale_x_continuous(
    breaks = seq_along(levels(factor(conference_map$conference))),
    labels = levels(factor(conference_map$conference)),
    expand = expansion(add = 0.6)
  ) +
  scale_y_reverse() +
  labs(
    title = "NBA Teams by Conference",
    x = NULL,
    y = NULL
  ) +
  theme_minimal() +
  theme(
    axis.text.y = element_blank(),
    panel.grid = element_blank()
  )

NBA Standings Table with Logos

Create a gt table with team logos:

standings_table <- team_wins |>
  head(10) |>
  mutate(
    logo = team_abbreviation,
    rank = row_number()
  ) |>
  select(rank, logo, team_abbreviation, wins, games, win_pct)

standings_table |>
  gt() |>
  gt_sdv_logos(columns = "logo", sport = "nba", height = 35) |>
  fmt_number(columns = "win_pct", decimals = 3) |>
  cols_label(
    rank = "#",
    logo = "Team",
    team_abbreviation = "Abbrev",
    wins = "Wins",
    games = "Games",
    win_pct = "Win %"
  ) |>
  tab_header(
    title = "NBA Top 10",
    subtitle = paste("Season", season)
  )

Player Performance Comparison

Compare the scoring leaders with the rebounding leaders:

per_game <- player_stats |>
  group_by(athlete_id, athlete_display_name) |>
  summarise(
    points = mean(points, na.rm = TRUE),
    rebounds = mean(rebounds, na.rm = TRUE),
    games = n(),
    .groups = "drop"
  ) |>
  filter(games >= 20)

comparison <- bind_rows(
  per_game |>
    slice_max(points, n = 5, with_ties = FALSE) |>
    transmute(athlete_id, category = "Points per Game", value = points),
  per_game |>
    slice_max(rebounds, n = 5, with_ties = FALSE) |>
    transmute(athlete_id, category = "Rebounds per Game", value = rebounds)
) |>
  group_by(category) |>
  mutate(rank = row_number()) |>
  ungroup()

ggplot(comparison, aes(x = rank, y = value)) +
  geom_sdv_headshots(
    aes(player_id = athlete_id),
    sport = "nba",
    height = 0.2
  ) +
  facet_wrap(~category, scales = "free_y") +
  scale_x_continuous(breaks = 1:5) +
  labs(
    title = "NBA Top Performers",
    subtitle = paste("Season", season),
    x = "Rank",
    y = NULL
  ) +
  theme_minimal()

Axis Labels with Logos

Replace axis labels with team logos:

top_8 <- team_wins |>
  head(8) |>
  mutate(team_abbreviation = factor(team_abbreviation, levels = team_abbreviation))

ggplot(top_8, aes(x = team_abbreviation, y = win_pct)) +
  geom_col(aes(fill = team_abbreviation), width = 0.6) +
  scale_fill_sdv(sport = "nba", alpha = 0.7) +
  scale_x_sdv(sport = "nba") +
  theme_minimal() +
  theme_x_sdv() +
  labs(
    title = "Top 8 NBA Teams by Win %",
    x = NULL,
    y = "Win Percentage"
  ) +
  theme(legend.position = "none")

Next Steps