Opta adapter

Opta: Classic F24 event arrays.

Use this adapter when your raw data is Opta F24-shaped event arrays — plain objects with typeId, qualifiers, contestantId, and x/y coordinates. That's what older F24 XML feeds decode to and what most existing Opta pipelines produce. The adapter handles direction flipping between halves, qualifier decoding, and the squads.json join for player labels. If you're on the modern Stats Perform JSON API instead, use the Stats Perform adapter below — same underlying data model, different envelope.

Input shape
Opta F24 event stream + MatchContext; type-34 lineup events + squads.json for team sheets.
Status
Primary provider
Raw feed
Opta
Provider-shaped. Their coordinates, their qualifier codes, their outcome strings.
Messy
Campos adapter
fromOpta.shots(raw, ctx)
Flips direction, standardises outcomes, filters own-goals and shootouts, resolves player labels.
One call
Typed data
ShotEvent[]
Drop into <ShotMap />, your own React view, a server-side report, or an analysis script.
Ready
Opta vs Stats Perform — same company, different envelope

Opta was acquired into Perform Group in 2013 and the merger with Stats LLC in 2019 created Stats Perform — so "Opta data" and "Stats Perform data" are the same event model. Use this adapter for the classic F24-shaped event arrays; use Stats Perform for the modern MA1/MA3 JSON API. They produce identical canonical output.

Get it running

One import, one call per surface.

import { fromOpta } from "@withqwerty/campos-adapters";

const events = fromOpta.events(rawEvents, matchContext);
const shots = fromOpta.shots(rawEvents, matchContext);
const passes = fromOpta.passes(rawEvents, matchContext);
const lineups = fromOpta.matchLineups(
  { home: homeLineupEvent, away: awayLineupEvent },
  { squads, matchId },
);
const formation = fromOpta.formations(lineupEvent, { squads });
What this adapter ships

The calls you can make today.

Shipped
events() returns Event[]

Every recognised match event in a canonical shape.

Shipped
shots() returns ShotEvent[]

Just the shots, ready to plot.

Shipped
passes() returns PassEvent[]

Pass trajectories with start, end, and result.

Shipped
matchLineups() returns MatchLineups

Home and away team sheets with starters and bench.

Shipped
formations() returns FormationTeamData

Kickoff tactical shape for one side.

What you can build with this

Charts that drop straight onto this adapter's output.

Supported cards plot from this adapter with no extra work. Partial cards plot, but read the scope note. Dimmed cards need a surface this provider doesn't ship.

The direction problem, solved

Why a second-half away shot at raw (18, 30) becomes canonical (82, 70).

Opta's coordinates are stored from the perspective of the team attacking in that half — not the team taking the action. The adapter reads MatchContext.periodDirections, flips x and y when needed, and hands back coordinates already in the canonical frame where attackers shoot rightward. No axis-flipping in your chart code.

{
  "raw": {
    "periodId": 2,
    "contestantId": "away",
    "x": 18,
    "y": 30
  },
  "normalized": {
    "x": 82,
    "y": 70,
    "period": 2,
    "outcome": "off-target"
  }
}
Current scope

What's honest about this adapter today.

Event coverage

Full canonical event kinds.

Shots, passes, tackles, interceptions, duels, clearances, fouls, goalkeeper actions, take-ons, recoveries, substitutions, and cards. Tackles and fouls share typeId 4 and are dispatched by outcome.

Lineups and formations

Kickoff-focused.

matchLineups() covers starters, bench ordering, captain, formation, shirt numbers, and squad-joined labels. It does not invent substitution minutes or explicit player coordinates — those require additional feeds Opta doesn't ship in type-34.

What it asks of you

Direction context, explicitly.

Every event-level call needs a MatchContext with period directions. The adapter won't guess it — direction is a match-level fact, not an event-level one, so it belongs in context, not in the event stream.

Lineage

What this adapter borrows from whom.

Concrete credits for the projects that did the underlying research or laid the reference tables we read from.

kloppy Python

Provider type-ID → canonical kind mapping, coordinate transformers, and direction-by-period normalisation patterns. Our reference for Opta, StatsBomb, Wyscout, Stats Perform, and Sportec.

mplsoccer Python

Pitch-standardisation conventions we cross-checked our canonical Campos coordinate frame against.

Raw example — what the payload looks like before and after
Raw F24 event
{
  "id": 1608522038,
  "eventId": 119,
  "typeId": 16,
  "periodId": 1,
  "timeMin": 13,
  "timeSec": 0,
  "contestantId": "4t83rqbdbekinxl5fz2ygsyta",
  "playerId": "dq7sreeigwha462ih1rx93105",
  "playerName": "M. Layún",
  "outcome": 1,
  "x": 88.7,
  "y": 46,
  "qualifier": [
    {
      "qualifierId": 17
    },
    {
      "qualifierId": 20
    },
    {
      "qualifierId": 22
    },
    {
      "qualifierId": 56,
      "value": "Center"
    },
    {
      "qualifierId": 77
    },
    {
      "qualifierId": 102,
      "value": "53.3"
    },
    {
      "qualifierId": 103,
      "value": "22.2"
    },
    {
      "qualifierId": 113
    },
    {
      "qualifierId": 193,
      "value": "2"
    },
    {
      "qualifierId": 213,
      "value": "0.24"
    }
  ]
}
Match context
{
  "matchId": "duklz4bwjxf7gne6j5gq20qnd",
  "homeTeamId": "ehd2iemqmschhj2ec0vayztzz",
  "awayTeamId": "4t83rqbdbekinxl5fz2ygsyta",
  "periods": {
    "firstHalf": {
      "homeAttacksToward": "decreasing-x"
    },
    "secondHalf": {
      "homeAttacksToward": "increasing-x"
    }
  }
}
Canonical output
[
  {
    "kind": "shot",
    "id": "duklz4bwjxf7gne6j5gq20qnd:1608522038",
    "matchId": "duklz4bwjxf7gne6j5gq20qnd",
    "teamId": "4t83rqbdbekinxl5fz2ygsyta",
    "playerId": "dq7sreeigwha462ih1rx93105",
    "playerName": "M. Layún",
    "minute": 13,
    "addedMinute": null,
    "second": 0,
    "period": 1,
    "x": 88.7,
    "y": 46,
    "xg": 0.24,
    "xgot": null,
    "outcome": "goal",
    "bodyPart": "right-foot",
    "isOwnGoal": false,
    "isPenalty": false,
    "context": "regular-play",
    "goalMouthY": 15.6,
    "goalMouthZ": 58.4,
    "provider": "opta",
    "providerEventId": "1608522038",
    "sourceMeta": {
      "typeId": 16,
      "eventId": 119,
      "outcome": 1
    }
  }
]
Compare providers