Mô đun:Medical cases chart

local yesno = require('Module:Yesno')
local barBox = require('Module:Bar box')
local lang = mw.getLanguage("en")
local i18n = require('Module:Medical cases chart/i18n')"vi"
local monthAbbrs = {}
for i = 1, 12 do
	monthAbbrsi = lang:formatDate('M', '2020-' .. ('%02d'):format(i))
end
local function is(v)
	return (v or ) ~= 
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 =
	 .. p._monthStr(label) ..  ..
	 .. p._monthStr(label)  .. 
	return outString
end
function p._monthStr (str)
	if str == "Jan" then return "Thg 1"
	elseif str == "Feb" then return "Thg 2"
	elseif str == "Mar" then return "Thg 3"
	elseif str == "Apr" then return "Thg 4"
	elseif str == "May" then return "Thg 5"
	elseif str == "Jun" then return "Thg 6"
	elseif str == "Jul" then return "Thg 7"
	elseif str == "Aug" then return "Thg 8"
	elseif str == "Sep" then return "Thg 9"
	elseif str == "Oct" then return "Thg 10"
	elseif str == "Nov" then return "Thg 11"
	elseif str == "Dec" then return "Thg 12"
	end
	return str
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 monthsi.l do
			customtoggles#customtoggles+1 = ' mw-customtoggle-' .. year .. lang:lc(monthsi.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 monthsi.l do
			customtoggles#customtoggles+1 = ' mw-customtoggle-' .. lang:lc(monthsi.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, duration .. ' ngày gần nhất')
end
function p._buildTogglesBar(dateList, duration, nooverlap)
	local years = 
	local months, combinationsL = years1.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 #dateListi == 0 then -- specific date
			addMonth(dateListi)
			months#months.l = months#months.l or dateListi.l -- so that both ...-mon and ...-mon-lX classes are created
		elseif #dateListi == 1 then -- interval within month
			addMonth(dateListi1)
			months#months.l = months#months.l or dateListi.l
		else -- multimonth interval
			for j, month in ipairs(dateListi) do
				addMonth(month)
				months#months.combinations#months#months.combinations+1 = dateListi.id
			end
			combinationsL#combinationsL+1 = dateListi.id:find('-l%d+$') and dateListi.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 (dateListi.mon or dateListi1.mon) ~= months#months.mon
		if i == 0 then -- start of first and final month
			months#months.s = tonumber(os.date('%d', dateList1.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',	--tử vong
	'mcc-r',	--hồi phục
	'mcc-c',	--số ca hoặc altlbl1
	'mcc-a2',	--altlbl2
	'mcc-a3'	--altlbl3
}
function p._customBarStacked(args)
	local barargs = {}
	barargs1 = args1
	-- (next update)
	-- local function _numwidth(i)
	-- 	return args.numwidth:sub(i,i)
	-- end
	local function _numwidth(nw)
		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
		return 3.5
	end
	if args7 or args8 then -- is it acceptable to have one and not the other?
		barargs2 =
			 .. (args7 or ) ..  ..
			 .. (args8 or ) .. 
	end
	if #args.numwidth == 4 then
		barargs.note2 = (args9 or args10) and (args.numwidth:sub(3,3) ~= 'n' and  .. (args9 or ) ..  or ) ..
			 .. (args10 or ) ..  or 
			
		local pad = args.numwidth:sub(3, 3) == 'n' and '0' or '0.3em'
	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
	barargs2 =
		 .. (args7 or ) ..  ..
		 .. (args8 or ) .. 
	
	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 =
			 .. (args9 or ) ..  ..
			 .. (args10 or ) .. 
	end
	for i=1,5 do
		barargs2*i + 1 = p._barColorsi
		barargs2*i + 2 = (tonumber(argsi+1) or 0)/(tonumber(args.divisor) or 1)
		barargs'title' .. i = argsi+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 = {}
	
	barargs1 = (args1 or '⋮') .. (args.note0 or )
	barargs2 = args2 or 0
	barargs3 = args3 or 0
	if args'alttot1' then
		barargs4 = args'alttot1'
	elseif args4 then
		barargs4 = (args4 or 0) - (barargs2 or 0) - (barargs3 or 0)
	else
		barargs4 = 0
	end
	barargs5 = args5 or 0
	if args'alttot2' then
		barargs6 = args'alttot2'
	elseif args6 then
		barargs6 = (args6 or 0) - (barargs2 or 0) - (barargs3 or 0)
	else
		barargs6 = 0
	end
	barargs7 = args7
	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(args1) and is(argsvaluecol) then
				change = '(' .. i18n'=' .. ')'
			else
				change = is(argschangecol) and '(' .. argschangecol .. ')' or 
			end
		end
		return change
	end
	barargs8 = changeArg(1,7,8)
	barargs9 = args9 or 
	barargs10 = 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 = true
		else
			barargs.collapsed = false
		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}
						prevRow1 = 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
					prevRow1 = os.date('%Y-%m-%d', prevRow.nDate)
				elseif prevRow.nAltStartDate and prevRow.nAltStartDate >= prevRow.nEndDate then
					error('Một dòng trong khoảng thời gian liên tiếp dài 1 ngày thiếu một ngày')
				end
			end
		else
			if barargs.enddate then
				bValid, barargs.nEndDate = pcall(getUnix, barargs.enddate)
				assert(bValid, 'giá trị enddate không hợp lệ "' .. 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
					barargs1 = 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('Một dòng trong khoảng thời gian liên tiếp dài 1 ngày thiếu một ngày')
				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 barargs4
				prevnum = prevRow.alttot1 or prevRow4
			elseif data == 'alttot2' then
				num = barargs.alttot2 or barargs6
				prevnum = prevRow.alttot2 or prevRow6
			elseif data then
				num = barargsdata+1
				prevnum = prevRowdata+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(rounding:format(percent)) -- round to two sigfigs
								percent = tonumber(mw.ustring.format(rounding, 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 = tonumber(args.divisor) and tonumber(args.divisor) or maxparam / (0.92 * args.barwidth)--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
-- func -> obj
p._barColors = {
	'#A50026', --tử vong
	'SkyBlue', --hồi phục
	'Tomato', --số ca hoặc altlbl1
	'Gold', --altlbl2
	'OrangeRed' --altlbl3
}
function p._legend0(args)
	return  ..
		 ..
			'    ' ..  ..
		' ' .. (args2 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
			argskey = value:lower()
		end
	end
	
	local barargs = {}
	
	barargs.css = 'Mô đun: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('barwidth không tồn tại')
	end
	local function _numwidth(i)
		local nw = args.numwidth:sub(i,i)
		return assert(numwidthnw, 'unrecognized numwidth' .. i .. )
	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
	
	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" chỉ được cho phép trong numwidth3')
	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
	local barwidth = 280
	if tonumber(barwidth) then
		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 = args.location
	location = mw.ustring.upper(mw.ustring.sub(location,1,1)) .. mw.ustring.sub(location,2)
	local navbartitle = args.outbreak .. '/' ..
		(args.location3 and args.location3 .. '/' or ) ..
		(args.location2 and args.location2 .. '/' or ) ..
		'Biểu đồ số ca nhiễm tại ' .. location
	
	local navbar = require('Module:Navbar')._navbar
	
	title1 = (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}) ..
		')'
	title2 = p._legend0({p._barColors1, i18n.noOfDeaths})
	args.recoveries = args.recoveries == nil and true or args.recoveries
	title3 = args.recoveries and spaces(3) .. p._legend0({p._barColors2, args.reclbl or i18n.recoveries}) or 
	title4 = args.altlbl1 ~= 'hide' and spaces(3) .. p._legend0({p._barColors3, args.altlbl1 or i18n.activeCases}) or 
	title5 = args.altlbl2 and spaces(3) .. p._legend0({p._barColors4, args.altlbl2}) or 
	title6 = args.altlbl3 and spaces(3) .. p._legend0({p._barColors5, 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
	
	buildargs.right1data = args.right1data or args.right1 == i18n.noOfCases and 3
	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
	
	title7 = togglesbar and  .. togglesbar or 
	barargs.title = table.concat(title)
	barargs.left1 =  .. i18n.date .. 
	barargs.right1 =  .. args.right1 .. 
	
	if args.right2 then
		barargs.right2 =  .. args.right2 .. 
	end
	barargs.caption = args.caption or args.footer
	local box = barBox._box(barargs)
	return tostring(box)
end
local getArgs = require('Module:Arguments').getArgs
function p.barColors(frame)
	local args = getArgs(frame)
	return p._barColorstonumber(args1)
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
-- deprecation warning
function p.monthToggleButton(frame)
	return "Bản mẫu này không còn được dùng nữa, bạn có thể xóa nó"
end
return p