Can Campos build this?
This is the main showcase for Campos. Each entry starts with a real football data visualisation published by an analyst or data team, breaks it into reusable Campos pieces, then composes and styles those pieces to match the original.
If a reference surface exposes a layout, styling, or composition gap, the gap is the work. That makes this route a better product test than isolated component demos.
Composition Pressure
The library has to hold together as one football surface, not a gallery of unrelated widgets.
Data Pressure
Real reference work reveals which adapter and chart seams are already strong enough for production-looking output.
Styling Pressure
Recreating published analyst work forces the callback styling model to prove it can handle editorial demands without bespoke one-off APIs.
1. Target Visualization

Original by @R_by_Ryo · Data: Opta
2. Component Breakdown
The recreation uses two Campos components: XGTimeline for the cumulative xG step chart, and two ShotMap instances (one per team) for the split pitch view. Here they are individually:
3. Styled Recreation
The same components composed and styled to match the reference. Custom layout code handles the score header, team stats summary, side-by-side shot maps, and shared outcome legend.
How this was composed
import { ShotMap, XGTimeline } from "@withqwerty/campos-react";
import type { Shot } from "@withqwerty/campos-schema";
// Outcome color palette
const OUTCOME_MARKERS = {
fill: ({ shot }) => {
const colors = {
goal: "#22883e", saved: "#e8943a",
blocked: "#999", "off-target": "#1a1a1a",
};
return colors[shot.outcome] ?? "#1a1a1a";
},
fillOpacity: () => 1,
shape: () => "circle",
};
// Shared half-pitch config
const halfPitchProps = {
crop: "half",
preset: "opta",
markers: OUTCOME_MARKERS,
showShotTrajectory: false,
showLegend: false,
showSizeScale: false,
showHeaderStats: false,
framePadding: 0,
pitchColors: { fill: "#fff", lines: "#ccc" },
};
export function MatchSummary({ homeShots, awayShots, allShots, meta }) {
return (
<div>
{/* xG Timeline */}
<XGTimeline
shots={allShots}
homeTeam={meta.homeTeamId}
awayTeam={meta.awayTeamId}
teamColors={["#c8102e", "#08285e"]}
showAreaFill={false}
/>
{/* Split shot map: two half-pitches joined */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<ShotMap
shots={awayShots}
side="attack"
attackingDirection="left" // goal faces left
{...halfPitchProps}
/>
<ShotMap
shots={homeShots}
side="attack"
attackingDirection="right" // goal faces right
{...halfPitchProps}
/>
</div>
</div>
);
}