viewof filters = {
const makeSelect = window.footballMaps.makeSelect
const container = document.createElement("div")
container.className = "player-filter-bar"
const row = document.createElement("div")
row.className = "filter-row"
// League select
const leagueOpts = ["All Leagues", ...leagueCodes]
const league = makeSelect(leagueOpts, "ENG", "League", x => x === "All Leagues" ? x : (leagueNames[x] || x))
// Season select (populated after data loads — starts with placeholder)
const season = makeSelect(["All Seasons"], "All Seasons", "Season")
// Matchweek select (populated after data loads — Wed-Tue groupings)
const matchday = makeSelect(["All Matchweeks"], "All Matchweeks", "Matchweek")
// Team / Opp / H-A / Day (populated after matchStats loads)
const team = makeSelect(["All Teams"], "All Teams", "Team")
const opp = makeSelect(["All Opponents"], "All Opponents", "Vs")
const haSelect = makeSelect(["All", "Home", "Away"], "All", "H/A")
const daySelect = makeSelect(["All Days", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], "All Days", "Day")
row.appendChild(league.wrap)
row.appendChild(season.wrap)
row.appendChild(matchday.wrap)
row.appendChild(team.wrap)
row.appendChild(opp.wrap)
row.appendChild(haSelect.wrap)
row.appendChild(daySelect.wrap)
// Date range — populated reactively once matchStats loads. Defaults to
// the played-game window of the selected season (see _updateDates handler).
const dateWrap = document.createElement("div")
dateWrap.className = "filter-round-wrap"
const dLbl = document.createElement("span")
dLbl.className = "filter-label"
dLbl.textContent = "Dates"
const dMin = document.createElement("input")
dMin.type = "date"; dMin.className = "round-input"; dMin.style.minWidth = "8.5rem"
const dSep = document.createElement("span")
dSep.className = "round-sep"; dSep.textContent = "–"
const dMax = document.createElement("input")
dMax.type = "date"; dMax.className = "round-input"; dMax.style.minWidth = "8.5rem"
dateWrap.appendChild(dLbl); dateWrap.appendChild(dMin); dateWrap.appendChild(dSep); dateWrap.appendChild(dMax)
row.appendChild(dateWrap)
// Agg toggle. Hidden for ratings-sourced tabs (Value, EPV components, Ratings)
// because those values come pre-aggregated as per-90 in ratings.parquet — the
// toggle would do nothing, and a non-functional control reads as a bug.
const aggWrap = document.createElement("div")
aggWrap.className = "filter-agg-wrap"
aggWrap.dataset.role = "agg-toggle"
const btnAvg = document.createElement("button")
btnAvg.className = "agg-btn"
btnAvg.textContent = "Per Game"
btnAvg.dataset.mode = "avg"
const btnP90 = document.createElement("button")
btnP90.className = "agg-btn"
btnP90.textContent = "Per 90"
btnP90.dataset.mode = "p90"
const btnTot = document.createElement("button")
btnTot.className = "agg-btn active"
btnTot.textContent = "Total"
btnTot.dataset.mode = "total"
aggWrap.appendChild(btnAvg)
aggWrap.appendChild(btnP90)
aggWrap.appendChild(btnTot)
row.appendChild(aggWrap)
container.appendChild(row)
// ── State and dispatch ──
container.value = {
league: "ENG", season: "All Seasons", matchday: "All Matchweeks",
team: "All Teams", opponent: "All Opponents", homeAway: "All", day: "All Days",
dateMin: "", dateMax: "",
aggMode: "total"
}
function emit() {
container.dispatchEvent(new Event("input", { bubbles: true }))
}
league.sel.addEventListener("change", () => {
container.value = { ...container.value, league: league.sel.value, team: "All Teams", opponent: "All Opponents" }
emit()
})
season.sel.addEventListener("change", () => {
container.value = { ...container.value, season: season.sel.value, matchday: "All Matchweeks", team: "All Teams", opponent: "All Opponents" }
emit()
})
matchday.sel.addEventListener("change", () => {
container.value = { ...container.value, matchday: matchday.sel.value }
emit()
})
team.sel.addEventListener("change", () => {
container.value = { ...container.value, team: team.sel.value }
emit()
})
opp.sel.addEventListener("change", () => {
container.value = { ...container.value, opponent: opp.sel.value }
emit()
})
haSelect.sel.addEventListener("change", () => {
container.value = { ...container.value, homeAway: haSelect.sel.value }
emit()
})
daySelect.sel.addEventListener("change", () => {
container.value = { ...container.value, day: daySelect.sel.value }
emit()
})
let dateTimer
function updateDate() {
clearTimeout(dateTimer)
dateTimer = setTimeout(() => {
container.value = { ...container.value, dateMin: dMin.value || "", dateMax: dMax.value || "" }
emit()
}, 300)
}
dMin.addEventListener("change", updateDate)
dMax.addEventListener("change", updateDate)
for (const btn of [btnAvg, btnP90, btnTot]) {
btn.addEventListener("click", () => {
aggWrap.querySelectorAll("button").forEach(b => b.classList.remove("active"))
btn.classList.add("active")
container.value = { ...container.value, aggMode: btn.dataset.mode }
emit()
})
}
// One-time flag so we auto-default to the latest real season on first data load,
// but never clobber the user's explicit choice on subsequent re-renders.
let _seasonAutoDefaulted = false
// Expose method to update season options externally
container._updateSeasons = (options, defaultVal) => {
const currentVal = container.value.season
while (season.sel.firstChild) season.sel.removeChild(season.sel.firstChild)
for (const opt of options) {
const o = document.createElement("option")
o.value = opt
o.textContent = opt
season.sel.appendChild(o)
}
// Preserve user's current selection if still valid
if (options.includes(currentVal)) {
season.sel.value = currentVal
}
// One-time: auto-default to latest real season when data first arrives.
// Subsequent calls respect whatever the user has picked.
if (!_seasonAutoDefaulted && defaultVal && defaultVal !== "All Seasons" && options.includes(defaultVal)) {
_seasonAutoDefaulted = true
container.value = { ...container.value, season: defaultVal, matchday: "All Matchweeks" }
season.sel.value = defaultVal
container.dispatchEvent(new Event("input", { bubbles: true }))
}
}
// Expose method to update matchday options externally
container._updateMatchdays = (options) => {
const currentVal = container.value.matchday
while (matchday.sel.firstChild) matchday.sel.removeChild(matchday.sel.firstChild)
for (const opt of options) {
const o = document.createElement("option")
o.value = opt
o.textContent = opt
matchday.sel.appendChild(o)
}
// Preserve user's current matchweek selection if still valid; otherwise fall
// back to "All Matchweeks". Never force-reset when the user has an active pick.
if (options.includes(currentVal)) {
matchday.sel.value = currentVal
} else {
matchday.sel.value = "All Matchweeks"
container.value = { ...container.value, matchday: "All Matchweeks" }
container.dispatchEvent(new Event("input", { bubbles: true }))
}
}
// Generic option-list refresher for the new selects (Team, Opp).
// Day select is static so doesn't need this.
function makeUpdater(sel, key, allLabel) {
return (options) => {
const currentVal = container.value[key]
while (sel.firstChild) sel.removeChild(sel.firstChild)
for (const opt of options) {
const o = document.createElement("option")
o.value = opt; o.textContent = opt
sel.appendChild(o)
}
if (options.includes(currentVal)) {
sel.value = currentVal
} else {
sel.value = allLabel
container.value = { ...container.value, [key]: allLabel }
container.dispatchEvent(new Event("input", { bubbles: true }))
}
}
}
container._updateTeams = makeUpdater(team.sel, "team", "All Teams")
container._updateOpps = makeUpdater(opp.sel, "opponent", "All Opponents")
// Expose method to set date inputs to a season's played-game window.
// Called from the matchStats data-load cell whenever season changes.
container._updateDates = (min, max) => {
dMin.value = min || ""
dMax.value = max || ""
container.value = { ...container.value, dateMin: dMin.value, dateMax: dMax.value }
container.dispatchEvent(new Event("input", { bubbles: true }))
}
return container
}
// ── Destructure for downstream reactivity ────────────────────
leagueFilter = filters.league
seasonFilter = filters.season
matchdayFilter = filters.matchday
teamFilter = filters.team
oppFilter = filters.opponent
homeAwayFilter = filters.homeAway
dayFilter = filters.day
dateRange = ({ min: filters.dateMin, max: filters.dateMax })
aggMode = filters.aggMode