Chart
From RammWiki
More actions
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 }