Module:Medical cases chart/data
--- Example usage:
 --- {{#invoke:Medical cases chart/data|externalData|page=COVID-19 cases in Santa Clara County, California.tab|recoveries=hospitalized|cases=totalConfirmedCases}}
 --- =p._externalData({datapage=&quot;COVID-19 cases in Santa Clara County, California.tab&quot;,datarecoveries=&quot;hospitalized&quot;,datacases=&quot;totalConfirmedCases&quot;})
 
 local p = {}
 local lang = mw.getContentLanguage()
 local english = mw.getLanguage(&quot;en&quot;)
 
 local function round(x)
 	return (math.modf(x + (x &lt; 0 and -0.5 or 0.5)))
 end
 
 local function formatChange(previous, current)
 	if not previous or previous == 0 then
 		return
 	end
 	if previous == current then
 		return &quot;=&quot;
 	end
 	
 	local change = current / previous * 100 - 100
 	local sign = change &lt; 0 and &quot;−&quot; or &quot;+&quot;
 	return mw.ustring.format(&quot;%s%s%%&quot;, sign, lang:formatNum(round(math.abs(change))))
 end
 
 function p._externalData(args)
 	local data = mw.ext.data.get(args.datapage)
 	
 	local dateIndex
 	local deathsIndex
 	local recoveriesIndex
 	local casesIndex
 	local class4Index
 	local class5Index
 	for i, field in ipairs(data.schema.fields) do
 		if field.name == &quot;date&quot; or field.name == args.datadate then
 			dateIndex = i
 		elseif field.name == &quot;deaths&quot; or field.name == args.datadeaths then
 			deathsIndex = i
 		elseif field.name == &quot;recoveries&quot; or field.name == args.datarecoveries then
 			recoveriesIndex = i
 		elseif field.name == &quot;cases&quot; or field.name == args.datacases then
 			casesIndex = i
 		elseif field.name == &quot;class4&quot; or field.name == args.dataclass4 then
 			class4Index = i
 		elseif field.name == &quot;class5&quot; or field.name == args.dataclass5 then
 			class5Index = i
 		end
 	end
 	assert(dateIndex, &quot;Date field not found.&quot;)
 	assert(deathsIndex or not args.datadeaths, &quot;Deaths field not found.&quot;)
 	assert(recoveriesIndex or not args.datarecoveries, &quot;Recoveries field not found.&quot;)
 	assert(casesIndex or not args.datacases, &quot;Cases field not found.&quot;)
 	assert(class4Index or not args.dataclass4, &quot;Class 4 field not found.&quot;)
 	assert(class5Index or not args.dataclass5, &quot;Class 5 field not found.&quot;)
 	
 	-- Restructure the data as tables with keys.
 	local records = {}
 	for i, row in ipairs(data.data) do
 		local record = {
 			date = row[dateIndex],
 			deaths = deathsIndex and row[deathsIndex],
 			recoveries = recoveriesIndex and row[recoveriesIndex],
 			cases = casesIndex and row[casesIndex],
 			class4 = class4Index and row[class4Index],
 			class5 = class5Index and row[class5Index],
 			options = {},
 			streak = 1,
 		}
 		local prevRecord = records[#records] or {}
 		if casesIndex and not prevRecord.cases and record.cases and record.cases &gt; 0 then
 			record.options.firstright1 = &quot;y&quot;
 		end
 		if deathsIndex and prevRecord.deaths == 0 and record.deaths and record.deaths &gt; 0 then
 			record.options.firstright2 = &quot;y&quot;
 		end
 		if deathsIndex and (prevRecord.deaths or prevRecord.assumedDeaths) and not record.deaths then
 			record.assumedDeaths = prevRecord.deaths or prevRecord.assumedDeaths
 		end
 		if casesIndex and (prevRecord.cases or prevRecord.assumedCases) and not record.cases then
 			record.assumedCases = prevRecord.cases or prevRecord.assumedCases
 		end
 		if record.deaths == prevRecord.deaths
 			and record.recoveries == prevRecord.recoveries
 			and record.cases == prevRecord.cases
 			and record.class4 == prevRecord.class4
 			and record.class5 == prevRecord.class5 then
 			record.streak = prevRecord.streak + 1
 		end
 		table.insert(records, record)
 	end
 	
 	-- Collapse streaks of identical data.
 	for i = #records, 1, -1 do
 		local record = records[i]
 		if record.streak &gt; 3 then
 			for j = i, i - record.streak + 3, -1 do
 				table.remove(records, j)
 			end
 			i = i - record.streak + 2
 			record = records[i]
 			record.options.collapse = &quot;y&quot;
 			record.options.id = english:formatDate(&quot;M&quot;, record.date):lower()
 			record.date = nil
 		end
 	end
 	
 	-- Stringify the data.
 	local rows = {}
 	for i, record in ipairs(records) do
 		local prevRecord = records[i - 1] or {}
 		local row = {
 			record.date or &quot;&quot;,
 			tostring(record.deaths or record.assumedDeaths or &quot;&quot;),
 			tostring(record.recoveries or &quot;&quot;),
 			tostring(record.cases or record.assumedCases or &quot;&quot;),
 			tostring(record.class4 or &quot;&quot;),
 			tostring(record.class5 or &quot;&quot;),
 			record.cases and lang:formatNum(record.cases) or &quot;&quot;,
 			record.cases and formatChange(prevRecord.cases or prevRecord.assumedCases, record.cases) or &quot;&quot;,
 			record.deaths and lang:formatNum(record.deaths) or &quot;&quot;,
 			record.deaths and formatChange(prevRecord.deaths or prevRecord.assumedDeaths, record.deaths) or &quot;&quot;,
 		}
 		for k, v in pairs(record.options) do
 			table.insert(row, string.format(&quot;%s=%s&quot;, k, v))
 		end
 		table.insert(rows, table.concat(row, &quot;;&quot;))
 	end
 	return table.concat(rows, &quot;\n&quot;)
 end
 
 function p.externalData(frame)
 	local args = {}
 	for k,v in pairs(frame.args) do
 		if (v or '') ~= '' then
 			args['data'..k] = v
 		end
 	end
 	return p._externalData(args)
 end