Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Documentation for this module may be created at Module:Chart/doc

local keywords = {
	pieChart     = "pie chart",
	slices       = "slices",
	slice        = "slice",
	radius       = "radius",
	innerRadius  = "inner radius",
	innerPercent = "inner percent",
}

local defColors = mw.loadData("Module:Chart/Default colors")

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

local function pieChart(frame)
	local args, lang = frame.args, mw.getContentLanguage()
	local function getArg(k, def) return args[keywords[k]] or def or "" end

	-- parse slices: (value:name:color:link)
	local delimiter = args.delimiter or ":"
	local values, colors, names, links = {}, {}, {}, {}

	local fallbackPalette = { "red","blue","green","yellow","fuchsia","aqua","brown","orange","purple","sienna" }

	local function addSlice(i, slice)
		local v, name, col, link = unpack(mw.text.split(slice, "%s*"..delimiter.."%s*"))
		values[i] = tonumber(lang:parseFormattedNumber(v)) or error(("Slice %d not a number: %s"):format(i, v or ""))

		-- name normalization for lookup
		local normName = name and mw.text.trim(name) or nil

		-- resolve color priority: explicit > lookup by name > fallback palette rotation
		if not nulOrWhitespace(col) then
			colors[i] = col
		elseif normName and defColors[normName] then
			colors[i] = defColors[normName]
		else
			colors[i] = fallbackPalette[((i - 1) % #fallbackPalette) + 1]
		end

		if not nulOrWhitespace(name) then
			names[i] = name
		elseif not nulOrWhitespace(link) then
			names[i] = link
		else
			names[i] = "?"
		end
		links[i]  = link or ""
	end

	local i = 0
	local slicesStr = getArg("slices")
	for s in (slicesStr or ""):gmatch("%b()") do
		i = i + 1
		addSlice(i, s:match("^%(%s*(.-)%s*%)$"))
	end
	for k,v in pairs(args) do
		local ind = k:match("^"..keywords.slice.."%s+(%d+)$")
		if ind then addSlice(tonumber(ind), v) end
	end
	if #values == 0 then error("no slices found") end

	-- radii
	local radius = tonumber(getArg("radius", 100)) or 100
	local innerPx = tonumber(getArg("innerRadius",""))
	local innerPct = tonumber(getArg("innerPercent",""))
	local innerR = innerPx and math.max(0, math.min(radius-1, innerPx))
		or (innerPct and radius * math.max(0, math.min(99, innerPct))/100)
		or math.floor(radius * 0.6)

	local size = radius*2
	local stroke = 1
	local pad = stroke/2
	local viewSize = size + stroke
	local cx, cy = radius + pad, radius + pad

	-- arc helpers
	local function arc(x, y, r, a1, a2)
		local x1, y1 = x + r*math.cos(a1), y + r*math.sin(a1)
		local x2, y2 = x + r*math.cos(a2), y + r*math.sin(a2)
		local large = (a2-a1) % (2*math.pi) > math.pi and 1 or 0
		return string.format("A %.3f %.3f 0 %d 1 %.3f %.3f", r, r, large, x2, y2), x1, y1, x2, y2
	end

	local function arcInner(x, y, r, a1, a2)
		local x1, y1 = x + r*math.cos(a1), y + r*math.sin(a1)
		local x2, y2 = x + r*math.cos(a2), y + r*math.sin(a2)
		local large = (a1-a2) % (2*math.pi) > math.pi and 1 or 0
		return string.format("A %.3f %.3f 0 %d 0 %.3f %.3f", r, r, large, x2, y2), x1, y1, x2, y2
	end

	-- build SVG
	local total = 0 for _,v in ipairs(values) do total = total + v end
	local angle = -math.pi/2
	local paths = {}

	for idx,v in ipairs(values) do
		if #values == 1 then
			local tooltip = (names[idx] ~= "" and names[idx] or "?") .. ": " .. v
			local outer = string.format('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="%s" stroke="#fff" stroke-width="1"/>',
				cx, cy, radius, colors[idx])
			local inner = string.format('<circle cx="%.3f" cy="%.3f" r="%.3f" fill="black"/>',
				cx, cy, innerR)
			local group = string.format('<g><title>%s</title>%s%s</g>', mw.text.encode(tooltip), outer, inner)
			if not nulOrWhitespace(links[idx]) then
				local titleObj = mw.title.new(links[idx])
				if titleObj then
					local url = titleObj:localUrl()
					group = string.format('<a xlink:href="%s">%s</a>', mw.text.encode(url), group)
				end
			end
			table.insert(paths, group)
		else
			local a1 = angle
			local a2 = angle + (v/total)*2*math.pi
			local outerArc, xOuterStart, yOuterStart = arc(cx, cy, radius, a1, a2)
			local innerArc, xInnerStart, yInnerStart = arcInner(cx, cy, innerR, a2, a1)
			local d = string.format("M %.3f %.3f %s L %.3f %.3f %s Z",
				xOuterStart, yOuterStart, outerArc, xInnerStart, yInnerStart, innerArc)
			local tooltip = (names[idx] ~= "" and names[idx] or "?") .. ": " .. v
			local path = string.format('<path d="%s" fill="%s" stroke="#fff" stroke-width="1"><title>%s</title></path>',
				d, colors[idx], mw.text.encode(tooltip))
			if not nulOrWhitespace(links[idx]) then
				local titleObj = mw.title.new(links[idx])
				if titleObj then
					local url = titleObj:localUrl()
					path = string.format('<a xlink:href="%s">%s</a>', mw.text.encode(url), path)
				end
			end
			table.insert(paths, path)
			angle = a2
		end
	end

	local svg = string.format(
		'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' ..
		'width="%d" height="%d" viewBox="0 0 %d %d" style="background:none">%s</svg>',
		size, size, viewSize, viewSize, table.concat(paths, "\n")
	)

	return frame:callParserFunction{ name = "#tag", args = { "html", svg } }
end

return { [keywords.pieChart] = pieChart }