
Leaderboard Dashboards with sdvplotR, gt, and Quarto
Saiem Gilani
Source: vignettes/leaderboard-dashboards.Rmd
leaderboard-dashboards.RmdOn this page
Introduction
This article demonstrates how to create interactive leaderboard dashboards using sdvplotR, gt tables, and Quarto. These patterns are perfect for tracking standings, player stats, and team performance in real-time.
Basic Leaderboard with Logos
Create a simple leaderboard table with team logos:
# Sample standings data
standings <- data.frame(
team = c("KC", "BUF", "SF", "PHI", "DAL", "MIA", "CIN", "BAL"),
wins = c(9, 8, 8, 7, 7, 6, 6, 6),
losses = c(2, 3, 3, 4, 4, 5, 5, 5),
ties = c(0, 0, 0, 0, 0, 0, 0, 0),
pct = c(0.818, 0.727, 0.727, 0.636, 0.636, 0.545, 0.545, 0.545)
) |>
mutate(
logo = team,
rank = row_number()
) |>
select(rank, logo, team, wins, losses, ties, pct)
standings |>
gt() |>
gt_sdv_logos(columns = "logo", league = "nfl", height = 35) |>
fmt_number(columns = "pct", decimals = 3) |>
cols_label(
rank = "#",
logo = "Team",
team = "Abbrev",
wins = "W",
losses = "L",
ties = "T",
pct = "Win %"
) |>
tab_header(
title = "NFL Standings",
subtitle = "Example data"
) |>
tab_footnote(
footnote = "Example data | Viz: sdvplotR",
locations = cells_title(groups = "title")
)| NFL Standings1 | ||||||
| Example data | ||||||
| # | Team | Abbrev | W | L | T | Win % |
|---|---|---|---|---|---|---|
| 1 | ![]() |
KC | 9 | 2 | 0 | 0.818 |
| 2 | ![]() |
BUF | 8 | 3 | 0 | 0.727 |
| 3 | ![]() |
SF | 8 | 3 | 0 | 0.727 |
| 4 | ![]() |
PHI | 7 | 4 | 0 | 0.636 |
| 5 | ![]() |
DAL | 7 | 4 | 0 | 0.636 |
| 6 | ![]() |
MIA | 6 | 5 | 0 | 0.545 |
| 7 | ![]() |
CIN | 6 | 5 | 0 | 0.545 |
| 8 | ![]() |
BAL | 6 | 5 | 0 | 0.545 |
| 1 Example data | Viz: sdvplotR | ||||||
Advanced Leaderboard with Team Colors
Create a leaderboard with team-colored rows:
# Add team colors to standings
standings_colored <- standings |>
mutate(
primary_color = sdv_team_colors("nfl", team, which = "primary"),
secondary_color = sdv_team_colors("nfl", team, which = "secondary")
)
standings_colored |>
gt() |>
gt_sdv_logos(columns = "logo", league = "nfl", height = 35) |>
fmt_number(columns = "pct", decimals = 3) |>
cols_label(
rank = "#",
logo = "Team",
team = "Abbrev",
wins = "W",
losses = "L",
ties = "T",
pct = "Win %"
) |>
tab_header(
title = "NFL Standings",
subtitle = "Example data"
) |>
data_color(
columns = "pct",
palette = c("lightblue", "darkblue"),
# pick black or white text by WCAG contrast (gt's default, APCA, put white text on
# mid-blue cells at 3.1:1)
contrast_algo = "wcag"
)| NFL Standings | ||||||||
| Example data | ||||||||
| # | Team | Abbrev | W | L | T | Win % | primary_color | secondary_color |
|---|---|---|---|---|---|---|---|---|
| 1 | ![]() |
KC | 9 | 2 | 0 | 0.818 | #e31837 | #ffb612 |
| 2 | ![]() |
BUF | 8 | 3 | 0 | 0.727 | #00338d | #c60c30 |
| 3 | ![]() |
SF | 8 | 3 | 0 | 0.727 | #aa0000 | #b3995d |
| 4 | ![]() |
PHI | 7 | 4 | 0 | 0.636 | #004c54 | #a5acaf |
| 5 | ![]() |
DAL | 7 | 4 | 0 | 0.636 | #002244 | #b0b7bc |
| 6 | ![]() |
MIA | 6 | 5 | 0 | 0.545 | #008e97 | #f58220 |
| 7 | ![]() |
CIN | 6 | 5 | 0 | 0.545 | #fb4f14 | #000000 |
| 8 | ![]() |
BAL | 6 | 5 | 0 | 0.545 | #241773 | #9e7c0c |
Multi-Sport Leaderboard
Create leaderboards for multiple sports in one table:
# Sample multi-sport data
multi_sport_standings <- data.frame(
league = c(rep("NFL", 4), rep("NBA", 4), rep("MLB", 4)),
team = c("KC", "BUF", "SF", "PHI",
"BOS", "DEN", "MIL", "PHX",
"LAD", "ATL", "HOU", "BAL"),
wins = c(9, 8, 8, 7, 25, 23, 22, 20, 95, 90, 88, 85),
losses = c(2, 3, 3, 4, 8, 10, 11, 13, 67, 72, 74, 77)
) |>
mutate(
pct = wins / (wins + losses),
rank = row_number()
) |>
select(league, rank, team, wins, losses, pct)
multi_sport_standings |>
gt() |>
gt_sdv_logos(columns = "team", league = "nfl", height = 30, locations = cells_body(columns = team, rows = league == "NFL")) |>
gt_sdv_logos(columns = "team", league = "nba", height = 30, locations = cells_body(columns = team, rows = league == "NBA")) |>
gt_sdv_logos(columns = "team", league = "mlb", height = 30, locations = cells_body(columns = team, rows = league == "MLB")) |>
fmt_number(columns = "pct", decimals = 3) |>
cols_label(
league = "Sport",
rank = "#",
team = "Team",
wins = "W",
losses = "L",
pct = "Win %"
) |>
tab_header(
title = "Multi-Sport Leaderboard",
subtitle = "Top Teams Across Sports"
)| Multi-Sport Leaderboard | |||||
| Top Teams Across Sports | |||||
| Sport | # | Team | W | L | Win % |
|---|---|---|---|---|---|
| NFL | 1 | ![]() |
9 | 2 | 0.818 |
| NFL | 2 | ![]() |
8 | 3 | 0.727 |
| NFL | 3 | ![]() |
8 | 3 | 0.727 |
| NFL | 4 | ![]() |
7 | 4 | 0.636 |
| NBA | 5 | ![]() |
25 | 8 | 0.758 |
| NBA | 6 | ![]() |
23 | 10 | 0.697 |
| NBA | 7 | ![]() |
22 | 11 | 0.667 |
| NBA | 8 | ![]() |
20 | 13 | 0.606 |
| MLB | 9 | ![]() |
95 | 67 | 0.586 |
| MLB | 10 | ![]() |
90 | 72 | 0.556 |
| MLB | 11 | ![]() |
88 | 74 | 0.543 |
| MLB | 12 | ![]() |
85 | 77 | 0.525 |
Player Leaderboard with Headshots
Create a player stats table with headshots:
# Sample player data
player_stats <- data.frame(
player_id = c("00-0033873", "00-0026498", "00-0035228", "00-0033869"),
player_name = c("P. Mahomes", "M. Stafford", "K. Murray", "J. Allen"),
team = c("KC", "LAR", "ARI", "BUF"),
pass_yds = c(4200, 3800, 3500, 4100),
pass_td = c(32, 28, 25, 30),
pass_int = c(8, 12, 10, 9)
) |>
mutate(
rank = row_number()
) |>
select(rank, player_id, player_name, team, pass_yds, pass_td, pass_int)
player_stats |>
gt() |>
gt_sdv_headshots(columns = "player_id", league = "nfl", id_system = "gsis", height = 40) |>
fmt_number(columns = c("pass_yds", "pass_td", "pass_int")) |>
cols_label(
rank = "#",
player_id = "Player",
player_name = "Name",
team = "Team",
pass_yds = "Yards",
pass_td = "TD",
pass_int = "INT"
) |>
tab_header(
title = "NFL Quarterback Leaderboard",
subtitle = "Top Passers by Yards"
)| NFL Quarterback Leaderboard | ||||||
| Top Passers by Yards | ||||||
| # | Player | Name | Team | Yards | TD | INT |
|---|---|---|---|---|---|---|
| 1 | ![]() |
P. Mahomes | KC | 4,200.00 | 32.00 | 8.00 |
| 2 | ![]() |
M. Stafford | LAR | 3,800.00 | 28.00 | 12.00 |
| 3 | ![]() |
K. Murray | ARI | 3,500.00 | 25.00 | 10.00 |
| 4 | ![]() |
J. Allen | BUF | 4,100.00 | 30.00 | 9.00 |
Quartile-Based Leaderboard
Create a leaderboard with quartile indicators:
# Add quartile information
standings_quartile <- standings |>
mutate(
quartile = case_when(
pct >= 0.75 ~ "Q1 (Elite)",
pct >= 0.50 ~ "Q2 (Good)",
pct >= 0.25 ~ "Q3 (Average)",
TRUE ~ "Q4 (Struggling)"
)
)
standings_quartile |>
gt() |>
gt_sdv_logos(columns = "logo", league = "nfl", height = 35) |>
fmt_number(columns = "pct", decimals = 3) |>
cols_label(
rank = "#",
logo = "Team",
team = "Abbrev",
wins = "W",
losses = "L",
ties = "T",
pct = "Win %",
quartile = "Quartile"
) |>
tab_header(
title = "NFL Standings by Quartile",
subtitle = "Example data"
) |>
tab_row_group(
label = "Elite Teams",
rows = pct >= 0.75
) |>
tab_row_group(
label = "Playoff Teams",
rows = pct >= 0.50 & pct < 0.75
)| NFL Standings by Quartile | |||||||
| Example data | |||||||
| # | Team | Abbrev | W | L | T | Win % | Quartile |
|---|---|---|---|---|---|---|---|
| Playoff Teams | |||||||
| 2 | ![]() |
BUF | 8 | 3 | 0 | 0.727 | Q2 (Good) |
| 3 | ![]() |
SF | 8 | 3 | 0 | 0.727 | Q2 (Good) |
| 4 | ![]() |
PHI | 7 | 4 | 0 | 0.636 | Q2 (Good) |
| 5 | ![]() |
DAL | 7 | 4 | 0 | 0.636 | Q2 (Good) |
| 6 | ![]() |
MIA | 6 | 5 | 0 | 0.545 | Q2 (Good) |
| 7 | ![]() |
CIN | 6 | 5 | 0 | 0.545 | Q2 (Good) |
| 8 | ![]() |
BAL | 6 | 5 | 0 | 0.545 | Q2 (Good) |
| Elite Teams | |||||||
| 1 | ![]() |
KC | 9 | 2 | 0 | 0.818 | Q1 (Elite) |
Quarto Dashboard Integration
Create a Quarto dashboard with sdvplotR visualizations:
# In a Quarto document (.qmd), use this structure:
# ---
# title: "NFL Dashboard"
# format: dashboard
# ---
# ## Standings
# ```{r}
# standings |>
# gt() |>
# gt_sdv_logos(columns = "logo", league = "nfl")
# ```
# ## Team Performance
# ```{r}
# ggplot(standings, aes(x = reorder(team, pct), y = pct)) +
# geom_col(aes(fill = team), width = 0.7) +
# scale_fill_sdv(league = "nfl") +
# theme_minimal()
# ```Real-Time Leaderboard Updates
Create a leaderboard that updates automatically:
# Function to fetch and display standings
update_standings <- function(league = "nfl", week = NULL) {
# In a real implementation, this would fetch live data
# For demonstration, we'll use sample data
message(paste("Updating", toupper(league), "standings..."))
# Sample standings (replace with real data fetch)
standings <- data.frame(
team = sample(sdv_teams(league)$abbr, 10),
wins = sample(0:16, 10),
losses = sample(0:16, 10)
) |>
mutate(
pct = wins / (wins + losses),
logo = team,
rank = row_number()
) |>
arrange(desc(pct)) |>
head(10)
standings |>
gt() |>
gt_sdv_logos(columns = "logo", league = league, height = 30) |>
fmt_number(columns = "pct", decimals = 3)
}
# Update standings
update_standings("nfl")| team | wins | losses | pct | logo | rank |
|---|---|---|---|---|---|
| MIN | 16 | 0 | 1.000 | ![]() |
4 |
| IND | 7 | 2 | 0.778 | ![]() |
6 |
| ARI | 15 | 6 | 0.714 | ![]() |
7 |
| DAL | 8 | 5 | 0.615 | ![]() |
1 |
| CHI | 11 | 9 | 0.550 | ![]() |
9 |
| BAL | 14 | 13 | 0.519 | ![]() |
8 |
| KC | 5 | 7 | 0.417 | ![]() |
2 |
| TB | 2 | 3 | 0.400 | ![]() |
10 |
| LAC | 3 | 16 | 0.158 | ![]() |
3 |
| NE | 0 | 11 | 0.000 | ![]() |
5 |
Conditional Formatting
Apply conditional formatting based on performance:
# Add performance indicators
standings_conditional <- standings |>
mutate(
performance = case_when(
pct >= 0.75 ~ "π₯ Hot",
pct >= 0.50 ~ "β
Good",
pct >= 0.25 ~ "β οΈ Average",
TRUE ~ "β Struggling"
)
)
standings_conditional |>
gt() |>
gt_sdv_logos(columns = "logo", league = "nfl", height = 35) |>
fmt_number(columns = "pct", decimals = 3) |>
cols_label(
rank = "#",
logo = "Team",
team = "Abbrev",
wins = "W",
losses = "L",
ties = "T",
pct = "Win %",
performance = "Status"
) |>
tab_header(
title = "NFL Standings with Status",
subtitle = "Example data"
)| NFL Standings with Status | |||||||
| Example data | |||||||
| # | Team | Abbrev | W | L | T | Win % | Status |
|---|---|---|---|---|---|---|---|
| 1 | ![]() |
KC | 9 | 2 | 0 | 0.818 | π₯ Hot |
| 2 | ![]() |
BUF | 8 | 3 | 0 | 0.727 | β Good |
| 3 | ![]() |
SF | 8 | 3 | 0 | 0.727 | β Good |
| 4 | ![]() |
PHI | 7 | 4 | 0 | 0.636 | β Good |
| 5 | ![]() |
DAL | 7 | 4 | 0 | 0.636 | β Good |
| 6 | ![]() |
MIA | 6 | 5 | 0 | 0.545 | β Good |
| 7 | ![]() |
CIN | 6 | 5 | 0 | 0.545 | β Good |
| 8 | ![]() |
BAL | 6 | 5 | 0 | 0.545 | β Good |
Integrating with oddsapiR
Combine standings with betting odds:
# Sample odds data (replace with real oddsapiR data)
odds_data <- data.frame(
team = c("KC", "BUF", "SF", "PHI"),
spread = c(-7.5, -3.5, -6.5, -10.5),
moneyline = c(-350, -180, -280, -550),
total = c(48.5, 51.5, 47.5, 45.5)
)
# Merge with standings
standings_with_odds <- standings |>
filter(team %in% odds_data$team) |>
left_join(odds_data, by = "team") |>
mutate(logo = team)
standings_with_odds |>
gt() |>
gt_sdv_logos(columns = "logo", league = "nfl", height = 35) |>
fmt_number(columns = c("pct", "spread", "total"), decimals = 1) |>
fmt_number(columns = "moneyline", decimals = 0) |>
cols_label(
rank = "#",
logo = "Team",
team = "Abbrev",
wins = "W",
losses = "L",
pct = "Win %",
spread = "Spread",
moneyline = "ML",
total = "O/U"
) |>
tab_header(
title = "NFL Standings with Betting Odds",
subtitle = "Combining Performance and Odds"
)| NFL Standings with Betting Odds | |||||||||
| Combining Performance and Odds | |||||||||
| # | Team | Abbrev | W | L | ties | Win % | Spread | ML | O/U |
|---|---|---|---|---|---|---|---|---|---|
| 1 | ![]() |
KC | 9 | 2 | 0 | 0.8 | β7.5 | β350 | 48.5 |
| 2 | ![]() |
BUF | 8 | 3 | 0 | 0.7 | β3.5 | β180 | 51.5 |
| 3 | ![]() |
SF | 8 | 3 | 0 | 0.7 | β6.5 | β280 | 47.5 |
| 4 | ![]() |
PHI | 7 | 4 | 0 | 0.6 | β10.5 | β550 | 45.5 |
Best Practices for Dashboards
Keep it Simple: Focus on key metrics that matter to your audience
Use Consistent Branding: Apply the same colors, fonts, and logos across all visualizations
Make it Interactive: Use Quarto dashboards for interactive exploration
Update Regularly: Set up automated updates to keep data fresh
Optimize for Mobile: Ensure dashboards work on mobile devices
Add Context: Include captions, footnotes, and data sources
Related SportsDataverse packages
- sdvplot - the Python plotting package
- sdvplot-js - the JavaScript / TypeScript plotting packages
- cfbfastR - college football
- hoopR - menβs basketball
- wehoop - womenβs basketball
- baseballr - baseball
- fastRhockey - hockey
- oddsapiR - betting odds
- sportyR - playing surfaces
- sportsdataverse-py - the Python package
- sportsdataverse-R - the R meta-package


























