Module:Sandbox/Ita140188/chartsvg explained

--

lua

--[[
    keywords are used for languages: they are the names of the actual
    parameters of the template
]]

local keywords = {
    barChart = 'bar chart',
    width = 'width',
    height = 'height',
    stack = 'stack',
    colors = 'colors',
    group = 'group',
    xlegend = 'x legends',
    tooltip = 'tooltip',
    accumulateTooltip = 'tooltip value accumulation',
    links = 'links',
    defcolor = 'default color',
    scalePerGroup = 'scale per group',
    unitsPrefix = 'units prefix',
    unitsSuffix = 'units suffix',
    groupNames = 'group names',
    hideGroupLegends = 'hide group legends',
    slices = 'slices',
    slice = 'slice',
    radius = 'radius',
    percent = 'percent',

} -- here is what you want to translate




function barChart(frame)
	
	
    local res = {}
    local args = frame.args -- can be changed to frame:getParent.args
    local values, xlegends, colors, tooltips, yscales = {}, {}, {}, {},{}, {}, {}
    local groupNames, unitsSuffix, unitsPrefix, links = {}, {}, {}, {}
    local width, height, stack, delimiter = 500, 350, false, args.delimiter or ':'
    local chartWidth, chartHeight, defcolor, scalePerGroup, accumulateTooltip

    local numGroups, numValues
    local scaleWidth

	local defColors = require "Module:Plotter/DefaultColors"
	local hideGroupLegends
	
	local function nulOrWhitespace(s)
	    return not s or mw.text.trim(s) == ''
	end

	table.insert(res, frame:extensionTag{ name = 'templatestyles', args = { src = 'TemplateStyles sandbox/Ita140188/styles.css'} })


	function createGroupList(tab, legends, cols)
	    if #legends > 1 and not hideGroupLegends then
	        table.insert(tab, mw.text.tag('div', { style = string.format("width:%spx;", chartWidth) }))
	        local list = {}
	        local spanStyle = "padding:0 1em;background-color:%s;border:1px solid %s;margin-right:1em;-webkit-print-color-adjust:exact;"
	        for gi = 1, #legends do
	            local span = mw.text.tag('span', { style = string.format(spanStyle, cols[gi], cols[gi]) }, ' ') .. ' '..  legends[gi]
	            table.insert(list, mw.text.tag('li', {}, span))
	        end
	        table.insert(tab,
	            mw.text.tag('ul',
	                {style="width:100%;list-style:none;-webkit-column-width:12em;-moz-column-width:12em;column-width:12em;"},
	                table.concat(list, '\n')
	)
	)
	        table.insert(tab, '</div>')
	    end
	end



    function validate
        function asGroups(name, tab, toDuplicate, emptyOK)
            if #tab == 0 and not emptyOK then
                error("must supply values for " .. keywords[name])
            end
            if #tab == 1 and toDuplicate then
                for i = 2, numGroups do tab[i] = tab[1] end
            end
            if #tab > 0 and #tab ~= numGroups then
                error (keywords[name] .. ' must contain the same number of items as the number of groups, but it contains ' .. #tab .. ' items and there are ' .. numGroups .. ' groups')
            end
        end

        -- do all sorts of validation here, so we can assume all params are good from now on.
        -- among other things, replace numerical values with mw.language:parseFormattedNumber result


        chartHeight = height - 80
        numGroups = #values
        numValues = #values[1]
        defcolor = defcolor or 'blue'
        colors[1] = colors[1] or defcolor
        scaleWidth = scalePerGroup and 80 * numGroups or 30
        chartWidth = width -scaleWidth
        asGroups('unitsPrefix', unitsPrefix, true, true)
        asGroups('unitsSuffix', unitsSuffix, true, true)
        asGroups('colors', colors, true, true)
        asGroups('groupNames', groupNames, false, false)
        if stack and scalePerGroup then
            error(string.format('Illegal settings: %s and %s are incompatible.', keyword.stack, keyword.scalePerGroup))
        end
        for gi = 2, numGroups do
            if #values[gi] ~= numValues then error(keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1") end
        end
        if #xlegends ~= numValues then error('Illegal number of ' .. keywords.xlegend .. '. Should be exactly ' .. numValues) end
    end

    function extractParams
        function testone(keyword, key, val, tab)
            i = keyword == key and 0 or key:match(keyword .. "%s+(%d+)")
            if not i then return end
            i = tonumber(i) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
            if i > 0 then tab[i] = {} end
            for s in mw.text.gsplit(val, '%s*' .. delimiter .. '%s*') do
                table.insert(i == 0 and tab or tab[i], s)
            end
            return true
        end

        for k, v in pairs(args) do
            if k == keywords.width then
                width = tonumber(v)
                if not width or width < 200 then
                    error('Illegal width value (must be a number, and at least 200): ' .. v)
                end
            elseif k == keywords.height then
                height = tonumber(v)
                if not height or height < 200 then
                    error('Illegal height value (must be a number, and at least 200): ' .. v)
                end
            elseif k == keywords.stack then stack = true
            elseif k == keywords.scalePerGroup then scalePerGroup = true
            elseif k == keywords.defcolor then defcolor = v
            elseif k == keywords.accumulateTooltip then accumulateTooltip = not nulOrWhitespace(v)
            elseif k == keywords.hideGroupLegends then hideGroupLegends = not nulOrWhitespace(v)
            else
                for keyword, tab in pairs({
                    group = values,
                    xlegend = xlegends,
                    colors = colors,
                    tooltip = tooltips,
                    unitsPrefix = unitsPrefix,
                    unitsSuffix = unitsSuffix,
                    groupNames = groupNames,
                    links = links,
                    }) do
                        if testone(keywords[keyword], k, v, tab)
                            then break
                        end
                end
            end
        end
    end

    function roundup(x) -- returns the next round number: eg., for 30 to 39.999 will return 40, for 3000 to 3999.99 wil return 4000. for 10 - 14.999 will return 15.
        local ordermag = 10 ^ math.floor(math.log10(x))
        local normalized = x /  ordermag
        local top = normalized >= 1.5 and (math.floor(normalized + 1)) or 1.5
        return ordermag * top, top, ordermag
    end

    function calcHeightLimits -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.
        if stack then
            local sums = {}
            for _, group in pairs(values) do
                for i, val in ipairs(group) do sums[i] = (sums[i] or 0) + val end
            end
            local sum = math.max(unpack(sums))
            for i = 1, #values do yscales[i] = sum end
        else
            for i, group in ipairs(values) do yscales[i] = math.max(unpack(group)) end
        end
        for i, scale in ipairs(yscales) do yscales[i] = roundup(scale * 0.9999) end
        if not scalePerGroup then for i = 1, #values do yscales[i] = math.max(unpack(yscales)) end end
    end

    function tooltip(gi, i, val)
        if tooltips and tooltips[gi] and not nulOrWhitespace(tooltips[gi][i]) then return tooltips[gi][i], true end
        local groupName = not nulOrWhitespace(groupNames[gi]) and groupNames[gi] .. ': ' or ''
        local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
        local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
        return mw.ustring.gsub(groupName .. prefix .. mw.getContentLanguage:formatNum(tonumber(val) or 0) .. suffix, '_', ' '), false
    end

    function calcHeights(gi, i, val)
        local barHeight = math.floor(val / yscales[gi] * chartHeight + 0.5) -- add half to make it "round" instead of "trunc"
        local top, base = chartHeight - barHeight, 0
        if stack then
            local rawbase = 0
            for j = 1, gi - 1 do rawbase = rawbase + values[j][i] end -- sum the "i" value of all the groups below our group, gi.
            base = math.floor(chartHeight * rawbase / yscales[gi]) -- normally, and especially if it's "stack", all the yscales must be equal.
        end
        return barHeight, top - base
    end

    function groupBounds(i)
        local setWidth = math.floor(chartWidth / numValues)
        -- local setOffset = (i - 1) * setWidth
        local setOffset = (i - 1) * setWidth
        return setOffset, setWidth
    end

    function calcx(gi, i)
        local setOffset, setWidth = groupBounds(i)
        if stack or numGroups == 1 then
            local barWidth = math.min(38, math.floor(0.8 * setWidth))
            return setOffset + (setWidth - barWidth) / 2, barWidth
        end
        setWidth = 0.85 * setWidth
        local barWidth = math.floor(0.75 * setWidth / numGroups)
        local left = setOffset + math.floor((gi - 1) / numGroups * setWidth)
        return left, barWidth
    end

    function drawbar(gi, i, val, ttval)
        if val == '0' then return end -- do not show single line (borders....) if value is 0, or rather, '0'. see talkpage

        local color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip(gi, i, ttval or val)
        local left, barWidth = calcx(gi, i)
        local barHeight, top = calcHeights(gi, i, val)
        
        -- borders so it shows up when printing
        local style = string.format("x:%spx;y:%spx;height:%spx;width:%spx;fill:%s;", left, top, barHeight-1, barWidth-2, color)
        local link = links[gi] and links[gi][i] or ''
        -- local img = not nulOrWhitespace(link) and mw.ustring.format('[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '') or ''
        local img = string.format("<title>%s</title>",tooltip);
        table.insert(res, mw.text.tag('rect', { style = style, class = "chart2-bar" }, img))
    end





    function drawYScale
    	
        function drawSingle(gi, color, single)
            local yscale = yscales[gi]
            local _, top, ordermag = roundup(yscale * 0.999)
            local numnotches = top <= 1.5 and top * 4
                    or top < 4  and top * 2
                    or top
            local valStyleStrCntnr = 'display:block;position:relative;height:%spx;text-align:right;margin:0px;' -- SINGLE ELEMENT OF Y AXIS
            local valStyleStrValue = 'display:block;position:relative;float:right;height:%spx;text-align:right;margin:%spx 0px 0px 0px;vertical-align:middle;line-height:%spx;' -- value
            local valStyleStrNotch = 'display:block;position:relative;float:right;height:%spx;text-align:right;width:5px; border-top:1px solid black' -- notch
                -- or 'position:relative;height=20px;text-align:right;vertical-align:middle;max-width:%spx;top:%spx;left:3px;background-color:%s;color:white;font-weight:bold;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;padding:0 2px'
            -- local notchStyleStr = 'position:absolute;height=1px;min-width:5px;top:%spx;left:%spx;border:1px solid %s;'
            for i = 1, numnotches do
                local val = (numnotches - i + 1) / numnotches * yscale -- value of this notch
                local y = (1 / numnotches * chartHeight)   --chartHeight - calcHeights(gi, 1, val) -- height of a single notch
                local divCntnr = mw.text.tag('div', { style = string.format(valStyleStrCntnr, y, color) })
                local divValue = mw.text.tag('div', { style = string.format(valStyleStrValue, y, -y/2, y, color) }, mw.getContentLanguage:formatNum(tonumber(val) or 0))
                local divNotch = mw.text.tag('div', { style = string.format(valStyleStrNotch, y, color) }, '&nbsp;') 
                table.insert(res, divCntnr)
                table.insert(res, divNotch)
                table.insert(res, divValue)
                table.insert(res, '</div>')
                table.insert(res, '<div style="clear:right;display:block"></div>')
                
                
                -- div = mw.text.tag('div', { style = string.format(notchStyleStr, y, width - 4, color) }, '')
                -- table.insert(res, div)
            end
        end

        if scalePerGroup then
            -- local colWidth = 80
            -- local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"
            
            
            -- local colStyle = "height:%spx;border-right:1px solid %s;color:%s;display:inline-block;text-align:right"
            -- for gi = 1, numGroups do
            --     -- local left = (gi - 1) * colWidth
            --     local color = colors[gi] or defcolor
            --     -- table.insert(res, mw.text.tag('div', { style = string.format(colStyle, chartHeight, colWidth, left, color, color) }))
            --     table.insert(res, mw.text.tag('div', { style = string.format(colStyle, chartHeight, color, color) }))
            --     drawSingle(gi, color)
            --     table.insert(res, '</div>')
            -- end
        else
            drawSingle(1, 'black',  true) -- gi is the id of y axis when more than 1 
        end
    end




    function drawXlegends
        local setOffset, setWidth
        local legendDivStyleFormat = "display:block;float:left;position:relative;vertical-align:top;width:%spx;text-align:center;margin:0px 0px 0px %spx;"
        -- local tickDivstyleFormat = "position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"
        local offsetleft = 0;
        setOffset, setWidth = groupBounds(1)
        for i = 1, numValues do
            if not nulOrWhitespace(xlegends[i]) then
                table.insert(res, mw.text.tag('div', { style = string.format(legendDivStyleFormat, setWidth, offsetleft) }, xlegends[i] or ''))
                offsetleft=0;
            else
            	offsetleft=offsetleft+setWidth;
            end
            
                -- setOffset, setWidth = groupBounds(i)
                -- table.insert(res, mw.text.tag('div', { style = string.format(legendDivStyleFormat, setWidth) }, xlegends[i] or ''))
            
        end
    end




    function drawChart
        table.insert(res, mw.text.tag('div', { style = string.format("position:relative;padding:1em 0em 1em 0em;") })) -- container div
        table.insert(res, mw.text.tag('div', { style = string.format("position:relative;") })) -- container div
		table.insert(res, mw.text.tag('div', { style = string.format("position:relative;height:%spx;display:inline-block;text-align:right;vertical-align:top;", chartHeight) }))
        drawYScale
        table.insert(res, '</div><div style="position:relative;display:inline-block">')
        
        -- table.insert(res, mw.text.tag('div', { style = string.format("height:%spx;width:%spx;border-left:1px black solid;border-bottom:1px black solid;display:block;margin:0px;padding:0px;", chartHeight, chartWidth) })) -- the actual chart
        table.insert(res, mw.text.tag('svg', { style = string.format("height:%spx;width:%spx;border-left:1px black solid;border-bottom:1px black solid;", chartHeight, chartWidth) })) -- the actual chart
        local acum = stack and accumulateTooltip and {}
        for gi, group in pairs(values) do
            for i, val in ipairs(group) do
                if acum then acum[i] = (acum[i] or 0) + val end
                drawbar(gi, i, val, acum and acum[i])
            end
        end
        -- table.insert(res, '</div>')
        table.insert(res, '</svg>')
        
        table.insert(res, mw.text.tag('div', { style = string.format("position:relative;width:%spx;", chartWidth) })) -- X legends
        drawXlegends
        table.insert(res, '</div>')
        table.insert(res, '</div>')
        table.insert(res, '</div>')
        createGroupList(res, groupNames, colors)
        table.insert(res, '</div>')
    end

    extractParams
    validate
    calcHeightLimits
    drawChart
    return table.concat(res, "\n")
end

return {
    ['bar-chart'] = barChart,
    [keywords.barChart] = barChart,
}
--