Module:Chart: Difference between revisions

Content deleted Content added
Attempting to add border to bar charts for better printing
Copyedit. Add number got to error message.
 
(26 intermediate revisions by 16 users not shown)
Line 1:
--<source lang=lua>
--[[
keywords are used for languages: they are the names of the actual
parameters of the template
]]
 
local keywords = {
barChart = 'bar chart',
pieChart = 'pie chart',
width = 'width',
height = 'height',
stack = 'stack',
colors = 'colors',
group = 'group',
xlegend = 'x legends',
yticks = 'y tick marks',
tooltip = 'tooltip',
tooltip = 'tooltip',
accumulateTooltip = 'tooltip value accumulation',
links = 'links',
defcolor = 'default color',
scalePerGroup = 'scale per group',
unitsPrefix = 'units prefix',
unitsSuffix = 'units suffix',
groupNames = 'group names',
hideGroupLegends = 'hide group legends',
slices = 'slices',
slice = 'slice',
radius = 'radius',
percent = 'percent',
 
} -- here is what you want to translate
 
local defColors = require mw.loadData("Module:PlotterChart/DefaultColorsDefault colors")
local hideGroupLegends
 
local function nulOrWhitespace( s )
return not s or mw.text.trim( s ) == ''
end
 
local function createGroupList( tab, legends, cols )
if #legends > 1 and not hideGroupLegends then
table.insert( tab, mw.text.tag( 'div' ) )
local list = {}
local spanStyle = "padding:0 1em;background-color:%s;box-shadowborder:2px -1px 4pxsolid 0 silver%s;margin-right:1em;-webkit-print-color-adjust:exact;"
for gi = 1, #legends do
local span = mw.text.tag( 'span', { style = string.format( spanStyle, cols[gi], cols[gi] ) }, '&nbsp;' ) .. ' '.. legends[gi]
table.insert( list, mw.text.tag( 'li', {}, span ) )
end
table.insert( tab,
mw.text.tag( 'ul',
{style="width:100%;list-style:none;-webkit-column-width:12em;-moz-column-width:12em;column-width:12em;"},
table.concat( list, '\n' )
)
)
)
)
table.insert( tab, '</div>' )
end
end
 
local function pieChart( frame )
local res, imslices, args = {}, {}, frame.args
local radius
local values, colors, names, legends, links = {}, {}, {}, {}, {}
local delimiter = args.delimiter or ':'
local lang = mw.getContentLanguage()
 
local function getArg( s, def, subst, with )
local result = args[keywords[s]] or def or ''
if subst and with then result = mw.ustringstring.gsub( result, subst, with ) end
return result
end
 
local function analyzeParams()
local function addSlice( i, slice )
local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
values[i] = tonumber( lang:parseFormattedNumber( value ) )
or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', sliceStrslice ) )
colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]
names[i] = name or ''
links[i] = link
end
radius = getArg( 'radius', 150 )
hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
local slicesStr = getArg( 'slices' )
local prefix = getArg( 'unitsPrefix', '', '_', ' ' )
local suffix = getArg( 'unitsSuffix', '', '_', ' ' )
local percent = args[keywords.percent]
local sum = 0
local i, value = 0
for slice in mw.ustring.gmatch( slicesStr or '', "%b()" ) do
i = i + 1
addSlice( i, mw.ustring.match( slice, '^%(%s*(.-)%s*%)$' ) )
end
for k, v in pairs(args) do
local ind = mw.ustring.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )
if ind then addSlice( tonumber( ind ), v ) end
end
for _, val in ipairs( values ) do sum = sum + val end
for i, value in ipairs( values ) do
local addprec = percent and string.format( ' (%0.1f%%)', value / sum * 100 ) or ''
legends[i] = mw.ustring.format( '%s: %s%s%s%s', names[i], prefix, lang:formatNum( value ), suffix, addprec )
links[i] = mw.text.trim( links[i] or mw.ustring.format( '[[#noSuchAnchor|%s]]', legends[i] ) )
end
end
 
radius = getArg( 'radius', 150 )
function addRes( ... )
hideGroupLegends = not nulOrWhitespace( args[keywords.hideGroupLegends] )
for _, v in pairs( { ... } ) do
local slicesStr = getArg( 'slices' )
table.insert( res, v )
local prefix = getArg( 'unitsPrefix', '', '_', ' ' )
end
local suffix = getArg( 'unitsSuffix', '', '_', ' ' )
end
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 createImageMap()
local ind = string.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )
addRes( '{{#tag:imagemap|', 'Image:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )
if ind then addSlice( tonumber( ind ), v ) end
addRes( unpack( imslices ) )
end
addRes( 'desc none', '}}' )
end
 
for _, val in ipairs( values ) do sum = sum + val end
function drawSlice( i, q, start )
for i, value in ipairs( values ) do
local color = colors[i]
local addprec = percent and string.format( ' (%0.1f%%)', value / sum * 100 ) or ''
local angle = start * 2 * math.pi
legends[i] = string.format( '%s: %s%s%s%s', names[i], prefix, lang:formatNum( value ), suffix, addprec )
local sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )
links[i] = mw.text.trim( links[i] or string.format( '[[#noSuchAnchor|%s]]', legends[i] ) )
local wsin, wcos = sin * radius, cos * radius
end
local s1, s2, w1, w2, w3, w4, width, border
end
local style
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 function addRes( ... )
local style = string.format( 'position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx', s1, radius, s2, radius, radius, radius )
for _, v in if start <= pairs( q{ -... 1} ) * 0.25 thendo
table.insert( res, v )
style = string.format( '%s;border:0;background-color:%s', style, color )
end
else
end
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', { class = 'transborder', style = style }, '' ) )
end
 
local function createSlicescreateImageMap()
addRes( '{{#tag:imagemap|', 'File:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )
function coordsOfAngle( angle )
addRes( unpack( imslices ) )
return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
addRes( 'desc none', '}}' )
end
end
 
local function drawSlice( i, local sumq, start = 0, 0)
local color = colors[i]
for _, value in ipairs( values ) do sum = sum + value end
local angle = start * 2 * math.pi
for i, value in ipairs(values) do
local sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )
local poly = { 'poly 100 100' }
local startCwsin, endCwcos = sin start* / sumradius, ( start + value )cos /* sumradius
local s1, s2, w1, w2, w3, w4, border
local startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )
if q == 1 then
for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) end
border = 'left'
for angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 do
w1, w2, w3, w4 = 0, 0, wsin, wcos
table.insert( poly, coordsOfAngle( angle ) )
s1, s2 = 'bottom', 'left'
end
elseif q == 2 then
table.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )
border = 'bottom'
table.insert( imslices, table.concat( poly, ' ' ) )
w1, w2, w3, w4 = 0, wcos, wsin, 0
start = start + values[i]
s1, s2 = 'bottom', 'right'
end
elseif q == 3 then
end
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 )
analyzeParams()
if start <= ( q - 1 ) * 0.25 then
if #values == 0 then error( "no slices found - can't draw pie chart" ) end
addRes( mw.text.tag( 'div', { class = 'chart', style = string.format( 'margin-top%s;border:0.5em;maxbackground-widthcolor:%spx;s', radius * 2 ) }style, )color )
else
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 ) } ) )
style = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )
createSlices()
end
addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
addRes( mw.text.tag( 'div', { style = style }, '' ) )
createImageMap()
end
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
 
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
local function createSlices()
createGroupList( res, legends, colors ) -- legends
local function coordsOfAngle( angle )
addRes( '</div>' ) -- close containing div
return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )
return frame:preprocess( table.concat( res, '\n' ) )
end
 
local sum, start = 0, 0
for _, value in ipairs( values ) do sum = sum + value end
for i, value in ipairs(values) do
local poly = { 'poly 100 100' }
local startC, endC = start / sum, ( start + value ) / sum
local startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )
for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) end
for angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 do
table.insert( poly, coordsOfAngle( angle ) )
end
table.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )
table.insert( imslices, table.concat( poly, ' ' ) )
start = start + values[i]
end
end
 
analyzeParams()
if #values == 0 then error( "no slices found - can't draw pie chart" ) end
addRes( mw.text.tag( 'div', { class = 'chart noresize', style = string.format( 'margin-top:0.5em;max-width:%spx;', radius * 2 ) } ) )
addRes( mw.text.tag( 'div', { style = string.format( 'position:relative;min-width:%spx;min-height:%spx;max-width:%spx;overflow:hidden;', radius * 2, radius * 2, radius * 2 ) } ) )
createSlices()
addRes( mw.text.tag( 'div', { style = string.format( 'position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;', radius * 2, radius * 2 ) } ) )
createImageMap()
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
addRes( '</div>' ) -- close "position:relative" div that contains slices and imagemap.
createGroupList( res, legends, colors ) -- legends
addRes( '</div>' ) -- close containing div
return frame:preprocess( table.concat( res, '\n' ) )
end
 
 
local function barChart( frame )
local res = {}
local args = frame.args -- can be changed to frame:getParent().args
local values, xlegends, colors, tooltips, yscales = {}, {}, {}, {} ,{}, {}, {}
local groupNames, unitsSuffix, unitsPrefix, links = {}, {}, {}, {}
local width, height, yticks, stack, delimiter = 500, 350, -1, false, args.delimiter or ':'
local chartWidth, chartHeight, defcolor, scalePerGroup, accumulateTooltip
 
 
local numGroups, numValues
local scaleWidth
 
local function validate()
local function asGroups( name, tab, toDuplicate, emptyOK )
if #tab == 0 and not emptyOK then
error( "must supply values for " .. keywords[name] )
end
if #tab == 1 and toDuplicate then
for i = 2, numGroups do tab[i] = tab[1] end
end
if #tab > 0 and #tab ~= numGroups then
error ( keywords[name] .. ' must contain the same number of items as the number of groups, but it contains ' .. #tab .. ' items and there are ' .. numGroups .. ' groups')
end
end
 
-- do all sorts of validation here, so we can assume all params are good from now on.
local numGroups, numValues
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
local scaleWidth
 
function validate()
function asGroups( name, tab, toDuplicate, emptyOK )
if #tab == 0 and not emptyOK then
error( "must supply values for " .. keywords[name] )
end
if #tab == 1 and toDuplicate then
for i = 2, numGroups do tab[i] = tab[1] end
end
if #tab > 0 and #tab ~= numGroups then
error ( keywords[name] .. ' must contain the same number of items as the number of groups, but it contains ' .. #tab .. ' items and there are ' .. numGroups .. ' groups')
end
end
 
chartHeight = height - 80
-- do all sorts of validation here, so we can assume all params are good from now on.
numGroups = #values
-- among other things, replace numerical values with mw.language:parseFormattedNumber() result
numValues = #values[1]
defcolor = defcolor or 'blue'
colors[1] = colors[1] or defcolor
scaleWidth = scalePerGroup and 80 * numGroups or 100
chartWidth = width - scaleWidth
asGroups( 'unitsPrefix', unitsPrefix, true, true )
asGroups( 'unitsSuffix', unitsSuffix, true, true )
asGroups( 'colors', colors, true, true )
asGroups( 'groupNames', groupNames, false, false )
if stack and scalePerGroup then
error( string.format( 'Illegal settings: %s and %s are incompatible.', keywords.stack, keywords.scalePerGroup ) )
end
for gi = 2, numGroups do
if #values[gi] ~= numValues then error( keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1" ) end
end
if #xlegends ~= numValues then error( 'Illegal number of ' .. keywords.xlegend .. '. Should be exactly ' .. numValues .. ' not ' .. #xlegends) end
end
 
local function extractParams()
local function testone( keyword, key, val, tab )
local i = keyword == key and 0 or key:match( keyword .. "%s+(%d+)" )
if not i then return end
i = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
if i > 0 then tab[i] = {} end
for s in mw.text.gsplit( val, '%s*' .. delimiter .. '%s*' ) do
table.insert( i == 0 and tab or tab[i], s )
end
return true
end
 
for k, v in pairs( args ) do
chartHeight = height - 80
if k == keywords.width then
numGroups = #values
width = tonumber( v )
numValues = #values[1]
if not width or width < 200 then
defcolor = defcolor or 'blue'
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
colors[1] = colors[1] or defcolor
end
scaleWidth = scalePerGroup and 80 * numGroups or 100
elseif k == keywords.height then
chartWidth = width -scaleWidth
height = tonumber( v )
asGroups( 'unitsPrefix', unitsPrefix, true, true )
if not height or height < 200 then
asGroups( 'unitsSuffix', unitsSuffix, true, true )
error( 'Illegal height value (must be a number, and at least 200): ' .. v )
asGroups( 'colors', colors, true, true )
end
asGroups( 'groupNames', groupNames, false, false )
elseif k == keywords.stack then stack = true
if stack and scalePerGroup then
elseif k == keywords.yticks then yticks = tonumber(v) or -1
error( string.format( 'Illegal settings: %s and %s are incompatible.', keyword.stack, keyword.scalePerGroup ) )
elseif k == keywords.scalePerGroup then scalePerGroup = true
end
elseif k == keywords.defcolor then defcolor = v
for gi = 2, numGroups do
elseif k == keywords.accumulateTooltip then accumulateTooltip = not nulOrWhitespace( v )
if #values[gi] ~= numValues then error( keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1" ) end
elseif k == keywords.hideGroupLegends then hideGroupLegends = not nulOrWhitespace( v )
end
else
if #xlegends ~= numValues then error( 'Illegal number of ' .. keywords.xlegend .. '. Should be exatly ' .. numValues ) end
for keyword, tab in pairs( {
end
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 extractParams()
local ordermag = 10 ^ math.floor( math.log10( x ) )
function testone( keyword, key, val, tab )
local normalized = x / ordermag
i = keyword == key and 0 or key:match( keyword .. "%s+(%d+)" )
local top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5
if not i then return end
return ordermag * top, top, ordermag
i = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")
end
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 calcHeightLimits() -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.
for k, v in pairs( args ) do
if k == keywords.widthstack then
local sums = {}
width = tonumber( v )
for _, group in pairs( values ) do
if not width or width < 200 then
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
error( 'Illegal width value (must be a number, and at least 200): ' .. v )
end
end
local sum = math.max( unpack( sums ) )
elseif k == keywords.height then
for i = 1, #values do yscales[i] = sum end
height = tonumber( v )
else
if not height or height < 200 then
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
error( 'Illegal height value (must be a number, and at least 200): ' .. v )
end
end
for i, scale in ipairs( yscales ) do yscales[i] = roundup( scale * 0.9999 ) end
elseif k == keywords.stack then stack = true
if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
elseif k == keywords.scalePerGroup then scalePerGroup = true
end
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 tooltip( gi, i, val )
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.
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true end
local ordermag = 10 ^ math.floor( math.log10( x ) )
local groupName = mw.text.killMarkers(not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or '')
local normalized = x / ordermag
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
local top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5
local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
return ordermag * top, top, ordermag
return string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
end
end
 
local function calcHeights( gi, i, val )
function calcHeightLimits() -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.
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
if stack then
local top, base = chartHeight - barHeight, 0
local sums = {}
if stack then
for _, group in pairs( values ) do
for j = 1, gi - 1 do
for i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val end
if tonumber(values[j][i]) > 0 then
end
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
local sum = math.max( unpack( sums ) )
end
for i = 1, #values do yscales[i] = sum end
end
else
end
for i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) end
return barHeight, top - base
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 tooltipgroupBounds( gi, i, val )
local setWidth = math.floor( chartWidth / numValues )
if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true end
local setOffset = ( i - 1 ) * setWidth
local groupName = not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or ''
return setOffset, setWidth
local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''
end
local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''
return mw.ustring.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
end
 
local function calcHeightscalcx( gi, i, val )
local setOffset, setWidth = groupBounds( i )
local barHeight = math.floor( val / yscales[gi] * chartHeight + 0.5 ) -- add half to make it "round" instead of "trunc"
if stack or numGroups == 1 then
local top, base = chartHeight - barHeight, 0
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
if stack then
return setOffset + (setWidth - barWidth) / 2, barWidth
local rawbase = 0
end
for j = 1, gi - 1 do rawbase = rawbase + values[j][i] end -- sum the "i" value of all the groups below our group, gi.
setWidth = 0.85 * setWidth
base = math.floor( chartHeight * rawbase / yscales[gi] ) -- normally, and especially if it's "stack", all the yscales must be equal.
local barWidth = math.floor( 0.75 * setWidth / numGroups )
end
local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )
return barHeight, top - base
return left, barWidth
end
end
 
local function groupBoundsdrawbar( 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 setWidth = math.floor( chartWidth / numValues )
local setOffset = ( i - 1 ) * setWidth
return setOffset, setWidth
end
 
local color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, ttval or val )
function calcx( gi, i )
local setOffsetleft, setWidthbarWidth = groupBoundscalcx( gi, i )
local barHeight, top = calcHeights( gi, i, val )
if stack or numGroups == 1 then
local barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )
return setOffset + (setWidth - barWidth) / 2, barWidth
end
setWidth = 0.85 * setWidth
local barWidth = math.floor( 0.75 * setWidth / numGroups )
local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )
return left, barWidth
end
 
-- borders so it shows up when printing
function drawbar( gi, i, val, ttval )
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 color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, ttval or val )
left, top, barHeight-1, local leftbarWidth-2, barWidth-2, = calcx( gicolor, i color)
local barHeight, toplink = calcHeights(links[gi] and links[gi, ][i,] valor )''
local img = not nulOrWhitespace( link ) and string.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
local style = string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;border:1px solid %s;border-bottom:none;box-shadow:2px -1px 4px 0 silver;overflow:hidden;",
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
left, top, barHeight-1, barWidth-2, barWidth-2, color, color)
end
local link = links[gi] and links[gi][i] or ''
local img = not nulOrWhitespace( link ) and mw.ustring.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''
table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
end
 
 
local function drawYScale()
local function drawSingle( gi, color, width, yticks, single )
local yscale = yscales[gi]
local _, top, ordermag = roundup( yscale * 0.999 )
local numnotches = topyticks <>= 1.50 and top *yticks 4or
or (top <= 4 1.5 and top * 24
or top < 4 and top * 2
or top
or top)
local valStyleStr =
local valStyleStr =
single and 'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'
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
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
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;veriticalvertical-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 + 51, setWidth - 102, setWidth - 102 ) }, xlegends[i] or '' ) )
table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) }, '' ) )
end
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
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,
}
--</source>