Jump to content

Module:Chart

विकिपीडिया से
imported>קיפודנחש के द्वारा 15:30, 8 अप्रैल 2013 के बदलाव
--[[
next 100 lines or so are copied from mw.text package, which is not yet available.
once this library is added to wmf distributions, the whole chunk 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

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] = '</' .. 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

    return table.concat( ret )
end

function mw.text.trim( s )
    return mw.ustring.match(s, "^%s*(.-)%s*$")
end

-- everything up to here should be removed once mw.text becomes avaulable.

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, stack, delimiter = 500, 350, false, ':'
    local chartWidth, chartHeight, defcolor, scalePerGroup

    local keywords = {
        width = 'width',
        height = 'height',
        stack = 'stack',
        colors = 'colors',
        group = 'group',
        xlegend = 'x legends',
        tooltip = 'tooltip',
        links = 'links',
        defcolor = 'default color',
        scalePerGroup = 'scale per group',
        unitsPrefix = 'units prefix',
        unitsSuffix = 'units suffix',
        groupNames = 'group names',
    } -- here is where you want to translate

    local numGroups, numValues
    local scaleWidth

    function nulOrWhitespace( s )
        return not s or mw.text.trim( s ) == ''
    end

    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

        -- 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.', keyword.stack, keyword.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 exatly ' .. numValues ) 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

        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.scalePerGroup then scalePerGroup = true
            elseif k == keywords.defcolor then defcolor = 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

    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

    function calcHeightLimits() -- if limits were passed by user, use ithem, 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 ) end
        if not scalePerGroup then for i = 1, #values do yscales[i] = math.max( unpack( yscales ) ) end end
    end

    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 = 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 mw.ustring.gsub(groupName .. prefix .. mw.getContentLanguage():formatNum( tonumber( val ) or 0 ) .. suffix, '_', ' '), false
    end

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

    function groupBounds( i )
        local setWidth = math.floor( chartWidth / numValues )
        local setOffset = ( i - 1 ) * setWidth
        return setOffset, setWidth
    end

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

    function drawbar( gi, i, val )
        local color, tooltip, custom = 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;overflow:hidden;",
                        left, top, barHeight, barWidth, barWidth, color)
        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


    function drawYScale()
        function drawSingle( gi, color, width, single )
            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 ) }, 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
            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 )
                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
            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 - 5, setWidth - 10, setWidth - 10 ) }, xlegends[i] or '' ) )
                table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) }, '' ) )
            end
        end
    end

    function printGroupList()
        if #groupNames > 1 then
            local list = {}
            local spanStyle = "padding:0 1em;background-color:%s;box-shadow:4px -3px 3px 1px grey;"
            for gi = 1, #groupNames do
                local span = mw.text.tag( 'span', { style = string.format( spanStyle, colors[gi] ) }, '&nbsp;' ) .. ' '..  groupNames[gi]
                table.insert( list, mw.text.tag( 'li', {}, span ) )
            end
            table.insert( res, mw.text.tag( 'ul', {style="width:100%;list-style:none;-webkit-column-width:10em;-moz-column-width:10em;column-width:10em;"}, table.concat( list, '\n' ) ) )
        end
    end

    function drawChart()
        table.insert( res, mw.text.tag( 'div', { style = string.format( '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 ) } ) )

         for gi, group in pairs( values ) do
             for i, val in ipairs( group ) do
                drawbar( gi, i, val )
            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>' )
        printGroupList()
        table.insert( res, '</div>' )
    end

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

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