Jump to content

Module:Medical cases chart

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Alexiscoutinho (talk | contribs) at 13:02, 1 January 2021 (Granted EUA for refactored toggling system (with year toggles)). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

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

local language = 'en-US' -- local default language

local i18n = require("Module:Medical cases chart/i18n")[language]

local function is(v)
	return (v or '') ~= ''
end

local p = {}

function p._barColors(n)
	local colors = {
		'#A50026', --deaths
		'SkyBlue', --recoveries
		'Tomato', --cases or altlbl1
		'Gold', --altlbl2
		'OrangeRed' --altlbl3
	}

	return colors[n]
end

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._customBarStacked(args)
	barargs = {}
	barargs[1] = args[1]

	local function _numwidth(nw)
		if nw == 'n' then
			return 0
		elseif nw == 't' then
			return 2.45
		elseif nw == 'm' then
			return 3.5
		elseif nw == 'w' then
			return 4.55
		elseif nw == 'x' then
			return 5.6
		elseif nw == 'd' then
			return 3.5
		end

		return 3.5
	end

	width1 = 3.5
	width2 = 3.5
	if is(args.numwidth) then
		width1 = _numwidth(mw.ustring.sub(args.numwidth,1,1))
		width2 = _numwidth(mw.ustring.sub(args.numwidth,2,2))
		width3 = _numwidth(mw.ustring.sub(args.numwidth,3,3))
		width4 = _numwidth(mw.ustring.sub(args.numwidth,4,4))
	end

	barargs[2] =
		'<span class="cbs-ibr" style="padding:0 0.3em 0 0; width:' .. width1 .. 'em">' .. (args[7] or '') .. '</span>' ..
		'<span class="cbs-ibl" style="width:' .. width2 .. 'em">' .. (args[8] or '') .. '</span>'

	if mw.ustring.len(args.numwidth) == 4 then
		local padding = '0.3em'
		if mw.ustring.sub(args.numwidth,3,3) == 'n' then
			padding = '0'
		end

		barargs.note2 =
			'<span class="cbs-ibr" style="padding:0 ' .. padding .. ' 0 0; width:' .. width3 .. 'em">' .. (args[9] or '') .. '</span>' ..
			'<span class="cbs-ibl" style="width:' .. width4 .. 'em">' .. (args[10] or '') .. '</span>'
	end

	for i=1,5 do
		barargs[2*i + 1] = p._barColors(i)
		barargs[2*i + 2] = (tonumber(args[i+1]) or 0)/(tonumber(args.divisor) or 1)
		barargs['title' .. i] = args[i+1]
	end

	barargs.align = 'cdcc'
	barargs.collapsed = args.collapsed
	barargs.id = args.id
	barargs.rowstyle = is(tonumber(args.rowheight)) and ('line-height:'..args.rowheight..';') or nil

	return barBox._stacked(barargs)
end

function p._row(args)
	local barargs = {}
	local rowDate = args.prevDate or ''

	if is(args[1]) then
		if pcall(function () lang:formatDate('', args[1]) end) then
			barargs[1] = args[1]
			rowDate = args[1]
		else
			barargs[1] = '<strong class="error">' .. i18n.invalidTime .. '</strong>'
		end
	else
		barargs[1] = '⋮'
	end
	barargs[1] = barargs[1] .. (args['note0'] or '')

	barargs[2] = args[2] or 0
	barargs[3] = args[3] or 0

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

	barargs[5] = args[5] or 0

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

	barargs[7] = args[7] or ''

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

		return change
	end

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

	barargs.divisor = args.divisor or 1
	barargs.numwidth = args.numwidth
	barargs.rowheight = args.rowheight

	if yesno(args.collapsible) == true then
		local duration = tonumber(args.duration) or 15
		if args.collapsed then
			barargs.collapsed = args.collapsed
		elseif args.rowsToEnd >= duration then
			barargs.collapsed = 'y'
		else
			barargs.collapsed = ''
		end

		if args.id then
			barargs.id = args.id
		elseif args.nooverlap and args.rowsToEnd < duration then
			barargs.id = 'l' .. duration
		else
			barargs.id = (args.multiYear and lang:formatDate('Y', rowDate) .. '-' or '') ..
				mw.ustring.lower(lang:formatDate('M', rowDate)) .. (args.rowsToEnd < duration and '-l' .. duration or '')
		end
	else
		barargs.collapsed = ''
		barargs.id = ''
	end

	return p._customBarStacked(barargs)
end

function p._buildBars(args)
	local lines = mw.text.split(args.data, '\n')
	local frame = mw.getCurrentFrame()

	local bars, rows, dateList, prevRow, maxparam = {}, {}, {}, '', 1
	for k, line in pairs(lines) do
		local barargs, i = {}, 1
		for parameter in mw.text.gsplit(line, ';') do
			parameter = mw.text.trim(parameter)
			if string.find(parameter, '^%a') then
				parameter = mw.text.split(parameter, '=')
				if parameter[1] == 'alttot1' or parameter[1] == 'alttot2' then
					parameter[2] = tonumber(frame:callParserFunction('#expr', parameter[2]))
					if is(parameter[2]) then
						maxparam = math.max(maxparam, parameter[2])
					end
				end
				barargs[parameter[1]] = parameter[2]
			else
				if is(parameter) then
					if i >= 2 and i <= 6 then
						parameter = tonumber(frame:callParserFunction('#expr', frame:callParserFunction('formatnum',parameter,'R')))
						maxparam = math.max(maxparam, parameter or 1)
					end
					barargs[i] = parameter
					if i == 7 or i == 9 then
						parameter = tonumber(mw.ustring.match(frame:callParserFunction('formatnum',parameter,'R'), '^%d*'))
						maxparam = math.max(maxparam, parameter or 1)
					end
				end
				i = i + 1
			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 = tonumber(barargs.alttot1 or barargs[4])
				prevnum = tonumber(prevRow.alttot1 or prevRow[4])
			elseif data == 'alttot2' then
				num = tonumber(barargs.alttot2 or barargs[6])
				prevnum = tonumber(prevRow.alttot2 or prevRow[6])
			elseif is(data) then
				num = tonumber(barargs[tonumber(data) + 1])
				prevnum = tonumber(prevRow[tonumber(data) + 1])
			end

			if is(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 yesno(barargs['firstright' .. col] ~= true) 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(mw.ustring.format(rounding, percent)) -- round to two sigfigs

								if percent > 0 then
									change = '+' .. lang:formatNum(percent) .. '%'
								elseif percent < 0 then
									change = lang:formatNum(percent) .. '%'
								else
									change = i18n['=']
								end
							end
						else -- data has not changed since previous row
							change = i18n['=']
						end
					else -- no data on previous row
						barargs['firstright' .. col] = true -- set to (n.a.)
					end
				end
			end

			return value, change
		end

		if not is(barargs[7]) then
			barargs[7], barargs[8] = fillCols(1, barargs[8])
		end
		if not is(barargs[9]) then
			barargs[9], barargs[10] = fillCols(2, barargs[10])
		end

		if is(barargs[1]) then
			local e, year, mon, d = pcall(
				function ()
					return lang:formatDate('Y', barargs[1]),
						lang:formatDate('M', barargs[1]),
						lang:formatDate('j', barargs[1])
				end
			)
			if e then
				dateList[#dateList+1] = {year=year, mon=mon, d=d}
			end
		end

		barargs.prevDate = prevRow[1]
		rows[#rows+1] = barargs
		prevRow = barargs
	end

	local nooverlap, lastRow = args.nooverlap, #dateList
	if nooverlap == true then
		if #dateList <= args.duration then
			nooverlap = false
		else
			lastRow = #dateList - args.duration
		end
	end

	local years = {{year=dateList[1].year, months={{mon=dateList[1].mon, s=dateList[1].d}}}}
	local months = years[1].months
	for i=2,lastRow do -- deduplicate years and months
		if dateList[i].mon ~= months[#months].mon then -- new month
			months[#months].e = dateList[i-1].d -- store end of previous month
			if dateList[i].year ~= years[#years].year then -- new year
				years[#years+1] = {year=dateList[i].year, months={}}
				months = years[#years].months -- switch months list
			end
			months[#months+1] = {mon=dateList[i].mon, s=dateList[i].d} -- store start of this month
		end
	end
	months[#months].e = dateList[lastRow].d -- store end of final month
	for i=1,#rows do -- build rows
		rows[i].divisor = tonumber(args.divisor) and tonumber(args.divisor) or maxparam / (0.95 * args.barwidth)
		rows[i].numwidth = args.numwidth
		rows[i].collapsible = args.collapsible
		rows[i].multiYear = #years > 1
		rows[i].rowsToEnd = #rows - i
		rows[i].rowheight = args.rowheight
		rows[i].duration = args.duration
		rows[i].nooverlap = nooverlap

		bars[i] = p._row(rows[i])
	end

	return table.concat(bars), years
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, duration, nooverlap)
	local lmon, label = mw.ustring.lower(month.mon), month.mon
	local id = (year and year .. '-' or '') .. lmon
	local customtoggles = ' mw-customtoggle-' .. id

	if nooverlap and month.l 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 .. '-l' .. duration or '')
	end

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

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

	if #years > 1 then
		local year = years[#years].year
		repeat
			customtoggles[#customtoggles+1] = ' mw-customtoggle-' .. year .. '-' .. mw.ustring.lower(months[i].mon) .. '-' .. id
			if i == 1 then
				year = years[#years-1].year
				months = years[#years-1].months
				i = #months
			else
				i = i - 1
			end
		until not months[i].l
	else
		repeat
			customtoggles[#customtoggles+1] = ' mw-customtoggle-' .. mw.ustring.lower(months[i].mon) .. '-' .. id
			i = i - 1
		until not months[i].l
	end

	return p._toggleButton(true, table.concat(customtoggles), id, 'Last&nbsp;' .. duration .. '&nbsp;days')
end

function p._chart(args)
	local navbar = require('Module:Navbar')._navbar
	lang = mw.getContentLanguage()

	local barargs = {}

	local function _numwidth(p)
		local nw = mw.ustring.sub(args.numwidth or '',p,p)
		if nw == 'n' then
			return 0
		elseif nw == 't' then
			return 40
		elseif nw == 'm' then
			return 55
		elseif nw == 'w' then
			return 70
		elseif nw == 'x' then
			return 85
		elseif nw == 'd' then
			return 55
		end

		return 0
	end

	local numwidth = 120
	local right1 = numwidth - 8 -- -8 because of padding
	if args.numwidth then
		numwidth = _numwidth(1) + 10 + _numwidth(2)
		if mw.ustring.len(args.numwidth) == 4 then
			numwidth = numwidth + _numwidth(3) + _numwidth(4)
			if mw.ustring.sub(args.numwidth,3,3) == 'n' then
				numwidth = numwidth + 6
			else
				numwidth = numwidth + 10
			end
		end

		right1 = _numwidth(1) + 2 + _numwidth(2)
		if not args.right2 and mw.ustring.len(args.numwidth) == 4 then
			right1 = right1 + _numwidth(3) + _numwidth(4)
			if mw.ustring.sub(args.numwidth,3,3) == 'n' then
				numwidth = numwidth + 6
			else
				numwidth = numwidth + 10
			end
		end
	end

	local barwidth = 280

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

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

	barargs.float = args.float and args.float or 'right'
	local location = mw.ustring.gsub(args.location, 'the ', '')
	location = mw.ustring.upper(mw.ustring.sub(location,1,1)) .. mw.ustring.sub(location,2)

	local navbartitle = args.outbreak .. ' data/' ..
		(args.location3 and args.location3 .. '/' or '') ..
		(args.location2 and args.location2 .. '/' or '') ..
		location .. ' medical cases chart'

	-- get duration for toggles
	local duration = 15 -- default if manual togglesbar is last 15 days
	if yesno(args.collapsible) == true and ( not is(args.togglesbar) ) then
		duration = tonumber(args.duration) or 15 -- default if auto togglesbar is last 15 days
	end

	local togglesbar = ''
	if args.rows then
		barargs.bars = args.rows
		if yesno(args.collapsible) == true then
			togglesbar = is(args.togglesbar) and args.togglesbar or ''
		end
	elseif is(args.data) or is(args.datapage) then
		local buildargs = {}

		local nooverlap = yesno(args.nooverlap)

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

		local years
		barargs.bars, years = p._buildBars(buildargs)
		local months = years[#years].months

		if yesno(args.collapsible) == true then
			if is(args.togglesbar) then
				togglesbar = args.togglesbar
			else -- automatically generate toggles
				years[#years].l = true -- to activate toggle and months bar
				if nooverlap then
					if months[#months].e ~= lang:formatDate('t', years[#years].year .. '-' .. months[#months].mon) then
						months[#months].l = true -- so that the days interval is added to partial months
					end
				else
					local i, ndaysleft = #months, duration
					repeat
						months[i].l = true -- so that both ...-mon and ...-mon-lX classes are created
						ndaysleft = ndaysleft - (months[i].e - months[i].s + 1)
						if i == 1 and #years > 1 then
							months = years[#years-1].months
							i = #months
						else
							i = i - 1
						end
					until ndaysleft <= 0
				end

				local monthToggles, divs = {}, nil
				if #years > 1 then
					local yearToggles, monthsDivs = {}, {}
					for i=1,#years do
						yearToggles[#yearToggles+1] = p._yearToggleButton(years[i])
						monthToggles = {}
						months = years[i].months
						for j=1,#months do
							monthToggles[#monthToggles+1] =
								p._monthToggleButton(years[i].year, months[j], duration, nooverlap)
						end
						monthsDivs[#monthsDivs+1] =
							'<div class="mw-collapsible' .. (years[i].l and '' or ' mw-collapsed') ..
							'" id="mw-customcollapsible-' .. years[i].year .. '">' .. table.concat(monthToggles) .. '</div>'
					end
					divs = '<div>' .. table.concat(yearToggles) .. '</div>' .. table.concat(monthsDivs)
				else
					for i=1,#months do
						monthToggles[#monthToggles+1] = p._monthToggleButton(nil, months[i], duration, nooverlap)
					end
					divs = '<div>' .. table.concat(monthToggles) .. '</div>'
				end
				divs = divs .. '<div>' .. p._lastXToggleButton(years, duration) .. '</div>'
				togglesbar = '<div class="nomobile" style="text-align:center">' .. divs .. '</div>'
			end
		end
	end

	local title = {}
	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 '') .. '<span class="nowrap">&nbsp;&nbsp;</span>(' ..
		navbar({[1] = navbartitle, titleArg = ':' .. mw.getCurrentFrame():getParent():getTitle(), mini = 1, nodiv = 1}) ..
		')<br />'
	title[2] = p._legend0({[1] = p._barColors(1), [2] = 'Deaths'})
	if yesno(args.recoveries) == false then
		title[3] = ''
	else
		title[3] = '<span class="nowrap">&nbsp;&nbsp;&nbsp;</span>' .. p._legend0({[1] = p._barColors(2), [2] = args.reclbl or i18n.recoveries})
	end
	title[4] = '<span class="nowrap">&nbsp;&nbsp;&nbsp;</span>' .. p._legend0({[1] = p._barColors(3), [2] = args.altlbl1 or i18n.activeCases})
	if args.altlbl2 then
		title[5] = '<span class="nowrap">&nbsp;&nbsp;&nbsp;</span>' .. p._legend0({[1] = p._barColors(4), [2] = args.altlbl2})
	else
		title[5] = ''
	end
	if args.altlbl3 then
		title[6] = '<span class="nowrap">&nbsp;&nbsp;&nbsp;</span>' .. p._legend0({[1] = p._barColors(5), [2] = args.altlbl3}) ..'\n'
	else
		title[6] = '\n'
	end
	title[7] = togglesbar


	barargs.title = table.concat(title)
	barargs.left1 =
		'<div class="center" style="width:77px">' .. -- 85-8 because of padding
			"'''" .. i18n.date .. "'''" ..
		'</div>'

	barargs.right1 =
		'<div class="center" style="width:' .. right1 .. 'px">' ..
			"'''" .. (args.right1 or i18n.noOfCases) .. "'''" ..
		'</div>'

	if args.right2 then
		local right2 = _numwidth(3) + _numwidth(4)
		if mw.ustring.sub(args.numwidth,3,3) == 'n' then
			right2 = right2 - 2
		else
			right2 = right2 + 2
		end

		barargs.right2 =
		'<div class="center" style="width:' ..right2 ..'px">' ..
			"'''" .. args.right2 .. "'''" ..
		'</div>'
	end

	barargs.caption = args.caption
	barargs.css = 'Template:Medical_cases_chart/styles.css'
	return barBox._box(barargs)
end

function p.chart(frame)
	local args = getArgs(frame, {
		valueFunc = function (key, value)
			if value then
				value = mw.text.trim(value)
				if ({['numwidth']=1,['barwidth']=1,['recoveries']=1,['changetype']=1})[mw.ustring.gsub(key,"%A","")] then
					value = mw.ustring.lower(value) --make numwidth, barwidth, recoveries, and changetype lower case
				end
				if is(value) then
					return value
				end
			end
			return nil
		end
	})
	return p._chart(args)
end

function p.barColors(frame)
	return p._barColors(tonumber(frame:getParent().args[1]))
end

function p.buildBars(frame)
	local bars = p._buildBars(frame.args)
	return bars
end

function p.monthToggleButton(frame)
	local args = {}
	args.month = {frame:getParent().args.month or frame:getParent().args[1] or 'l15'}
	args.active = frame:getParent().args.active or 'true'
	args.duration = frame:getParent().args.duration or 15
	args.nonewline = true
	return p._monthToggleButton(args)
end

return p