Module:Chart: Difference between revisions

Content deleted Content added
No edit summary
Copyedit. Add number got to error message.
 
(79 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 = {}, {}, {}
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',
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 validate()
if start <= ( q - 1 ) * 0.25 then
function asGroups( name, tab, toDuplicate, emptyOK )
style = string.format( '%s;border:0;background-color:%s', style, color )
if #tab == 0 and not emptyOK then
else
error( "must supply values for " .. keywords[name] )
style = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )
end
end
if #tab == 1 and toDuplicate then
addRes( mw.text.tag( 'div', { style = style }, '' ) )
for i = 2, numGroups do tab[i] = tab[1] end
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 function createSlices()
-- do all sorts of validation here, so we can assume all params are good from now on.
local function coordsOfAngle( angle )
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
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()
chartHeight = height - 80
if #values == 0 then error( "no slices found - can't draw pie chart" ) end
numGroups = #values
addRes( mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:0.5em;max-width:%spx;', radius * 2 ) } ) )
numValues = #values[1]
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 ) } ) )
defcolor = defcolor or 'blue'
createSlices()
scaleWidth = scalePerGroup and 80 * numGroups or 100
addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
chartWidth = width -scaleWidth
createImageMap()
asGroups( 'unitsPrefix', unitsPrefix, true, true )
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
asGroups( 'unitsSuffix', unitsSuffix, true, true )
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
asGroups( 'colors', colors, true, true )
createGroupList( res, legends, colors ) -- legends
asGroups( 'groupNames', groupNames, false, false )
addRes( '</div>' ) -- close containing div
if stack and scalePerGroup then
return frame:preprocess( table.concat( res, '\n' ) )
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
 
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
 
local function barChart( frame )
for k, v in pairs( args ) do
local res = {}
if k == keywords.width then
local args = frame.args -- can be changed to frame:getParent().args
width = tonumber( v )
local values, xlegends, colors, tooltips, yscales = {}, {}, {}, {}, {}
if not width or width < 200 then
local groupNames, unitsSuffix, unitsPrefix, links = {}, {}, {}, {}
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
local width, height, yticks, stack, delimiter = 500, 350, -1, false, args.delimiter or ':'
end
local chartWidth, chartHeight, defcolor, scalePerGroup, accumulateTooltip
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,
} ) 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
 
local numGroups, numValues
function calcHeightLimits() -- if limits were passed by user, use ithem, otherwise calculate. for "stack" there's only one limet.
local scaleWidth
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 = roundup( 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 ) end
if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
end
 
local function tooltipvalidate( gi, i, val )
local function asGroups( name, tab, toDuplicate, emptyOK )
function nulOrWhitespace( s )
if #tab == 0 and not emptyOK then
return not s or mw.text.trim( s ) == ''
error( "must supply values for " .. keywords[name] )
end
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.
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i] end
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
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 groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix
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 = 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;",
end
left, top, barHeight, barWidth, barWidth, color)
elseif k == keywords.height then
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, "" ) )
height = tonumber( v )
end
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.
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 -- not ready yet/
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 tonumber(values[j][i]) > 0 then
setOffset, setWidth = groupBounds( i )
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
-- 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 - 10 ) }, '' ) )
end
return barHeight, top - base
end
end
 
local function groupBounds( i )
function printGroupList()
local setWidth = math.floor( chartWidth / numValues )
if #groupNames > 0 then
local setOffset = ( i - 1 ) * setWidth
local list = {}
return setOffset, setWidth
local spanStyle = "letter-spacing:4px;font-size:1.3em;color:white;background-color:%s;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;padding:3px 1em"
end
for gi = 1, #groupNames do
local span = mw.text.tag( 'span', { style = string.format( spanStyle, colors[gi]) }, groupNames[gi] )
table.insert( list, mw.text.tag( 'li', { style = "margin: 4px;" }, span ) )
end
table.insert( res, mw.text.tag( 'ul', {}, 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,
}