Coordinates & pitch types
Provider coordinate systems, the transform pipeline, and how to move data between them.
Every football data provider draws its pitch on a different grid. PitchKit makes the grid an
explicit choice — the type prop on <Pitch> — and feeds every layer through one transform
pipeline, so your data goes in in its provider's native units and everything stays
aligned.
Supported pitch types
type | Extent | Origin | y direction | Notes |
|---|---|---|---|---|
statsbomb | 120 × 80 | top-left | down | Abstract units |
opta | 100 × 100 | bottom-left | up | Normalised percentage grid |
uefa | 105 × 68 | bottom-left | up | Real metres |
skillcorner | 105 × 68 | centre spot | up | Real metres, per-match extent |
wyscout | 100 × 100 | top-left | down | Normalised percentage grid |
metrica | 1 × 1 | top-left | down | Normalised 0..1 grid |
impect | 105 × 68 | centre spot | up | Real metres, standardised |
<Pitch type="statsbomb">{/* data in 120x80, y-down */}</Pitch>
<Pitch type="opta">{/* data in 0-100, y-up */}</Pitch>
<Pitch type="skillcorner">{/* metres from the centre spot */}</Pitch>
<Pitch type="wyscout">{/* data in 0-100, y-down */}</Pitch>
<Pitch type="metrica">{/* data in 0-1, y-down */}</Pitch>
<Pitch type="impect">{/* metres from the centre spot, 105x68 */}</Pitch>opta and wyscout are both 0–100 on both axes, but not interchangeable: Opta's origin is
bottom-left with y increasing upward, Wyscout's is top-left with y increasing downward.
Plotting one provider's data on the other's type mirrors the pitch vertically, and nothing
errors — every coordinate is still in range. metrica has Wyscout's orientation on a 0..1
grid instead, so Metrica data on wyscout crowds into the top-left corner. mplsoccer calls
this type metricasports.
A percentage grid renders at the real pitch's proportions, not as a square, because the transform converts each axis to metres before deriving the pitch's on-screen shape — x spans 105 m of grass and y only 68 m, even though both run 0–100. The markings still don't scale with an axis override; see below.
skillcorner is the one grid whose origin is not a corner: its coordinates are metres
measured from the centre spot, so x runs -52.5..52.5 and y -34..34. Layers take that
raw, with no lifting step — see the data loaders.
It is also the one grid whose real-world extent varies. SkillCorner pitches are the stadiums’ actual sizes (104, 105 and 106 m across their open data), so pass the match’s own values when you want the outline exact:
<Pitch type="skillcorner" dimensions={{ length: match.pitch_length, width: match.pitch_width }} />The markings deliberately do not scale with it — a penalty area is 16.5 m deep on any pitch
— so only the outline, halfway line and goal lines move. Overriding a normalised grid like
opta throws instead, because a percentage grid has no metres to set.
Marking positions (box sizes, penalty spots, circle radii) are sourced from mplsoccer's published per-provider constants rather than derived from real-world metres — real event data is published against those exact landmark values, and deriving our own would silently misalign rendered markings against it.
Orientation and cropping
Display orientation is a view concern, not a fact about the data — the same StatsBomb coordinates render horizontally or vertically:
import { cropForHalf, getPitchDimensions } from "@pitchkit/core";
const dims = getPitchDimensions("statsbomb");
// Attacking-half shot map framing:
<VerticalPitch type="statsbomb" crop={cropForHalf(dims)}>
<Scatter data={shots} x={(s) => s.x} y={(s) => s.y} />
</VerticalPitch>;crop takes any { x0, y0, x1, y1 } window in provider units; cropForHalf is the
convenience for the most common one.
Converting between providers
createStandardizeTransform maps coordinates from one provider grid to another (mplsoccer's
Standardizer) — useful when combining data sources:
import { createStandardizeTransform, getPitchDimensions } from "@pitchkit/core";
const optaToStatsBomb = createStandardizeTransform(
getPitchDimensions("opta"),
getPitchDimensions("statsbomb"),
);
const [x, y] = optaToStatsBomb([50, 50]); // -> [60, 40]Pixels and back
Inside a <Pitch>, usePitch() exposes the active
pixel transform: transform.toPixel for provider → screen, and transform.toProvider for
screen → provider — the inverse you need to ingest clicks or pointer positions as pitch
coordinates.