Modul:Vorlage:plebiscite-graph

Aus vBundesrepublik
Version vom 6. September 2026, 19:42 Uhr von Administrator (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „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 =…“)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen

Modul:Vorlage:LuaModuleDoc:144: attempt to index field 'wikibase' (a nil value)


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