Jump to content

Module:Medical cases chart/sandbox2: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
intermediate save
finished stas summary on mause-over
Line 340: Line 340:
xData1Key = (type(xData1Key) == "number") and (xData1Key+1) or xData1Key
xData1Key = (type(xData1Key) == "number") and (xData1Key+1) or xData1Key
xData2Key = (type(xData2Key) == "number") and (xData2Key+1) or xData2Key
xData2Key = (type(xData2Key) == "number") and (xData2Key+1) or xData2Key
local nPop = args.population
local nPop = not (args.population == nil) and tonumber(args.population) or nil
local bIsW1 = sChngTp1 == 'w' and nPop
local bIsW1 = sChngTp1 == 'w' and nPop
local bIsW2 = sChngTp2 == 'w' and nPop
local bIsW2 = sChngTp2 == 'w' and nPop


local rows, prevRow, tStats = {}, {}, {}
local rows, prevRow, tStats = {}, {}, {}
local nData1Diff1Max, nData1Diff1MaxDate, nData12Diff1Max, nData2Diff1MaxDate
local nData1Diff1Max, nData1Diff1MaxDate, nData2Diff1Max, nData2Diff1MaxDate
local nData1i7Max, nData1i7MaxDate, nData12i7Max, nData2i7MaxDate
local nData1i7Max, nData1i7MaxDate, nData2i7Max, nData2i7MaxDate
for line in mw.text.gsplit(args.data, '\n') do
for line in mw.text.gsplit(args.data, '\n') do
local i, barargs = 1, {}
local i, barargs = 1, {}
Line 438: Line 438:
local nData1 = barargs[xData1Key] or barargs[4]
local nData1 = barargs[xData1Key] or barargs[4]
local nData2 = barargs[xData2Key] or barargs[2]
local nData2 = barargs[xData2Key] or barargs[2]
if bIsW1 and nData1 then
Line 444: Line 443:
-- local nData1i7Max, nData1i7MaxDate, nData12i7Max, nData2i7MaxDate
-- local nData1i7Max, nData1i7MaxDate, nData12i7Max, nData2i7MaxDate


if bIsW1 and nData1 then
tBarStats.nData1 = nData1
tBarStats.nData1 = nData1
-- if stats exist from day before
tBarStats.nPop = nPop
if not (tBarStats1 == nil) then
if not (tBarStats1 == nil) and not (tBarStats1.nData1 == nil) then
tBarStats.nData1Diff1 = nData1 - tBarStats1.nData1
if not (tBarStats1.nData1 == nil) then
if nData1Diff1Max == nil or nData1Diff1Max < tBarStats.nData1Diff1 then
tBarStats.nData1Diff1 = nData1 - tBarStats1.nData1
if nData1Diff1Max == nil or nData1Diff1Max < tBarStats.nData1Diff1 then
nData1Diff1Max = tBarStats.nData1Diff1
nData1Diff1MaxDate = barargs[1]
nData1Diff1Max = tBarStats.nData1Diff1
nData1Diff1MaxDate = barargs[1]
end
end
end
end
end
-- if stats exist from 7 days before
if not (tBarStats7 == nil) then
if not (tBarStats7 == nil) then
if not (tBarStats7.nData1 == nil) then
if not (tBarStats7.nData1 == nil) then
tBarStats.nData1Diff7 = nData1 - tBarStats7.nData1
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
end
if not (tBarStats7.nData1Diff1 == nil) then
if not (tBarStats7.nData1Diff1 == nil) then
Line 468: Line 466:
end
end
end
end
-- if stats exist from 14 days before
if not (tBarStats14 == nil) then
if not (tBarStats14 == nil) then
if not (tBarStats14.nData1 == nil) then
if not (tBarStats14.nData1 == nil) then
Line 479: Line 478:
if bIsW2 and nData2 then
if bIsW2 and nData2 then
tBarStats.nData2 = nData2
tBarStats.nData2 = nData2
if not (tBarStats1 == nil) then
-- if stats exist from day before
if not (tBarStats1.nData2 == nil) then
if not (tBarStats1 == nil) and not (tBarStats1.nData2 == nil) then
tBarStats.nData2Diff1 = nData2 - tBarStats1.nData2
tBarStats.nData2Diff1 = nData2 - tBarStats1.nData2
if nData2Diff1Max == nil or nData2Diff1Max < tBarStats.nData2Diff1 then
nData2Diff1Max = tBarStats.nData2Diff1
nData2Diff1MaxDate = barargs[1]
end
end
end
end
-- if stats exist from 7 days before
if not (tBarStats7 == nil) then
if not (tBarStats7 == nil) then
if not (tBarStats7.nData2 == nil) then
if not (tBarStats7.nData2 == nil) then
tBarStats.nData2Diff7 = nData2 - tBarStats7.nData2
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
end
end
-- if stats exist from 14 days before
if not (tBarStats14 == nil) then
if not (tBarStats14 == nil) then
if not (tBarStats14.nData2 == nil) then
if not (tBarStats14.nData2 == nil) then
tBarStats.nData2Diff14 = nData2 - tBarStats14.nData2
tBarStats.nData2Diff14 = nData2 - tBarStats14.nData2
end
if not (tBarStats14.nData2Diff1 == nil) then
tBarStats.nData2P14Diff1 = tBarStats14.nData2Diff1
end
end
end
end
Line 517: Line 531:
if not (tBarStats == nil) and not (tBarStats["nData"..col] == nil) then
if not (tBarStats == nil) and not (tBarStats["nData"..col] == nil) then
local nDiff7 = tBarStats["nData"..col.."Diff7"]
local nDiff7 = tBarStats["nData"..col.."Diff7"]
--local sChngCmt
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
if nDiff7 == nil then
change = i18n.na
change = i18n.na
else
else
change = '<span title="7 days incidence per 100,000 population.">' .. tostring(mw.ustring.format('%.1f', nDiff7/args.population*100000)) .. '</span>'
change = '<span title="'.. sChngCmt .. '">' .. tostring(mw.ustring.format('%.1f', nDiff7/args.population*100000)) .. '</span>'
end
end
local nValue = tBarStats["nData"..col]
local nValue = tBarStats["nData"..col]
Line 540: Line 559:
end
end
if col == 1 and not (nData1Diff1Max == nil) and not (sCmnt == "") then
if col == 1 and not (nData1Diff1Max == nil) and not (sCmnt == "") then
sCmnt = sCmnt .. ", all-time max: +" .. lang:formatNum(nData1Diff1Max) .. " at " .. nData1Diff1MaxDate
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
end
value = '<span title="' .. sCmnt ..'">' .. nValue .. '</span>'
value = '<span title="' .. sCmnt ..'">' .. lang:formatNum(nValue) .. '</span>'
elseif data and num then -- nothing in column, source found, and data exists
elseif data and num then -- nothing in column, source found, and data exists

Revision as of 18:52, 22 January 2022

local yesno = require('Module:Yesno')
local BarBox = unpack(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._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 =
		'<span class="mw-collapsible' .. on  .. customtoggles .. '" id="mw-customcollapsible-' .. id .. '" ' ..
		'style="border:2px solid lightblue">' .. label .. '</span>' ..
		'<span class="mw-collapsible' .. off .. customtoggles .. '" id="mw-customcollapsible-' .. id .. '">' .. label  .. '</span>'
	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 = {{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] =
				'<div class="mw-collapsible' .. (year.l and '' or ' mw-collapsed') ..
				'" id="mw-customcollapsible-' .. year.year .. '">' .. table.concat(monthToggles) .. '</div>'
		end
		divs = '<div>' .. table.concat(yearToggles) .. '</div>' .. table.concat(monthsDivs)
	else
		for i, month in ipairs(months) do
			monthToggles[#monthToggles+1] = p._monthToggleButton(nil, month)
		end
		divs = '<div>' .. table.concat(monthToggles) .. '</div>'
	end
	divs = divs .. '<div>' .. p._lastXToggleButton(years, duration, combinationsL) .. '</div>'
	return '<div class="nomobile" style="text-align:center">' .. divs .. '</div>'
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] =
			'<span class=mcc-r' .. _numwidth(1) .. '>' .. (args[7] or '') .. '</span>' ..
			'<span class=mcc-l' .. _numwidth(2) .. '>' .. (args[8] or '') .. '</span>'
	end
	if #args.numwidth == 4 then
		barargs.note2 = (args[9] or args[10]) and
			(args.numwidth:sub(3,3) ~= 'n' and '<span class=mcc-r' .. _numwidth(3) .. '>' .. (args[9] or '') .. '</span>' or '') ..
			'<span class=mcc-l' .. _numwidth(4) .. '>' .. (args[10] or '') .. '</span>'
		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 = '<span title="'.. sChngCmt .. '">' .. tostring(mw.ustring.format('%.1f', nDiff7/args.population*100000)) .. '</span>'
				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 = '<span title="' .. sCmnt ..'">' .. lang:formatNum(nValue)  .. '</span>'
			
			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 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 ('<span title="7 days rolling average of daily change">r7: ' .. tostring(mw.ustring.format('%.0f', nDiff/7)) .. '</span>') or i18n.na
										else
											change = nDiff and ('<span title="7 days incidence per 100,000 population.">' .. tostring(mw.ustring.format('%.1f', nDiff/args.population*100000)) .. '</span>') or i18n.na
											if tPrevDayRow and tPrevDayRow[nDatCol] then
												value = '<span title="daily change: +' .. lang:formatNum(num - tPrevDayRow[nDatCol]) .. '>' .. value  .. '</span>'
											end
										end
									else
										change = i18n.na
									end
								else
									change = i18n.na
								end


							else -- change type is "percent", "only percent" or undefined
								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
		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

	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
		'<span style="font-size:90%; margin:0px">' ..
			'<span style="background-color:' .. (args[1] or 'none') ..
			'; border:' .. (args.border or 'none') ..
			'; color:' .. (args[1] or 'none') .. '">' ..
				'&nbsp;&nbsp;&nbsp;&nbsp;' .. '</span>' ..
			'&nbsp;' .. (args[2] or '') .. '</span>'
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 = '&nbsp;'
		return '<span class="nowrap">' .. nbsp:rep(n) .. '</span>'
	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}) ..
		')<br />'

	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

	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 '<br />' .. togglesbar or ''
	barargs.title = table.concat(title)

	barargs.left1 = '<div style="width:7.08em">' .. i18n.date .. '</div>'
	barargs.right1 = '<div class=center style="width:' .. right1width .. 'em">' .. args.right1 .. '</div>' --center isn't necessary with proper
	if args.right2 then																					   --numwidth, but better safe than sorry
		barargs.right2 = '<div class=center style="width:' .. right2width ..'em">' .. args.right2 .. '</div>'
	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