Mô đun:Medical cases chart/sandbox

local yesno = require('Module:Yesno')
local barBox = require('Module:Bar box')

local lang = mw.getContentLanguage()
local language = lang:getCode()
local i18n = require('Module:Medical cases chart/i18n')[language]
assert(i18n, 'no chart translations to: ' .. mw.language.fetchLanguageName(language, 'en'))
local monthAbbrs = {}
for i = 1, 12 do
monthAbbrs[i] = lang:formatDate('M', '2020-' .. ('%02d'):format(i))
end

local p = {}

function p._toggleButton(active, customtoggles, id, label)
local on = active and or ' mw-collapsed'
local off = active and ' mw-collapsed' or
local outString =
.. label .. ..
.. label ..
return outString
end

function p._yearToggleButton(year)
return p._toggleButton(year.l, ' mw-customtoggle-' .. year.year, year.year, year.year)
end

function p._monthToggleButton(year, month)
local lmon, label = lang:lc(month.mon), month.mon
local id = (year or ) .. lmon
local customtoggles = ' mw-customtoggle-' .. id

if month.s then
label = label .. '&nbsp;' .. month.s -- "Mmm "
if month.s ~= month.e then -- "Mmm –"
label = label .. '–' .. month.e
end
else
customtoggles = customtoggles .. (month.l and customtoggles .. month.l or )
end

for i, combination in ipairs(month.combinations) do
customtoggles = customtoggles .. ' mw-customtoggle-' .. combination -- up to 2 combinations per month so no need to table.concat()
end

return p._toggleButton(false, customtoggles, id, label)
end

function p._lastXToggleButton(years, duration, combinationsL)
local months, id = years[years].months, 'l' .. duration
local i, customtoggles = months, {' mw-customtoggle-' .. id}

if years > 1 then
local year = years[years].year
while months[i].l do
customtoggles[customtoggles+1] = ' mw-customtoggle-' .. year .. lang:lc(months[i].mon) .. '-' .. id
if i 1 then
if year years[years].year then
year = years[years-1].year
months = years[years-1].months
i = months
else -- either first month is also lastX month or lastX spans more than 2 years, which is not intended yet
break
end
else
i = i - 1
end
end
else
while i > 0 and months[i].l do
customtoggles[customtoggles+1] = ' mw-customtoggle-' .. lang:lc(months[i].mon) .. '-' .. id
i = i - 1
end
end

for i, combinationL in ipairs(combinationsL) do
customtoggles[customtoggles+1] = ' mw-customtoggle-' .. combinationL -- up to 3 combinationsL in 90 days
end

return p._toggleButton(true, table.concat(customtoggles), id, mw.ustring.format(i18n.lastXDays, duration))
end

function p._buildTogglesBar(dateList, duration, nooverlap)
local years = }}}
local months, combinationsL = years[1].months, {}

local function addMonth(month)
if month.mon ~= months[months].mon then -- new month
if month.year ~= years[years].year then -- new year
years[years+1] = {year=month.year, months={}}
months = years[years].months -- switch months list
end
months[months+1] = {mon=month.mon, combinations={}}
end
end
for i = 2, dateList do -- deduplicate years and months
if dateList[i] == 0 then -- specific date
addMonth(dateList[i])
months[months].l = months[months].l or dateList[i].l -- so that both ...-mon and ...-mon-lX classes are created
elseif dateList[i] == 1 then -- interval within month
addMonth(dateList[i][1])
months[months].l = months[months].l or dateList[i].l
else -- multimonth interval
for j, month in ipairs(dateList[i]) do
addMonth(month)
months[months].combinations[months[months].combinations+1] = dateList[i].id
end
combinationsL[combinationsL+1] = dateList[i].id:find('-l%d+$') and dateList[i].id
end
end

if nooverlap then
local lastDate = dateList[dateList]
months[months].e = tonumber(os.date('%d', lastDate.nDate or lastDate.nEndDate or lastDate.nAltEndDate)) -- end of final month

local i = dateList
repeat
i = i - 1
until i == 0 or (dateList[i].mon or dateList[i][1].mon) ~= months[months].mon
if i == 0 then -- start of first and final month
months[months].s = tonumber(os.date('%d', dateList[1].nDate))
else
months[months].s = 1
end
end

years[years].l = true -- to activate toggle and respective months bar

local monthToggles, divs = {}, nil
if years > 1 then
local yearToggles, monthsDivs = {}, {}
for i, year in ipairs(years) do
yearToggles[yearToggles+1] = p._yearToggleButton(year)
monthToggles = {}
months = year.months
for j, month in ipairs(months) do
monthToggles[monthToggles+1] = p._monthToggleButton(year.year, month)
end
monthsDivs[monthsDivs+1] =
.. table.concat(monthToggles) ..
end
divs = .. table.concat(yearToggles) .. .. table.concat(monthsDivs)
else
for i, month in ipairs(months) do
monthToggles[monthToggles+1] = p._monthToggleButton(nil, month)
end
divs = .. table.concat(monthToggles) ..
end
divs = divs .. .. p._lastXToggleButton(years, duration, combinationsL) ..
return .. divs ..
end

p._barColors = {
'A50026', --deaths
'SkyBlue', --recoveries
'Tomato', --cases or altlbl1
'Gold', --altlbl2
'OrangeRed' --altlbl3
}

function p._customBarStacked(args)
local barargs = {}

barargs[1] = args[1]

local function _numwidth(i)
local nw = args.numwidth:sub(i, i)
if nw 'n' then
return 0
elseif nw 't' then
return 2.45
elseif nw 'm' or nw 'd' then
return 3.5
elseif nw 'w' then
return 4.55
elseif nw 'x' then
return 5.6
end
end

barargs[2] =
.. (args[7] or ) .. ..
.. (args[8] or ) ..

if args.numwidth == 4 then
local pad = args.numwidth:sub(3, 3) == 'n' and '0' or '0.3em'
barargs.note2 =
.. (args[9] or ) .. ..
.. (args[10] or ) ..
end

for i = 1, 5 do
barargs[2i + 1] = p._barColors[i]
barargs[2i + 2] = args[i+1] / args.divisor
barargs['title' .. i] = args[i+1]
end

barargs.align = 'cdcc'
barargs.collapsed = args.collapsed
barargs.id = args.id
barargs.rowstyle = args.rowheight and 'line-height:'.. args.rowheight ..';'

return barBox._stacked(barargs)
end

function p._row(args)
local barargs = {}

barargs[1] = (args[1] or '⋮') .. (args.note0 or )
barargs[2] = args[2] or 0
barargs[3] = args[3] or 0

if args['alttot1'] then
barargs[4] = args['alttot1']
elseif args[4] then
barargs[4] = (args[4] or 0) - (barargs[2] or 0) - (barargs[3] or 0)
else
barargs[4] = 0
end

barargs[5] = args[5] or 0

if args['alttot2'] then
barargs[6] = args['alttot2']
elseif args[6] then
barargs[6] = (args[6] or 0) - (barargs[2] or 0) - (barargs[3] or 0)
else
barargs[6] = 0
end

barargs[7] = args[7]

local function changeArg(firstright, valuecol, changecol)
local change =
if args['firstright' .. firstright] then
change = '(' .. i18n.na .. ')'
elseif not args[1] and args[valuecol] then
change = '(=)'
else
change = args[changecol] and '(' .. args[changecol] .. ')' or
end
change = change .. (args['note' .. firstright] or )

return change
end

barargs[8] = changeArg(1, 7, 8)
barargs[9] = args[9]
barargs[10] = changeArg(2, 9, 10)

barargs.divisor = args.divisor
barargs.numwidth = args.numwidth
barargs.rowheight = args.rowheight

local dates
if args.collapsible then
local duration = args.duration
if args.daysToEnd >= duration then
barargs.collapsed = 'y'
else
barargs.collapsed =
end

if args.nooverlap and args.daysToEnd em or y > ey

dates.l = args.daysToEnd = 2 and i updatePeriod then
if nDateDiff == 2 updatePeriod then
prevRow = {nDate=barargs.nDate-updatePeriod}
prevRow[1] = os.date('%Y-%m-%d', prevRow.nDate)
else
prevRow = {nStartDate=(prevRow.nDate or prevRow.nEndDate)+updatePeriod, nEndDate=barargs.nDate-updatePeriod}
end
rows[rows+1] = prevRow
end
else
prevRow.nEndDate = barargs.nDate - updatePeriod
if prevRow.nStartDate == prevRow.nEndDate then
prevRow.nDate = prevRow.nEndDate
prevRow[1] = os.date('%Y-%m-%d', prevRow.nDate)
-- as nAltStartDate assumes a minimal multiday interval, it's possible for it to be greater if a true previous span is 1 day
elseif prevRow.nAltStartDate and prevRow.nAltStartDate >= prevRow.nEndDate then
error('a row in a consecutive intervals group is 1 day long and misses the date parameter')
end
end
else
if barargs.enddate then
bValid, barargs.nEndDate = pcall(getUnix, barargs.enddate)
assert(bValid, 'invalid enddate "' .. barargs.enddate .. '"')
end
if prevRow.nDate or prevRow.nEndDate then
barargs.nStartDate = (prevRow.nDate or prevRow.nEndDate) + updatePeriod
if barargs.nStartDate == barargs.nEndDate then
barargs.nDate = barargs.nEndDate
barargs[1] = os.date('%Y-%m-%d', barargs.nDate)
end
else
prevRow.nAltEndDate = (prevRow.nStartDate or prevRow.nAltStartDate) + updatePeriod
barargs.nAltStartDate = prevRow.nAltEndDate + updatePeriod
if barargs.nEndDate and barargs.nAltStartDate >= barargs.nEndDate then
error('a row in a consecutive intervals group is 1 day long and misses the date parameter')
end
end
end

local function fillCols(col, change)
local data = args['right' .. col .. 'data']
local changetype = args['changetype' .. col]
local value, num, prevnum

if data == 'alttot1' then
num = barargs.alttot1 or barargs[4]
prevnum = prevRow.alttot1 or prevRow[4]
elseif data == 'alttot2' then
num = barargs.alttot2 or barargs[6]
prevnum = prevRow.alttot2 or prevRow[6]
elseif data then
num = barargs[data+1]
prevnum = prevRow[data+1]
end

if data and num then -- nothing in column, source found, and data exists
value = changetype == 'o' and or lang:formatNum(num) -- set value to num if changetype isn't 'o'

if not change and not barargs['firstright' .. col] then
if prevnum and prevnum ~= 0 then -- data on previous row
if num - prevnum ~= 0 then --data has changed since previous row
change = num-prevnum
if changetype == 'a' then -- change type is "absolute"
if change > 0 then
change = '+' .. lang:formatNum(change)
end
else -- change type is "percent", "only percent" or undefined
local percent = 100 change / prevnum -- calculate percent
local rounding = math.abs(percent) >= 10 and '%.0f' or math.abs(percent) >= 1 and '%.1f' or '%.2f'
percent = tonumber(rounding:format(percent)) -- round to two sigfigs

if percent > 0 then
change = '+' .. lang:formatNum(percent) .. '%'
elseif percent = args.duration
end

local bars, dateList = {}, {}
for i, row in ipairs(rows) do -- build rows
row.divisor = divisor
row.numwidth = args.numwidth
row.rowheight = args.rowheight
row.collapsible = args.collapsible
row.duration = args.duration
row.nooverlap = args.nooverlap
row.daysToEnd = (lastDate - (row.nDate or row.nEndDate or row.nAltEndDate)) / 86400
row.multiyear = multiyear

bars[bars+1], dateList[dateList+1] = p._row(row)
end

return table.concat(bars), dateList
end

function p._legend0(args)
return
..
..
'&nbsp;&nbsp;&nbsp;&nbsp;' .. ..
'&nbsp;' .. (args[2] or ) ..
end

function p._chart(args)
for key, value in pairs(args) do
if ({float=1, barwidth=1, numwidth=1, changetype=1})[key:gsub('%d', )] then
args[key] = value:lower()
end
end

local barargs = {}

barargs.css = 'Module:Medical cases chart/styles.css'
barargs.float = args.float or 'right'

args.barwidth = args.barwidth or 'medium'
local barwidth
if args.barwidth == 'thin' then
barwidth = 120
elseif args.barwidth == 'medium' then
barwidth = 280
elseif args.barwidth == 'wide' then
barwidth = 400
elseif args.barwidth == 'auto' then
barwidth = 'auto'
else
error('unrecognized barwidth')
end

local function _numwidth(i)
local nw = args.numwidth:sub(i, i)
if nw 'n' then
return 0
elseif nw 't' then
return 40
elseif nw 'm' or nw 'd' then
return 55
elseif nw 'w' then
return 70
elseif nw 'x' then
return 85
else
error('unrecognized numwidth[' .. i .. ']')
end
end

args.numwidth = args.numwidth or 'mm'
local numwidth = _numwidth(1) + 10 + _numwidth(2)
local right1width = numwidth
if args.numwidth == 4 then
numwidth = numwidth + _numwidth(3) + _numwidth(4)
if args.numwidth:sub(3, 3) == 'n' then
numwidth = numwidth + 6
else
numwidth = numwidth + 10
end
if not args.right2 then
right1width = numwidth
end
end
right1width = right1width - 8 -- -8 because of padding

if tonumber(barwidth) then
barargs.width = 85 + barwidth + numwidth .. 'px'
barargs.barwidth = barwidth .. 'px'
else
barargs.width = 'auto'
barargs.barwidth = 'auto'
end

local title = {}

local function spaces(n)
local nbsp = '&nbsp;'
return .. nbsp:rep(n) ..
end

local location = lang:ucfirst(mw.ustring.gsub(args.location, i18n.the_, ))
local navbartitle = args.outbreak .. i18n._data .. '/' ..
(args.location3 and args.location3 .. '/' or ) ..
(args.location2 and args.location2 .. '/' or ) ..
location .. i18n._medicalCasesChart

local navbar = require('Module:Navbar/sandbox')._navbar
title[1] = (args.pretitle and args.pretitle .. ' ' or ) ..
args.disease .. ' ' .. i18n.casesIn .. ' ' .. args.location ..
(args.location2 and ', ' .. args.location2 or ) ..
(args.location3 and ', ' .. args.location3 or ) ..
(args.posttitle and ' ' .. args.posttitle or ) .. spaces(2) ..'(' ..
navbar({navbartitle, titleArg=':' .. mw.getCurrentFrame():getParent():getTitle(), mini=1, nodiv=1}) ..
')'

title[2] = p._legend0({p._barColors[1], i18n.deaths})
args.recoveries = args.recoveries == nil and true or args.recoveries
title[3] = args.recoveries and spaces(3) .. p._legend0({p._barColors[2], args.reclbl or i18n.recoveries}) or
title[4] = args.altlbl1 ~= 'hide' and spaces(3) .. p._legend0({p._barColors[3], args.altlbl1 or i18n.activeCases}) or
title[5] = args.altlbl2 and spaces(3) .. p._legend0({p._barColors[4], args.altlbl2}) or
title[6] = args.altlbl3 and spaces(3) .. p._legend0({p._barColors[5], args.altlbl3}) or

local togglesbar, buildargs = nil, {}

args.right1 = args.right1 or i18n.noOfCases
args.duration = args.duration or 15
args.nooverlap = args.nooverlap or false

buildargs.barwidth = tonumber(barwidth) or 280
buildargs.numwidth = args.numwidth
buildargs.rowheight = args.rowheight
if args.datapage then
local externalData = require('Module:Medical cases chart/data')._externalData
buildargs.data = externalData(args)
else
buildargs.data = args.data
end
-- if no right1data and right1 title is cases, use 3rd classification
buildargs.right1data = args.right1data or args.right1 == i18n.noOfCases and 3
-- if no right2data and right2 title is deaths, use 1st classification
buildargs.right2data = args.right2data or (args.right2 i18n.noOfDeaths or args.right2 i18n.noOfDeaths2) and 1
buildargs.changetype1 = (args.changetype1 or args.changetype or ):sub(1, 1) -- 1st letter
buildargs.changetype2 = (args.changetype2 or args.changetype or ):sub(1, 1) -- 1st letter
buildargs.collapsible = args.collapsible
buildargs.duration = args.duration
buildargs.nooverlap = args.nooverlap

local dateList
barargs.bars, dateList = p._buildBars(buildargs)

if buildargs.collapsible then
togglesbar = p._buildTogglesBar(dateList, args.duration, args.nooverlap)
end

title[7] = togglesbar and .. togglesbar or
barargs.title = table.concat(title)

barargs.left1 =
.. -- 85-8 because of padding
"" .. i18n.date .. "" ..

barargs.right1 =
..
"" .. args.right1 .. "" ..

if args.right2 then
local right2width = numwidth - right1width - 16 -- -8-8...
barargs.right2 =
..
"" .. args.right2 .. "" ..
end

barargs.caption = args.caption
return barBox._box(barargs)
end

function p.chart(frame)
local getArgs = require('Module:Arguments').getArgs
local args = getArgs(frame, {
valueFunc = function (key, value)
if value and value ~= then
key = key:gsub('%d', )
if ({rowheight=1, duration=1, rightdata=1})[key] then -- if key in {...}
return tonumber(value) or value
end
if ({recoveries=1, collapsible=1, nooverlap=1})[key] then
return yesno(value)
end
return value
end
return nil
end
})
return p._chart(args)
end

return p