Module:Standingsv3
From elfpedia.eu
Documentation for this module may be created at Module:Standingsv3/doc
local M = {}
-- Constant for the maximum number of games per team in the regular season
local MAX_GAMES_PER_TEAM = 12
-- Division assignments (This data is within the module but can be centralized if needed)
local teamDivisions = {
-- North Division
["Rhein Fire"] = "North",
["Nordic Storm"] = "North",
["Hamburg Sea Devils"] = "North",
["Berlin Thunder"] = "North",
-- South Division
["Madrid Bravos"] = "South",
["Munich Ravens"] = "South",
["Raiders Tirol"] = "South",
["Helvetic Mercenaries"] = "South",
-- West Division
["Stuttgart Surge"] = "West",
["Frankfurt Galaxy"] = "West",
["Paris Musketeers"] = "West",
["Cologne Centurions"] = "West",
-- East Division
["Vienna Vikings"] = "East",
["Prague Lions"] = "East",
["Panthers Wrocław"] = "East",
["Fehérvár Enthroners"] = "East",
}
-- Defined colors for division headers
local divisionHeaderColors = {
North = "#4B04D9",
West = "#193368",
South = "#BC1F25",
East = "#17462D",
}
-- Defined colors for playoff status in the table rows
local playoffRowColors = {
eliminated = "#FFDDDD", -- Light red for eliminated teams
qualified_wildcard = "#E0F0FF", -- Light blue for qualified wildcards
qualified_division = "#FFF8E0", -- Light yellow for qualified division winners (without bye)
qualified_bye = "#E0FFE0", -- Light green for qualified division winners (with bye)
}
-- Playoff symbols are no longer directly added to team names, relying on row colors.
-- Keeping this table for reference or potential future use, but not for direct display in table.
local playoffSymbols = {
-- Symbols are deprecated for direct display, relying on row colors.
-- qualified = "✓",
-- won_division = "✓✓",
-- homefield_advantage = "ʜ",
-- eliminated = "†",
}
-- Function to retrieve game data from Cargo
function M.getGameData(season)
local cargo_query_params = {
tables = "GameSchedule",
fields = "AwayTeam, HomeTeam, AwayScore, HomeScore, IF(AwayScore > HomeScore, AwayTeam, IF(HomeScore > AwayScore, HomeTeam, ''))=Winner, IsPlayoff, Week, Phase, PlayoffRound",
where = "Season=" .. tostring(season) .. " AND Phase='Regular season' AND IsPlayoff=0 AND PlayoffRound=0",
order_by = "Week ASC, GameID ASC",
limit = "500" -- Increase limit if more than 500 games per season
}
local query = mw.ext.cargo.query(cargo_query_params.tables, cargo_query_params.fields, cargo_query_params)
return query
end
-- Calculates the last N games of a team as an LWLWW string with colors
function M.calculateTeamLastGames(teamName, allGames, numGames)
numGames = numGames or 3 -- Default to 3
local teamGames = {}
for _, game in ipairs(allGames) do
-- Ensure the game is completed to have a W/L
local homeScore = tonumber(game.HomeScore)
local awayScore = tonumber(game.AwayScore)
if type(homeScore) == 'number' and type(awayScore) == 'number' then
if game.AwayTeam == teamName or game.HomeTeam == teamName then
table.insert(teamGames, game)
end
end
end
table.sort(teamGames, function(a, b)
local weekA = tonumber(a.Week) or 0
local weekB = tonumber(b.Week) or 0
if weekA ~= weekB then
return weekA < weekB
end
return false
end)
local lastGamesOutput = {}
local gamesCount = 0
for i = #teamGames, 1, -1 do
if gamesCount >= numGames then
break
end
local game = teamGames[i]
local outcome = ""
local color = ""
if game.Winner == teamName then
outcome = "W"
color = "#008000"
elseif game.Winner ~= "" then -- if there's a winner and it's not our team, it's a loss
outcome = "L"
color = "#FF0000"
else
outcome = "" -- Should not happen for completed games with a winner
end
if outcome ~= "" then -- Only count games with a defined outcome (W/L)
table.insert(lastGamesOutput, 1, '<span style="color:' .. color .. '; font-weight:bold;">' .. outcome .. '</span>')
gamesCount = gamesCount + 1
end
end
if #lastGamesOutput > 0 then
return table.concat(lastGamesOutput, "")
else
return '-'
end
end
-- Function to calculate team statistics and gather opponents for SOV/SOS
function M.calculateTeamStats(games)
local teamStats = {}
-- First pass: Initialize teams and collect basic statistics (W/L, PF/PA)
-- and the history of games for SOV/SOS
for _, game in ipairs(games) do
local awayTeam = game.AwayTeam
local homeTeam = game.HomeTeam
local awayScore = tonumber(game.AwayScore) or 0
local homeScore = tonumber(game.HomeScore) or 0
local winner = game.Winner
-- Ensure teams are initialized
if not teamStats[awayTeam] then
teamStats[awayTeam] = { W = 0, L = 0, PF = 0, PA = 0, DivW = 0, DivL = 0, Name = awayTeam, GamesPlayedHistory = {}, H2HMatches = {}, AwayPF = 0 }
end
if not teamStats[homeTeam] then
teamStats[homeTeam] = { W = 0, L = 0, PF = 0, PA = 0, DivW = 0, DivL = 0, Name = homeTeam, GamesPlayedHistory = {}, H2HMatches = {}, AwayPF = 0 }
end
-- Only if the game is completed, update W/L/PF/PA and H2H
local gamePlayed = (type(tonumber(game.AwayScore)) == 'number' and type(tonumber(game.HomeScore)) == 'number')
if gamePlayed then
teamStats[awayTeam].PF = teamStats[awayTeam].PF + awayScore
teamStats[awayTeam].PA = teamStats[awayTeam].PA + homeScore
teamStats[homeTeam].PF = teamStats[homeTeam].PF + homeScore
teamStats[homeTeam].PA = teamStats[homeTeam].PA + awayScore
if winner == awayTeam then
teamStats[awayTeam].W = teamStats[awayTeam].W + 1
teamStats[homeTeam].L = teamStats[homeTeam].L + 1
elseif winner == homeTeam then
teamStats[homeTeam].W = teamStats[homeTeam].W + 1
teamStats[awayTeam].L = teamStats[awayTeam].L + 1
end
local awayDivision = teamDivisions[awayTeam]
local homeDivision = teamDivisions[homeTeam]
if awayDivision and homeDivision and awayDivision == homeDivision then
if winner == awayTeam then
teamStats[awayTeam].DivW = teamStats[awayTeam].DivW + 1
teamStats[homeTeam].DivL = teamStats[homeTeam].DivL + 1
elseif winner == homeTeam then
teamStats[homeTeam].DivW = teamStats[homeTeam].DivW + 1
teamStats[awayTeam].DivL = teamStats[awayTeam].DivL + 1
end
end
-- Store H2H match details
if teamStats[awayTeam].H2HMatches[homeTeam] == nil then teamStats[awayTeam].H2HMatches[homeTeam] = { wins = 0, losses = 0, pf = 0, pa = 0, away_pf = 0 } end
if teamStats[homeTeam].H2HMatches[awayTeam] == nil then teamStats[homeTeam].H2HMatches[awayTeam] = { wins = 0, losses = 0, pf = 0, pa = 0, away_pf = 0 } end
if winner == awayTeam then
teamStats[awayTeam].H2HMatches[homeTeam].wins = teamStats[awayTeam].H2HMatches[homeTeam].wins + 1
teamStats[homeTeam].H2HMatches[awayTeam].losses = teamStats[homeTeam].H2HMatches[awayTeam].losses + 1
elseif winner == homeTeam then
teamStats[homeTeam].H2HMatches[awayTeam].wins = teamStats[homeTeam].H2HMatches[awayTeam].wins + 1
teamStats[awayTeam].H2HMatches[homeTeam].losses = teamStats[awayTeam].H2HMatches[homeTeam].losses + 1
end
teamStats[awayTeam].H2HMatches[homeTeam].pf = (teamStats[awayTeam].H2HMatches[homeTeam].pf or 0) + awayScore
teamStats[awayTeam].H2HMatches[homeTeam].pa = (teamStats[awayTeam].H2HMatches[homeTeam].pa or 0) + homeScore
teamStats[homeTeam].H2HMatches[awayTeam].pf = (teamStats[homeTeam].H2HMatches[awayTeam].pf or 0) + homeScore
teamStats[homeTeam].H2HMatches[awayTeam].pa = (teamStats[homeTeam].H2HMatches[awayTeam].pa or 0) + awayScore
-- H2H Away Points: Points scored by the team as the away team in the H2H match
if game.AwayTeam == awayTeam then
teamStats[awayTeam].H2HMatches[homeTeam].away_pf = (teamStats[awayTeam].H2HMatches[homeTeam].away_pf or 0) + awayScore
end
if game.AwayTeam == homeTeam then -- Correct H2H away_pf for homeTeam when it was AWAY
teamStats[homeTeam].H2HMatches[awayTeam].away_pf = (teamStats[homeTeam].H2HMatches[awayTeam].away_pf or 0) + awayScore
end
end
-- Store ALL games for SOV/SOS calculation later, regardless of whether they are played
-- This 'GamesPlayedHistory' will contain all opponents on the schedule.
table.insert(teamStats[awayTeam].GamesPlayedHistory, { opponent = homeTeam, is_win = (gamePlayed and winner == awayTeam) })
table.insert(teamStats[homeTeam].GamesPlayedHistory, { opponent = awayTeam, is_win = (gamePlayed and winner == homeTeam) })
end
-- Recalculate overall AwayPF accurately after all games are processed
for teamName, stats in pairs(teamStats) do
stats.AwayPF = 0 -- Reset to recalculate accurately
for _, game in ipairs(games) do
local currentTeamScore = tonumber(game.AwayScore) -- Score if the current team was away
local opponentTeamScore = tonumber(game.HomeScore) -- Opponent's score if current team was away
if type(currentTeamScore) == 'number' and type(opponentTeamScore) == 'number' then -- Game was played
if game.AwayTeam == teamName then
stats.AwayPF = stats.AwayPF + currentTeamScore
end
end
end
end
-- Second pass for derived statistics (Win%, PD, RemainingGames, SOV, SOS)
for teamName, stats in pairs(teamStats) do
stats.WinP = (stats.W + stats.L > 0) and (stats.W / (stats.W + stats.L)) or 0
stats.DivWinP = (stats.DivW + stats.DivL > 0) and (stats.DivW / (stats.DivW + stats.DivL)) or 0
stats.PD = stats.PF - stats.PA
-- Number of games played and remaining games
stats.GamesPlayedCount = stats.W + stats.L
stats.RemainingGames = MAX_GAMES_PER_TEAM - stats.GamesPlayedCount
if stats.RemainingGames < 0 then stats.RemainingGames = 0 end -- Cannot be negative
-- Initialize SOV/SOS fields
stats.SOV = 0
stats.SOS = 0
local defeatedOpponentWins = 0
local defeatedOpponentLosses = 0
local allScheduleOpponentWins = 0
local allScheduleOpponentLosses = 0
-- Collect data for SOV and SOS
-- Iterate through the team's entire game history
for _, gameInfo in ipairs(stats.GamesPlayedHistory) do
local opponentStats = teamStats[gameInfo.opponent] -- Get the opponent's current W/L
if opponentStats then
-- For SOS: Sum W/L of all opponents on the schedule
allScheduleOpponentWins = allScheduleOpponentWins + opponentStats.W
allScheduleOpponentLosses = allScheduleOpponentLosses + opponentStats.L
-- For SOV: Sum W/L only of defeated opponents
if gameInfo.is_win then -- is_win is true if the team won the game AND it was played
defeatedOpponentWins = defeatedOpponentWins + opponentStats.W
defeatedOpponentLosses = defeatedOpponentLosses + opponentStats.L
end
end
end
-- Calculate SOV and SOS
local sov_denom = defeatedOpponentWins + defeatedOpponentLosses
stats.SOV = (sov_denom > 0) and (defeatedOpponentWins / sov_denom) or 0
local sos_denom = allScheduleOpponentWins + allScheduleOpponentLosses
stats.SOS = (sos_denom > 0) and (allScheduleOpponentWins / sos_denom) or 0
end
return teamStats
end
-- Tie-breaking function with all rules
function M.compareTeams(statsA, statsB, season, is_for_division_sorting)
is_for_division_sorting = is_for_division_sorting or false
-- 1. Number of wins
if statsA.W ~= statsB.W then
return statsA.W > statsB.W
end
-- DIVISION-SPECIFIC TIE-BREAKER: Win Percentage within Division
if is_for_division_sorting then
if statsA.DivWinP ~= statsB.DivWinP then
return statsA.DivWinP > statsB.DivWinP
end
end
-- 2. Head-to-head matchup (direct wins/losses between A and B)
local h2h_A = statsA.H2HMatches[statsB.Name]
local h2h_B = statsB.H2HMatches[statsA.Name]
local h2h_A_wins = (h2h_A and h2h_A.wins) or 0
local h2h_B_wins = (h2h_B and h2h_B.wins) or 0
if (h2h_A_wins + h2h_B_wins) > 0 then
if h2h_A_wins ~= h2h_B_wins then
return h2h_A_wins > h2h_B_wins
end
end
-- 3. Strength of Victory (SOV)
if statsA.SOV ~= statsB.SOV then
return statsA.SOV > statsB.SOV
end
-- 4. Strength of Schedule (SOS)
if statsA.SOS ~= statsB.SOS then
return statsA.SOS > statsB.SOS
end
-- More specific H2H tie-breakers
if (h2h_A_wins + h2h_B_wins) > 0 then
local h2h_A_pf = (h2h_A and h2h_A.pf) or 0
local h2h_A_pa = (h2h_A and h2h_A.pa) or 0
local h2h_B_pf = (h2h_B and h2h_B.pf) or 0
local h2h_B_pa = (h2h_B and h2h_B.pa) or 0
-- 5. Points difference in head-to-head matchups
local h2h_A_pd = h2h_A_pf - h2h_A_pa
local h2h_B_pd = h2h_B_pf - h2h_B_pa
if h2h_A_pd ~= h2h_B_pd then
return h2h_A_pd > h2h_B_pd
end
-- 6. Points scored at away games of head-to-head matchups
local h2h_A_away_pf = (h2h_A and h2h_A.away_pf) or 0
local h2h_B_away_pf = (h2h_B and h2h_B.away_pf) or 0
if h2h_A_away_pf ~= h2h_B_away_pf then
return h2h_A_away_pf > h2h_B_away_pf
end
end
-- 7. Total point difference (PD)
if statsA.PD ~= statsB.PD then
return statsA.PD > statsB.PD
end
-- 8. Total points scored (PF)
if statsA.PF ~= statsB.PF then
return statsA.PF > statsB.PF
end
-- 9. Points scored at away games (AwayPF) - total points scored by the team in all its away games
if statsA.AwayPF ~= statsB.AwayPF then
return statsA.AwayPF > statsB.AwayPF
end
-- 10. Coin toss (alphabetical as last resort)
return statsA.Name < statsB.Name
end
-- Function to determine playoff teams and seeding, as well as elimination
function M.getPlayoffTeams(allTeamStats, season)
local divisionWinners = {}
local nonDivisionWinners = {}
-- playoffStatus stores { status = "...", rank = N, isQualified = bool, isEliminated = bool, isSafeDivWinner = bool, isQualifiedAsDivWinner = bool, hasBye = bool }
local playoffStatus = {}
local divisions = {}
-- Group teams by division
for teamName, stats in pairs(allTeamStats) do
local division = teamDivisions[teamName]
if division then
if not divisions[division] then
divisions[division] = {}
end
table.insert(divisions[division], { Team = teamName, Stats = stats, Division = division })
playoffStatus[teamName] = { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
else
-- Initialize teams without division if they are in allTeamStats
playoffStatus[teamName] = { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
end
end
-- 1. Determine Division Winners and check their secure qualification
for divName, teamsInDiv in pairs(divisions) do
if #teamsInDiv > 0 then
-- Sort Division Teams using division-specific tie-breakers
table.sort(teamsInDiv, function(a, b)
return M.compareTeams(a.Stats, b.Stats, season, true)
end)
local divWinnerCandidate = teamsInDiv[1]
table.insert(divisionWinners, divWinnerCandidate)
-- Check for "Secure Division Winner" (mathematically eliminates competitors in division)
if #teamsInDiv >= 2 then
local secondPlaceTeam = teamsInDiv[2]
if divWinnerCandidate.Stats.W > (secondPlaceTeam.Stats.W + secondPlaceTeam.Stats.RemainingGames) then
playoffStatus[divWinnerCandidate.Team].isSafeDivWinner = true
end
end
end
end
-- Sort Division Winners for overall seeding
table.sort(divisionWinners, function(a, b)
return M.compareTeams(a.Stats, b.Stats, season, false)
end)
-- Assign playoff status and ranks based on sorted Division Winners
local currentRank = 1
for i, divWinner in ipairs(divisionWinners) do
local teamName = divWinner.Team
if i <= 2 then -- Top 2 Seeds get a Bye
playoffStatus[teamName].status = "Div" .. i .. " (Bye)"
playoffStatus[teamName].rank = currentRank
playoffStatus[teamName].isQualified = true
playoffStatus[teamName].isQualifiedAsDivWinner = true
playoffStatus[teamName].hasBye = true
-- isSafeDivWinner is set above, remains true here
else -- Division Winners without a Bye
playoffStatus[teamName].status = "Div" .. i
playoffStatus[teamName].rank = currentRank
playoffStatus[teamName].isQualified = true
playoffStatus[teamName].isQualifiedAsDivWinner = true
end
currentRank = currentRank + 1
end
-- Collect non-division winners for Wild Card spots
for teamName, stats in pairs(allTeamStats) do
if not playoffStatus[teamName] or not playoffStatus[teamName].isQualified then -- if not already qualified as Div Winner
table.insert(nonDivisionWinners, { Team = teamName, Stats = stats })
end
end
-- Sort non-division winners for Wild Card seeding
table.sort(nonDivisionWinners, function(a, b)
return M.compareTeams(a.Stats, b.Stats, season, false)
end)
-- Assign Wild Card spots and check their secure qualification
local wildCardCount = 0
-- Use a temporary list to track all qualified teams in correct order
local allQualifiedTeamsSorted = {}
for teamName, pInfo in pairs(playoffStatus) do
if pInfo.isQualified and pInfo.rank then
table.insert(allQualifiedTeamsSorted, { Team = teamName, Stats = allTeamStats[teamName], PlayoffInfo = pInfo })
end
end
table.sort(allQualifiedTeamsSorted, function(a, b) return a.PlayoffInfo.rank < b.PlayoffInfo.rank end)
-- Now add the Wildcard candidates
for i, teamInfo in ipairs(nonDivisionWinners) do
local teamName = teamInfo.Team
if not playoffStatus[teamName].isQualified then -- Ensure the team is not already qualified as a Division Winner (redundancy check)
wildCardCount = wildCardCount + 1
if wildCardCount <= 2 then -- Top 2 Wildcards
playoffStatus[teamName].status = "Wild Card"
playoffStatus[teamName].rank = currentRank
playoffStatus[teamName].isQualified = true
table.insert(allQualifiedTeamsSorted, { Team = teamName, Stats = allTeamStats[teamName], PlayoffInfo = playoffStatus[teamName] })
currentRank = currentRank + 1
else
break -- Only consider the top 2 wildcards
end
end
end
-- --- Elimination and calculable qualification logic ---
local lastPlayoffSpotTeam = nil
if #allQualifiedTeamsSorted >= 6 then
lastPlayoffSpotTeam = allQualifiedTeamsSorted[6].Stats -- The 6th seed
end
local firstNonPlayoffTeam = nil
-- Find the first non-qualified team in the globally sorted list
local allTeamsSortedOverall = {}
for teamName, stats in pairs(allTeamStats) do
table.insert(allTeamsSortedOverall, { Team = teamName, Stats = stats })
end
table.sort(allTeamsSortedOverall, function(a, b)
return M.compareTeams(a.Stats, b.Stats, season, false) -- Use global sorting
end)
for _, teamInfo in ipairs(allTeamsSortedOverall) do
if not playoffStatus[teamInfo.Team].isQualified then
firstNonPlayoffTeam = teamInfo.Stats
break
end
end
-- Check calculable qualification (if not already done by seeding above)
if firstNonPlayoffTeam then
local minWinsToQualifySafely = firstNonPlayoffTeam.W + firstNonPlayoffTeam.RemainingGames
for teamName, pInfo in pairs(playoffStatus) do
if not pInfo.isQualified and allTeamStats[teamName] then
if allTeamStats[teamName].W > minWinsToQualifySafely then
pInfo.isQualified = true -- Mark as qualified
end
end
end
end
-- Check calculable elimination
if lastPlayoffSpotTeam then
for teamName, pInfo in pairs(playoffStatus) do
if not pInfo.isQualified and allTeamStats[teamName] then -- Only check if not already qualified
local teamMaxPossibleWins = allTeamStats[teamName].W + allTeamStats[teamName].RemainingGames
if teamMaxPossibleWins < lastPlayoffSpotTeam.W then
pInfo.isEliminated = true
end
end
end
end
-- Final check for elimination: If a team has no more games left and is not qualified
for teamName, pInfo in pairs(playoffStatus) do
if not pInfo.isQualified and not pInfo.isEliminated and allTeamStats[teamName] then
if allTeamStats[teamName].GamesPlayedCount >= MAX_GAMES_PER_TEAM then -- All games played
pInfo.isEliminated = true
end
end
end
return playoffStatus
end
-- Function to generate the HTML table
function M.generateTable(frame)
local args = frame:getParent().args
local season = tonumber(args.season) or 2025
if not season then
return "<p>Error: Please provide a season (e.g., {{#invoke:Standingsv3|generateTable|season=2025}})</p>"
end
local games = M.getGameData(season)
local allTeamStats = M.calculateTeamStats(games)
local playoffStatus = M.getPlayoffTeams(allTeamStats, season) -- Retrieves extended status information
local divisions_with_teams = {
North = {},
South = {},
West = {},
East = {}
}
-- Assign teams to divisions and update their playoff status
for teamName, stats in pairs(allTeamStats) do
local division = teamDivisions[teamName]
if division then
table.insert(divisions_with_teams[division], stats)
-- Add playoff status to the team stats object
stats.PlayoffInfo = playoffStatus[teamName] or { isQualified = false, isEliminated = false, isSafeDivWinner = false, isQualifiedAsDivWinner = false, hasBye = false, rank = nil, status = nil }
end
end
local rankedDivisions = {}
for divisionName, teams in pairs(divisions_with_teams) do
if #teams > 0 then
table.sort(teams, function(a, b)
return M.compareTeams(a, b, season, true) -- Sorting within the division
end)
rankedDivisions[divisionName] = teams
else
rankedDivisions[divisionName] = {}
end
end
local output_parts = {}
local hasContent = false
local divisionOrder = {"North", "South", "West", "East"}
table.insert(output_parts, '<div class="standings-grid-container" style="margin-left: 0; margin-right: auto; overflow-x: auto;">')
for _, divisionName in ipairs(divisionOrder) do
local teamsInDivision = rankedDivisions[divisionName]
local headerBgColor = divisionHeaderColors[divisionName] or "#F8F9FA"
if teamsInDivision and #teamsInDivision > 0 then
table.insert(output_parts, '<div class="standings-container">')
hasContent = true
local division_table_html = {}
table.insert(division_table_html, '<table class="wikitable sortable">')
table.insert(division_table_html, '<tr>')
table.insert(division_table_html, '<th colspan="8" style="background-color:' .. headerBgColor .. '; color:white; font-weight:bold; text-align:center;">' .. divisionName .. ' Division</th>')
table.insert(division_table_html, '</tr>')
-- Column headers
table.insert(division_table_html, '<tr>')
table.insert(division_table_html, '<th>Team</th>')
table.insert(division_table_html, '<th style="text-align:center;">W</th>')
table.insert(division_table_html, '<th style="text-align:center;">L</th>')
table.insert(division_table_html, '<th style="text-align:center;">Win%</th>')
table.insert(division_table_html, '<th style="text-align:center;">DIV</th>')
table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">PF</th>')
table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">PA</th>')
table.insert(division_table_html, '<th class="mobile-hidden-col" style="text-align:center;">L3</th>')
table.insert(division_table_html, '</tr>')
-- Data rows
for i, teamStats in ipairs(teamsInDivision) do
local rowStyle = ""
local playoffInfo = teamStats.PlayoffInfo -- Access new status information
-- Set row background color based on playoff status
-- Priority of status display: Eliminated (by color) > Bye > Division Winner > Wild Card
if playoffInfo.isEliminated then
rowStyle = ' style="background-color:' .. playoffRowColors.eliminated .. ' !important;"'
elseif playoffInfo.hasBye then -- Has Bye (Seed #1 or #2)
rowStyle = ' style="background-color:' .. playoffRowColors.qualified_bye .. ' !important;"'
elseif playoffInfo.isQualifiedAsDivWinner then -- Qualified as Division Winner (but without Bye)
rowStyle = ' style="background-color:' .. playoffRowColors.qualified_division .. ' !important;"'
elseif playoffInfo.isQualified then -- Qualified as Wild Card
rowStyle = ' style="background-color:' .. playoffRowColors.qualified_wildcard .. ' !important;"'
end
local teamLastGames = M.calculateTeamLastGames(teamStats.Name, games)
table.insert(division_table_html, '<tr' .. rowStyle .. '>')
-- Team name without any symbols
local accessibilityLabel = ""
if playoffInfo.isEliminated then
accessibilityLabel = '<span class="sr-only">(Eliminated from playoff contention)</span>'
elseif playoffInfo.hasBye then
accessibilityLabel = '<span class="sr-only">(Bye Week, already qualified for semifinals)</span>'
elseif playoffInfo.isQualifiedAsDivWinner then
accessibilityLabel = '<span class="sr-only">(Division Winner)</span>'
elseif playoffInfo.isQualified then
accessibilityLabel = '<span class="sr-only">(Qualified for playoffs)</span>'
end
table.insert(division_table_html, '<td>[[' .. teamStats.Name .. ']]' .. accessibilityLabel .. '</td>')
table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.W .. '</td>')
table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.L .. '</td>')
table.insert(division_table_html, '<td style="text-align:center;">' .. string.format("%.3f", teamStats.WinP) .. '</td>')
table.insert(division_table_html, '<td style="text-align:center;">' .. teamStats.DivW .. '-' .. teamStats.DivL .. '</td>')
table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamStats.PF .. '</td>')
table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamStats.PA .. '</td>')
table.insert(division_table_html, '<td class="mobile-hidden-col" style="text-align:center;">' .. teamLastGames .. '</td>')
table.insert(division_table_html, '</tr>')
end
table.insert(division_table_html, '</table>')
table.insert(output_parts, table.concat(division_table_html, '\n'))
table.insert(output_parts, '</div>')
end
end
table.insert(output_parts, '</div>')
-- Add legend (adapted to use colors only and a two-column layout)
table.insert(output_parts, '\n<div class="standings-legend" style="margin-top: 1em; display: flow-root;">') -- Use flow-root for self-clearing float container
table.insert(output_parts, '<span style="font-weight:bold; font-size:1.2em; display:block; margin-bottom:0.5em;">Legend:</span>') -- Changed from h4 to span for less separation
table.insert(output_parts, '<ul style="list-style-type: none; padding: 0; margin: 0;">')
-- Helper function to generate a list item for the legend
local function createLegendItem(color, description)
return '<li style="float: left; width: 48%; margin-right: 2%; margin-bottom: 0.5em; box-sizing: border-box;">' ..
'<span style="display: inline-block; width: 1em; height: 1em; background-color: ' .. color .. '; border: 1px solid #ccc; vertical-align: middle; margin-right: 0.5em;"></span> ' ..
description ..
'</li>'
end
table.insert(output_parts, createLegendItem(playoffRowColors.qualified_bye, 'Automatically qualified for Semifinals (Bye Week)'))
table.insert(output_parts, createLegendItem(playoffRowColors.qualified_division, 'Division Winner'))
table.insert(output_parts, createLegendItem(playoffRowColors.qualified_wildcard, 'Playoff Spot Clinched'))
table.insert(output_parts, createLegendItem(playoffRowColors.eliminated, 'Eliminated from Playoff Contention'))
table.insert(output_parts, '</ul>')
table.insert(output_parts, '</div>\n')
return table.concat(output_parts, '\n')
end
return M