ShotMap
Half-pitch shot map with publishable defaults. The hero fixture uses 14 real StatsBomb open-data shots normalized through @withqwerty/campos-adapters, then rendered with the default Opta-style editorial encoding.
Interactive props.
Canonical behaviors.
Callback-first marker styling can push the chart toward an analytical read without changing the shot payload.
Same analytical preset, but the glyph system now distinguishes body part rather than shot context.
When xG is missing, marker size falls back cleanly and the chart drops the size scale without breaking the outcome read.
Static export uses the dedicated SVG path with a fixed outline pitch treatment. Hover UI disappears, but the pitch and markers stay deterministic.
Pitch drawn, honest copy, and no fake marks when nothing is plottable.
Shared concerns.
Data Contract
ShotMap expects canonical Campos shots. Adapter ownership stays outside
the component, so the page examples only cover rendering choices.
Shot target (endX / endY)
Optional fields on each Shot place the ball after the attempt in Campos
pitch space (0–100). Adapters populate them when the raw feed includes an end location:
StatsBomb uses shot.end_location;
Opta and WhoScored use qualifiers 140/141 (same
as pass end coordinates) when present; Wyscout uses the second
positions[] point on shot events. Not every shot in every feed includes
target data—lines only render when both coordinates are present.
Interaction
The live component owns hover and focus tooltips per shot marker. Static export
uses
ShotMapStaticSvg, which intentionally removes that UI layer.
Migration note
The built-in analytical default now uses colorScale="magma" instead of "turbo". If you need the earlier
StatsBomb-style output, set
colorScale="turbo" explicitly.
Themeability
Pitch theming comes from Stadia. Use the floating controller for the built-in
surface presets, and use pitchColors only when the chart needs a custom
palette.
Composition
ShotMap owns the chart card, optional header stats, scale, legend, and
tooltip. Wrap it in layout chrome if needed, but do not rebuild its internal pitch
semantics outside the component.
Normal, small, smallest.
Pitch-based charts scale fluidly to whatever width the parent gives them. The 'smallest' cell is sized for a 5×4 small-multiples grid (~140px wide). Vertical orientation only — horizontal pitches are too wide for small-multiples.
Best-practice examples.
Minimal usage
Keep provider parsing outside the component. Zero-config defaults already produce a publishable half-pitch shot map.
import { ShotMap } from "@withqwerty/campos-react";
import type { Shot } from "@withqwerty/campos-schema";
type Props = {
shots: Shot[];
};
export function TeamShotMap({ shots }: Props) {
return <ShotMap shots={shots} />;
}
Analytical encoding
Use callback-first style props when the chart needs a different reading, but keep the data shape unchanged.
import { ShotMap } from "@withqwerty/campos-react";
import type { Shot } from "@withqwerty/campos-schema";
type Props = {
shots: Shot[];
};
export function TeamShotMapAnalytical({ shots }: Props) {
return (
<ShotMap
shots={shots}
preset="statsbomb"
markers={{
fill: ({ shot }) => {
const xg = shot.xg ?? 0;
if (xg > 0.4) return "#dc2626";
if (xg > 0.2) return "#2563eb";
return "#16a34a";
},
shape: {
by: ({ shot }) => shot.bodyPart ?? "other",
values: {
head: "triangle",
other: "square",
},
fallback: "circle",
},
}}
trajectories={{
stroke: { by: ({ shot }) => shot.outcome, values: { goal: "#dc2626", blocked: "#2563eb" } },
strokeDasharray: { by: ({ shot }) => shot.outcome, values: { blocked: "4 3" } },
}}
/>
);
}
Public surface.
| Prop | Type | Default | Description |
|---|---|---|---|
shots | readonly Shot[] | required | Normalized shot data. Keep provider parsing outside the component and pass canonical Campos shots in. |
preset | "opta" | "statsbomb" | "opta" | High-level visual encoding preset. Opta is the editorial default; StatsBomb switches to analytical color/shape semantics. |
colorScale | "magma" | "cividis" | "turbo" | "coolwarm" | "magma" | xG color ramp used by the built-in analytical presets. Since the 2026-04 chart-library pass, the analytical default is magma instead of turbo; pass `colorScale="turbo"` to retain the earlier output. |
markers | ShotMapMarkersStyle | — | Callback-first marker styling surface for fill, fillOpacity, stroke, strokeWidth, opacity, size, and shape. Supports constants, object maps, and callbacks. |
showHeaderStats | boolean | false | Summary row (shots, goals, total xG) above the chart. Off by default so the pitch stays primary in tight layouts. |
showScaleBar | boolean | true | Controls the analytical xG color bar when the built-in preset exposes one. Useful for compact small-multiple tiles and dense comparison grids. |
showShotTrajectory | boolean | true | Lines from shot origin to endX/endY when the shot payload includes a target position. Toggle off for a cleaner scatter or when end data is noisy. |
trajectories | ShotTrajectoryStyle | — | Callback-first styling surface for shot target lines: show, stroke, strokeWidth, opacity, strokeDasharray, strokeLinecap. Defaults follow the pitch theme when stroke is omitted. |
pitchTheme | "primary" | "secondary" | "primary" | Delegated pitch surface preset from Stadia. |
pitchColors | PitchColors | — | Direct pitch color overrides for branded or editorial treatments. |
Create a React component using Campos ShotMap. Import ShotMap from @withqwerty/campos-react, keep provider parsing outside the component, show the smallest good usage first, then show one callback-first styling variant using markers and trajectories.