Skip to contents
On this page

Updated 2026-10-12: the 2026-27 season, 4-7 games in per team. Early-season tables move a lot from week to week.

This page is rebuilt every week with the site. It builds the standings, charts goal differential with logos and ranks the points leaders with their headshots, for the latest NHL season with games: the season to date from October to April, the final regular season once it is over. Data: fastRhockey’s box-score releases and the NHL’s api-web.nhle.com standings.

Picking the season

NHL seasons are named by the year they end (2025-26 is 2026) and start in the fall, so until September the calendar points at the season that ended in June. For a season with no file yet fastRhockey::load_nhl_team_box() warns and returns no rows, and a new season’s file holds only preseason games at first, so the helper treats “no regular-season games” as “not published” and the page steps back one season. A game id’s fifth and sixth digits are its type: 02 is the regular season, 03 the playoffs. During the season the standings come from api-web.nhle.com as of today; once the regular season is over, as of its last day. That call goes through live(): if the API does not answer, the page shows a note in place of the standings and the goal-differential chart for that week instead of failing the site build.

library(sdvplotR)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
library(gt)

today <- Sys.Date()
day_label <- function(day, month = "%b") sub(" 0", " ", format(as.Date(day), paste(month, "%d"))) # "Oct 5"
# fastRhockey names a season by the year it ends (2025-26 is 2026); it starts in
# the fall, so until September the calendar points at the season that ended in June
current <- as.integer(format(today, "%Y")) + (format(today, "%m") >= "09")
label <- function(season) sprintf("%d-%02d", season - 1, season %% 100)
# an NHL game id's fifth and sixth digits are its type: 01 preseason, 02 regular season, 03 playoffs
game_type <- function(game_id) (as.numeric(game_id) %/% 10000) %% 100

# A season's team box scores, or NULL when it has no regular-season games yet:
# load_nhl_team_box() warns and returns no rows for a season it has no file for,
# and a new season's file holds only preseason games at first
team_games <- function(season) {
  box <- tryCatch(suppressWarnings(fastRhockey::load_nhl_team_box(seasons = season)), error = function(e) NULL)
  if (NROW(box) == 0 || !any(game_type(box$game_id) == 2)) NULL else box
}

season <- current
box <- team_games(season)
if (is.null(box)) {
  season <- current - 1
  box <- team_games(season)
}
stopifnot(!is.null(box))

box <- mutate(box, game_date = as.Date(game_date), type = game_type(game_id))
last_regular <- max(box$game_date[box$type == 2])
playoffs <- filter(box, type == 3)
in_season <- season == current && nrow(playoffs) == 0
# games per team from the release file (they differ early on)
games_in <- paste(unique(range(table(box$team_abbrev[box$type == 2]))), collapse = "-")

# A live API call, or NULL when the API does not answer: one bad week then shows
# a note in place of a table instead of failing the whole site build
live <- function(expr) tryCatch(expr, error = function(e) NULL)
unavailable <- function(what, ok) {
  if (ok) "" else sprintf("*%s were unavailable when this page was built on %s; they return next week.*", what, today)
}
# api-web.nhle.com standings: as of today in season, as of the last regular-season day after it
standings <- live(fastRhockey::nhl_standings(date = if (in_season) NULL else format(last_regular)))
standings_ok <- NROW(standings) > 0
if (in_season) {
  status <- sprintf(
    "the %s season, %s games in per team. Early-season tables move a lot from week to week.",
    label(season), games_in
  )
  through <- sprintf("through %s (%s games in)", day_label(last_regular), games_in)
} else if (season < current) {
  status <- sprintf(
    "the offseason, so this is the final %s regular season; the %s season has no regular-season games yet.",
    label(season), label(current)
  )
  through <- "final regular season"
} else if (as.numeric(today - max(playoffs$game_date)) <= 10) {
  status <- sprintf("the final %s regular season; the playoffs are under way.", label(season))
  through <- "final regular season"
} else {
  status <- sprintf("the offseason, so this is the final %s regular season.", label(season))
  through <- "final regular season"
}

1. Standings

Grouped by division in the NHL’s own order. gt_sdv_logos() reads the NHL’s three-letter team codes (LAK, NJD, TBL) through sdvplotR’s aliases, so nothing is mapped by hand.

standings |>
  arrange(division_name, division_sequence) |>
  transmute(
    division = division_name,
    logo = team_abbr,
    team = team_common_name,
    gp = games_played, w = wins, l = losses, otl = ot_losses,
    pts = points, pts_pct = point_pctg,
    gf = goals_for, ga = goals_against, diff = goal_differential,
    l10 = paste(l10_wins, l10_losses, l10_ot_losses, sep = "-"),
    streak = paste0(streak_code, streak_count)
  ) |>
  gt(groupname_col = "division", id = "nhl-standings") |>
  tab_header(
    title = paste("NHL standings,", label(season)),
    subtitle = paste0(toupper(substr(through, 1, 1)), substring(through, 2))
  ) |>
  fmt_number(pts_pct, decimals = 3) |>
  fmt_number(diff, decimals = 0, force_sign = TRUE) |>
  cols_label(
    logo = "", team = "Team", gp = "GP", w = "W", l = "L", otl = "OTL", pts = "PTS",
    pts_pct = "PTS%", gf = "GF", ga = "GA", diff = "DIFF", l10 = "Last 10", streak = "Streak"
  ) |>
  tab_source_note("Data: api-web.nhle.com via fastRhockey | Viz: sdvplotR") |>
  gt_sdv_logos(columns = logo, league = "nhl", height = 24) |>
  gt_theme_swiss()
NHL standings, 2026-27
Through Oct 11 (4-7 games in)
Team GP W L OTL PTS PTS% GF GA DIFF Last 10 Streak
Atlantic
Tampa Bay Lightning Lightning 5 4 1 0 8 0.800 16 12 +4 4-1-0 W4
Ottawa Senators Senators 5 4 1 0 8 0.800 15 11 +4 4-1-0 W2
Boston Bruins Bruins 6 4 2 0 8 0.667 20 13 +7 4-2-0 W2
Toronto Maple Leafs Maple Leafs 6 3 2 1 7 0.583 20 18 +2 3-2-1 W1
Buffalo Sabres Sabres 6 3 3 0 6 0.500 16 22 −6 3-3-0 W1
Florida Panthers Panthers 6 2 2 2 6 0.500 12 17 −5 2-2-2 L2
Montreal Canadiens Canadiens 5 2 2 1 5 0.500 15 19 −4 2-2-1 W1
Detroit Red Wings Red Wings 5 1 3 1 3 0.300 10 15 −5 1-3-1 OT1
Central
Utah Mammoth Mammoth 6 4 2 0 8 0.667 22 15 +7 4-2-0 W1
Minnesota Wild Wild 5 3 1 1 7 0.700 15 9 +6 3-1-1 W1
Winnipeg Jets Jets 5 3 1 1 7 0.700 13 15 −2 3-1-1 L1
Colorado Avalanche Avalanche 5 3 2 0 6 0.600 26 17 +9 3-2-0 L1
Nashville Predators Predators 5 2 1 2 6 0.600 14 14 0 2-1-2 OT1
Dallas Stars Stars 5 2 2 1 5 0.500 11 8 +3 2-2-1 OT1
St. Louis Blues Blues 5 2 2 1 5 0.500 14 16 −2 2-2-1 W1
Chicago Blackhawks Blackhawks 6 1 5 0 2 0.167 12 26 −14 1-5-0 L2
Metropolitan
Carolina Hurricanes Hurricanes 7 5 1 1 11 0.786 27 17 +10 5-1-1 W5
New York Rangers Rangers 7 5 2 0 10 0.714 23 13 +10 5-2-0 W1
Pittsburgh Penguins Penguins 6 3 2 1 7 0.583 22 17 +5 3-2-1 W1
Washington Capitals Capitals 5 3 2 0 6 0.600 17 14 +3 3-2-0 L1
New Jersey Devils Devils 4 2 2 0 4 0.500 10 14 −4 2-2-0 W1
Columbus Blue Jackets Blue Jackets 4 2 2 0 4 0.500 13 14 −1 2-2-0 L1
New York Islanders Islanders 5 2 3 0 4 0.400 16 13 +3 2-3-0 L1
Philadelphia Flyers Flyers 7 0 5 2 2 0.143 8 28 −20 0-5-2 L4
Pacific
Vegas Golden Knights Golden Knights 6 5 1 0 10 0.833 23 14 +9 5-1-0 W4
Edmonton Oilers Oilers 5 4 0 1 9 0.900 25 17 +8 4-0-1 W4
Anaheim Ducks Ducks 5 4 1 0 8 0.800 18 13 +5 4-1-0 W2
Seattle Kraken Kraken 6 4 2 0 8 0.667 26 17 +9 4-2-0 W2
San Jose Sharks Sharks 5 3 2 0 6 0.600 13 17 −4 3-2-0 L1
Vancouver Canucks Canucks 7 2 5 0 4 0.286 24 35 −11 2-5-0 L4
Los Angeles Kings Kings 4 0 2 2 2 0.250 10 17 −7 0-2-2 OT1
Calgary Flames Flames 5 0 5 0 0 0.000 8 27 −19 0-5-0 L5
Data: api-web.nhle.com via fastRhockey | Viz: sdvplotR

2. Goal differential

Goals for minus goals against, all 32 teams, in team colors; scale_x_sdv() and theme_x_sdv() swap the team codes on the x axis for logos.

gd <- standings |>
  mutate(team = sdv_resolve(team_abbr, league = "nhl")) |>
  arrange(desc(goal_differential), team) # the team breaks a tie, so an unchanged week renders the same

ggplot(gd, aes(x = factor(team, levels = team), y = goal_differential, fill = team)) +
  geom_col(width = 0.75) +
  geom_hline(yintercept = 0, colour = "grey20", linewidth = 0.4) +
  scale_fill_sdv(league = "nhl", guide = "none") +
  scale_x_sdv(league = "nhl", size = 20) +
  labs(
    title = paste("NHL goal differential,", label(season), through),
    subtitle = "Goals for minus goals against",
    x = NULL, y = "Goal differential",
    caption = "Data: api-web.nhle.com via fastRhockey | Viz: sdvplotR"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.major.x = element_blank(),
    panel.grid.minor = element_blank()
  ) +
  theme_x_sdv()

Bar chart of every NHL team's goal differential, best to worst, each bar in the team's primary color with the team's logo under it on the horizontal axis.

3. Points leaders with headshots

Goals and assists from the regular-season skater box scores, stacked into points. The box scores carry NHL API player ids, which geom_sdv_headshots(id_system = "league") turns into the NHL’s own headshots; the game rosters give the full names.

skaters <- fastRhockey::load_nhl_player_box(seasons = season) |>
  filter(game_type(game_id) == 2)
full_names <- fastRhockey::load_nhl_game_rosters(seasons = season) |>
  distinct(player_id, full_name) |>
  filter(!duplicated(player_id))
stopifnot(identical(class(skaters$player_id), class(full_names$player_id)))

leaders <- skaters |>
  arrange(game_date, game_id) |>
  group_by(player_id) |>
  summarise(
    team = last(team_abbrev), gp = n(),
    goals = sum(goals), assists = sum(assists), points = sum(points),
    .groups = "drop"
  ) |>
  arrange(desc(points), desc(goals), player_id) |>
  slice_head(n = 10) |>
  left_join(full_names, by = "player_id") |>
  mutate(full_name = factor(full_name, levels = rev(full_name)))
top <- max(leaders$points)

bars <- tidyr::pivot_longer(leaders, c(goals, assists), names_to = "part", values_to = "n") |>
  mutate(part = factor(part, levels = c("assists", "goals"), labels = c("Assists", "Goals")))

ggplot(leaders, aes(y = full_name)) +
  geom_col(data = bars, aes(x = n, fill = part), width = 0.7) +
  geom_sdv_headshots(
    aes(x = -0.07 * top, player_id = player_id),
    league = "nhl", id_system = "league", height = 0.085
  ) +
  geom_sdv_logos(aes(x = points + 0.06 * top, team = team), league = "nhl", height = 0.06) +
  geom_text(aes(x = points + 0.12 * top, label = points), hjust = 0, fontface = "bold", size = 3.8) +
  scale_fill_manual(values = c(Goals = "#1f3b73", Assists = "#9fb4d8"), breaks = c("Goals", "Assists")) +
  scale_x_continuous(limits = c(-0.14 * top, 1.22 * top), expand = c(0, 0)) +
  labs(
    title = paste("NHL points leaders,", label(season)),
    # the box-score release can trail the live standings by a day or two
    subtitle = paste0(
      "Goals and assists, regular season",
      if (in_season) paste(", games through", day_label(max(skaters$game_date))) else ""
    ),
    x = "Points", y = NULL, fill = NULL,
    caption = "Data: fastRhockey box scores | Viz: sdvplotR"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.major.y = element_blank(),
    panel.grid.minor = element_blank(),
    legend.position = "top", legend.justification = "left"
  )

Stacked horizontal bar chart of the ten NHL points leaders, goals in dark blue and assists in light blue, with each player's headshot at the left of the bar, the team logo at its end and the points total beside it.

4. Interactive version

The goal differential bars from section 2 as a plotly chart: hover a bar for the team’s record and its goals for and against. plotly draws no logos on its own, so sdv_axis_logos() hides the team codes and puts a logo under each bar.

plotly::plot_ly(
  gd, x = ~factor(team, levels = team), y = ~goal_differential, type = "bar",
  marker = list(color = unname(sdv_team_colors("nhl", gd$team))),
  hoverinfo = "text", textposition = "none",
  text = ~sprintf(
    "%s, %d-%d-%d<br>%d goals for, %d against (%+d)",
    team_common_name, wins, losses, ot_losses, goals_for, goals_against, goal_differential
  )
) |>
  plotly::layout(
    title = list(text = paste("NHL goal differential,", label(season)), x = 0.02),
    xaxis = list(title = ""), yaxis = list(title = "Goal differential"),
    margin = list(b = 50, t = 50)
  ) |>
  sdv_axis_logos(gd$team, "nhl")

More recipes like this one, in plotly, ggiraph, leaflet and reactable, are in Interactive charts.

  • NHL walks through fastRhockey and sdvplotR more broadly.
  • MLB builds the same three views for baseball.
  • Social graphics, automated posts leaderboards like these on a schedule.

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