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.