Victory
Victory builds charts from React components and lets you swap the
component that draws each point or label. sdvplot gives it four pieces. teamTheme and teamColorPalette, in
@sportsdataverse/sdvplot/victory, colour a chart by team. <LogoPoint> and <LogoTick>, in
@sportsdataverse/sdvplot/react, draw team logos as a scatter's points and as an axis' tick labels. The colour helpers
never import victory (install victory, 37.3.6 or later, beside them: an optional peer), and all four are
synchronous, so load the league first (loadLeague). Each chart below is drawn by Victory in your browser.
Scoring margins, each opponent's logo at the end of its bar
The Chiefs' 2024 regular season, game by game: the bar is the scoring margin and the logo at its end is the opponent.
<LogoPoint league team /> is the dataComponent of a VictoryScatter: Victory hands it each point's position and
data row, and it draws the logo of the row's team field there, in place of the point's symbol. teamTheme({ league, teams, base }) is the chart's theme. Built over Victory's own VictoryTheme.clean, whose axes and fonts it keeps, it
gives a lone bar series the first team's colour.
import { KC_PHI_GAMES_2024 } from "@sportsdataverse/examples/data";
import { loadLeague } from "@sportsdataverse/sdvplot";
import { LogoPoint } from "@sportsdataverse/sdvplot/react";
import { teamTheme } from "@sportsdataverse/sdvplot/victory";
import { VictoryAxis, VictoryBar, VictoryChart, VictoryScatter, VictoryTheme } from "victory";
await loadLeague("nfl");
// Kansas City's 17 regular-season games: the margin (Chiefs' points less the opponent's) and who they played.
const games = KC_PHI_GAMES_2024.filter((g) => g.home_team === "KC" || g.away_team === "KC").map((g) => {
const home = g.home_team === "KC";
return {
week: g.week,
margin: home ? g.home_score - g.away_score : g.away_score - g.home_score,
opponent: home ? g.away_team : g.home_team,
};
});
if (games.length !== 17) throw new Error(`expected 17 Chiefs games, got ${games.length}`);
(
// teamTheme over Victory's clean theme: a lone bar series takes the first team's colour, the Chiefs' red
<VictoryChart
theme={teamTheme({ league: "nfl", teams: ["KC"], base: VictoryTheme.clean })}
width={640}
height={320}
domainPadding={{ x: 20, y: 24 }}
padding={{ top: 24, right: 24, bottom: 48, left: 56 }}
>
<VictoryAxis label="week" tickValues={games.map((g) => g.week)} offsetY={48} />
<VictoryAxis dependentAxis label="scoring margin" />
<VictoryBar data={games} x="week" y="margin" barWidth={14} />
{/* each opponent's logo at the end of its bar: 2 x size px tall */}
<VictoryScatter
data={games}
x="week"
y="margin"
size={10}
dataComponent={<LogoPoint league="nfl" team="opponent" />}
/>
</VictoryChart>
);
Victory hands a dataComponent pixels and no plot height, so the logo is sized in px: as tall as the point it
replaces, twice the scatter's size. Set size on the VictoryScatter. A height (the spec adapters' fraction of the
plot) throws. A team with no logo draws nothing and warns once, as the ECharts adapter does. Victory's event handlers
(tooltips, a voronoi container) stay on the logo. A wordmark markType, alpha, a season (a value or a field) and
embed (from embedSources) work as in the other adapters.
Logo tick labels on a dark background
<LogoTick league size /> is a VictoryAxis' tickLabelComponent: each tick label that names a team is drawn as the
team's logo, size px tall (default 24), on the side of Victory's label anchor the text would have run (under a bottom
axis, left of a left one). The logos stay upright: they do not rotate with a tick label's angle. A label with no
logo keeps its text, drawn by Victory's own VictoryLabel. <LogoTick>
loads that from victory when it first draws one, and warns once. A missing victory throws OptionalDependencyError
then. On a server render such a label is left out until the page's scripts run; the logos themselves render on the
server.
The bars take their colours from teamColorPalette(league, teams, { background }), one per team in order. On a dark
background a team colour under 3:1 contrast gives way to the team's other colour, or is lightened until it reads, and a
colour that would then sit too close to an earlier team's (CIE76 ΔE under 20) takes the team's other colour, or else
the colour nearest its own (its lightness, hue or chroma stepped) that is distinct and still reads; if none is, it
keeps the readable colour and warns once. Here Denver and
New England share one navy. The chart's own background and axis ink are yours to set (here in the axes'
style), or come with the base theme.
import { AFC_WEEKS_2024 } from "@sportsdataverse/examples/data";
import { loadLeague } from "@sportsdataverse/sdvplot";
import { LogoTick } from "@sportsdataverse/sdvplot/react";
import { teamColorPalette } from "@sportsdataverse/sdvplot/victory";
import { VictoryAxis, VictoryBar, VictoryChart } from "victory";
await loadLeague("nfl");
// Each team picked by id from the week-by-week margins (null is the bye); its season's point differential is their sum.
const teams = ["BUF", "KC", "LAC", "DEN", "MIA", "NYJ", "NE", "LV"];
const background = "#181a1b";
// every bar's colour keeps 3:1 against the background and stays apart from the others (DEN and NE share a navy)
const colors = teamColorPalette("nfl", teams, { background });
if (colors.length !== teams.length) throw new Error(`${colors.length} colours for ${teams.length} teams`);
const rows = teams.map((team, i) => {
const season = AFC_WEEKS_2024.find((s) => s.team === team);
if (season === undefined) throw new Error(`no 2024 weeks for ${team}`);
return { team, diff: season.margins.reduce((n: number, m) => n + (m ?? 0), 0), color: colors[i] };
});
const ink = { fill: "#e3e3e3", fontSize: 12 };
(
<VictoryChart
width={640}
height={340}
domainPadding={{ x: 30, y: 12 }}
padding={{ top: 24, right: 24, bottom: 56, left: 56 }}
style={{ parent: { background } }}
>
{/* the team axis sits at the bottom whatever the sign of the bars; each label is the team's logo, 32 px */}
<VictoryAxis
offsetY={56}
tickLabelComponent={<LogoTick league="nfl" size={32} />}
style={{ axis: { stroke: "#5f6368" } }}
/>
<VictoryAxis
dependentAxis
label="point differential"
style={{
axis: { stroke: "#5f6368" },
grid: { stroke: "#3a3d3f" },
tickLabels: ink,
axisLabel: { ...ink, padding: 40 },
}}
/>
<VictoryBar
data={rows}
x="team"
y="diff"
barWidth={36}
style={{ data: { fill: ({ datum }) => datum.color } }}
/>
</VictoryChart>
);
Theme or list
teamColorPalette has the shape of the ECharts, Recharts and Svelte adapters' teamColorPalette, (league, teams, options), and returns the list Victory takes as a colorScale. teamTheme is the Victory-native form of the same
colours: it writes them into a theme as the colorScale of every group, stack, pie and legend, so each child of a
VictoryGroup is drawn in its team's colour without a colorScale prop, and gives the first team's colour to a lone
line, area, bar or scatter. Pass the teams in the order the chart draws them. Everything else comes from base; without
one, Victory draws the axes unstyled.
React Native
These pieces target Victory on the web and have not been tried in React Native. victory-native 41 and later is a
different library, drawn with Skia, with no theme object and no dataComponent, so teamTheme, <LogoPoint> and
<LogoTick> (react-dom SVG) do not carry over. The colours teamColorPalette returns are plain hex strings, which a
victory-native series takes as its color, and logoUrlSync gives a logo's URL to load as a Skia image.
Reading back what was drawn
Every image carries the data-sdv-* attributes that drawnMarks from @sportsdataverse/sdvplot/testing reads, so a
test can render the chart (jsdom, or a browser) and check which teams were drawn where. x and y are Victory's pixel
positions: a point's centre, or a tick label's anchor. height reads in px, the size the logo was drawn at, because
Victory hands neither component a plot height.