# Quickstart

Source: https://www.pitchkitjs.com/docs/quickstart

> Build a real chart from real data — the move that produced England's goal in the Euro 2024 final.

By the end of this page you'll have plotted a real passage of play from a real match: the
move that produced England's goal in the **Euro 2024 final** — Pickford's throw out, Saka
driving a third of the pitch up the right, the cutback, and Cole Palmer's finish.

[Cole Palmer equalises for England — Spain v England, Euro 2024 final](https://www.youtube.com/watch?v=lBOS43RWfY0&t=315s) (video)

Nothing here is hardcoded. The match arrives over the network, the move is found by
filtering it, and the chart is a stack of PitchKit layers over the result.

```tsx
import { useEffect, useState } from "react";
import { Annotate, Arrows, Comet, Pitch, Scatter } from "@pitchkit/react";
import {
  fetchMatchEvents,
  isCarry,
  isGoal,
  isPass,
  shots,
} from "@pitchkit/data-providers/statsbomb";
import type {
  StatsBombCarry,
  StatsBombEvent,
  StatsBombPass,
  StatsBombShot,
} from "@pitchkit/data-providers/statsbomb";

/** Euro 2024 final — Spain 2–1 England, Berlin, 14 July 2024. */
const EURO_2024_FINAL = 3943043;

interface Chain {
  passes: StatsBombPass[];
  carries: StatsBombCarry[];
  goal: StatsBombShot;
}

/**
 * The possession that produced England's goal, cut off at the goal itself.
 *
 * StatsBomb stamps every event with a `possession` number and the team that
 * owned it, so the move is a filter rather than a reconstruction. Two
 * details do the real work:
 *
 * - A possession does **not** end at the shot — it runs on until the ball
 *   changes hands, so it has to be truncated at the goal itself.
 * - A possession contains the *other* team's events too (pressures, blocks,
 *   an interception that didn't stick), so it's filtered down to the team
 *   that owned it.
 */
function goalChain(events: StatsBombEvent[]): Chain | undefined {
  const goal = shots(events)
    .filter(isGoal)
    .find((shot) => shot.team.name === "England");
  if (!goal) return undefined;

  const possession = events.filter((event) => event.possession === goal.possession);
  const upToGoal = possession.slice(0, possession.indexOf(goal) + 1);
  const attacking = upToGoal.filter((event) => event.team.id === event.possession_team.id);

  return {
    passes: attacking.filter(isPass),
    // A unit or two is a touch adjustment, not progression — and it
    // renders as a speck rather than a trail. (StatsBomb x/y are abstract
    // units on a 120 x 80 grid, not metres.)
    carries: attacking
      .filter(isCarry)
      .filter((carry) => Math.hypot(carry.endX - carry.x, carry.endY - carry.y) > 2),
    goal,
  };
}

/**
 * The quickstart's finished chart: load a real match, isolate the move that
 * produced a goal, and plot it — passes as arrows, carries as comet trails,
 * the goal as a labelled marker.
 */
export function QuickstartChainBasic() {
  const [chain, setChain] = useState<Chain | undefined>();
  const [failed, setFailed] = useState(false);

  useEffect(() => {
    fetchMatchEvents(EURO_2024_FINAL)
      .then((events) => setChain(goalChain(events)))
      .catch(() => setFailed(true));
  }, []);

  return (
    <div>
      <p className="mb-3 text-xs text-fd-muted-foreground">
        {failed
          ? "Couldn't reach StatsBomb open data."
          : chain === undefined
            ? "Fetching Spain 2–1 England from StatsBomb open data (~3 MB)…"
            : `${chain.passes.length} passes · ${chain.carries.length} ${chain.carries.length === 1 ? "carry" : "carries"} · ${chain.goal.player?.name ?? "Unknown"}, ${chain.goal.minute}'`}
      </p>

      <Pitch type="statsbomb">
        <Comet
          data={chain?.carries ?? []}
          x={(carry) => carry.x}
          y={(carry) => carry.y}
          x2={(carry) => carry.endX}
          y2={(carry) => carry.endY}
          gradient
          endWidth={5}
          tooltip={(carry) => `${carry.player?.name ?? "Unknown"} — carry`}
        />
        <Arrows
          data={chain?.passes ?? []}
          x={(pass) => pass.x}
          y={(pass) => pass.y}
          x2={(pass) => pass.endX}
          y2={(pass) => pass.endY}
          strokeWidth={2}
          strokeOpacity={0.85}
          tooltip={(pass) => `${pass.player?.name ?? "Unknown"} — pass`}
        />
        <Scatter
          data={chain?.passes ?? []}
          x={(pass) => pass.x}
          y={(pass) => pass.y}
          r={3}
          stroke="white"
          strokeWidth={1.5}
          tooltip={(pass) => pass.player?.name ?? "Unknown"}
        />
        <Scatter
          data={chain ? [chain.goal] : []}
          x={(goal) => goal.x}
          y={(goal) => goal.y}
          r={6}
          fill="var(--pitch-marker-goal)"
          stroke="white"
          strokeWidth={2}
          tooltip={(goal) =>
            `${goal.player?.name ?? "Unknown"} — goal, ${goal.shot.statsbomb_xg.toFixed(2)} xG`
          }
        />
        <Annotate
          data={chain ? [chain.goal] : []}
          x={(goal) => goal.x}
          y={(goal) => goal.y}
          label={(goal) => `Goal · ${goal.shot.statsbomb_xg.toFixed(2)} xG`}
          offsetY={-12}
        />
      </Pitch>
    </div>
  );
}
```

## Install

`react` has the components you render, `core` has the coordinate transforms underneath
them, and `data-providers` is a convenience library for handling data.

```bash
npm install @pitchkit/core @pitchkit/react @pitchkit/data-providers
```

## Step 1 — load a match

The Euro 2024 final is `3943043`; every fixture is discoverable through
`fetchCompetitions` and `fetchMatches` (see [Events](/docs/data/statsbomb/events)).

```ts
import { fetchMatchEvents } from "@pitchkit/data-providers/statsbomb";

const events = await fetchMatchEvents(3943043); // Spain 2–1 England
```

`events` is \~3,500 StatsBomb events in StatsBomb's own shape — nothing is renamed. The one
addition is that `location` arrays are lifted to `x` / `y` (and `end_location` to
`endX` / `endY`), so an accessor can read them directly.

<Callout type="warn">
  A match file is roughly 3 MB. Fetch it once and cache it — don't call `fetchMatchEvents` inside a
  render.
</Callout>

## Step 2 — draw a pitch

`<Pitch>` owns the coordinate system. Tell it which provider's grid your numbers are on and
it does the rest — StatsBomb's is 120 × 80, so nothing needs normalising. It's responsive by
default: it fills its container and keeps the pitch's aspect ratio. Passing explicit
`width`/`height` is available but not necessary.

```tsx
import { Pitch } from "@pitchkit/react";

<Pitch type="statsbomb" />;
```

## Step 3 — find the move

This is the part that's actually football rather than plumbing.

Start with the goal. `shots()` narrows the feed to shots, `isGoal` is an ordinary predicate,
so finding it is one chain of array methods:

```ts
import { isCarry, isGoal, isPass, shots } from "@pitchkit/data-providers/statsbomb";

const goal = shots(events)
  .filter(isGoal)
  .find((shot) => shot.team.name === "England");
if (!goal) return;
```

Now work backwards. StatsBomb stamps every event with a `possession` number and the team
that owned it, so "the move that led to this goal" is a filter:

```ts
const possession = events.filter((event) => event.possession === goal.possession);
const upToGoal = possession.slice(0, possession.indexOf(goal) + 1);
const attacking = upToGoal.filter((event) => event.team.id === event.possession_team.id);

const passes = attacking.filter(isPass);
const carries = attacking.filter(isCarry);
```

Two details there are easy to get wrong:

* **A possession doesn't end at the shot.** It runs on until the ball genuinely changes
  hands. A goal is the tidy case — this possession ends on Palmer's finish, so the `slice`
  costs nothing. Point the same code at a blocked shot or a save and the possession carries
  on through the rebound, and without the `slice` you'd draw all of it.
* **A possession contains the other team's events.** Pressures, blocks, an interception that
  didn't stick are all stamped with the same `possession` number. Comparing `team` against
  `possession_team` keeps the chain to the side building it.

## Step 4 — plot the move

Layers stack inside the `<Pitch>`, in paint order. Each one takes plain data plus **accessor
functions**:

```tsx
import { Arrows, Comet, Pitch, Scatter } from "@pitchkit/react";

<Pitch type="statsbomb">
  <Comet
    data={carries}
    x={(carry) => carry.x}
    y={(carry) => carry.y}
    x2={(carry) => carry.endX}
    y2={(carry) => carry.endY}
    gradient
  />
  <Arrows
    data={passes}
    x={(pass) => pass.x}
    y={(pass) => pass.y}
    x2={(pass) => pass.endX}
    y2={(pass) => pass.endY}
  />
  <Scatter
    data={[goal]}
    x={(g) => g.x}
    y={(g) => g.y}
    r={6}
    tooltip={(g) => `${g.player?.name} — goal, ${g.shot.statsbomb_xg.toFixed(2)} xG`}
  />
</Pitch>;
```

Saka's carry up the right is the one comet on the chart, and it's why carries are worth
drawing separately: as an arrow it would be indistinguishable from the long pass that
started the move.

Most props accept a static value *or* a per-datum function, so styling is data-driven
without a separate encoding step:

```tsx
<Scatter data={allShots} x={(s) => s.x} y={(s) => s.y} r={(s) => 3 + s.shot.statsbomb_xg * 9} />
```

## The finished chart

That's the example at the top of this page: five passes and one carry, goalkeeper to goal.
Pickford throws to Bellingham, Bellingham to Palmer, Palmer wide to Saka, Saka carries to
the byline and cuts it back, Bellingham bounces it into Palmer's path, and Palmer finishes
from the edge of the box at 0.04 xG.

Expand &#x2A;*[View Code](#quickstart-chain-basic)** on the preview for the complete component,
including the loading states and the carry filter.

## Where to go next

* **[Coordinates & pitch types](/docs/guides/coordinates)** — what `type="statsbomb"`
  actually sets, and how to plot Opta, UEFA, SkillCorner or your own grid.
* **[Overlays](/docs/overlays/scatter)** — every layer, one page each, with a live example.
* **[Data](/docs/data)** — the rest of `@pitchkit/data-providers`: StatsBomb
  [360 tracking](/docs/data/statsbomb/360) and [SkillCorner](/docs/data/skillcorner/tracking).
* **[Theming](/docs/styling/theming)** — the pitch is styled with CSS variables, so it
  inherits your app's look.
* **[Build with AI](/docs/agents)** — hand an agent the real API instead of letting it guess.
