Mô đun:Chart

--
--
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',
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 "Module:Plotter/DefaultColors"
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-shadow:2px -1px 4px 0 silver;margin-right:1em;"
for gi = 1, #legends do
local span = mw.text.tag( 'span', { style = string.format( spanStyle, colsgi ) }, ' ' ) .. ' '..  legendsgi
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,  )
end
end

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()

function getArg( s, def, subst, with )
local result = argskeywordss or def or 
if subst and with then result = mw.ustring.gsub( result, subst, with ) end
return result
end

function analyzeParams()
function addSlice( i, slice )
local value, name, color, link = unpack( mw.text.split( slice, '%s*' .. delimiter .. '%s*' ) )
valuesi = tonumber( lang:parseFormattedNumber( value ) )
or error( string.format( 'Slice %d: "%s", first item("%s") could not be parsed as a number', i, value or , sliceStr ) )
colorsi = not nulOrWhitespace( color ) and color or defColorsi * 2
namesi = name or 
linksi = link
end

radius = getArg( 'radius', 150 )
hideGroupLegends = not nulOrWhitespace( argskeywords.hideGroupLegends )
local slicesStr = getArg( 'slices' )
local prefix = getArg( 'unitsPrefix', , '_', ' ' )
local suffix = getArg( 'unitsSuffix', , '_', ' ' )
local percent = argskeywords.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 
legendsi = mw.ustring.format( '%s: %s%s%s%s', namesi, prefix, lang:formatNum( value ), suffix, addprec )
linksi = mw.text.trim( linksi or mw.ustring.format( '%s', legendsi ) )
end
end

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

function createImageMap()
addRes(  )
end

function drawSlice( i, q, start )
local color = colorsi
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, width, border
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 style = string.format( 'position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx', s1, radius, s2, radius, radius, radius )
if start ' ) -- 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


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, accumulateTooltip


local numGroups, numValues
local scaleWidth

function validate()
function asGroups( name, tab, toDuplicate, emptyOK )
if #tab == 0 and not emptyOK then
error( "must supply values for " .. keywordsname )
end
if #tab == 1 and toDuplicate then
for i = 2, numGroups do tabi = tab1 end
end
if #tab > 0 and #tab ~= numGroups then
error ( keywordsname .. ' 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 = #values1
defcolor = defcolor or 'blue'
colors1 = colors1 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 #valuesgi ~= 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 tabi = {} end
for s in mw.text.gsplit( val, '%s*' .. delimiter .. '%s*' ) do
table.insert( i == 0 and tab or tabi, 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 = 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 sumsi = ( sumsi or 0 ) + val end
end
local sum = math.max( unpack( sums ) )
for i = 1, #values do yscalesi = sum end
else
for i, group in ipairs( values ) do yscalesi = math.max( unpack( group ) ) end
end
for i, scale in ipairs( yscales ) do yscalesi = roundup( scale ) end
if not scalePerGroup then for i = 1, #values do yscalesi = math.max( unpack( yscales ) ) end end
end

function tooltip( gi, i, val )
if tooltips and tooltipsgi and not nulOrWhitespace( tooltipsgii ) then return tooltipsgii, true end
local groupName = not nulOrWhitespace( groupNamesgi ) and groupNamesgi .. ': ' or 
local prefix = unitsPrefixgi or unitsPrefix1 or 
local suffix = unitsSuffixgi or unitsSuffix1 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 / yscalesgi * chartHeight + 0.5 ) -- add half to make it "round" insstead of "trunc"
local top, base = chartHeight - barHeight, 0
if stack then
local rawbase = 0
for j = 1, gi - 1 do rawbase = rawbase + valuesji end -- sum the "i" value of all the groups below our group, gi.
base = math.floor( chartHeight * rawbase / yscalesgi ) -- 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 )
if stack 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

function drawbar( gi, i, val, ttval )
local color, tooltip, custom = colorsgi or defcolor or 'blue', tooltip( gi, i, ttval or 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:2px -1px 4px 0 silver;overflow:hidden;",
left, top, barHeight, barWidth, barWidth, color)
local link = linksgi and linksgii or 
local img = not nulOrWhitespace( link ) and mw.ustring.format( '%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 = yscalesgi
local _, top, ordermag = roundup( yscale * 0.999 )
local numnotches = top ' )
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( xlegendsi ) 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 ) }, xlegendsi or  ) )
table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) },  ) )
end
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 ) } ) )
local acum = stack and accumulateTooltip and {}
for gi, group in pairs( values ) do
for i, val in ipairs( group ) do
if acum then acumi = ( acumi or 0 ) + val end
drawbar( gi, i, val, acum and acumi )
end
end
table.insert( res,  )
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,  )
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,  )
table.insert( res,  )
createGroupList( res, groupNames, colors )
table.insert( res,  )
end

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

return {
'bar-chart' = barChart,
keywords.barChart = barChart,
keywords.pieChart = pieChart,
}
--