Module:Chart: Difference between revisions

From Semantic Name Authority Repository Cymru
Jump to navigation Jump to search
(Created page with "--<source lang=lua> --keywords are used for languages: they are the names of the actual parameters of the template local keywords = { barChart = 'bar chart', pieChart = 'pie 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',...")
 
No edit summary
 
Line 1: Line 1:
--<source lang=lua>
--[[
--[[
    keywords are used for languages: they are the names of the actual
keywords are used for languages: they are the names of the actual
    parameters of the template
parameters of the template
]]
]]


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


} -- here is what you want to translate
} -- here is what you want to translate


local defColors = require "Module:Plotter/DefaultColors"
local defColors = mw.loadData("Module:Chart/Default colors")
local hideGroupLegends
local hideGroupLegends


local function nulOrWhitespace( s )
local function nulOrWhitespace( s )
    return not s or mw.text.trim( s ) == ''
return not s or mw.text.trim( s ) == ''
end
end


local function createGroupList( tab, legends, cols )
local function createGroupList( tab, legends, cols )
    if #legends > 1 and not hideGroupLegends then
if #legends > 1 and not hideGroupLegends then
        table.insert( tab, mw.text.tag( 'div' ) )
table.insert( tab, mw.text.tag( 'div' ) )
        local list = {}
local list = {}
        local spanStyle = "padding:0 1em;background-color:%s;box-shadow:2px -1px 4px 0 silver;margin-right:1em;"
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
for gi = 1, #legends do
            local span = mw.text.tag( 'span', { style = string.format( spanStyle, cols[gi] ) }, '&nbsp;' ) .. ' '..  legends[gi]
local span = mw.text.tag( 'span', { style = string.format( spanStyle, cols[gi], cols[gi] ) }, '&nbsp;' ) .. ' '..  legends[gi]
            table.insert( list, mw.text.tag( 'li', {}, span ) )
table.insert( list, mw.text.tag( 'li', {}, span ) )
        end
end
        table.insert( tab,
table.insert( tab,
            mw.text.tag( 'ul',
mw.text.tag( 'ul',
                {style="width:100%;list-style:none;-webkit-column-width:12em;-moz-column-width:12em;column-width:12em;"},
{style="width:100%;list-style:none;column-width:12em;"},
                table.concat( list, '\n' )
table.concat( list, '\n' )
            )
)
        )
)
        table.insert( tab, '</div>' )
table.insert( tab, '</div>' )
    end
end
end
end


function pieChart( frame )
local function pieChart( frame )
    local res, imslices, args = {}, {}, frame.args
local res, imslices, args = {}, {}, frame.args
    local radius
local radius
    local values, colors, names, legends, links = {}, {}, {}, {}, {}
local values, colors, names, legends, links = {}, {}, {}, {}, {}
    local delimiter = args.delimiter or ':'
local delimiter = args.delimiter or ':'
    local lang = mw.getContentLanguage()
local lang = mw.getContentLanguage()


    function getArg( s, def, subst, with )
local function getArg( s, def, subst, with )
        local result = args[keywords[s]] or def or ''
local result = args[keywords[s]] or def or ''
        if subst and with then result = mw.ustring.gsub( result, subst, with ) end
if subst and with then result = string.gsub( result, subst, with ) end
        return result
return result
    end
end


    function analyzeParams()
local function analyzeParams()
        function addSlice( i, slice )
local function addSlice( i, slice )
            local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
            values[i] = tonumber( lang:parseFormattedNumber( value ) )
values[i] = tonumber( lang:parseFormattedNumber( value ) )
                or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', sliceStr ) )
or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', slice ) )
            colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]
colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]
            names[i] = name or ''
names[i] = name or ''
            links[i] = link
links[i] = link
        end
end
       
        radius = getArg( 'radius', 150 )
        hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
        local slicesStr = getArg( 'slices' )
        local prefix = getArg( 'unitsPrefix', '', '_', ' ' )
        local suffix = getArg( 'unitsSuffix', '', '_', ' ' )
        local percent = args[keywords.percent]
        local sum = 0
        local i, value = 0
        for slice in mw.ustring.gmatch( slicesStr or '', "%b()" ) do
            i = i + 1
            addSlice( i, mw.ustring.match( slice, '^%(%s*(.-)%s*%)$' ) )
        end
       
        for k, v in pairs(args) do
            local ind = mw.ustring.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )
            if ind then addSlice( tonumber( ind ), v ) end
        end
       
        for _, val in ipairs( values ) do sum = sum + val end
        for i, value in ipairs( values ) do
            local addprec = percent and string.format( ' (%0.1f%%)', value / sum * 100 ) or ''
            legends[i] = mw.ustring.format( '%s: %s%s%s%s', names[i], prefix, lang:formatNum( value ), suffix, addprec )
            links[i] = mw.text.trim( links[i] or mw.ustring.format( '[[#noSuchAnchor|%s]]', legends[i] ) )
        end
    end


    function addRes( ... )
radius = getArg( 'radius', 150 )
        for _, v in pairs( { ... } ) do
hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
            table.insert( res, v )
local slicesStr = getArg( 'slices' )
        end
local prefix = getArg( 'unitsPrefix', '', '_', ' ' )
    end
local suffix = getArg( 'unitsSuffix', '', '_', ' ' )
local percent = args[keywords.percent]
local sum = 0
local i = 0
for slice in string.gmatch( slicesStr or '', "%b()" ) do
i = i + 1
addSlice( i, string.match( slice, '^%(%s*(.-)%s*%)$' ) )
end


    function createImageMap()
for k, v in pairs(args) do
        addRes( '{{#tag:imagemap|', 'Image:Circle frame white.svg{{!}}' .. ( radius * 2 ) .. 'px' )
local ind = string.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )
        addRes( unpack( imslices ) )
if ind then addSlice( tonumber( ind ), v ) end
        addRes( 'desc none', '}}' )
end
    end


    function drawSlice( i, q, start )
for _, val in ipairs( values ) do sum = sum + val end
        local color = colors[i]
for i, value in ipairs( values ) do
        local angle = start * 2 * math.pi
local addprec = percent and string.format( ' (%0.1f%%)', value / sum * 100 ) or ''
        local sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )
legends[i] = string.format( '%s: %s%s%s%s', names[i], prefix, lang:formatNum( value ), suffix, addprec )
        local wsin, wcos = sin * radius, cos * radius
links[i] = mw.text.trim( links[i] or string.format( '[[#noSuchAnchor|%s]]', legends[i] ) )
        local s1, s2, w1, w2, w3, w4, width, border
end
        local style
end
        if q == 1 then
            border = 'left'
            w1, w2, w3, w4 = 0, 0, wsin, wcos
            s1, s2 = 'bottom', 'left'
        elseif q == 2 then
            border = 'bottom'
            w1, w2, w3, w4 = 0, wcos, wsin, 0
            s1, s2 = 'bottom', 'right'
        elseif q == 3 then
            border = 'right'
            w1, w2, w3, w4 = wsin, wcos, 0, 0
            s1, s2 = 'top', 'right'
        else
            border = 'top'
            w1, w2, w3, w4 = wsin, 0, 0, wcos
            s1, s2 = 'top', 'left'
        end


        local style = string.format( 'position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx', s1, radius, s2, radius, radius, radius )
local function addRes( ... )
        if start <= ( q - 1 ) * 0.25 then
for _, v in pairs( { ... } ) do
            style = string.format( '%s;border:0;background-color:%s', style, color )
table.insert( res, v )
        else
end
            style = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )
end
        end
        addRes( mw.text.tag( 'div', { class = 'transborder', style = style }, '' ) )
    end


    function createSlices()
local function createImageMap()
        function coordsOfAngle( angle )
addRes( '{{#tag:imagemap|', 'File:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )
            return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
addRes( unpack( imslices ) )
        end
addRes( 'desc none', '}}' )
end


        local sum, start = 0, 0
local function drawSlice( i, q, start )
        for _, value in ipairs( values ) do sum = sum + value end
local color = colors[i]
        for i, value in ipairs(values) do
local angle = start * 2 * math.pi
            local poly = { 'poly 100 100' }
local sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )
            local startC, endC = start / sum, ( start + value ) / sum
local wsin, wcos = sin * radius, cos * radius
            local startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )
local s1, s2, w1, w2, w3, w4, border
            for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) end
if q == 1 then
            for angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 do
border = 'left'
                table.insert( poly, coordsOfAngle( angle ) )
w1, w2, w3, w4 = 0, 0, wsin, wcos
            end
s1, s2 = 'bottom', 'left'
            table.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )
elseif q == 2 then
            table.insert( imslices, table.concat( poly, ' ' ) )
border = 'bottom'
            start = start + values[i]
w1, w2, w3, w4 = 0, wcos, wsin, 0
        end
s1, s2 = 'bottom', 'right'
    end
elseif q == 3 then
border = 'right'
w1, w2, w3, w4 = wsin, wcos, 0, 0
s1, s2 = 'top', 'right'
else
border = 'top'
w1, w2, w3, w4 = wsin, 0, 0, wcos
s1, s2 = 'top', 'left'
end


    analyzeParams()
local style = string.format( 'border:solid transparent;position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx', s1, radius, s2, radius, radius, radius )
    if #values == 0 then error( "no slices found - can't draw pie chart" ) end
if start <= ( q - 1 ) * 0.25 then
    addRes( mw.text.tag( 'div', { style = string.format( "max-width:%spx", radius * 2 ) } ) )
style = string.format( '%s;border:0;background-color:%s', style, color )
    addRes( mw.text.tag( 'div', { style = string.format( 'position:relative;min-width:%spx;min-height:%spx;max-width:%spx;overflow:hidden;', radius * 2, radius * 2, radius * 2 ) } ) )
else
    createSlices()
style = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )
    addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
end
    createImageMap()
addRes( mw.text.tag( 'div', { style = style }, '' ) )
    addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
end
    addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
 
    createGroupList( res, legends, colors ) -- legends
local function createSlices()
    addRes( '</div>' ) -- close containing div
local function coordsOfAngle( angle )
    return frame:preprocess( table.concat( res, '\n' ) )
return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
end
 
local sum, start = 0, 0
for _, value in ipairs( values ) do sum = sum + value end
for i, value in ipairs(values) do
local poly = { 'poly 100 100' }
local startC, endC =  start / sum, ( start + value ) / sum
local startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )
for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) end
for angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 do
table.insert( poly,  coordsOfAngle( angle ) )
end
table.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )
table.insert( imslices, table.concat( poly, ' ' ) )
start = start + values[i]
end
end
 
analyzeParams()
if #values == 0 then error( "no slices found - can't draw pie chart" ) end
addRes( mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:0.5em;max-width:%spx;', radius * 2 ) } ) )
addRes( mw.text.tag( 'div', { style = string.format( 'position:relative;min-width:%spx;min-height:%spx;max-width:%spx;overflow:hidden;', radius * 2, radius * 2, radius * 2 ) } ) )
createSlices()
addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
createImageMap()
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
createGroupList( res, legends, colors ) -- legends
addRes( '</div>' ) -- close containing div
return frame:preprocess( table.concat( res, '\n' ) )
end
end




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


local function validate()
local 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


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


    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] .. ' should contain the same number of items as the number of groups (' .. numGroups .. ')')
            end
        end


        -- do all sorts of validation here, so we can assume all params are good from now on.
chartHeight = height - 80
        -- among other things, replace numerical values with mw.language:parseFormattedNumber() result
numGroups = #values
numValues = #values[1]
defcolor = defcolor or 'blue'
colors[1] = colors[1] or defcolor
scaleWidth = scalePerGroup and 80 * numGroups or 100
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.', keywords.stack, keywords.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


local function extractParams()
local function testone( keyword, key, val, tab )
local 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


        chartHeight = height - 80
for k, v in pairs( args ) do
        numGroups = #values
if k == keywords.width then
        numValues = #values[1]
width = tonumber( v )
        defcolor = defcolor or 'blue'
if not width or width < 200 then
        colors[1] = colors[1] or defcolor
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
        scaleWidth = scalePerGroup and 80 * numGroups or 100
end
        chartWidth = width -scaleWidth
elseif k == keywords.height then
        asGroups( 'unitsPrefix', unitsPrefix, true, true )
height = tonumber( v )
        asGroups( 'unitsSuffix', unitsSuffix, true, true )
if not height or height < 200 then
        asGroups( 'colors', colors, true, true )
error( 'Illegal height value (must be a number, and at least 200): ' .. v )
        asGroups( 'groupNames', groupNames, false, false )
end
        if stack and scalePerGroup then
elseif k == keywords.stack then stack = true
            error( string.format( 'Illegal settings: %s and %s are incompatible.', keyword.stack, keyword.scalePerGroup ) )
elseif k == keywords.yticks then yticks = tonumber(v) or -1
        end
elseif k == keywords.scalePerGroup then scalePerGroup = true
        for gi = 2, numGroups do
elseif k == keywords.defcolor then defcolor = v
            if #values[gi] ~= numValues then error( keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1" ) end
elseif k == keywords.accumulateTooltip then accumulateTooltip = not nulOrWhitespace( v )
        end
elseif k == keywords.hideGroupLegends then hideGroupLegends = not nulOrWhitespace( v )
        if #xlegends ~= numValues then error( 'Illegal number of ' .. keywords.xlegend .. '. Should be exactly ' .. numValues ) end
else
    end
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 extractParams()
local 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.
        function testone( keyword, key, val, tab )
local ordermag = 10 ^ math.floor( math.log10( x ) )
            i = keyword == key and 0 or key:match( keyword .. "%s+(%d+)" )
local normalized = x /  ordermag
            if not i then return end
local top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5
            i = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
return ordermag * top, top, ordermag
            if i > 0 then tab[i] = {} end
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
local function calcHeightLimits() -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.
            if k == keywords.width then
if stack then
                width = tonumber( v )
local sums = {}
                if not width or width < 200 then
for _, group in pairs( values ) do
                    error( 'Illegal width value (must be a number, and at least 200): ' .. v )
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
                end
end
            elseif k == keywords.height then
local sum = math.max( unpack( sums ) )
                height = tonumber( v )
for i = 1, #values do yscales[i] = sum end
                if not height or height < 200 then
else
                    error( 'Illegal height value (must be a number, and at least 200): ' .. v )
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
                end
end
            elseif k == keywords.stack then stack = true
for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale * 0.9999 ) end
            elseif k == keywords.scalePerGroup then scalePerGroup = true
if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
            elseif k == keywords.defcolor then defcolor = v
end
            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 function tooltip( gi, i, val )
        local ordermag = 10 ^ math.floor( math.log10( x ) )
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true end
        local normalized = x /  ordermag
local groupName = mw.text.killMarkers(not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or '')
        local top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
        return ordermag * top, top, ordermag
local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
    end
return string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
end


    function calcHeightLimits() -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.
local function calcHeights( gi, i, val )
        if stack then
local barHeight = math.floor( val / yscales[gi] * chartHeight + 0.5 ) -- add half to make it "round" instead of "trunc"
            local sums = {}
local top, base = chartHeight - barHeight, 0
            for _, group in pairs( values ) do
if stack then
                for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
local rawbase = 0
            end
for j = 1, gi - 1 do rawbase = rawbase + values[j][i] end -- sum the "i" value of all the groups below our group, gi.
            local sum = math.max( unpack( sums ) )
base = math.floor( chartHeight * rawbase / yscales[gi] ) -- normally, and especially if it's "stack", all the yscales must be equal.
            for i = 1, #values do yscales[i] = sum end
end
        else
if barHeight < 2 then
            for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
barHeight = 2 -- Otherwise the template would try to create a bar with a negative height
        end
end
        for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale * 0.9999 ) end
return barHeight, top - base
        if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
end
    end


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


    function calcHeights( gi, i, val )
local function calcx( gi, i )
        local barHeight = math.floor( val / yscales[gi] * chartHeight + 0.5 ) -- add half to make it "round" insstead of "trunc"
local setOffset, setWidth = groupBounds( i )
        local top, base = chartHeight - barHeight, 0
if stack or numGroups == 1 then
        if stack then
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
            local rawbase = 0
return setOffset + (setWidth - barWidth) / 2, barWidth
            for j = 1, gi - 1 do rawbase = rawbase + values[j][i] end -- sum the "i" value of all the groups below our group, gi.
end
            base = math.floor( chartHeight * rawbase / yscales[gi] ) -- normally, and especially if it's "stack", all the yscales must be equal.
setWidth = 0.85 * setWidth
        end
local barWidth = math.floor( 0.75 * setWidth / numGroups )
        return barHeight, top - base
local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )
    end
return left, barWidth
end


    function groupBounds( i )
local function drawbar( gi, i, val, ttval )
        local setWidth = math.floor( chartWidth / numValues )
if val == '0' then return end -- do not show single line (borders....) if value is 0, or rather, '0'. see talkpage
        local setOffset = ( i - 1 ) * setWidth
        return setOffset, setWidth
    end


    function calcx( gi, i )
local color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, ttval or val )
        local setOffset, setWidth = groupBounds( i )
local left, barWidth = calcx( gi, i )
        if stack or numGroups == 1 then
local barHeight, top = calcHeights( gi, i, val )
            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 )
-- borders so it shows up when printing
        local color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, ttval or val )
local style = string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;-webkit-print-color-adjust:exact;border:1px solid %s;border-bottom:none;overflow:hidden;",
        local left, barWidth = calcx( gi, i )
left, top, barHeight-1, barWidth-2, barWidth-2, color, color)
        local barHeight, top = calcHeights( gi, i, val )
local link = links[gi] and links[gi][i] or ''
        local style = string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;box-shadow:2px -1px 4px 0 silver;overflow:hidden;",
local img = not nulOrWhitespace( link ) and string.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
                        left, top, barHeight, barWidth, barWidth, color)
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
        local link = links[gi] and links[gi][i] or ''
end
        local img = not nulOrWhitespace( link ) and mw.ustring.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
        table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
    end




    function drawYScale()
local function drawYScale()
        function drawSingle( gi, color, width, single )
local function drawSingle( gi, color, width, yticks, single )
            local yscale = yscales[gi]
local yscale = yscales[gi]
            local _, top, ordermag = roundup( yscale * 0.999 )
local _, top, ordermag = roundup( yscale * 0.999 )
            local numnotches = top <= 1.5 and top * 4
local numnotches = yticks >= 0 and yticks or
                    or top < 4  and top * 2
(top <= 1.5 and top * 4
                    or top
or top < 4  and top * 2
            local valStyleStr =
or top)
                single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
local valStyleStr =
                or 'position:absolute;height=20px;text-align:right;vertical-align:middle;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'
single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
            local notchStyleStr = 'position:absolute;height=1px;min-width:5px;top:%spx;left:%spx;border:1px solid %s;'
or 'position:absolute;height=20px;text-align:right;vertical-align:middle;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'
            for i = 1, numnotches do
local notchStyleStr = 'position:absolute;height=1px;min-width:5px;top:%spx;left:%spx;border:1px solid %s;'
                local val = i / numnotches * yscale
for i = 1, numnotches do
                local y = chartHeight - calcHeights( gi, 1, val )
local val = i / numnotches * yscale
                local div = mw.text.tag( 'div', { style = string.format( valStyleStr, width - 10, y - 10, color ) }, mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) )
local y = chartHeight - calcHeights( gi, 1, val )
                table.insert( res, div )
local div = mw.text.tag( 'div', { style = string.format( valStyleStr, width - 10, y - 10, color ) }, mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) )
                div = mw.text.tag( 'div', { style = string.format( notchStyleStr, y, width - 4, color ) }, '' )
table.insert( res, div )
                table.insert( res, div )
div = mw.text.tag( 'div', { style = string.format( notchStyleStr, y, width - 4, color ) }, '' )
            end
table.insert( res, div )
        end
end
end


        if scalePerGroup then
if scalePerGroup then
            local colWidth = 80
local colWidth = 80
            local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"
local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"
            for gi = 1, numGroups do
for gi = 1, numGroups do
                local left = ( gi - 1 ) * colWidth
local left = ( gi - 1 ) * colWidth
                local color = colors[gi] or defcolor
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, colWidth, left, color, color ) } ) )
                drawSingle( gi, color, colWidth )
drawSingle( gi, color, colWidth, yticks )
                table.insert( res, '</div>' )
table.insert( res, '</div>' )
            end
end
        else
else
            drawSingle( 1, 'black', scaleWidth, true )
drawSingle( 1, 'black', scaleWidth, yticks, true )
        end
end
    end
end


    function drawXlegends()
local function drawXlegends()
        local setOffset, setWidth
local setOffset, setWidth
        local legendDivStyleFormat = "position:absolute;left:%spx;top:10px;min-width:%spx;max-width:%spx;text-align:center;veritical-align:top;"
local legendDivStyleFormat = "position:absolute;left:%spx;top:10px;min-width:%spx;max-width:%spx;text-align:center;vertical-align:top;"
        local tickDivstyleFormat = "position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"
local tickDivstyleFormat = "position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"
        for i = 1, numValues do
for i = 1, numValues do
            if not nulOrWhitespace( xlegends[i] ) then
if not nulOrWhitespace( xlegends[i] ) then
                setOffset, setWidth = groupBounds( i )
setOffset, setWidth = groupBounds( i )
                -- setWidth = 0.85 * setWidth
-- setWidth = 0.85 * setWidth
                table.insert( res, mw.text.tag( 'div', { style = string.format( legendDivStyleFormat, setOffset + 5, setWidth - 10, setWidth - 10 ) }, xlegends[i] or '' ) )
table.insert( res, mw.text.tag( 'div', { style = string.format( legendDivStyleFormat, setOffset + 5, setWidth - 10, setWidth - 10 ) }, xlegends[i] or '' ) )
                table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) }, '' ) )
table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) }, '' ) )
            end
end
        end
end
    end
end


    function drawChart()
local function drawChart()
        table.insert( res, mw.text.tag( 'div', { style = string.format( 'max-width:%spx;', width ) } ) )
table.insert( res, mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:1em;max-width:%spx;', width ) } ) )
        table.insert( res, mw.text.tag( 'div', { style = string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;", height, width, width ) } ) )
table.insert( res, mw.text.tag( 'div', { style = string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;", height, width, width ) } ) )


        table.insert( res, mw.text.tag( 'div', { style = string.format("float:right;position:relative;min-height:%spx;min-width:%spx;max-width:%spx;border-left:1px black solid;border-bottom:1px black solid;", chartHeight, chartWidth, chartWidth ) } ) )
table.insert( res, mw.text.tag( 'div', { style = string.format("float:right;position:relative;min-height:%spx;min-width:%spx;max-width:%spx;border-left:1px black solid;border-bottom:1px black solid;", chartHeight, chartWidth, chartWidth ) } ) )
        local acum = stack and accumulateTooltip and {}
local acum = stack and accumulateTooltip and {}
        for gi, group in pairs( values ) do
for gi, group in pairs( values ) do
            for i, val in ipairs( group ) do
for i, val in ipairs( group ) do
                if acum then acum[i] = ( acum[i] or 0 ) + val end
if acum then acum[i] = ( acum[i] or 0 ) + val end
                drawbar( gi, i, val, acum and acum[i] )
drawbar( gi, i, val, acum and acum[i] )
            end
end
        end
end
        table.insert( res, '</div>' )
table.insert( res, '</div>' )
        table.insert( res, mw.text.tag( 'div', { style = string.format("position:absolute;height:%spx;min-width:%spx;max-width:%spx;", chartHeight, scaleWidth, scaleWidth, scaleWidth ) } ) )
table.insert( res, mw.text.tag( 'div', { style = string.format("position:absolute;height:%spx;min-width:%spx;max-width:%spx;", chartHeight, scaleWidth, scaleWidth, scaleWidth ) } ) )
        drawYScale()
drawYScale()
        table.insert( res, '</div>' )
table.insert( res, '</div>' )
        table.insert( res, mw.text.tag( 'div', { style = string.format( "position:absolute;top:%spx;left:%spx;width:%spx;", chartHeight, scaleWidth, chartWidth ) } ) )
table.insert( res, mw.text.tag( 'div', { style = string.format( "position:absolute;top:%spx;left:%spx;width:%spx;", chartHeight, scaleWidth, chartWidth ) } ) )
        drawXlegends()
drawXlegends()
        table.insert( res, '</div>' )
table.insert( res, '</div>' )
        table.insert( res, '</div>' )
table.insert( res, '</div>' )
        createGroupList( res, groupNames, colors )
createGroupList( res, groupNames, colors )
        table.insert( res, '</div>' )
table.insert( res, '</div>' )
    end
end


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


return {
return {
    ['bar-chart'] = barChart,
['bar-chart'] = barChart,
    [keywords.barChart] = barChart,
[keywords.barChart] = barChart,
    [keywords.pieChart] = pieChart,
[keywords.pieChart] = pieChart,
}
}
--</source>

Latest revision as of 14:18, 20 January 2023

Documentation for this module may be created at Module:Chart/doc

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

local keywords = {
	barChart = 'bar chart',
	pieChart = 'pie chart',
	width = 'width',
	height = 'height',
	stack = 'stack',
	colors = 'colors',
	group = 'group',
	xlegend = 'x legends',
	yticks = 'y tick marks',
	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

local defColors = mw.loadData("Module:Chart/Default colors")
local hideGroupLegends

local function nulOrWhitespace( s )
	return not s or mw.text.trim( s ) == ''
end

local function createGroupList( tab, legends, cols )
	if #legends > 1 and not hideGroupLegends then
		table.insert( tab, mw.text.tag( 'div' ) )
		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] ) }, '&nbsp;' ) .. ' '..  legends[gi]
			table.insert( list, mw.text.tag( 'li', {}, span ) )
		end
		table.insert( tab,
			mw.text.tag( 'ul',
				{style="width:100%;list-style:none;column-width:12em;"},
				table.concat( list, '\n' )
			)
		)
		table.insert( tab, '</div>' )
	end
end

local function pieChart( frame )
	local res, imslices, args = {}, {}, frame.args
	local radius
	local values, colors, names, legends, links = {}, {}, {}, {}, {}
	local delimiter = args.delimiter or ':'
	local lang = mw.getContentLanguage()

	local function getArg( s, def, subst, with )
		local result = args[keywords[s]] or def or ''
		if subst and with then result = string.gsub( result, subst, with ) end
		return result
	end

	local function analyzeParams()
		local function addSlice( i, slice )
			local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
			values[i] = tonumber( lang:parseFormattedNumber( value ) )
				or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', slice ) )
			colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]
			names[i] = name or ''
			links[i] = link
		end

		radius = getArg( 'radius', 150 )
		hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
		local slicesStr = getArg( 'slices' )
		local prefix = getArg( 'unitsPrefix', '', '_', ' ' )
		local suffix = getArg( 'unitsSuffix', '', '_', ' ' )
		local percent = args[keywords.percent]
		local sum = 0
		local i = 0
		for slice in string.gmatch( slicesStr or '', "%b()" ) do
			i = i + 1
			addSlice( i, string.match( slice, '^%(%s*(.-)%s*%)$' ) )
		end

		for k, v in pairs(args) do
			local ind = string.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )
			if ind then addSlice( tonumber( ind ), v ) end
		end

		for _, val in ipairs( values ) do sum = sum + val end
		for i, value in ipairs( values ) do
			local addprec = percent and string.format( ' (%0.1f%%)', value / sum * 100 ) or ''
			legends[i] = string.format( '%s: %s%s%s%s', names[i], prefix, lang:formatNum( value ), suffix, addprec )
			links[i] = mw.text.trim( links[i] or string.format( '[[#noSuchAnchor|%s]]', legends[i] ) )
		end
	end

	local function addRes( ... )
		for _, v in pairs( { ... } ) do
			table.insert( res, v )
		end
	end

	local function createImageMap()
		addRes( '{{#tag:imagemap|', 'File:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )
		addRes( unpack( imslices ) )
		addRes( 'desc none', '}}' )
	end

	local function drawSlice( i, q, start )
		local color = colors[i]
		local angle = start * 2 * math.pi
		local sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )
		local wsin, wcos = sin * radius, cos * radius
		local s1, s2, w1, w2, w3, w4, border
		if q == 1 then
			border = 'left'
			w1, w2, w3, w4 = 0, 0, wsin, wcos
			s1, s2 = 'bottom', 'left'
		elseif q == 2 then
			border = 'bottom'
			w1, w2, w3, w4 = 0, wcos, wsin, 0
			s1, s2 = 'bottom', 'right'
		elseif q == 3 then
			border = 'right'
			w1, w2, w3, w4 = wsin, wcos, 0, 0
			s1, s2 = 'top', 'right'
		else
			border = 'top'
			w1, w2, w3, w4 = wsin, 0, 0, wcos
			s1, s2 = 'top', 'left'
		end

		local style = string.format( 'border:solid transparent;position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx', s1, radius, s2, radius, radius, radius )
		if start <= ( q - 1 ) * 0.25 then
			style = string.format( '%s;border:0;background-color:%s', style, color )
		else
			style = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )
		end
		addRes( mw.text.tag( 'div', { style = style }, '' ) )
	end

	local function createSlices()
		local function coordsOfAngle( angle )
			return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
		end

		local sum, start = 0, 0
		for _, value in ipairs( values ) do sum = sum + value end
		for i, value in ipairs(values) do
			local poly = { 'poly 100 100' }
			local startC, endC =  start / sum, ( start + value ) / sum
			local startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )
			for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) end
			for angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 do
				table.insert( poly,  coordsOfAngle( angle ) )
			end
			table.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )
			table.insert( imslices, table.concat( poly, ' ' ) )
			start = start + values[i]
		end
	end

	analyzeParams()
	if #values == 0 then error( "no slices found - can't draw pie chart" ) end
	addRes( mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:0.5em;max-width:%spx;', radius * 2 ) } ) )
	addRes( mw.text.tag( 'div', { style = string.format( 'position:relative;min-width:%spx;min-height:%spx;max-width:%spx;overflow:hidden;', radius * 2, radius * 2, radius * 2 ) } ) )
	createSlices()
	addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
	createImageMap()
	addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
	addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
	createGroupList( res, legends, colors ) -- legends
	addRes( '</div>' ) -- close containing div
	return frame:preprocess( table.concat( res, '\n' ) )
end


local 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, yticks, stack, delimiter = 500, 350, -1, false, args.delimiter or ':'
	local chartWidth, chartHeight, defcolor, scalePerGroup, accumulateTooltip


	local numGroups, numValues
	local scaleWidth

	local function validate()
		local 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 100
		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.', keywords.stack, keywords.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

	local function extractParams()
		local function testone( keyword, key, val, tab )
			local 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.yticks then yticks = tonumber(v) or -1
			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

	local 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

	local 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

	local 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 = mw.text.killMarkers(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 string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
	end

	local 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
		if barHeight < 2 then
			barHeight = 2 -- Otherwise the template would try to create a bar with a negative height
		end 
		return barHeight, top - base
	end

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

	local 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

	local 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("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;-webkit-print-color-adjust:exact;border:1px solid %s;border-bottom:none;overflow:hidden;",
						left, top, barHeight-1, barWidth-2, barWidth-2, color, color)
		local link = links[gi] and links[gi][i] or ''
		local img = not nulOrWhitespace( link ) and string.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
		table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
	end


	local function drawYScale()
		local function drawSingle( gi, color, width, yticks, single )
			local yscale = yscales[gi]
			local _, top, ordermag = roundup( yscale * 0.999 )
			local numnotches = yticks >= 0 and yticks or
					(top <= 1.5 and top * 4
					or top < 4  and top * 2
					or top)
			local valStyleStr =
				single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
				or 'position:absolute;height=20px;text-align:right;vertical-align:middle;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 = i / numnotches * yscale
				local y = chartHeight - calcHeights( gi, 1, val )
				local div = mw.text.tag( 'div', { style = string.format( valStyleStr, width - 10, y - 10, color ) }, mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) )
				table.insert( res, 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"
			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 ) } ) )
				drawSingle( gi, color, colWidth, yticks )
				table.insert( res, '</div>' )
			end
		else
			drawSingle( 1, 'black', scaleWidth, yticks, true )
		end
	end

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

	local function drawChart()
		table.insert( res, mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:1em;max-width:%spx;', width ) } ) )
		table.insert( res, mw.text.tag( 'div', { style = string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;", height, width, width ) } ) )

		table.insert( res, mw.text.tag( 'div', { style = string.format("float:right;position:relative;min-height:%spx;min-width:%spx;max-width:%spx;border-left:1px black solid;border-bottom:1px black solid;", chartHeight, chartWidth, chartWidth ) } ) )
		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, mw.text.tag( 'div', { style = string.format("position:absolute;height:%spx;min-width:%spx;max-width:%spx;", chartHeight, scaleWidth, scaleWidth, scaleWidth ) } ) )
		drawYScale()
		table.insert( res, '</div>' )
		table.insert( res, mw.text.tag( 'div', { style = string.format( "position:absolute;top:%spx;left:%spx;width:%spx;", chartHeight, scaleWidth, chartWidth ) } ) )
		drawXlegends()
		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,
	[keywords.pieChart] = pieChart,
}