Module:Chart Explained

--

local keywords = -- 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 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;border:1px solid %s;margin-right:1em;-webkit-print-color-adjust:exact;" for gi = 1, #legends do local span = mw.text.tag('span',, ' ') .. ' '.. legends[gi] table.insert(list, mw.text.tag('li',, span)) end table.insert(tab, mw.text.tag('ul', , table.concat(list, '\n') ) ) table.insert(tab, '

') endend

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 = string.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 , slice)) 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 = 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('%s', legends[i])) end end

local function addRes(...) for _, v in pairs do table.insert(res, v) end end

local function createImageMap addRes('') 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',, )) 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 = 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',)) addRes(mw.text.tag('div',)) createSlices addRes(mw.text.tag('div',)) createImageMap addRes('') -- close "position:relative" div that contains slices and imagemap. addRes('') -- close "position:relative" div that contains slices and imagemap. createGroupList(res, legends, colors) -- legends addRes('') -- 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. -- among other things, replace numerical values with mw.language:parseFormattedNumber result

chartHeight = height - 80 numGroups = #values numValues = #values[1] defcolor = defcolor or 'blue' colors[1] = colors[1] or defcolor scaleWidth = scalePerGroup and 80 * numGroups or 100 chartWidth = width - scaleWidth asGroups('unitsPrefix', unitsPrefix, true, true) asGroups('unitsSuffix', unitsSuffix, true, true) asGroups('colors', colors, true, true) asGroups('groupNames', groupNames, false, false) if stack and scalePerGroup then error(string.format('Illegal settings: %s and %s are incompatible.', 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) 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 if k

keywords.width then width = tonumber(v) if not width or width < 200 then error('Illegal width value (must be a number, and at least 200): ' .. v) end elseif k

keywords.height then height = tonumber(v) if not height or height < 200 then error('Illegal height value (must be a number, and at least 200): ' .. v) end elseif k

keywords.stack then stack = true elseif k

keywords.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 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. 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 = 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 tooltip(gi, i, val) if tooltips and tooltips[gi] and not nulOrWhitespace(tooltips[gi][i]) then return tooltips[gi][i], true end local 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, '_', ' '), false end

local 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 sizes local top, base = chartHeight - barHeight, 0 if stack then for j = 1, gi - 1 do 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 groupBounds(i) local setWidth = math.floor(chartWidth / numValues) local setOffset = (i - 1) * setWidth return setOffset, setWidth end

local function calcx(gi, i) local setOffset, setWidth = groupBounds(i) 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

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) local left, barWidth = calcx(gi, i) local barHeight, top = calcHeights(gi, i, val)

-- borders so it shows up when printing 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;", 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('', link, custom and tooltip or ) or table.insert(res, mw.text.tag('div',, 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 = 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',, mw.getContentLanguage:formatNum(tonumber(val) or 0)) table.insert(res, div) div = mw.text.tag('div',, ) 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',)) drawSingle(gi, color, colWidth, yticks) table.insert(res, '') 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',, xlegends[i] or )) table.insert(res, mw.text.tag('div',, )) end end end

local function drawChart table.insert(res, mw.text.tag('div',)) table.insert(res, mw.text.tag('div',))

table.insert(res, mw.text.tag('div',)) 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, '') table.insert(res, mw.text.tag('div',)) drawYScale table.insert(res, '') table.insert(res, mw.text.tag('div',)) drawXlegends table.insert(res, '') table.insert(res, '') createGroupList(res, groupNames, colors) table.insert(res, '') end

extractParams validate calcHeightLimits drawChart return table.concat(res, "\n")end

return