Bước tới nội dung

Mô đun:Piechart

Bách khoa toàn thư mở Wikipedia

local p = {}local priv = {} p.__priv = privlocal forPrinting = "-webkit-print-color-adjust: exact; print-color-adjust: exact;"function p.color(frame)	local index = tonumber(priv.trim(frame.args[1]))	return ' ' .. priv.defaultColor(index)endfunction p.pie(frame)	local json_data = priv.trim(frame.args[1])	local options = {}	if (frame.args.meta) then		options.meta = priv.trim(frame.args.meta)	end	local html = p.renderPie(json_data, options)	return priv.trim(html)endfunction p.setupOptions(user_options)	local options = {		size = 100,		autoscale = false,		ariahidechart = false,		legend = false,		direction = "",		width = "",		caption = "",		footer = "",		labelformat = "",	}	options.style = ""	if user_options and user_options.meta then		local decodeSuccess, rawOptions = pcall(function()			return mw.text.jsonDecode(user_options.meta, mw.text.JSON_TRY_FIXING)		end)		if not decodeSuccess then			rawOptions = false		end		if rawOptions then			if type(rawOptions.size) == "number" then				options.size = math.floor(rawOptions.size)			end			options.autoscale = rawOptions.autoscale or false 			if rawOptions.legend then options.legend = true end			if rawOptions.ariahidechart then options.ariahidechart = true end			if (type(rawOptions.direction) == "string") then				local sanitized = rawOptions.direction:gsub("[^a-z0-9%-]", "")				options.direction = 'flex-direction: ' .. sanitized .. ';'				options.width = 'width: max-content;'			end			if (type(rawOptions.width) == "string") then				local sanitized = rawOptions.width:gsub("[^a-z0-9%-]", "")				options.width = 'width: ' .. sanitized .. ';'			end			if (type(rawOptions.caption) == "string") then options.caption = rawOptions.caption end			if (type(rawOptions.footer) == "string") then options.footer = rawOptions.footer end			if (type(rawOptions.labelformat) == "string") then options.labelformat = rawOptions.labelformat end		end		if options.width ~= "" then options.style = options.style .. options.width end		if options.direction ~= "" then options.style = options.style .. options.direction end	end	if (options.legend) then options.ariahidechart = true end	return optionsendp.__priv.legendDebug = falsefunction p.renderPie(json_data, user_options)	if type(json_data) ~= "string" or #json_data < 2 then error('invalid piechart data') end	local decodeSuccess, data = pcall(function()		return mw.text.jsonDecode(json_data, mw.text.JSON_TRY_FIXING)	end)	if not decodeSuccess then error('invalid piechart data: '..json_data) end	local options = p.setupOptions(user_options)	local ok, total = p.prepareEntries(data, options)	local html = "<div class='smooth-pie-container' style='"..options.style.."'>"	if not ok then html = html .. priv.renderErrors(data) end	if options.legend then html = html .. p.renderLegend(data, options) end	if p.__priv.legendDebug then return html end	local header, items, footer = p.renderEntries(ok, total, data, options)	html = html .. header .. items .. footer	html = html .. "\n</div>"	return htmlendfunction priv.boundaryFormatting(diff)	local value = 0.0	if diff <= 1.0 then value = math.ceil(diff / 0.2) * 0.2 	else value = math.ceil(diff / 0.5) * 0.5 end	return string.format("%.1f", value)endfunction priv.willAutoscale(sum)	local diff = sum - 100	local grace = 1	return diff > graceendfunction priv.sumErrorTracking(sum, items)    -- Disabled on viwiki to reduce category clutterendfunction p.prepareEntries(data, options)	local sum = priv.sumValues(data);	if priv.willAutoscale(sum) then options.autoscale = true end	local ok = true	local no = 0	local total = #data	for index, entry in ipairs(data) do		no = no + 1		if not priv.prepareSlice(entry, no, sum, total, options) then			no = no - 1			ok = false		end	end	total = no	return ok, totalendfunction priv.sumValues(data)	local sum = 0;	for _, entry in ipairs(data) do		local value = entry.value		if not (type(value) ~= "number" or value < 0) then sum = sum + value end	end	return sumendfunction priv.renderErrors(data)	local html = "\n<ol class='chart-errors' style='display:none'>"	for _, entry in ipairs(data) do		if entry.error then			local entryJson = mw.text.jsonEncode(entry)			html = html .. "\n<li>".. entryJson .."</li>"		end	end	return html .. "\n</ol>\n"endfunction priv.prepareSlice(entry, no, sum, total, options)	local autoscale = options.autoscale	local value = entry.value	if (type(value) ~= "number" or value < 0) then		if autoscale then			entry.error = "cannot autoscale unknown value"			return false		end		value = 100 - sum	end	if autoscale then		entry.raw = value		value = (value / sum) * 100	end	entry.value = value	entry.label = priv.prepareLabel(options.labelformat, entry)	entry.bcolor = priv.backColor(entry, no, total) .. ";color:#000"	return trueendfunction p.renderLegend(data, options)	local html = ""	if options.caption ~= "" or options.footer ~= "" then		html = "\n<div class='smooth-pie-legend-container'>"	end	if options.caption ~= "" then		html = html .. "<div class='smooth-pie-caption'>" .. options.caption .. "</div>"	end	html = html .. "\n<ol class='smooth-pie-legend'>"	for _, entry in ipairs(data) do		if not entry.error then html = html .. priv.renderLegendItem(entry, options) end	end	html = html .. "\n</ol>\n"	if options.footer ~= "" then		html = html .. "<div class='smooth-pie-footer'>" .. options.footer .. "</div>"	end	if options.caption ~= "" or options.footer ~= "" then html = html .. "</div>\n" end	return htmlendfunction priv.renderLegendItem(entry, options)	if entry.visible ~= nil and entry.visible == false then return "" end	local label = entry.label	local bcolor = entry.bcolor	local html = "\n<li>"	if p.__priv.legendDebug then forPrinting = "" end	html = html .. '<span class="l-color" style="'..forPrinting..bcolor..'"></span>'	html = html .. '<span class="l-label">'..label..'</span>'	return html .. "</li>"endfunction p.renderEntries(ok, total, data, options)    -- Fallback cuts if json load fails	p.cuts = {0.000000,0.062832,0.125664,0.188496,0.251327,0.314159,0.376991,0.439823,0.502655}    local success, loadedCuts = pcall(mw.loadJsonData, 'Module:Piechart/cuts.json')    if success then p.cuts = loadedCuts end	local first = true	local previous = 0	local items = ""	for index, entry in ipairs(data) do		if not entry.error then			items = items .. priv.renderItem(previous, entry, options)			previous = previous + entry.value		end	end	local header = priv.renderHeader(options)	local footer = '\n<div class="smooth-pie-border"></div></div>'	return header, items, footerendfunction priv.renderHeader(options)	local bcolor = 'background:#888;color:#000'	local size = options.size	local aria = ""	if (options.ariahidechart) then aria = 'aria-hidden="true"' end	local style = 'width:'..size..'px;height:'..size..'px;'..bcolor..';'..forPrinting	local html = [[<div class="smooth-pie" style="]]..style..[[" ]]..aria..[[>]]	return htmlendfunction priv.renderItem(previous, entry, options)	local value = entry.value	local label = entry.label	local bcolor = entry.bcolor	if (value < 0.03) then return "" end	if value < 10 then		if previous > 1 then previous = previous - 0.01 end		value = value + 0.02	else		if previous > 1 then previous = previous - 0.1 end		value = value + 0.2	end	if previous + value > 100 then		if previous >= 100 then return "" end		value = 100 - previous	end	local html =  ""	if (value >= 50) then		html = priv.sliceWithClass('pie50', 50, value, previous, bcolor, label)	elseif (value >= 25) then		html = priv.sliceWithClass('pie25', 25, value, previous, bcolor, label)	elseif (value >= 12.5) then		html = priv.sliceWithClass('pie12-5', 12.5, value, previous, bcolor, label)	elseif (value >= 7) then		html = priv.sliceWithClass('pie7', 7, value, previous, bcolor, label)	elseif (value >= 5) then		html = priv.sliceWithClass('pie5', 5, value, previous, bcolor, label)	else		local cutIndex = priv.round(value*10)		if cutIndex < 1 then cutIndex = 1 end		local cut = p.cuts[cutIndex] or 0		local transform = priv.rotation(previous)		html = priv.sliceX(cut, transform, bcolor, label)	end		return htmlendfunction priv.round(number) return math.floor(number + 0.5) endfunction priv.sliceWithClass(sizeClass, sizeStep, value, previous, bcolor, label)	local transform = priv.rotation(previous)	local html =  ""	html = html .. priv.sliceBase(sizeClass, transform, bcolor, label)	if (value > sizeStep) then		local extra = value - sizeStep		transform = priv.rotation(previous + extra)		html = html .. priv.sliceBase(sizeClass, transform, bcolor, label)	end	return htmlendfunction priv.sliceBase(sizeClass, transform, bcolor, label)	local style = bcolor	if transform ~= "" then style = style .. '; ' .. transform end	return '\n\t<div class="'..sizeClass..'" style="'..style..'" title="'..p.extract_text(label)..'"></div>'endfunction priv.sliceX(cut, transform, bcolor, label)	local path = 'clip-path: polygon(0% 0%, '..cut..'% 0%, 0 100%)'	return '\n\t<div style="'..transform..'; '..bcolor..'; '..path..'" title="'..p.extract_text(label)..'"></div>'endfunction priv.rotation(value)	if (value > 0.001) then		local f = string.format("%.7f", value / 100)		f = f:gsub("(%d)0+$", "%1") 		return "transform: rotate("..f.."turn)"	end	return ''endfunction priv.formatNum(value)	local v = ""	if (value < 10) then v = string.format("%.2f", value)	else v = string.format("%.1f", value) end	-- Vietnamese uses comma for decimal separator	v = v:gsub("%.", ",")	return vendfunction priv.formatLargeNum(value)	local lang = mw.language.getContentLanguage()	local v = lang:formatNum(value)	return vendfunction priv.prepareLabel(tpl, entry)	if not tpl or tpl == "" then		if not entry.label or entry.label == "" then			tpl = "$v"		else			if entry.label:find("%$[a-z]") then tpl = entry.label			else tpl = "$L: $v" end		end	end	local labelLabel = entry.label and entry.label or priv.getLangOther()	local pRaw = priv.formatNum(entry.value)	local pp = pRaw .. "%%"	local label = tpl	label = label:gsub("%$label", "$L"):gsub("%$auto", "$v"):gsub("%$value", "$d"):gsub("%$percent", "$p")	label = label:gsub("%$L", labelLabel)	local d = priv.formatLargeNum(entry.raw and entry.raw or entry.value)	local v = entry.raw and (d .. " (" .. pp .. ")") or pp	label = label:gsub("%$p", pp):gsub("%$d", d):gsub("%$v", v)	return labelendlocal colorPalette = {'#1b7837','#7fbf7b','#d9f0d3','#762a83','#af8dc3','#e7d4e8','#d73027','#fc8d59','#fee090','#4575b4','#91bfdb','#e0f3f8',}local lastColor = '#fff'function priv.backColor(entry, no, total)	if (type(entry.color) == "string") then		local sanitizedColor = entry.color:gsub('&#35;', '#'):gsub("[^a-zA-Z0-9#%-]", "")		return 'background:' .. sanitizedColor	else		local color = priv.defaultColor(no, total)		return 'background:' .. color	endendfunction priv.defaultColor(no, total)	local color = lastColor	if no <= 0 then return color end	local size = #colorPalette	if not total or total == 0 then total = size + 1 end	local colorNo = priv.defaultColorNo(no, total, size)	if colorNo > 0 then color = colorPalette[colorNo] end	return colorendfunction priv.defaultColorNo(no, total, size)	local color = 0 	local colorGroupSize = 3	local colorGroups = 4	if total == 1 then color = 1	elseif total <= colorGroupSize * (colorGroups - 1) then		if no < total then color = no		else			local groupIndex = ((no - 1) % colorGroupSize)			if groupIndex == 0 or groupIndex == 1 then color = no+1			else color = no end		end	elseif no < total then		color = ((no - 1) % size) + 1	end	return colorendfunction priv.trim(s)	return (s:gsub("^%s+", ""):gsub("%s+$", ""))endfunction p.extract_text(label)	label = label:gsub("%[%[[^|%]]+|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1"):gsub("<[^>]+>", ""):gsub("<", "&lt;"):gsub(">", "&gt;"):gsub("'", "&#39;"):gsub("\"", "&quot;")	return labelendfunction p.parseEnumParams(frame)	local args = frame:getParent().args	return priv.parseEnumParams(args)endfunction priv.parseEnumParams(args)	local result = {}	local i = 1	local sum = 0.0	local hasCustomColor = false 	while args["value" .. i] do		local entry = { value = tonumber(args["value" .. i]) or 0 }		local label = args["label" .. i]		if label and label ~= "" then entry.label = label end		hasCustomColor = false		local color = args["color" .. i]		if color and color ~= "" then			entry.color = color			hasCustomColor = true		end		table.insert(result, entry)		sum = sum + entry.value		i = i + 1	end	local willAutoscale = priv.willAutoscale(sum)	for _, entry in ipairs(result) do		local label = entry.label		if label and not label:find("%$[a-z]") then			if willAutoscale then entry.label = label .. " $v"			else entry.label = label .. " ($p)" end		end	end		-- Support for 'other' or 'khác'	local langOther = priv.getLangOther()	local colorOther = "#FEFDFD"	local otherValue = 100 - sum	local otherParam = args["other"] or args["khác"]	local otherLabelParam = args["other-label"] or args["nhãn khác"]	local otherColorParam = args["other-color"] or args["màu khác"]	if otherParam and otherParam ~= "" then		if otherValue < 0.001 then otherValue = 0 end		local otherEntry = { label = (otherLabelParam or langOther) .. " ($p)" }		if otherColorParam and otherColorParam ~= "" then			otherEntry.color = otherColorParam		else			otherEntry.color = colorOther		end		table.insert(result, otherEntry)	elseif otherValue > 0.01 then		if hasCustomColor then			table.insert(result, {visible = false, label = langOther .. " ($v)", color = colorOther})		else			table.insert(result, {visible = false, label = langOther .. " ($v)"})		end	end	return mw.text.jsonEncode(result)endfunction priv.getLangOther()	return "Khác"endlocal trueValues = { ["true"] = true, ["1"] = true, ["on"] = true, ["yes"] = true, ["có"] = true }function priv.isTrueishValue(value)	if not value or value == "" then return nil end	value = priv.trim(value)	if value == "" then return nil end	return trueValues[value:lower()] or falseendfunction p.parseMetaParams(frame)	local args = frame:getParent().args	local meta = {}	local thumb = args["thumb"]	if args["value1"] or (thumb and (thumb == "right" or thumb == "left")) then		meta.size = 200		meta.legend = true	end	if args["meta"] then		local decodeSuccess, tempMeta = pcall(function()			return mw.text.jsonDecode(args["meta"], mw.text.JSON_TRY_FIXING)		end)		if decodeSuccess then meta = tempMeta end	end	if args["size"] then meta.size = tonumber(args["size"]) end	if args["radius"] and tonumber(args["radius"]) then meta.size = 2 * tonumber(args["radius"]) end	if args["autoscale"] then meta.autoscale = priv.isTrueishValue(args["autoscale"]) end	if args["legend"] then meta.legend = priv.isTrueishValue(args["legend"]) end	if args["ariahidechart"] then meta.ariahidechart = priv.isTrueishValue(args["ariahidechart"]) end	if args["direction"] and args["direction"] ~= "" then meta.direction = args["direction"]:gsub("[^a-z0-9%-]", "") end	if args["width"] and args["width"] ~= "" then meta.width = args["width"]:gsub("[^a-z0-9%-]", "") end		if args["caption"] and args["caption"] ~= "" then meta.caption = args["caption"] end	if args["chú thích"] and args["chú thích"] ~= "" then meta.caption = args["chú thích"] end		if args["footer"] and args["footer"] ~= "" then meta.footer = args["footer"] end	if args["labelformat"] and args["labelformat"] ~= "" then meta.labelformat = args["labelformat"] end	return mw.text.jsonEncode(meta)endreturn p