PassSonar
Single-subject directional pass profile. Eight wedges encode attempted volume; an inner concentric wedge encodes completion. Forward (toward the opposition goal) sits at 12 o'clock by convention.
Canonical behaviors.
Two midfielders sharing scaleMaxAttempts so the wedge sizes are honestly comparable.
Same player, different period — sharing scaleMaxAttempts surfaces the tactical shift.
A centre-back's first-half passes — most of the wheel is empty by design. The chart should not balance the bins.
No valid passes renders the shared empty-state pill, not a zeroed wheel.
Use this when the chart already has external chrome (a player card, a table cell).
Composition patterns.
Four player sonars sharing one consumer-computed scaleMaxAttempts. The grid layer ignores pitch view hints — PassSonar is a non-pitch radial chart.
Use the shared chart-frame note slot for sample, role, or encoding context instead of building prose around the card.
Shared concerns.
When to use PassSonar
A single subject's directional pass profile, summarised. Reading is forward at the top, attempted vs completed at a glance. Best for player or team comparison cards and small-multiples scouting grids.
When to reach for PassMap instead
When the story is the individual passes themselves — recipients, length, location on the pitch — not their direction distribution. PassMap shows raw passes, PassSonar aggregates them.
When to reach for PassFlow instead
When the story is how a team moves the ball through different pitch zones. PassFlow bins by pitch zone and aggregates direction within each zone; PassSonar bins by direction for one subject across the whole pitch.
When to reach for PassNetwork instead
When the story is the team shape — average positions and the most-frequent passing edges between teammates. PassNetwork is a team-structure view; PassSonar is a single-subject distribution view.
Width pressure.
At small widths the auto direction-labels switch off so wedges remain readable.
Theme-aware fills, summary text, and tooltip — wrapped together inside one React island so the Astro nested-island bug stays fixed.
Best-practice examples.
Minimal usage
Pass an array of canonical PassEvent objects for one subject. The chart bins by direction and encodes attempted + completed automatically.
import { PassSonar } from "@withqwerty/campos-react";
import type { PassEvent } from "@withqwerty/campos-schema";
type Props = { passes: PassEvent[]; subjectLabel: string };
export function PlayerPassSonar({ passes, subjectLabel }: Props) {
return <PassSonar passes={passes} subjectLabel={subjectLabel} />;
}
Side-by-side comparison
Use scaleMaxAttempts to share a radial scale across cards so volume reads honestly.
import { PassSonar } from "@withqwerty/campos-react";
export function MidfielderComparison({ a, b }) {
const scale = Math.max(maxBin(a.passes), maxBin(b.passes));
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
<PassSonar passes={a.passes} subjectLabel={a.label} scaleMaxAttempts={scale} />
<PassSonar passes={b.passes} subjectLabel={b.label} scaleMaxAttempts={scale} />
</div>
);
}
Subject discriminant
Pass `subjectId` to enforce the one-subject invariant. Mismatched events are dropped and surface as a warning in the chart frame.
<PassSonar
passes={matchPasses}
subjectId="332325" // Declan Rice — drops everyone else
subjectKind="player"
subjectLabel="Declan Rice"
/>
Public surface.
| Prop | Type | Default | Description |
|---|---|---|---|
passes | ReadonlyArray<PassEvent> | — | Canonical pass events. Filter to a single subject upstream, or use `subjectId` to enforce. |
subjectLabel | string | — | Display label for the chart's centre summary block. |
subjectId | string | — | Optional discriminant. When present, mismatched events are dropped and counted as a warning. |
subjectKind | "player" | "team" | "player" | Which PassEvent field `subjectId` matches against. |
scaleMaxAttempts | number | auto | Explicit shared radial scale. Values <1, NaN, or Infinity auto-scale and emit a warning. Non-integer values round up. |
showLegend | boolean | true | Toggle the attempted/completed legend. |
showSummary | boolean | true | Toggle the central summary block (subject + counts + completion %). |
directionLabels | "compass" | "cartesian" | false | "compass" | `"compass"` prints a bearing label at each canonical direction. `"cartesian"` prints four static axis labels (Toward Goal / Away / 90° Left / 90° Right). `false` hides labels — useful for small-multiples grids and thumbnails. |
directionLabelsText | PassSonarDirectionLabelsText | built-in English defaults | Optional overrides for the four cartesian axis labels. Provide an empty string to hide a specific axis. |
showGuide | boolean | true | When false, hides the outer guide ring and the inner hub disc. Produces a minimal wedges-only look that matches Scout Lab / mplsoccer idioms. |
wedges | PassSonarWedgesStyle | — | First-class styling for attempted/completed fills, opacity, stroke. Constant, map, or callback per StyleValue. |
text | PassSonarTextStyle | — | Direction-label and centre-summary text styling. Slot is exposed via the callback context. |
seriesColors | ThemePalette | ["#3b82f6", "#22c55e"] | Two-colour palette: index 0 = attempted fill, index 1 = completed overlay. |
methodologyNotes | ChartMethodologyNotes | — | Shared chart-frame note slot for sample/role/methodology context. |
`methodologyNotes` is supported in the live React surface. The static export contract stays bounded to the constant style props and the chart's data inputs; callbacks and methodology-notes content do not survive `createExportFrameSpec`.
Create a React component using Campos PassSonar. Import PassSonar from @withqwerty/campos-react and PassEvent from @withqwerty/campos-schema. Filter passes to one subject upstream (or pass subjectId). Default size is publishable; use scaleMaxAttempts only when you need cross-chart comparability.