Module:Datastaven

Uit Wiki Raamsdonks Historie

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

Standaard:

{{#invoke:Datastaven|main}}

Speciaal voor eindstanden kan ook gebruik worden gemaakt van:

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

Kleuren

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


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