모듈:Chart

--
--
keywords are used for languages: they are the names of the actual
parameters of the template
local keywords = {
barChart = '막대 도표',
pieChart = '원 도표',
width = '넓이',
height = '높이',
stack = '쌓기',
colors = '색',
group = '집합',
xlegend = 'x 범례',
	yticks = 'y 틱 마크',
tooltip = '말풍선',
accumulateTooltip = '말풍선 합계',
links = '연결',
defcolor = '기본색',
scalePerGroup = '집합 축척',
unitsPrefix = '머리 단위',
unitsSuffix = '꼬리 단위',
groupNames = '집합 이름',
hideGroupLegends = '집합 범례 숨김',
slices = '조각합',
slice = '조각',
radius = '반지름',
percent = '백분율',
} -- 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', { style = string.format( spanStyle, colsgi, colsgi ) }, ' ' ) .. ' '..  legendsgi
			table.insert( list, mw.text.tag( 'li', {}, span ) )
		end
		table.insert( tab,
			mw.text.tag( 'ul',
				{style="width:100%;list-style:none;column-width:12em;"},
				table.concat( list, '\n' )
			)
		)
		table.insert( tab,  )
	end
end
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 = argskeywordss 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*' ) )
			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 , slice ) )
			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 = 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 
			legendsi = string.format( '%s: %s%s%s%s', namesi, prefix, lang:formatNum( value ), suffix, addprec )
			linksi = mw.text.trim( linksi or string.format( '%s', legendsi ) )
		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 = 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, 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 ' ) -- 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 " .. 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 .. ' 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 = #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.', keywords.stack, keywords.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 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 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
	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 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 * 0.9999 ) end
		if not scalePerGroup then for i = 1, #values do yscalesi = math.max( unpack( yscales ) ) end end
	end
	local function tooltip( gi, i, val )
		if tooltips and tooltipsgi and not nulOrWhitespace( tooltipsgii ) then return tooltipsgii, true end
		local groupName = mw.text.killMarkers(not nulOrWhitespace( groupNamesgi ) and groupNamesgi .. ': ' or )
		local prefix = unitsPrefixgi or unitsPrefix1 or 
		local suffix = unitsSuffixgi or unitsSuffix1 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 / yscalesgi * 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(valuesji) > 0 then
					base = base + math.max( 2, math.floor( valuesji / yscalesgi * 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 = colorsgi 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 = linksgi and linksgii or 
		local img = not nulOrWhitespace( link ) and string.format( '%s', link, custom and tooltip or  ) or 
		table.insert( res, mw.text.tag( 'div', { style = style, title = tooltip, }, img ) )
	end
	local function drawYScale()
		local function drawSingle( gi, color, width, yticks, single )
			local yscale = yscalesgi
			local _, top, ordermag = roundup( yscale * 0.999 )
			local numnotches = yticks >= 0 and yticks or
					(top ' )
			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( xlegendsi ) then
				setOffset, setWidth = groupBounds( i )
				-- setWidth = 0.85 * setWidth
				table.insert( res, mw.text.tag( 'div', { style = string.format( legendDivStyleFormat, setOffset + 1, setWidth - 2, setWidth - 2 ) }, xlegendsi or  ) )
				table.insert( res, mw.text.tag( 'div', { style = string.format( tickDivstyleFormat, setOffset + setWidth / 2 ) },  ) )
			end
		end
	end
	local function drawChart()
		table.insert( res, mw.text.tag( 'div', { class = 'chart noresize', style = string.format( '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 ) 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,
	'bar chart' = barChart,
	'pie chart' = pieChart,
	keywords.barChart = barChart,
	keywords.pieChart = pieChart,
}