Cell kinds
A column's kind decides what its cells show: the value as written, a formatted number, a colour, a logo. sdvtables
has 21 kinds, one c.<kind>(key, options) call each, with sdvplotR's defaults; all of them render on the same eight
AFC teams, so you can compare them directly. Kinds that look a team up (logo, wordmark, the team colours) need
the league loaded, which prepare and renderHTMLAsync do.
Text and numbers
text shows the value as written; int and num format numbers (digits, a forced sign, a prefix or suffix), and a
missing value stays blank.
| Team | Quarterback | Division |
|---|---|---|
| KC | Patrick Mahomes | West |
| LAC | Justin Herbert | West |
| DEN | Bo Nix | West |
| LV | Gardner Minshew | West |
| BUF | Josh Allen | East |
| MIA | Tua Tagovailoa | East |
| NYJ | Aaron Rodgers | East |
| NE | Drake Maye | East |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.text("qb", { label: "Quarterback" }),
c.text("division", { align: "center" }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | W | L | PF | PA |
|---|---|---|---|---|
| KC | 15 | 2 | 385 | 326 |
| LAC | 11 | 6 | 402 | 301 |
| DEN | 10 | 7 | 425 | 311 |
| LV | 4 | 13 | 309 | 434 |
| BUF | 13 | 4 | 525 | 368 |
| MIA | 8 | 9 | 345 | 364 |
| NYJ | 5 | 12 | 338 | 404 |
| NE | 4 | 13 | 289 | 417 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.int("wins", { label: "W" }),
c.int("losses", { label: "L" }),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Net EPA/play | Points for |
|---|---|---|
| KC | +0.063 | 385 pts |
| LAC | +0.101 | 402 pts |
| DEN | +0.108 | 425 pts |
| LV | −0.146 | 309 pts |
| BUF | +0.190 | 525 pts |
| MIA | −0.019 | 345 pts |
| NYJ | −0.045 | 338 pts |
| NE | 289 pts |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// New England has no net EPA on file (null): the cell stays blank.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.num("net_epa", { label: "Net EPA/play", digits: 3, forceSign: true }),
c.num("pf", { label: "Points for", digits: 0, suffix: " pts" }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
pct multiplies a share by 100, rank writes ordinals, delta(from, to) shows to - from with an arrow, and
tally writes a record from several columns, optionally with one part's share.
| Team | Wins | Losses | Win % |
|---|---|---|---|
| KC | 15 | 2 | 88.2% |
| LAC | 11 | 6 | 64.7% |
| DEN | 10 | 7 | 58.8% |
| LV | 4 | 13 | 23.5% |
| BUF | 13 | 4 | 76.5% |
| MIA | 8 | 9 | 47.1% |
| NYJ | 5 | 12 | 29.4% |
| NE | 4 | 13 | 23.5% |
import { STANDINGS } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// pct multiplies by 100 (scale: true, the default): pass shares, not percentages.
const rows = STANDINGS.map((r) => ({ ...r, win_share: r.wins / (r.wins + r.losses + r.ties) }));
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [c.text("team"), c.int("wins"), c.int("losses"), c.pct("win_share", { label: "Win %" })])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, rows);
| Team | SRS rank |
|---|---|
| KC | 10th |
| LAC | 9th |
| DEN | 8th |
| LV | 26th |
| BUF | 4th |
| MIA | 21st |
| NYJ | 24th |
| NE | 29th |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [c.text("team"), c.rank("srs_rank", { label: "SRS rank" })])
.title("AFC, 2024: league-wide SRS rank")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | PF | Point diff. |
|---|---|---|
| KC | 385 | ▲ 59 |
| LAC | 402 | ▲ 101 |
| DEN | 425 | ▲ 114 |
| LV | 309 | ▼ 125 |
| BUF | 525 | ▲ 157 |
| MIA | 345 | ▼ 19 |
| NYJ | 338 | ▼ 66 |
| NE | 289 | ▼ 128 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// delta(from, to) shows to - from: here points for minus points against, green up and red down.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.int("pf", { label: "PF" }),
c.delta("pa", "pf", { label: "Point diff.", decimals: 0, arrows: true }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Record |
|---|---|
| KC | 15-2-0 (88.2%) |
| LAC | 11-6-0 (64.7%) |
| DEN | 10-7-0 (58.8%) |
| LV | 4-13-0 (23.5%) |
| BUF | 13-4-0 (76.5%) |
| MIA | 8-9-0 (47.1%) |
| NYJ | 5-12-0 (29.4%) |
| NE | 4-13-0 (23.5%) |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.tally(["wins", "losses", "ties"], { label: "Record", share: true, shareOf: 0 }),
])
.title("AFC, 2024: W-L-T (win share)")
.build();
await renderHTMLAsync(spec, STANDINGS);
Colour on a scale
colorPills puts each value in a pill coloured on a scale and colorRanks fills the cell, green to red
(RANK_PALETTE; reverse when lower is better). Pass a domain so two tables share a scale.
| Team | Net EPA/play |
|---|---|
| KC | 0.063 |
| LAC | 0.101 |
| DEN | 0.108 |
| LV | -0.146 |
| BUF | 0.190 |
| MIA | -0.019 |
| NYJ | -0.045 |
| NE |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// A domain fixes the scale so tables can be compared; without one the pills span the observed range and warn.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.colorPills("net_epa", { label: "Net EPA/play", digits: 3, domain: [-0.2, 0.2] }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Points for | Points against |
|---|---|---|
| KC | 385 | 326 |
| LAC | 402 | 301 |
| DEN | 425 | 311 |
| LV | 309 | 434 |
| BUF | 525 | 368 |
| MIA | 345 | 364 |
| NYJ | 338 | 404 |
| NE | 289 | 417 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// The default palette is RANK_PALETTE (green to red); reverse it when lower is better.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.colorRanks("pf", { label: "Points for", domain: [280, 530], reverse: true }),
c.colorRanks("pa", { label: "Points against", domain: [280, 530] }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
percentileBar places a marker on a track; colorResults fills a whole row by its W or L.
| Team | Win % |
|---|---|
| KC | |
| LAC | |
| DEN | |
| LV | |
| BUF | |
| MIA | |
| NYJ | |
| NE |
import { STANDINGS } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// With scale "auto" (the default), values that are all in [0, 1] are read as shares of the domain.
const rows = STANDINGS.map((r) => ({ ...r, win_share: r.wins / (r.wins + r.losses + r.ties) }));
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [c.text("team"), c.percentileBar("win_share", { label: "Win %", domain: [0, 100] })])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, rows);
| Team | Qb | Last game |
|---|---|---|
| KC | Patrick Mahomes | L |
| LAC | Justin Herbert | W |
| DEN | Bo Nix | W |
| LV | Gardner Minshew | L |
| BUF | Josh Allen | L |
| MIA | Tua Tagovailoa | L |
| NYJ | Aaron Rodgers | W |
| NE | Drake Maye | W |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// The cell holds W or L; the whole row takes the win or loss fill.
const spec = defineTable<Standing>()
.columns((c) => [c.text("team"), c.text("qb"), c.colorResults("result_last", { label: "Last game" })])
.title("AFC, 2024: final regular-season game")
.build();
await renderHTMLAsync(spec, STANDINGS);
Flags
highlight marks the cells that match a predicate, highlightNa marks missing values, and indicatorBox fills a box
for the values listed as truthy.
| Team | Wins | Losses |
|---|---|---|
| KC | 15 | 2 |
| LAC | 11 | 6 |
| DEN | 10 | 7 |
| LV | 4 | 13 |
| BUF | 13 | 4 |
| MIA | 8 | 9 |
| NYJ | 5 | 12 |
| NE | 4 | 13 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// The rule is data (a predicate), not a function, so the spec stays serializable.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.highlight("wins", { key: "wins", op: ">=", value: 11 }, { label: "Wins", bold: true }),
c.int("losses"),
])
.title("AFC, 2024: 11 or more wins")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Net EPA/play |
|---|---|
| KC | 0.063 |
| LAC | 0.101 |
| DEN | 0.108 |
| LV | -0.146 |
| BUF | 0.19 |
| MIA | -0.019 |
| NYJ | -0.045 |
| NE | n/a |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// New England's net EPA is null in the sample data.
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.highlightNa("net_epa", { label: "Net EPA/play", missingText: "n/a", italic: true }),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Won week 18 |
|---|---|
| KC | |
| LAC | |
| DEN | |
| LV | |
| BUF | |
| MIA | |
| NYJ | |
| NE |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// truthy lists the values that fill the box; everything else gets the neutral box.
const spec = defineTable<Standing>()
.columns((c) => [c.text("team"), c.indicatorBox("result_last", { label: "Won week 18", truthy: ["W"] })])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
Teams and players
logo and wordmark look the team up in the league data and show its archived mark; headshot builds an ESPN
headshot from the player id alone, and image shows any URL a row holds.
| Team | Division |
|---|---|
KC | West |
LAC | West |
DEN | West |
LV | West |
BUF | East |
MIA | East |
NYJ | East |
NE | East |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// renderHTMLAsync loads the NFL shard first; the cell holds the archive URL, nothing is fetched to render it.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl", includeName: true }), c.text("division")])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Wins | Losses |
|---|---|---|
![]() | 15 | 2 |
![]() | 11 | 6 |
![]() | 10 | 7 |
![]() | 4 | 13 |
![]() | 13 | 4 |
![]() | 8 | 9 |
![]() | 5 | 12 |
![]() | 4 | 13 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [
c.wordmark("team", { league: "nfl", label: "Team", height: 24 }),
c.int("wins"),
c.int("losses"),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Quarterback | Team | |
|---|---|---|
![]() | Patrick Mahomes | KC |
![]() | Justin Herbert | LAC |
![]() | Bo Nix | DEN |
![]() | Gardner Minshew | LV |
![]() | Josh Allen | BUF |
![]() | Tua Tagovailoa | MIA |
![]() | Aaron Rodgers | NYJ |
![]() | Drake Maye | NE |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// ESPN headshot URLs are built from the id alone: no shard to load.
const spec = defineTable<Standing>()
.columns((c) => [
c.headshot("qb_espn_id", { league: "nfl", label: "" }),
c.text("qb", { label: "Quarterback" }),
c.text("team"),
])
.title("AFC starting quarterbacks, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Qb | |
|---|---|---|
![]() | KC | Patrick Mahomes |
![]() | LAC | Justin Herbert |
![]() | DEN | Bo Nix |
![]() | LV | Gardner Minshew |
![]() | BUF | Josh Allen |
![]() | MIA | Tua Tagovailoa |
![]() | NYJ | Aaron Rodgers |
![]() | NE | Drake Maye |
import { STANDINGS } from "@sportsdataverse/examples/data";
import { loadLeague, logoUrlSync } from "@sportsdataverse/sdvplot";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// image draws the URL the row holds; here sdvplot's archive logo URL, added to each row.
await loadLeague("nfl");
const rows = STANDINGS.map((r) => ({ ...r, logo_url: logoUrlSync(r.team, "nfl") ?? null }));
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [
c.image("logo_url", { label: "", height: "40px", alt: "team" }),
c.text("team"),
c.text("qb"),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, rows);
teamColorBar draws a bar in the team colour, teamColorBg tints the cell, and mergeStackTeamColor stacks two
values with the lower one in the team colour.
| Team | Qb | Wins |
|---|---|---|
| KC | Patrick Mahomes | 15 |
| LAC | Justin Herbert | 11 |
| DEN | Bo Nix | 10 |
| LV | Gardner Minshew | 4 |
| BUF | Josh Allen | 13 |
| MIA | Tua Tagovailoa | 8 |
| NYJ | Aaron Rodgers | 5 |
| NE | Drake Maye | 4 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [c.teamColorBar("team", { league: "nfl", barWidth: 6 }), c.text("qb"), c.int("wins")])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Team | Qb | Wins |
|---|---|---|
| KC | Patrick Mahomes | 15 |
| LAC | Justin Herbert | 11 |
| DEN | Bo Nix | 10 |
| LV | Gardner Minshew | 4 |
| BUF | Josh Allen | 13 |
| MIA | Tua Tagovailoa | 8 |
| NYJ | Aaron Rodgers | 5 |
| NE | Drake Maye | 4 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
const spec = defineTable<Standing>()
.columns((c) => [
c.teamColorBg("team", { league: "nfl", which: "secondary", alpha: 0.5 }),
c.text("qb"),
c.int("wins"),
])
.title("AFC, 2024: secondary colours at 50%")
.build();
await renderHTMLAsync(spec, STANDINGS);
| Quarterback | Wins |
|---|---|
Patrick Mahomes
KC | 15 |
Justin Herbert
LAC | 11 |
Bo Nix
DEN | 10 |
Gardner Minshew
LV | 4 |
Josh Allen
BUF | 13 |
Tua Tagovailoa
MIA | 8 |
Aaron Rodgers
NYJ | 5 |
Drake Maye
NE | 4 |
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";
// key on top, stack below; team picks the colour (made readable against the table background).
const spec = defineTable<Standing>()
.columns((c) => [
c.mergeStackTeamColor("qb", "team", "team", { league: "nfl", label: "Quarterback" }),
c.int("wins"),
])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
In the gallery: Every cell kind.
KC
LAC
DEN
LV
BUF
MIA
NYJ
NE














