XGTimeline annotation tiers: goals always, big-xG non-goals up to 3 per 15 min, rest tooltip-only
Shot annotations follow a three-tier policy. Tier 1 — goals — always annotate. Tier 2 — non-goals with xG ≥ 0.3, capped at 3 per 15-minute window. Tier 3 — everything else — surfaces only in the tooltip. Stops dense matches (30+ shots) becoming unreadable without losing the story.
Context
Every shot is a marker on the curve; every marker is a candidate for an annotation label. Dense matches (Erling Haaland vs Luton, FIFA stoppage-time onslaughts) break any “label every shot” policy — labels collide, collision avoidance shifts markers off their position, and the reader loses the curve. Labelling only goals is safe but drops the analytically interesting “huge chance spurned” story. The middle ground is a tiered policy.
Decision
- Tier 1 — outcome is goal. Always annotated. No limit.
- Tier 2 — non-goal with xG ≥
0.3(TIER_2_XG_THRESHOLD). Up toTIER_2_MAX_PER_WINDOW = 3annotations per 15-minute calendar window (0–15, 15–30, 30–45, 45–60, 60–75, 75–90, 90+). Tier 2 within a crowded window sorts by xG desc; overflow falls to Tier 3. - Tier 3 — everything else. Marker renders; annotation does not. Tooltip still shows xG, outcome, and metadata.
Window boundaries are calendar minutes, not equal-count buckets — aligns annotation density with match clock.
Consequences
- Dense matches degrade gracefully without the consumer touching any prop.
- Thresholds are editorial constants, not a public API. Consumers who need per-chart control over density fork the annotation logic at the composition layer.
- Tier 2 sorting by xG means the “best missed chances” stay labelled even in crowded windows; noise shots get demoted first.
- Accessibility tree still includes every shot regardless of tier; the tiering is visual, not semantic.