dd8156376d
- buildStatsbombViz.ts: processes the 2022 WC Final (Argentina 3-3 France) from StatsBomb open data → shots+xG, cumulative xG race, starting-XI pass networks, totals, shootout result. Output committed (11KB) to avoid build-time fetch - Custom SVG viz (no chart lib): reusable Pitch (StatsBomb 120x80 coords), ShotMap (xG-sized dots, teams attacking opposite goals), XgRace (stepped cumulative xG with goal markers, d3-scale), PassNetwork (nodes ∝ involvement, links ∝ pass volume) - StoryPage: narrative with hero scoreline, by-the-numbers bars, and the three visualizations with explanatory copy - Verified in browser: all three render correctly, no console errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
// Shared domain types — used by the client, the server, and the build scripts.
|
||
|
||
export type Stage = 'group' | 'r32' | 'r16' | 'qf' | 'sf' | 'third' | 'final';
|
||
export type MatchStatus = 'scheduled' | 'live' | 'finished';
|
||
|
||
/** One side of a fixture: a known team, or an unresolved knockout placeholder. */
|
||
export interface TeamSlot {
|
||
/** Canonical team name when known, else null (unresolved knockout slot). */
|
||
team: string | null;
|
||
/** Raw openfootball placeholder: '2A' | 'W74' | 'L101' | '3A/B/C/D/F' | null. */
|
||
placeholder: string | null;
|
||
/** Display label: the team name, or a pretty placeholder ("Runner-up A"). */
|
||
label: string;
|
||
}
|
||
|
||
export interface Fixture {
|
||
/** Canonical 1..104 match number (group matches 1-72, knockout 73-104). */
|
||
num: number;
|
||
stage: Stage;
|
||
/** 'A'..'L' for group stage, else null. */
|
||
group: string | null;
|
||
/** Human round label from the source ("Matchday 1", "Round of 32", …). */
|
||
round: string;
|
||
/** 1 | 2 | 3 — the team's nth group game, else null. */
|
||
groupRound: number | null;
|
||
/** Kickoff as an ISO 8601 UTC timestamp. */
|
||
kickoff: string;
|
||
venue: string;
|
||
home: TeamSlot;
|
||
away: TeamSlot;
|
||
status: MatchStatus;
|
||
homeScore: number | null;
|
||
awayScore: number | null;
|
||
/** Live match minute when status === 'live'. */
|
||
minute: number | null;
|
||
}
|
||
|
||
export interface FixturesFile {
|
||
season: string;
|
||
generatedAt: string;
|
||
groups: string[];
|
||
venues: string[];
|
||
fixtures: Fixture[];
|
||
}
|
||
|
||
export interface StandingRow {
|
||
team: string;
|
||
played: number;
|
||
won: number;
|
||
drawn: number;
|
||
lost: number;
|
||
gf: number;
|
||
ga: number;
|
||
gd: number;
|
||
points: number;
|
||
/** 1-based rank within the group after sorting. */
|
||
rank: number;
|
||
}
|
||
|
||
/** What the server pushes to clients: fixtures with any live overlay + tables. */
|
||
export interface Snapshot {
|
||
season: string;
|
||
/** When the live layer last refreshed (ISO), or null if seed-only. */
|
||
updatedAt: string | null;
|
||
/** Which live source produced the current scores. */
|
||
source: 'seed' | 'football-data' | 'sofascore';
|
||
fixtures: Fixture[];
|
||
/** Group letter → ranked standings. */
|
||
tables: Record<string, StandingRow[]>;
|
||
}
|
||
|
||
// ---- model / predictions ----
|
||
|
||
/** Per-team tournament odds from the Monte Carlo simulation. */
|
||
export interface TeamOdds {
|
||
team: string;
|
||
winGroup: number;
|
||
qualify: number;
|
||
reachR16: number;
|
||
reachQF: number;
|
||
reachSF: number;
|
||
reachFinal: number;
|
||
champion: number;
|
||
}
|
||
|
||
export interface MatchProbs {
|
||
home: number;
|
||
draw: number;
|
||
away: number;
|
||
}
|
||
|
||
/** A single per-match prediction (group + resolved knockout games). */
|
||
export interface MatchPrediction {
|
||
num: number;
|
||
home: string;
|
||
away: string;
|
||
probs: MatchProbs;
|
||
lambdaHome: number;
|
||
lambdaAway: number;
|
||
topScore: { home: number; away: number; p: number };
|
||
}
|
||
|
||
/** A point on the championship-odds-over-time chart. */
|
||
export interface OddsHistoryPoint {
|
||
t: string;
|
||
label: string;
|
||
top: { team: string; champion: number }[];
|
||
}
|
||
|
||
export interface ModelSnapshot {
|
||
generatedAt: string;
|
||
/** Date the underlying ratings reflect (advances as results re-rate teams). */
|
||
asOf: string;
|
||
iterations: number;
|
||
matches: MatchPrediction[];
|
||
odds: TeamOdds[];
|
||
history: OddsHistoryPoint[];
|
||
}
|
||
|
||
// ---- data-story viz (StatsBomb open data, a fixed historical match) ----
|
||
|
||
export interface VizShot {
|
||
team: string;
|
||
player: string;
|
||
minute: number;
|
||
/** StatsBomb pitch coords: x 0–120 (attacking toward 120), y 0–80. */
|
||
x: number;
|
||
y: number;
|
||
xg: number;
|
||
outcome: string;
|
||
goal: boolean;
|
||
}
|
||
|
||
export interface XgPoint { minute: number; cum: number; }
|
||
|
||
export interface PassNode { player: string; x: number; y: number; passes: number; }
|
||
export interface PassEdge { a: string; b: string; count: number; }
|
||
export interface PassNetwork { players: PassNode[]; edges: PassEdge[]; }
|
||
|
||
export interface TeamTotals { xg: number; shots: number; goals: number; passes: number; passAccuracy: number; }
|
||
|
||
export interface StoryViz {
|
||
matchId: number;
|
||
competition: string;
|
||
stage: string;
|
||
date: string;
|
||
home: string;
|
||
away: string;
|
||
homeScore: number;
|
||
awayScore: number;
|
||
/** Final result line, e.g. "Argentina won 4–2 on penalties". */
|
||
result: string;
|
||
pitch: { length: number; width: number };
|
||
shots: VizShot[];
|
||
goals: { team: string; player: string; minute: number; xg: number }[];
|
||
xgRace: { home: XgPoint[]; away: XgPoint[] };
|
||
passNetwork: { home: PassNetwork; away: PassNetwork };
|
||
totals: { home: TeamTotals; away: TeamTotals };
|
||
}
|
||
|
||
// ---- WebSocket protocol ----
|
||
// The snapshot is small (~104 fixtures), so the server simply re-pushes the full
|
||
// snapshot on every change — no diffing, no client-side merge races. Predictions
|
||
// are heavier to compute, so they ride a separate message sent only when the set
|
||
// of finished results changes.
|
||
|
||
/** Server → client. */
|
||
export type ServerMessage =
|
||
| { t: 'snapshot'; snapshot: Snapshot }
|
||
| { t: 'predictions'; model: ModelSnapshot };
|