Component

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.

Martín Zubimendi38 / 44 (86%)
Attempted passes
Completed passes
Stories

Canonical behaviors.

Side-by-side comparison

Two midfielders sharing scaleMaxAttempts so the wedge sizes are honestly comparable.

Martín Zubimendi38 / 44 (86%)
Attempted passes
Completed passes
Declan Rice26 / 39 (67%)
Attempted passes
Completed passes
First half vs second half

Same player, different period — sharing scaleMaxAttempts surfaces the tactical shift.

Zubimendi · 1st half38 / 44 (86%)
Attempted passes
Completed passes
No passes for Zubimendi · 2nd half
Attempted passes
Completed passes
Sparse subject

A centre-back's first-half passes — most of the wheel is empty by design. The chart should not balance the bins.

Saliba · 1st half37 / 40 (93%)
Attempted passes
Completed passes
Empty state

No valid passes renders the shared empty-state pill, not a zeroed wheel.

No passes for Bukayo Saka
Attempted passes
Completed passes
Summary off

Use this when the chart already has external chrome (a player card, a table cell).

Attempted passes
Completed passes
Recipes

Composition patterns.

Small multiples grid

Four player sonars sharing one consumer-computed scaleMaxAttempts. The grid layer ignores pitch view hints — PassSonar is a non-pitch radial chart.

Martín Zubimendi38 / 44 (86%)
Declan Rice26 / 39 (67%)
Jurriën Timber29 / 34 (85%)
William Saliba37 / 40 (93%)
Methodology note

Use the shared chart-frame note slot for sample, role, or encoding context instead of building prose around the card.

Jurriën Timber29 / 34 (85%)
Attempted passes
Completed passes
Wedge radius is √(attempted / scaleMax). Inner overlay is completed-only passes — wider inner = higher completion rate.
Cross-cutting

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.

Responsive

Width pressure.

Compact container

At small widths the auto direction-labels switch off so wedges remain readable.

Martín Zubimendi38 / 44 (86%)
Attempted passes
Completed passes
Dark theme

Theme-aware fills, summary text, and tooltip — wrapped together inside one React island so the Astro nested-island bug stays fixed.

Martín Zubimendi38 / 44 (86%)
Attempted passes
Completed passes
Usage

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"
/>
                  
API

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`.

Use with AI
LLM Prompt
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.