--[[keywords are used for languages: they are the names of the actualparameters of the template]]local keywords = {barChart = 'bar chart',pieChart = 'pie chart',width = 'width',height = 'height',stack = 'stack',colors = 'colors',group = 'group',xlegend = 'x legends',yticks = 'y tick marks',tooltip = 'tooltip',accumulateTooltip = 'tooltip value accumulation',links = 'links',defcolor = 'default color',scalePerGroup = 'scale per group',unitsPrefix = 'units prefix',unitsSuffix = 'units suffix',groupNames = 'group names',hideGroupLegends = 'hide group legends',slices = 'slices',slice = 'slice',radius = 'radius',percent = 'percent',} -- here is what you want to translatelocal defColors = mw.loadData("Module:Chart/Default colors")local hideGroupLegendslocal function nulOrWhitespace( s )return not s or mw.text.trim( s ) == ''endlocal function createGroupList( tab, legends, cols )if #legends > 1 and not hideGroupLegends thentable.insert( tab, mw.text.tag( 'div' ) )local list = {}local spanStyle = "padding:0 1em;background-color:%s;border:1px solid %s;margin-right:1em;-webkit-print-color-adjust:exact;"for gi = 1, #legends dolocal span = mw.text.tag( 'span', { style = string.format( spanStyle, cols[gi], cols[gi] ) }, '&nbsp;' ) .. ' '..  legends[gi]table.insert( list, mw.text.tag( 'li', {}, span ) )endtable.insert( tab,mw.text.tag( 'ul',{style="list-style:none;column-width:12em;"},table.concat( list, '\n' )))table.insert( tab, '</div>' )endendlocal function pieChart( frame )local res, imslices, args = {}, {}, frame.argslocal radiuslocal values, colors, names, legends, links = {}, {}, {}, {}, {}local delimiter = args.delimiter or ':'local lang = mw.getContentLanguage()local function getArg( s, def, subst, with )local result = args[keywords[s]] or def or ''if subst and with then result = string.gsub( result, subst, with ) endreturn resultendlocal function analyzeParams()local function addSlice( i, slice )local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )values[i] = tonumber( lang:parseFormattedNumber( value ) )or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or '', slice ) )colors[i] = not nulOrWhitespace( color ) and color or defColors[i * 2]names[i] = name or ''links[i] = linkendradius = 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 = 0local i = 0for slice in string.gmatch( slicesStr or '', "%b()" ) doi = i + 1addSlice( i, string.match( slice, '^%(%s*(.-)%s*%)$' ) )endfor k, v in pairs(args) dolocal ind = string.match( k, '^' .. keywords.slice .. '%s+(%d+)$' )if ind then addSlice( tonumber( ind ), v ) endendfor _, val in ipairs( values ) do sum = sum + val endfor i, value in ipairs( values ) dolocal 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] ) )endendlocal function addRes( ... )for _, v in pairs( { ... } ) dotable.insert( res, v )endendlocal function createImageMap()addRes( '{{#tag:imagemap|', 'File:Circle frame.svg{{!}}' .. ( radius * 2 ) .. 'px' )addRes( unpack( imslices ) )addRes( 'desc none', '}}' )endlocal function drawSlice( i, q, start )local color = colors[i]local angle = start * 2 * math.pilocal sin, cos = math.abs( math.sin( angle ) ), math.abs( math.cos( angle ) )local wsin, wcos = sin * radius, cos * radiuslocal s1, s2, w1, w2, w3, w4, borderif q == 1 thenborder = 'left'w1, w2, w3, w4 = 0, 0, wsin, wcoss1, s2 = 'bottom', 'left'elseif q == 2 thenborder = 'bottom'w1, w2, w3, w4 = 0, wcos, wsin, 0s1, s2 = 'bottom', 'right'elseif q == 3 thenborder = 'right'w1, w2, w3, w4 = wsin, wcos, 0, 0s1, s2 = 'top', 'right'elseborder = 'top'w1, w2, w3, w4 = wsin, 0, 0, wcoss1, s2 = 'top', 'left'endlocal 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 thenstyle = string.format( '%s;border:0;background-color:%s', style, color )elsestyle = string.format( '%s;border-width:%spx %spx %spx %spx;border-%s-color:%s', style, w1, w2, w3, w4, border, color )endaddRes( mw.text.tag( 'div', { style = style }, '' ) )endlocal function createSlices()local function coordsOfAngle( angle )return ( 100 + math.floor( 100 * math.cos( angle ) ) ) .. ' ' .. ( 100 - math.floor( 100 * math.sin( angle ) ) )endlocal sum, start = 0, 0for _, value in ipairs( values ) do sum = sum + value endfor i, value in ipairs(values) dolocal poly = { 'poly 100 100' }local startC, endC =  start / sum, ( start + value ) / sumlocal startQ, endQ = math.floor( startC * 4 + 1 ), math.floor( endC * 4 + 1 )for q = startQ, math.min( endQ, 4 ) do drawSlice( i, q, startC ) endfor angle = startC * 2 * math.pi, endC * 2 * math.pi, 0.02 dotable.insert( poly,  coordsOfAngle( angle ) )endtable.insert( poly, coordsOfAngle( endC * 2 * math.pi ) .. ' 100 100 ' .. links[i] )table.insert( imslices, table.concat( poly, ' ' ) )start = start + values[i]endendanalyzeParams()if #values == 0 then error( "no slices found - can't draw pie chart" ) endaddRes( 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 ) -- legendsaddRes( '</div>' ) -- close containing divreturn frame:preprocess( table.concat( res, '\n' ) )endlocal function barChart( frame )local res = {}local args = frame.args -- can be changed to frame:getParent().argslocal 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, accumulateTooltiplocal numGroups, numValueslocal scaleWidthlocal function validate()local function asGroups( name, tab, toDuplicate, emptyOK )if #tab == 0 and not emptyOK thenerror( "must supply values for " .. keywords[name] )endif #tab == 1 and toDuplicate thenfor i = 2, numGroups do tab[i] = tab[1] endendif #tab > 0 and #tab ~= numGroups thenerror ( keywords[name] .. ' must contain the same number of items as the number of groups, but it contains ' .. #tab .. ' items and there are ' .. numGroups .. ' groups')endend-- do all sorts of validation here, so we can assume all params are good from now on.-- among other things, replace numerical values with mw.language:parseFormattedNumber() resultchartHeight = height - 80numGroups = #valuesnumValues = #values[1]defcolor = defcolor or 'blue'colors[1] = colors[1] or defcolorscaleWidth = scalePerGroup and 80 * numGroups or 100chartWidth = width - scaleWidthasGroups( 'unitsPrefix', unitsPrefix, true, true )asGroups( 'unitsSuffix', unitsSuffix, true, true )asGroups( 'colors', colors, true, true )asGroups( 'groupNames', groupNames, false, false )if stack and scalePerGroup thenerror( string.format( 'Illegal settings: %s and %s are incompatible.', keywords.stack, keywords.scalePerGroup ) )endfor gi = 2, numGroups doif #values[gi] ~= numValues then error( keywords.group .. " " .. gi .. " does not have same number of values as " .. keywords.group .. " 1" ) endendif #xlegends ~= numValues then error( 'Illegal number of ' .. keywords.xlegend .. '. Should be exactly ' .. numValues ) endendlocal 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 endi = tonumber( i ) or error("Expect numerical index for key " .. keyword .. " instead of '" .. key .. "'")if i > 0 then tab[i] = {} endfor s in mw.text.gsplit( val, '%s*' .. delimiter .. '%s*' ) dotable.insert( i == 0 and tab or tab[i], s )endreturn trueendfor k, v in pairs( args ) doif k == keywords.width thenwidth = tonumber( v )if not width or width < 200 thenerror( 'Illegal width value (must be a number, and at least 200): ' .. v )endelseif k == keywords.height thenheight = tonumber( v )if not height or height < 200 thenerror( 'Illegal height value (must be a number, and at least 200): ' .. v )endelseif k == keywords.stack then stack = trueelseif k == keywords.yticks then yticks = tonumber(v) or -1elseif k == keywords.scalePerGroup then scalePerGroup = trueelseif k == keywords.defcolor then defcolor = velseif k == keywords.accumulateTooltip then accumulateTooltip = not nulOrWhitespace( v )elseif k == keywords.hideGroupLegends then hideGroupLegends = not nulOrWhitespace( v )elsefor keyword, tab in pairs( {group = values,xlegend = xlegends,colors = colors,tooltip = tooltips,unitsPrefix = unitsPrefix,unitsSuffix = unitsSuffix,groupNames = groupNames,links = links,} ) doif testone( keywords[keyword], k, v, tab )then breakendendendendendlocal 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 /  ordermaglocal top = normalized >= 1.5 and ( math.floor( normalized + 1 ) ) or 1.5return ordermag * top, top, ordermagendlocal function calcHeightLimits() -- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.if stack thenlocal sums = {}for _, group in pairs( values ) dofor i, val in ipairs( group ) do sums[i] = ( sums[i] or 0 ) + val endendlocal sum = math.max( unpack( sums ) )for i = 1, #values do yscales[i] = sum endelsefor i, group in ipairs( values ) do yscales[i] = math.max( unpack( group ) ) endendfor i, scale in ipairs( yscales ) do yscales[i] = roundup( scale * 0.9999 ) endif not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end endendlocal function tooltip( gi, i, val )if tooltips and tooltips[gi] and not nulOrWhitespace( tooltips[gi][i] ) then return tooltips[gi][i], true endlocal groupName = mw.text.killMarkers(not nulOrWhitespace( groupNames[gi] ) and groupNames[gi] .. ': ' or '')local prefix = unitsPrefix[gi] or unitsPrefix[1] or ''local suffix = unitsSuffix[gi] or unitsSuffix[1] or ''return string.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), falseendlocal function calcHeights( 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 sizeslocal top, base = chartHeight - barHeight, 0if stack thenfor j = 1, gi - 1 doif tonumber(values[j][i]) > 0 thenbase = 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 endendendreturn barHeight, top - baseendlocal function groupBounds( i )local setWidth = math.floor( chartWidth / numValues )local setOffset = ( i - 1 ) * setWidthreturn setOffset, setWidthendlocal function calcx( gi, i )local setOffset, setWidth = groupBounds( i )if stack or numGroups == 1 thenlocal barWidth = math.min( 38, math.floor( 0.8 * setWidth ) )return setOffset + (setWidth - barWidth) / 2, barWidthendsetWidth = 0.85 * setWidthlocal barWidth = math.floor( 0.75 * setWidth / numGroups )local left = setOffset + math.floor( ( gi - 1 ) / numGroups * setWidth )return left, barWidthendlocal 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 talkpagelocal color, tooltip, custom = colors[gi] or defcolor or 'blue', tooltip( gi, i, ttval or val )local left, barWidth = calcx( gi, i )local barHeight, top = calcHeights( gi, i, val )-- borders so it shows up when printinglocal style = string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;-webkit-print-color-adjust:exact;border:1px solid %s;border-bottom:none;overflow:hidden;",left, top, barHeight-1, barWidth-2, barWidth-2, color, color)local link = links[gi] and links[gi][i] or ''local img = not nulOrWhitespace( link ) and string.format( '[[File:Transparent.png|1000px|link=%s|%s]]', link, custom and tooltip or '' ) or ''table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )endlocal function drawYScale()local function drawSingle( gi, color, width, yticks, single )local yscale = yscales[gi]local _, top, ordermag = roundup( yscale * 0.999 )local numnotches = yticks >= 0 and yticks or(top <= 1.5 and top * 4or top < 4  and top * 2or 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 dolocal val = i / numnotches * yscalelocal 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 )endendif scalePerGroup thenlocal colWidth = 80local colStyle = "position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"for gi = 1, numGroups dolocal left = ( gi - 1 ) * colWidthlocal color = colors[gi] or defcolortable.insert( res, mw.text.tag( 'div', { style = string.format( colStyle, chartHeight, colWidth, left, color, color ) } ) )drawSingle( gi, color, colWidth, yticks )table.insert( res, '</div>' )endelsedrawSingle( 1, 'black', scaleWidth, yticks, true )endendlocal function drawXlegends()local setOffset, setWidthlocal 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 doif not nulOrWhitespace( xlegends[i] ) thensetOffset, setWidth = groupBounds( i )-- setWidth = 0.85 * setWidthtable.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 ) }, '' ) )endendendlocal 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 ) dofor i, val in ipairs( group ) doif acum then acum[i] = ( acum[i] or 0 ) + val enddrawbar( gi, i, val, acum and acum[i] )endendtable.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>' )endextractParams()validate()calcHeightLimits()drawChart()return table.concat( res, "\n" )endreturn {['bar-chart'] = barChart,[keywords.barChart] = barChart,[keywords.pieChart] = pieChart,}