Modulo:Graph/sandbox
Aspetto
local p = {}
local cfg = mw.loadData( 'Modulo:Chart/sandbox/Configurazione' );
local function dump(t, ...)
local args = {...}
for _, s in ipairs(args) do
table.insert(t, s)
end
end
-- ===============================================================================
-- Costruisce una struttura dati da codificare in json per realizzare un grafico
-- a torta visualizzabile utilizzando il tag graph
-- https://www.mediawiki.org/wiki/Extension:Graph
-- i nomi dei parametri sono localizzati in base a chart/Config
--
-- ===============================================================================
function build_pie_chart_json(data, args)
local graph = {
name = args[cfg.localization.name] or 'grafico a torta',
data = {
{
name = "table",
values = data,
transform = { { type = "pie", value = "data.x" } }
}
},
marks = {
{
type = "arc",
from = { data = "table"},
properties = {
enter = {
x = { field = "data.x", group = "width", mult = 0.5 },
y = { field = "data.x", group = "height", mult = 0.5 },
startAngle = {field = "startAngle"},
endAngle = {field = "endAngle"},
innerRadius = {value = 0},
outerRadius = {value = tonumber(args[cfg.localization.radius]) or cfg.radius_default },
stroke = {value = "#fff"},
},
update = { fill = { field = "data.color"} },
hover = { fill = {value = "pink"} }
},
}
}
}
if args[cfg.localization.show_label] then
graph.marks[#graph.marks+1] = {type = "text",
from = { data = "table"},
properties = {
enter = {
x = { field = "data.x", group = "width", mult = 0.5 },
y = { field = "data.x", group = "height", mult = 0.5 },
radius = { value = tonumber(args[cfg.localization.radius]) or cfg.radius_default, offset = 8 },
theta = { field = "midAngle"},
fill = { value = "#000" },
align = { value = "center" },
baseline = { value = "middle" },
text = { field = "data.label"}
}
}
}
end
return graph
end
local function legend_item(color, text)
local item = mw.html.create('p'):cssText('margin:0px;font-size:100%;text-align:left')
item:tag('span'):cssText(string.format('border:none;background-color:%s;color:%s;', color, color)):wikitext("██")
item:wikitext(string.format(" %s", text))
return item
end
local function build_legend(data, args)
local legend = mw.html.create('div'):addClass('thumbcaption')
legend:wikitext(args[cfg.localization.caption] or '')
for _,datum in ipairs(data) do
legend:node(legend_item(datum.color, mw.ustring.format('%s (%s %%)', datum.label, mw.getContentLanguage():formatNum(datum.x))))
end
return legend
end
function p.pie_chart(frame)
args = require('Modulo:Arguments').getArgs(frame)
local value_string = cfg.localization.value
local label_string = cfg.localization.label
local color_string = cfg.localization.color
local index = 1
local data = {}
local colors = {}
local labels = {}
while true do
local index_s = tostring(index)
local value = tonumber(args[value_string .. index_s])
if value then
data[index] = {
x = value,
label = args[label_string .. index_s] or '',
color = args[color_string .. index_s] or cfg.default_colors[index] or cfg.default_color
}
index = index + 1
else
break
end
end
-- Se è definito 'other' assumo che sia calcolato su base %, calcolo il suo valore e l'aggiungo alla tabella dati
if args[cfg.localization.other] then
local total = 0
for _,datum in ipairs(data) do
total = total + datum.x
end
local other_value = math.max(0, 100 - total)
data[index] = {
x = other_value,
label = args[cfg.localization.label_other] or 'altri',
color = args[cfg.localization.color_other] or cfg.default_colors[index] or cfg.default_color
}
end
if #data == 0 then
return ''
end
local graph = build_pie_chart_json(data, args)
local json
-- se il parametro debug_json è stato valorizzato ritorna il codice json generato invece di generare il grafico
if args[cfg.localization.debug_json] then
return frame:extensionTag('syntaxhighlight', mw.text.jsonEncode(graph, mw.text.JSON_PRETTY), {lang='json'})
end
local legend = build_legend(data, args)
local html = mw.html.create('div')
:addClass(string.format('thumb t%s', args[cfg.localization.thumb] or 'right'))
html:tag('div')
:addClass('thumbinner')
:cssText('width:202px')
:tag('div')
:cssText("background-color:white;margin:auto;position:relative;width:200px;height:200px;overflow:hidden")
:wikitext(frame:extensionTag( 'graph', mw.text.jsonEncode(graph) ))
:done()
:node(legend)
return html
end
function p._chart(args)
-- chart width
local graphwidth = tonumber(args[cfg.localization.width])
-- chart height
local graphheight = tonumber(args[cfg.localization.height])
-- chart type
local type = args[cfg.localizatio.type] or "line"
-- interpolation mode: linear, step-before, step-after, basis, basis-open, basis-closed (type=line only), bundle (type=line only), cardinal, cardinal-open, cardinal-closed (type=line only), monotone
local interpolate = args[cfg.localization.interpolate]
-- mark colors (if no colors are given, the default 10 color palette is used)
local colors = stringArray(args[cfg.colors]) or "category10"
-- x and y axis caption
local xTitle = args[cfg.localizazion.xAxisTitle]
local yTitle = args[cfg.localizazion.yAxisTitle]
-- override x and y axis minimum and maximum
local xMin = tonumber(args[cfg.localizazion.xAxisMin])
local xMax = tonumber(args[cfg.localizazion.xAxisMax])
local yMin = tonumber(args[cfg.localizazion.yAxisMin])
local yMax = tonumber(args[cfg.localizazion.yAxisMax])
-- override x and y axis label formatting
local xFormat = args[cfg.localizazion.xAxisFormat]
local yFormat = args[cfg.localizazion.yAxisFormat]
-- show legend, optionally caption
local legend = args[cfg.localizazion.legend]
-- format JSON output
local formatJSON = args[cfg.localizazion.formatjson]
-- get x values
local x = numericArray(frame.args.x)
-- get y values (series)
local y = {}
local index = 1
local seriesTitles = {}
if args[y] then
y[1] = NumericArray(value)
seriesTitles[1] = args[string.gsub(cfg.localization.yTitle, '#', '')] or args[string.gsub(cfg.localization.yTitle, '1', '')] or "y"
index = 2
end
while true do
if args[y..index] then
y[#y+1] = numericArray(args[y..index])
seriesTitles[#seriesTitles+1] = args[string.gsub(cfg.localization.yTitle, index, '')] or (y .. index)
end
end
-- create data tuples, consisting of series index, x value, y value
local data = { name = "chart", values = {} }
for i = 1, #y do
for j = 1, #x do
if j <= #y[i] then data.values[#data.values + 1] = { series = seriesTitles[i], x = x[j], y = y[i][j] } end
end
end
-- use stacked charts
local stacked = false
local stats
if string.sub(type, 1, 7) == "stacked" then
type = string.sub(type, 8)
if #y > 1 then -- ignore stacked charts if there is only one series
stacked = true
-- calculate statistics of data as stacking requires cumulative y values
stats =
{
name = "stats", source = "chart", transform =
{
{ type = "facet", keys = { "data.x" } },
{ type = "stats", value = "data.y" }
}
}
end
end
-- create scales
local xscale =
{
name = "x",
type = "linear",
range = "width",
zero = false, -- do not include zero value
nice = true, -- force round numbers for y scale
domain = { data = "chart", field = "data.x" }
}
if xMin then xscale.domainMin = xMin end
if xMax then xscale.domainMax = xMax end
if xMin or xMax then xscale.clamp = true end
if type == "rect" then xscale.type = "ordinal" end
local yscale =
{
name = "y",
type = "linear",
range = "height",
-- area charts have the lower boundary of their filling at y=0 (see marks.properties.enter.y2), therefore these need to start at zero
zero = type ~= "line",
nice = true
}
if yMin then yscale.domainMin = yMin end
if yMax then yscale.domainMax = yMax end
if yMin or yMax then yscale.clamp = true end
if stacked then
yscale.domain = { data = "stats", field = "sum" }
else
yscale.domain = { data = "chart", field = "data.y" }
end
local colorScale =
{
name = "color",
type = "ordinal",
range = colors
}
local alphaScale
-- if there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale
if isTable(colors) then
local alphas = {}
local hasAlpha = false
for i = 1, #colors do
local a, rgb = string.match(colors[i], "#(%x%x)(%x%x%x%x%x%x)")
if a then
hasAlpha = true
alphas[i] = tostring(tonumber(a, 16) / 255.0)
colors[i] = "#" .. rgb
else
alphas[i] = "1"
end
end
for i = #colors + 1, #y do alphas[i] = "1" end
if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end
end
-- for bar charts with multiple series: each series is grouped by the x value, therefore the series need their own scale within each x group
local groupScale
if type == "rect" and not stacked and #y > 1 then
groupScale =
{
name = "series",
type = "ordinal",
range = "width",
domain = { field = "data.series" }
}
xscale.padding = 0.2 -- pad each bar group
end
-- decide if lines (strokes) or areas (fills) should be drawn
local colorField
if type == "line" then colorField = "stroke" else colorField = "fill" end
-- create chart markings
local marks =
{
type = type,
properties =
{
-- chart creation event handler
enter =
{
x = { scale = "x", field = "data.x" },
y = { scale = "y", field = "data.y" }
},
-- chart update event handler
update = { },
-- chart hover event handler
hover = { }
}
}
marks.properties.update[colorField] = { scale = "color" }
marks.properties.hover[colorField] = { value = "red" }
if alphaScale then marks.properties.update[colorField .. "Opacity"] = { scale = "transparency" } end
-- for bars and area charts set the lower bound of their areas
if type == "rect" or type == "area" then
if stacked then
-- for stacked charts this lower bound is cumulative/stacking
marks.properties.enter.y2 = { scale = "y", field = "y2" }
else
--[[
for non-stacking charts the lower bound is y=0
TODO: "yscale.zero" is currently set to "true" for this case, but "false" for all other cases.
For the similar behavior "y2" should actually be set to where y axis crosses the x axis,
if there are only positive or negative values in the data ]]
marks.properties.enter.y2 = { scale = "y", value = 0 }
end
end
-- for bar charts ...
if type == "rect" then
-- set 1 pixel width between the bars
marks.properties.enter.width = { scale = "x", band = true, offset = -1 }
-- for multiple series the bar marking need to use the "inner" series scale, whereas the "outer" x scale is used by the grouping
if not stacked and #y > 1 then
marks.properties.enter.x.scale = "series"
marks.properties.enter.x.field = "data.series"
marks.properties.enter.width.scale = "series"
end
end
-- stacked charts have their own (stacked) y values
if stacked then marks.properties.enter.y.field = "y" end
-- set interpolation mode
if interpolate then marks.properties.enter.interpolate = { value = interpolate } end
if #y == 1 then marks.from = { data = "chart" } else
-- if there are multiple series, connect colors to series
marks.properties.update[colorField].field = "data.series"
if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "data.series" end
-- apply a grouping (facetting) transformation
marks =
{
type = "group",
marks = { marks },
from =
{
data = "chart",
transform =
{
{
type = "facet",
keys = { "data.series" }
}
}
}
}
-- for stacked charts apply a stacking transformation
if stacked then
marks.from.transform[2] = { type = "stack", point = "data.x", height = "data.y" }
else
-- for bar charts the series are side-by-side grouped by x
if type == "rect" then
marks.from.transform[1].keys = "data.x"
marks.scales = { groupScale }
marks.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
end
end
end
-- create legend
if legend then
legend =
{
{
fill = "color",
stroke = "color",
title = legend
}
}
end
-- construct final output object
local output =
{
width = graphwidth,
height = graphheight,
data = { data, stats },
scales = { xscale, yscale, colorScale, alphaScale },
axes =
{
{
type = "x",
scale = "x",
title = xTitle,
format = xFormat
},
{
type = "y",
scale = "y",
title = yTitle,
format = yFormat
}
},
marks = { marks },
legends = legend
}
local flags
if args[cfg.localization.debug_json] then flags = mw.text.JSON_PRETTY end
return mw.text.jsonEncode(output, flags)
end
function p.chart(frame)
local args = getArgs(frame, {parentOnly = true})
local chart = p._chart(args)
if args[cfg.localization.debug_json] then
return frame:extensionTag('syntaxhighlight', mw.text.jsonEncode(graph, mw.text.JSON_PRETTY), {lang='json'})
end
return frame:extensionTag('graph', chart)
end
return p