events()
returns Event[] Every recognised match event in a canonical shape.
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.
fromOpta.shots(raw, ctx) ShotEvent[] <ShotMap />, your own React view, a server-side
report, or an analysis script.
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.
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 }); events()
returns Event[] Every recognised match event in a canonical shape.
shots()
returns ShotEvent[] Just the shots, ready to plot.
passes()
returns PassEvent[] Pass trajectories with start, end, and result.
matchLineups()
returns MatchLineups Home and away team sheets with starters and bench.
formations()
returns FormationTeamData Kickoff tactical shape for one side.
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.
Plot events, shots, passes, and formations directly on the pitch.
Time-series and xG-driven views from shots().
These charts are adapter-independent — they take pre-aggregated inputs.
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"
}
} 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.
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.
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.
Concrete credits for the projects that did the underlying research or laid the reference tables we read from.
Provider type-ID → canonical kind mapping, coordinate transformers, and direction-by-period normalisation patterns. Our reference for Opta, StatsBomb, Wyscout, Stats Perform, and Sportec.
Pitch-standardisation conventions we cross-checked our canonical Campos coordinate frame against.
{
"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"
}
]
} {
"matchId": "duklz4bwjxf7gne6j5gq20qnd",
"homeTeamId": "ehd2iemqmschhj2ec0vayztzz",
"awayTeamId": "4t83rqbdbekinxl5fz2ygsyta",
"periods": {
"firstHalf": {
"homeAttacksToward": "decreasing-x"
},
"secondHalf": {
"homeAttacksToward": "increasing-x"
}
}
} [
{
"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
}
}
]