Skip to content
Individual game performances — each row is one player in one match. Sort by any stat column to find the best single-game performances (e.g., most goals, most tackles). High = above average for that stat. Low = below average.
Show code
statsEsc = window . statsEsc
statsTable = window . statsTable
base = window . DATA_BASE_URL
leagueCodes = window . footballMaps . domesticLeagues
leagueNames = window . footballMaps . leagueNames
posToGroup = window . footballMaps . posToGroup
footballPosColors = window . footballMaps . posGroupColors
statDefs = window . footballStatDefs || {}
// Categories: exclude ratings and custom
catKeys = Object . keys (statDefs). filter (k => ! statDefs[k]. page && k !== "custom" )
Show code
// ── Category toggle ──────────────────────────────────────────
viewof category = {
const _key = "_statCategory_" + window . location . pathname . replace (/ [^a-z0-9] /gi , "_" )
const _saved = window [_key] || "scoring"
const _default = catKeys. includes (_saved) ? _saved : "scoring"
const container = html `<div class="stats-category-toggle football"></div>`
for (const key of catKeys) {
const btn = document . createElement ("button" )
btn. className = "stats-cat-btn" + (key === _default ? " active" : "" )
btn. textContent = statDefs[key]. label
btn. dataset . cat = key
btn. addEventListener ("click" , () => {
container. querySelectorAll (".stats-cat-btn" ). forEach (b => b. classList . remove ("active" ))
btn. classList . add ("active" )
container. value = key
window [_key] = key
container. dispatchEvent (new Event ("input" , { bubbles : true }))
})
container. appendChild (btn)
}
container. value = _default
return container
}
Show code
_ratingsRaw = {
try { return await window . fetchParquet (base + "football/ratings.parquet" ) } catch (e) { return null }
}
_ratingsPositions = {
if (! _ratingsRaw) return {}
const m = {}
for (const r of _ratingsRaw) { if (r. player_name && r. position ) m[r. player_name ] = r. position }
return m
}
Show code
viewof filters = {
function makeSelect (options, defaultVal, label) {
const wrap = document . createElement ("div" )
wrap. className = "filter-select-wrap"
const lbl = document . createElement ("span" )
lbl. className = "filter-label"
lbl. textContent = label
const sel = document . createElement ("select" )
sel. className = "filter-select"
for (const opt of options) {
const o = document . createElement ("option" )
o. value = opt; o. textContent = opt
if (opt === defaultVal) o. selected = true
sel. appendChild (o)
}
wrap. appendChild (lbl); wrap. appendChild (sel)
return { wrap, sel }
}
const container = document . createElement ("div" )
container. className = "player-filter-bar"
const row = document . createElement ("div" )
row. className = "filter-row"
const leagueOpts = ["All Leagues" , ... leagueCodes. map (c => ` ${ leagueNames[c] || c} ( ${ c} )` )]
const league = makeSelect (leagueOpts, leagueOpts. find (l => l. includes ("ENG" )) || leagueOpts[0 ], "League" )
const season = makeSelect (["All Seasons" ], "All Seasons" , "Season" )
row. appendChild (league. wrap )
row. appendChild (season. wrap )
container. appendChild (row)
container. value = { league : "ENG" , season : "All Seasons" }
function emit () { container. dispatchEvent (new Event ("input" , { bubbles : true })) }
league. sel . addEventListener ("change" , () => {
const raw = league. sel . value
const code = raw === "All Leagues" ? "All Leagues" : raw. match (/ \((\w+)\)$ / )?. [1 ] || raw
container. value = { ... container. value , league : code, season : "All Seasons" }
emit ()
})
season. sel . addEventListener ("change" , () => {
container. value = { ... container. value , season : season. sel . value }
emit ()
})
container. _updateSeasons = function (opts, defaultVal) {
const oldVal = container. value . season
while (season. sel . firstChild ) season. sel . removeChild (season. sel . firstChild )
for (const opt of opts) {
const o = document . createElement ("option" )
o. value = opt; o. textContent = opt
if (opt === defaultVal) o. selected = true
season. sel . appendChild (o)
}
if (oldVal !== defaultVal) {
container. value = { ... container. value , season : defaultVal }
emit ()
}
}
return container
}
leagueFilter = filters. league
seasonFilter = filters. season
Show code
matchStats = {
const codes = leagueFilter === "All Leagues" ? leagueCodes : [leagueFilter]
const results = []
for (const code of codes) {
try {
const data = await window . fetchParquet (base + `football/match-stats- ${ code} .parquet` )
if (data) results. push (... data)
} catch (e) {
console . warn (`[game-logs] match-stats- ${ code} load failed:` , e)
}
}
return results. length > 0 ? results : null
}
// Load game-logs for Value tab
_gameLogs = {
try { return await window . fetchParquet (base + "football/game-logs.parquet" ) } catch (e) { return null }
}
seasonOptions = {
const src = matchStats || _gameLogs
if (! src) return ["All Seasons" ]
const seasons = [... new Set (src. map (d => String (d. season )))]. sort (). reverse ()
return ["All Seasons" , ... seasons]
}
// Update season dropdown
{
const el = document . querySelector (".player-filter-bar" )
if (el && el. _updateSeasons ) {
const defaultSeason = seasonOptions[1 ] || "All Seasons"
el. _updateSeasons (seasonOptions, defaultSeason)
}
}
Show code
// ── Build game log table data ────────────────────────────────
tableData = {
const catDef = statDefs[category]
if (! catDef) return null
const source = catDef. source
let rawData
if (source === "gameLogs" ) {
rawData = _gameLogs
} else {
rawData = matchStats
}
if (! rawData) return null
const effectiveSeason = seasonFilter === "All Seasons" ? null : seasonFilter
let games = rawData
// League filter (for game-logs which may span leagues)
if (leagueFilter !== "All Leagues" && source === "gameLogs" ) {
games = games. filter (d => d. league === leagueFilter)
}
// Season filter
if (effectiveSeason) games = games. filter (d => String (d. season ) === effectiveSeason)
// Build rows — no aggregation
const statCols = catDef. columns . filter (c => c !== "pass_pct" )
return games. map (g => {
const pos = (g. position === "Substitute" || g. position === "Sub" )
? (_ratingsPositions[g. player_name ] || g. position )
: g. position
const posGroup = posToGroup[pos] || ""
const matchDate = g. match_date ? String (g. match_date ). replace ("Z" , "" ). slice (0 , 10 ) : ""
const row = {
player_name : g. player_name || "" ,
pos_group : posGroup,
team : g. team_name || "" ,
opponent : g. opponent || "" ,
date : matchDate,
mins : g. minsPlayed || 0
}
for (const col of statCols) {
const v = Number (g[col])
row[col] = isNaN (v) ? null : + (v. toFixed (1 ))
}
// Computed columns
if (catDef. compute ) {
row. pass_pct = row. passes > 0 ? Math . round (100 * row. passes_accurate / row. passes ) : null
}
return row
})
}
Show code
viewof search = tableData == null
? html ``
: Inputs. search (tableData, { placeholder : "Search players..." })
Show code
// ── Render table ─────────────────────────────────────────────
{
if (tableData == null || tableData. length === 0 )
return html `<p class="text-muted">No data available for this category and filter combination.</p>`
const catDef = statDefs[category]
const statCols = catDef. columns
const columns = ["player_name" , "pos_group" , "team" , "opponent" , "date" , "mins" , ... statCols]
const header = {
player_name : "Player" , pos_group : "POS" , team : "Team" ,
opponent : "Vs" , date : "Date" , mins : "Mins" ,
... catDef. header
}
const format = {}
for (const col of statCols) {
format[col] = v => v != null ? String (Math . round (Number (v))) : ""
}
const heatmap = {}
for (const col of statCols) {
if (catDef. heatmap ?. [col]) heatmap[col] = catDef. heatmap [col]
}
const render = {}
const renderTeamCell = window . footballMaps ?. renderTeamCell
if (renderTeamCell) {
render. team = (val) => renderTeamCell (val)
render. opponent = (val) => renderTeamCell (val)
}
render. pos_group = (val) => {
if (! val) return ""
const col = footballPosColors[val] || "#888"
return `<span class="pos-badge" style="background: ${ col} "> ${ statsEsc (val)} </span>`
}
const groups = [
{ label : "" , span : 6 },
{ label : catDef. label , span : statCols. length }
]
const sortCol = catDef. sortCol || statCols[0 ]
return statsTable (search, {
columns, header, groups, format, heatmap, render,
sort : sortCol, reverse : true ,
pageSize : 25
})
}