Football Team Ratings
Team Panna ratings aggregated from top player ratings
Show code
{
return html`<div class="breadcrumb"><a href="index.html">Football</a> > Team Ratings</div>
<div class="page-legend">Team Panna ratings (sum of top squad players, relative to league average).
<span class="legend-tag legend-good">Offense</span> = xG created.
<span class="legend-tag legend-bad">Defense</span> = xG prevented (negative = better).
</div>`
}Show code
allTeamRatings = {
if (!football_data) return null
const byTeam = new Map()
for (const p of football_data) {
const k = p.league + "|" + p.team
if (!byTeam.has(k)) byTeam.set(k, { team: p.team, league: p.league, players: [] })
byTeam.get(k).players.push(p)
}
const TOP_N = 20
const teams = []
for (const [, g] of byTeam) {
g.players.sort((a, b) => (b.panna ?? 0) - (a.panna ?? 0))
const top = g.players.slice(0, TOP_N)
const panna = top.reduce((s, p) => s + (p.panna ?? 0), 0)
const offense = top.reduce((s, p) => s + (p.offense ?? 0), 0)
const defense = top.reduce((s, p) => s + (p.defense ?? 0), 0)
const best = g.players[0]
teams.push({
team: g.team, league: g.league, panna, offense, defense,
n_players: g.players.length,
top_player: best?.player_name ?? "",
top_panna: best?.panna ?? 0
})
}
// Relative to league average within each league
const byLeague = new Map()
for (const t of teams) {
if (!byLeague.has(t.league)) byLeague.set(t.league, [])
byLeague.get(t.league).push(t)
}
for (const leagueTeams of byLeague.values()) {
const cols = ["panna", "offense", "defense"]
const n = leagueTeams.length
if (n === 0) continue
const avgs = {}
for (const c of cols) avgs[c] = leagueTeams.reduce((s, t) => s + t[c], 0) / n
for (const t of leagueTeams) {
for (const c of cols) t[c] = t[c] - avgs[c]
}
}
return teams.sort((a, b) => b.panna - a.panna).map((t, i) => ({ rank: i + 1, ...t }))
}Show code
viewof leagueFilter = {
if (!football_data) return html`<p></p>`
const leagues = [...new Set(football_data.map(d => d.league).filter(Boolean))].sort()
const options = ["All Leagues", ...leagues]
const display = new Map(options.map(l => [l, l.replace(/_/g, " ")]))
const makeSelect = window.footballMaps.makeSelect
const bar = document.createElement("div")
bar.className = "player-filter-bar"
const row = document.createElement("div")
row.className = "filter-row"
const { wrap, sel } = makeSelect(options, "All Leagues", "League", x => display.get(x) || x)
row.appendChild(wrap)
bar.appendChild(row)
bar.value = "All Leagues"
sel.addEventListener("change", () => {
bar.value = sel.value
bar.dispatchEvent(new Event("input", { bubbles: true }))
})
return bar
}Show code
Show code
// ── View toggle (Table / Scatter) ───────────────────────────
{
const _key = "_viewMode_" + window.location.pathname.replace(/[^a-z0-9]/gi, "_")
if (!window[_key]) window[_key] = "Table"
const container = document.createElement("div")
container.className = "pos-pills"
for (const label of ["Table", "Scatter"]) {
const btn = document.createElement("button")
btn.className = "pos-pill" + (label === window[_key] ? " active" : "")
btn.textContent = label
btn.addEventListener("click", () => {
container.querySelectorAll(".pos-pill").forEach(b => b.classList.remove("active"))
btn.classList.add("active")
window[_key] = label
const isTable = label === "Table"
const tableView = document.querySelector(".team-ratings-table-view")
const scatterView = document.querySelector(".team-ratings-scatter-view")
if (tableView) tableView.style.display = isTable ? "" : "none"
if (scatterView) scatterView.style.display = isTable ? "none" : ""
})
container.appendChild(btn)
}
return container
}Show code
Show code
// ── Scatter plot (always renders, starts hidden) ─────────────
{
if (!footballTeamData || footballTeamData.length === 0) return html``
const metricOpts = [
{ value: "panna", label: "Panna" },
{ value: "offense", label: "Offense" },
{ value: "defense", label: "Defense" }
]
const defaultX = "offense"
const defaultY = "defense"
const headerSrc = Object.fromEntries(metricOpts.map(m => [m.value, m.label]))
const wrapper = document.createElement("div")
wrapper.className = "team-ratings-scatter-view"
wrapper.style.display = window["_viewMode_" + window.location.pathname.replace(/[^a-z0-9]/gi, "_")] === "Scatter" ? "" : "none"
const axisBar = document.createElement("div")
axisBar.className = "scatter-axis-bar"
const xLabel = document.createElement("label")
xLabel.textContent = "X: "
const xSel = document.createElement("select")
for (const opt of metricOpts) {
const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.label; xSel.appendChild(o)
}
xSel.value = defaultX
xLabel.appendChild(xSel)
const yLabel = document.createElement("label")
yLabel.textContent = "Y: "
const ySel = document.createElement("select")
for (const opt of metricOpts) {
const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.label; ySel.appendChild(o)
}
ySel.value = defaultY
yLabel.appendChild(ySel)
axisBar.appendChild(xLabel)
axisBar.appendChild(yLabel)
wrapper.appendChild(axisBar)
const chartDiv = document.createElement("div")
wrapper.appendChild(chartDiv)
function drawChart(xCol, yCol) {
while (chartDiv.firstChild) chartDiv.removeChild(chartDiv.firstChild)
window.chartHelpers.drawScatterPlot(chartDiv, {
data: footballTeamData,
xCol, yCol,
xLabel: headerSrc[xCol] || xCol,
yLabel: headerSrc[yCol] || yCol,
labelCol: "team",
format: { [xCol]: v => Number(v).toFixed(1), [yCol]: v => Number(v).toFixed(1) },
hrefFn: (row) => `team.html#team=${encodeURIComponent(row.team)}`,
tooltipFn: (tip, row, xC, yC, xL, yL, f) => {
const header = document.createElement("div")
header.className = "scatter-tip-header"
const crest = window.footballMaps.teamCrest(row.team)
if (crest) {
const badge = document.createElement("img")
badge.className = "scatter-tip-headshot"
badge.src = crest
badge.alt = ""
badge.style.borderRadius = "4px"
header.appendChild(badge)
}
const info = document.createElement("div")
const nameEl = document.createElement("div")
nameEl.className = "scatter-tip-name"
nameEl.textContent = row.team || ""
info.appendChild(nameEl)
if (row.league) {
const leagueEl = document.createElement("div")
leagueEl.className = "scatter-tip-team"
leagueEl.textContent = String(row.league || "").replace(/_/g, " ")
info.appendChild(leagueEl)
}
header.appendChild(info)
tip.appendChild(header)
const fX = f[xC] ? f[xC](row[xC]) : Number(row[xC]).toFixed(1)
const fY = f[yC] ? f[yC](row[yC]) : Number(row[yC]).toFixed(1)
window.chartHelpers.buildFieldTooltip(tip, "", [[xL, fX], [yL, fY]], true)
const title = tip.querySelector(".ft-title")
if (title && !title.textContent) title.remove()
}
})
}
drawChart(defaultX, defaultY)
xSel.addEventListener("change", () => drawChart(xSel.value, ySel.value))
ySel.addEventListener("change", () => drawChart(xSel.value, ySel.value))
return wrapper
}Show code
{
if (!footballTeamData || footballTeamData.length === 0) {
return html`<p class="text-muted">No team data available.</p>`
}
if (window.footballMaps.loadCrests) await window.footballMaps.loadCrests()
const showLeague = leagueFilter === "All Leagues"
const columns = showLeague
? ["rank", "team", "league", "panna", "offense", "defense", "top_player"]
: ["rank", "team", "panna", "offense", "defense", "top_player"]
const header = { rank: "#", team: "Team", league: "League", panna: "Panna", offense: "Off", defense: "Def", top_player: "Best Player" }
const groups = showLeague
? [{ label: "", span: 3 }, { label: "Team Panna (vs league avg)", span: 3 }, { label: "", span: 1 }]
: [{ label: "", span: 2 }, { label: "Team Panna (vs league avg)", span: 3 }, { label: "", span: 1 }]
const tableEl = statsTable(footballTeamSearch, {
columns, mobileCols: ["rank", "team", "panna", "offense", "defense"], header, groups,
heatmap: { panna: "high-good", offense: "high-good", defense: "low-good" },
heatmapData: footballTeamData,
render: {
team: window.footballMaps?.renderTeamCell || ((v) => `<strong>${statsEsc(v)}</strong>`),
league: (v) => statsEsc(String(v || "").replace(/_/g, " ")),
top_player: (v) => v ? `<a href="player.html#name=${encodeURIComponent(v)}" class="player-link">${statsEsc(v)}</a>` : ""
},
filters: {
panna: "range"
},
sort: "panna",
reverse: true,
rows: 25
})
const wrap = document.createElement("div")
wrap.className = "team-ratings-table-view"
wrap.style.display = window["_viewMode_" + window.location.pathname.replace(/[^a-z0-9]/gi, "_")] === "Table" ? "" : "none"
wrap.appendChild(tableEl)
return wrap
}