モジュール:Medical cases chart
表示
local yesno = require('Module:Yesno')
local barBox = require('Module:Bar box')
local lang = mw.getContentLanguage()
local language = lang:getCode()
local i18n = require('Module:Medical cases chart/i18n')[language]
assert(i18n, '次の言語に翻訳された図表はありません: ' .. mw.language.fetchLanguageName(language, 'ja'))
local monthAbbrs = {}
for i = 1, 12 do
monthAbbrs[i] = lang:formatDate('M', '2020-' .. ('%02d'):format(i))
end
local p = {}
function p._toggleButton(active, customtoggles, id, label)
local on = active and '' or ' mw-collapsed'
local off = active and ' mw-collapsed' or ''
local outString =
'<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 .. i18n.sp .. month.s .. i18n.d -- "M月X日"
if month.s ~= month.e then -- "M月X日–X日"
label = label .. '–' .. month.e .. i18n.d
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
p._barColors = {
'DimGrey', --deaths
'SkyBlue', --recoveries
'Tomato', --cases or altlbl1
'Gold', --altlbl2
'OrangeRed' --altlbl3
}
function p._customBarStacked(args)
local barargs = {}
barargs[1] = args[1]
local function _numwidth(i)
local nw = args.numwidth:sub(i, i)
if nw == 'n' then
return 0
elseif nw == 't' then
return 2.45
elseif nw == 'm' or nw == 'd' then
return 3.5
elseif nw == 'w' then
return 4.55
elseif nw == 'x' then
return 6.5
end
end
barargs[2] =
'<span class="cbs-ibr" style="padding:0 0.3em 0 0; width:' .. _numwidth(1) .. 'em">' .. (args[7] or '') .. '</span>' ..
'<span class="cbs-ibl" style="width:' .. _numwidth(2) .. 'em">' .. (args[8] or '') .. '</span>'
if #args.numwidth == 4 then
local pad = args.numwidth:sub(3, 3) == 'n' and '0' or '0.3em'
barargs.note2 =
'<span class="cbs-ibr" style="padding:0 ' .. pad .. ' 0 0; width:' .. _numwidth(3) .. 'em">' .. (args[9] or '') .. '</span>' ..
'<span class="cbs-ibl" style="width:' .. _numwidth(4) .. 'em">' .. (args[10] or '') .. '</span>'
end
for i = 1, 5 do
barargs[2*i + 1] = p._barColors[i]
barargs[2*i + 2] = args[i+1] / args.divisor
barargs['title' .. i] = args[i+1]
end
barargs.align = 'cdcc'
barargs.collapsed = args.collapsed
barargs.id = args.id
barargs.rowstyle = args.rowheight and 'line-height:'.. args.rowheight ..';'
return barBox._stacked(barargs)
end
function p._row(args)
local barargs = {}
barargs[1] = (args[1] or '⋮') .. (args.note0 or '')
barargs[2] = args[2] or 0
barargs[3] = args[3] or 0
if args['alttot1'] then
barargs[4] = args['alttot1']
elseif args[4] then
barargs[4] = (args[4] or 0) - (barargs[2] or 0) - (barargs[3] or 0)
else
barargs[4] = 0
end
barargs[5] = args[5] or 0
if args['alttot2'] then
barargs[6] = args['alttot2']
elseif args[6] then
barargs[6] = (args[6] or 0) - (barargs[2] or 0) - (barargs[3] or 0)
else
barargs[6] = 0
end
barargs[7] = args[7]
local function changeArg(firstright, valuecol, changecol)
local change = ''
if args['firstright' .. firstright] then
change = '(' .. i18n.na .. ')'
elseif not args[1] and args[valuecol] then
change = '(=)'
else
change = args[changecol] and '(' .. args[changecol] .. ')' or ''
end
change = change .. (args['note' .. firstright] or '')
return change
end
barargs[8] = changeArg(1, 7, 8)
barargs[9] = args[9]
barargs[10] = changeArg(2, 9, 10)
barargs.divisor = args.divisor
barargs.numwidth = args.numwidth
barargs.rowheight = args.rowheight
local dates
if args.collapsible then
local duration = args.duration
if args.daysToEnd >= duration then
barargs.collapsed = 'y'
else
barargs.collapsed = ''
end
if args.nooverlap and args.daysToEnd < 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 = ''
barargs.id = ''
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
local rows, prevRow = {}, {}
for line in mw.text.gsplit(args.data, '\n') do
local i, barargs = 1, {}
-- 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))
assert(parameter, 'データ引数2から6はフォーマットしないでください')
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, '不正な日付 "' .. 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, '不正な終了日 "' .. barargs.enddate .. '"')
end
if prevRow.nDate or prevRow.nEndDate then
barargs.nStartDate = (prevRow.nDate or prevRow.nEndDate) + updatePeriod
if barargs.nStartDate == barargs.nEndDate then
barargs.nDate = barargs.nEndDate
barargs[1] = os.date('%Y-%m-%d', barargs.nDate)
end
else
prevRow.nAltEndDate = (prevRow.nStartDate or prevRow.nAltStartDate) + updatePeriod
barargs.nAltStartDate = prevRow.nAltEndDate + updatePeriod
if barargs.nEndDate and barargs.nAltStartDate >= barargs.nEndDate then
error('a row in a consecutive intervals group is 1 day long and misses the date parameter')
end
end
end
local function fillCols(col, change)
local data = args['right' .. col .. 'data']
local changetype = args['changetype' .. col]
local value, num, prevnum
if data == 'alttot1' then
num = barargs.alttot1 or barargs[4]
prevnum = prevRow.alttot1 or prevRow[4]
elseif data == 'alttot2' then
num = barargs.alttot2 or barargs[6]
prevnum = prevRow.alttot2 or prevRow[6]
elseif data then
num = barargs[data+1]
prevnum = prevRow[data+1]
end
if data and num then -- nothing in column, source found, and data exists
value = changetype == 'o' and '' or lang:formatNum(num) -- set value to num if changetype isn't 'o'
if not change and not barargs['firstright' .. col] then
if prevnum and prevnum ~= 0 then -- data on previous row
if num - prevnum ~= 0 then --data has changed since previous row
change = num-prevnum
if changetype == 'a' then -- change type is "absolute"
if change > 0 then
change = '+' .. lang:formatNum(change)
end
else -- change type is "percent", "only percent" or undefined
local percent = 100 * change / prevnum -- calculate percent
local rounding = math.abs(percent) >= 10 and '%.0f' or math.abs(percent) >= 1 and '%.1f' or '%.2f'
percent = tonumber(rounding:format(percent)) -- round to two sigfigs
if percent > 0 then
change = '+' .. lang:formatNum(percent) .. '%'
elseif percent < 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
-- 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]) / (0.95 * args.barwidth)
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.rowheight = args.rowheight
row.collapsible = args.collapsible
row.duration = args.duration
row.nooverlap = args.nooverlap
row.daysToEnd = (lastDate - (row.nDate or row.nEndDate or row.nAltEndDate)) / 86400
row.multiyear = multiyear
bars[#bars+1], dateList[#dateList+1] = p._row(row)
end
return table.concat(bars), dateList
end
function p._legend0(args)
return
'<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') .. '">' ..
' ' .. '</span>' ..
' ' .. (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 = 'Template: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が認識できません')
end
local function _numwidth(i)
local nw = args.numwidth:sub(i, i)
if nw == 'n' then
return 0
elseif nw == 't' then
return 40
elseif nw == 'm' or nw == 'd' then
return 55
elseif nw == 'w' then
return 70
elseif nw == 'x' then
return 85
else
error('numwidthが認識できない値[' .. i .. ']です')
end
end
args.numwidth = args.numwidth or 'mm'
local numwidth = _numwidth(1) + 10 + _numwidth(2)
local right1width = numwidth
if #args.numwidth == 4 then
numwidth = numwidth + _numwidth(3) + _numwidth(4)
if args.numwidth:sub(3, 3) == 'n' then
numwidth = numwidth + 6
else
numwidth = numwidth + 10
end
if not args.right2 then
right1width = numwidth
end
end
right1width = right1width - 8 -- -8 because of padding
if tonumber(barwidth) then
barargs.width = 85 + barwidth + numwidth .. 'px'
barargs.barwidth = barwidth .. 'px'
else
barargs.width = 'auto'
barargs.barwidth = 'auto'
end
local title = {}
local function spaces(n)
local nbsp = ' '
return '<span class="nowrap">' .. nbsp:rep(n) .. '</span>'
end
local location = lang:ucfirst(mw.ustring.gsub(args.location, i18n.the_, ''))
local navbartitle = args.outbreak .. 'データ/症例数の推移/図表/' ..
(args.location3 and args.location3 .. '/' or '') ..
(args.location2 and args.location2 .. '/' or '') ..
location .. ''
local navbar = require('Module:Navbar')._navbar
title[1] = '<div style="display:flex; justify-content:center;">' .. (args.pretitle and args.pretitle .. '' or '') ..
(args.location3 and '' .. args.location3 or '') .. (args.postposition3 and args.postposition3 .. '' or '') ..
(args.location2 and '' .. args.location2 or '') .. (args.postposition2 and args.postposition2 .. '' or '') ..
args.location .. '' .. i18n.casesIn .. '' .. args.disease ..
(args.posttitle and 'の症例数' .. args.posttitle or 'の症例数') .. '<span class="nowrap"> </span>(<div style="font-size:75%;">' ..
navbar({[1] = navbartitle, titleArg = ':' .. mw.getCurrentFrame():getParent():getTitle(), mini = 1, nodiv = 1}) ..
'</div>)</div>'
title[2] = p._legend0({p._barColors[1], i18n.deaths})
args.recoveries = args.recoveries == nil and true or args.recoveries
title[3] = args.recoveries and spaces(3) .. p._legend0({p._barColors[2], args.reclbl or i18n.recoveries}) or ''
title[4] = args.altlbl1 ~= 'hide' and spaces(3) .. p._legend0({p._barColors[3], args.altlbl1 or i18n.activeCases}) or ''
title[5] = args.altlbl2 and spaces(3) .. p._legend0({p._barColors[4], args.altlbl2}) or ''
title[6] = args.altlbl3 and spaces(3) .. p._legend0({p._barColors[5], args.altlbl3}) or ''
local togglesbar, buildargs = nil, {}
args.right1 = args.right1 or i18n.noOfCases
args.duration = args.duration or 15
args.nooverlap = args.nooverlap or false
buildargs.barwidth = tonumber(barwidth) or 280
buildargs.numwidth = args.numwidth
buildargs.rowheight = args.rowheight
if args.datapage then
local externalData = require('Module:Medical cases chart/data')._externalData
buildargs.data = externalData(args)
else
buildargs.data = args.data
end
-- if no right1data and right1 title is cases, use 3rd classification
buildargs.right1data = args.right1data or args.right1 == i18n.noOfCases and 3
-- if no right2data and right2 title is deaths, use 1st classification
buildargs.right2data = args.right2data or (args.right2 == i18n.noOfDeaths or args.right2 == i18n.noOfDeaths2) and 1
buildargs.changetype1 = (args.changetype1 or args.changetype or ''):sub(1, 1) -- 1st letter
buildargs.changetype2 = (args.changetype2 or args.changetype or ''):sub(1, 1) -- 1st letter
buildargs.collapsible = args.collapsible
buildargs.duration = args.duration
buildargs.nooverlap = args.nooverlap
local dateList
barargs.bars, dateList = p._buildBars(buildargs)
if buildargs.collapsible then
togglesbar = p._buildTogglesBar(dateList, args.duration, args.nooverlap)
end
title[7] = togglesbar and '<br />' .. togglesbar or ''
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:' .. right1width .. 'px">' ..
"'''" .. args.right1 .. "'''" .. '</div>'
if args.right2 then
local right2width = numwidth - right1width - 16 -- -8-8...
barargs.right2 =
'<div class="center" style="width:' .. right2width ..'px">' ..
"'''" .. args.right2 .. "'''" .. '</div>'
end
barargs.caption = args.caption
return barBox._box(barargs)
end
function p.chart(frame)
local getArgs = require('Module:Arguments').getArgs
local args = getArgs(frame, {
valueFunc = function (key, value)
if value and value ~= '' then
key = key:gsub('%d', '')
if ({rowheight=1, duration=1, rightdata=1})[key] then -- if key in {...}
return tonumber(value) or value
end
if ({recoveries=1, collapsible=1, nooverlap=1})[key] then
return yesno(value)
end
return value
end
return nil
end
})
return p._chart(args)
end
return p