Skip to main content

Decorations

Decorations are everything around and across the cells: titles and notes, rules and stripes, rows picked out, legends, a long table folded into blocks. Each is one builder method (sdvplotR's gt_* helpers, renamed), they stack in any order, and like the rest of a spec they are plain data. All 25 are shown here on the same eight AFC teams.

Titles and notes​

title and subtitle caption the table; titleHeader adds a kicker and a date. sourceNote writes a footnote (escaped unless you mark it unsafe), and caption538 a ruled note and source line in the FiveThirtyEight style.

AFC, 2024
TeamWinsLosses
152
116
107
413
134
89
512
413
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024Regular season, East and West divisions
TeamWinsLosses
152
116
107
413
134
89
512
413
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.title("AFC, 2024")
.subtitle("Regular season, East and West divisions")
.build();
await renderHTMLAsync(spec, STANDINGS);
NFL 2024
AFC standingsEast and West divisions
Final, week 18
TeamWinsLosses
152
116
107
413
134
89
512
413
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.titleHeader("AFC standings", {
kicker: "NFL 2024",
subtitle: "East and West divisions",
date: "Final, week 18",
})
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamPFPA
385326
402301
425311
309434
525368
345364
338404
289417
Data: nflverse
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// The note is escaped unless you mark it unsafe (then it is inserted as HTML: only for markup you wrote).
const spec = defineTable<Standing>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
])
.title("AFC, 2024")
.sourceNote('Data: <a href="https://nflverse.nflverse.com">nflverse</a>', { unsafe: true })
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamWinsLosses
152
116
107
413
134
89
512
413
Kansas City lost two games all season.
SOURCE: NFLVERSE
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.title("AFC, 2024")
.caption538({ top: "Kansas City lost two games all season.", bottom: "SOURCE: NFLVERSE" })
.build();
await renderHTMLAsync(spec, STANDINGS);

socialTag adds handles with their icons, watermark faint text (or an image) behind the rows, and font one Google font for the whole table.

AFC, 2024
TeamWinsLosses
152
116
107
413
134
89
512
413
Data: nflverse
SportsDataverse | sportsdataverse
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.socialTag({ x: "SportsDataverse", gh: "sportsdataverse" }, { caption: "Data: nflverse" })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamQbWinsLosses
Patrick Mahomes152
Justin Herbert116
Bo Nix107
Gardner Minshew413
Josh Allen134
Tua Tagovailoa89
Aaron Rodgers512
Drake Maye413
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// text becomes an inline SVG background; pass image (a URL) instead for a logo.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.text("qb"), c.int("wins"), c.int("losses")])
.watermark({ text: "SAMPLE", angle: -20, opacity: 0.08 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamQbWinsLosses
Patrick Mahomes152
Justin Herbert116
Bo Nix107
Gardner Minshew413
Josh Allen134
Tua Tagovailoa89
Aaron Rodgers512
Drake Maye413
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// google: true (the default) adds the Google Fonts <link>; set it false for a font the page already loads.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.text("qb"), c.int("wins"), c.int("losses")])
.font("Roboto Slab", { weight: 500 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);

Rules, bars and stripes​

borderBars draws colour bars above or below the table, borderGrid rules between columns, and cutline a dashed line after the nth row.

AFC, 2024
TeamWinsLosses
152
116
107
413
134
89
512
413
SportsDataverse
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// Bars only: one bar per colour. With text (or an image): one bar in the first colour holding it.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.borderBars("top", ["#0B1A33", "#7FE6DC"], { barHeight: 6 })
.borderBars("bottom", ["#0B1A33"], { barHeight: 28, text: "SportsDataverse", textSize: 13 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamWinsLossesPF
152385
116402
107425
413309
134525
89345
512338
413289
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.logo("team", { league: "nfl" }),
c.int("wins"),
c.int("losses"),
c.int("pf", { label: "PF" }),
])
.borderGrid({ color: "#C8C8C8", includeLabels: true })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamWinsLosses
152
134
116
107
89
512
413
413
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// Sorted by wins, the four playoff teams among these eight sit above the line.
const rows = [...STANDINGS].sort((a, b) => b.wins - a.wins);
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.cutline(4, { label: ["Playoff line"] })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, rows);

Groups​

groupBy(key) adds a heading row per group, in the order the rows arrive; groupStripes shades every other group.

AFC, 2024
TeamQbWinsLosses
West
Patrick Mahomes152
Justin Herbert116
Bo Nix107
Gardner Minshew413
East
Josh Allen134
Tua Tagovailoa89
Aaron Rodgers512
Drake Maye413
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// Rows are grouped in the order they arrive: sort them first if the groups are interleaved.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.text("qb"), c.int("wins"), c.int("losses")])
.groupBy("division")
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamQbWinsLosses
West
Patrick Mahomes152
Justin Herbert116
Bo Nix107
Gardner Minshew413
East
Josh Allen134
Tua Tagovailoa89
Aaron Rodgers512
Drake Maye413
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.logo("team", { league: "nfl" }), c.text("qb"), c.int("wins"), c.int("losses")])
.groupBy("division")
.groupStripes({ color: "#EEF3FA", start: 1 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);

Rows picked out​

Each takes a predicate or row indices. boldRows sets rows in bold, spotlight lights one row and dims the rest, and rowAccent colours each row's edge by a column's value.

AFC, 2024: 13 or more wins
TeamWinsLosses
152
116
107
413
134
89
512
413
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.boldRows({ key: "wins", op: ">=", value: 13 }, { highlightColor: "#FFF3B0" })
.title("AFC, 2024: 13 or more wins")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024: Buffalo
TeamQbWinsLosses
Patrick Mahomes152
Justin Herbert116
Bo Nix107
Gardner Minshew413
Josh Allen134
Tua Tagovailoa89
Aaron Rodgers512
Drake Maye413
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.logo("team", { league: "nfl" }), c.text("qb"), c.int("wins"), c.int("losses")])
.spotlight({ key: "team", op: "==", value: "BUF" }, { fill: "#E8EEF9", accentColor: "#00338D" })
.title("AFC, 2024: Buffalo")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024: East in blue, West in red
TeamWinsLosses
152
116
107
413
134
89
512
413
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// A palette maps each division to a colour; hide (the default) drops the division column itself.
const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.text("division"), c.int("wins"), c.int("losses")])
.rowAccent("division", { palette: { East: "#00338D", West: "#E31837" }, width: 6 })
.title("AFC, 2024: East in blue, West in red")
.build();
await renderHTMLAsync(spec, STANDINGS);

Reading the numbers​

legendContinuous keys a colour scale (by default the one a colorPills column recorded) and legendDiscrete a set of named colours. scaleNote divides columns and says so, outliers flags values beyond 1.5 IQR, and significance adds stars from a p-value column.

AFC, 2024
TeamNet EPA/play
0.063
0.101
0.108
-0.146
0.190
-0.019
-0.045
Net EPA per play
-0.20.2
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// With no palette or domain of its own, the legend reads the scale the colorPills column recorded.
const spec = defineTable<Standing>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.colorPills("net_epa", { label: "Net EPA/play", digits: 3, domain: [-0.2, 0.2] }),
])
.legendContinuous({ title: "Net EPA per play", digits: 1 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
Final regular-season game
WonLost
AFC, 2024
TeamQbWeek 18
Patrick MahomesL
Justin HerbertW
Bo NixW
Gardner MinshewL
Josh AllenL
Tua TagovailoaL
Aaron RodgersW
Drake MayeW
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.logo("team", { league: "nfl" }),
c.text("qb"),
c.colorResults("result_last", { label: "Week 18" }),
])
.legendDiscrete({ Won: "#5DA271", Lost: "#C84630" }, { heading: "Final regular-season game" })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamPF / gPA / g
22.619.2
23.617.7
25.018.3
18.225.5
30.921.6
20.321.4
19.923.8
17.024.5
Points per game (17 games).
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// Any divisor works; 1,000 and 1,000,000 come with their own wording ("Figures in thousands.").
const spec = defineTable<Standing>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
])
.scaleNote(["pf", "pa"], {
divisor: 17,
decimals: 1,
where: "both",
note: "Points per game (17 games).",
labelSuffix: "/ g",
})
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamPFPA
385326
402301
425311
309434
525†368
345364
338404
289417
Marked values fall outside 1.5 × IQR of the column quartiles.
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// Buffalo's 525 points for sits above the upper fence; nothing in points against does.
const spec = defineTable<Standing>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
])
.outliers(["pf", "pa"], { symbol: "†", note: true })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
8 AFC teams, 2024: which records beat a coin flip?
TeamWin %p vs .500
88.2%***0.002
64.7%0.332
58.8%0.629
23.5%**0.049
76.5%**0.049
47.1%1.000
29.4%0.143
23.5%**0.049
*** p < 0.01, ** p < 0.05, * p < 0.1
import { STANDINGS } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// p: an exact two-sided binomial test of each record against a coin flip (no ties in these rows).
const choose = (n: number, k: number): number => (k === 0 ? 1 : (choose(n, k - 1) * (n - k + 1)) / k);
function pVs500(w: number, l: number): number {
let tail = 0;
for (let k = Math.max(w, l); k <= w + l; k++) tail += choose(w + l, k) / 2 ** (w + l);
return Math.min(1, 2 * tail);
}
const rows = STANDINGS.map((r) => ({
...r,
win_share: r.wins / (r.wins + r.losses),
p: pVs500(r.wins, r.losses),
}));
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.pct("win_share", { label: "Win %" }),
c.num("p", { label: "p vs .500", digits: 3 }),
])
.significance([{ estimate: "win_share", p: "p" }], { hideP: false })
.title("8 AFC teams, 2024: which records beat a coin flip?")
.build();
await renderHTMLAsync(spec, rows);

Layout​

wrapLabels wraps long column labels, marginalia sets a column as side notes, and tiers turns rows into a tier list with one row per tier.

AFC, 2024
TeamPoints
scored,
regular
season
Points
allowed,
regular
season
385326
402301
425311
309434
525368
345364
338404
289417
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.logo("team", { league: "nfl" }),
c.int("pf", { label: "Points scored, regular season" }),
c.int("pa", { label: "Points allowed, regular season" }),
])
.wrapLabels({ width: 10 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
AFC, 2024
TeamWinsLossesStarting QB
152Patrick Mahomes
116Justin Herbert
107Bo Nix
413Gardner Minshew
134Josh Allen
89Tua Tagovailoa
512Aaron Rodgers
413Drake Maye
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses"), c.text("qb")])
.marginalia(["qb"], { label: "Starting QB", width: 160 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
8 AFC teams, 2024: SRS tiers
SRS top 10
11th to 20th
21st to 32nd
import { STANDINGS } from "@sportsdataverse/examples/data";
import { loadLeague, logoUrlSync } from "@sportsdataverse/sdvplot";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// One row per tier: its label, then a logo URL per team (league-wide SRS rank) in the image columns.
await loadLeague("nfl");
const levels = ["SRS top 10", "11th to 20th", "21st to 32nd"];
const tierOf = (rank: number): string | undefined => levels[rank <= 10 ? 0 : rank <= 20 ? 1 : 2];
const rows = levels.map((tier) => {
const logos = STANDINGS.filter((r) => tierOf(r.srs_rank) === tier).map(
(r) => logoUrlSync(r.team, "nfl") ?? null,
);
// four image columns: a fifth team in a tier would be dropped without a word
if (logos.length > 4) throw new Error(`${tier} has ${logos.length} teams but 4 image columns`);
return { tier, t1: logos[0] ?? null, t2: logos[1] ?? null, t3: logos[2] ?? null, t4: logos[3] ?? null };
});
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [c.text("tier"), c.text("t1"), c.text("t2"), c.text("t3"), c.text("t4")])
.tiers(levels, "tier", ["t1", "t2", "t3", "t4"], {
colors: ["#FF7F7F", "#FFDF7F", "#BFBFBF"],
imgHeight: "44px",
style: "light",
})
.title("8 AFC teams, 2024: SRS tiers")
.build();
await renderHTMLAsync(spec, rows);

snake folds a long table into side-by-side blocks; snakeAlign is the split it uses, for laying out blocks yourself.

AFC, 2024
TeamWinsLossesTeamWinsLosses
152134
11689
107512
413413
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.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.snake({ nCols: 2 })
.title("AFC, 2024")
.build();
await renderHTMLAsync(spec, STANDINGS);
[
  [
    "KC",
    "LAC",
    "DEN"
  ],
  [
    "LV",
    "BUF",
    "MIA"
  ],
  [
    "NYJ",
    "NE",
    "(fill)"
  ]
]
import { STANDINGS } from "@sportsdataverse/examples/data";
import { snakeAlign } from "@sportsdataverse/sdvtables";

// Eight rows into three blocks of three; the last block is padded with fill (null unless you pass one).
snakeAlign(STANDINGS, { nCols: 3 }).map((block) => block.map((r) => r?.team ?? "(fill)"));

In the gallery: Every decoration and Layout.