Module:Chart: Difference between revisions

Content deleted Content added
Created page with '--[[ next 100 lines or so are copied from mw.text package, which is not yet available. once this library is added to wmf distributions, all functions and variabl...'
 
Copyedit. Add number got to error message.
 
(89 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, all functions and variables that look lke "mw.text.XXX
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
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()
function mw.text.trim( s )
if #values == 0 then error( "no slices found - can't draw pie chart" ) end
return mw.ustring.match(s, "^%s*(.-)%s*$")
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
-- everything up to this point should be removed once mw.text becomes available.
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 = {}, {}, {}
local width, height, stack, delimiter = 500, 350, false, ':'
local chartWidth, chartHeight, defcolor, scalePerGroup
 
local keywords = {
width = 'width',
height = 'height',
stack = 'stack',
colors = 'colors',
group = 'group',
xlegend = 'x legend',
yscale = 'y scale',
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 barChart( frame )
local numGroups, numValues
local res = {}
local scaleWidth
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
 
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
 
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
 
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.
chartHeight = height - 80
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
numGroups = #values
numValues = #values[1]
defcolor = defcolor or 'blue'
scaleWidth = scalePerGroup and 40 * numGroups or 60
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
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
 
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.', elseif k ==keywords.stack, keywords.scalePerGroup then scalePerGroup =) true)
end
elseif k == keywords.defcolor then defcolor = v
for gi = 2, numGroups do
else
if #values[gi] ~= numValues then error( keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1" ) end
for keyword, tab in pairs( {
end
group = values,
if #xlegends ~= numValues then error( 'Illegal number of ' .. keywords.xlegend .. '. Should be exactly ' .. numValues .. ' not ' .. #xlegends) end
xlegend = xlegends,
end
colors = colors,
tooltip = tooltips,
unitsPrefix = unitPrefix,
unitsSuffix = unitsSuffix,
groupNames = groupNames,
} ) do
if testone( keywords[keyword], k, v, tab )
then break
end
end
end
end
end
 
local function extractParams()
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 testone( keyword, key, val, tab )
local ordermag = 10 ^ math.floor( math.log10( x ) )
local 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
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
function calcHeightLimits() -- if limits were passed by user, use ithem, otherwise calculate. for "stack" there's only one limet.
if k == keywords.width then
if #yscales > 0 then return end
width = tonumber( v )
if stack then
if not width or width < 200 then
local sums = {}
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
for _, group in pairs( values ) do
end
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
elseif k == keywords.height then
end
height = tonumber( v )
local sum = roundup( math.max( unpack( sums ) ) )
if not height or height < 200 then
for i = 1, #values do yscales[i] = sum end
error( 'Illegal height value (must be a number, and at least 200): ' .. v )
else
end
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
elseif k == keywords.stack then stack = true
end
elseif k == keywords.yticks then yticks = tonumber(v) or -1
for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale ) 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
 
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 tooltip( gi, i, val )
local ordermag = 10 ^ math.floor( math.log10( x ) )
function nulOrWhitespace( s )
local normalized = x / ordermag
return not s or mw.text.trim( s ) == ''
local top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5
end
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 tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i] end
if stack then
local groupName = not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or ''
local sums = {}
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
for _, group in pairs( values ) do
local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
return groupName .. prefix .. val .. suffix
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 calcHeightstooltip( gi, i, val )
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true end
local barHeight = math.floor( val / yscales[gi] * chartHeight )
local groupName = mw.text.killMarkers(not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or '')
local top, base = chartHeight - barHeight, 0
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
if stack then
local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
local rawbase = 0
return string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
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.
end
return barHeight, top - base
end
 
local function groupBoundscalcHeights( gi, i, val )
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 setWidth = math.floor( chartWidth / numValues )
local top, base = chartHeight - barHeight, 0
local setOffset = ( i - 1 ) * setWidth
if stack then
return setOffset, setWidth
for j = 1, gi - 1 do
end
if tonumber(values[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
end
end
end
return barHeight, top - base
end
 
local function calcxgroupBounds( gi, i )
local setOffset, setWidth = groupBoundsmath.floor( ichartWidth / numValues )
local setOffset = ( i - 1 setWidth = 0.85) * setWidth
return setOffset, setWidth
if stack then
end
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
return setOffset + (setWidth - barWidth) / 2, barWidth
end
local barWidth = math.floor( 0.75 * setWidth / numGroups )
local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )
return left, barWidth
end
 
local function drawbarcalcx( gi, i, val )
local setOffset, setWidth = groupBounds( i )
local color, tooltip = colors[gi] or defcolor or 'blue', tooltip( gi, i, val )
if stack or numGroups == 1 then
local left, barWidth = calcx( gi, i )
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
local barHeight, top = calcHeights( gi, i, val )
return setOffset + (setWidth - barWidth) / 2, barWidth
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)
setWidth = 0.85 * setWidth
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, "" ) )
local barWidth = math.floor( 0.75 * setWidth / numGroups )
end
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 )
function drawYScale()
local left, barWidth = calcx( gi, i )
function drawSingle( gi, color, width, single )
local barHeight, top = calcHeights( gi, i, val )
local yscale = yscales[gi]
local _, top, ordermag = roundup( yscale * 0.999 )
local numnotches = top <= 1.5 and top * 4
or top < 4 and top * 2
or top
local 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 ) }, val )
table.insert( res, div )
div = mw.text.tag( 'div', { style = string.format( notchStyleStr, y, width - 4, color ) }, '' )
table.insert( res, div )
end
end
 
-- borders so it shows up when printing
if scalePerGroup then -- not ready yet/
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 colWidth = 40
left, top, barHeight-1, barWidth-2, barWidth-2, color, color)
local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"
local link = links[gi] and links[gi][i] or ''
for gi = 1, numGroups do
local img = not nulOrWhitespace( link ) and string.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
local left = ( gi - 1 ) * 40
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
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
 
function drawXlegends()
local setOffset, setWidth
local legendDivStyleFormat = "position:absolute;left:%spx;top:10px;min-width:%spx;max-width:%spx;text-align:center;veritical-align:top;"
local tickDivstyleFormat = "position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"
for i = 1, numValues do
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 - 10 ) }, '' ) )
end
end
 
local function printGroupListdrawYScale()
local function drawSingle( gi, color, width, yticks, single )
if #groupNames > 0 then
local listyscale = {}yscales[gi]
local _, top, ordermag = roundup( yscale * 0.999 )
for gi = 1, #groupNames do
local numnotches = yticks >= 0 and yticks or
local square = mw.text.tag( 'span', { style = string.format( 'background-color:%s;padding:0 0.5em;margin:0 0.5em;', colors[gi] or defcolor ) }, ' ' )
(top <= 1.5 and top * 4
table.insert( list, '*' .. square .. ' ' .. groupNames[gi] )
or top < 4 and top * end2
or top)
table.insert( res, table.concat( list, '\n' ) )
local valStyleStr =
end
single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
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'
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
function drawChart()
local colWidth = 80
table.insert( res, mw.text.tag( 'div', { style = string.format( 'max-width:%spx;', width ) } ) )
local table.insert( res, mw.text.tag( 'div', { stylecolStyle = string.format("min-position:absolute;height:%spx;min-width:%spx;max-widthleft:%spx;",border-right:1px height,solid width, width ) } ) )%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()
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 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()
for gi, group in pairs( values ) do
table.insert( res, mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'padding-top:10px;margin-top:1em;max-width:%spx;', width ) } ) )
for i, val in ipairs( group ) do
table.insert( res, mw.text.tag( 'div', { style = string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;", height, width, width ) } ) )
drawbar( gi, i, val )
end
end
 
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, '</div>' )
local acum = stack and accumulateTooltip and {}
table.insert( res, mw.text.tag( 'div', { style = string.format("position:absolute;height:%s;min-width:%s;max-width:%spx;", chartHeight, scaleWidth, scaleWidth, scaleWidth ) } ) )
for gi, group in pairs( values ) do
drawYScale()
for i, val in ipairs( group ) do
table.insert( res, '</div>' )
if acum then acum[i] = ( acum[i] or 0 ) + val end
table.insert( res, mw.text.tag( 'div', { style = string.format("float:right;position:relative;width:%spx;", chartWidth ) } ) )
drawbar( gi, i, val, acum and acum[i] )
drawXlegends()
end
table.insert( res, '</div>' )
end
table.insert( res, '</div>' )
table.insert( res, '</div>' )
printGroupList()
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, '</div>' )
drawYScale()
end
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,
}