User:קיפודנחש/sandbox/Module:BarChart

-- -- next several functions are copied from mw.text package, which is not yet available. once this library is added to wmf distributions, these functions should be removed, u = require("libraryUtil") mw = mw or {} mw.text = mw.text or {} local htmlencode_map = { ['>'] = '&gt;', ['<'] = '&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

function mw.text.encode( s, charset ) charset = charset or '<>&"\'\194\160'   s = mw.ustring.gsub( s, '[' .. charset .. ']', function ( m )        if not htmlencode_map[m] then            local e = string.format( '&#%d;', mw.ustring.codepoint( m ) )            htmlencode_map[m] = e            htmldecode_map[e] = m        end        return htmlencode_map[m]    end )    return s end function mw.text.split( text, pattern, plain )    local ret = {}    for m in gsplit( text, pattern, plain ) do        ret[#ret+1] = m    end    return ret end

function mw.text.gsplit( text, pattern, plain ) local s, l = 1, mw.ustring.len( text ) return function if s then local e, n = mw.ustring.find( text, pattern, s, plain ) local ret if not e then ret = mw.ustring.sub( text, s ) s = nil elseif n < e then -- Empty separator! ret = mw.ustring.sub( text, s, e ) if e < l then s = e + 1 else s = nil 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

function mw.text.tag( name, attrs, content ) local named = false if type( name ) == 'table' then named = true name, attrs, content = name.name, name.attrs, name.content 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 ret = { '<' .. name } for k, v in pairs( attrs or {} ) do       if type( k ) ~= 'string' then error( "bad named argument attrs to 'tag' (keys must be strings, found " .. type( k ) .. ")", 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 tp = type( content ) if content == nil then ret[#ret+1] = '>' elseif content == false then ret[#ret+1] = ' />' elseif tp == 'string' or tp == 'number' then ret[#ret+1] = '>' ret[#ret+1] = content ret[#ret+1] = ''   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

return table.concat( ret ) end

function barChart( frame ) local res, dbgres = {}, {} local args = frame.args -- can be changed to frame:getParent.args local values, xlegends, ylegends, colors, links, tooltips, yscales = {}, {}, {}, {} ,{}, {}, {} local width, height, stack, delimiter = 500, 350, false, ':' local defcolor, scalePerGroup

local keywords = { width = 'width', height = 'height', stack = 'stack', color = 'color', group = 'group', xlegend = 'x legend', ylegend = 'y legend', yscale = 'y scale', link = 'links', tooltip = 'tooltip', defcolor = 'default color', scalePerGroup = 'scale per group', } -- here is where you want to translate

local numgroups, numvalues

function validate -- 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 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

for k, v in pairs( args ) do           if k == keywords.width then width = tonumber( v ) or error( 'Illegal width value: ' .. v ) elseif k == keywords.height then height = tonumber( v ) or error( 'Illegal height value: ' .. v ) 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, ylegend = ylegends, color = colors, link = links, tooltip = tooltip } ) do                   if testone( keywords[keyword], k, v, tab ) then break end end end end

numgroups = #values

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 return normalized >= 1.5 and ordermag * ( math.floor( normalized + 1 ) ) or ordermag * 1.5 end

function calcHeightLimits -- if limits were passed by user, use ithem, otherwise calculate. for "stack" there's only one limet. if #yscales > 0 then return end 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

function tooltip( gi, i, val ) return tooltips and tooltips[gi] and tooltips[gi[i]] or val end

function calcHeights( gi, i, val ) local barHeight = math.floor( val / yscales[gi] * height ) local top, base = height - 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( height * rawbase / yscales[gi] ) -- normally, and especially if it's "stack", all the yscales must be equal. end table.insert( dbgres, string.format( "calcHeights: gi=%s, i=%s, val=%s, top=%s, base=%s", gi, i, val, top, base ) ) return barHeight, top - base end

function calcx( gi, i ) local numGroups, numInGroup = #values, #values[1] local setWidth = math.floor( width / numInGroup ) local setOffset = ( i - 1 ) * setWidth if stack then return setOffset, math.min( 38, math.floor( 0.8 * setWidth ) ) end local barWidth = math.floor( 0.75 * setWidth / numGroups ) local left = setOffset + math.floor( 0.85 * ( gi - 1 ) / numGroups * setWidth ) table.insert( dbgres, string.format( "calcx: gi=%s, i=%s, left=%s, barWidth=%s", gi, i, left, barWidth ) ) return left, barWidth end

function drawbar( gi, i, val ) local color, tooltip = colors[gi] or defcolor or 'blue', tooltip( gi, i, val ) local left, barWidth = calcx( gi, i ) local barHeight, top = calcHeights( gi, i, val ) 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;",                       left, top, barHeight, barWidth, barWidth, color) table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, "" ) ) end

function drawChart table.insert( res, mw.text.tag( 'div', { style = string.format("position: relative;min-height:%spx;min-width:%spx;max-width:%spx", height, width, width ) } ) ) -- here we should draw the axis for gi, group in pairs( values ) do            for i, val in ipairs( group ) do                table.insert( dbgres, string.format( "drawChart: gi: %s, i: %s, res: %s", gi, i, val ) ) drawbar( gi, i, val ) end end table.insert( res, ' ' ) end

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

return { ['bar-chart'] = barChart } --