Skip to main content

Team colours

A team's colours carry its identity on a chart as strongly as its logo, and they are the part a reader notices when they are wrong. sdvplot ships each team's primary and secondary colour with the league data, so colour lookups need no network and no hand-kept table. This guide covers the lookups, the colour arithmetic for text and contrast, and picking two colours that tell a matchup apart.

A palette keyed by your values​

palette(league, values) returns { value: "#hex" } keyed by whatever you passed, ready to hand to any chart library's colour option. Without values it covers the whole league, keyed by abbreviation.

{
  "KC": "#e31837",
  "BUF": "#00338d",
  "PHI": "#004c54"
}
import { palette } from "@sportsdataverse/sdvplot";

await palette("nfl", ["KC", "BUF", "PHI"]);
KC#e31837LAC#007bc7DEN#002244LV#000000BUF#00338dMIA#008e97NYJ#003f2dNE#002244
import * as Plot from "@observablehq/plot";
import { STANDINGS } from "@sportsdataverse/examples/data";
import { onColor, palette } from "@sportsdataverse/sdvplot";

// { team: "#hex" } keyed by your own values; Plot draws CSS colour strings as they are.
const colors = await palette(
"nfl",
STANDINGS.map((s) => s.team),
);
const swatches = Object.entries(colors).map(([team, hex]) => ({ team, hex, ink: onColor(hex) }));

Plot.plot({
height: 90,
x: { axis: null },
y: { axis: null },
marks: [
Plot.cell(swatches, { x: "team", fill: "hex", inset: 2 }),
Plot.text(swatches, { x: "team", text: (d) => `${d.team}\n${d.hex}`, fill: "ink" }),
],
});

Primary and secondary, in your order​

teamColors(league, values, { which }) returns one colour per value, in order; which: "secondary" picks the second colour. teamColorsSync is the same call once the league is loaded.

[
  "#fdb927",
  "#ffffff"
]
import { teamColors } from "@sportsdataverse/sdvplot";

await teamColors("nba", ["LAL", "BOS"], { which: "secondary" });
{
  "KC": {
    "primary": "#e31837",
    "secondary": "#ffb612"
  },
  "LAC": {
    "primary": "#007bc7",
    "secondary": "#ffc20e"
  },
  "DEN": {
    "primary": "#002244",
    "secondary": "#fb4f14"
  },
  "LV": {
    "primary": "#000000",
    "secondary": "#a5acaf"
  },
  "BUF": {
    "primary": "#00338d",
    "secondary": "#c60c30"
  },
  "MIA": {
    "primary": "#008e97",
    "secondary": "#f58220"
  },
  "NYJ": {
    "primary": "#003f2d",
    "secondary": "#000000"
  },
  "NE": {
    "primary": "#002244",
    "secondary": "#c60c30"
  }
}
import { STANDINGS } from "@sportsdataverse/examples/data";
import { teamColors, teamColorsSync } from "@sportsdataverse/sdvplot";

const teams = STANDINGS.map((s) => s.team);
const primary = await teamColors("nfl", teams); // loads the league, then one colour per value
const secondary = teamColorsSync("nfl", teams, { which: "secondary" }); // the league is loaded now

Object.fromEntries(teams.map((t, i) => [t, { primary: primary[i], secondary: secondary[i] }]));

Text on a colour, and contrast​

onColor picks black or white text for a fill, contrast is the WCAG ratio between two colours, luminance the relative luminance it is built on. mix blends two colours, solid flattens a translucent colour over white, and hex6 writes any hex form as #rrggbb.

{
  "kc": "#e31837",
  "lv": "#000000",
  "hex6('#E18')": "#ee1188",
  "luminance(kc)": "0.173",
  "contrast(kc, lv)": "4.45",
  "onColor(kc)": "#ffffff",
  "mix(kc, lv, 0.5)": "#720c1c",
  "solid(`${kc}80`)": "#f18b9b"
}
import { contrast, hex6, luminance, mix, onColor, solid, teamColors } from "@sportsdataverse/sdvplot";

const [kc = "#e31837", lv = "#000000"] = await teamColors("nfl", ["KC", "LV"]);

{
kc,
lv,
"hex6('#E18')": hex6("#E18"), // short and upper-case forms become #rrggbb
"luminance(kc)": luminance(kc).toFixed(3),
"contrast(kc, lv)": contrast(kc, lv).toFixed(2), // WCAG ratio; 4.5 is the body-text bar
"onColor(kc)": onColor(kc), // black or white text on it
"mix(kc, lv, 0.5)": mix(kc, lv, 0.5),
"solid(`${kc}80`)": solid(`${kc}80`), // half-transparent over white, as an opaque colour
};

Two colours for one matchup​

Two teams that share a colour (two reds, two navies) need a pair that reads apart. matchupColors(teamA, teamB) is Game on Paper's picker: one pair for a light page and one for a dark page, each keeping a team's primary whenever it works. matchupColorsSync is the same once the league is loaded.

{
  "light": [
    "#9e1b32",
    "#2c2a29"
  ],
  "dark": [
    "#ffffff",
    "#ba0c2f"
  ]
}
import { matchupColors } from "@sportsdataverse/sdvplot";

await matchupColors("Alabama", "Georgia", { league: "cfb" });
{
  "light": [
    "#9e1b32",
    "#2c2a29"
  ],
  "dark": [
    "#ffffff",
    "#ba0c2f"
  ]
}
import { loadLeague, matchupColorsSync } from "@sportsdataverse/sdvplot";

await loadLeague("cfb");
matchupColorsSync("Alabama", "Georgia", { league: "cfb" });

In the gallery: Identity, colours, marks.