Module:Chart: Difference between revisions

Content deleted Content added
No edit summary
Copyedit. Add number got to error message.
 
(64 intermediate revisions by 20 users not shown)
Line 1:
--[[
keywords are used for languages: they are the names of the actual
next 100 lines or so are copied from mw.text package, which is not yet available.
parameters of the template
once this library is added to wmf distributions, the whole chunk should be removed
]]
u = require( "libraryUtil" )
mw = mw or {}
mw.text = mw.text or {}
local htmlencode_map = {
['>'] = '>',
['<'] = '&lt;',
['&'] = '&amp;',
['"'] = '&quot;',
["'"] = '&#039;',
['\194\160'] = '&#nbsp;',
}
local htmldecode_map = {}
for k, v in pairs( htmlencode_map ) do
htmldecode_map[v] = k
end
local decode_named_entities = nil
 
local keywords = {
function mw.text.encode( s, charset )
barChart = 'bar chart',
charset = charset or '<>&"\'\194\160'
pieChart = 'pie chart',
s = mw.ustring.gsub( s, '[' .. charset .. ']', function ( m )
width = 'width',
if not htmlencode_map[m] then
height = 'height',
local e = string.format( '&#%d;', mw.ustring.codepoint( m ) )
stack = 'stack',
htmlencode_map[m] = e
colors = 'colors',
htmldecode_map[e] = m
group = 'group',
end
xlegend = 'x legends',
return htmlencode_map[m]
yticks = 'y tick marks',
end )
tooltip = 'tooltip',
return s
accumulateTooltip = 'tooltip value accumulation',
end
links = 'links',
function mw.text.split( text, pattern, plain )
defcolor = 'default color',
local ret = {}
scalePerGroup = 'scale per group',
for m in gsplit( text, pattern, plain ) do
unitsPrefix = 'units prefix',
ret[#ret+1] = m
unitsSuffix = 'units suffix',
end
groupNames = 'group names',
return ret
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 mw.text.gsplitcreateGroupList( texttab, patternlegends, plaincols )
if #legends > 1 and not hideGroupLegends then
local s, l = 1, mw.ustring.len( text )
table.insert( tab, mw.text.tag( 'div' ) )
return function ()
local list = {}
if s then
local spanStyle = "padding:0 1em;background-color:%s;border:1px solid %s;margin-right:1em;-webkit-print-color-adjust:exact;"
local e, n = mw.ustring.find( text, pattern, s, plain )
for gi = 1, #legends do
local ret
local span = mw.text.tag( 'span', { style = string.format( spanStyle, cols[gi], cols[gi] ) }, '&nbsp;' ) .. ' '.. legends[gi]
if not e then
table.insert( list, mw.text.tag( 'li', {}, span ) )
ret = mw.ustring.sub( text, s )
end
s = nil
table.insert( tab,
elseif n < e then
mw.text.tag( 'ul',
-- Empty separator!
{style="list-style:none;column-width:12em;"},
ret = mw.ustring.sub( text, s, e )
table.concat( list, '\n' )
if e < l then
)
s = e + 1
)
else
table.insert( tab, '</div>' )
s = nil
end
end
else
ret = e > s and mw.ustring.sub( text, s, e - 1 ) or ''
s = n + 1
end
return ret
end
end, nil, nil
end
 
local function mw.text.tagpieChart( name, attrs, contentframe )
local res, imslices, args = {}, {}, frame.args
local named = false
local radius
if type( name ) == 'table' then
local values, colors, names, legends, links = {}, {}, {}, {}, {}
named = true
local delimiter = args.delimiter or ':'
name, attrs, content = name.name, name.attrs, name.content
local lang = mw.getContentLanguage()
u.checkTypeForNamedArg( 'tag', 'name', name, 'string' )
u.checkTypeForNamedArg( 'tag', 'attrs', attrs, 'table', true )
else
u.checkType( 'tag', 1, name, 'string' )
u.checkType( 'tag', 2, attrs, 'table', true )
end
 
local retfunction =getArg( {s, '<'def, ..subst, namewith })
local result = args[keywords[s]] or def or ''
for k, v in pairs( attrs or {} ) do
if subst and with then result = string.gsub( result, subst, with ) end
if type( k ) ~= 'string' then
return result
error( "bad named argument attrs to 'tag' (keys must be strings, found " .. type( k ) .. ")",
end
2 )
end
if string.match( k, '[\t\r\n\f /<>"\'=]' ) then
error( "bad named argument attrs to 'tag' (invalid key '" .. k .. "')", 2 )
end
local tp = type( v )
if tp == 'boolean' then
if v then
ret[#ret+1] = ' ' .. k
end
elseif tp == 'string' or tp == 'number' then
ret[#ret+1] = string.format( ' %s="%s"', k, mw.text.encode( tostring( v ) ) )
else
error( "bad named argument attrs to 'tag' (value for key '" .. k .. "' may not be " .. tp .. ")", 2 )
end
end
 
local tpfunction = typeanalyzeParams( content )
local function addSlice( i, slice )
if content == nil then
local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
ret[#ret+1] = '>'
values[i] = tonumber( lang:parseFormattedNumber( value ) )
elseif content == false then
or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', slice ) )
ret[#ret+1] = ' />'
colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]
elseif tp == 'string' or tp == 'number' then
ret names[#ret+1i] = name or '>'
links[i] = link
ret[#ret+1] = content
end
ret[#ret+1] = '</' .. name .. '>'
else
if named then
u.checkTypeForNamedArg( 'tag', 'content', content, 'string, number, nil, or false' )
else
u.checkType( 'tag', 3, content, 'string, number, nil, or false' )
end
end
 
radius = getArg( 'radius', 150 )
return table.concat( ret )
hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
end
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
function mw.text.trim( s )
local ind = return mw.ustringstring.match(s k, "'^' .. keywords.slice .. '%s*+(.-)%s*d+)$"' )
if ind then addSlice( tonumber( ind ), v ) end
end
end
 
for _, val in ipairs( values ) do sum = sum + val end
-- everything up to here should be removed once mw.text becomes avaulable.
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 barChartaddRes( frame... )
for _, v in localpairs( res{ =... {} ) do
table.insert( res, v )
local args = frame.args -- can be changed to frame:getParent().args
end
local values, xlegends, colors, tooltips, yscales = {}, {}, {}, {} ,{}, {}, {}
end
local groupNames, unitsSuffix, unitsPrefix, links = {}, {}, {}, {}
local width, height, stack, delimiter = 500, 350, false, ':'
local chartWidth, chartHeight, defcolor, scalePerGroup
 
local function createImageMap()
local keywords = {
addRes( '{{#tag:imagemap|', 'File:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )
width = 'width',
addRes( unpack( imslices ) )
height = 'height',
addRes( 'desc none', '}}' )
stack = 'stack',
end
colors = 'colors',
group = 'group',
xlegend = 'x legends',
tooltip = 'tooltip',
links = 'links',
defcolor = 'default color',
scalePerGroup = 'scale per group',
unitsPrefix = 'units prefix',
unitsSuffix = 'units suffix',
groupNames = 'group names',
} -- here is where you want to translate
 
local function drawSlice( i, q, start )
local numGroups, numValues
local color = colors[i]
local scaleWidth
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 )
function nulOrWhitespace( s )
if start <= ( q - 1 ) * 0.25 then
return not s or mw.text.trim( s ) == ''
style = string.format( '%s;border:0;background-color:%s', style, color )
end
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 validatecreateSlices()
local function asGroupscoordsOfAngle( name, tab, toDuplicate, emptyOKangle )
return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
if #tab == 0 and not emptyOK then
end
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
 
local sum, start = 0, 0
-- do all sorts of validation here, so we can assume all params are good from now on.
for _, value in ipairs( values ) do sum = sum + value end
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
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
 
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.', 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 exatly ' .. numValues ) end
end
 
local function barChart( frame )
function extractParams()
local res = {}
function testone( keyword, key, val, tab )
local args = frame.args -- can be changed to frame:getParent().args
i = keyword == key and 0 or key:match( keyword .. "%s+(%d+)" )
local values, xlegends, colors, tooltips, yscales = {}, {}, {}, {}, {}
if not i then return end
local groupNames, unitsSuffix, unitsPrefix, links = {}, {}, {}, {}
i = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
local width, height, yticks, stack, delimiter = 500, 350, -1, false, args.delimiter or ':'
if i > 0 then tab[i] = {} end
local chartWidth, chartHeight, defcolor, scalePerGroup, accumulateTooltip
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
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 numGroups, numValues
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 scaleWidth
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 validate()
function calcHeightLimits() -- if limits were passed by user, use ithem, otherwise calculate. for "stack" there's only one limet.
local function asGroups( name, tab, toDuplicate, emptyOK )
if stack then
if #tab == 0 and not emptyOK then
local sums = {}
error( "must supply values for " .. keywords[name] )
for _, group in pairs( values ) do
end
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
if #tab == 1 and toDuplicate then
end
for i = 2, numGroups do tab[i] = tab[1] end
local sum = math.max( unpack( sums ) )
end
for i = 1, #values do yscales[i] = sum end
if #tab > 0 and #tab ~= numGroups then
else
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')
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
end
end
for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale ) end
if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
end
 
-- do all sorts of validation here, so we can assume all params are good from now on.
function tooltip( gi, i, val )
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
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 )
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
 
chartHeight = height - 80
function groupBounds( i )
numGroups = #values
local setWidth = math.floor( chartWidth / numValues )
numValues = #values[1]
local setOffset = ( i - 1 ) * setWidth
defcolor = defcolor or 'blue'
return setOffset, setWidth
colors[1] = colors[1] or defcolor
end
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 .. ' not ' .. #xlegends) end
end
 
local function calcxextractParams( gi, i )
local function testone( keyword, key, val, tab )
local setOffset, setWidth = groupBounds( i )
local i = keyword == key and 0 or key:match( keyword .. "%s+(%d+)" )
setWidth = 0.85 * setWidth
if not i then return end
if stack then
i = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
if i > 0 then tab[i] = {} end
return setOffset + (setWidth - barWidth) / 2, barWidth
for s in mw.text.gsplit( val, '%s*' .. delimiter .. '%s*' ) do
end
table.insert( i == 0 and tab or tab[i], s )
local barWidth = math.floor( 0.75 * setWidth / numGroups )
end
local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )
return left, barWidthtrue
end
 
for k, v in pairs( args ) do
function drawbar( gi, i, val )
if k == keywords.width then
local color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, val )
width = tonumber( v )
local left, barWidth = calcx( gi, i )
if not width or width < 200 then
local barHeight, top = calcHeights( gi, i, val )
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
local style = string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;box-shadow:4px -3px 3px 1px grey;overflow:hidden;",
end
left, top, barHeight, barWidth, barWidth, color)
elseif k == keywords.height then
local link = links[gi] and links[gi][i] or ''
height = tonumber( v )
local img = not nulOrWhitespace( link ) and mw.ustring.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
if not height or height < 200 then
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
error( 'Illegal height value (must be a number, and at least 200): ' .. v )
end
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.
function drawYScale()
if stack then
function drawSingle( gi, color, width, single )
local yscalesums = yscales[gi]{}
for _, group in pairs( values ) do
local _, top, ordermag = roundup( yscale * 0.999 )
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
local numnotches = top <= 1.5 and top * 4
end
or top < 4 and top * 2
local sum = math.max( unpack( sums ) )
or top
for i = 1, #values do yscales[i] = sum end
local valStyleStr =
else
single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
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'
end
local notchStyleStr = 'position:absolute;height=1px;min-width:5px;top:%spx;left:%spx;border:1px solid %s;'
for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale * 0.9999 ) end
for i = 1, numnotches do
if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
local val = i / numnotches * yscale
end
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
 
local function tooltip( gi, i, val )
if scalePerGroup then
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true end
local colWidth = 80
local groupName = mw.text.killMarkers(not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or '')
local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
for gi = 1, numGroups do
local leftsuffix = ( unitsSuffix[gi] -or unitsSuffix[1] ) *or colWidth''
return string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
local color = colors[gi] or defcolor
end
table.insert( res, mw.text.tag( 'div', { style = string.format( colStyle, chartHeight, colWidth, left, color, color ) } ) )
drawSingle( gi, color, colWidth )
table.insert( res, '</div>' )
end
else
drawSingle( 1, 'black', scaleWidth, true )
end
end
 
local function calcHeights( gi, i, val )
function drawXlegends()
local barHeight = math.max( 2, math.floor( val / yscales[gi] * chartHeight + 0.5 ) ) -- add half to make it "round" instead of "trunc", min height to 2 to avoid negative bar sizes
local setOffset, setWidth
local top, base = chartHeight - barHeight, 0
local legendDivStyleFormat = "position:absolute;left:%spx;top:10px;min-width:%spx;max-width:%spx;text-align:center;veritical-align:top;"
if stack then
local tickDivstyleFormat = "position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"
for ij = 1, numValuesgi - 1 do
if not nulOrWhitespacetonumber( xlegendsvalues[j][i]) )> 0 then
base = base + math.max( 2, math.floor( values[j][i] / yscales[gi] * chartHeight + 0.5 ) ) -- sum the "i" value of all the groups below our group, gi, and keep the same calculation for each bar
setOffset, setWidth = groupBounds( i )
end
-- setWidth = 0.85 * setWidth
end
table.insert( res, mw.text.tag( 'div', { style = string.format( legendDivStyleFormat, setOffset - 5, setWidth - 10, setWidth - 10 ) }, xlegends[i] or '' ) )
end
table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) }, '' ) )
return barHeight, top - base
end
end
end
 
local function groupBounds( i )
function printGroupList()
local setWidth = math.floor( chartWidth / numValues )
if #groupNames > 1 then
local setOffset = ( i - 1 ) * setWidth
local list = {}
return setOffset, setWidth
local spanStyle = "padding:0 1em;background-color:%s;box-shadow:4px -3px 3px 1px grey;margin:0 1em;"
end
for gi = 1, #groupNames do
local span = mw.text.tag( 'span', { style = string.format( spanStyle, colors[gi] ) }, '&nbsp;' ) .. ' '.. groupNames[gi]
table.insert( list, mw.text.tag( 'li', {}, span ) )
end
table.insert( res, mw.text.tag( 'ul', {style="width:100%;list-style:none;-webkit-column-width:10em;-moz-column-width:10em;column-width:10em;"}, table.concat( list, '\n' ) ) )
end
end
 
local function calcx( gi, i )
function drawChart()
local setOffset, setWidth = groupBounds( i )
table.insert( res, mw.text.tag( 'div', { style = string.format( 'max-width:%spx;', width ) } ) )
if stack or numGroups == 1 then
table.insert( res, mw.text.tag( 'div', { style = string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;", height, width, width ) } ) )
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 )
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 ) } ) )
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 )
for gi, group in pairs( values ) do
local left, barWidth = calcx( gi, i )
for i, val in ipairs( group ) do
local barHeight, top = drawbarcalcHeights( gi, i, val )
end
end
 
-- borders so it shows up when printing
table.insert( res, '</div>' )
table.insert( res, mw.text.tag( 'div', { 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 chartHeight,solid scaleWidth%s;border-bottom:none;overflow:hidden;", scaleWidth, scaleWidth ) } ) )
left, top, barHeight-1, barWidth-2, barWidth-2, color, color)
drawYScale()
local link = links[gi] and links[gi][i] or ''
table.insert( res, '</div>' )
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 = string.format( "position:absolute;top:%spx;left:%spx;width:%spx;", chartHeight, scaleWidth, chartWidth ) } ) )
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
drawXlegends()
end
table.insert( res, '</div>' )
table.insert( res, '</div>' )
printGroupList()
table.insert( res, '</div>' )
end
 
 
extractParams()
local function drawYScale()
validate()
local function drawSingle( gi, color, width, yticks, single )
calcHeightLimits()
local yscale = yscales[gi]
drawChart()
local _, top, ordermag = roundup( yscale * 0.999 )
return table.concat( res, "\n" )
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 + 1, setWidth - 2, setWidth - 2 ) }, 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( 'padding-top:10px;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,
}