Skip to main content

Identity and resolution

Every other function in sdvplot starts by turning what you have (an abbreviation, an ESPN id, a full name, a relocated franchise's old code) into one stable team_id for a league. Get this step right and logos, colours and headshots follow; get it wrong and a chart silently drops a team. This guide shows how a value becomes a team, what happens when it does not, and how to see the data behind the answer.

Loading a league​

League data ships with the package as one chunk per league. loadLeague imports it once (nothing is downloaded) and returns the league's index; every …Sync function needs its league loaded first, while the async ones load it themselves.

{
  "league": "nfl",
  "team_id": "12",
  "abbr": "KC",
  "name": "Kansas City Chiefs",
  "short_name": "Chiefs",
  "location": "Kansas City",
  "program": "pro",
  "conference_id": "nfl:afc",
  "conference": "American Football Conference",
  "color_primary": "#e31837",
  "color_secondary": "#ffb612",
  "color_source": "nflverse"
}
import { loadLeague } from "@sportsdataverse/sdvplot";

const nfl = await loadLeague("nfl");
nfl.teams.find((t) => t.abbr === "KC");

A value becomes a team id​

resolve reads each value through the id systems in PRIORITY order and returns one team_id per value. Name the system with idSystem when you know it; the systems in EXPLICIT_ONLY are never tried unless you name them.

[
  "12",
  "12",
  "13"
]
import { resolve } from "@sportsdataverse/sdvplot";

await resolve(["KC", "Kansas City Chiefs", "OAK"], "nfl", { season: 2019 });
{
  "auto": [
    "12",
    "12",
    "12"
  ],
  "espn": "12",
  "name": "12",
  "PRIORITY": [
    "team_id",
    "espn",
    "espn_abbr",
    "nhl",
    "nflverse",
    "mlbstats",
    "nba_api",
    "hockeytech",
    "ncaa",
    "pff",
    "cricinfo",
    "cfbd",
    "bref",
    "sportsipy",
    "fangraphs",
    "sdvplotr",
    "name"
  ],
  "EXPLICIT_ONLY": [
    "nhl_id"
  ]
}
import { EXPLICIT_ONLY, PRIORITY, resolve } from "@sportsdataverse/sdvplot";

// "auto" tries the systems in PRIORITY order; the first one holding the value decides.
const auto = await resolve(["12", "KC", "Kansas City Chiefs"], "nfl");
// Name the system when you know it: "12" is ESPN's id for Kansas City.
const espn = await resolve("12", "nfl", { idSystem: "espn" });
const name = await resolve("Kansas City Chiefs", "nfl", { idSystem: "name" });

{
auto,
espn,
name,
PRIORITY,
// Systems "auto" never tries: pass them as idSystem.
EXPLICIT_ONLY,
};

Seasons and relocations​

A franchise keeps its team_id across moves: OAK in 2019 and LV in 2024 are the same Raiders, QUE in 1994 and COL in 2024 the same NHL franchise. Pass season and an abbreviation resolves to whoever held it that year.

{
  "OAK 2019": "13",
  "LV 2024": "13",
  "QUE 1994": "17",
  "COL 2024": "17"
}
import { loadLeague, resolveSync } from "@sportsdataverse/sdvplot";

await loadLeague("nfl");
await loadLeague("nhl");

{
"OAK 2019": resolveSync("OAK", "nfl", { season: 2019 }),
"LV 2024": resolveSync("LV", "nfl", { season: 2024 }),
"QUE 1994": resolveSync("QUE", "nhl", { season: 1994 }),
"COL 2024": resolveSync("COL", "nhl", { season: 2024 }),
};

Every team, and candidates for a typo​

teams lists a league's teams. suggest ranks candidates for a value that did not resolve; it never picks one for you.

[
  "1  BOS  Boston Bruins",
  "10  MTL  Montreal Canadiens",
  "11  NJ  New Jersey Devils",
  "12  NYI  New York Islanders",
  "124292  SEA  Seattle Kraken",
  "129764  UTAH  Utah Mammoth",
  "13  NYR  New York Rangers",
  "14  OTT  Ottawa Senators",
  "15  PHI  Philadelphia Flyers",
  "16  PIT  Pittsburgh Penguins",
  "17  COL  Colorado Avalanche",
  "18  SJ  San Jose Sharks",
  "19  STL  St. Louis Blues",
  "2  BUF  Buffalo Sabres",
  "20  TB  Tampa Bay Lightning",
  "21  TOR  Toronto Maple Leafs",
  "22  VAN  Vancouver Canucks",
  "23  WSH  Washington Capitals",
  "25  ANA  Anaheim Ducks",
  "26  FLA  Florida Panthers",
  "27  NSH  Nashville Predators",
  "28  WPG  Winnipeg Jets",
  "29  CBJ  Columbus Blue Jackets",
  "3  CGY  Calgary Flames",
  "30  MIN  Minnesota Wild",
  "37  VGK  Vegas Golden Knights",
  "4  CHI  Chicago Blackhawks",
  "5  DET  Detroit Red Wings",
  "6  EDM  Edmonton Oilers",
  "7  CAR  Carolina Hurricanes",
  "8  LA  Los Angeles Kings",
  "9  DAL  Dallas Stars"
]
import { teams } from "@sportsdataverse/sdvplot";

const nhl = await teams("nhl");
nhl.map((t) => `${t.team_id} ${t.abbr ?? "-"} ${t.name ?? ""}`);
[
  [
    "12",
    "Kansas City Chiefs"
  ]
]
import { suggest } from "@sportsdataverse/sdvplot";

// [team_id, name] pairs, best first. suggest never picks one for you.
await suggest("Kansas Cty", "nfl");

How values and seasons are compared​

Values compare as normalised text (normValue: an integral float loses its .0, case and accents fold), and a season is one year (normSeason; a split season is its ending year). seasonBounds and latestSeason give a league's range: latestSeason is the latest season any dated alias names, not the current season.

{
  "normValue('12.0')": "12",
  "normValue(' Montréal ')": "montreal",
  "normSeason(2020)": 2020,
  "normSeason('2020-21')": "season must be a year such as 2020, got \"2020-21\"; for a split season pass its ending year (2021 for '2020-21')",
  "seasonBounds('nfl')[0]": 1920,
  "latestSeason('nfl')": 2026,
  "LEAGUES.length": 28,
  "VARIANTS.length": 32
}
import {
InputError,
LEAGUES,
VARIANTS,
latestSeason,
normSeason,
normValue,
seasonBounds,
} from "@sportsdataverse/sdvplot";

let splitSeason = "";
try {
normSeason("2020-21");
} catch (e) {
if (e instanceof InputError) splitSeason = e.message;
}

{
// Ids compare as text: an integral float loses its ".0"; case and accents fold.
"normValue('12.0')": normValue("12.0"),
"normValue(' Montréal ')": normValue(" Montréal "),
"normSeason(2020)": normSeason(2020),
"normSeason('2020-21')": splitSeason,
// The first season; the upper bound is next year, so it moves every January.
"seasonBounds('nfl')[0]": seasonBounds("nfl")?.[0],
"latestSeason('nfl')": latestSeason("nfl"),
"LEAGUES.length": LEAGUES.length,
"VARIANTS.length": VARIANTS.length,
};

Columns to rows​

Every mark takes an array of row objects. rowsFrom turns column-shaped data (an R data frame or a pandas habit) into rows.

[
  {
    "team": "KC",
    "wins": 15,
    "losses": 2
  },
  {
    "team": "LAC",
    "wins": 11,
    "losses": 6
  },
  {
    "team": "DEN",
    "wins": 10,
    "losses": 7
  },
  {
    "team": "LV",
    "wins": 4,
    "losses": 13
  },
  {
    "team": "BUF",
    "wins": 13,
    "losses": 4
  },
  {
    "team": "MIA",
    "wins": 8,
    "losses": 9
  },
  {
    "team": "NYJ",
    "wins": 5,
    "losses": 12
  },
  {
    "team": "NE",
    "wins": 4,
    "losses": 13
  }
]
import { STANDINGS } from "@sportsdataverse/examples/data";
import { rowsFrom } from "@sportsdataverse/sdvplot";

// Column-shaped data (an R or pandas habit) to the row objects Plot and the marks take.
rowsFrom({
team: STANDINGS.map((s) => s.team),
wins: STANDINGS.map((s) => s.wins),
losses: STANDINGS.map((s) => s.losses),
});

Warnings, strict mode and errors​

A value that does not resolve becomes undefined with a warning, given once per distinct set of unresolved values for the life of the process: a call that repeats it is silent until resetWarnings(). setWarningHandler routes warnings elsewhere. strict: true throws UnresolvedTeamError instead.

{
  "warning": "1 value(s) did not resolve to a nfl team: 'XYZ' (unknown). Use suggest() for candidates, or strict: true to throw.",
  "timesWarned": 2,
  "error": "UnresolvedTeamError: 1 value(s) did not resolve to a nfl team: 'XYZ' (unknown)"
}
import {
SdvplotError,
UnresolvedTeamError,
loadLeague,
resetWarnings,
resolveSync,
setWarningHandler,
} from "@sportsdataverse/sdvplot";

await loadLeague("nfl");
const seen: string[] = [];
setWarningHandler((message) => seen.push(message)); // instead of console.warn
resolveSync(["KC", "XYZ"], "nfl"); // [KC's id, undefined] and one warning
resolveSync(["KC", "XYZ"], "nfl"); // the same warning again: silent
resetWarnings();
resolveSync(["KC", "XYZ"], "nfl"); // warns again
setWarningHandler(null); // back to console.warn

let error = "";
try {
resolveSync(["KC", "XYZ"], "nfl", { strict: true });
} catch (e) {
if (e instanceof UnresolvedTeamError && e instanceof SdvplotError) error = `${e.name}: ${e.message}`;
}

{ warning: seen[0], timesWarned: seen.length, error };

Every error is an SdvplotError. InputError means the call itself is wrong (here an unknown league, which the types also reject); DownloadError is an OfflineError you can catch to fall back to the bundled data; and UnsupportedTargetError means the code ran somewhere it cannot draw.

{
  "loadLeague(\"nfll\")": {
    "name": "InputError",
    "instanceof InputError": true,
    "instanceof OfflineError": false,
    "instanceof UnsupportedTargetError": false,
    "message": "unknown league \"nfll\"; known leagues: aaf, ahl, cfb, cricket, echl, mbb, milb, mlb, nba, nbagl, ncaa_baseball, ncaa_mhockey, ncaa_softball, ncaa_whockey, nfl, nhl, ohl, phf, pwhl, qmjhl, soccer, ufl, usfl, ushl, wbb, whl, wnba, xfl"
  },
  "marks(\"KC\", \"nfl\", { full: true }) during an outage": {
    "name": "DownloadError",
    "instanceof InputError": false,
    "instanceof OfflineError": true,
    "instanceof UnsupportedTargetError": false,
    "url": "https://sdv.nyc3.cdn.digitaloceanspaces.com/assets/public/manifest/marks.csv",
    "status": 503,
    "message": "manifest download failed: https://sdv.nyc3.cdn.digitaloceanspaces.com/assets/public/manifest/marks.csv answered 503"
  },
  "logoWatermarks([\"PHI\", \"KC\"], …) in Node": {
    "name": "UnsupportedTargetError",
    "instanceof InputError": false,
    "instanceof OfflineError": false,
    "instanceof UnsupportedTargetError": true,
    "message": "logo watermarks needs a DOM: build it in the browser (onMount, $effect, an Astro client:only island), not during SSR"
  }
}
import {
DownloadError,
InputError,
OfflineError,
SdvplotError,
UnsupportedTargetError,
loadLeague,
marks,
} from "@sportsdataverse/sdvplot";
import { logoWatermarks } from "@sportsdataverse/sdvplot/chartjs";

const caught = async (f: () => unknown) => {
try {
await f();
return "no error";
} catch (e) {
if (!(e instanceof SdvplotError)) throw e;
return {
name: e.name,
"instanceof InputError": e instanceof InputError,
"instanceof OfflineError": e instanceof OfflineError,
"instanceof UnsupportedTargetError": e instanceof UnsupportedTargetError,
...(e instanceof DownloadError ? { url: e.url, status: e.status } : {}),
message: e.message,
};
}
};

// The only download in sdvplot is the full logo manifest; this fetch answers as a CDN outage would. Catch
// OfflineError (DownloadError is one) to fall back to the bundled marks.
const outage = async (): Promise<Response> => new Response("", { status: 503 });

{
// @ts-expect-error: the league is checked at compile time too
'loadLeague("nfll")': await caught(() => loadLeague("nfll")),
'marks("KC", "nfl", { full: true }) during an outage': await caught(() =>
marks("KC", "nfl", { full: true, fetch: outage }),
),
// the Chart.js image plugins build <img> elements: in Node (SSR) there is no DOM to build them in
'logoWatermarks(["PHI", "KC"], …) in Node': await caught(() =>
logoWatermarks(["PHI", "KC"], { league: "nfl" }),
),
};

In the gallery: Identity, colours, marks.