WIFX / iframe.html
WIFX Deploy
deploy: 2026-04-28 14:43 UTC
9f405c6
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>WIFX — Women's International Football Rankings</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.plot.ly/plotly-2.35.0.min.js" charset="utf-8"></script>
<style>
.section-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
cursor: pointer;
user-select: none;
padding: 0.5rem 0;
gap: 1rem;
}
.section-header:hover h2 { opacity: 0.8; }
.section-header-text { flex: 1; }
.section-header h2 { margin: 0 0 0.25rem; }
.section-header .section-desc { margin: 0; }
.section-chevron {
font-size: 1.1rem;
color: #888;
margin-top: 0.35rem;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.collapsible-section.collapsed .section-chevron { transform: rotate(-90deg); }
.section-body {
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.2s ease;
max-height: 9999px;
opacity: 1;
}
.collapsible-section.collapsed .section-body {
max-height: 0;
opacity: 0;
pointer-events: none;
}
/* ---- Permalink button ---- */
.section-permalink {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 6px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.12);
color: #aaa;
cursor: pointer;
flex-shrink: 0;
margin-top: 0.2rem;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
text-decoration: none;
}
.section-permalink svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.collapsible-section:hover .section-permalink { opacity: 1; }
.section-permalink:hover { background: rgba(79,195,247,0.15); color: #4fc3f7; border-color: rgba(79,195,247,0.3); }
.section-permalink.copied { background: rgba(129,199,132,0.2); color: #81c784; border-color: rgba(129,199,132,0.4); opacity: 1; }
/* ---- Copied toast ---- */
#permalink-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(8px);
background: #1e2a38;
color: #e6eef8;
border: 1px solid rgba(79,195,247,0.3);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.8rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 9999;
}
#permalink-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---- Iframe-native layout ---- */
body { padding-top: 0.5rem; }
.collapsible-section { border-top: none; }
</style>
</head>
<body class="iframe-mode">
<div id="permalink-toast">Link copied</div>
<main>
<!-- WIFX Score — open by default -->
<section id="wifx-score" class="collapsible-section">
<div class="section-header" data-target="wifx-score">
<div class="section-header-text">
<h2>WIFX Score</h2>
<p class="section-desc">Proprietary algorithm combining strength of schedule, competition quality, and career achievement across 20+ competitions worldwide.</p>
</div>
<a class="section-permalink" data-section="wifx-score" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<h3>Top 100 WIFXScores</h3>
<div id="wifx-top100-list" class="player-list"></div>
</div>
</div>
</div>
</section>
<!-- Cognitive Performance Index — collapsed by default -->
<section id="wifx-cpi" class="collapsible-section collapsed">
<div class="section-header" data-target="wifx-cpi">
<div class="section-header-text">
<h2>Cognitive Performance Index (CPI)</h2>
<p class="section-desc">Proxy model quantifying cognitive load and positional fluency. Combines Position Familiarity (PF), Natural Instincts (NI), and Reaction Time (RT) derived from 1.8M records across 8 competitions.</p>
</div>
<a class="section-permalink" data-section="wifx-cpi" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<h3>Top 100 CPI Scores</h3>
<div id="wifx-cpi-list" class="player-list"></div>
</div>
</div>
</div>
</section>
<!-- WIFX Global Rankings — collapsed by default -->
<section id="wifx-teams" class="collapsible-section collapsed">
<div class="section-header" data-target="wifx-teams">
<div class="section-header-text">
<h2>WIFX Global Rankings</h2>
<p class="section-desc">Offensive and defensive ratings for national teams across major tournaments (WWC 2019, WWC 2023, Euros 2022, Euros 2025).</p>
</div>
<a class="section-permalink" data-section="wifx-teams" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<div class="tab-bar" id="wifx-teams-tabs">
<button class="active" data-metric="wifx_global_ranking">WIFX Global Ranking</button>
<button data-metric="net">Net Rating</button>
<button data-metric="offensive">Offensive</button>
</div>
<div id="wifx-teams-chart" class="plotly-chart"></div>
</div>
</div>
</div>
</section>
<!-- WIFX Club Team Rankings — collapsed by default -->
<section id="wifx-clubs" class="collapsible-section collapsed">
<div class="section-header" data-target="wifx-clubs">
<div class="section-header-text">
<h2>WIFX Club Team Rankings</h2>
<p class="section-desc">Offensive and defensive ratings for club teams across major domestic leagues and continental competitions.</p>
</div>
<a class="section-permalink" data-section="wifx-clubs" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<div class="tab-bar" id="wifx-clubs-tabs">
<button class="active" data-metric="wifx_global_club_ranking">WIFX Club Ranking</button>
<button data-metric="net">Net Rating</button>
<button data-metric="offensive">Offensive</button>
</div>
<div id="wifx-clubs-chart" class="plotly-chart"></div>
</div>
</div>
</div>
</section>
<!-- WIFX Confederation Club Scores — collapsed by default -->
<section id="wifx-confederations" class="collapsible-section collapsed">
<div class="section-header" data-target="wifx-confederations">
<div class="section-header-text">
<h2>WIFX Confederation Club Scores</h2>
<p class="section-desc">Club rankings from confederation championships (UEFA, AFC, CONMEBOL, CONCACAF, CAF, OFC) — 15 years (2010–2025).</p>
</div>
<a class="section-permalink" data-section="wifx-confederations" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<div class="tab-bar" id="conf-tabs">
<button class="active" data-conf="all">All</button>
<button data-conf="UEFA">UEFA</button>
<button data-conf="CONMEBOL">CONMEBOL</button>
<button data-conf="AFC">AFC</button>
<button data-conf="CONCACAF">CONCACAF</button>
<button data-conf="CAF">CAF</button>
<button data-conf="OFC">OFC</button>
</div>
<div id="conf-chart" class="plotly-chart"></div>
</div>
</div>
</div>
</section>
<!-- WIFX Historical Scores — collapsed by default -->
<section id="wifx-historical" class="collapsible-section collapsed">
<div class="section-header" data-target="wifx-historical">
<div class="section-header-text">
<h2>WIFX Historical Scores</h2>
<p class="section-desc">Retired players ranked by WIFX Score. Legends of the game who have stepped away from professional football.</p>
</div>
<a class="section-permalink" data-section="wifx-historical" title="Copy link" onclick="copyPermalink(event,this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
<span class="section-chevron">&#9660;</span>
</div>
<div class="section-body">
<div class="chart-row">
<div class="chart-box full">
<h3>Historical WIFX Rankings</h3>
<div id="wifx-historical-list" class="player-list"></div>
</div>
</div>
</div>
</section>
</main>
<!-- Hover card — WIFX Score -->
<div id="player-card" class="player-card hidden">
<div class="pc-header">
<div class="pc-photo" id="pc-photo"></div>
<div class="pc-identity">
<div class="pc-name" id="pc-name"></div>
<div class="pc-team" id="pc-team"></div>
<div class="pc-comp" id="pc-comp"></div>
</div>
</div>
<div class="pc-stats">
<div class="pc-stat"><span class="pc-stat-label">WIFX Score</span><span class="pc-stat-value" id="pc-wifx"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Offensive</span><span class="pc-stat-value" id="pc-off"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Creative</span><span class="pc-stat-value" id="pc-cre"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Defensive</span><span class="pc-stat-value" id="pc-def"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Events</span><span class="pc-stat-value" id="pc-events"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Accolades</span><span class="pc-stat-value" id="pc-awards"></span></div>
</div>
</div>
<!-- Hover card — CPI -->
<div id="cpi-card" class="player-card hidden">
<div class="pc-header">
<div class="pc-photo" id="cpi-photo"></div>
<div class="pc-identity">
<div class="pc-name" id="cpi-name"></div>
<div class="pc-team" id="cpi-team"></div>
<div class="pc-comp" id="cpi-position"></div>
</div>
</div>
<div class="pc-stats">
<div class="pc-stat"><span class="pc-stat-label">CPI Score</span><span class="pc-stat-value" id="cpi-score"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Position Familiarity</span><span class="pc-stat-value" id="cpi-pf"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Natural Instincts</span><span class="pc-stat-value" id="cpi-ni"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Reaction Time</span><span class="pc-stat-value" id="cpi-rt"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Events</span><span class="pc-stat-value" id="cpi-events"></span></div>
<div class="pc-stat"><span class="pc-stat-label">Matches</span><span class="pc-stat-value" id="cpi-matches"></span></div>
</div>
</div>
<script>
const DARK = {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#e6eef8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { l: 140, r: 20, t: 30, b: 50 },
xaxis: { gridcolor: 'rgba(255,255,255,0.06)', zerolinecolor: 'rgba(255,255,255,0.1)' },
yaxis: { gridcolor: 'rgba(255,255,255,0.06)', zerolinecolor: 'rgba(255,255,255,0.1)' },
};
const CFG = { displayModeBar: true, responsive: true, displaylogo: false };
const COLORS = ['#4fc3f7','#81c784','#ffb74d','#f06292','#ba68c8','#4dd0e1','#aed581','#ff8a65'];
const TOP_N = 100;
const TEAM_TOP_N = 25;
const GROUP_SIZE = 25;
function hbar(divId, labels, values, color, hoverText) {
const trace = {
y: labels.slice().reverse(),
x: values.slice().reverse(),
type: 'bar', orientation: 'h',
marker: { color: Array.isArray(color) ? color.slice().reverse() : (color || '#4fc3f7') },
text: hoverText ? hoverText.slice().reverse() : undefined,
hoverinfo: hoverText ? 'text' : 'x+y',
};
const barHeight = labels.length > 50 ? 22 : 28;
const layout = {
paper_bgcolor: '#f0f0f0',
plot_bgcolor: '#f0f0f0',
xaxis: { title: 'Value', gridcolor: '#ccc' },
yaxis: { automargin: true, gridcolor: '#ccc', tickfont: { size: 10 } },
height: Math.max(400, labels.length * barHeight),
margin: { l: 180, r: 20, t: 30, b: 50 }
};
Plotly.newPlot(divId, [trace], layout, CFG);
}
// ---- WIFX Global Rankings ----
function renderWIFXTeams(data) {
if (!data || !data.all_teams) return;
const chartId = 'wifx-teams-chart';
const aggregated = data.all_teams.filter(t => t.matches >= 200);
function draw(view) {
const keyMap = { wifx_global_ranking: 'wifx_global_ranking', offensive: 'offensive_rating', net: 'net_rating' };
const key = keyMap[view];
const sorted = [...aggregated].sort((a, b) => b[key] - a[key]).slice(0, TEAM_TOP_N);
const colorVals = ['#ba68c8', '#81c784', '#4fc3f7', '#ffb74d', '#f06292', '#4dd0e1', '#aed581', '#ff8a65'];
const barColors = sorted.map((_, i) => colorVals[i % colorVals.length]);
hbar(chartId,
sorted.map(d => d.team),
sorted.map(d => d[key] ?? 0),
barColors,
sorted.map(d => `${d.team}<br>${view}: ${(d[key] ?? 0).toFixed(1)}<br>Off: ${(d.offensive_rating ?? 0).toFixed(1)} | Def: ${(d.defensive_rating ?? 0).toFixed(1)}<br>Goals: ${d.goals_scored}-${d.goals_conceded}<br>Matches: ${d.matches}`)
);
}
draw('wifx_global_ranking');
document.getElementById('wifx-teams-tabs').addEventListener('click', e => {
if (!e.target.dataset.metric) return;
document.querySelectorAll('#wifx-teams-tabs button').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
draw(e.target.dataset.metric);
});
}
// ---- WIFX Club Team Rankings ----
function renderWIFXClubs(data) {
if (!data || !data.all_teams) return;
const chartId = 'wifx-clubs-chart';
const aggregated = data.all_teams.filter(t => t.matches >= 100);
function draw(view) {
const keyMap = { wifx_global_club_ranking: 'wifx_global_club_ranking', offensive: 'offensive_rating', net: 'net_rating' };
const key = keyMap[view];
const sorted = [...aggregated].sort((a, b) => b[key] - a[key]).slice(0, TEAM_TOP_N);
const colorVals = ['#ba68c8', '#81c784', '#4fc3f7', '#ffb74d', '#f06292', '#4dd0e1', '#aed581', '#ff8a65'];
const barColors = sorted.map((_, i) => colorVals[i % colorVals.length]);
hbar(chartId,
sorted.map(d => d.team),
sorted.map(d => d[key] ?? 0),
barColors,
sorted.map(d => `${d.team}<br>${view}: ${(d[key] ?? 0).toFixed(1)}<br>Off: ${(d.offensive_rating ?? 0).toFixed(1)}<br>Wins: ${d.championship_wins || 0}<br>Matches: ${d.matches}`)
);
}
draw('wifx_global_club_ranking');
document.getElementById('wifx-clubs-tabs').addEventListener('click', e => {
if (!e.target.dataset.metric) return;
document.querySelectorAll('#wifx-clubs-tabs button').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
draw(e.target.dataset.metric);
});
}
// Shared aggregation for teams/clubs
function aggregateTeams(teams) {
const agg = {};
for (const t of teams) {
if (!agg[t.team]) {
agg[t.team] = { team: t.team, matches: 0, goals_scored: 0, goals_conceded: 0, offensive_rating_sum: 0, defensive_rating_sum: 0, net_rating_sum: 0, composite_rating_sum: 0 };
}
agg[t.team].matches += t.matches || 0;
agg[t.team].goals_scored += t.goals_scored || 0;
agg[t.team].goals_conceded += t.goals_conceded || 0;
agg[t.team].offensive_rating_sum += (t.offensive_rating || 0) * (t.matches || 1);
agg[t.team].defensive_rating_sum += (t.defensive_rating || 0) * (t.matches || 1);
agg[t.team].net_rating_sum += (t.net_rating || 0) * (t.matches || 1);
agg[t.team].composite_rating_sum += (t.composite_rating || 0) * (t.matches || 1);
}
return Object.values(agg).map(t => ({
team: t.team,
matches: t.matches,
goals_scored: t.goals_scored,
goals_conceded: t.goals_conceded,
offensive_rating: t.matches ? t.offensive_rating_sum / t.matches : 0,
defensive_rating: t.matches ? t.defensive_rating_sum / t.matches : 0,
net_rating: t.matches ? t.net_rating_sum / t.matches : 0,
composite_rating: t.matches ? t.composite_rating_sum / t.matches : 0,
}));
}
// ---- WIFX Confederation Club Scores ----
function renderConfederations(data) {
if (!data || !data.club_confederation_scores) return;
const clubs = data.club_confederation_scores;
const CONF_COLORS = { UEFA:'#4fc3f7', CONCACAF:'#81c784', CONMEBOL:'#ffb74d', CAF:'#f06292', AFC:'#ba68c8', OFC:'#4dd0e1' };
// Aggregate by team
const agg = {};
for (const c of clubs) {
if (!agg[c.team]) {
agg[c.team] = { team: c.team, country: c.country, confederation: c.confederation, championships_won: 0, finals_reached: 0, wifx_club_score_sum: 0, years: 0 };
}
agg[c.team].championships_won += c.championships_won || 0;
agg[c.team].finals_reached += c.finals_reached || 0;
agg[c.team].wifx_club_score_sum += c.wifx_club_score || 0;
agg[c.team].years += 1;
}
const aggregated = Object.values(agg).map(t => ({
...t, wifx_club_score: t.wifx_club_score_sum / t.years
}));
function draw(conf) {
const filtered = conf === 'all' ? aggregated : aggregated.filter(c => c.confederation === conf);
const sorted = [...filtered].sort((a, b) => b.wifx_club_score - a.wifx_club_score).slice(0, TEAM_TOP_N);
const trace = {
y: sorted.map(d => d.team).reverse(),
x: sorted.map(d => d.wifx_club_score).reverse(),
type: 'bar', orientation: 'h',
marker: { color: conf === 'all' ? sorted.map(d => CONF_COLORS[d.confederation] || '#aaa').reverse() : CONF_COLORS[conf] || '#4fc3f7' },
text: sorted.map(d => `${d.team} (${d.confederation})<br>Score: ${(d.wifx_club_score ?? 0).toFixed(1)}<br>Championships: ${d.championships_won}<br>Finals: ${d.finals_reached}`).reverse(),
hoverinfo: 'text',
};
Plotly.newPlot('conf-chart', [trace], {
paper_bgcolor: '#f0f0f0',
plot_bgcolor: '#f0f0f0',
xaxis: { title: 'WIFX Club Score', gridcolor: '#ccc' },
yaxis: { automargin: true, gridcolor: '#ccc' },
height: Math.max(400, sorted.length * 28),
margin: { l: 150, r: 20, t: 30, b: 50 }
}, CFG);
}
draw('all');
document.getElementById('conf-tabs').addEventListener('click', e => {
if (!e.target.dataset.conf) return;
document.querySelectorAll('#conf-tabs button').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
draw(e.target.dataset.conf);
});
}
// ---- Permalink copy ----
const BASE_URL = 'https://huggingface.co/spaces/AMadabhushi/WIFX';
let toastTimer;
function copyPermalink(e, btn) {
e.stopPropagation(); // don't toggle the section
const section = btn.dataset.section;
const url = BASE_URL + '?section=' + section;
navigator.clipboard.writeText(url).then(() => {
btn.classList.add('copied');
const toast = document.getElementById('permalink-toast');
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
btn.classList.remove('copied');
toast.classList.remove('show');
}, 2000);
});
}
// ---- Iframe embed API ----
(function () {
const params = new URLSearchParams(window.location.search);
// ?section=<id> — open only that section, collapse the rest
const targetSection = params.get('section');
if (targetSection) {
document.querySelectorAll('.collapsible-section').forEach(s => {
s.id === targetSection
? s.classList.remove('collapsed')
: s.classList.add('collapsed');
});
}
// Report height to parent whenever layout changes
function reportHeight() {
parent.postMessage({ type: 'wifx:height', height: document.body.scrollHeight }, '*');
}
// Report after fonts/images settle and after data loads
window.addEventListener('load', () => setTimeout(reportHeight, 400));
// postMessage API — parent (WordPress) can drive the dashboard
// Supported actions:
// { action: 'open', section: 'wifx-score' } — expand a section
// { action: 'collapse', section: 'wifx-teams' } — collapse a section
// { action: 'toggle', section: 'wifx-cpi' } — toggle a section
// { action: 'ping' } — responds with wifx:ready
window.addEventListener('message', e => {
if (!e.data || typeof e.data !== 'object') return;
const { action, section } = e.data;
if (action === 'ping') {
e.source.postMessage({ type: 'wifx:ready' }, e.origin || '*');
return;
}
if (!section) return;
const el = document.getElementById(section);
if (!el) return;
if (action === 'open') el.classList.remove('collapsed');
if (action === 'collapse') el.classList.add('collapsed');
if (action === 'toggle') el.classList.toggle('collapsed');
// Resize Plotly charts if section just opened
if (!el.classList.contains('collapsed')) {
el.querySelectorAll('.plotly-chart').forEach(c => { if (c.layout) Plotly.Plots.resize(c); });
}
setTimeout(reportHeight, 350);
e.source && e.source.postMessage({ type: 'wifx:ack', action, section }, e.origin || '*');
});
// Re-report height whenever a section is toggled
document.addEventListener('click', e => {
if (e.target.closest('.section-header')) setTimeout(reportHeight, 350);
});
})();
// ---- Collapsible sections ----
document.querySelectorAll('.section-header').forEach(header => {
header.addEventListener('click', () => {
const section = header.closest('.collapsible-section');
const wasCollapsed = section.classList.contains('collapsed');
section.classList.toggle('collapsed');
// Resize any Plotly charts inside the section once it opens
if (wasCollapsed) {
const charts = section.querySelectorAll('.plotly-chart');
charts.forEach(el => {
if (el.layout) Plotly.Plots.resize(el);
});
}
});
});
// ---- CPI Player List ----
function renderCPIList(cpiData) {
if (!cpiData || !Array.isArray(cpiData)) return;
const sorted = [...cpiData].sort((a, b) => b.cpi_score - a.cpi_score).slice(0, TOP_N);
const MAX_CPI = 10;
const listEl = document.getElementById('wifx-cpi-list');
if (!listEl) return;
function renderCPIRow(p, i) {
const pct = ((p.cpi_score / MAX_CPI) * 100).toFixed(0);
const hue = Math.round(Math.min(p.cpi_score / MAX_CPI, 1) * 120);
const compShort = (p.competition || '').replace(/_/g, ' ');
return `<div class="pl-row" data-cpi-idx="${i}">
<div class="pl-score-ring" style="--pct:${Math.min(pct, 100)};--hue:${hue}">
<span>${p.cpi_score.toFixed(1)}</span>
</div>
<div class="pl-rank">${i + 1}</div>
<div class="pl-info">
<div class="pl-name">${p.player}</div>
<div class="pl-meta">${p.team} · ${p.primary_position}</div>
</div>
</div>`;
}
let html = '';
const groups = Math.ceil(sorted.length / GROUP_SIZE);
for (let g = 0; g < groups; g++) {
const start = g * GROUP_SIZE;
const end = Math.min(start + GROUP_SIZE, sorted.length);
const rows = sorted.slice(start, end).map((p, j) => renderCPIRow(p, start + j)).join('');
if (g === 0) {
html += `<div class="pl-group">${rows}</div>`;
} else {
const label = `${start + 1}${end}`;
html += `<div class="pl-group-toggle" data-target="cpi-group-${g}">
<span class="pl-toggle-label">Show ${label}</span>
<span class="pl-toggle-arrow">&#9660;</span>
</div>
<div class="pl-group pl-group-collapsed" id="cpi-group-${g}">${rows}</div>`;
}
}
listEl.innerHTML = html;
listEl.querySelectorAll('.pl-group-toggle').forEach(toggle => {
const gIdx = +toggle.dataset.target.replace('cpi-group-', '');
const s = gIdx * GROUP_SIZE + 1;
const e = Math.min((gIdx + 1) * GROUP_SIZE, sorted.length);
const rangeLabel = `${s}\u2013${e}`;
toggle.addEventListener('click', () => {
const target = document.getElementById(toggle.dataset.target);
const collapsed = target.classList.toggle('pl-group-collapsed');
toggle.querySelector('.pl-toggle-arrow').innerHTML = collapsed ? '&#9660;' : '&#9650;';
toggle.querySelector('.pl-toggle-label').textContent = (collapsed ? 'Show ' : 'Hide ') + rangeLabel;
});
});
// CPI hover card
const cpiCard = document.getElementById('cpi-card');
let cpiHideTimeout;
listEl.addEventListener('mouseover', e => {
const row = e.target.closest('.pl-row');
if (!row) return;
clearTimeout(cpiHideTimeout);
const idx = +row.dataset.cpiIdx;
const p = sorted[idx];
if (!p) return;
document.getElementById('cpi-photo').textContent = p.player.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
document.getElementById('cpi-name').textContent = p.player;
document.getElementById('cpi-team').textContent = p.team;
document.getElementById('cpi-position').textContent = p.primary_position;
document.getElementById('cpi-score').textContent = p.cpi_score.toFixed(2);
document.getElementById('cpi-pf').textContent = (p.pf_score ?? 0).toFixed(2);
document.getElementById('cpi-ni').textContent = (p.ni_score ?? 0).toFixed(2);
document.getElementById('cpi-rt').textContent = (p.rt_score ?? 0).toFixed(2);
document.getElementById('cpi-events').textContent = (p.total_events || 0).toLocaleString();
document.getElementById('cpi-matches').textContent = p.match_count || 0;
const nameEl = row.querySelector('.pl-name');
const nameRect = nameEl.getBoundingClientRect();
const cardW = 320;
let left = nameRect.left + Math.min(nameEl.scrollWidth, nameRect.width) + 16;
if (left + cardW > window.innerWidth) left = window.innerWidth - cardW - 16;
let top = nameRect.top + window.scrollY - 40;
cpiCard.style.left = left + 'px';
cpiCard.style.top = top + 'px';
cpiCard.classList.remove('hidden');
});
listEl.addEventListener('mouseout', e => {
if (!e.target.closest('.pl-row')) return;
cpiHideTimeout = setTimeout(() => cpiCard.classList.add('hidden'), 200);
});
cpiCard.addEventListener('mouseenter', () => clearTimeout(cpiHideTimeout));
cpiCard.addEventListener('mouseleave', () => cpiCard.classList.add('hidden'));
}
// ---- Load and render all data ----
async function loadAllData() {
try {
// Load all data in parallel
const [scores, historicalScores, cpiScores, teams, clubs, confs] = await Promise.all([
fetch('output/wifx_scores.json').then(r => r.json()),
fetch('output/wifx_historical_scores.json').then(r => r.json()),
fetch('output/wifx_cpi_player_scores.json').then(r => r.json()),
fetch('output/wifx_national_team_scores.json').then(r => r.json()),
fetch('output/wifx_club_team_scores.json').then(r => r.json()),
fetch('output/wifx_club_confederation_scores.json').then(r => r.json()),
]);
// WIFX Score - Top 100 Player List
if (scores.all_players) {
// Awards counts come from the JSON (`awards: {nwsl, intl}` per player),
// sourced from data/manual_overrides/player_awards.csv via
// build_dashboard_data.py. Render-only — no scoring math here.
const totalAwards = (p) => {
const a = p.awards || {};
return (a.nwsl || 0) + (a.intl || 0);
};
const rankSort = (a, b) => b.WIFXScore - a.WIFXScore;
const filtered = scores.all_players.filter(p => p.total_events >= 1000);
const activePlayers = filtered.slice().sort(rankSort).slice(0, TOP_N);
// Historical players from separate JSON
const histFiltered = (historicalScores.all_players || []).filter(p => p.total_events >= 100);
const historicalPlayers = histFiltered.slice().sort(rankSort).slice(0, 50);
const MAX_DISPLAY = 25;
// --- Render player list with collapsible groups of 25 ---
const listEl = document.getElementById('wifx-top100-list');
function renderPlayerRow(p, i, listId) {
const pct = ((p.WIFXScore / MAX_DISPLAY) * 100).toFixed(0);
const hue = Math.round(Math.min(p.WIFXScore / MAX_DISPLAY, 1) * 120);
const compShort = p.display_comp || '';
const awardCount = totalAwards(p);
const awardsTag = awardCount > 0 ? `<span class="pl-awards">&#9733; ${awardCount}</span>` : '';
return `<div class="pl-row" data-idx="${i}" data-list="${listId}">
<div class="pl-score-ring" style="--pct:${Math.min(pct, 100)};--hue:${hue}">
<span>${p.WIFXScore.toFixed(1)}</span>
</div>
<div class="pl-rank">${i + 1}</div>
<div class="pl-info">
<div class="pl-name">${p.player}${awardsTag}</div>
<div class="pl-meta">${p.team}${compShort ? ' · ' + compShort : ''}</div>
</div>
</div>`;
}
function renderPlayerList(container, players, listId) {
let html = '';
const groups = Math.ceil(players.length / GROUP_SIZE);
for (let g = 0; g < groups; g++) {
const start = g * GROUP_SIZE;
const end = Math.min(start + GROUP_SIZE, players.length);
const rows = players.slice(start, end).map((p, j) => renderPlayerRow(p, start + j, listId)).join('');
if (g === 0) {
html += `<div class="pl-group">${rows}</div>`;
} else {
const label = `${start + 1}${end}`;
html += `<div class="pl-group-toggle" data-target="${listId}-group-${g}">
<span class="pl-toggle-label">Show ${label}</span>
<span class="pl-toggle-arrow">&#9660;</span>
</div>
<div class="pl-group pl-group-collapsed" id="${listId}-group-${g}">${rows}</div>`;
}
}
container.innerHTML = html;
container.querySelectorAll('.pl-group-toggle').forEach(toggle => {
const range = toggle.dataset.target.replace(`${listId}-group-`, '');
const gIdx = +range;
const s = gIdx * GROUP_SIZE + 1;
const e = Math.min((gIdx + 1) * GROUP_SIZE, players.length);
const rangeLabel = `${s}\u2013${e}`;
toggle.addEventListener('click', () => {
const target = document.getElementById(toggle.dataset.target);
const collapsed = target.classList.toggle('pl-group-collapsed');
toggle.querySelector('.pl-toggle-arrow').innerHTML = collapsed ? '&#9660;' : '&#9650;';
toggle.querySelector('.pl-toggle-label').textContent = (collapsed ? 'Show ' : 'Hide ') + rangeLabel;
});
});
}
renderPlayerList(listEl, activePlayers, 'active');
// Render historical players
const histEl = document.getElementById('wifx-historical-list');
if (histEl) renderPlayerList(histEl, historicalPlayers, 'historical');
// Shared hover card logic
const card = document.getElementById('player-card');
let hideTimeout;
const allLists = { active: activePlayers, historical: historicalPlayers };
function attachHoverCard(containerEl) {
containerEl.addEventListener('mouseover', e => {
const row = e.target.closest('.pl-row');
if (!row) return;
clearTimeout(hideTimeout);
const idx = +row.dataset.idx;
const listId = row.dataset.list;
const p = allLists[listId][idx];
if (!p) return;
document.getElementById('pc-photo').textContent = p.player.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
document.getElementById('pc-name').textContent = p.player;
document.getElementById('pc-team').textContent = p.team;
document.getElementById('pc-comp').textContent = p.display_comp || '';
document.getElementById('pc-wifx').textContent = p.WIFXScore.toFixed(2);
document.getElementById('pc-off').textContent = (p.offensive_score ?? 0).toFixed(1);
document.getElementById('pc-cre').textContent = (p.creative_score ?? 0).toFixed(1);
document.getElementById('pc-def').textContent = (p.defensive_score ?? 0).toFixed(1);
document.getElementById('pc-events').textContent = p.total_events.toLocaleString();
document.getElementById('pc-awards').textContent = totalAwards(p) || '0';
const nameEl = row.querySelector('.pl-name');
const nameRect = nameEl.getBoundingClientRect();
const textRight = nameRect.left + Math.min(nameEl.scrollWidth, nameRect.width);
const cardW = 320;
let left = textRight + 16;
if (left + cardW > window.innerWidth) left = window.innerWidth - cardW - 16;
let top = nameRect.top + window.scrollY - 40;
card.style.left = left + 'px';
card.style.top = top + 'px';
card.classList.remove('hidden');
});
containerEl.addEventListener('mouseout', e => {
const row = e.target.closest('.pl-row');
if (!row) return;
hideTimeout = setTimeout(() => card.classList.add('hidden'), 200);
});
}
attachHoverCard(listEl);
if (histEl) attachHoverCard(histEl);
card.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
card.addEventListener('mouseleave', () => card.classList.add('hidden'));
}
// CPI rankings
renderCPIList(cpiScores);
// Render team/club/confederation charts
renderWIFXTeams(teams);
renderWIFXClubs(clubs);
renderConfederations(confs);
} catch (err) {
console.error('Dashboard load error:', err);
}
}
loadAllData().then(() => {
setTimeout(() => parent.postMessage({ type: 'wifx:height', height: document.body.scrollHeight }, '*'), 600);
parent.postMessage({ type: 'wifx:ready' }, '*');
});
</script>
</body>
</html>