In The Game
  • Home
  • Blog
  • AFL
    • Overview

    • Player Stats
    • Player Ratings
    • Player Game Logs
    • Player Comparison

    • Team Stats
    • Team Game Logs
    • Team Ratings

    • Matches
    • Ladder
    • Definitions
  • Football
    • Overview

    • Player Stats
    • Player Ratings
    • Player Game Logs
    • Player Comparison

    • Team Stats
    • Team Game Logs
    • Team Ratings

    • Leagues
    • Matches
    • Definitions
  • About
Skip to content

AFL Player Game Logs

Individual game performances — find the best single-game performances across AFL

AFL > Player Game Logs

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 disposals, highest EPV). High = above average for that stat. Low = below average.

Show code
statsEsc = window.statsEsc
statsTable = window.statsTable
defs = window.aflStatDefs
predToFull = window.aflTeamMaps?.predToFull || {}
fullToPred = window.aflTeamMaps?.fullToPred || {}
shortName = window.aflTeamMaps?.canonicalToShort || {}

base = window.DATA_BASE_URL
Show code
gameLogs = {
  try {
    return await window.fetchParquet(base + "afl/game-logs.parquet")
  } catch (e) {
    console.error("[game-logs] game-logs load failed:", e)
    return null
  }
}

gameStats = {
  try {
    return await window.fetchParquet(base + "afl/game-stats.parquet")
  } catch (e) {
    console.warn("[game-logs] game-stats load failed:", e)
    return null
  }
}

playerDetails = {
  try {
    return await window.fetchParquet(base + "afl/player-details.parquet")
  } catch (e) {
    console.warn("[game-logs] player-details load failed:", e)
    return null
  }
}

ratings = {
  try {
    return await window.fetchParquet(base + "afl/ratings.parquet")
  } catch (e) {
    console.warn("[game-logs] ratings load failed:", e)
    return null
  }
}

predictions = {
  try {
    return await window.fetchParquet(base + "afl/predictions.parquet")
  } catch (e) {
    console.warn("[game-logs] predictions load failed:", e)
    return null
  }
}
Show code
nameMap = {
  const m = new Map()
  if (playerDetails) { for (const d of playerDetails) m.set(d.player_id, d.player_name) }
  if (ratings) { for (const r of ratings) m.set(r.player_id, r.player_name) }
  return m
}

posMap = {
  const m = new Map()
  if (ratings) {
    const sorted = [...ratings].sort((a, b) => (a.season - b.season) || (a.round - b.round))
    for (const r of sorted) m.set(r.player_id, r.position_group)
  }
  return m
}

// Home/Away lookup from predictions
homeAwayMap = {
  const m = new Map()
  if (predictions) {
    for (const p of predictions) {
      const h = predToFull[p.home_team] || p.home_team
      const a = predToFull[p.away_team] || p.away_team
      m.set(`${p.season}-${p.round}-${h}`, "home")
      m.set(`${p.season}-${p.round}-${a}`, "away")
    }
  }
  return m
}
Show code
categories = Object.entries(defs)
  .filter(([k, v]) => v.source !== "ratings" && k !== "custom")
  .map(([k, v]) => ({ key: k, label: v.label }))

viewof category = {
  const _key = "_statCategory_" + window.location.pathname.replace(/[^a-z0-9]/gi, "_")
  const _saved = window[_key] || categories[0].key
  const _default = categories.some(c => c.key === _saved) ? _saved : categories[0].key
  const wrap = document.createElement("div")
  wrap.className = "epv-toggle"
  wrap.value = _default
  for (const cat of categories) {
    const btn = document.createElement("button")
    btn.className = "epv-toggle-btn" + (cat.key === _default ? " active" : "")
    btn.textContent = cat.label
    btn.addEventListener("click", () => {
      wrap.querySelectorAll(".epv-toggle-btn").forEach(b => b.classList.remove("active"))
      btn.classList.add("active")
      wrap.value = cat.key
      window[_key] = cat.key
      wrap.dispatchEvent(new Event("input", { bubbles: true }))
    })
    wrap.appendChild(btn)
  }
  return wrap
}
Show code
seasonOptions = {
  const src = gameStats || gameLogs
  if (!src) return ["All Seasons"]
  const seasons = [...new Set(src.map(d => d.season))].sort((a, b) => b - a)
  return ["All Seasons", ...seasons.map(String)]
}

teamOptions = {
  const src = gameStats || gameLogs
  if (!src) return ["All Teams"]
  const teams = [...new Set(src.map(d => predToFull[d.team] || d.team))].filter(Boolean).sort()
  return ["All Teams", ...teams]
}
Show code
viewof filters = {
  const defaultSeason = seasonOptions[1] || "All Seasons"

  function getRoundRange(season) {
    const src = gameStats || gameLogs
    if (!src) return { min: 0, max: 30 }
    const allGames = season === "All Seasons" ? src : src.filter(d => String(d.season) === season)
    let rMin = Infinity, rMax = -Infinity
    for (const d of allGames) { if (d.round != null) { if (d.round < rMin) rMin = d.round; if (d.round > rMax) rMax = d.round } }
    return { min: rMin === Infinity ? 0 : rMin, max: rMax === -Infinity ? 30 : rMax }
  }

  let roundBounds = getRoundRange(defaultSeason)

  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 season = makeSelect(seasonOptions, defaultSeason, "Season")
  const team = makeSelect(teamOptions, "All Teams", "Team")
  const haSelect = makeSelect(["All", "Home", "Away"], "All", "H/A")
  const seasonType = makeSelect(["All", "Regular", "Finals"], "All", "Type")

  row.appendChild(season.wrap)
  row.appendChild(team.wrap)
  row.appendChild(haSelect.wrap)
  row.appendChild(seasonType.wrap)

  // Round range
  const roundWrap = document.createElement("div")
  roundWrap.className = "filter-round-wrap"
  const rLbl = document.createElement("span")
  rLbl.className = "filter-label"
  rLbl.textContent = "Rounds"
  const rMin = document.createElement("input")
  rMin.type = "number"; rMin.className = "round-input"
  rMin.min = roundBounds.min; rMin.max = roundBounds.max; rMin.value = roundBounds.min
  const rSep = document.createElement("span")
  rSep.className = "round-sep"; rSep.textContent = "–"
  const rMax = document.createElement("input")
  rMax.type = "number"; rMax.className = "round-input"
  rMax.min = roundBounds.min; rMax.max = roundBounds.max; rMax.value = roundBounds.max
  roundWrap.appendChild(rLbl); roundWrap.appendChild(rMin); roundWrap.appendChild(rSep); roundWrap.appendChild(rMax)
  row.appendChild(roundWrap)

  container.appendChild(row)

  container.value = {
    season: defaultSeason,
    team: "All Teams",
    homeAway: "All",
    seasonType: "All",
    roundMin: +rMin.value,
    roundMax: +rMax.value
  }

  function emit() { container.dispatchEvent(new Event("input", { bubbles: true })) }

  season.sel.addEventListener("change", () => {
    roundBounds = getRoundRange(season.sel.value)
    rMin.min = roundBounds.min; rMin.max = roundBounds.max; rMin.value = roundBounds.min
    rMax.min = roundBounds.min; rMax.max = roundBounds.max; rMax.value = roundBounds.max
    container.value = { ...container.value, season: season.sel.value, roundMin: roundBounds.min, roundMax: roundBounds.max }
    emit()
  })
  team.sel.addEventListener("change", () => { container.value = { ...container.value, team: team.sel.value }; emit() })
  haSelect.sel.addEventListener("change", () => { container.value = { ...container.value, homeAway: haSelect.sel.value }; emit() })
  seasonType.sel.addEventListener("change", () => { container.value = { ...container.value, seasonType: seasonType.sel.value }; emit() })

  let roundTimer
  function clampRound(input) {
    if (input.value === "") return
    const v = +input.value
    if (v < roundBounds.min) input.value = roundBounds.min
    if (v > roundBounds.max) input.value = roundBounds.max
  }
  function updateRound() {
    clearTimeout(roundTimer)
    roundTimer = setTimeout(() => {
      clampRound(rMin); clampRound(rMax)
      container.value = { ...container.value, roundMin: rMin.value === "" ? null : +rMin.value, roundMax: rMax.value === "" ? null : +rMax.value }
      emit()
    }, 500)
  }
  rMin.addEventListener("input", updateRound); rMax.addEventListener("input", updateRound)
  rMin.addEventListener("blur", () => { clampRound(rMin); updateRound() })
  rMax.addEventListener("blur", () => { clampRound(rMax); updateRound() })

  return container
}

seasonFilter = filters.season
teamFilter = filters.team
homeAwayFilter = filters.homeAway
seasonTypeFilter = filters.seasonType
roundRange = ({ min: filters.roundMin, max: filters.roundMax })
Show code
// ── Build game log table data ────────────────────────────────
tableData = {
  const catDef = defs[category]
  if (!catDef) return null

  const source = catDef.source
  const rawData = source === "gameLogs" ? gameLogs : gameStats
  if (!rawData) return null

  const effectiveSeason = seasonFilter === "All Seasons" ? null : Number(seasonFilter)

  let games = rawData

  // Season filter
  if (effectiveSeason) games = games.filter(d => d.season === effectiveSeason)

  // Round range filter
  if (roundRange.min != null) games = games.filter(d => d.round >= roundRange.min)
  if (roundRange.max != null) games = games.filter(d => d.round <= roundRange.max)

  // Season type filter
  if (seasonTypeFilter === "Regular") games = games.filter(d => !window.aflTeamMaps.isFinals(d))
  if (seasonTypeFilter === "Finals") games = games.filter(d => window.aflTeamMaps.isFinals(d))

  // Team filter
  if (teamFilter !== "All Teams") {
    const pred = fullToPred[teamFilter] || teamFilter
    games = games.filter(d => d.team === pred || (predToFull[d.team] || d.team) === teamFilter)
  }

  // Home/Away filter
  if (homeAwayFilter !== "All") {
    const target = homeAwayFilter.toLowerCase()
    games = games.filter(d => {
      const teamFull = predToFull[d.team] || d.team
      return homeAwayMap.get(`${d.season}-${d.round}-${teamFull}`) === target
    })
  }

  // Build rows — no aggregation, each game is its own row
  const statCols = catDef.columns
  const dp = catDef.format ? 3 : 1

  const posAbbr = {
    "Key Defender": "KD", "Medium Defender": "MDEF", "Midfielder": "MID",
    "Medium Forward": "MFWD", "Key Forward": "KF", "Ruck": "RK",
    "Wingman": "WNG",
    "KEY_DEFENDER": "KD", "MEDIUM_DEFENDER": "MDEF", "MIDFIELDER": "MID",
    "MEDIUM_FORWARD": "MFWD", "KEY_FORWARD": "KF", "RUCK": "RK",
    "MIDFIELDER_FORWARD": "MFWD", "WINGMAN": "WNG"
  }

  return games.map(g => {
    const name = g.player_name || nameMap.get(g.player_id) || g.player_id
    const teamFull = predToFull[g.team] || g.team
    const opp = g.opponent || g.opp || ""
    const rawPos = posMap.get(g.player_id) || g.position_group || ""
    const posShort = posAbbr[rawPos] || rawPos

    const row = {
      player_id: g.player_id,
      player_name: name,
      team: shortName[teamFull] || teamFull,
      position_group: posShort,
      round: g.round,
      date: g.date || "",
      opponent: shortName[opp] || opp,
      tog: g.time_on_ground_percentage != null ? Math.round(g.time_on_ground_percentage) : null
    }

    for (const col of statCols) {
      const v = Number(g[col])
      row[col] = isNaN(v) ? null : +(v.toFixed(dp))
    }

    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 = defs[category]
  const statCols = catDef.columns
  const posColors = window.aflTeamMaps?.posColors || {}
  const renderPlayerCell = window.aflTeamMaps?.renderPlayerCell
  const renderTeamCell = window.aflTeamMaps?.renderTeamCell

  // Identity columns + stat columns
  const columns = ["player_name", "position_group", "team", "round", "date", "opponent", "tog", ...statCols]

  const header = {
    player_name: "Player", position_group: "POS", team: "Team",
    round: "Rnd", date: "Date", opponent: "Vs", tog: "TOG",
    ...catDef.header
  }

  const format = {}
  for (const col of statCols) {
    const dp = catDef.format?.[col] ?? 1
    format[col] = v => v != null ? Number(v).toFixed(dp) : ""
  }

  const heatmap = {}
  for (const col of statCols) {
    if (catDef.heatmap?.[col]) heatmap[col] = catDef.heatmap[col]
  }

  format.date = v => {
    if (!v) return ""
    const d = new Date(v + "T00:00:00")
    return d.toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" })
  }

  // Abbreviation → color lookup from posColors
  const abbrColorMap = {}
  for (const [k, v] of Object.entries(posColors)) abbrColorMap[v.a] = v.c

  const teamAbbrevMap = window.aflTeamMaps?.teamAbbrevMap || {}

  const render = {}
  if (renderPlayerCell) {
    render.player_name = (val, row) => renderPlayerCell(val, {
      player_id: row.player_id,
      team: row.team,
      position_group: row.position_group
    })
  }
  render.team = (val) => {
    const full = predToFull[val] || val
    const abbr = teamAbbrevMap[full] || teamAbbrevMap[val] || val
    const logo = window.aflTeamMaps?.teamLogo(val)
    const logoHtml = logo ? `<img src="${statsEsc(logo)}" alt="" style="width:18px;height:18px;object-fit:contain;vertical-align:middle" onerror="this.style.display='none'"> ` : ""
    return `<a href="team#team=${encodeURIComponent(full)}" class="player-link">${logoHtml}${statsEsc(abbr)}</a>`
  }
  render.opponent = render.team
  render.position_group = (val) => {
    if (!val) return ""
    const col = abbrColorMap[val] || "#888"
    return `<span class="pos-badge" style="background:${col}">${statsEsc(val)}</span>`
  }

  const groups = [
    { label: "", span: 7 },
    ...(catDef.groups || [{ 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,
    filters: { round: "range" }
  })
}
 

Pete Owen · Sydney · © 2026 · Source

Privacy | Disclaimer