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 Team Game Logs

Team-level game performances — find the best single-game team performances across AFL

AFL > Team Game Logs

Team-level game performances — each row is one team in one match. Sort by any stat column to find the best single-game team performances (e.g., most handballs, 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("[team-game-logs] game-logs load failed:", e)
    return null
  }
}

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

predictions = {
  try {
    return await window.fetchParquet(base + "afl/predictions.parquet")
  } catch (e) {
    console.warn("[team-game-logs] predictions load failed:", e)
    return null
  }
}
Show code
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
}

dateMap = {
  const m = new Map()
  if (predictions) {
    for (const p of predictions) {
      if (!p.start_time) continue
      const d = p.start_time.slice(0, 10)
      const h = predToFull[p.home_team] || p.home_team
      const a = predToFull[p.away_team] || p.away_team
      // Key by team+opponent to handle double-headers (same round, different games)
      m.set(`${p.season}-${p.round}-${h}-${a}`, d)
      m.set(`${p.season}-${p.round}-${a}-${h}`, d)
    }
  }
  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 = "_teamGameLogCat_" + 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 team 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)
  const norm = n => predToFull[n] || n

  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
  const isFinals = window.aflTeamMaps?.isFinals || (() => false)
  if (seasonTypeFilter === "Regular") games = games.filter(d => !isFinals(d))
  if (seasonTypeFilter === "Finals") games = games.filter(d => isFinals(d))

  // Aggregate player rows into team-game rows (exclude computed cols from sum)
  const computedCols = new Set(["score"])
  const statCols = catDef.columns
  const sumCols = statCols.filter(c => !computedCols.has(c))
  const teamGameMap = new Map()

  for (const row of games) {
    const t = norm(row.team)
    if (!t) continue
    const round = Number(row.round)
    const season = Number(row.season)
    const opp = norm(row.opponent || row.opp || "")
    // Key by opponent too — handles double-headers (e.g., rescheduled games in same round)
    const key = `${season}|||${round}|||${t}|||${opp}`

    if (!teamGameMap.has(key)) {
      teamGameMap.set(key, { team: t, season, round, opponent: opp, vals: {} })
      for (const c of sumCols) teamGameMap.get(key).vals[c] = 0
    }
    const entry = teamGameMap.get(key)
    for (const c of sumCols) {
      entry.vals[c] = (entry.vals[c] || 0) + (Number(row[c]) || 0)
    }
  }

  // Apply team filter
  let entries = [...teamGameMap.values()]
  if (teamFilter !== "All Teams") {
    entries = entries.filter(e => e.team === teamFilter)
  }

  // Home/Away filter
  if (homeAwayFilter !== "All") {
    const target = homeAwayFilter.toLowerCase()
    entries = entries.filter(e => {
      return homeAwayMap.get(`${e.season}-${e.round}-${e.team}`) === target
    })
  }

  // Build output rows
  const dp = catDef.format ? 3 : 1
  return entries.map(e => {
    const row = {
      team: shortName[e.team] || e.team,
      team_full: e.team,
      season: e.season,
      round: e.round,
      date: dateMap.get(`${e.season}-${e.round}-${e.team}-${e.opponent}`) || "",
      opponent: shortName[e.opponent] || e.opponent
    }
    for (const col of sumCols) {
      const v = e.vals[col]
      row[col] = isNaN(v) ? null : +(v.toFixed(dp))
    }
    // Computed columns
    if (row.goals != null || row.behinds != null) {
      row.score = (row.goals || 0) * 6 + (row.behinds || 0)
    }
    return row
  })
}
Show code
viewof search = tableData == null
  ? html``
  : Inputs.search(tableData, { placeholder: "Search teams..." })
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 renderTeamCell = window.aflTeamMaps?.renderTeamCell
  const showSeason = seasonFilter === "All Seasons"

  const idCols = showSeason
    ? ["team", "season", "round", "date", "opponent"]
    : ["team", "round", "date", "opponent"]
  const columns = [...idCols, ...statCols]

  const header = {
    team: "Team", season: "Season", round: "Rnd", date: "Date", opponent: "Vs",
    ...catDef.header
  }

  const format = {}
  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" })
  }
  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]
  }

  const render = {}
  if (renderTeamCell) {
    render.team = (val, row) => renderTeamCell(row.team_full || val)
    // Opponent always compact: logo + abbreviation
    render.opponent = (val) => {
      const esc = window.statsEsc || (s => s)
      const full = predToFull[val] || val
      const abbr = window.aflTeamMaps?.predToAbbr?.[full] || window.aflTeamMaps?.teamAbbrevMap?.[full] || val
      const logo = window.aflTeamMaps?.teamLogo?.(val)
      const logoHtml = logo ? '<img src="' + esc(logo) + '" alt="" style="width:18px;height:18px;object-fit:contain;vertical-align:middle">' : ""
      return '<span style="display:inline-flex;align-items:center;gap:0.3rem">' + logoHtml + esc(abbr) + '</span>'
    }
  }

  const groups = [
    { label: "", span: idCols.length },
    ...(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