Module:Navbox timeline: Difference between revisions
Appearance
AZMindroma (talk | contribs) No edit summary Tag: Manual revert |
AZMindroma (talk | contribs) Attempting to introduce months again |
||
| Line 5: | Line 5: | ||
local getArgs = require('Module:Arguments').getArgs | local getArgs = require('Module:Arguments').getArgs | ||
local p = {} | local p = {} | ||
-- Convert year-month to numeric value for comparison | |||
local function dateToNumber(year, month) | |||
return year * 12 + (month or 1) - 1 | |||
end | |||
-- Convert numeric value back to year and month | |||
local function numberToDate(num) | |||
local year = math.floor(num / 12) | |||
local month = (num % 12) + 1 | |||
return year, month | |||
end | |||
-- Get month abbreviation | |||
local function getMonthAbbr(month) | |||
local months = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | |||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'} | |||
return months[month] or '' | |||
end | |||
-- Parse date string (supports YYYY, YYYY-MM, or YYYY-YYYY formats) | |||
local function parseDate(dateStr) | |||
if not dateStr then return nil, nil end | |||
-- Match YYYY-MM format | |||
local year, month = dateStr:match('^(%d%d%d%d)%-(%d%d?)$') | |||
if year and month then | |||
return tonumber(year), tonumber(month) | |||
end | |||
-- Match YYYY format | |||
year = dateStr:match('^(%d%d%d%d)$') | |||
if year then | |||
return tonumber(year), 1 | |||
end | |||
return nil, nil | |||
end | |||
-- Add blank table cells | -- Add blank table cells | ||
local function addBlank(args, row, prev, current) | local function addBlank(args, row, prev, current) | ||
if row and prev < current then | if row and prev < current then | ||
if | local duration = current - prev | ||
if duration > 0 then | |||
row:tag('td') | row:tag('td') | ||
:addClass('timeline-blank') | :addClass('timeline-blank') | ||
:cssText(args.blankstyle) | :cssText(args.blankstyle) | ||
:attr('colspan', | :attr('colspan', duration) | ||
end | end | ||
end | end | ||
| Line 36: | Line 61: | ||
local rows = { | local rows = { | ||
[1] = {}, | [1] = {}, | ||
minDate = math.huge, | |||
maxDate = -math.huge, | |||
hasLabels = false | hasLabels = false, | ||
useMonths = yesno(args.months) ~= false | |||
} | } | ||
| Line 55: | Line 81: | ||
-- Data cell key value pairs | -- Data cell key value pairs | ||
elseif key then | elseif key then | ||
rows[#rows][cellIndex][key] = val | rows[#rows][cellIndex][key] = val | ||
-- New row | -- New row | ||
| Line 65: | Line 90: | ||
elseif cellIndex > 0 | elseif cellIndex > 0 | ||
and rows[#rows][cellIndex].item | and rows[#rows][cellIndex].item | ||
and not rows[#rows][cellIndex]. | and not rows[#rows][cellIndex].startDate | ||
then | then | ||
local dates = mw.text.split(fullVal, '-', true) | local dates = mw.text.split(fullVal, '-', true) | ||
local startYear | local startYear, startMonth, endYear, endMonth | ||
if startYear == | if #dates == 2 then | ||
-- Handle YYYY-YYYY format | |||
startYear, startMonth = parseDate(dates[1]) | |||
endYear, endMonth = parseDate(dates[2]) | |||
elseif #dates == 3 then | |||
-- Handle YYYY-MM-YYYY format | |||
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2]) | |||
endYear, endMonth = parseDate(dates[3]) | |||
elseif #dates == 4 then | |||
-- Handle YYYY-MM-YYYY-MM format | |||
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2]) | |||
endYear, endMonth = tonumber(dates[3]), tonumber(dates[4]) | |||
else | |||
-- Single date | |||
startYear, startMonth = parseDate(fullVal) | |||
endYear, endMonth = startYear, (startMonth == 12) and 1 or (startMonth + 1) | |||
if startMonth == 12 then endYear = endYear + 1 end | |||
end | |||
if not startYear then | |||
error('Argument ' .. num .. ' is an invalid time range', 0) | error('Argument ' .. num .. ' is an invalid time range', 0) | ||
end | end | ||
-- Set defaults | |||
startMonth = startMonth or 1 | |||
endMonth = endMonth or 1 | |||
endYear = endYear or (tonumber(os.date('%Y')) + 1) | |||
local startDate = dateToNumber(startYear, startMonth) | |||
local endDate = dateToNumber(endYear, endMonth) | |||
if endDate <= startDate then | |||
endDate = startDate + 1 -- Minimum duration of 1 month | |||
end | end | ||
rows[#rows][cellIndex].startDate = startDate | |||
rows[#rows][cellIndex].endDate = endDate | |||
rows[#rows][cellIndex].startYear = startYear | rows[#rows][cellIndex].startYear = startYear | ||
rows[#rows][cellIndex].startMonth = startMonth | |||
rows[#rows][cellIndex].endYear = endYear | rows[#rows][cellIndex].endYear = endYear | ||
rows[#rows][cellIndex].endMonth = endMonth | |||
if | if startDate < rows.minDate then | ||
rows. | rows.minDate = startDate | ||
end | end | ||
if | if endDate > rows.maxDate then | ||
rows. | rows.maxDate = endDate | ||
end | end | ||
-- New item | -- New item | ||
| Line 97: | Line 152: | ||
-- Add overrides from arguments | -- Add overrides from arguments | ||
if args.startoffset then | if args.startoffset then | ||
rows. | rows.minDate = rows.minDate - tonumber(args.startoffset) | ||
end | end | ||
if args.startyear | if args.startyear then | ||
local startMonth = tonumber(args.startmonth) or 1 | |||
local customStart = dateToNumber(tonumber(args.startyear), startMonth) | |||
if customStart < rows.minDate then | |||
rows.minDate = customStart | |||
end | |||
end | end | ||
if args.endoffset then | if args.endoffset then | ||
rows. | rows.maxDate = rows.maxDate + tonumber(args.endoffset) | ||
end | end | ||
if args.endyear | if args.endyear then | ||
local endMonth = tonumber(args.endmonth) or 12 | |||
local customEnd = dateToNumber(tonumber(args.endyear), endMonth) | |||
if customEnd > rows.maxDate then | |||
rows.maxDate = customEnd | |||
end | |||
end | end | ||
| Line 118: | Line 181: | ||
local function renderDates(args, tbl, rows, invert) | local function renderDates(args, tbl, rows, invert) | ||
local showDecades = yesno(args.decades) | local showDecades = yesno(args.decades) | ||
local yearRow = tbl:tag('tr') | local useMonths = rows.useMonths | ||
local yearRow = tbl:tag('tr'):addClass('timeline-row') | |||
-- Create label | -- Create label | ||
| Line 127: | Line 190: | ||
:addClass('navbox-group timeline-label') | :addClass('navbox-group timeline-label') | ||
:cssText(args.labelstyle) | :cssText(args.labelstyle) | ||
:attr('rowspan', showDecades ~= false and '2' or '1') | :attr('rowspan', (showDecades ~= false and useMonths) and '3' or | ||
(showDecades ~= false or useMonths) and '2' or '1') | |||
:wikitext(args.label or '') | :wikitext(args.label or '') | ||
| Line 135: | Line 199: | ||
-- Create decade row | -- Create decade row | ||
if showDecades ~= false then | if showDecades ~= false then | ||
local decadeRow = tbl:tag('tr') | local decadeRow = tbl:tag('tr'):addClass('timeline-row') | ||
local currentDate = rows.minDate | |||
local | |||
if not invert then | if not invert then | ||
decadeRow, yearRow = yearRow, decadeRow | decadeRow, yearRow = yearRow, decadeRow | ||
end | end | ||
while | while currentDate < rows.maxDate do | ||
local dur = math.min( | local year, month = numberToDate(currentDate) | ||
local nextDecade = math.ceil(year / 10) * 10 | |||
local decadeEnd = dateToNumber(nextDecade, 1) | |||
local dur = math.min(decadeEnd - currentDate, rows.maxDate - currentDate) | |||
decadeRow:tag('th') | decadeRow:tag('th') | ||
| Line 155: | Line 220: | ||
:wikitext(math.floor(year / 10) .. '0s') | :wikitext(math.floor(year / 10) .. '0s') | ||
currentDate = currentDate + dur | |||
end | |||
end | |||
-- Create month row if using months | |||
local monthRow = nil | |||
if useMonths then | |||
monthRow = tbl:tag('tr'):addClass('timeline-row') | |||
if invert and showDecades ~= false then | |||
-- Reorder for footer | |||
monthRow, yearRow, decadeRow = decadeRow, monthRow, yearRow | |||
end | end | ||
end | end | ||
-- Populate year row | -- Populate year row | ||
local | local totalDuration = rows.maxDate - rows.minDate | ||
local currentDate = rows.minDate | |||
while currentDate < rows.maxDate do | |||
local year, month = numberToDate(currentDate) | |||
local nextYear = dateToNumber(year + 1, 1) | |||
local yearDuration = math.min(nextYear - currentDate, rows.maxDate - currentDate) | |||
local width = (yearDuration / totalDuration) * 100 | |||
yearRow:tag('th') | yearRow:tag('th') | ||
:attr('scope', 'col') | :attr('scope', 'col') | ||
| Line 169: | Line 250: | ||
:cssText(args.yearstyle) | :cssText(args.yearstyle) | ||
:cssText('width:' .. width .. '%') | :cssText('width:' .. width .. '%') | ||
:wikitext( | :attr('colspan', yearDuration) | ||
:wikitext(tostring(year)) | |||
-- Add months for this year | |||
if useMonths and monthRow then | |||
local yearStart = currentDate | |||
while yearStart < nextYear and yearStart < rows.maxDate do | |||
local monthYear, monthNum = numberToDate(yearStart) | |||
local monthWidth = (1 / totalDuration) * 100 | |||
monthRow:tag('th') | |||
:attr('scope', 'col') | |||
:addClass('timeline-month') | |||
:cssText(args.datestyle) | |||
:cssText(args.monthstyle) | |||
:cssText('width:' .. monthWidth .. '%') | |||
:wikitext(getMonthAbbr(monthNum)) | |||
yearStart = yearStart + 1 | |||
end | |||
end | |||
currentDate = nextYear | |||
end | end | ||
end | end | ||
| Line 175: | Line 278: | ||
-- Render the timeline itself | -- Render the timeline itself | ||
local function renderTimeline(args, tbl, rows) | local function renderTimeline(args, tbl, rows) | ||
for rowNum, row in ipairs(rows) do | for rowNum, row in ipairs(rows) do | ||
local rowElement = tbl:tag('tr') | local rowElement = tbl:tag('tr'):addClass('timeline-row') | ||
if | if rows.hasLabels or args.label then | ||
rowElement:tag('th') | |||
:attr('scope', 'row') | :attr('scope', 'row') | ||
:addClass('navbox-group timeline-label') | :addClass('navbox-group timeline-label') | ||
:cssText(args.labelstyle) | :cssText(args.labelstyle) | ||
:cssText(row.labelstyle) | :cssText(row.labelstyle) | ||
:wikitext(row.label) | :wikitext(row.label or '') | ||
end | end | ||
local prevEndDate = rows.minDate | |||
local | |||
for cellNum, cell in ipairs(row) do | for cellNum, cell in ipairs(row) do | ||
if cell.startDate == nil then | |||
if cell. | |||
error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0) | error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0) | ||
end | end | ||
-- Add blanks before the cell | -- Add blanks before the cell | ||
addBlank(args, rowElement, | addBlank(args, rowElement, prevEndDate, cell.startDate) | ||
rowElement:tag('td') | |||
:addClass('timeline-item') | :addClass('timeline-item') | ||
:cssText(args.itemstyle) | :cssText(args.itemstyle) | ||
:cssText(cell.style or '') | :cssText(cell.style or '') | ||
:attr('colspan', cell. | :attr('colspan', cell.endDate - cell.startDate) | ||
:wikitext(cell.item) | :wikitext(cell.item) | ||
prevEndDate = cell.endDate | |||
end | end | ||
-- Add blanks to the end of the row | -- Add blanks to the end of the row | ||
addBlank(args, rowElement, | addBlank(args, rowElement, prevEndDate, rows.maxDate) | ||
end | end | ||
end | end | ||
| Line 244: | Line 323: | ||
listpadding = '0' | listpadding = '0' | ||
} | } | ||
local passthrough = { | local passthrough = { | ||
'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image', | 'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image', | ||
'imageleft | 'imageleft', 'style', 'bodystyle', 'basestyle', | ||
'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle', | 'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle', | ||
'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass', | 'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass', | ||
'imageclass' | 'imageclass' | ||
} | } | ||
local rows = timelineInfo(args) | local rows = timelineInfo(args) | ||
local wrapper = mw.html.create('table') | local wrapper = mw.html.create('table') | ||
Revision as of 17:21, 9 June 2025
Documentation for this module may be created at Module:Navbox timeline/doc
require('strict')
local yesno = require('Module:Yesno')
local navbox = require('Module:Navbox')._navbox
local getArgs = require('Module:Arguments').getArgs
local p = {}
-- Convert year-month to numeric value for comparison
local function dateToNumber(year, month)
return year * 12 + (month or 1) - 1
end
-- Convert numeric value back to year and month
local function numberToDate(num)
local year = math.floor(num / 12)
local month = (num % 12) + 1
return year, month
end
-- Get month abbreviation
local function getMonthAbbr(month)
local months = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}
return months[month] or ''
end
-- Parse date string (supports YYYY, YYYY-MM, or YYYY-YYYY formats)
local function parseDate(dateStr)
if not dateStr then return nil, nil end
-- Match YYYY-MM format
local year, month = dateStr:match('^(%d%d%d%d)%-(%d%d?)$')
if year and month then
return tonumber(year), tonumber(month)
end
-- Match YYYY format
year = dateStr:match('^(%d%d%d%d)$')
if year then
return tonumber(year), 1
end
return nil, nil
end
-- Add blank table cells
local function addBlank(args, row, prev, current)
if row and prev < current then
local duration = current - prev
if duration > 0 then
row:tag('td')
:addClass('timeline-blank')
:cssText(args.blankstyle)
:attr('colspan', duration)
end
end
end
-- Get timeline data
local function timelineInfo(args)
local rows = {
[1] = {},
minDate = math.huge,
maxDate = -math.huge,
hasLabels = false,
useMonths = yesno(args.months) ~= false
}
for num, fullVal in ipairs(args) do
local key, val = fullVal:match('^([a-z]+): *(.*)$')
local cellIndex = #rows[#rows]
-- Row data key value pairs
if cellIndex == 0 and key then
rows[#rows][key] = val
-- Record that there are labels
if key == 'label' then
rows.hasLabels = true
end
-- Data cell key value pairs
elseif key then
rows[#rows][cellIndex][key] = val
-- New row
elseif fullVal == '' then
if next(rows[#rows]) then
table.insert(rows, {})
end
-- Add date to cell with item already and no date
elseif cellIndex > 0
and rows[#rows][cellIndex].item
and not rows[#rows][cellIndex].startDate
then
local dates = mw.text.split(fullVal, '-', true)
local startYear, startMonth, endYear, endMonth
if #dates == 2 then
-- Handle YYYY-YYYY format
startYear, startMonth = parseDate(dates[1])
endYear, endMonth = parseDate(dates[2])
elseif #dates == 3 then
-- Handle YYYY-MM-YYYY format
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
endYear, endMonth = parseDate(dates[3])
elseif #dates == 4 then
-- Handle YYYY-MM-YYYY-MM format
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
endYear, endMonth = tonumber(dates[3]), tonumber(dates[4])
else
-- Single date
startYear, startMonth = parseDate(fullVal)
endYear, endMonth = startYear, (startMonth == 12) and 1 or (startMonth + 1)
if startMonth == 12 then endYear = endYear + 1 end
end
if not startYear then
error('Argument ' .. num .. ' is an invalid time range', 0)
end
-- Set defaults
startMonth = startMonth or 1
endMonth = endMonth or 1
endYear = endYear or (tonumber(os.date('%Y')) + 1)
local startDate = dateToNumber(startYear, startMonth)
local endDate = dateToNumber(endYear, endMonth)
if endDate <= startDate then
endDate = startDate + 1 -- Minimum duration of 1 month
end
rows[#rows][cellIndex].startDate = startDate
rows[#rows][cellIndex].endDate = endDate
rows[#rows][cellIndex].startYear = startYear
rows[#rows][cellIndex].startMonth = startMonth
rows[#rows][cellIndex].endYear = endYear
rows[#rows][cellIndex].endMonth = endMonth
if startDate < rows.minDate then
rows.minDate = startDate
end
if endDate > rows.maxDate then
rows.maxDate = endDate
end
-- New item
else
table.insert(rows[#rows], { item = fullVal })
end
end
-- Add overrides from arguments
if args.startoffset then
rows.minDate = rows.minDate - tonumber(args.startoffset)
end
if args.startyear then
local startMonth = tonumber(args.startmonth) or 1
local customStart = dateToNumber(tonumber(args.startyear), startMonth)
if customStart < rows.minDate then
rows.minDate = customStart
end
end
if args.endoffset then
rows.maxDate = rows.maxDate + tonumber(args.endoffset)
end
if args.endyear then
local endMonth = tonumber(args.endmonth) or 12
local customEnd = dateToNumber(tonumber(args.endyear), endMonth)
if customEnd > rows.maxDate then
rows.maxDate = customEnd
end
end
return rows
end
-- Render the date rows
local function renderDates(args, tbl, rows, invert)
local showDecades = yesno(args.decades)
local useMonths = rows.useMonths
local yearRow = tbl:tag('tr'):addClass('timeline-row')
-- Create label
if args.label or rows.hasLabels then
local labelCell = mw.html.create('th')
:attr('scope', 'col')
:addClass('navbox-group timeline-label')
:cssText(args.labelstyle)
:attr('rowspan', (showDecades ~= false and useMonths) and '3' or
(showDecades ~= false or useMonths) and '2' or '1')
:wikitext(args.label or '')
yearRow:node(labelCell)
end
-- Create decade row
if showDecades ~= false then
local decadeRow = tbl:tag('tr'):addClass('timeline-row')
local currentDate = rows.minDate
if not invert then
decadeRow, yearRow = yearRow, decadeRow
end
while currentDate < rows.maxDate do
local year, month = numberToDate(currentDate)
local nextDecade = math.ceil(year / 10) * 10
local decadeEnd = dateToNumber(nextDecade, 1)
local dur = math.min(decadeEnd - currentDate, rows.maxDate - currentDate)
decadeRow:tag('th')
:attr('scope', 'col')
:addClass('timeline-decade')
:cssText(args.datestyle)
:cssText(args.decadestyle)
:attr('colspan', dur)
:wikitext(math.floor(year / 10) .. '0s')
currentDate = currentDate + dur
end
end
-- Create month row if using months
local monthRow = nil
if useMonths then
monthRow = tbl:tag('tr'):addClass('timeline-row')
if invert and showDecades ~= false then
-- Reorder for footer
monthRow, yearRow, decadeRow = decadeRow, monthRow, yearRow
end
end
-- Populate year row
local totalDuration = rows.maxDate - rows.minDate
local currentDate = rows.minDate
while currentDate < rows.maxDate do
local year, month = numberToDate(currentDate)
local nextYear = dateToNumber(year + 1, 1)
local yearDuration = math.min(nextYear - currentDate, rows.maxDate - currentDate)
local width = (yearDuration / totalDuration) * 100
yearRow:tag('th')
:attr('scope', 'col')
:addClass('timeline-year')
:cssText(args.datestyle)
:cssText(args.yearstyle)
:cssText('width:' .. width .. '%')
:attr('colspan', yearDuration)
:wikitext(tostring(year))
-- Add months for this year
if useMonths and monthRow then
local yearStart = currentDate
while yearStart < nextYear and yearStart < rows.maxDate do
local monthYear, monthNum = numberToDate(yearStart)
local monthWidth = (1 / totalDuration) * 100
monthRow:tag('th')
:attr('scope', 'col')
:addClass('timeline-month')
:cssText(args.datestyle)
:cssText(args.monthstyle)
:cssText('width:' .. monthWidth .. '%')
:wikitext(getMonthAbbr(monthNum))
yearStart = yearStart + 1
end
end
currentDate = nextYear
end
end
-- Render the timeline itself
local function renderTimeline(args, tbl, rows)
for rowNum, row in ipairs(rows) do
local rowElement = tbl:tag('tr'):addClass('timeline-row')
if rows.hasLabels or args.label then
rowElement:tag('th')
:attr('scope', 'row')
:addClass('navbox-group timeline-label')
:cssText(args.labelstyle)
:cssText(row.labelstyle)
:wikitext(row.label or '')
end
local prevEndDate = rows.minDate
for cellNum, cell in ipairs(row) do
if cell.startDate == nil then
error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0)
end
-- Add blanks before the cell
addBlank(args, rowElement, prevEndDate, cell.startDate)
rowElement:tag('td')
:addClass('timeline-item')
:cssText(args.itemstyle)
:cssText(cell.style or '')
:attr('colspan', cell.endDate - cell.startDate)
:wikitext(cell.item)
prevEndDate = cell.endDate
end
-- Add blanks to the end of the row
addBlank(args, rowElement, prevEndDate, rows.maxDate)
end
end
function p.main(frame)
local args = getArgs(frame, {
removeBlanks = false,
wrappers = 'Template:Navbox timeline'
})
local targs = {
listpadding = '0'
}
local passthrough = {
'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image',
'imageleft', 'style', 'bodystyle', 'basestyle',
'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle',
'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass',
'imageclass'
}
local rows = timelineInfo(args)
local wrapper = mw.html.create('table')
:addClass('timeline-wrapper notheme')
local tbl = wrapper:tag('tr')
:tag('td')
:addClass('timeline-wrapper-cell')
:tag('table')
:addClass('timeline-table')
renderDates(args, tbl, rows)
renderTimeline(args, tbl, rows)
if yesno(args.footer) then
renderDates(args, tbl, rows, true)
end
for _, name in ipairs(passthrough) do
targs[name] = args[name]
end
targs.templatestyles = 'Module:Navbox timeline/styles.css'
targs.list1 = tostring(wrapper)
return navbox(targs)
end
return p