Modul:Vorlage:plebiscite-graph
Zur Navigation springen
Zur Suche springen
expandTemplate: template "Vorlage:LuaModuleDoc/Navigation" does not exist
local p = {}
function p.bars(frame)
local args = frame:getParent().args
local hasArgs = false
for k, v in pairs(args) do
hasArgs = true
break
end
if not hasArgs then args = frame.args end
-- localisation table (i18n)
local i18n = {
error_no_title = 'Fehler: Ein "titel" (oder "title") ist für das Diagramm zwingend erforderlich.',
source_prefix = "'''Quelle:''' ",
bar_default_prefix = "Balken ",
option_default_prefix = "Option ",
option_yes = "Ja",
option_no = "Nein",
nvoters_name_default = "Stimmverzicht",
majority_cast = "Mehrheit der abgegebenen Stimmen",
majority_valid = "Mehrheit der gültigen Stimmen",
bracket_cast = "abgegebene Stimmen",
bracket_valid = "gültige Stimmen",
quorum_turnout = "Beteiligungs",
quorum_approval = "Zustimmungs",
quorum_veto = "Veto",
quorum_suffix = "quorum"
}
-- alias-mapping for German parameters
local aliasMap = {
title = "titel",
subtitle = "untertitel",
legend_type = "legendentyp",
source = "quelle",
annotation = "anmerkung",
btitle = "btitel",
eligible = "stimmberechtigte",
cast = "abgegeben",
valid = "gültig",
nvoters_name = "nstimmende_name",
nvoters_color = "nstimmende_farbe",
votes = "stimmen",
proposal = "vorlage",
color = "farbe",
threshold = "schwelle",
threshold_type = "schwellen_typ",
threshold_mode = "schwellen_modus",
threshold_legend = "schwelle_legende",
quorum = "quorum",
quorum_type = "quorum_typ",
quorum_mode = "quorum_modus",
quorum_value = "quorum_wert",
quorum_legend = "quorum_legende",
-- Parameter für das zweite Quorum
quorum2 = "quorum2",
quorum2_type = "quorum2_typ",
quorum2_mode = "quorum2_modus",
quorum2_value = "quorum2_wert",
quorum2_legend = "quorum2_legende"
}
-- alias-mapping for German values
local valueMap = {
turnout = "Beteiligung",
approval = "Zustimmung",
veto = "Veto",
relative = "relativ",
absolute = "absolut",
valid = "gültig",
cast = "abgegeben",
common = "gemeinsam",
exists = "vorhanden"
}
-- support function for localised parameters
local function getArg(b, key)
local deKey = aliasMap[key] or key
local val = nil
if key == "bar_title" then
local deBtitle = aliasMap["btitle"]
val = args["b" .. b .. "_" .. deBtitle] or args["b" .. b .. "_btitle"]
if b == 1 then
val = val or args[deBtitle] or args["btitle"]
end
return val
end
if b == 1 then
val = args["b1_" .. deKey] or args["b1_" .. key] or args[deKey] or args[key]
else
val = args["b" .. b .. "_" .. deKey] or args["b" .. b .. "_" .. key]
end
if not val then
local base, num = key:match("^(%a+)(%d+)$")
if base and num then
local deBase = aliasMap[base] or base
if b == 1 then
val = args["b1_" .. deBase .. num] or args["b1_" .. base .. num] or args[deBase .. num] or args[base .. num]
else
val = args["b" .. b .. "_" .. deBase .. num] or args["b" .. b .. "_" .. base .. num]
end
end
end
return val
end
local title = getArg(1, "title")
if not title or title == "" then
return '<span style="color:red; font-weight:bold;">' .. i18n.error_no_title .. '</span>'
end
local function cleanNumber(val)
if not val or val == "" then return nil end
local s = tostring(val):gsub("[.,%s]", "")
return tonumber(s)
end
local function isValidHexColor(val)
if type(val) ~= "string" then return false end
return val:match("^#[0-9a-fA-F]+$") and (#val == 4 or #val == 5 or #val == 7 or #val == 9)
end
local legendTypeArg = getArg(1, "legend_type")
local legendCommon = (legendTypeArg == "common" or legendTypeArg == valueMap.common)
local maxBars = 10
local maxGroups = 20
local fallbackColor = "#cccccc"
local allData = {}
local globalLegendItems = {}
local legendKeys = {}
for b = 1, maxBars do
local hasVotes = false
local votesTotal = 0
for i = 1, maxGroups do
local v = cleanNumber(getArg(b, "votes" .. i))
if v then hasVotes = true; votesTotal = votesTotal + v end
end
local quorumArg = getArg(b, "quorum")
if quorumArg == "exists" or quorumArg == valueMap.exists or hasVotes or getArg(b, "cast") or getArg(b, "valid") then
local eligibleVal = cleanNumber(getArg(b, "eligible"))
local validValParam = cleanNumber(getArg(b, "valid"))
local castVal = cleanNumber(getArg(b, "cast"))
-- Keine ungültigen Stimmen mehr: gültige = abgegebene Stimmen
local validVal = validValParam or castVal or votesTotal
castVal = validVal
local thresholdRaw = getArg(b, "threshold")
local tType = getArg(b, "threshold_type") or "valid"
local tMode = getArg(b, "threshold_mode") or "relative"
local tVal = tonumber((string.gsub(tostring(thresholdRaw or ""), ",", ".")))
local tLegendCustom = getArg(b, "threshold_legend")
-- Erstes Quorum auslesen
local qStatus = (quorumArg == "exists" or quorumArg == valueMap.exists)
local qType = getArg(b, "quorum_type") or "turnout"
local qMode = getArg(b, "quorum_mode") or "relative"
local qThresholdRaw = getArg(b, "quorum_value")
local qVal = tonumber((string.gsub(tostring(qThresholdRaw or ""), ",", ".")))
local qLegendCustom = getArg(b, "quorum_legend")
-- Zweites Quorum auslesen
local q2Arg = getArg(b, "quorum2")
local q2Status = (q2Arg == "exists" or q2Arg == valueMap.exists)
local q2Type = getArg(b, "quorum2_type") or "turnout"
local q2Mode = getArg(b, "quorum2_mode") or "relative"
local q2ThresholdRaw = getArg(b, "quorum2_value")
local q2Val = tonumber((string.gsub(tostring(q2ThresholdRaw or ""), ",", ".")))
local q2LegendCustom = getArg(b, "quorum2_legend")
local tempBarData = {}
local currentVisualSum = 0
local voteOrder = {}
if qType == "veto" or qType == valueMap.veto then
for i = 2, maxGroups, 2 do table.insert(voteOrder, i) end
for i = 1, maxGroups, 2 do table.insert(voteOrder, i) end
else
for i = 1, maxGroups do table.insert(voteOrder, i) end
end
for _, i in ipairs(voteOrder) do
local vRaw = getArg(b, "votes" .. i)
if vRaw and vRaw ~= "" then
local votes = cleanNumber(vRaw) or 0
local defaultProposal = (i == 1 and i18n.option_yes or (i == 2 and i18n.option_no or i18n.option_default_prefix .. i))
local proposal = getArg(b, "proposal" .. i) or defaultProposal
local rawColor = getArg(b, "color" .. i) or (i == 1 and "#128710" or (i == 2 and "#D93025" or fallbackColor))
local color = isValidHexColor(rawColor) and rawColor or fallbackColor
table.insert(tempBarData, {proposal = proposal, value = votes, color = color, isVote = true})
currentVisualSum = currentVisualSum + votes
if legendCommon and not legendKeys[proposal] then
table.insert(globalLegendItems, {proposal = proposal, color = color})
legendKeys[proposal] = true
end
end
end
if qStatus and eligibleVal then
local uncast = math.max(0, eligibleVal - castVal)
local rawNvColor = getArg(b, "nvoters_color") or "#E2E8F0"
local nvColor = isValidHexColor(rawNvColor) and rawNvColor or "#E2E8F0"
local nvName = (getArg(b, "nvoters_name") or i18n.nvoters_name_default)
table.insert(tempBarData, {proposal = nvName, value = uncast, color = nvColor, isNV = true})
currentVisualSum = currentVisualSum + uncast
if legendCommon and not legendKeys[nvName] then
table.insert(globalLegendItems, {proposal = nvName, color = nvColor})
legendKeys[nvName] = true
end
end
local m1, m2, m3 = nil, nil, nil
local m1Label, m2Label, m3Label = "", "", ""
local bracketWidth = 0
-- Berechnung Mehrheit (Marker 1)
if tVal and currentVisualSum > 0 then
local basis = (tType == "cast" or tType == valueMap.cast) and castVal or validVal
m1 = (tMode == "relative" or tMode == valueMap.relative) and (((tVal / 100) * basis) / currentVisualSum * 100) or ((tVal / currentVisualSum) * 100)
bracketWidth = (basis / currentVisualSum) * 100
m1Label = (tLegendCustom and tLegendCustom ~= "") and tLegendCustom or ((tType == "cast" or tType == valueMap.cast) and i18n.majority_cast or i18n.majority_valid)
end
-- Berechnung Erstes Quorum (Marker 2)
if qStatus and qVal and currentVisualSum > 0 then
m2 = (qMode == "relative" or qMode == valueMap.relative) and (((qVal / 100) * eligibleVal) / currentVisualSum * 100) or ((qVal / currentVisualSum) * 100)
if qLegendCustom and qLegendCustom ~= "" then
m2Label = qLegendCustom
else
local qPrefix = i18n.quorum_turnout
if qType == "approval" or qType == valueMap.approval then qPrefix = i18n.quorum_approval
elseif qType == "veto" or qType == valueMap.veto then qPrefix = i18n.quorum_veto end
m2Label = (qThresholdRaw or "0") .. " % " .. qPrefix .. i18n.quorum_suffix
end
end
-- Berechnung Zweites Quorum (Marker 3)
if q2Status and q2Val and currentVisualSum > 0 then
m3 = (q2Mode == "relative" or q2Mode == valueMap.relative) and (((q2Val / 100) * eligibleVal) / currentVisualSum * 100) or ((q2Val / currentVisualSum) * 100)
if q2LegendCustom and q2LegendCustom ~= "" then
m3Label = q2LegendCustom
else
local q2Prefix = i18n.quorum_turnout
if q2Type == "approval" or q2Type == valueMap.approval then q2Prefix = i18n.quorum_approval
elseif q2Type == "veto" or q2Type == valueMap.veto then q2Prefix = i18n.quorum_veto end
m3Label = (q2ThresholdRaw or "0") .. " % " .. q2Prefix .. i18n.quorum_suffix
end
end
local markerTexts = {}
if m1 then table.insert(markerTexts, {text = m1Label, type = 1, pos = m1}) end
if m2 then table.insert(markerTexts, {text = m2Label, type = 2, pos = m2}) end
if m3 then table.insert(markerTexts, {text = m3Label, type = 3, pos = m3}) end
table.insert(allData, {
data = tempBarData, total = currentVisualSum, id = b,
validTotal = validVal, tType = tType,
markerTexts = markerTexts,
marker1 = m1, marker1Label = m1Label,
marker2 = m2, marker2Label = m2Label,
marker3 = m3, marker3Label = m3Label,
bracketWidth = bracketWidth, bracketLabel = ((tType == "cast" or tType == valueMap.cast) and i18n.bracket_cast or i18n.bracket_valid)
})
end
end
-- Rendering
local container = mw.html.create('div'):css({['float']='right', ['width']='320px', ['border']='1px solid #a2a9b1', ['padding']='10px', ['background']='#f8f9fa', ['margin']='0 0 1em 1em', ['font-family']='sans-serif'})
container:tag('div'):css({['font-weight']='bold', ['font-size']='1.1em', ['text-align']='center', ['margin-bottom']='1px'}):wikitext(title)
local subtitle = getArg(1, "subtitle")
if subtitle and subtitle ~= "" then container:tag('div'):css({['font-size']='0.85em', ['color']='#555', ['text-align']='center', ['margin-bottom']='10px'}):wikitext(subtitle) end
for _, bSet in ipairs(allData) do
local bTitle = getArg(bSet.id, "bar_title")
if bTitle then container:tag('div'):css({['font-size']='0.9em', ['font-weight']='bold', ['margin-top']='15px', ['border-bottom']='1px solid #eee'}):wikitext(bTitle) end
local labelLevelHeight = 16
local totalLabelHeight = #bSet.data * labelLevelHeight + 10
local graphWrapper = container:tag('div'):css({['position'] = 'relative', ['width'] = '100%', ['margin-top'] = '15px', ['margin-bottom'] = totalLabelHeight .. 'px'})
local graphContainer = graphWrapper:tag('div'):css({['position']='relative', ['width']='100%', ['height']='24px', ['border']='1px solid #777', ['background']='#eee', ['display']='flex', ['box-sizing']='border-box', ['z-index']='2'})
local currentOffset = 0
for i, g in ipairs(bSet.data) do
local percent = (g.value / bSet.total) * 100
-- Mouseover: Name und absolute Zahl
local barSegment = graphContainer:tag('div'):css({['flex']=string.format("%.6f 0 0", g.value), ['background-color']=g.color, ['height']='100%', ['position']='relative'}):attr('title', string.format("%s: %s", g.proposal, mw.language.getContentLanguage():formatNum(g.value)))
-- Vertical label lines & Text unter dem Balken
if percent > 0 then
local centerX = currentOffset + (percent / 2)
local lineContainer = graphWrapper:tag('div'):css({['position']='absolute', ['top']='24px', ['left']='0', ['width']='100%', ['height']='1px'})
-- Vertical line
lineContainer:tag('div'):css({['position']='absolute', ['left']=string.format("%.2f%%", centerX), ['top']='0', ['width']='1px', ['height']=(i * labelLevelHeight) .. 'px', ['background-color']='#aaa'})
-- Horizontal line to center
local lineWidth = math.abs(centerX - 50)
local lineLeft = math.min(centerX, 50)
lineContainer:tag('div'):css({['position']='absolute', ['left']=string.format("%.2f%%", lineLeft), ['top']=(i * labelLevelHeight) .. 'px', ['width']=string.format("%.2f%%", lineWidth), ['height']='1px', ['background-color']='#aaa'})
-- Label text (nur noch ein Prozentwert)
local labelContent = string.format("%.1f%%", percent)
if g.isNV then
labelContent = labelContent .. " (" .. mw.language.getContentLanguage():formatNum(g.value) .. ")"
end
graphWrapper:tag('div'):css({
['position'] = 'absolute',
['top'] = (24 + i * labelLevelHeight - 7) .. 'px',
['left'] = '50%',
['transform'] = 'translateX(-50%)',
['background'] = '#f8f9fa',
['padding'] = '0 4px',
['font-size'] = '10px',
['font-weight'] = 'bold',
['white-space'] = 'nowrap',
['z-index'] = '3'
}):wikitext(labelContent)
end
currentOffset = currentOffset + percent
end
-- Markers (Threshold/Quorums) - Linien einzeichnen
if bSet.marker1 then
graphContainer:tag('div'):css({['position']='absolute', ['left']=string.format("%.2f%%", bSet.marker1), ['top']='-2px', ['bottom']='-2px', ['z-index']='10', ['width']='2px', ['background-color']='#000', ['cursor']='help'}):attr('title', bSet.marker1Label)
end
if bSet.marker2 then
graphContainer:tag('div'):css({['position']='absolute', ['left']=string.format("%.2f%%", bSet.marker2), ['top']='-2px', ['bottom']='-2px', ['z-index']='10', ['width']='0', ['border-left']='2px dotted #000', ['cursor']='help'}):attr('title', bSet.marker2Label)
end
if bSet.marker3 then
graphContainer:tag('div'):css({['position']='absolute', ['left']=string.format("%.2f%%", bSet.marker3), ['top']='-2px', ['bottom']='-2px', ['z-index']='10', ['width']='0', ['border-left']='2px dashed #000', ['cursor']='help'}):attr('title', bSet.marker3Label)
end
-- Local Legend
if not legendCommon then
local leg = container:tag('div'):css({['margin-top']='5px'})
local icons = leg:tag('div'):css({['display']='flex', ['flex-wrap']='wrap', ['gap']='5px', ['font-size']='0.8em'})
for _, g in ipairs(bSet.data) do
local e = icons:tag('div')
e:tag('span'):css({['display']='inline-block', ['width']='8px', ['height']='8px', ['background-color']=g.color, ['border']='1px solid #999', ['margin-right']='3px'})
e:tag('span'):css({['font-weight']='bold'}):wikitext(g.proposal)
end
if #bSet.markerTexts > 0 then
local mLeg = leg:tag('div'):css({['font-size']='0.75em', ['color']='#444', ['margin-top']='6px'})
for _, tObj in ipairs(bSet.markerTexts) do
local row = mLeg:tag('div'):css({['display']='flex', ['align-items']='center', ['margin-bottom']='2px'})
local bTop = 'none'
if tObj.type == 2 then bTop = '2px dotted #000'
elseif tObj.type == 3 then bTop = '2px dashed #000' end
local bgCol = (tObj.type == 2 or tObj.type == 3) and 'transparent' or '#000'
row:tag('span'):css({['display']='inline-block', ['width']='12px', ['height']='3px', ['background-color']=bgCol, ['border-top']=bTop, ['margin-right']='8px'})
row:tag('span'):wikitext(tObj.text)
end
end
end
end
-- Global Legend
if legendCommon and #globalLegendItems > 0 then
local leg = container:tag('div'):css({['margin-top']='15px', ['padding-top']='10px', ['border-top']='1px solid #ccc'})
local icons = leg:tag('div'):css({['display']='flex', ['flex-wrap']='wrap', ['gap']='8px', ['font-size']='0.8em'})
for _, g in ipairs(globalLegendItems) do
local e = icons:tag('div'):css({['display']='flex', ['align-items']='center'})
e:tag('span'):css({['display']='inline-block', ['width']='10px', ['height']='10px', ['background-color']=g.color, ['border']='1px solid #999', ['margin-right']='4px', ['flex-shrink']='0'})
e:tag('span'):css({['font-weight']='bold'}):wikitext(g.proposal)
end
if allData[1] and #allData[1].markerTexts > 0 then
local mLeg = leg:tag('div'):css({['font-size']='0.75em', ['color']='#444', ['margin-top']='10px'})
for _, tObj in ipairs(allData[1].markerTexts) do
local row = mLeg:tag('div'):css({['display']='flex', ['align-items']='center', ['margin-bottom']='2px'})
local bTop = 'none'
if tObj.type == 2 then bTop = '2px dotted #000'
elseif tObj.type == 3 then bTop = '2px dashed #000' end
local bgCol = (tObj.type == 2 or tObj.type == 3) and 'transparent' or '#000'
row:tag('span'):css({['display']='inline-block', ['width']='12px', ['height']='3px', ['background-color']=bgCol, ['border-top']=bTop, ['margin-right']='8px'})
row:tag('span'):wikitext(tObj.text)
end
end
end
-- Annotation (Sicheres Kürzen für UTF-8)
local annotation = getArg(1, "annotation")
if annotation and annotation ~= "" then
if mw.ustring.len(annotation) > 250 then
annotation = mw.ustring.sub(annotation, 1, 247) .. "..."
end
container:tag('div'):css({['font-size']='0.75em', ['margin-top']='10px', ['border-top']='1px solid #ccc', ['padding-top']='5px', ['color']='#333'}):wikitext("'''Anmerkung:''' " .. annotation)
end
local source = getArg(1, "source")
if source and source ~= "" then
container:tag('div'):css({['font-size']='0.7em', ['margin-top']='10px', ['border-top']='1px solid #ccc', ['padding-top']='5px', ['color']='#666'}):wikitext(i18n.source_prefix .. source)
end
return tostring(container)
end
return p