Modulo:Graph/sandbox
Aspetto
local p = {}
local cfg = mw.loadData( 'Modulo:Chart/sandbox/Configurazione' );
local getArgs = require('Module:Arguments').getArgs
local errors = { }
local function dump(t, ...)
local args = {...}
for _, s in ipairs(args) do
table.insert(t, s)
end
end
-- ===============================================================================
-- Add error to errors list
-- ===============================================================================
local function add_error(msg, ...)
if arg then
errors[#errors+1] = mw.ustring.format(msg, ...)
else
errors[#errors+1] = msg
end
end
-- ===============================================================================
-- Consolidate errors message and add error category
-- ===============================================================================
local function errors_output(nocat)
if #errors > 0 then
local out = string.format('<strong class="error">%s</strong>', table.concat(errors, "; "))
if nocat or not cfg.uncategorized_namespaces[mw.title.getCurrentTitle().ns] then
out = out .. '[[Category:' .. cfg.errors_category .. ']]'
end
return out
end
return ''
end
-- ===============================================================================
-- Return true if val is not nil and is a string in the array cfg.yes_values
-- ===============================================================================
local function return_yes_value(val)
return val and cfg.yes_values[mw.ustring.lower(val)]
end
-- ===============================================================================
-- Return true if val is not nil and is a string in the array cfg.no_value
-- ===============================================================================
local function return_no_value(val)
return val and cfg.no_values[mw.ustring.lower(val)]
end
-- ===============================================================================
-- Return an array of value comprise between axes_min and axes_max
-- ===============================================================================
local function return_axis_thick_values(axes_min, axes_max, thick_interval)
local thick_values = {}
local thick = axes_min + thick_interval
while thick < axes_min do
thick_values[#thick_values+1] = thick
thick = thick + thick_interval
end
end
-- ===============================================================================
-- Return an array of numbers splitting a string at ","
-- Check for the presence of an alternative separator and alternative
-- symbol for decimal separator
-- ===============================================================================
local function numericArray(csv)
if not csv then return end
local list = {}
-- se csv contiene un ";" assume che i valori siano separati da ";" e il separatore decimale è "."
if mw.ustring.find(csv, cfg.separator.list) then
list = mw.text.split(mw.ustring.gsub(csv, "%s", ""), cfg.separator.list)
for index,v in ipairs(list) do
list[index] = mw.ustring.gsub(v, cfg.separator.decimal, ".")
end
else
list = mw.text.split(mw.ustring.gsub(csv, "%s", ""), ",")
end
local result = {}
for i, val in ipairs(list) do
if val == '' then
result[i] = 'x'
else
result[i] = tonumber(val)
end
end
return result
end
-- ===============================================================================
-- Return an array of string splitting at ","
-- ===============================================================================
local function stringArray(csv)
if not csv then return end
local t = {}
for s in mw.text.gsplit(csv, ",") do
t[#t+1] = mw.text.trim(s)
end
return t
end
-- ==============================================================================
-- Return true if t is a table
-- ===============================================================================
local function isTable(t)
return type(t) == "table"
end
-- ==============================================================================
-- Extend table replicating content
-- ==============================================================================
local function extend_table(t, new_len)
if #t >= new_len then return t end
local pos = 1
local old_len = #t
for i = #t+1, new_len do
t[i] = t[pos]
pos = pos + 1
if pos > old_len then pos = 1 end
end
return t
end
-- ==============================================================================
-- Generate a color palette from the name (must be in cfg.colors_palette)
-- ==============================================================================
local function generate_color_palette(palette, len)
if not cfg.colors_palette[palette] then return nil end
local palette_len, color_palette = cfg.colors_palette[palette][1],cfg.colors_palette[palette][2]
local t = {}
local pos = 1
for i = 1, len do
t[i] = color_palette[pos]
pos = pos + 1
if pos > palette_len then pos = 1 end
end
return t
end
local function wrap_graph(align, graph, legend)
local html = mw.html.create('div')
:addClass(string.format('thumb t%s', align or 'right'))
html:tag('div')
:addClass('thumbinner')
:wikitext(graph)
--:cssText(string.format('width:%s', tostring(width)))
--: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 tostring(html)
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.default.radius },
stroke = {value = "#fff"},
},
update = { fill = { field = "data.color"} },
hover = { fill = {value = "red"} }
},
}
}
}
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
-- OLD da rivedere per integrazione nella pie_chart
--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
local function build_legend(colors, labels, title)
local legend = mw.html.create('div'):addClass('thumbcaption')
legend:wikitext(title or '')
if not isTable(colors) then
colors = generate_color_palette(colors, #labels)
end
for i,label in ipairs(labels) do
legend:node(legend_item(colors[i], label))
end
return legend
end
local function serie_min(serie)
local minimum
for index,value in ipairs(serie) do
if index == 1 or value < minimum then
minimum = value
end
end
return minimum
end
local function serie_max(serie)
local maximum
for index,value in ipairs(serie) do
if index == 1 or value > maximum then
maximum = value
end
end
return maximum
end
function p.pie_chart(frame)
args = getArgs(frame, {parentOnly = true})
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.colors_palette.category10[index] or
cfg.colors_palette.category10[#cfg.colors_palette.category10]
}
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.colors_palette.category10[index] or
cfg.colors_palette.category10[#cfg.colors_palette.category10]
}
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
local function build_ax(args, ax_name)
local ax = {}
local title = args[cfg.localization[ax_name .. 'AxisTitle']]
local format = args[cfg.localization[ax_name .. 'AxisFormat']]
local grid = cfg.default[ax_name .. 'Grid']
if grid then
grid = not return_no_value(args[cfg.localization[ax_name .. 'Grid']])
else
grid = return_yes_value(args[cfg.localization[ax_name .. 'Grid']])
end
ax = {
type = ax_name,
scale = ax_name,
title = title,
format = format,
grid = grid,
layer = "back"
}
if args[cfg.localization[ax_name .. 'AxisPrimaryThick']] then
ax.values = numericArray(args[cfg.localization[ax_name .. 'AxisPrimaryThick']])
elseif args[cfg.localization[ax_name .. 'AxisPrimaryThickNumber']] then
local n_thicks = tonumber(args[cfg.localization[ax_name .. 'AxisPrimaryThickNumber']])
if n_thicks then
args.thicks = n_thicks
end
end
if args[cfg.localization[ax_name .. 'AxisSecondaryThick']] then
local secondary_thick = tonumber(args[cfg.localization[ax_name .. 'AxisSecondaryThick']])
if secondary_thick then
ax.subdivide = secondary_thick
end
end
return ax
end
local function get_graph_type(graph_string)
if graph_string == nil then return "line", false end
local graph_type = cfg.graph_type[mw.ustring.lower(graph_string)]
if graph_type then return graph_type[1], graph_type[2] end
add_error("tipo di grafico %s non riconosciuto", graph_type)
end
-- ===================================================================================
-- Imported and modified from en:Module:Chart revision 670068988 of 5 july 2015
-- ===================================================================================
function p._chart(args)
local graphwidth = tonumber(args[cfg.localization.width]) or cfg.default.width
local graphheight = tonumber(args[cfg.localization.height]) or cfg.default.height
local graph_type, is_stacked = get_graph_type(args[cfg.localization.type])
local interpolate = args[cfg.localization.interpolate]
if interpolate and not cfg.interpolate[interpolate] then
add_error('Valore per %s "%s" non valido', cfg.localization.interpolate, interpolate)
interpolate = nil
end
-- get marks colors, use default category10 palette as default,
-- if colors is not the name of a palette then read it as an array of colors
local colors = args[cfg.localization.colors]
if colors == nil then
colors = "category10"
elseif not cfg.colors_palette[colors] then
colors = stringArray(colors)
end
-- get marks symbols, default symbol is used if the type of graph is line, otherwise the default
-- is not to use symbol.
local symbols = args[cfg.localization.symbols] or cfg.default.symbol
if graph_type ~= "line" or symbols == "no" then
symbols = nil
else
symbols = stringArray(symbols)
end
local symbol_size = tonumber(args[cfg.localization.symbolSize]) or cfg.default.symbol_size
local stroke_thickness
if graph_type =="line" then
stroke_thickness = numericArray(args[cfg.localization.strokeThickness])
end
-- override x and y axis minimum and maximum
local xMin = tonumber(args[cfg.localization.xAxisMin])
local xMax = tonumber(args[cfg.localization.xAxisMax])
local yMin = tonumber(args[cfg.localization.yAxisMin])
local yMax = tonumber(args[cfg.localization.yAxisMax])
-- show legend, optionally caption
local internal_legend = args[cfg.localization.internal_legend]
-- get x values
local x = numericArray(args.x)
local force_x_ordinal = false
if #x == 0 then
force_x_ordinal = true
else
for _,val in ipairs(x) do
if val == 'x' then force_x_ordinal = true end
break
end
end
if force_x_ordinal then x = stringArray(args.x) end
-- get y values (series)
local y = {}
local index = 1
local seriesTitles = {}
if args.y then
y[1] = numericArray(args.y)
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'..tostring(index)] then
y[index] = numericArray(args['y'..tostring(index)])
seriesTitles[#seriesTitles+1] = args[string.gsub(cfg.localization.yTitle, '#', tostring(index))] or ('y' .. tostring(index))
index = index + 1
else
break
end
end
-- ignore stacked charts if there is only one series
if #y == 1 then is_stacked = false end
-- assure that colors, stroke_thickness and symbols table are at least the same lenght that the number of
-- y series
if isTable(colors) then colors = extend_table(colors, #y) end
if isTable(stroke_thickness) then stroke_thickness = extend_table(stroke_thickness, #y) end
if isTable(symbols) then symbols = extend_table(symbols, #y) end
-- build axes
x_ax = build_ax(args, 'x')
y_ax = build_ax(args, 'y')
axes = { x_ax, y_ax }
-- create data tuples, consisting of series index, x value, y value
local data = { name = "chart", values = {} }
for i, yserie in ipairs(y) do
for j = 1, math.min(#yserie, #x) do
if yserie[j] ~= 'x' then data.values[#data.values + 1] = { series = seriesTitles[i], x = x[j], y = yserie[j] } end
end
end
-- calculate statistics of data as stacking requires cumulative y values
local stats
if is_stacked then
stats =
{
name = "stats", source = "chart", transform =
{
{ type = "facet", keys = { "data.x" } },
{ type = "stats", value = "data.y" }
}
}
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 graph_type == "rect" or force_x_ordinal 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 = graph_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 is_stacked then
yscale.domain = { data = "stats", field = "sum" }
else
yscale.domain = { data = "chart", field = "data.y" }
end
-- Color scale
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, color in ipairs(colors) do
local a, rgb = string.match(color, "#(%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", graph_type = "ordinal", range = alphas } end
end
-- Stroke width scale
local strokeScale
if stroke_thickness then strokeScale = { name = "stroke", type = "ordinal", range = stroke_thickness } end
-- Symbols scale
local symbolsScale
if symbols then symbolsScale = { name = "symbols", type = "ordinal", range = symbols } 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 graph_type == "rect" and not is_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 graph_type == "line" then colorField = "stroke" else colorField = "fill" end
-- create chart markings
local marks =
{
type = graph_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 = { }
}
}
if strokeScale then
marks.properties.enter.strokeWidth = { scale = "stroke"}
end
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 graph_type == "rect" or graph_type == "area" then
if is_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 graph_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 is_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 is_stacked then marks.properties.enter.y.field = "y" end
-- set interpolation mode
if interpolate then marks.properties.enter.interpolate = { value = interpolate } end
local symbolsMarks
if symbolsScale then
symbolsMarks = {
type = "symbol",
from = { data = "chart" },
properties = {
enter =
{
x = { scale = "x", field = "data.x" },
y = { scale = "y", field = "data.y" },
shape = { scale = "symbols" },
size = { value = symbol_size }
},
update = { stroke = { scale = "color"} }
}
}
end
if #y == 1 then
marks.from = { data = "chart" }
marks = { marks, symbolsMarks }
else
-- if there are multiple series, connect colors to series
if graph_type == "rect" and return_yes_value(args[cfg.localization.colorsByGroup]) then
marks.properties.update[colorField].field = "data.x"
else
marks.properties.update[colorField].field = "data.series"
end
if symbolsScale then symbolsMarks.properties.enter.shape.field = "data.series" end
if strokeScale then marks.properties.enter.strokeWidth.field = "data.series" end
if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "data.series" end
-- apply a grouping (facetting) transformation
marks =
{
type = "group",
marks = { marks, symbolsMarks },
from =
{
data = "chart",
transform =
{
{
type = "facet",
keys = { "data.series" }
}
}
}
}
-- for stacked charts apply a stacking transformation
if is_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 graph_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
marks = { marks }
end
-- create legend
if internal_legend then
legend =
{
{
fill = "color",
stroke = "color",
title = internal_legend
}
}
end
-- construct final output object
local scales = { xscale, yscale, colorScale}
if alphaScale then scales[#scales+1] = alphaScale end
if strokeScale then scales[#scales+1] = strokeScale end
if symbolsScale then scales[#scales+1] = symbolsScale end
local output =
{
width = graphwidth,
height = graphheight,
data = { data, stats },
scales =scales,
axes = axes,
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), args[cfg.localization.external_legend], colors, seriesTitles
end
function p.chart(frame)
local args = getArgs(frame, {parentOnly = true})
local chart_json, external_legend, colors, labels = p._chart(args)
local legend
if external_legend then
legend = build_legend(colors, labels, external_legend)
end
if args[cfg.localization.debug_json] then
return frame:extensionTag('syntaxhighlight', chart_json)
end
local chart= frame:extensionTag('graph', chart_json)
local align = args[cfg.localization.thumb]
return wrap_graph(align, chart, legend) .. errors_output(args.NoTracking)
end
return p