Module:Infobox country

From Meta, a Wikimedia project coordination wiki
Module documentation
local infobox = require( 'Module:Infobox' )
local graph = require( 'Module:Graph' )

local baseColors = {
	'1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf'
}

local countryBlacklist = {
	'AZ','BH','BY','CN','CU','DJ','EG','GQ','ER','ET','IR','KZ','LA','LY','MM','KP','PK','RU','SA','SO','SD','SY','TH','TR','TM','AE','UZ','VE','VN','YE'
}

function filter( arr, fn )
	local r = {}
	for i, v in pairs( arr ) do
		if fn( v ) then
			table.insert( r, v )
		end
	end
	return r
end

function incrementer( start )
	local i = start
	return function ()
		i = i + 1
		return i
	end
end

function round( num )
	if num < 0.95 then
		return math.floor( num * 10 + 0.5 ) / 10
	else
		return math.floor( num + 0.5 )
	end
end

-- Note: This breaks once numbers start going up to the 1e+14.. range.
function formatNumber( num )
	local nString = tostring( num )
	local tDigits = #nString % 3
	
	if tDigits == 0 then
		tDigits = 3
	end
	
	return string.sub( nString, 0, tDigits ) ..
		string.gsub( string.sub( nString, tDigits + 1 ), "...", ",%1" )
end

function formatDate( y, m )
	return y .. '-' .. ( m > 9 and m or '0' .. m )
end

function getTimestamp( dateString )
	local dateParts = mw.text.split( dateString, '-' )
	return 1000 * os.time( {
		year = dateParts[ 1 ],
		month = tonumber( dateParts[ 2 ] ),
		day = 1,
		hour = 0
	} )
end

function runI18n( msg, ... )
	local r = msg
	for i, v in pairs( arg ) do
		r = string.gsub( r, '%$' .. i, v )
	end
	return r
end

function unfuzzyI18n( ... )
	-- Fuzzy translations are surrounded by span tags that need to be removed
	-- before using the msg as a title.
	local text = string.gsub( runI18n( ... ), '<span class="mw%-translate%-fuzzy">', '' )
	text = string.gsub( text, '</span>', '' )
	return text
end

function tryWikidataLink( item )
	local label = mw.wikibase.getLabel( item )
	if label then
		return "[[d:" .. item .. '|' .. label .. ']]'
	else
		return item
	end
end

function notEmpty( arg )
	return arg and arg ~= ''
end

-- TODO: Clean this up a bit.
function collateRows( data, group_by, initRow, addFn )
	table.sort( data, function ( a, b ) return a[ group_by ] > b[ group_by ] end )
	
	local result = {}
	local currentRow
	local currentKey
	for _, row in pairs( data ) do
		if row[ group_by ] ~= currentKey then
			currentRow = initRow( row )
			table.insert( result, currentRow )
			currentKey = row[ group_by ]
		end
		addFn( currentRow, row )
	end
	return result
end

function getForCountry( data, country, type )
	local cd = filter( data, function ( row ) return row[ 2 ] == country and row[ 3 ] == type end )
	return cd
end

function getForCountries( data, countries, type )
	local cd = filter( data, function ( row ) return row[ 3 ] == type and tableIncludes( countries, row[ 2 ] ) end )
	return cd
end

function tableIncludes( arr, toFind )
	if arr[ 2 ] then
		for _, value in pairs( arr ) do
			if value == toFind then
				return true
			end
		end
		return false
	else
		return arr[ 1 ] == toFind
	end
end

function getAssumedEditorCount( num )
	-- num is rounded up to the nearest ten in the dataset.
	-- I have no idea how to estimate its actual value, so these are randomish
	-- estimates
	if num == 10 then
		return 3
	elseif num < 50 then
		return num - 5
	else
		return num - 4.5
	end
end

function getMinEditorCount( num )
	-- num is rounded up to the nearest ten in the dataset.
	return num - 9
end

function getAssumedPageViewsCount( num )
	-- dunno
	return num
end

function sumObject( data, fn )
	local sum = 0
	for i, v in pairs( data ) do
		sum = sum + fn( v )
	end
	return sum
end

-- Unused?
function getTotalEditorCount( data )
	return sumObject( data, function ( v ) return getAssumedEditorCount( v[ 4 ] ) end )
end

function parse_dbname( dbname, useLang )
	
	local l = #dbname
	
	local sourceOfTranslations = 'Translations:Template:OurProjects/'
	
	-- These two are unused, but might be necessary is someone deletes the
	-- pile of translations at Template:OurProjects.
	--local specialProjectsNames = {
	--	-- Hm...
	--	wikidata = 'Wikidata',
	--	metawiki = 'Meta-Wiki',
	--	mediawikiwiki = 'Mediawiki',
	--	commons = 'Commons'
	--}
	--local baseProjectsNames = {
	--	wikibooks = 'Wikibooks',
	--	wiktionary = 'Wiktionary',
	--	wikivoyage = 'Wikivoyage',
	--	wikinews = 'Wikinews',
	--	wikiquote = 'Wikiquote',
	--	wikisource = 'Wikisource',
	--	wikiversity = 'Wikiversity',
	--	wiki = 'Wikipedia'
	--}
	
	local specialProjects = {
		wikidatawiki = '38',
		commonswiki = '36',
		specieswiki = '34'
	}
	local annoyingToTranslateProjects = {
		metawiki = 'Meta-Wiki',
		mediawikiwiki = 'Mediawiki',
	}
	local baseProjects = {
		wikibooks = '16',
		wiktionary = '8',
		wikivoyage = '32',
		wikinews = '12',
		wikiquote = '20',
		wikisource = '24',
		wikiversity = '28',
		wiki = '4'
	}
	local langCode, langName, projectNum, projectName
	if annoyingToTranslateProjects[ dbname ] then
		-- Just use the English name until a solution is found. These probably
		-- won't ever show up anyway.
		return nil, annoyingToTranslateProjects[ dbname ]
	elseif specialProjects[ dbname ] then
		-- These don't have set languages
		projectNum = specialProjects[ dbname ]
	else
		for i, v in pairs( baseProjects ) do
			local projectCode = string.match( dbname, i .. '$' )
			if projectCode then
				langCode = string.sub( dbname, 0, #dbname - #projectCode )
				projectNum = baseProjects[ projectCode ]
				break
			end
		end
	end
	
	
	if langCode then
		langName = mw.language.fetchLanguageName( string.gsub( langCode, '_', '-' ), useLang )
	end
	
	if projectNum then
		-- Translate project names.
		local tBase = sourceOfTranslations -- number / lang
		pcall( function ()
			projectName = mw.getCurrentFrame():expandTemplate{ title =
				tBase .. projectNum .. '/' .. useLang
			}
		end )
		if not projectName then
			-- Fallback to English
			projectName = mw.getCurrentFrame():expandTemplate{ title =
				tBase .. projectNum .. '/en'
			}
		end
		return langName, projectName
	else
		-- not found
	end
end

function lang_sum( data, project, type )
	-- Not sure this will be necessary. There are stats available on 100+ and total. Unsure if it's the same thing.
	local sum = 0
	for i, v in pairs( data ) do
		if v[ 1 ] == project and v[ 3 ] == type then
			sum = sum + getAssumedEditorCount( v[ 4 ] )
		end
	end
	return sum
end

function pageviews_project_sum( data, project )
	local sum = 0
	for i, v in pairs( data ) do
		if v[ 1 ] == project then
			sum = sum + getAssumedPageViewsCount( v[ 3 ] )
		end
	end
	return sum
end

function process_editor_data( data, cd, countrySum, countryName, editorType )
	
	local r = {}
	
	for i = 1, 10 do -- We only want the top-10 projects.
		local row = cd[ i ]
		
		if not row then
			break
		end
		
		local project = row[ 1 ]
		local count = row[ 2 ]
		local assumedCount = row[ 3 ]
		local minCount = row[ 4 ] -- For editors
		
		
		if count > 10 then
			table.insert( r, {
				project,
				count,
				
				assumedCount / lang_sum( data, project, editorType ),
				assumedCount / countrySum,
				
				assumedCount,
				minCount
			} )
		end
	end
	
	return r
end

-- 'enwiki' -> 'English Wikipedia'
function getNameFromDb( project, isPageViews, useLang, i18n )
	local r = ''
	local langName, projectName = parse_dbname( project, useLang )
	if langName and isPageViews then
		--r = langName .. ' ' .. projectName
		r = runI18n( i18n.language_project, langName, projectName )
	elseif isPageViews then
		r = projectName
	else
		-- Editor counts are limited to Wikipedias. Don't list project name.
		r = langName
	end
	
	return r
end

-- TODO: Merge with getStatsTable. Need to share vars. 
function getTextFromCount( count, ofProject, ofCountry, isPageViews, i18n, si18n, project_name, assumedCount, minCount )
	local text = ''
	if isPageViews then
		-- Count is in thousands at the start
		local m = { { 'B', 1000000 }, { 'M', 1000 }, { 'K', 1 } }
		for i, v in pairs( m ) do
			local letter, mag = v[ 1 ], v[ 2 ]
			if count > mag * 1 then
				
				text = '<span title="' .. 
					unfuzzyI18n( i18n.viewsHovertext, formatNumber( count * 1000 ) ) ..
					'">~' .. ( round( count / mag * 100 ) / 100 ) .. letter .. '</span>'
				-- round
				break
			end
		end
		--text = '~' .. count * 1000
	else
		
		local displayCount = math.floor( assumedCount / 10 + 0.5 ) * 10 -- round to nearest ten
		text = '<span title="' .. unfuzzyI18n( i18n.editorsHovertext, minCount, count ) .. '">~' .. displayCount .. '</span>'
	end
	
	-- Figure out some way to describe the stats more fully.
	local ofCountryText = round( ofCountry * 100 ) .. '%%'
	local ofProjectText = round( ofProject * 100 ) .. '%%'
	text = text ..
		' (' ..
			'<span title="' .. 
				unfuzzyI18n( si18n.ofCountryH, ofCountryText, project_name ) .. 
			'">' ..
				runI18n( si18n.ofCountry, ofCountryText ) .. '</span>, ' ..
			'<span title="' .. 
				unfuzzyI18n( si18n.ofProjectH, ofProjectText, project_name ) ..
			'">' ..
				runI18n( i18n.ofProject, ofProjectText ) .. '</span>' ..
		')'
	
	return text
end

function colorDot( i )
	return '<span style="display:inline-block; height: 6px; width:6px; border-radius:5px; background: ' ..
		'#' .. baseColors[ i ] ..
		';"></span>'
end

function getStatsTable( data, isPageViews, useLang, i18n, si18n )
	local r = {}
	
	for i = 1, 10 do
		local dataRow = data[ i ]
		if not dataRow then
			break
		end
		
		local project, count, ofProject, ofCountry = unpack( dataRow )
		
		local row = {}
		
		local project_name = getNameFromDb( project, isPageViews, useLang, i18n )
		
		row[ 1 ] = (
				-- Don't show dots for pageviews, for now.
				-- No history, so no graph, so no need to color-code.
				isPageViews and '' or
				colorDot( i ) .. ' '
			) ..
			project_name
		row[ 2 ] = getTextFromCount( count, ofProject, ofCountry, isPageViews, i18n, si18n, project_name, dataRow[ 5 ], dataRow[ 6 ] )
		
		table.insert( r, row )
	end
	return r
end

function getEditorData( data, countryCode, editorType )
	-- Fields: dbname, country, type, count
	local DB_NAME, COUNTRY, EDITORTYPE, COUNT = 1, 2, 3, 4 -- columns in the dataset
	
	local cd = getForCountries( data, countryCode, editorType )
	
	-- Sum of all editors in region
	local countrySum = sumObject( cd, function ( v ) return getAssumedEditorCount( v[ COUNT ] ) end )
	
	-- Create a new table, summing up the countries' totals.
	-- New table format: [ DB_NAME, count, assumedCount ]
	local cd2 = collateRows( cd, DB_NAME, function ( row )
		return { row[ DB_NAME ], 0, 0, 0 }
	end, function ( row, toAdd )
		row[ 2 ] = row[ 2 ] + toAdd[ COUNT ]
		row[ 3 ] = row[ 3 ] + getAssumedEditorCount( toAdd[ COUNT ] )
		row[ 4 ] = row[ 4 ] + getMinEditorCount( toAdd[ COUNT ] )
	end )
	
	-- Sort by count
	table.sort( cd2, function ( a, b ) return a[ 2 ] > b[ 2 ] end )
	
	return process_editor_data( data, cd2, countrySum, countryCode, editorType )
end

function getPageViewData( countryCode )
	
	local DB_NAME, COUNTRY, COUNT = 1, 2, 3 -- columns in the dataset
	local data = findMostRecentMonthWithData( 'Pageviews-by-country-monthly-$1.tab' ).data
	local dataForCountry = filter( data, function ( row ) return tableIncludes( countryCode, row[ COUNTRY ] ) end )
	local dataForCountry2 = collateRows( dataForCountry, DB_NAME, function ( row )
		return { row[ DB_NAME ], 0, 0 }
	end, function ( row, toAdd )
		row[ 2 ] = row[ 2 ] + toAdd[ COUNT ]
		row[ 3 ] = row[ 3 ] + getAssumedPageViewsCount( toAdd[ COUNT ] )
	end )
	table.sort( dataForCountry2, function ( a, b ) return a[ COUNT ] > b[ COUNT ] end )
	local r = {}
	for i = 1, 10 do
		local row = dataForCountry2[ i ]
		if row then
			local project = row[ DB_NAME ]
			local count = row[ 2 ]
			if count > 1 then
				local assumedCount = row[ 3 ]
				local countrySum = sumObject( dataForCountry, function ( cRow )
					return cRow[ 3 ]
				end )
				table.insert( r, {
					project,
					count,
					assumedCount / pageviews_project_sum( data, project ),
					assumedCount / countrySum
				} )
			end
		end
	end
	
	return r
end

-- For a set of monthly data files on Commons with a standard naming including a
-- year-month string, find the most recent one that's been uploaded.
-- Returns data, date string, year, month
function findMostRecentMonthWithData( fileNameFormat )
	
	local currentDate = os.date( '*t' )
	local year, month = currentDate.year, currentDate.month
	
	local fixIndex = function()
		if month == 0 then
			-- months are 1-indexed, roll over
			year = year - 1
			month = 12
		end
	end
	
	local dateString
	local currentData
	
	repeat
		month = month - 1
		fixIndex()
		dateString = formatDate( year, month )
		local fileName = string.gsub( fileNameFormat, '%$1', dateString )
		currentData = mw.ext.data.get( fileName )
	until currentData
	
	return currentData, dateString, year, month
end

function getRawEditorData()
	local gapSizeInMonths = 1 -- 1 means show every month, 2 is every other month, etc.
	local numberOfYearsToDisplay = 2 -- Note that geoeditors data starts in 2018.
	
	-- Get all the files
	
	local dates = {}
	local files = {}
	
	local currentData, currentDateString, currentYear, currentMonth =
		findMostRecentMonthWithData( 'Geoeditors-monthly-$1.tab' )
	
	-- Find the last two years of data
	local year, month = currentYear - numberOfYearsToDisplay, currentMonth
	
	while formatDate( year, month ) ~= currentDateString do
		local dateString = formatDate( year, month )
		table.insert( dates, dateString )
		local file = mw.ext.data.get( 'Geoeditors-monthly-' .. dateString .. '.tab' )
		table.insert( files, file and file.data )
		
		month = month + gapSizeInMonths
		if month > 12 then
			month = month - 12
			year = year + 1
		end
	end 
	
	table.insert( files, currentData.data )
	table.insert( dates, currentDateString )
	
	return files, dates
	
end

function findEditorCountPoint( data, project, countries, editorType )
	local length = #data
	local sum = 0
	for i = 1, length do
		local row = data[ i ]
		if row[ 1 ] == project and row[ 3 ] == editorType and tableIncludes( countries, row[ 2 ] ) then
			sum = sum + row[ 4 ]
		end
	end
	return sum
end

-- For populating the graphs
-- For each of the projects given, generate a list of editor counts in past dates
function getEditorHistory( listOfLanguages, pastData, country, editorType )
	local r = {}
	local numberOfSnapshots = #pastData
	
	for ii = 1, #listOfLanguages do
		local project = listOfLanguages[ ii ][ 1 ]
		
		local h = {}
		table.insert( r, h )
		for i = 1, numberOfSnapshots do
			local snapshot = pastData[ i ]
			if snapshot then
				local count = findEditorCountPoint( snapshot, project, country, editorType )
				table.insert( h, count )
			else
				-- Not available
				table.insert( h, '' )
			end
		end
	end
	return r
end

function build_graph( editorHistory, dates )
	
	local chartParams = {
		width = 230,
		height = 80,
		type = 'line',
		xType = 'date', yType = 'number',
		yGrid = true,
		xAxisMin = getTimestamp( dates[ 1 ] ),
		yAxisMin = 0,
		xAxisAngle = 45,
		-- No legend in-graph. Place the colors next to their listings.
		colors = '#' .. table.concat( baseColors, ',#' )
	}
	
	for i = 1, #editorHistory do
		chartParams[ 'y' .. i ] = table.concat( editorHistory[ i ], ',' )
	end
	
	chartParams.x = table.concat( dates, ',' )
	
	local chartJson = graph.chart( { args = chartParams } )

	-- Turn it into a Lua object, and then back to JSON, so that ticks
	-- can be overridden.
	local chartObject = mw.text.jsonDecode( chartJson )
	
	chartObject.axes[ 2 ].ticks = 3
	-- chartObject.axes[ 3 ].ticks = 3 -- If the dates also need to be limited...
	
	return mw.text.jsonEncode( chartObject )
end

return {
	infobox = function( frame )
		local TESTING = false
		
		local useLang = mw.getCurrentFrame():expandTemplate{ title = 'Template:uselang' }
		
		-- Note that we'll need to accept arguments as well...
		-- Also i18n
		local args = frame.args
		
		-- temp. replace with local i18n = {} later.
		local i18n = {
			showmore = '(Show more)',
			ofCountry = '~$1 of country',
			ofRegion = '~$1 of region',
			ofProject = '~$1 of project',
			viewsHovertext = '$1 views, rounded up to the nearest thousand',
			editorsHovertext = '$1 - $2 editors',
			editorsOfCountry = 'Roughly $1 of Wikipedia editors located in the country are $2 Wikipedia editors',
			editorsOfCountryR = 'Roughly $1 of Wikipedia editors located in the region are $2 Wikipedia editors',
			editorsOfProject = 'Roughly $1 of $2 Wikipedia editors are located in the country. (Project totals exclude editors from countries in the Country Protection List.)',
			editorsOfProjectR = 'Roughly $1 of $2 Wikipedia editors are located in the region. (Project totals exclude editors from countries in the Country Protection List.)',
			pageviewsOfCountry = 'Roughly $1 of pageviews from the country are of $2',
			pageviewsOfCountryR = 'Roughly $1 of pageviews from the region are of $2',
			pageviewsOfProject = 'Roughly $1 of $2 pageviews come from readers located in the country',
			pageviewsOfProjectR = 'Roughly $1 of $2 pageviews come from readers located in the region',
			
			organizations = "Wikimedia organizations",
			wikiproject = "Wikiproject",
			censored = "Projects censored",
			
			editors = 'Editors',
			readers = 'Readers',
			category = 'Category',
			
			byactiveeditors = 'Major Wikipedias by active editors',
			byveryactiveeditors = 'Major Wikipedias by very active editors',
			activeeditorssub = '5-99 edits per month',
			veryactiveeditorssub = '100+ edits per month',
			bymonthlypageviews = 'Major projects by monthly pageviews',
			
			excluding = 'Excluding editors from $1',
			
			language_project = '$1 $2'
		}
		
		-- Populate i18n
		for argName, argValue in pairs( args ) do
			local is18n = string.sub( argName, 1, 5 ) == 'i18n-'
			if is18n then
				i18n[ string.sub( argName, 6 ) ] = argValue
			end
		end
		
		local rawEditorData, dates = getRawEditorData()
		
		-- Args fed to the infobox.
		local iBArgs = {
			--above = args.commonTitle or frame:getTitle(),
			above = notEmpty( args.commonTitle ) and args.commonTitle or mw.getCurrentFrame():getTitle()
		}
		
		local countryCode = args.code
		
		if not countryCode then
			--error( 'Argument code= is unspecified' )
		end
		
		local countryCodes = mw.text.split( countryCode, ',' )
		local isSingleCountry = #countryCodes == 1
		
		if notEmpty( args.organizations ) then
			iBArgs.label1 = i18n.organizations
			iBArgs.data1 = args.organizations
		end
		
		if notEmpty( args.censorship ) then
			iBArgs.label2 = i18n.censored
			iBArgs.data2 = args.censorship
		end
		
		if notEmpty( args.wikiproject ) then
			iBArgs.label3 = i18n.wikiproject
			iBArgs.data3 = tryWikidataLink( args.wikiproject )
		end
		
		local editorSubbox = { child = 'yes', decat = 'yes' }
		local pageviewsSubbox = { child = 'yes', decat = 'yes' }
		
		local fillTable = function ( targetInfobox, tIncrementer, dataTable, key )
			-- Add the data from dataTable to targetInfobox
			for i = 1, #dataTable do
				local rowNumber = tIncrementer()
				targetInfobox[ 'label' .. rowNumber ] = dataTable[ i ][ 1 ]
				--targetInfobox[ 'data' .. rowNumber ] = dataTable[ i ][ 2 ] ..
				--	( i == 3 and #dataTable > 3 and
				--		'<br /><span class="mw-customtoggle-' .. key .. '">' .. i18n.showmore .. '</span>' or
				--		'' )
				targetInfobox[ 'data' .. rowNumber ] = dataTable[ i ][ 2 ]
				-- Collapsible rows
				if i > 3 then
					targetInfobox[ 'rowclass' .. rowNumber ] = 'mw-collapsible mw-collapsed'
					targetInfobox[ 'rowid' .. rowNumber ] = 'mw-customcollapsible-' .. key
				end
			end
			if #dataTable > 3 then
				targetInfobox[ 'data' .. tIncrementer() ] = 
					'<span class="mw-customtoggle-' .. key .. '" data-collapsetext="t2" data-expandtext="t3">' ..
						i18n.showmore .. '</span>'
			end
		end
		
		-- EDITOR STATS
		do
			-- Should there be one of those green/red arrows?
			-- Ideally this should eventually link to a wikistats page with more data, with this just being a summary
			
			
			-- Need to get the overall editor counts somehow. Otherwise things look ridiculous
			-- in projects dominated by countries on the hidden list.
			
			editorSubbox.title = i18n.editors
			
			if notEmpty( args.editorcat ) then
				editorSubbox.label1 = i18n.category
				editorSubbox.data1 = tryWikidataLink( args.editorcat )
			end
			
			local rowsCompleted = incrementer( 2 )
			
			local blacklisted = filter( countryCodes, function ( code )
				return tableIncludes( countryBlacklist, code )
			end )
			
			local excludingText = ''
			if #blacklisted > 0 and #blacklisted < #countryCodes then
				excludingText = '<br />' .. runI18n( i18n.excluding, mw.text.listToText( blacklisted ) )
			end
			
			for editorType = 1, 2 do
				local editorTypeCode = ( { '5..99', '100..' } )[ editorType ]
				
				local editorData = getEditorData( rawEditorData[ #rawEditorData ], countryCodes, editorTypeCode )
				local editorTable = getStatsTable( editorData, false, useLang, i18n, {
					ofCountryH = isSingleCountry and i18n.editorsOfCountry or i18n.editorsOfCountryR,
					ofProjectH = isSingleCountry and i18n.editorsOfProject or i18n.editorsOfProjectR,
					ofCountry =  isSingleCountry and i18n.ofCountry or i18n.ofRegion
				} )
				
				if #editorTable ~= 0 then
					editorSubbox[ 'data' .. rowsCompleted() ] = ( {
						i18n.byactiveeditors .. '<br />(' .. i18n.activeeditorssub .. ')',
						i18n.byveryactiveeditors .. '<br />(' .. i18n.veryactiveeditorssub .. ')'
					} )[ editorType ] .. excludingText
					
					fillTable( editorSubbox, rowsCompleted, editorTable, 'editorlist-' .. editorType )
					
					local editorHistory = getEditorHistory( editorData, rawEditorData, countryCodes, editorTypeCode )
					
					if not TESTING then
						editorSubbox[ 'data' .. rowsCompleted() ] = mw.getCurrentFrame():extensionTag( { name = 'graph', content = build_graph( editorHistory, dates ) } )
					else
						-- For testing
						editorSubbox[ 'data' .. rowsCompleted() ] = '<graph>' .. build_graph( editorHistory, dates ) .. '</graph>'
					end
				end
			end
		end
		
		-- PAGEVIEWS
		do
			pageviewsSubbox.title = i18n.readers
			
			local pageviewData = getPageViewData( countryCodes )
			local pageviewTable = getStatsTable( pageviewData, true, useLang, i18n, {
				ofCountryH = isSingleCountry and i18n.pageviewsOfCountry or i18n.pageviewsOfCountryR,
				ofProjectH = isSingleCountry and i18n.pageviewsOfProject or i18n.pageviewsOfProjectR,
				ofCountry =  isSingleCountry and i18n.ofCountry or i18n.ofRegion
			} )
			
			if pageviewTable[ 1 ] then
				pageviewsSubbox.data1 = i18n.bymonthlypageviews
				
				local rowsCompleted = incrementer( 2 )
				
				fillTable( pageviewsSubbox, rowsCompleted, pageviewTable, 'pageviewlist' )
			else
				-- Something to indicate that the table isn't available?
			end
		end
		
		-- TODO in the future when stats are available: Donor statistics.
		
		
		-- The infobox module stuffs everything into globals. The only way to
		-- avoid breaking everything is to reload the entire module via invoke.
		iBArgs.header4 = mw.getCurrentFrame():expandTemplate( { title = 'Infobox', args = editorSubbox } )
		iBArgs.header5 = mw.getCurrentFrame():expandTemplate( { title = 'Infobox', args = pageviewsSubbox } )
		
		--mw.log( os.clock() )
		
		return infobox.infobox( iBArgs )
		
		-- to test:
		--p.infobox({args={
		--	code='US',
		--	organizations = '[[Wikimedia Foundation]]<br />[[Wikimedia New York City]]<br />[[Wikimedia District of Columbia]]',
		--	censorship='None', wikiproject = 'Q10816993', editorcat = 'Q8244096'
		--}})
	end,
	test = function () -- Just for testing
		--local a = mw.ext.data.get( 'Geoeditors-monthly-2018-10.tab' ).data
		local a = mw.ext.data.get( 'Geoeditors-monthly-2020-02.tab' ).data
		local total = 0
		local us = 0
		for i = 1, #a do
			local r = a[ i ]
			if r[ 3 ] == '5..99' then
				local v = r[ 4 ] - 9
				if r[ 2 ] == 'US' then
					us = us + v
				end
				total = total + v
			end
		end
		--return total
		return us / total
	end
}