Module:Medical cases chart/sandbox

local yesno = require('Module:Yesno')

local BarBox = unpack(require('Module:Bar/sandbox'))

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._findIntervalRow(tRows, nTime, nTol, bAll)

-- Loop backwards in tRows, assuming it to have monotonically increasing nDate entries in forward order.

-- The first row with nDate within nTime +-nTol is returned.

-- If nDate table entry is nil, the row will be skipped.

-- If moving backwards a time stamp is found dating back to before tolerance window, nil is returned.

-- If the end of tRows is reached without a match for the tolerance window, also nil is returned.

-- With bAll present and true all rows back to the specified time window will be returned, or all rows back to

-- the first row, that does not lie beyond nTime +-nTol if no match was found.

local tRet = nil

if bAll then

tRet = {}

end

for nRowNum = #tRows, 1 ,-1 do

if tRows[nRowNum] and tRows[nRowNum].nDate then

local nDiff = nTime - tRows[nRowNum].nDate

if nDiff > -nTol then

if nDiff < nTol then

if bAll then

tRet[#tRet + 1] = tRows[nRowNum]

return tRet

else

return tRows[nRowNum]

end

else

return bAll and tRet or nil

end

end

end

if bAll then

tRet[#tRet + 1] = tRows[nRowNum]

end

end

--return tRows[nRowNum]

return nil

end

function p._toggleButton(active, customtoggles, id, label)

local on = active and '' or ' mw-collapsed'

local off = active and ' mw-collapsed' or ''

local outString =

'

'style="border:2px solid lightblue">' .. 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 .. ' ' .. 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 = {{year=dateList[1].year, months={{mon=dateList[1].mon, combinations={}}}}}

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

local numwidth = {n=0, t=2.45, m=3.5, d=3.5, w=4.55, x=5.6}

local bkgClasses = {

'mcc-d', --deaths

'mcc-r', --recoveries

'mcc-c', --cases or altlbl1

'mcc-a2', --altlbl2

'mcc-a3' --altlbl3

}

function p._customBarStacked(args)

local barargs = {}

barargs[1] = args[1]

local function _numwidth(i)

return args.numwidth:sub(i,i)

end

if args[7] or args[8] then -- is it acceptable to have one and not the other?

barargs[2] =

'' .. (args[7] or '') .. '' ..

'' .. (args[8] or '') .. ''

end

if #args.numwidth == 4 then

barargs.note2 = (args[9] or args[10]) and

(args.numwidth:sub(3,3) ~= 'n' and '' .. (args[9] or ) .. '' or ) ..

'' .. (args[10] or '') .. ''

or ''

end

for i = 1, 5 do

barargs[i+2] = args[i+1] / args.divisor

barargs['title' .. i] = args[i+1]

end

barargs.align = 'cdcc'

barargs.bkgclasses = bkgClasses

barargs.collapsed = args.collapsed

barargs.id = args.id

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 ~= '' and 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

local dates

if args.collapsible then

local duration = args.duration

if args.daysToEnd >= duration then

barargs.collapsed = true

else

barargs.collapsed = false

end

if args.nooverlap and args.daysToEnd < duration then

barargs.id = 'l' .. duration

elseif args.nDate then

dates = {year=tonumber(os.date('%Y', args.nDate)), mon=lang:formatDate('M', os.date('%Y-%m', args.nDate)),

l=args.daysToEnd < duration and '-l' .. duration, nDate=args.nDate}

barargs.id = (args.multiyear and dates.year or ) .. lang:lc(dates.mon) .. (dates.l or )

else

local id, y, m, ey, em = {},

tonumber(os.date('%Y', args.nStartDate or args.nAltStartDate)),

tonumber(os.date('%m', args.nStartDate or args.nAltStartDate)),

tonumber(os.date('%Y', args.nEndDate or args.nAltEndDate )),

tonumber(os.date('%m', args.nEndDate or args.nAltEndDate ))

dates = {nStartDate=args.nStartDate, nAltStartDate=args.nAltStartDate, nEndDate=args.nEndDate, nAltEndDate=args.nAltEndDate}

repeat

id[#id+1] = (args.multiyear and y or '') .. lang:lc(monthAbbrs[m])

dates[#dates+1] = {year=y, mon=monthAbbrs[m]}

y = y + math.floor(m / 12)

m = m % 12 + 1

until y == ey and m > em or y > ey

dates.l = args.daysToEnd < duration and '-l' .. duration

id = table.concat(id, '-') .. (dates.l or '')

barargs.id = id

dates.id = id

end

else

barargs.collapsed = false

end

return p._customBarStacked(barargs), dates

end

function p._buildBars(args)

local frame = mw.getCurrentFrame()

local updatePeriod = 86400 -- temporary implementation only supports daily updates

local function getUnix(timestamp)

return lang:formatDate('U', timestamp)

end

-- some info for changetype 'w'

local sChngTp1 = args.changetype1

local sChngTp2 = args.changetype2

local xData1Key = args.right1data or 3

local xData2Key = args.right2data or 1

xData1Key = (type(xData1Key) == "number") and (xData1Key+1) or xData1Key

xData2Key = (type(xData2Key) == "number") and (xData2Key+1) or xData2Key

local nPop = not (args.population == nil) and tonumber(args.population) or nil

local bIsW1 = sChngTp1 == 'w' and nPop

local bIsW2 = sChngTp2 == 'w' and nPop

local rows, prevRow, tStats = {}, {}, {}

local nData1Diff1Max, nData1Diff1MaxDate, nData2Diff1Max, nData2Diff1MaxDate

local nData1i7Max, nData1i7MaxDate, nData2i7Max, nData2i7MaxDate

for line in mw.text.gsplit(args.data, '\n') do

local i, barargs = 1, {}

-- line parameter parsing, basic type/missing value handling

for parameter in mw.text.gsplit(line, ';') do

if parameter:find('^%s*%a') then

parameter = mw.text.split(parameter, '=')

parameter[1] = mw.text.trim(parameter[1])

if parameter[1]:find('^alttot') then

parameter[2] = tonumber(frame:callParserFunction('#expr', parameter[2]))

else

parameter[2] = mw.text.trim(parameter[2])

if parameter[1]:find('^firstright') then

parameter[2] = yesno(parameter[2])

elseif parameter[2] == '' then

parameter[2] = nil

end

end

barargs[parameter[1]] = parameter[2]

else

parameter = mw.text.trim(parameter)

if parameter ~= '' then

if i >= 2 and i <= 6 then

parameter = tonumber(frame:callParserFunction('#expr', parameter))

if not parameter then

error(('Data parameters 2 to 6 must not be formatted. i=%d, line=%s'):format(i, line))

end

end

barargs[i] = parameter

end

i = i + 1

end

end

local bValid, nDateDiff

-- get relevant date info based on previous row

if barargs[1] then

bValid, barargs.nDate = pcall(getUnix, barargs[1])

assert(bValid, 'invalid date "' .. barargs[1] .. '"')

if prevRow.nDate or prevRow.nEndDate then

nDateDiff = barargs.nDate - (prevRow.nDate or prevRow.nEndDate)

if nDateDiff > 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

-- update tStats if at least one column changetype is 'w'

local tBarStats = nil

if barargs[1] and (bIsW1 or bIsW2) then

bValid, barargs.nDate = pcall(getUnix, barargs[1])

assert(bValid, 'invalid date "' .. barargs[1] .. '"')

barargs.nDate = tonumber(barargs.nDate)

tBarStats = {}

local tBarStats1 = tStats[barargs.nDate-86400] -- previous days info

local tBarStats7 = tStats[barargs.nDate-604800] -- 7 days before info

local tBarStats14 = tStats[barargs.nDate-1209600] -- 14 days before info

local nData1 = barargs[xData1Key] or barargs[4]

local nData2 = barargs[xData2Key] or barargs[2]

-- local nData1Diff1Max, nData1Diff1MaxDate, nData12Diff1Max, nData2Diff1MaxDate

-- local nData1i7Max, nData1i7MaxDate, nData12i7Max, nData2i7MaxDate

if bIsW1 and nData1 then

tBarStats.nData1 = nData1

-- if stats exist from day before

if not (tBarStats1 == nil) and not (tBarStats1.nData1 == nil) then

tBarStats.nData1Diff1 = nData1 - tBarStats1.nData1

if nData1Diff1Max == nil or nData1Diff1Max < tBarStats.nData1Diff1 then

nData1Diff1Max = tBarStats.nData1Diff1

nData1Diff1MaxDate = barargs[1]

end

end

-- if stats exist from 7 days before

if not (tBarStats7 == nil) then

if not (tBarStats7.nData1 == nil) then

tBarStats.nData1Diff7 = nData1 - tBarStats7.nData1

if nData1i7Max == nil or nData1i7Max < tBarStats.nData1Diff7/nPop*100000 then

nData1i7Max = tBarStats.nData1Diff7/nPop*100000

nData1i7MaxDate = barargs[1]

end

end

if not (tBarStats7.nData1Diff1 == nil) then

tBarStats.nData1P7Diff1 = tBarStats7.nData1Diff1

end

end

-- if stats exist from 14 days before

if not (tBarStats14 == nil) then

if not (tBarStats14.nData1 == nil) then

tBarStats.nData1Diff14 = nData1 - tBarStats14.nData1

end

if not (tBarStats14.nData1Diff1 == nil) then

tBarStats.nData1P14Diff1 = tBarStats14.nData1Diff1

end

end

end

if bIsW2 and nData2 then

tBarStats.nData2 = nData2

-- if stats exist from day before

if not (tBarStats1 == nil) and not (tBarStats1.nData2 == nil) then

tBarStats.nData2Diff1 = nData2 - tBarStats1.nData2

if nData2Diff1Max == nil or nData2Diff1Max < tBarStats.nData2Diff1 then

nData2Diff1Max = tBarStats.nData2Diff1

nData2Diff1MaxDate = barargs[1]

end

end

-- if stats exist from 7 days before

if not (tBarStats7 == nil) then

if not (tBarStats7.nData2 == nil) then

tBarStats.nData2Diff7 = nData2 - tBarStats7.nData2

if nData2i7Max == nil or nData2i7Max < tBarStats.nData2Diff7/nPop*100000 then

nData2i7Max = tBarStats.nData2Diff7/nPop*100000

nData2i7MaxDate = barargs[1]

end

end

if not (tBarStats7.nData2Diff1 == nil) then

tBarStats.nData2P7Diff1 = tBarStats7.nData2Diff1

end

end

-- if stats exist from 14 days before

if not (tBarStats14 == nil) then

if not (tBarStats14.nData2 == nil) then

tBarStats.nData2Diff14 = nData2 - tBarStats14.nData2

end

if not (tBarStats14.nData2Diff1 == nil) then

tBarStats.nData2P14Diff1 = tBarStats14.nData2Diff1

end

end

end

tStats[barargs.nDate] = tBarStats

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

-- changetype w

if not (tBarStats == nil) and not (tBarStats["nData"..col] == nil) then

local nDiff7 = tBarStats["nData"..col.."Diff7"]

local sChngCmt = ""

if col == 1 and not (nData1i7Max == nil) then

sChngCmt = "all time high: " .. mw.ustring.format('%.1f', nData1i7Max) .. " on " .. nData1i7MaxDate

elseif col == 2 and not (nData2i7Max == nil) then

sChngCmt = "all time high: " .. mw.ustring.format('%.1f', nData2i7Max) .. " on " .. nData2i7MaxDate

end

if nDiff7 == nil then

change = i18n.na

else

change = '' .. tostring(mw.ustring.format('%.1f', nDiff7/args.population*100000)) .. ''

end

local nValue = tBarStats["nData"..col]

local nDiff1 = tBarStats["nData"..col.."Diff1"]

local nP7Diff1 = tBarStats["nData"..col.."P7Diff1"]

local nP14Diff1 = tBarStats["nData"..col.."P14Diff1"]

local sCmnt

if nDiff1 == nil then

sCmnt = ""

else

sCmnt = "daily change: +" .. lang:formatNum(nDiff1)

end

if not (nP7Diff1 == nil) and not (sCmnt == "") then

sCmnt = sCmnt .. ", 7 days before: +" .. lang:formatNum(nP7Diff1)

end

if not (nP14Diff1 == nil) and not (sCmnt == "") then

sCmnt = sCmnt .. ", 14 days before: +" .. lang:formatNum(nP14Diff1)

end

if col == 1 and not (nData1Diff1Max == nil) and not (sCmnt == "") then

sCmnt = sCmnt .. ", all-time high: +" .. lang:formatNum(nData1Diff1Max) .. " on " .. nData1Diff1MaxDate

end

if col == 2 and not (nData2Diff1Max == nil) and not (sCmnt == "") then

sCmnt = sCmnt .. ", all-time high: +" .. lang:formatNum(nData2Diff1Max) .. " on " .. nData2Diff1MaxDate

end

value = '' .. lang:formatNum(nValue) .. ''

elseif 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 yesno(args.weekly) then

prevnum = nil

if barargs.nDate and rows then

local tPrevWeekRow = p._findIntervalRow(rows, barargs.nDate-7*24*3600, 3600, false)

if tPrevWeekRow then

prevnum = tPrevWeekRow[(col==1) and 4 or 2]

end

end

end

if prevnum and prevnum ~= 0 then -- data on previous row

if num - prevnum ~= 0 then --data has changed since previous row

local nChange = num - prevnum

if changetype == 'a' then -- change type is "absolute"

if nChange > 0 then

change = '+' .. lang:formatNum(nChange)

end

elseif (changetype == 'w' and args.population) then -- changetype == 'r' or

-- change type is "r"(olling average over 7 days period) or "w"(eekly incidence per 100.000 pop)

if barargs.nDate and rows then

-- find data row from 7 days before +- 1 hour

local tIntervRow = p._findIntervalRow(rows, barargs.nDate-7*24*3600, 3600, false)

local tPrevDayRow = p._findIntervalRow(rows, barargs.nDate-24*3600, 3600, false)

if tIntervRow then

local nDatCol = (col==1) and 4 or 2

local nDiff = tIntervRow[nDatCol] and (num - tIntervRow[nDatCol]) or nil

if changetype == 'r' then

change = nDiff and ('r7: ' .. tostring(mw.ustring.format('%.0f', nDiff/7)) .. '') or i18n.na

else

change = nDiff and ('' .. tostring(mw.ustring.format('%.1f', nDiff/args.population*100000)) .. '') or i18n.na

if tPrevDayRow and tPrevDayRow[nDatCol] then

value = '

local percent = 100 * nChange / 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 < 0 then

change = lang:formatNum(percent) .. '%'

else

change = '='

end

end

else -- data has not changed since previous row

change = '='

end

else -- no data on previous row

barargs['firstright' .. col] = true -- set to (n.a.)

end

end

end

return value, change

end

if not barargs[7] then

barargs[7], barargs[8] = fillCols(1, barargs[8])

end

if not barargs[9] then

barargs[9], barargs[10] = fillCols(2, barargs[10])

end

rows[#rows+1] = barargs

prevRow = barargs

end

--error(mw.dumpObject(tStats))

-- calculate and pass repetitive (except daysToEnd) parameters to each row

local lastRow = rows[#rows]

local total = {lastRow[2] or 0, lastRow[3] or 0, [4]=lastRow[5] or 0}

total[3] = lastRow.alttot1 or lastRow[4] and lastRow[4] - total[1] - total[2] or 0

total[5] = lastRow.alttot2 or lastRow[6] and lastRow[6] - total[1] - total[2] or 0

local divisor = (total[1] + total[2] + total[3] + total[4] + total[5]) / (args.barwidth - 5) --should be -3 if borders didn't go inward

local firstDate, lastDate = rows[1].nDate, lastRow.nDate or lastRow.nEndDate

local multiyear = os.date('%Y', firstDate) ~= os.date('%Y', lastDate - (args.nooverlap and args.duration * 86400 or 0))

if args.collapsible ~= false then

args.collapsible = (lastDate - firstDate) / 86400 >= args.duration

end

local bars, dateList = {}, {}

for i, row in ipairs(rows) do -- build rows

if (not yesno(args.weekly)) or (math.mod(i-1, 7) == 0) then

row.divisor = divisor

row.numwidth = args.numwidth

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

end

return table.concat(bars, '\n'), dateList

end

p._barColors = { -- also in styles.css

'#A50026', --deaths

'SkyBlue', --recoveries

'Tomato', --cases or altlbl1

'Gold', --altlbl2

'OrangeRed' --altlbl3

}

function p._legend0(args)

return

'' ..

'' ..

'    ' .. '' ..

' ' .. (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)

return assert(numwidth[nw], 'unrecognized numwidth[' .. i .. ']')

end

args.numwidth = args.numwidth or 'mm'

if args.numwidth:sub(1,1) == 'n' or args.numwidth:sub(2,2) == 'n' or args.numwidth:sub(4,4) == 'n' then

error('"n" is only allowed in numwidth[3]')

end

local buffer = 0.3 --until automatic numwidth determination

local right1width, right2width = _numwidth(1) + 0.3 + _numwidth(2) + buffer, 0

if #args.numwidth == 4 then

right2width = _numwidth(3) + _numwidth(4) + buffer

if args.numwidth:sub(3,3) ~= 'n' then

right2width = right2width + 0.3

end

if args.right2 then

right2width = math.ceil(right2width / 0.88 * 100) / 100 -- from td scale to th

else

right1width = right1width + 0.8 + right2width

right2width = 0

end

end

right1width = math.ceil(right1width / 0.88 * 100) / 100

if tonumber(barwidth) then

-- transform colswidth from th to td scale, add it with border-spacing, and finally transform to table scale

local relwidth = math.ceil(((7.08 + right1width + right2width) * 0.88 + 0.8 * (args.right2 and 5 or 4)) * 88) / 100

barargs.width = 'calc(' .. relwidth .. 'em + ' .. barwidth .. 'px)' --why do the bar borders go inward (no +2)?

barargs.barwidth = barwidth .. 'px'

else

barargs.width = 'auto'

barargs.barwidth = 'auto'

end

barargs.lineheight = args.rowheight

local title = {}

local function spaces(n)

local 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')._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:gsub('d', 'm')

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

buildargs.population = args.population

buildargs.weekly = args.weekly

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 = '

' .. i18n.date .. '
'

barargs.right1 = '

' .. args.right1 .. '
' --center isn't necessary with proper

if args.right2 then --numwidth, but better safe than sorry

barargs.right2 = '

' .. args.right2 .. '
'

end

barargs.footer = args.footer

local box = BarBox.create(barargs)

return tostring(box)

end

local getArgs = require('Module:Arguments').getArgs

function p.barColors(frame)

local args = getArgs(frame)

return p._barColors[tonumber(args[1])]

end

function p.chart(frame)

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