PassSonar is always drawn in the attack-adjusted frame; heading-frame is deferred
Pass angles on `PassSonar` are always computed from canonical-frame start/end points (`atan2(endY - y, endX - x)`), so 0 rad always points toward the opposition goal. This is the "adjusted" convention some references call out. Heading-frame ("regular") sonars require tracking data and are deferred until a tracking adapter lands.
Context
Two sonar frame-of-reference conventions exist in public work:
- Attack-adjusted / pitch-frame — 0 rad (the top of the sonar) always
points toward the opposition goal. A “forward” bar means “toward the
goal the player is attacking”, regardless of which way the team is
shooting that half. This is what StatsBomb’s
pass.angleencodes, what mplsoccer’sPitch.sonarproduces, and what McKinley’s reference (/Volumes/WQ/ref_code/PassSonar/) renders viacoord_polar(start=pi, direction=1)on attacker-perspective data. - Heading-frame / body-frame — 0 rad points in the direction the passer’s body is facing at the moment of the pass. A “forward” bar means “the ball went where the player was facing”. This requires per-event heading data, which is only available in tracking-data products (or hybrid tracking+event feeds).
Some consumers use “adjusted” to mean the first form and “regular” to mean the second. The terminology isn’t standardised, so we pin down the frame in this decision doc.
Decision
Campos PassSonar is always drawn in the attack-adjusted frame. Specifically:
- Compute layer derives the angle from canonical start/end points:
atan2(pass.endY - pass.y, pass.endX - pass.x)(packages/react/src/compute/pass-sonar.ts:246). - Campos canonical coordinates are attacker-perspective with
x = 0own goal andx = 100opposition goal (seecanonical-pitch-coordinates.md).atan2on canonical-frame deltas therefore always yields “0 rad = toward opposition goal”. - Adapters never read a provider’s
pass.anglefield. Provider conventions vary (StatsBomb pitch-frame, Opta raw absolute, tracking-derived body-frame) and passing any of them through unchanged leaks that ambiguity into the rendered chart. We always recompute.
No frame prop ships in v0.2. Adding a single-valued prop for
forward-compatibility is code smell; the invariant is documented here
and in JSDoc on ComputePassSonarInput and PassSonarProps instead.
Heading-frame is deferred
Heading-frame (“regular”) sonars are a distinct analytical product and will land with the tracking-adapter work, not as a retrofit onto event data. When that ships:
- Add
frame: "attacking" | "heading"toPassSonarPropsandComputePassSonarInput, defaulting to"attacking"(no behaviour change for existing consumers). - Under
frame: "heading", compute angle fromatan2(endY - y, endX - x) - headingAtPass. RequiresheadingAtPasson thePassEventschema, which only tracking adapters will supply. - If
frame: "heading"is requested butheadingAtPassis missing, emit amissing-headingwarning and fall back to attack-adjusted.
Why no runtime detection of “adjusted vs regular”
The initial instinct was to have the renderer recognise which frame the input is in and adjust. That’s the wrong seam:
- Ingest-layer canonicalisation (coordinate frame) handles it already.
Any
PassEventreaching compute has canonical coordinates by contract — the adjusted-frame angle drops out of the maths. - Runtime detection would have to infer intent from statistical regularities (e.g. “most players pass forward → the mode bin must be forward”), which breaks on goalkeepers, centre-backs under pressure, and low-sample cases.
- It also duplicates responsibility that the coordinate-invariants doc already places on adapters.
Consequences
- JSDoc on
PassSonarPropsandComputePassSonarInputgains an explicit “angles are computed in the attack-adjusted frame” note with a pointer to this decision doc. - PassSonar spec (
docs/specs/pass-sonar-spec.md) gains an “Frame of reference” section documenting the invariant and the deferred heading-frame extension. - No code change today. The existing implementation is already correct; this doc just nails down the contract so future tracking-adapter work knows the seam.
- A future
frame: "heading"option will be additive and default-safe; no migration will be required for consumers who don’t opt in.