Module:Medical cases chart/sandbox2
This module depends on the following other modules: |
Description
[edit]This template should be used for all outbreak, epidemic and pandemic medical cases charts based on {{Bar box}} to maintain consistency. It displays horizontal bars for up to 5 different classifications of cases for each valid date or interval. It offers two columns to make numbers explicit and to show relative or absolute changes. It also uses a sophisticated toggling system to control the visualization of data rows across many months or years. It is designed to be flexible, but still standardizes some parts of the chart. This template should be transcluded in other templates, NOT in article pages. Suggest features in the talk page or code them if they're neither controversial nor incompatibility prone.
Usage
[edit]{{Medical cases chart |float = side of the page where the chart will be located (left|center|right|none) [optional, defaults to: right] |barwidth = width of the stacked bars area (thin|medium|wide|auto) [optional, defaults to: medium] |numwidth = max width of the numbers in the right columns (AA or AAAA)←(n|t|m|w|x|d) [suggested, defaults to: mm; see info below] |rowheight = height of each bar in multiples of the text height [optional, defaults to: 1.6] |pretitle = text at the beginning of the title [optional] |disease = name of the disease |location = location of the outbreak the chart is showing |location2 = broader location such as state/province or country [optional] |location3 = broadest location such as country [optional] |posttitle = text at the end of the title [optional] |outbreak = name of the main outbreak for the links of the {{navbar}} [see info below] |recoveries = whether to display recoveries in the legend (yesno) [optional, defaults to: yes] |reclbl = alternate label for the 2nd cases classification [optional, defaults to: Recoveries] |altlbl1 = alternate label for the 3rd cases classification (hide) [optional, defaults to: Active cases] |altlbl2 = alternate label for the 4th cases classification [optional] |altlbl3 = alternate label for the 5th cases classification [optional] |collapsible= whether rows are collapsible (forced no if days span <= duration) (yesno) [optional, defaults to: 'auto'] |duration = span of last days to initially display to control default chart height [optional, defaults to: 15] |nooverlap = whether to prevent the last month's toggle from overlapping, in days, [optional, defaults to: no; see info below] with the "Last XX days" toggle (yesno) |right1 = heading of the 1st data column [optional, defaults to: # of cases] |right1data = cases classification of the 1st column for auto filling (1-5|alttot1-2) [optional, defaults to: 3, if right1 is "# of cases"] |changetype1= calculate percent change (%), absolute change (#), [optional, defaults to: percent] or weekly incidence (w) (p|a|w) |right2 = heading of the 2nd data column [optional] |right2data = cases classification of the 2nd column for auto filling (1-5|alttot1-2) [optional, defaults to: 1, if right2 is "# of deaths"] |changetype2= calculate percent change (%), absolute change (#), onlypercent (o), [optional, defaults to: percent] or weekly incidence (w) (p|a|o|w) |changetype = applies to both 1st and 2nd change columns [optional] |population = population count, required for weekly incidence, otherwise no effect [optional, if changetype w is set without population, it will default to percent] |datapage = tabular data page from Commons to scrape cases data from [optional; see its talk section and module] |data = data lines for each valid date or interval [suggested; see Data's syntax] |footer = footer of the chart [suggested] }}
It may be desirable to make a specific outbreak chart have different widths depending on the page it is displayed. The barwidth
parameter can be used to achieve this. For example, |barwidth={{{barwidth|wide}}}
will display the chart wide in the template page itself, but will allow different widths when the outbreak template is transcluded with this parameter in article pages.
numwidth
is a sequence of the initials of none, thin, medium, wide, extra wide and default, and it determines the maximum width of each number in the data columns. Therefore, one should be chosen that minimizes the total width, but which doesn't make the numbers break/wrap on mobile view. Using 2 or 4 characters allocates one or two data columns, respectively. For example, |numwidth=mw
sets the right1 value column to medium and the right1 change column to wide. |numwidth=mwnt
sets the right1 value column to medium, the right1 change column to wide, the right2 value column to none, and the right2 change column to thin.
numwidth | Number | Percent change[a] | Absolute change[a] |
---|---|---|---|
t | 1−9,999 | none | ±1−±99 |
m | 10,000−99,999 | ±X.X% or ±1%−±99% | ±100−±999 |
w | 100,000−9,999,999 | ±0.XX% or +100%−±999% | ±1,000−±99,999 |
x | 10,000,000−999,999,999 | ±0.00XX% or ±1,000−±99,999% | ±100,000−±999,999 |
The {{navbar}} links are constructed like this: "outbreak
data/[
[
location3
/]
location2
/]
*location
medical cases chart". outbreak
may be more descriptive than disease
to avoid page name collisions. So, for example, COVID-19 charts have |outbreak=COVID-19 pandemic
. location
has "the " internally removed and is capitalized to become *location
.
By default, the "Last X days" button toggles days which intersect with those of the last months. This happens regardless of the state (activated or not) of the month buttons. In fact, due to limitations of custom collapsing, toggles are stateless (a workaround is used to simulate two 'states'). All in all, this can lead to situations where clicking one button causes unintuitive toggling like creating date gaps. nooverlap
works around this issue by forcing the buttons of the last months to not overlap with the "Last X days" button. The result is usually a partial last month button, "Mon XX-XX", where XX are the first and last days of the month that the button affects. Extra info in the main bug discussion.
To display absolute change in the first column and percent change in the second column, set the following (AA represents any two allowed characters, # represents the classification number being shown, which should be the same for right1data
and right2data
):
|numwidth=AAnw |right1data=# |changetype1=a |right2data=# |changetype2=o
Data
[edit]The data
parameter should be populated by a sequence of lines containing a different set of parameters separated by semicolons, ;
.
|data=
- date
- deaths
- 2nd classification (recoveries)
- 3rd classification (total or altlbl1)
- 4th classification #
- 5th classification #
- 1st column #
- 1st column change
- 2nd column #
- 2nd column change
- other parameters=values
- date
- deaths
- 2nd classification (recoveries)
- 3rd classification (total or altlbl1)
- 4th classification #
- 5th classification #
- 1st column #
- 1st column change
- 2nd column #
- 2nd column change
- other parameters=values
All values are optional, and empty values can be represented by sequential semicolons (e.g. ;;
). Omitting the date will treat the row as a date interval, unless the date can be automatically interpolated (for 1 day intervals). This is mostly done when no new cases are reported. Omitting a classification value makes a 0 width stacked bar and omitting all values or the line itself is recommended to depict dates where no data is reported.
The expression for total in the 3rd cases classification has deaths and recoveries automatically subtracted from it. If a manual calculation of the number in that classification (generally active cases) is wanted, use alttot1
. The same applies to the expression for total in the 5th cases classification and alttot2
.
The 1st column #
, 1st column change
, 2nd column #
, and 2nd column change
values can be automatically calculated if omitted. The 1st column values will be automatically calculated if right1
is omitted or |right1=# of cases
, and the 2nd column values will be automatically calculated if |right2=# of deaths
. If right1
or right2
are set to other values, the columns can still be automatically calculated by setting right1data
and right2data
to the classification number wanted for display in columns 1 and 2, respectively (e.g. 1
for deaths, 2
for recoveries, 3
for total, etc.). The changes in the first and second columns are automatically wrapped in parentheses.
The other parameters=values
can be any number of the parameters below and their values, separated by semicolons. See the examples.
alttot1 = alternate expression for active cases (3rd cases classification) alttot2 = alternate expression for number in the 5th cases classification firstright1= whether a change in the first column is not applicable (n.a.) (yesno) firstright2= whether a change in the second column is not applicable (n.a.) (yesno) enddate = end date of interval if automatic one causes incorrect toggling behavior [required if chart ends with interval] note0 = note text post-fixed to the date, that should create a minimum space consuming object, like a note link, to avoid wrapping note1 = like note0, only post-fixed to column 1 note2 = like note1, only post-fixed to column 2
Examples
[edit]{{#invoke:Medical cases chart|chart
|float=center
|numwidth=mw
|disease=Green Flu
|location=Savannah|location2=Georgia|location3=United States
|outbreak=2009 Green Flu outbreak
|recoveries=n
|data=
2009-04-13;;;42;;;42;firstright1=y
2009-04-14;;;356;;;356;+748%
2009-04-15;;;1503;;;1,503;+322%
2009-04-16;57;;5915;;;5,915;+294%
2009-04-17;2000;;9500;;;~9,500;+60.6%
}}
{{#invoke:Medical cases chart|chart
|barwidth=wide
|numwidth=mwwd
|rowheight=1.8
|pretitle=Approximate
|disease=Spanish Flu
|location=the World
|posttitle=(excluding Oceania)
|outbreak=1918-20 Spanish Flu pandemic
|altlbl1=Active confirmed
|altlbl2=Suspected
|altlbl3=Estimated
|collapsible=n
|right1=Confirmed cases
|right2=Including suspected and estimated cases
|data=
1918-03-10;2060-300;3000-800;6000;;;6000;firstright1=y
1918-07;12600;20000;40000;12000;;alttot2=(34000-15000-8700+40);40000;+500%;10500;firstright2=y
;12600;20000;40000;12000;;alttot2=(34000-15000-8700+40);40000;;10500
1919;100000;250000;;;1000000;;;1mi;+500k
}}
{{#invoke:Medical cases chart|chart
|numwidth=tttt
|disease=Ebola
|location=Guinea-Bissau
|outbreak=2014 EVD epidemic
|altlbl1=hide
|altlbl2=Moderate cases
|altlbl3=Severe cases (hospitalized)
|duration=5
|nooverlap=y
|right1=# of severe cases
|right1data=alttot2
|right2=# of deaths
|changetype=a
|data=
2014-01-01;;;;;;alttot2=1;;+1
2014-01-15;1;;;;;alttot2=1;;=;;+1
<!-- no data reported -->
2014-01-20;2;;;1;;alttot2=1;;=;;+1
<!-- no data reported -->note0={{efn|On Jan 21st, there was a national blackout that forced the data to be reported on the next day.}}
2014-01-22;2;;;2;;alttot2=2;;+1;;=
;2;;;2;;alttot2=2
;2;1;;1;;alttot2=2
2014-02-05;3;1;;1;;alttot2=1
;3;2;;;;alttot2=1;enddate=2014-03-01
;3;2;;1;;alttot2=1
2014-03-30;3;4;;;;alttot2=0;note1={{efn|On Mar 30th, new cases were reported just before the press conference, thus they weren't included in the official count.}}
2014-03-31;3;4;;2;;alttot2=1;;+1
2014-04-01;3;5;;4;;alttot2=2
2014-04-02;5;6;;5;;alttot2=3;note2={{efn|The death of a foreigner at a border crossing medical tent is under dispute if it should be included in Guinea-Bissau's count.}}
;5;8;;3;;alttot2=3;enddate=2014-04-04
|footer={{notelist}}
}}
Applied example
[edit]{{COVID-19 pandemic data/Mainland China medical cases chart}}
{{#invoke:Medical cases chart|chart
|numwidth=mmmw
|disease=COVID-19
|location=Mainland China
|outbreak=COVID-19 pandemic
|altlbl1=Tested
|altlbl2=Clinically diagnosed (C.D.)
|altlbl3=Tested or C.D.
|right1=Number of cases<br />(excluding C.D.)
|right2=Number of cases<br />(including C.D.)
|data=
...
|footer=
<div class="center" style="width:90%; margin-left:auto; margin-right:auto;"><small>...</small></div>
...
}}
TemplateData
[edit]Produces charts based on {{Bar box}} for outbreak, epidemic and pandemic medical cases.
Parameter | Description | Type | Status | |
---|---|---|---|---|
Float | float | side of the page where the chart will be located (left|center|right|none)
| String | optional |
Bar width | barwidth | width of the stacked bars area (thin|medium|wide|auto)
| String | optional |
Number width | numwidth | maximum width of the numbers in the right columns (AA or AAAA)←(n|t|m|w|x|d)
| String | suggested |
Row height | rowheight | height of each bar in multiples of the text height
| Number | optional |
Pretitle | pretitle | text at the beginning of the title | String | optional |
Disease | disease | name of the disease
| String | required |
Location | location | location of the outbreak the chart is showing
| String | required |
Location 2 | location2 | broader location such as state/province or country
| String | optional |
Location 3 | location3 | broadest location such as country
| String | optional |
Post-title | posttitle | text at the end of the title
| String | optional |
Outbreak | outbreak | name of the main outbreak for the links of the {{navbar}}
| String | required |
Recoveries | recoveries | whether to display recoveries in the legend (yesno)
| Unknown | optional |
Recoveries label | reclbl | alternate label for the recoveries classification
| String | optional |
Alternate label 1 | altlbl1 | alternate label for the third cases classification (hide)
| String | optional |
Alternate label 2 | altlbl2 | alternate label for the fourth cases classification
| String | optional |
Alternate label 3 | altlbl3 | alternate label for the fifth cases classification
| String | optional |
Collapsible | collapsible | whether the rows are collapsible (forced "no" if the days span <= Duration) (yesno)
| Unknown | optional |
Duration | duration | span of last days to initially display to control the default chart height
| Number | optional |
No overlap | nooverlap | whether to prevent the last month's toggle from overlapping, in days, with the "Last XX days" toggle (yesno)
| Unknown | optional |
Right 1 | right1 | heading of the first data column
| String | optional |
Right 1 data | right1data | cases classification of the first column for auto filling (1-5|alttot1-2)
| Unknown | optional |
Change type 1 | changetype1 | whether to calculate percent change (%) or absolute change (#) (p|a)
| String | optional |
Right 2 | right2 | heading of the second data column
| String | optional |
Right 2 data | right2data | cases classification of the second column for auto filling (1-5|alttot1-2)
| Unknown | optional |
Change type 2 | changetype2 | whether to calculate percent change (%), absolute change (#) or onlypercent (p|a|o)
| String | optional |
Change type | changetype | applies to both first and second change columns
| String | optional |
Data page | datapage | tabular data page from Commons to scrape cases data from
| Unknown | optional |
Data | data | data lines for each valid date or interval (see Data's syntax)
| Content | suggested |
Caption | caption | caption under the chart
| Content | suggested |
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._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 .. ' ' .. 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]
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
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') .. '">' ..
' ' .. '</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 = 'Module:Medical cases chart/styles.css'
barargs.float = args.float or 'right'
args.barwidth = args.barwidth or 'medium'
local barwidth
if args.barwidth == 'thin' then
barwidth = 120
elseif args.barwidth == 'medium' then
barwidth = 280
elseif args.barwidth == 'wide' then
barwidth = 400
elseif args.barwidth == 'auto' then
barwidth = 'auto'
else
error('unrecognized barwidth')
end
local function _numwidth(i)
local nw = args.numwidth:sub(i,i)
return assert(numwidth[nw], 'unrecognized numwidth[' .. i .. ']')
end
args.numwidth = args.numwidth or 'mm'
if args.numwidth:sub(1,1) == 'n' or args.numwidth:sub(2,2) == 'n' or args.numwidth:sub(4,4) == 'n' then
error('"n" is only allowed in numwidth[3]')
end
local buffer = 0.3 --until automatic numwidth determination
local right1width, right2width = _numwidth(1) + 0.3 + _numwidth(2) + buffer, 0
if #args.numwidth == 4 then
right2width = _numwidth(3) + _numwidth(4) + buffer
if args.numwidth:sub(3,3) ~= 'n' then
right2width = right2width + 0.3
end
if args.right2 then
right2width = math.ceil(right2width / 0.88 * 100) / 100 -- from td scale to th
else
right1width = right1width + 0.8 + right2width
right2width = 0
end
end
right1width = math.ceil(right1width / 0.88 * 100) / 100
if tonumber(barwidth) then
-- transform colswidth from th to td scale, add it with border-spacing, and finally transform to table scale
local relwidth = math.ceil(((7.08 + right1width + right2width) * 0.88 + 0.8 * (args.right2 and 5 or 4)) * 88) / 100
barargs.width = 'calc(' .. relwidth .. 'em + ' .. barwidth .. 'px)' --why do the bar borders go inward (no +2)?
barargs.barwidth = barwidth .. 'px'
else
barargs.width = 'auto'
barargs.barwidth = 'auto'
end
barargs.lineheight = args.rowheight
local title = {}
local function spaces(n)
local nbsp = ' '
return '<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