Module:Datastaven

Moduledocumentatie​[bekijk] [bewerk] [ververs] [geschiedenis]

Deze module kan gebruikt worden voor het in een staafdiagram weergeven van klasseringen / aantallen / rangen / posities / standen / eindstanden of iets in die trant. Of iets anders.

Deze module wordt toegepast in Sjabloon:Datastaven. Zie voor een overzicht van parameters de documentatie aldaar.

Gebruik bewerken

Standaard:

{{#invoke:Datastaven|main}}

Speciaal voor eindstanden kan ook gebruik worden gemaakt van:

{{#invoke:Datastaven|main|type=eindstanden}}

Kleuren bewerken

De kleuren van de staven kunnen zowel ingesteld worden op de groepen als op individuele staven. Als er geen kleur is opgegeven wordt er een uit het standaardkleurenpalet gehaald:

(Bewerk)

(Bewerk)

Zie ook bewerken

require('Module:No globals')

local p = {}
local getArgs = require('Module:Arguments').getArgs
local unpackItem = require('Module:Item').unpack
local _delink = require('Module:Delink')._delink
local _yesno = require('Module:Yesno')
local templatestyles = 'Module:Datastaven/styles.css'
local data = {}
local chartHeight = 180 -- In px
local extraHeight = 20 -- Extra height per extra tier, in px
local barWidth = 3 -- In em
local truncateX = false
local invertY = false
local ySuffix = ''
local colors = {
	'#999999', '#eeeeee', '#95291d', '#ea6a1a', '#aabc1e',
	'#edf669', '#3ec9a6', '#0b6b61', '#dda0dd', '#ffe4e1',
	'#fff943', '#eca72c', '#eee8aa', '#6bd425', '#618b25',
	'#e99fa8', '#e36271', '#db0c23', '#7197b6', '#325a8d'
}
local noGroupColor = '#999999'
-- Translations for parameter names
local w = {
	type = 'type',
	rankings = 'eindstanden',
	customLegend = 'aangepasteLegenda',
	chartHeight = 'grafiekhoogte',
	barWidth = 'staafbreedte',
	groups = 'groepen',
	note = 'opm',
	truncateX = 'xAfkappen',
	invertY = 'yOmkeren',
	x = 'x',
	y = 'y',
	yMax = 'yMax',
	ySuffix = 'ySuffix',
	yLabel = 'yLabel',
	yNote = 'yOpm',
	group = 'groep',
	subgroup = 'afd',
	label = 'label',
	color = 'kleur',
	tier = 'niveau',
	value = 'waarde',
	domains = 'domeinen',
	from = 'van',
	till = 'tot',
}

local function isItem(arg)
	-- An arg is considered an item if it starts with a pipe character.
	return string.find(mw.text.trim(arg), '|', 1, true) == 1
end

local function formatItem(item)
	-- Aliases
	item[w.x] = item[w.x] or item['1']
	item[w.y] = item[w.y] or item['2']
	item[w.group] = item[w.group] or item['3']
	item['1'] = nil
	item['2'] = nil
	item['3'] = nil
	
	item[w.y] = tonumber(item[w.y])
	item[w.yMax] = tonumber(item[w.yMax])
	item[w.tier] = tonumber(item[w.tier])
end

local function yesno(value, default)
	if _yesno(value) ~= nil then return _yesno(value) else return default end
end

local function pick(param, item)
	-- Picks a parameter from an item or else from it's group or else from data.
	local value = item[w[param]]
				  or item[w.group] and data.groups[item[w.group]][w[param]]
				  or data[param]
	
	if type(value) == "table" then
		-- Pick the value where x is within the domain.
		local x = tonumber(item[w.x])
		local domains = value[w.domains]
		
		if x and domains and type(domains) == "table" then
			for _, h in ipairs(domains) do
				if h[w.from] or h[w.till] then
					local from = h[w.from] or 0
					local till = h[w.till] or 9999
					if x > from and x <= till then return h[w.value] end
				end
			end
		end
		
		return value[w.value]
	end
	
	return value
end

local function minn(table)
	-- Returns the lowest positive numerical index of the given table, or zero
	-- if the table has no numerical indices.
	local minn, k = nil, nil
	repeat
		k = next(table, k)
		if type(k) == 'number' then
			if k == 1 then return 1 end
			if minn == nil or k < minn then minn = k end
		end
	until not k
	return minn or 0
end

local function countTiers(tiers)
	-- Gives the number of tiers, empty inbetweens included.
	return table.maxn(tiers) - minn(tiers) + 1
end

local function rankTiers(tiers)
	-- Ranks the tiers bottom up for convenience, since heights will be calculated
	-- from the bottom up. Example:
	--   [2] = true,           [2] = 4,
	--   [3] = true,    -->    [3] = 3,
	--   [5] = true,           [5] = 1,
	local highestTierNumber = table.maxn(tiers)
	
	for n, _ in pairs(tiers) do
		tiers[n] = highestTierNumber - n + 1
	end
	
	return tiers
end

local function mergeTables(t1, t2)
	if t1 and t2 then for k, v in pairs(t2) do t1[k] = v end end
	return t1
end

local function importGroups(basename)
	if basename == nil or basename == '' then return {} end
	
	return require('Module:Datastaven/Groepen/' .. basename)
end

local function extractData(args)
	-- Extract all the data we need from the args.
	data = {
		barWidth = args[w.barWidth] or barWidth,
		note = args[w.note],
		truncateX = yesno(args[w.truncateX], truncateX),
		invertY = yesno(args[w.invertY], invertY),
		yMax = args[w.yMax] or 0,
		ySuffix = args[w.ySuffix] or ySuffix,
		noGroupColor = args[w.color] or noGroupColor,
		customLegend = args[w.customLegend],
		bars = {},
		groups = {},
		tiers = {},
	}
	-- Import preset groups.
	local presetGroups = importGroups(args[w.groups])
	
	-- Extract from inline groups.
	for i = 1, 20 do
		local arg = args[w.group .. i]
		
		if arg and isItem(arg) then
			local group = unpackItem(arg)
			
			if group[w.group] then
				group = mergeTables(presetGroups[group[w.group]] or {}, group)
				data.groups[group[w.group]] = group -- Add to our groups
				group[w.label] = group[w.label] or group[w.group]
				group[w.group] = nil
				formatItem(group)
			end
		end
	end
	
	-- Extract from items.
	for _, arg in ipairs(args) do
		if isItem(arg) then
			local bar = unpackItem(arg)
			formatItem(bar)
			table.insert(data.bars, bar)
			
			if bar[w.y] then
				data.yMax = math.max(data.yMax, bar[w.y])
			end
			
			if bar[w.group] and data.groups[bar[w.group]] == nil then
				data.groups[bar[w.group]] = presetGroups[bar[w.group]]
											 or { [w.label] = bar[w.label] or bar[w.group],
											 	  [w.color] = bar[w.color] }
			end
			
			local tier = tonumber(pick('tier', bar))
			if tier then data.tiers[tier] = true end
		end
	end
	
	data.tiers = rankTiers(data.tiers)
	data.tiersCount = countTiers(data.tiers)
	data.chartHeight = args[w.chartHeight] or chartHeight + extraHeight * (data.tiersCount - 1)
	
	return data
end

local function calculateBarHeight(bar)
	local y = bar[w.y]
	local yMax = pick('yMax', bar)
	local tierRank = data.tiers[pick('tier', bar)] or 1
	local h = 0
	
	if y then
		if data.invertY then
			h = (1 - ((y - 1) / yMax)) * 100		 -- Height % (within it's tier)
			if y > yMax then h = 0 end
		else
			h = y / yMax * 100
			if y > yMax then h = 100 end
		end
	end
	
	h = (h + tierRank * 100 - 100) / data.tiersCount -- Add heights of lower tiers
	h = math.floor(h * 1000) / 1000					 -- Truncate number
	return h
end

local function pickColor(bar)
	local color = pick('color', bar)
	if color then return color end
	
	if bar[w.group] then
		color = table.remove(colors) or '#fff'
		data.groups[bar[w.group]][w.color] = color
	else
		color = data.noGroupColor
	end
	
	return color
end

local function delink(text)
	-- Removes (wiki)links from a text.
	if not type(text) == 'string' then return text end
	return _delink({ text, urls = 'no', comments = 'no', whitespace = 'no' })
end

local function drawTooltip(bar)
	local text = mw.html.create()
	local x = bar[w.x]
	local y = bar[w.y] and bar[w.y] .. pick('ySuffix', bar) or bar[w.yLabel]
	local yNote = bar[w.yNote]
	
	if x and x ~= '' then text:tag('b'):wikitext(x) end
	if x and (y or yNote) then text:wikitext('&ensp;') end
	if y then text:wikitext(y) end
	if y and yNote then text:wikitext(' ') end
	if yNote then text:wikitext(yNote) end
	
	if bar[w.group] or bar[w.label] then
		if tostring(text) ~= '' then text:tag('br') end
		text:wikitext(delink(pick('label', bar)))
		if bar[w.subgroup] then text:wikitext(' ' .. bar[w.subgroup]) end
	end
	
	return mw.html.create()
		:newline()
		:tag('div')
		:addClass('es-tip')
		:node(text)
		:done()
end

local function drawBars()
	local bars = mw.html.create()
	
	for _, bar in ipairs(data.bars) do
		local x = bar[w.x]
		local y = bar[w.yLabel] or bar[w.y]
		local h = calculateBarHeight(bar) .. '%'
		local c = pickColor(bar)
		
		if x and data.truncateX then x = string.sub(x, -2) end -- Show only the last two digits of the year
		
		bars
			:newline()
			:tag('li')
			:addClass('es-bar')
			:attr('tabindex', 0)
			:attr('data-x', x)
			:attr('data-y', y)
			:css('height', h)
			:css('background-color', c)
			:node(drawTooltip(bar))
			:newline()
	end
	
	return bars
end

local function orderItems(item1, item2)
	-- Order by tier number (asc).
	local t1 = pick('tier', item1) or 1000
	local t2 = pick('tier', item2) or 1000
	return t1 < t2
end

local function getLegendItems()
	local legendItems = {}
	local groups = {}
	
	-- Copy data.groups to groups.
	for k, g in pairs(data.groups) do groups[k] = g end
	
	-- Start legendItems with the groups chosen in `customLegend`.
	if data.customLegend then
		local codes = mw.text.split(data.customLegend, "%s*,%s*")
		
		for _, code in pairs(codes) do
			table.insert(legendItems, groups[code])
			groups[code] = nil
		end
	end
	
	-- Order the (remaining) groups by tier number, then append to legendItems.
	local iGroups = {}
	for _, g in pairs(groups) do table.insert(iGroups, g) end
	table.sort(iGroups, orderItems)
	for _, g in ipairs(iGroups) do table.insert(legendItems, g) end
	
	return legendItems
end

local function drawLegend()
	local groups = getLegendItems()
	local legend = mw.html.create('div')
		:addClass('es-legend')
		:tag('ul')
	
	for _, group in pairs(groups) do
		legend
			:newline()
			:tag('li')
			:tag('span')
			:css('background-color', pick('color', group) or '#fff')
			:done()
			:wikitext(pick('label', group))
	end
	
	return legend:done()
end

local function drawChart(args)
	data = extractData(args)
	-- return mw.dumpObject(data)
	
	if #data.bars == 0 then
		return "''Geen data om weer te geven.''"
	end
	
	local chart = mw.html.create()
		:tag('div')
		:addClass('es-chart')
		:css('overflow-y', 'hidden')
		:tag('ul')
		:addClass('es-grid')
		:css('width', data.barWidth * #data.bars .. 'em')
		:css('height', data.chartHeight .. 'px')
		:node(drawBars())
		:allDone()
		
	if data.note then chart:tag('div'):addClass('es-note'):wikitext(data.note) end
	chart:node(drawLegend())
	
	return tostring(chart)
end

function p.main(frame)
	local args = getArgs(frame)
	args[1] = args[1] or '' -- Data won't show when args[1] is absent
	
	-- Settings for rankings.
	if args[w.type] == w.rankings then
		invertY = true
		barWidth = 1.4
		truncateX = true
		ySuffix = 'e'
	end
	
	return frame:extensionTag{ name = 'templatestyles', args = { src = templatestyles } } .. drawChart(args)
end

return p