statsEsc =window.statsEsc// Per-team rating systems + group letter (panna/offense/defense/epr/psr/elo/bt// and their rank_* columns), straight from the parquet._wchStrength = {try { returnawaitwindow.fetchParquet(window.DATA_BASE_URL+"football/wc2026_team_strength.parquet") }catch (e) { console.error("[wc-compare] team_strength load failed:", e);returnnull }}// Group fixtures: prob_home/draw/away + pred_*_goals, used to spot a direct// group meeting and to calibrate Tiento's goals scale._wchPredictions = {try { returnawaitwindow.fetchParquet(window.DATA_BASE_URL+"football/wc2026_predictions.parquet") }catch (e) { console.error("[wc-compare] predictions load failed:", e);returnnull }}// Tournament odds from the LIVE in-browser sim (wc-live-sim.js): real +// in-progress results baked in, re-run every minute while games are live.// _wchFull carries matchups (per-KO-round ties) + meta; _wchSim is teams[]._wchFull =window.wcLiveSim.fullStream()_wchSim =window.wcLiveSim.simStream()
Show code
// Tiento aggregate rating — z-blend of panna/EPR/PSR/Elo on a goals-above-// average scale (see wcMaps.computeTeamRating for weights + why BT is excluded).wchRating = {if (_wchStrength ==null) returnnullreturnwindow.wcMaps.computeTeamRating(_wchStrength, _wchPredictions)}
Show code
wchPick = {const wc =window.wcMapsconst known =newSet([...(_wchStrength || []).map(r => r.team),...(_wchSim || []).map(r => r.team) ])// canonicalise a raw hash value against the known team setconst resolve = (raw) => {if (!raw) returnnullif (known.has(raw)) return rawconst n = wc.normalizeWcTeam(raw)return known.has(n) ? n :null }let a =resolve(window._getHashParam("a"))let b =resolve(window._getHashParam("b"))// Default ordering by champion probability (live sim), else parquet order.const ranked = (_wchSim && _wchSim.length)? [..._wchSim].sort((x, y) => (y.p_champ??0) - (x.p_champ??0)).map(r => r.team): (_wchStrength || []).map(r => r.team)if (!a) a = ranked.find(t => t !== b) ??nullif (!b) b = ranked.find(t => t !== a) ??null// guard against A === Bif (a && b && a === b) b = ranked.find(t => t !== a) ??nullif (a && b) document.title=`${a} vs ${b} — World Cup 2026 — In The Game`return { a, b }}wchA = wchPick.awchB = wchPick.b// Per-team rows used by every section belowwchStrA = _wchStrength?.find(r => r.team=== wchA) ??nullwchStrB = _wchStrength?.find(r => r.team=== wchB) ??nullwchSimA = _wchSim?.find(r => r.team=== wchA) ??nullwchSimB = _wchSim?.find(r => r.team=== wchB) ??nullwchGroupA = wchStrA?.group?? wchSimA?.group??nullwchGroupB = wchStrB?.group?? wchSimB?.group??null
Show code
// ── Hero: two flags + names + champ %, with a "vs" divider ─────────{if (_wchStrength ==null&& _wchSim ==null) returnhtml`<h1>World Cup 2026 head to head</h1><p class="text-muted">Data failed to load — check the console.</p>`if (wchA ==null|| wchB ==null) returnhtml`<h1>World Cup 2026 head to head</h1><p class="text-muted">Loading…</p>`const wc =window.wcMapsconst side = (team, sim) =>` <div class="wch-hero-side">${wc.flagImg(team,"wch-hero-flag")} <div class="wch-hero-name">${statsEsc(team)}</div> <div class="wch-hero-champ">${sim ?`<b>${sim.p_champ.toFixed(1)}%</b> to win it`:`<span class="text-muted">odds loading…</span>`}</div> </div>`const wrap =document.createElement("div") wrap.className="wch-hero" wrap.innerHTML=`${side(wchA, wchSimA)} <div class="wch-hero-vs">vs</div>${side(wchB, wchSimB)}`return wrap}
Two nations, side by side — champion odds, all seven rating systems plus the Tiento aggregate, each team’s road to the final, and the most likely knockout round they could meet.
// ── Two team selectors — each sets its hash param; data-loader reloads ──{if (_wchStrength ==null||!_wchStrength.length) returnhtml``const teams = [..._wchStrength].map(r => r.team).sort((a, b) => a.localeCompare(b))const selA = Inputs.select(teams, { label:"Team A",value: wchA }) selA.addEventListener("input", () => {window.location.hash="a="+encodeURIComponent(selA.value) +"&b="+encodeURIComponent(wchB ??"") })const selB = Inputs.select(teams, { label:"Team B",value: wchB }) selB.addEventListener("input", () => {window.location.hash="a="+encodeURIComponent(wchA ??"") +"&b="+encodeURIComponent(selB.value) })const wrap =document.createElement("div") wrap.className="wch-selectors" wrap.append(selA, selB)return wrap}
Show code
// ── Tiento + 7 ratings face-off: diverging bars, A left / B right ──{if (wchA ==null|| wchB ==null) returnhtml``if (wchStrA ==null|| wchStrB ==null) returnhtml`<p class="text-muted">No rating data for one of these teams.</p>`const a = wchStrA, b = wchStrBconst fmt3 = x => x !=null? x.toFixed(3) :"—"const fmt0 = x => x !=null? x.toFixed(0) :"—"const fmtGoals = x => x ==null?"—": (x >=0?"+":"−") +Math.abs(x).toFixed(2)const tA = wchRating ? wchRating.ratings.get(wchA) ??null:nullconst tB = wchRating ? wchRating.ratings.get(wchB) ??null:null// Each system: value getter + fmt + which direction wins. defense is// pre-sign-flipped at the pannadata layer (positive = good), like Tiento it// diverges from the raw-xG defense elsewhere — see world-cup-strength.qmd.const systems = [ { key:"rating",label:window.wcMaps.RATING_NAME,fmt: fmtGoals,valA: tA,valB: tB,rankA:null,rankB:null,hint:"Aggregate goals-above-average rating" }, { key:"panna",label:"Panna",fmt: fmt3,valA: a.panna,valB: b.panna,rankA: a.rank_panna,rankB: b.rank_panna,hint:"Squad player ratings" }, { key:"offense",label:"Offense",fmt: fmt3,valA: a.offense,valB: b.offense,rankA: a.rank_offense,rankB: b.rank_offense,hint:"Attacking squad value" }, { key:"defense",label:"Defense",fmt: fmt3,valA: a.defense,valB: b.defense,rankA: a.rank_defense,rankB: b.rank_defense,hint:"Defensive squad value (positive = good)" }, { key:"epr",label:"EPR",fmt: fmt3,valA: a.epr,valB: b.epr,rankA: a.rank_epr,rankB: b.rank_epr,hint:"Expected possession value / 90" }, { key:"psr",label:"PSR",fmt: fmt3,valA: a.psr,valB: b.psr,rankA: a.rank_psr,rankB: b.rank_psr,hint:"Box-score skill rating" }, { key:"elo",label:"Elo",fmt: fmt0,valA: a.elo,valB: b.elo,rankA: a.rank_elo,rankB: b.rank_elo,hint:"Team Elo rating" }, { key:"bt",label:"BT",fmt: fmt3,valA: a.bt,valB: b.bt,rankA: a.rank_bt,rankB: b.rank_bt,hint:"Bradley-Terry tournament strength" } ]const wrap =document.createElement("div")const h =document.createElement("h2"); h.textContent="Rating systems, head to head" wrap.appendChild(h)const note =document.createElement("p"); note.className="text-muted" note.style.cssText="font-size:0.84rem;margin:0 0 0.6rem" note.innerHTML=`Each row pits ${statsEsc(wchA)} (left) against ${statsEsc(wchB)} (right). The wider half is the higher value; rank is out of the 48 qualified nations. <a href="world-cup-strength.html">Full sortable table →</a>` wrap.appendChild(note)// Per-row bar: split track centred on a divider, each half scaled to the// larger of the two absolute values (so the winner fills its half).const card =document.createElement("div") card.className="wch-faceoff"for (const s of systems) {const vA = s.valA, vB = s.valBconst have = vA !=null&& vB !=null&&isFinite(vA) &&isFinite(vB)const max = have ?Math.max(Math.abs(vA),Math.abs(vB),1e-9) :1const wA = have ?Math.min(100,Math.abs(vA) / max *100) :0const wB = have ?Math.min(100,Math.abs(vB) / max *100) :0const aWins = have && vA > vBconst bWins = have && vB > vAconst row =document.createElement("div") row.className="wch-fo-row" row.title= s.hint row.innerHTML=` <div class="wch-fo-a${aWins ?" win":""}"> <span class="wch-fo-rank">${s.rankA!=null?`#${s.rankA}`:""}</span> <span class="wch-fo-val">${s.fmt(vA)}</span> <div class="wch-fo-track"><div class="wch-fo-fill a" style="width:${wA.toFixed(1)}%"></div></div> </div> <div class="wch-fo-lbl">${statsEsc(s.label)}</div> <div class="wch-fo-b${bWins ?" win":""}"> <div class="wch-fo-track"><div class="wch-fo-fill b" style="width:${wB.toFixed(1)}%"></div></div> <span class="wch-fo-val">${s.fmt(vB)}</span> <span class="wch-fo-rank">${s.rankB!=null?`#${s.rankB}`:""}</span> </div>` card.appendChild(row) } wrap.appendChild(card)return wrap}
Show code
// ── Road to the final face-off: paired bars per stage ─────────────{if (wchA ==null|| wchB ==null) returnhtml``if (wchSimA ==null&& wchSimB ==null) returnhtml``const stages = [ { key:"advance",label:"Reach R32",sub:"advance from group" }, { key:"p_R16",label:"Reach R16",sub:"last 16" }, { key:"p_QF",label:"Reach QF",sub:"quarter-final" }, { key:"p_SF",label:"Reach SF",sub:"semi-final" }, { key:"p_final",label:"Reach Final",sub:"Jul 19" }, { key:"p_champ",label:"Champions",sub:"lift the trophy" } ]const wrap =document.createElement("div") wrap.style.marginTop="1.6rem"const head =document.createElement("div") head.style.cssText="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap"const h =document.createElement("h2"); h.textContent="Road to the final" head.append(h,window.wcLiveSim.liveBadge(_wchFull ? _wchFull.meta:null)) wrap.appendChild(head)const note =document.createElement("p"); note.className="text-muted" note.style.cssText="font-size:0.84rem;margin:0 0 0.6rem" note.innerHTML=`Probability of reaching each round, from a 10,000-tournament Monte Carlo simulation re-run live in your browser. <span class="wch-key"><span class="wch-key-dot a"></span>${statsEsc(wchA)}</span> <span class="wch-key"><span class="wch-key-dot b"></span>${statsEsc(wchB)}</span>` wrap.appendChild(note)const card =document.createElement("div") card.className="wch-road"for (const s of stages) {const vA = wchSimA?.[s.key]const vB = wchSimB?.[s.key]const row =document.createElement("div") row.className="wch-road-row"const pct = (v) => v !=null? (v >=10? v.toFixed(0) : v.toFixed(1)) +"%":"—" row.innerHTML=` <div class="wch-road-a"> <span class="wch-road-pct">${pct(vA)}</span> <div class="wch-road-track"><div class="wch-road-fill a" style="width:${vA !=null?Math.min(100, vA).toFixed(1) :0}%"></div></div> </div> <div class="wch-road-lbl"><span class="wch-road-stage">${statsEsc(s.label)}</span><span class="wch-road-sub">${statsEsc(s.sub)}</span></div> <div class="wch-road-b"> <div class="wch-road-track"><div class="wch-road-fill b" style="width:${vB !=null?Math.min(100, vB).toFixed(1) :0}%"></div></div> <span class="wch-road-pct">${pct(vB)}</span> </div>` card.appendChild(row) } wrap.appendChild(card)return wrap}
Show code
// ── If they meet ───────────────────────────────────────────────────// Same group -> they meet in the group stage (show the direct fixture if one// exists). Otherwise, scan the live sim's matchups (per-KO-round {a,b,pct}// ties): P(meet) = sum of pct across all rounds where the tie contains both,// and the single most-likely round to meet. matchups is null in the static-// parquet fallback -> that part hides, group meetings still show.{if (wchA ==null|| wchB ==null) returnhtml``const wc =window.wcMapsconst wrap =document.createElement("div") wrap.style.marginTop="1.6rem"const h =document.createElement("h2"); h.textContent="If they meet" wrap.appendChild(h)const sameGroup = wchGroupA !=null&& wchGroupA === wchGroupB// A direct group fixture (either ordering) from the predictions parquetconst directFx = (_wchPredictions || []).find(f => (f.home_team=== wchA && f.away_team=== wchB) || (f.home_team=== wchB && f.away_team=== wchA)) ||nullif (sameGroup) {const note =document.createElement("p") note.innerHTML=`${statsEsc(wchA)} and ${statsEsc(wchB)} are both in <a href="world-cup-group.html#group=${statsEsc(wchGroupA)}">Group ${statsEsc(wchGroupA)}</a>, so they meet in the group stage${directFx ?"":" — but the fixture isn't in the prediction set"}.` wrap.appendChild(note)if (directFx) {const card =document.createElement("div") card.className="match-cards-container" card.innerHTML= wc.fixtureCardHtml(directFx,null, { showDay:true,showGroupChip:true }) wrap.appendChild(card) }return wrap }// Different groups -> knockout-meeting odds from the live sim matchupsconst full = _wchFullif (!full ||!full.matchups) {const note =document.createElement("p"); note.className="text-muted" note.textContent="Knockout meeting odds load with the live simulation." wrap.appendChild(note)return wrap }const ROUND_LABELS = ["Round of 32","Round of 16","Quarter-final","Semi-final","Final"]const perRound = []let total =0for (let r =0; r < full.matchups.length; r++) {let pct =0for (const m of full.matchups[r]) {const hit = (m.a=== wchA && m.b=== wchB) || (m.a=== wchB && m.b=== wchA)if (hit) pct += m.pct } perRound.push({ round: ROUND_LABELS[r] ||`Round ${r}`, pct }) total += pct }const likeliest = [...perRound].filter(x => x.pct>0).sort((a, b) => b.pct- a.pct)[0] ||nullconst note =document.createElement("p") note.innerHTML= total >0?`Across 10,000 live simulations, ${statsEsc(wchA)} and ${statsEsc(wchB)} meet in the knockout rounds <b>${total.toFixed(1)}%</b> of the time`+ (likeliest ?` — most often in the <b>${statsEsc(likeliest.round)}</b> (${likeliest.pct.toFixed(1)}%).`:"."):`In different groups, ${statsEsc(wchA)} and ${statsEsc(wchB)} hardly ever meet — under 0.1% of the 10,000 live simulations.` wrap.appendChild(note)if (total >0) {const rows = perRound.filter(x => x.pct>0).sort((a, b) => b.pct- a.pct)const table =window.statsTable(rows, {columns: ["round","pct"],header: { round:"Round",pct:"P(meet)" },tooltip: { pct:"Share of all 10,000 sims where they meet in this round" },format: { pct: x => x.toFixed(1) +"%" },heatmap: { pct:"high-good" },sort:"pct",reverse:true,rows:6 }) wrap.appendChild(table) }return wrap}