# Coordinates & pitch types

Source: https://www.pitchkitjs.com/docs/guides/coordinates

> 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          | Normalized 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        | Normalized percentage grid    |

```tsx
<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>
```

`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.

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](/docs/data/skillcorner/tracking).

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:

```tsx
<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 normalized 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:

```tsx
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:

```tsx
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()`](/docs/api/react/functions/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.
