Skip to main content

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.

week12345789101112131415161718scoring margin−40−30−20−101020
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.

point differential−100−5050100150
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.