Skip to contents
On this page

Ten charts and tables from the 2025-26 NBA regular season: an offense-vs-defense quadrant and net-rating bars with logos, a conference bump chart, a scoring leaderboard with headshots, a shot chart on a team-colored court, tiers, a standings table, faceted logos, a home/road dumbbell and a table of category leaders, then the first chart again as an interactive one. Every number comes from hoopR’s ESPN release files on GitHub (load_nba_*()), so nothing here calls stats.nba.com.

Setup

library(sdvplotR)
library(ggplot2)
library(dplyr)
library(tidyr)
library(gt)

season <- 2026 # the 2025-26 season: hoopR names a season by the year it ends
label <- "2025-26"
source_note <- "Data: hoopR (ESPN) | Viz: sdvplotR"

theme_set(
  theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(face = "bold"),
      plot.title.position = "plot",
      plot.caption = element_text(color = "grey40", size = 8),
      plot.caption.position = "plot",
      panel.grid.minor = element_blank()
    )
)

The team box score has one row per team per game. season_type 2 is the regular season (3 is the playoffs, 5 the play-in). ESPN files the All-Star games as regular-season games too, between teams that aren’t NBA clubs, so an inner join on the ESPN team id to sdv_teams("nba") drops them and adds each team’s sdvplotR abbreviation and conference in the same step.

nba_teams <- sdv_teams("nba") |>
  transmute(team_id = as.integer(team_id), team = abbr, conference = sub(" Conference$", "", conference))

box <- hoopR::load_nba_team_box(seasons = season) |>
  filter(season_type == 2) |>
  inner_join(nba_teams, by = "team_id")

box |>
  distinct(game_id) |>
  nrow()
#> [1] 1231

That is one more than the 1,230 games of the schedule: the NBA Cup final between New York and San Antonio is filed as a regular-season game too. It stays in the box scores (it was played) but not in the standings, so those two teams show 83 games here.

1. Offense vs defense, with logos

Points scored and allowed per 100 possessions put all 30 teams on one chart. Possessions are estimated from the box score (FGA - OREB + TOV + 0.44 x FTA) and averaged with the opponent’s estimate for the same game. One Chicago game is missing its team turnovers, so total_turnovers falls back to the players’ turnovers there; without that, Chicago and its opponent that night, Orlando, would drop out of every chart below. The last line stops the build if a team ever goes missing.

games <- box |>
  mutate(
    poss = field_goals_attempted - offensive_rebounds +
      coalesce(total_turnovers, turnovers) + 0.44 * free_throws_attempted
  )
games <- games |>
  inner_join(
    games |> select(game_id, opponent_team_id = team_id, opp_poss = poss),
    by = c("game_id", "opponent_team_id")
  ) |>
  mutate(game_poss = (poss + opp_poss) / 2)

ratings <- games |>
  group_by(team, conference) |>
  summarise(
    games = n(),
    wins = sum(team_winner),
    pace = mean(game_poss),
    ortg = 100 * sum(team_score) / sum(game_poss),
    drtg = 100 * sum(opponent_team_score) / sum(game_poss),
    fg3a_rate = sum(three_point_field_goals_attempted) / sum(field_goals_attempted),
    fg3_pct = sum(three_point_field_goals_made) / sum(three_point_field_goals_attempted),
    .groups = "drop"
  ) |>
  mutate(net = ortg - drtg) |>
  arrange(desc(net), team)
stopifnot(nrow(ratings) == 30, !anyNA(ratings$net))

ratings |>
  select(team, conference, wins, ortg, drtg, net) |>
  head(5)
#> # A tibble: 5 × 6
#>   team  conference  wins  ortg  drtg   net
#>   <chr> <chr>      <int> <dbl> <dbl> <dbl>
#> 1 OKC   Western       64  116.  105. 10.9 
#> 2 DET   Eastern       60  114.  106.  7.93
#> 3 SA    Western       62  117.  109.  7.86
#> 4 BOS   Eastern       56  117.  109.  7.85
#> 5 NY    Eastern       54  117.  110.  6.40

Defensive rating is points allowed, so its axis is reversed: good defenses sit at the top and the best teams in the top-right corner.

x_lim <- range(ratings$ortg) + c(-1.5, 1.5)
y_lim <- range(ratings$drtg) + c(-2, 1.5)

ggplot(ratings, aes(ortg, drtg)) +
  geom_mean_lines(
    aes(x0 = ortg, y0 = drtg),
    color = "grey60", linetype = "dashed", linewidth = 0.4
  ) +
  geom_sdv_logos(aes(team = team), league = "nba", width = 0.065) +
  annotate(
    "text",
    x = x_lim[c(2, 1, 2, 1)], y = y_lim[c(1, 1, 2, 2)],
    hjust = c(1, 0, 1, 0), vjust = c(1.3, 1.3, -0.4, -0.4),
    label = c("Good offense, good defense", "Defense first", "Offense first", "Struggling"),
    color = "grey45", fontface = "italic", size = 3.5
  ) +
  scale_x_continuous(expand = expansion(0)) +
  scale_y_reverse(expand = expansion(0)) +
  expand_limits(x = x_lim, y = y_lim) +
  labs(
    title = paste("NBA offense vs defense,", label, "regular season"),
    x = "Offensive rating (points per 100 possessions)",
    y = "Defensive rating (points allowed per 100)",
    caption = source_note
  )

NBA teams of the 2025-26 regular season drawn as their logos, placed by offensive rating (horizontal) and defensive rating (vertical, reversed so better defenses are higher), with dashed league-average lines and a label in each corner.

2. Net rating, ranked, with logos on the axis

scale_x_sdv() turns a discrete axis of team abbreviations into logos, and theme_x_sdv() lets the axis draw them. Order the factor first: the bars keep that order and so do the logos.

ratings |>
  mutate(team = factor(team, levels = team)) |>
  ggplot(aes(team, net, fill = team)) +
  geom_col(width = 0.75) +
  geom_hline(yintercept = 0, linewidth = 0.4) +
  scale_fill_sdv(league = "nba") +
  scale_x_sdv(league = "nba", size = 16) +
  labs(
    title = paste("NBA net rating,", label, "regular season"),
    x = NULL,
    y = "Net rating (per 100 possessions)",
    caption = source_note
  ) +
  theme_x_sdv() +
  theme(legend.position = "none", panel.grid.major.x = element_blank())

Bar chart of NBA net rating per 100 possessions in 2025-26, best to worst, each bar in the team's color with the team's logo under it on the horizontal axis.

3. The race in the West, as a bump chart

Rank each team inside its conference by win percentage at the end of every week, then draw one line per team in its color with its logo at the finish. Ties are broken by point differential, then by abbreviation, not by the NBA’s tiebreakers, so the same data always draw the same chart.

weekly <- box |>
  mutate(
    week = as.Date(cut(game_date, "week", start.on.monday = TRUE)),
    margin = team_score - opponent_team_score
  ) |>
  group_by(team, conference, week) |>
  summarise(wins = sum(team_winner), games = n(), margin = sum(margin), .groups = "drop") |>
  complete(nesting(team, conference), week, fill = list(wins = 0, games = 0, margin = 0)) |>
  arrange(team, week) |>
  group_by(team) |>
  mutate(across(c(wins, games, margin), cumsum)) |>
  ungroup() |>
  mutate(week_no = dense_rank(week)) |>
  filter(week_no >= 3) |> # the first two weeks hold only a game or two
  arrange(conference, week_no, desc(wins / games), desc(margin), team) |>
  group_by(conference, week_no) |>
  mutate(rank = row_number()) |>
  ungroup()

west <- filter(weekly, conference == "Western")
finish <- filter(west, week_no == max(week_no))

ggplot(west, aes(week_no, rank)) +
  geom_line(aes(color = team), linewidth = 1.1, alpha = 0.85) +
  # the logos get no color aesthetic: a colour would tint the whole image
  geom_sdv_logos(aes(x = week_no + 1, team = team), data = finish, league = "nba", width = 0.04) +
  scale_color_sdv(league = "nba") +
  scale_y_reverse(breaks = 1:15) +
  scale_x_continuous(breaks = seq(5, 30, 5), expand = expansion(add = c(0.5, 1.5))) +
  labs(
    title = paste("The race in the West,", label),
    subtitle = "Conference rank by win percentage at the end of each week",
    x = "Week of the season",
    y = "Western Conference rank",
    caption = source_note
  ) +
  theme(legend.position = "none", panel.grid.major.x = element_blank())

Bump chart of the 15 Western Conference teams' weekly conference rank through the 2025-26 regular season, one line per team in its color, with each team's logo at its final rank on the right.

Swap "Western" for "Eastern" to draw the other conference from the same weekly frame.

4. A scoring leaderboard with headshots

The player box score carries ESPN athlete ids, which is what geom_sdv_headshots() takes for the NBA. Only games in box count, so the All-Star games stay out, and a player’s team is the one he played for last.

players <- hoopR::load_nba_player_box(seasons = season) |>
  semi_join(distinct(box, game_id), by = "game_id") |>
  filter(!did_not_play)

per_game <- players |>
  arrange(game_date) |>
  group_by(athlete_id, athlete_display_name) |>
  summarise(
    games = n(),
    team = last(team_abbreviation),
    across(
      c(points, rebounds, assists, steals, blocks, three_point_field_goals_made),
      mean
    ),
    .groups = "drop"
  ) |>
  filter(games >= 50)

scorers <- per_game |>
  arrange(desc(points), athlete_id) |>
  head(10) |>
  mutate(athlete_display_name = factor(athlete_display_name, levels = rev(athlete_display_name)))
ggplot(scorers, aes(points, athlete_display_name)) +
  geom_col(aes(fill = team), width = 0.7) +
  geom_sdv_logos(aes(x = points + 1.6, team = team), league = "nba", height = 0.07) +
  geom_sdv_headshots(aes(x = points + 4.6, player_id = athlete_id), league = "nba", height = 0.09) +
  geom_text(aes(x = points + 7.4, label = sprintf("%.1f", points)), fontface = "bold", hjust = 0) +
  scale_fill_sdv(league = "nba") +
  scale_x_continuous(limits = c(0, max(scorers$points) + 10), expand = expansion(0)) +
  labs(
    title = paste("NBA scoring leaders,", label),
    subtitle = "Points per game, 50 or more games",
    x = "Points per game",
    y = NULL,
    caption = source_note
  ) +
  theme(legend.position = "none", panel.grid.major.y = element_blank())

Horizontal bar chart of the ten NBA scoring leaders of 2025-26 (50 or more games), each bar in the player's team color, followed by the team logo, the player's headshot and his points per game.

These are ESPN athlete ids. stats.nba.com numbers players differently (PERSON_ID), and the two can’t be told apart by shape: an NBA Stats id read as an ESPN id draws someone else or nothing. For data keyed by NBA Stats ids, pass id_system = "league" to draw from the NBA’s own image CDN:

geom_sdv_headshots(aes(player_id = person_id), league = "nba", id_system = "league")

That CDN refuses requests from datacenter IPs, this site’s build server among them, so the articles here use ESPN ids. id_system works the same in gt_sdv_headshots(), reactable_sdv_headshots() and the headshot axis scales.

5. A shot chart on a team-colored court

load_nba_shots() holds ESPN’s shot locations. hoopR already converts them to feet on a center-court frame (coordinate_x from -47 to 47 along the court, coordinate_y from -25 to 25 across it), which is the frame sdv_surface()’s sportyR court uses, so they plot as they are. sdv_court_coords() is not needed here: it is for the stats.nba.com legacy frame (x_legacy/y_legacy from hoopR::load_nba_stats_shots(), tenths of a foot around the hoop). Shots at the right basket are rotated onto the left one so a half court holds them all; free throws are left out.

star <- scorers |>
  slice_max(points, n = 1, with_ties = FALSE)

shots <- hoopR::load_nba_shots(seasons = season) |>
  semi_join(distinct(box, game_id), by = "game_id") |>
  filter(athlete_id_1 == star$athlete_id, !grepl("Free Throw", type_text)) |>
  mutate(
    right = coordinate_x > 0,
    x = if_else(right, -coordinate_x, coordinate_x),
    y = if_else(right, -coordinate_y, coordinate_y),
    result = if_else(scoring_play, "Made", "Missed")
  )

made_color <- sdv_team_colors(star$team, league = "nba", which = "secondary")

sdv_surface("nba", star$team, display_range = "defense") +
  geom_point(
    aes(x, y),
    data = filter(shots, result == "Missed"),
    shape = 4, color = "grey25", size = 1.4, alpha = 0.6
  ) +
  geom_point(
    aes(x, y),
    data = filter(shots, result == "Made"),
    shape = 21, fill = made_color, color = "black", stroke = 0.3, size = 1.8
  ) +
  labs(
    title = paste0(star$athlete_display_name, ": every field goal attempt, ", label),
    subtitle = sprintf(
      "%d made (filled), %d missed (crosses): %.1f%% from the field",
      sum(shots$scoring_play), sum(!shots$scoring_play), 100 * mean(shots$scoring_play)
    ),
    caption = source_note
  ) +
  theme(plot.title = element_text(face = "bold"))

Half-court shot chart of the 2025-26 NBA scoring leader's field goal attempts on a court painted in his team's colors: made shots as filled circles, missed shots as grey crosses.

6. Team tiers from net rating

sdv_team_tiers() draws a tier list from a frame with tier_no and team. Here the tiers are cut from the net ratings above, best team first in each tier, so the list is the season’s numbers rather than an opinion.

tiers <- ratings |>
  mutate(
    # bins from worst to best, so the best bin (6 or better) becomes tier 1
    tier_no = 6L - cut(net, c(-Inf, -6, -2, 2, 6, Inf), labels = FALSE, right = FALSE)
  ) |>
  arrange(tier_no, desc(net)) |>
  select(tier_no, team)

sdv_team_tiers(
  tiers,
  league = "nba",
  title = paste("NBA tiers by net rating,", label),
  subtitle = "Regular-season net rating per 100 possessions, cut at +6, +2, -2 and -6",
  caption = source_note,
  tier_desc = c(
    "1" = "Contenders",
    "2" = "Playoff teams",
    "3" = "The middle",
    "4" = "Lottery",
    "5" = "Rebuilding"
  )
)

NBA team logos in five tiers cut from 2025-26 net rating, from Contenders (plus 6 or better) at the top to Rebuilding (minus 6 or worse) at the bottom.

7. A conference standings table with logos and cut lines

ESPN’s standings come long, one row per team and stat, so pivot them wide. gt_sdv_logos() turns the abbreviation column into logos and gt_cutline() marks the playoff and play-in lines: seeds 1-6 make the playoffs, 7-10 the play-in.

standings <- hoopR::load_nba_standings(seasons = season) |>
  select(group_name, team_abbreviation, team_short_display_name, stat_name, display_value) |>
  pivot_wider(names_from = stat_name, values_from = display_value)

standings |>
  filter(group_name == "Eastern Conference") |>
  mutate(seed = as.integer(playoffSeed), logo = team_abbreviation) |>
  arrange(seed) |>
  select(
    seed, logo, team_short_display_name, wins, losses, winPercent, gamesBehind,
    Home, Road, `Last Ten Games`, differential
  ) |>
  gt() |>
  tab_header(
    title = paste("Eastern Conference standings,", label),
    subtitle = "Seeds 1-6 make the playoffs; 7-10 the play-in"
  ) |>
  cols_label(
    seed = "", logo = "", team_short_display_name = "Team", wins = "W", losses = "L",
    winPercent = "Pct", gamesBehind = "GB", `Last Ten Games` = "L10",
    differential = "Diff"
  ) |>
  tab_source_note(source_note) |>
  gt_sdv_logos(columns = logo, league = "nba", height = 26) |>
  gt_theme_athletic() |>
  cols_align("left", columns = team_short_display_name) |>
  gt_cutline(after = c(6, 10), label = c("Playoffs", "Play-in"), label_position = "above")
Eastern Conference standings, 2025-26
Seeds 1-6 make the playoffs; 7-10 the play-in
Team W L Pct GB Home Road L10 Diff
1 Detroit Pistons Pistons 60 22 .732 - 31-9 28-13 8-2 +8.2
2 Boston Celtics Celtics 56 26 .683 4 30-11 26-15 8-2 +7.7
3 New York Knicks Knicks 53 29 .646 7 30-10 22-19 6-4 +6.4
4 Cleveland Cavaliers Cavaliers 52 30 .634 8 27-14 25-16 7-3 +4.1
5 Toronto Raptors Raptors 46 36 .561 14 24-17 22-19 6-4 +2.8
6 Atlanta Hawks Hawks 46 36 .561 14 24-17 22-19 6-4 +2.5
7 Philadelphia 76ers 76ers 45 37 .549 15 23-18 22-19 6-4 -0.2
8 Orlando Magic Magic 45 37 .549 15 25-15 19-20 7-3 +0.6
9 Charlotte Hornets Hornets 44 38 .537 16 21-20 23-18 6-4 +4.8
10 Miami Heat Heat 43 39 .524 17 26-15 17-24 5-5 +2.4
11 Milwaukee Bucks Bucks 32 50 .390 28 19-22 13-28 3-7 -6.2
12 Chicago Bulls Bulls 31 51 .378 29 18-23 13-28 2-8 -5.2
13 Brooklyn Nets Nets 20 62 .244 40 12-29 8-33 3-7 -10.0
14 Indiana Pacers Pacers 19 63 .232 41 11-30 8-33 3-7 -8.0
15 Washington Wizards Wizards 17 65 .207 43 11-30 6-35 0-10 -12.0
Data: hoopR (ESPN) | Viz: sdvplotR

8. Logos faceted by conference

geom_sdv_logos() is a ggplot2 layer, so it facets like any other geom, and geom_mean_lines() draws each panel’s own averages. How often teams shoot threes against how well they make them, East vs West:

ggplot(ratings, aes(fg3a_rate, fg3_pct)) +
  geom_mean_lines(aes(x0 = fg3a_rate, y0 = fg3_pct), color = "grey60", linewidth = 0.4) +
  geom_sdv_logos(aes(team = team), league = "nba", width = 0.09) +
  facet_wrap(~conference, labeller = labeller(conference = function(x) paste(x, "Conference"))) +
  scale_x_continuous(labels = scales::percent, expand = expansion(mult = 0.12)) +
  scale_y_continuous(labels = scales::percent_format(accuracy = 0.1), expand = expansion(mult = 0.12)) +
  labs(
    title = paste("Three-point volume vs accuracy,", label),
    x = "Share of field goal attempts from three",
    y = "Three-point percentage",
    caption = source_note
  ) +
  theme(strip.text = element_text(face = "bold", size = 11))

Two panels, Eastern and Western Conference, placing each NBA team's logo by its share of field goal attempts from three (horizontal) and its three-point percentage (vertical) in 2025-26, with each panel's average lines.

9. Home and road, as a dumbbell with logos on the axis

Home and road records come from the same standings, as strings like "29-12". One row per team, sorted by the gap between them; scale_y_sdv() and theme_y_sdv() put the logos on the vertical axis.

win_pct <- function(record) {
  parts <- strsplit(record, "-", fixed = TRUE)
  vapply(parts, function(p) as.numeric(p[1]) / sum(as.numeric(p)), numeric(1))
}

home_road <- standings |>
  transmute(team = team_abbreviation, home = win_pct(Home), road = win_pct(Road)) |>
  mutate(edge = home - road) |>
  arrange(edge, team) |>
  mutate(team = factor(team, levels = team))

ggplot(home_road, aes(y = team)) +
  geom_segment(aes(x = road, xend = home, yend = team), color = "grey70", linewidth = 1.2) +
  geom_point(aes(x = road), color = "#E15759", size = 3) +
  geom_point(aes(x = home), color = "#4E79A7", size = 3) +
  scale_y_sdv(league = "nba", size = 14) +
  scale_x_continuous(labels = scales::percent, limits = c(0, 1)) +
  labs(
    title = paste("Home and road win percentage,", label),
    subtitle = "<span style='color:#4E79A7'>**Home**</span> and <span style='color:#E15759'>**road**</span>, sorted by the home edge (largest at the top)",
    x = "Win percentage",
    y = NULL,
    caption = source_note
  ) +
  theme_y_sdv() +
  theme(plot.subtitle = ggtext::element_markdown(), panel.grid.major.y = element_blank())

Dumbbell chart of the 30 NBA teams' 2025-26 home and road win percentages, one row per team with its logo on the vertical axis, sorted by the size of the home edge.

10. Category leaders in a headshot table

gt_sdv_headshots() turns a column of ESPN athlete ids into headshots and gt_sdv_logos() a column of abbreviations into logos. One row per category: the per-game leader among players with 50 or more games.

categories <- c(
  points = "Points", rebounds = "Rebounds", assists = "Assists",
  steals = "Steals", blocks = "Blocks", three_point_field_goals_made = "3-pointers made"
)

per_game |>
  pivot_longer(names(categories), names_to = "stat", values_to = "value") |>
  arrange(desc(value), athlete_id) |>
  group_by(stat) |>
  slice(1) |>
  ungroup() |>
  arrange(match(stat, names(categories))) |>
  transmute(
    category = categories[stat],
    headshot = athlete_id,
    player = athlete_display_name,
    logo = team,
    value,
    games
  ) |>
  gt() |>
  tab_header(
    title = paste("NBA per-game leaders,", label),
    subtitle = "Regular season, 50 or more games"
  ) |>
  cols_label(category = "", headshot = "", player = "Player", logo = "Team", value = "Per game", games = "GP") |>
  fmt_number(value, decimals = 1) |>
  tab_source_note(source_note) |>
  gt_sdv_headshots(columns = headshot, league = "nba", height = 44) |>
  gt_sdv_logos(columns = logo, league = "nba", height = 28) |>
  gt_theme_sdv()
NBA per-game leaders, 2025-26
Regular season, 50 or more games
Player Per game GP
Points Player 3945274 headshot Luka Doncic Los Angeles Lakers 33.5 64
Rebounds Player 3112335 headshot Nikola Jokic Denver Nuggets 12.9 65
Assists Player 3112335 headshot Nikola Jokic Denver Nuggets 10.7 65
Steals Player 4684742 headshot Ausar Thompson Detroit Pistons 2.0 73
Blocks Player 5104157 headshot Victor Wembanyama San Antonio Spurs 3.1 65
3-pointers made Player 3945274 headshot Luka Doncic Los Angeles Lakers 4.0 64
Data: hoopR (ESPN) | Viz: sdvplotR

11. Make it interactive

The offense-vs-defense quadrant from section 1 again, built with native plotly. A transparent marker under each team carries the hover text, and add_sdv_logos() places the logos as layout images at the same coordinates. Hover a logo for the team’s record, both ratings and its net rating, the numbers that section 1 can only place and section 2 ranks.

nba_index <- sdv_teams("nba")
ratings_hover <- ratings |>
  mutate(
    name = nba_index$name[match(sdv_resolve(team, league = "nba"), nba_index$team_id)],
    tip = sprintf(
      "<b>%s</b> (%s)<br>%d-%d<br>Offensive rating %.1f, defensive %.1f<br>Net %+.1f per 100 possessions",
      name, conference, wins, games - wins, ortg, drtg, net
    )
  )

avg_line <- list(color = "grey", dash = "dash", width = 1)

plotly::plot_ly(
  ratings_hover, x = ~ortg, y = ~drtg, type = "scatter", mode = "markers",
  marker = list(size = 30, opacity = 0), text = ~tip, hoverinfo = "text"
) |>
  plotly::layout(
    title = list(text = paste("NBA offense vs defense,", label, "regular season"), x = 0.02),
    xaxis = list(title = "Offensive rating (points per 100 possessions)", range = x_lim, zeroline = FALSE),
    yaxis = list(title = "Defensive rating (points allowed per 100)", range = rev(y_lim), zeroline = FALSE),
    shapes = list(
      list(type = "line", x0 = mean(ratings$ortg), x1 = mean(ratings$ortg), yref = "paper", y0 = 0, y1 = 1, line = avg_line),
      list(type = "line", y0 = mean(ratings$drtg), y1 = mean(ratings$drtg), xref = "paper", x0 = 0, x1 = 1, line = avg_line)
    )
  ) |>
  add_sdv_logos(ratings_hover$ortg, ratings_hover$drtg, ratings_hover$team, "nba", size = 0.08)

x_lim and y_lim are section 1’s axis limits; rev() reverses the defense axis. The record counts every regular-season game in the box scores, so New York and San Antonio include the NBA Cup final. plotly loads each logo from its URL, so the page stays light. Interactive charts covers plotly, ggiraph and leaflet, and the Interactive NBA Net Rating recipe builds this quadrant and section 2’s net rating bars in plotly, step by step.

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}
}