Module:Sports stat rank/sandbox
Appearance
| This is the module sandbox page for Module:Sports stat rank (diff). |
| This module is rated as alpha. It is ready for limited use and third-party feedback. It may be used on a small number of pages, but should be monitored closely. Suggestions for new features or adjustments to input and output are welcome. |
| This module uses TemplateStyles: |
Module:Sports stat rank is meant to build stat ranking tables for sports teams.
Wikitext test cases are at Module:Sports stat rank/testcases wikitext.
Usage
[edit]The main command is {{#invoke:Sports stat rank|main}} statement.
In league season article for top stats
[edit]| What you type | {{#invoke:Sports stat rank|main
|order_cat = NSL
|sort_by = NAME
|show_num = false
|show_nat = true
|show_team = true
|show_cat = false
|show_totals = false
|threshold_rank = 5
|name_LAbdu = [[Latifah Abdu]]
|data_sort_value_name_LAbdu = Abdu, Latifah
|nat_LAbdu = {{flagicon|CAN}}
|team_LAbdu = [[Vancouver Rise]]
|NSL_LAbdu = 3
|name_JDF = [[Jessica De Filippo]]
|data_sort_value_name_JDF = De Filippo, Jessica
|nat_JDF = {{flagicon|CAN}}
|team_JDF = [[Vancouver Rise]]
|NSL_JDF = 9
|name_KHnt = [[Kaylee Hunter]]
|data_sort_value_name_KHnt = Hunter, Kaylee
|nat_KHnt = {{flagicon|CAN}}
|team_KHnt = [[AFC Toronto]]
|NSL_KHnt = 7
|name_LPchrsky = [[Lisa Pechersky]]
|data_sort_value_name_LPchrsky = Pechersky, Lisa
|nat_LPchrsky = {{flagicon|CAN}}
|team_LPchrsky = [[Montreal Roses]]
|NSL_LPchrsky = 2
|name_DBP = [[Delaney Baie Pridham]]
|data_sort_value_name_DBP = Pridham, Delayney Baie
|nat_DBP = {{flagicon|CAN}}
|team_DBP = [[Ottawa Rapid]]
|NSL_DBP = 8
|name_LRowe = [[Lauren Rowe]]
|data_sort_value_name_LRowe = Rowe, Lauren
|nat_LRowe = {{flagicon|CAN}}
|team_LRowe = [[AFC Toronto]]
|NSL_LRowe = 7
|source=[https://www.nsl.ca/stats NSL]
|updated=August 15, 2026
}}
| ||||||||||||||||||||||||||||
| What it looks like |
Updated to match(es) played on August 15, 2026. Source: NSL |
In team season article
[edit]| What you type | {{#invoke:Sports stat rank|main
|header_style = "background:#84ae99; {{box-shadow border|a|#ddcd8f|2px}}; color:#ffffff;"
|stat_name = Goals
|sort_by = NUM
|show_pos = true
|show_nat = true
|order_cat = NSL,WCC
|cat_NSL = [[2026 Northern Super League season|{{color|white|NSL}}]]
|cat_WCC = [[2026-27 W Canadian Championship|{{color|white|W Canadian Championship}}]]
|name_JDF = [[Jessica De Filippo]]
|num_JDF = 11
|pos_JDF = [[Forward (association football)|FW]]
|nat_JDF = {{flagicon|CAN}}
|NSL_JDF = 7
|WCC_JDF =
|name_ABout = [[Anna Bout]]
|num_ABout = 14
|pos_ABout = [[Midfielder (association football)|MF]]
|nat_ABout = {{flagicon|CAN}}
|NSL_ABout = 5
|WCC_ABout = 2
|name_MPante = [[Mia Pante]]
|num_MPante = 22
|pos_MPante = [[Defender (association football)|DF]]
|nat_MPante = {{flagicon|CAN}}
|NSL_MPante = 1
|WCC_MPante = 2
|adj_MPante = 1
|note_WCC_MPante = 1 goal scored in July 4 match counts toward both the NSL and the W Canadian Championship.
|source=[https://www.nsl.ca/stats NSL]
|updated=August 15, 2026
}}
| ||||||||||||||||||||||||||||||||||||||
| What it looks like |
Updated to match(es) played on August 15, 2026. Source: NSL
| ||||||||||||||||||||||||||||||||||||||
In derby article
[edit]| What you type | {{#invoke:Sports stat rank|main
|sort_by = NAME
|show_num = false
|order_cat = PAC,VAN
|cat_PAC = [[Pacific FC|Pacific]]
|cat_VAN = [[Vancouver FC|Vancouver]]
|name_ADíaz = [[Alejandro Díaz]]
|data_sort_value_name_ADíaz = Díaz, Alejandro
|nat_ADíaz = {{flagicon|CAN}}
|team_ADíaz = [[Vancouver Rise]]
|VAN_ADíaz = 7
|name_GBtr = [[Gabriel Bitar]]
|data_sort_value_name_GBtr = Bitar, Gabriel
|nat_GBtr = {{flagicon|CAN}}
|PAC_GBtr = 2
|VAN_GBtr = 1
|name_MApa = [[Manny Aparicio]]
|data_sort_value_name_MApa = Aparicio, Manny
|nat_MApa = {{flagicon|LBN}}
|PAC_MApa = 1
|VAN_MApa = 2
|source=[https://www.cplsoccer.ca/statistics CPL]
|updated=August 15, 2026
}}
| |||||||||||||||||||||||
| What it looks like |
Updated to match(es) played on August 15, 2026. Source: CPL | |||||||||||||||||||||||
Display options
[edit]- cat_order (required): Define order in which categories of stats be shown, using abbreviations such as CCC that will be used to define category values for each player, i.e. CCC_PPP
- E.g.
NSL,WCCfor a club competing in the Northern Super League and the W Canadian Championship. - E.g.
PAC,VANfor players who have scored in a derby between Pacific FC or Vancouver FC. - Note: must be defined even if show_cat is set to
false.
- E.g.
- stat_name (optional): Name for 'Total' column.
Totalby default. - header_style (optional): Set custom style for table header.
- sort_by (optional): Sorting option after sorting for stat total.
NAME(default): Sort by player name, using sort_data_value_name_PPP if defined.NUM: Sort by player number.
- adj_PPP: The number to deduct from a player PPP's total for the stat, to adjust for those arising in matches that count towards multiple competitions.
- note_CCC_PPP: Note to be placed beside a player PPP's stat in category CCC, primarily to explain adj_PPP.
- threshold_rank: The lowest rank to display. All players with the same stat will also be displayed, even if the total number of players exceeds the specified rank.
- threshold_stat: The lowest stat to display, inclusive.
- Note: show_totals is automatically
falseif the threshold limits are triggered, since showing column totals is confusing with a truncated list.
- show_num: Option to display column showing player's number.
trueby default. - show_pos: Option to display column showing player's playing position.
falseby default. - show_nat: Option to display column showing player's nationality.
falseby default. - show_team: Option to display column showing player's team.
falseby default. - show_cat: Option to display column(s) showing categories that the stat is derived from.
trueby default.- Note: cat_order must be set
- show_totals: Option to display totals row at the bottom of the table.
trueby default.
-- Module to build tables ranking top player in a particular stat in Sports
-- See documentation for details
require('strict')
local p = {}
-- Main function
function p.main(frame)
-- Declare locals
local getArgs = require('Module:Arguments').getArgs
local Args = getArgs(frame, {parentFirst = true})
assert(Args['order_cat'], 'Error: Please define order_cat')
local cat_order = mw.text.split(Args['order_cat'], '%s*[;,]%s*')
local stat_list = {['Totals'] = {}}
local t = {}
local templatestyles = frame:extensionTag{
name = 'templatestyles', args = { src = 'Module:Sports stat rank/styles.css' }
}
-- Load modules
local yesno = require('Module:Yesno')
local function defaultTrue(val)
return (yesno(val) == nil) and true or yesno(val)
end
local function defaultFalse(val)
return yesno(val) or false
end
-- Set show flags
local show_totals = defaultTrue(Args['show_totals'])
local show_num = defaultTrue(Args['show_num'])
local show_pos = defaultFalse(Args['show_pos'])
local show_nat = defaultFalse(Args['show_nat'])
local show_team = defaultFalse(Args['show_team'])
local show_cat = defaultTrue(Args['show_cat'])
-- Set threshold checks
local function checkThresholdRank(next_rank)
local threshold_rank = tonumber(Args['threshold_rank'])
if threshold_rank then
return next_rank <= threshold_rank
else
return true -- Check passes if no threshold rank is specified
end
end
local function checkThresholdStat(next_stat)
local threshold_stat = tonumber(Args['threshold_stat'])
if threshold_stat then
return next_stat >= threshold_stat
else
return true -- Check passes if no threshold rank is specified
end
end
-- Form list of players, stored under stat_list[playertotal][pname]
for arg, value in pairs(Args) do
-- Get player abbreviation
local pname = string.match(arg, '^name_(.+)')
if yesno(pname, true) then
local pstat = {total = 0}
-- Get player stats by category
for _, cname in pairs(cat_order) do
local stat = tonumber(Args[cname..'_'..pname]) or 0 -- Stat might not be set
pstat[cname] = stat
pstat.total = pstat.total + stat
-- Add stat to appropriate totals category
stat_list['Totals'][cname] = (stat_list['Totals'][cname] or 0) + stat
stat_list['Totals'].total = (stat_list['Totals'].total or 0) + stat
end
-- Apply stat adjustment to account for matches that count towards multiple competitions
local adj = tonumber(Args['adj_'..pname]) or 0
pstat.total = pstat.total - adj
stat_list['Totals'].total = stat_list['Totals'].total - adj
-- Add player to stats list
if not stat_list[pstat.total] then stat_list[pstat.total] = {} end -- Initialise dict for particular stat total
stat_list[pstat.total][pname] = pstat
-- Add player number sort value
stat_list[pstat.total][pname].num = tonumber(Args['num_'..pname])
-- Add player name sort value
local name_stripped_brackets = mw.ustring.gsub(Args['name_'..pname], '[%[%]]', '')
local name_clean = mw.ustring.gsub(name_stripped_brackets, '.-|', '') -- strip original page name
stat_list[pstat.total][pname].sortname = Args['data_sort_value_name_'..pname] or name_clean
end
end
-- Sort stat_list, by descending total goals
local total_array = {}
for total in pairs(stat_list) do
table.insert(total_array, total)
end
table.sort(total_array, function(a, b)
-- 'Totals' is placed at the bottom
if a == 'Totals' then return false end
if b == 'Totals' then return true end
-- Sort by stat, descending
return tonumber(a) > tonumber(b)
end)
-- Sort sub stat_list for each goals total, by player number or name
for k, v in pairs(stat_list) do
if k ~= 'Totals' then -- Skip 'Total' entry
local player_array = {}
for pname in pairs(v) do
table.insert(player_array, pname)
end
table.sort(player_array, function(pnamea, pnameb)
if Args['sort_by'] == 'NUM' then
-- Sort by player number, ascending, with no assigned player number placed lower
if v[pnamea].num and v[pnameb].num then return v[pnamea].num < v[pnameb].num end
if not v[pnamea].num then return false end
if not v[pnameb].num then return true end
end
-- Sort by player name otherwise, ascending
return v[pnamea].sortname < v[pnameb].sortname
end)
v.player_array = player_array
end
end
-- Create table header
local style = 'style='..(Args['header_style'] or '')
table.insert(t, '{| class="wikitable sortable" style="text-align:center"\n')
table.insert(t, '!width=15 '..style..'|Rank\n')
if show_num then table.insert(t, '!width=15 '..style..'|<abbr title="Squad number">No.</abbr>\n') end
if show_pos then table.insert(t, '!width=15 '..style..'|<abbr title="Player position">Pos</abbr>\n') end
if show_nat then table.insert(t, '!width=15 '..style..'|<abbr title="Nationality">Nat</abbr>\n') end
table.insert(t, '! '..style..'"|Player\n')
if show_team then table.insert(t, '! '..style..'|Team\n') end
if show_cat then
-- Add category columns
for _, cname in pairs(cat_order) do
table.insert(t, '!width=25 data-sort-order="desc" '..style..'|'..(Args['cat_'..cname] or '')..'\n')
end
end
-- Add totals column, named as defined with stat_name, or 'Total' by default
table.insert(t, '!data-sort-order="desc" '..style..'|'..(Args['stat_name'] or 'Total')..'\n')
-- Create data rows
local rank = 1 -- Track current ranking
for _, total in pairs(total_array) do
local is_totals = total == 'Totals'
if not is_totals then
-- Player row formatting
if not (checkThresholdRank(rank) and checkThresholdStat(total)) then
break -- Stop creating table rows if threshold checks fail.
end
local num_players = #stat_list[total].player_array
local first_player = true -- Track if it is the first player with that stat total, to manage new rows and total cell placement
table.insert(t, '|-\n')
table.insert(t, '! rowspan="'..num_players..'"|'..rank..'\n')
for _, pname in pairs(stat_list[total].player_array) do
local pstat = stat_list[total][pname]
if not first_player then table.insert(t, '|-\n') end
if show_num then table.insert(t, '|'..(Args['num_'..pname] or '')..'\n') end
if show_pos then table.insert(t, '|'..(Args['pos_'..pname] or '')..'\n') end
if show_nat then table.insert(t, '|'..(Args['nat_'..pname] or '')..'\n') end
table.insert(t, '| style="text-align:left;" data-sort-value="'..(pstat.sortname or '')..'" |'..(Args['name_'..pname] or '')..'\n')
if show_team then table.insert(t, '| style="text-align:left;" |'..(Args['team_'..pname] or '')..'\n') end
if show_cat then
-- Add category columns
for _, cname in ipairs(cat_order) do
table.insert(t, '|'..((pstat[cname] ~= 0) and pstat[cname] or ''))
if yesno(Args['note_'..cname..'_'..pname], true) then
table.insert(t, frame:expandTemplate{title='efn', args={group='lower-alpha', Args['note_'..cname..'_'..pname]}})
end
table.insert(t, '\n')
end
end
if first_player then table.insert(t, '| rowspan="'..num_players..'"|<b>'..total..'</b>\n') end
first_player = false
end
rank = rank + num_players
elseif show_totals then
-- Totals row formatting
local totals_name_colspan = 2 + (show_num and 1 or 0) + (show_pos and 1 or 0) + (show_nat and 1 or 0) + (show_team and 1 or 0) -- Rank and player columns are mandatory
table.insert(t, '|-style="background-color:#d8edf3; font-weight:bold;"\n')
table.insert(t, '| colspan="'..totals_name_colspan..'" | Totals\n')
if show_cat then
-- Add category columns
for _, cname in ipairs(cat_order) do
table.insert(t, '| '..stat_list['Totals'][cname]..'\n')
end
end
table.insert(t, '|'..stat_list['Totals'].total..'\n')
end
end
-- Close table
table.insert(t, '|}')
-- Create footer
local note_list = {}
if yesno(Args['updated'], true) then
table.insert(note_list, 'Updated to match(es) played on '..Args['updated']..'.')
end
if yesno(Args['source'], true) then
table.insert(note_list, 'Source: '..Args['source'])
end
local t_footer = '<div class="sports-stat-rank-notes">'..table.concat(note_list, ' ')..'</div>'..frame:expandTemplate{title='notelist', args={group='lower-alpha'}}
return templatestyles..'\n'..table.concat(t)..t_footer
end
return p