Modulo:Graph: differenze tra le versioni

Contenuto cancellato Contenuto aggiunto
Yurik (discussione | contributi)
updated to latest version from w:de:Module:Graph
m correggo
 
(16 versioni intermedie di 3 utenti non mostrate)
Riga 1:
-- version 2016-01-06 _PLEASE UPDATE when modifying anything_
local p = {}
local cfg = mw.loadData( 'Module:Graph/Configurazione' );
local getArgs = require('Module:Arguments').getArgs
local errors = { }
 
local function dump(t, ...)
local baseMapDirectory = "Module:Graph/"
local args = {...}
for _, s in ipairs(args) do
table.insert(t, s)
end
end
 
-- ===============================================================================
local function numericArray(csv)
-- Add error message to errors list, mgs_key must be a error key listed in
if not csv then return end
-- cfg.errors_key, args is an optional array of string
-- ===============================================================================
local function add_error(msg_key, args)
local msg = cfg.errors_key[msg_key]
if not msg then msg = cfg.errors_key.unknown_error end
if args then
errors[#errors+1] = mw.ustring.format(msg, unpack(args))
else
errors[#errors+1] = msg
end
end
 
-- ===============================================================================
local list = mw.text.split(csv, "%s*,%s*")
-- Consolidate errors messages and add error category
local result = {}
-- ===============================================================================
local isInteger = true
local function errors_output(nocat)
for i = 1, #list do
if #errors > 0 then
result[i] = tonumber(list[i])
local out = string.format('<strong class="error">%s</strong>', table.concat(errors, "; "))
if not result[i] then return end
if nocat or not cfg.uncategorized_namespaces[mw.title.getCurrentTitle().nsText] then
if isInteger then
out = out .. '[[Category:' .. cfg.errors_category .. ']]'
local int, frac = math.modf(result[i])
isInteger = frac == 0.0
end
return out
end
return result, isInteger''
end
 
-- ==============================================================================
local function stringArray(csv)
-- Return true if t is a table
if not csv then return end
-- ===============================================================================
local function isTable(t)
return type(t) == "table"
end
 
-- ===============================================================================
return mw.text.split(csv, "%s*,%s*")
-- Class to manage access to arguments
-- ===============================================================================
local Args = {}
Args.__index = Args
 
function Args.new(arguments)
local self = {}
self.args = arguments
return setmetatable(self, Args)
end
 
-- ===============================================================================
local function isTable(t) return type(t) == "table" end
-- Return value of parameter name
-- ===============================================================================
function Args:value(name, default, transform)
if cfg.localization[name] then
val = self.args[cfg.localization[name]]
if val and transform then
val = transform(val)
end
if val then
return val
end
return default
end
return --TODO raise an error ?
end
 
-- ===============================================================================
function p.map(frame)
-- Return value of parameter name as number
-- map path data for geographic objects
-- ===============================================================================
local basemap = frame.args.basemap or "WorldMap-iso2.json"
function Args:number(name, default)
-- scaling factor
local scaleval = tonumberself:value(frame.args.scalename) or 100
return (val and tonumber(val)) or default
-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections
end
local projection = frame.args.projection or "equirectangular"
-- defaultValue for geographic objects without data
local defaultValue = frame.args.defaultValue
local scaleType = frame.args.scaleType or "linear"
-- minimaler Wertebereich (nur für numerische Daten)
local domainMin = tonumber(frame.args.domainMin)
-- maximaler Wertebereich (nur für numerische Daten)
local domainMax = tonumber(frame.args.domainMax)
-- Farbwerte der Farbskala (nur für numerische Daten)
local colorScale = frame.args.colorScale or "category10"
-- show legend
local legend = frame.args.legend
-- format JSON output
local formatJson = frame.args.formatjson
 
 
-- map data are key-value pairs: keys are non-lowercase strings (ideally ISO codes) which need to match the "id" values of the map path data
-- ===============================================================================
-- Return array of value of parameters base_name, base_name2, ... base_namen
-- ===============================================================================
function Args:values_indexed(base_name)
local base_name_localized = cfg.localization[base_name]
if not base_name_localized then return end
local values = {}
local isNumbersindex = nil1
if self.args[base_name_localized] then
for name, value in pairs(frame.args) do
values[1] = self.args[base_name_localized]
if mw.ustring.find(name, "^[^%l]+$") then
index = 2
if isNumbers == nil then isNumbers = tonumber(value) end
local data = { id = name, v = value }
if isNumbers then data.v = tonumber(data.v) end
table.insert(values, data)
end
end
while true do
if not defaultValue then
local val = self.args[base_name_localized .. tostring(index)]
if isNumbers then defaultValue = 0 else defaultValue = "silver" end
if not val then break end
values[index] = val
index = index + 1
end
return values
end
 
-- ===============================================================================
-- create highlight scale
-- Return true if parameter arg is present and is a yes value (a valor
local scales
-- in the array cfg.yes_values
if isNumbers then
-- ===============================================================================
if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end
function Args:is_yes(name)
scales =
local val = self:value(name)
{
return val and cfg.yes_values[mw.ustring.lower(val)]
{
end
name = "color",
 
type = scaleType,
-- ===============================================================================
___domain = { data = "highlights", field = "v" },
-- Return true if parameter arg is present and is a yes value (a valor
range = colorScale,
-- in the array cfg.yes_values
nice = true
-- ===============================================================================
}
function Args:is_no(name)
}
local val = self:value(name)
if domainMin then scales[1].domainMin = domainMin end
return val and cfg.no_values[mw.ustring.lower(val)]
if domainMax then scales[1].domainMax = domainMax end
end
 
setmetatable(Args, { __call = function(_, ...) return Args.new(...) end })
 
-- ===============================================================================
local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent
-- Return an array of numbers splitting a string at ","
if exponent then
-- For localization purpose check for the presence of an alternative separator
scales[1].type = "pow"
-- and alternative symbol for decimal separator
scales[1].exponent = exponent
-- ===============================================================================
local function numericArray(csv, default_empty)
if not csv then return end
if default_empty == nil then default_empty = 'x' end
local list = {}
-- check for local separator character instead of ","
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
-- build output array replacing empty value with a "x"
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
 
-- ===============================================================================
-- create legend
-- Return an array of string splitting at ","
if legend then
-- ===============================================================================
legend =
local function stringArray(csv)
{
if not csv then return end
{
local t = {}
fill = "color",
for s in mw.text.gsplit(csv, ",") do
offset = 120,
t[#t+1] = mw.text.trim(s)
properties =
{
title = { fontSize = { value = 14 } },
labels = { fontSize = { value = 12 } },
legend =
{
stroke = { value = "silver" },
strokeWidth = { value = 1.5 }
}
}
}
}
end
return t
end
 
 
-- get map url
-- ==============================================================================
local basemapUrl
-- Extend table replicating content
if (string.sub(basemap, 1, 7) == "http://") or (string.sub(basemap, 1, 8) == "https://") or (string.sub(basemap, 1, 2) == "//") then
-- ==============================================================================
basemapUrl = basemap
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 a palette name (must be in cfg.colors_palette)
-- or an array of color values
-- ==============================================================================
local function generate_color_palette(palette, new_len)
local color_palette = {}
local palette_len
palette = palette or "category10"
if isTable(palette) and #palette == 1 and cfg.colors_palette[palette[1]] then
palette_len, color_palette = cfg.colors_palette[palette[1]][1], cfg.colors_palette[palette[1]][2]
elseif not isTable(palette) then
palette = (cfg.colors_palette[palette] and palette) or "category10"
palette_len, color_palette = cfg.colors_palette[palette][1], cfg.colors_palette[palette][2]
else
palette_len, color_palette = #palette, palette
-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.
end
if not string.find(basemap, ":") then basemap = baseMapDirectory .. basemap end
local new_len = new_len or palette_len
basemapUrl = mw.title.new(basemap):fullUrl("action=raw")
local t = {}
local pos = 1
for i = 1, new_len do
t[i] = color_palette[pos]
pos = pos + 1
if pos > palette_len then pos = 1 end
end
return t
end
 
-- ===================================================================================
local output =
-- Wrap the graph inside a div structure and add an optional legend extenal to the
{
-- graph tag
-- ===================================================================================
local function wrap_graph(graph, legend, align, width)
local html = mw.html.create('div'):addClass('thumb')
if align then
html:addClass('t' .. align)
else
html:css('display', 'inline-block')
end
 
html:tag('div')
:addClass('thumbinner')
:tag('div')
:wikitext(graph)
:done()
:tag('div')
:node(legend)
:css('width', tostring(width) .. 'px')
return tostring(html)
end
 
-- ===================================================================================
-- Build a legend item joining a color box with text
-- ===================================================================================
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("&nbsp;%s", text))
return item
end
 
-- ===================================================================================
-- Build a legend
-- ===================================================================================
local function build_legend(colors, labels, title, ncols)
local legend = mw.html.create('div'):addClass('thumbcaption'):css('text-align', 'center')
legend:wikitext(title or '')
local legend_list= mw.html.create('div')
local cols = tonumber(ncols or "1")
if cols>1 then
local col_string = tostring(cols)
legend_list
:css('-moz-column-count', col_string)
:css('-webkit-column-count', col_string)
:css('column-count:', col_string)
end
for i,label in ipairs(labels) do
legend_list:node(legend_item(colors[i], label))
end
legend:node(legend_list)
return legend
end
 
-- ===================================================================================
-- Return the json code to build a pie chart
-- ===================================================================================
function p.pie_chart_json(args)
local data = {}
for pos = 1,#args.values do
data[pos] = { x = args.labels[pos], y = args.values[pos] }
end
local graph = {
version = 2,
name = args.name,
width = 1, -- generic value as output size depends solely on map size and scaling factor
width = math.floor(args.graphwidth / 3),
height = 1, -- ditto
height = math.floor(args.graphwidth / 3),
data =
data = {
{
name = "table",
-- data source for the highlights
namevalues = "highlights"data,
transform = { { type = "pie", value = "x" } }
values = values
},
},
marks = {
{
type = "arc",
-- data source for map paths data
namefrom = { data = "countriestable",
transform = { { field = "y", type = "pie"} }
url = basemapUrl,
},
format = { type = "topojson", feature = "countries" },
transformproperties = {
enter = {
innerRadius = {value = args.inner_radius},
{
startAngle = { field = "layout_start"},
-- geographic transformation ("geopath") of map paths data
typeouterRadius = "geopath"{value = args.outer_radius },
valueendAngle = {field = "datalayout_end"}, -- data source
scalestroke = scale{value = "#fff"},
translatefill = { 0field = "x", 0scale = "color"},
projection = projection
},
{},
}
-- join ("zip") of mutiple data source: here map paths data and highlights
type = "lookup",
keys = { "id" }, -- key for map paths data
on = "highlights", -- name of highlight data source
onKey = "id", -- key for highlight data source
as = { "zipped" }, -- name of resulting table
default = { v = defaultValue } -- default value for geographic objects that could not be joined
}
}
}
},
marksscales = {
{
-- output markings (map paths and highlights)
{
type name = "pathcolor",
from = { datarange = "countries" }args.colors,
___domain = { data = "table", field = "x"},
properties =
["type"] = "ordinal"
{
enter = { path = { field = "layout_path" } },
update = { fill = { field = "zipped.v" } },
hover = { fill = { value = "darkgrey" } }
}
}
},
legends = legend
}
if (scales)args.internal_legend then
data[#data] = { { fill = "color", stroke = "color", title = args.internal_legend } }
output.scales = scales
end
output.marks[1].properties.update.fill.scale = "color"
local flags = args.debug_json and mw.text.JSON_PRETTY
end
return mw.text.jsonEncode(graph, flags)
 
local flags
if formatJson then flags = mw.text.JSON_PRETTY end
return mw.text.jsonEncode(output, flags)
end
 
-- ===================================================================================
local function deserializeXData(serializedX, xType, xMin, xMax)
-- Interface function for template:pie_chart
local x
-- ===================================================================================
 
function p.pie_chart(frame)
if not xType or xType == "integer" or xType == "number" then
local args = Args(getArgs(frame, {parentOnly = true}))
local isInteger
local pie_args = { }
x, isInteger = numericArray(serializedX)
pie_args.name = args:value('name', 'grafico a torta')
if x then
pie_args.values = numericArray(args:value('values'))
xMin = tonumber(xMin)
-- Se non trova valori validi termina
xMax = tonumber(xMax)
if not xTypepie_args.values or #pie_args.values == 0 then
add_error('no_values')
if isInteger then xType = "integer" else xType = "number" end
return errors_output(args.NoTracking)
end
end
else
pie_args.labels = args:values_indexed('label')
if xType then error("Numbers expected for parameter 'x'") end
-- Se è definito 'other' assumo che sia calcolato su base %, calcolo il suo valore e l'aggiungo alla tabella dati
if args:value('other') then
local total = 0
for _,val in ipairs(pie_args.values) do total = total + val end
if total > 0 and total < 100 then
pie_args.values[#pie_args.values+1]= math.max(0, 100 - total)
pie_args.labels[#pie_args.values] = "Altri"
end
end
-- build array of colors values
if not x then
x local palette = stringArray(serializedXargs:value('colors'))
if not xTypepalette then xTypepalette = "string"stringArray(args:value('color')) end
pie_args.colors = generate_color_palette(palette, #pie_args.values)
pie_args.graphwidth = args:number('width', cfg.default.width_piechart)
pie_args.outer_radius = pie_args.graphwidth / 2 - 5
if args:is_yes('ring') then
pie_args.inner_radius = pie_args.outer_radius / 3
else
pie_args.inner_radius = 0
end
pie_args.legend = args:value('internal_legend') or args:value('external_legend', 'Legenda')
 
for pos,txt in ipairs(pie_args.labels) do
return x, xType, xMin, xMax
pie_args.labels[pos] = txt .. ' (' .. mw.language.getContentLanguage():formatNum(pie_args.values[pos] or 0) .. '%)'
end
 
local function deserializeYData(serializedYs, yType, yMin, yMax)
local y = {}
local areAllInteger = true
 
for yNum, value in pairs(serializedYs) do
local yValues
if not yType or yType == "integer" or yType == "number" then
local isInteger
yValues, isInteger = numericArray(value)
if yValues then
areAllInteger = areAllInteger and isInteger
else
if yType then
error("Numbers expected for parameter '" .. name .. "'")
else
return deserializeYData(serializedYs, "string", yMin, yMax)
end
end
end
if not yValues then yValues = stringArray(value) end
 
y[yNum] = yValues
end
pie_args.debug_json = args:is_yes('debug_json')
if not yType then
local json_code = p.pie_chart_json(pie_args)
if areAllInteger then yType = "integer" else yType = "number" end
if pie_args.debug_json then return frame:extensionTag('syntaxhighlight', json_code) end
local external_legend
if not args:is_no('legend') then
external_legend = build_legend(pie_args.colors, pie_args.labels, pie_args.legend, args:value('nCols'))
end
local chart = frame:extensionTag('graph', json_code)
if yType == "integer" or yType == "number" then
local align = args:value('thumb')
yMin = tonumber(yMin)
return wrap_graph(chart, external_legend, align, pie_args.graphwidth ) .. errors_output(args:value('NoTracking'))
yMax = tonumber(yMax)
end
 
return y, yType, yMin, yMax
end
 
-- ===================================================================================
local function convertXYToManySeries(x, y, xType, yType, seriesTitles)
-- Generate data structure for x and y axes
local data =
-- ===================================================================================
{
local function build_ax(args, ax_name)
name = "chart",
 
format =
local ax = {
{
type = "json"ax_name,
scale = ax_name,
parse = { x = xType, y = yType }
title = args[ax_name .. 'title'],
},
format = args[ax_name .. 'format'],
values = {}
grid = args[ax_name .. 'grid'],
layer = "back"
}
if isTable(args[ax_name .. 'AxisPrimaryTicks']) then
for i = 1, #y do
ax.values = args[ax_name .. 'AxisPrimaryTicks']
for j = 1, #x do
elseif args[ax_name .. 'nTicks'] then
if j <= #y[i] then table.insert(data.values, { series = seriesTitles[i], x = x[j], y = y[i][j] }) end
ax.ticks = args[ax_name .. 'nTicks']
end
end
if args[ax_name .. 'SecondaryTicks'] then ax.subdivide = args[ax_name .. 'SecondaryTicks'] end
return data
return ax
end
 
-- ===================================================================================
local function convertXYToSingleSeries(x, y, xType, yType, yNames)
-- Return a json structure to generate a a line/area/bar chart
local data = { name = "chart", format = { type = "json", parse = { x = xType } }, values = {} }
-- Imported and modified from en:Module:Chart revision 670068988 of 5 july 2015
-- ===================================================================================
function p.chart_json(args)
-- build axes
local x_ax = build_ax(args, 'x')
local y_ax = build_ax(args, 'y')
local axes = { x_ax, y_ax }
 
-- create data tuples, consisting of series index, x value, y value
for j = 1, #y do data.format.parse[yNames[j]] = yType end
local data = { name = "chart", values = {} }
 
for i, =yserie 1,in #xipairs(args.y) do
localfor itemj = {1, x =math.min(#yserie, #args.x[i]) }do
if yserie[j] ~= 'x' then data.values[#data.values + 1] = { series = args.seriesTitles[i], x = args.x[j], y = yserie[j] } end
for j = 1, #y do item[yNames[j]] = y[j][i] end
end
 
table.insert(data.values, item)
end
-- calculate statistics of data as stacking requires cumulative y values
return data
local stats
end
if args.is_stacked then
 
stats =
local function getXScale(chartType, stacked, xMin, xMax, xType)
{
if chartType == "pie" then return end
name = "stats", source = "chart", transform = {
 
{
type = "aggregate",
summarize = { y = "sum" },
groupby = { "x" }
}
}
}
end
-- create scales
local xscale =
{
Line 288 ⟶ 433:
___domain = { data = "chart", field = "x" }
}
if args.xMin then xscale.domainMin = args.xMin end
if args.xMax then xscale.domainMax = args.xMax end
if args.xMin or args.xMax then xscale.clamp = true end
if chartTypeargs.graph_type == "rect" or args.force_x_ordinal then xscale.type = "ordinal" end
xscale.type = "ordinal"
if not stacked then xscale.padding = 0.2 end -- pad each bar group
else
if xType == "date" then xscale.type = "time"
elseif xType == "string" then xscale.type = "ordinal" end
end
 
return xscale
end
 
local function getYScale(chartType, stacked, yMin, yMax, yType)
if chartType == "pie" then return end
 
local yscale =
Line 311 ⟶ 444:
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 = chartTypeargs.graph_type ~= "line",
nice = true
}
if args.yMin then yscale.domainMin = args.yMin end
if args.yMax then yscale.domainMax = args.yMax end
if args.yMin or args.yMax then yscale.clamp = true end
if args.is_stacked then
if yType == "date" then yscale.type = "time"
yscale.___domain = { data = "stats", field = "sum_y" }
elseif yType == "string" then yscale.type = "ordinal" end
if stacked then
yscale.___domain = { data = "stats", field = "sum_y" }
else
yscale.___domain = { data = "chart", field = "y" }
end
-- Color scale
 
local colorScale = {
return yscale
end
 
local function getColorScale(colors, chartType, xCount, yCount)
if not colors then
if (chartType == "pie" and xCount > 10) or yCount > 10 then colors = "category20" else colors = "category10" end
end
 
local colorScale =
{
name = "color",
type = "ordinal",
range = args.colors,
___domain = { data = "chart", field = "series" }
}
if chartType == "pie" then colorScale.___domain.field = "x" end
return colorScale
end
 
local function getAlphaColorScale(colors, y)
local alphaScale
if args.alphas then alphaScale = { name = "transparency", graph_type = "ordinal", range = args.alphas } end
-- if there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale
-- Symbols scale
if isTable(colors) then
local alphas = {}symbolsScale
if type(args.symbols) == 'table' then
local hasAlpha = false
symbolsScale = { name = "symShape", type = "ordinal", range = args.symbols, ___domain = { data = "chart", field = "series" } }
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
return alphaScale
local groupScale
end
if args.graph_type == "rect" and not args.is_stacked and #args.y > 1 then
 
groupScale = { name = "series", type = "ordinal", range = "width", ___domain = { field = "series" } }
local function getValueScale(fieldName, min, max, type)
xscale.padding = 0.2 -- pad each bar group
local valueScale =
{
name = fieldName,
type = type or "linear",
___domain = { data = "chart", field = fieldName },
range = { min, max }
}
return valueScale
end
 
local function addInteractionToChartVisualisation(plotMarks, colorField, dataField)
-- initial setup
if not plotMarks.properties.enter then plotMarks.properties.enter = {} end
plotMarks.properties.enter[colorField] = { scale = "color", field = dataField }
 
-- action when cursor is over plot mark: highlight
if not plotMarks.properties.hover then plotMarks.properties.hover = {} end
plotMarks.properties.hover[colorField] = { value = "red" }
 
-- action when cursor leaves plot mark: reset to initial setup
if not plotMarks.properties.update then plotMarks.properties.update = {} end
plotMarks.properties.update[colorField] = { scale = "color", field = dataField }
end
 
local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale)
local chartvis =
{
type = "arc",
from = { data = "chart", transform = { { field = "y", type = "pie" } } },
 
properties =
{
enter = {
innerRadius = { value = innerRadius },
outerRadius = { },
startAngle = { field = "layout_start" },
endAngle = { field = "layout_end" },
stroke = { value = "white" },
strokeWidth = { value = linewidth or 1 }
}
}
}
if radiusScale then
chartvis.properties.enter.outerRadius.scale = radiusScale.name
chartvis.properties.enter.outerRadius.field = radiusScale.___domain.field
else
chartvis.properties.enter.outerRadius.value = outerRadius
end
 
-- decide if lines (strokes) or areas (fills) should be drawn
addInteractionToChartVisualisation(chartvis, "fill", "x")
local colorField
if args.graph_type == "line" then colorField = "stroke" else colorField = "fill" end
 
-- create chart markings
return chartvis
local marks =
end
 
local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, interpolate)
if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end
 
local chartvis =
{
type = chartTypeargs.graph_type,
properties =
{
Line 433 ⟶ 490:
{
x = { scale = "x", field = "x" },
y = { scale = "y", field = "y" },
 
}
},
-- chart update event handler
update = { },
-- chart hover event handler
hover = { }
}
}
marks.properties.update[colorField] = { scale = "color" }
addInteractionToChartVisualisation(chartvis, colorField, "series")
if marks.properties.hover[colorField] = { value = "strokered" then}
chartvis if alphaScale then marks.properties.enterupdate[colorField ..strokeWidth "Opacity"] = { valuescale = linewidth or 2.5"transparency" } end
end
 
if interpolate then chartvis.properties.enter.interpolate = { value = interpolate } end
 
if alphaScale then chartvis.properties.update[colorField .. "Opacity"] = { scale = "transparency" } end
-- for bars and area charts set the lower bound of their areas
if chartTypeargs.graph_type == "rect" or chartTypeargs.graph_type == "area" then
if stackedargs.is_stacked then
-- for stacked charts this lower bound is the end of the last cumulative/stacking element
chartvismarks.properties.enter.y2 = { scale = "y", field = "layout_end" }
else
--[[
Line 456 ⟶ 513:
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 ]]
chartvismarks.properties.enter.y2 = { scale = "y", value = 0 }
end
end
-- for bar charts ...
if chartTypeargs.graph_type == "rect" then
-- set 1 pixel width between the bars
chartvismarks.properties.enter.width = { scale = "x", band = true, offset = -1 }
-- for multiple series the bar marking needsneed to use the "inner" series scale, whereas the "outer" x scale is used by the grouping
if not stackedargs.is_stacked and yCount#args.y > 1 then
chartvismarks.properties.enter.x.scale = "series"
chartvismarks.properties.enter.x.field = "series"
chartvismarks.properties.enter.width.scale = "series"
end
end
if args.graph_type == "line" then marks.properties.enter.strokeWidth = { value = args.stroke_thickness } end
-- stacked charts have their own (stacked) y values
if stackedargs.is_stacked then chartvismarks.properties.enter.y.field = "layout_start" end
-- set interpolation mode
 
if args.interpolate then marks.properties.enter.interpolate = { value = args.interpolate } end
-- if there are multiple series group these together
local symbolsMarks
if yCount == 1 then
if symbolsScale then
chartvis.from = { data = "chart" }
symbolsMarks = {
type = "symbol",
from = { data = "chart" },
properties = {
enter = {
x = { scale = "x", field = "x" },
y = { scale = "y", field = "y" },
shape = { scale = "symShape", field = "series" },
stroke = { scale = "color", field = "series" },
},
}
}
if args.symbol_size then symbolsMarks.properties.enter.size = { value = args.symbol_size } end
if alphaScale then
symbolsMarks.properties.enter.fillOpacity = { scale = "transparency", field = "series" }
symbolsMarks.properties.enter.strokeOpacity = { scale = "transparency", field = "series" }
end
end
if #args.y == 1 then
marks.from = { data = "chart" }
marks = { marks, symbolsMarks }
else
-- if there are multiple series, connect colors to series
if args.graph_type == "rect" and args.colorsByGroup then
chartvis.properties.update[colorField].field = "series"
if alphaScale then chartvis marks.properties.update[colorField .. "Opacity"].field = "seriesx" end
else
marks.properties.update[colorField].field = "series"
end
if symbolsScale then
symbolsMarks.properties.enter.shape.field = "series"
end
if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "series" end
 
-- apply a grouping (facetting) transformation
chartvismarks =
{
type = "group",
marks = { chartvismarks, symbolsMarks },
from =
{
Line 498 ⟶ 585:
}
-- for stacked charts apply a stacking transformation
if stackedargs.is_stacked then
table.insert(chartvismarks.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "series" }, field = "y" } )
field = "y",
type = "stack",
sortby = { "-_id" },
groupby = { "x" }
})
else
-- for bar charts the series are side-by-side grouped by x
if chartTypeargs.graph_type == "rect" then
marks.from.transform[1].groupby = "x"
-- for bar charts with multiple series: each serie is grouped by the x value, therefore the series need their own scale within each x group
localmarks.scales = { groupScale =}
marks.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
{
name = "series",
type = "ordinal",
range = "width",
___domain = { field = "series" }
}
 
chartvis.from.transform[1].groupby = "x"
chartvis.scales = { groupScale }
chartvis.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
end
end
marks = { marks }
end
 
-- create legend
return chartvis
local legend
if args.internal_legend then
legend = { { fill = "color", stroke = "color", title = args.internal_legend } }
end
 
-- build final output object
local scales = { xscale, yscale, colorScale}
if alphaScale then scales[#scales+1] = alphaScale end
if symbolsScale then scales[#scales+1] = symbolsScale end
local output =
{
version = 2,
width = args.graphwidth,
height = args.graphheight,
data = { data, stats },
scales = scales,
axes = axes,
marks = marks ,
legends = legend
}
local flags = (args.debug_json and mw.text.JSON_PRETTY) or 0
return mw.text.jsonEncode(output, flags)
end
 
-- ===================================================================================
local function getTextMarks(chartType, outerRadius, radiusScale)
-- Interface function for template:Grafico a linee
local textmarks
-- ===================================================================================
if chartType == "pie" then
function p.chart(frame)
textmarks =
 
{
-- Read ax arguments
type = "text",
local function read_ax_arguments(args, ax_name, chart_arg)
from = { data = "chart", transform = { { field = "y", type = "pie" } } },
chart_arg[ax_name .. 'title'] = args:value(ax_name .. 'AxisTitle')
properties =
chart_arg[ax_name .. 'format'] = args:value(ax_name .. 'AxisFormat')
{
local grid = cfg.default[ax_name .. 'Grid']
enter =
if grid then
{
grid = not args:is_no(ax_name .. 'Grid')
x = { group = "width", mult = 0.5 },
y = { group = "height", mult = 0.5 },
radius = { offset = -4 },
theta = { field = "layout_mid" },
fill = { value = "white" },
align = { value = "center" },
baseline = { value = "top" },
text = { field = "y" },
angle = { field = "layout_mid", mult = 180.0 / math.pi },
fontSize = { value = math.ceil(outerRadius / 10) }
}
}
}
if radiusScale then
textmarks.properties.enter.radius.scale = radiusScale.name
textmarks.properties.enter.radius.field = radiusScale.___domain.field
else
grid = args:is_yes(ax_name .. 'Grid')
textmarks.properties.enter.radius.value = outerRadius
end
chart_arg[ax_name .. 'grid'] = grid
chart_arg[ax_name .. 'AxisPrimaryTicks'] = numericArray(args:value(ax_name .. 'AxisPrimaryTicks'))
chart_arg[ax_name .. 'nTicks'] = args:number(ax_name .. 'AxisPrimaryTicksNumber')
chart_arg[ax_name .. 'SecondaryTicks'] = args:number(ax_name .. 'AxisSecondaryTicks')
chart_arg[ax_name ..'Min'] = args:number(ax_name .. 'AxisMin')
chart_arg[ax_name ..'Max'] = args:number(ax_name .. 'AxisMax')
end
return textmarks
end
 
-- get graph type
local function getAxes(xTitle, xFormat, xType, yTitle, yFormat, yType, chartType)
local function get_graph_type(graph_string)
local xAxis, yAxis
if graph_string == nil then return "line", false end
if chartType ~= "pie" then
local graph_type = cfg.graph_type[mw.ustring.lower(graph_string)]
if xType == "integer" and not xFormat then xFormat = "d" end
if graph_type then return graph_type[1], graph_type[2] end
xAxis =
add_error('type_unknown', {graph_string})
{
end
type = "x",
scale = "x",
title = xTitle,
format = xFormat
}
 
local args = Args(getArgs(frame, {parentOnly = true}))
if yType == "integer" and not yFormat then yFormat = "d" end
-- analyze and build data to build the chart
yAxis =
local chart_arg = { }
{
chart_arg.graphwidth = args:number('width', cfg.default.width)
type = "y",
chart_arg.graphheight = args:number('height', cfg.default.height)
scale = "y",
chart_arg.graph_type, chart_arg.is_stacked = get_graph_type(args:value('type'))
title = yTitle,
chart_arg.interpolate = args:value('interpolate')
format = yFormat
if chart_arg.interpolate and not cfg.interpolate[chart_arg.interpolate] then
}
add_error('value_not_valid', {cfg.localization.interpolate, chart_arg.interpolate})
interpolate = nil
end
-- get marks symbols, default symbol is used if the type of graph is line, otherwise the default
 
-- is not to use symbol.
return xAxis, yAxis
if chart_arg.graph_type == "line" and not args:is_no('symbols') then
end
chart_arg.symbols = stringArray(args:value('symbols') or cfg.default.symbol)
 
chart_arg.symbol_size = args:number('symbolSize', cfg.default.symbol_size)
local function getLegend(legendTitle, chartType, outerRadius)
end
local legend =
if chart_arg.graph_type =="line" then
{
chart_arg.stroke_thickness = args:number('strokeThickness', cfg.default.stroke_thickness)
fill = "color",
stroke = "color",
title = legendTitle,
}
if chartType == "pie" then
-- move legend from center position to top
legend.properties = { legend = { y = { value = -outerRadius } } }
end
return-- show legend, optionally caption
chart_arg.internal_legend = args:value('internal_legend')
end
 
function p.chart(frame)
-- chart width and height
local graphwidth = tonumber(frame.args.width) or 200
local graphheight = tonumber(frame.args.height) or 200
-- chart type
local chartType = frame.args.type or "line"
-- interpolation mode for line and area charts: 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 = frame.args.interpolate
-- mark colors (if no colors are given, the default 10 color palette is used)
local colors = stringArray(frame.args.colors)
-- for line charts, the thickness of the line; for pie charts the gap between each slice
local linewidth = tonumber(frame.args.linewidth)
-- x and y axis caption
local xTitle = frame.args.xAxisTitle
local yTitle = frame.args.yAxisTitle
-- x and y value types
local xType = frame.args.xType
local yType = frame.args.yType
-- override x and y axis minimum and maximum
local xMin = frame.args.xAxisMin
local xMax = frame.args.xAxisMax
local yMin = frame.args.yAxisMin
local yMax = frame.args.yAxisMax
-- override x and y axis label formatting
local xFormat = frame.args.xAxisFormat
local yFormat = frame.args.yAxisFormat
-- show legend with given title
local legendTitle = frame.args.legend
-- show values as text
local showValues = frame.args.showValues
-- pie chart radiuses
local innerRadius = tonumber(frame.args.innerRadius) or 0
local outerRadius = math.min(graphwidth, graphheight)
-- format JSON output
local formatJson = frame.args.formatjson
 
-- get x values
chart_arg.x = numericArray(args:value('x'))
local x
chart_arg.force_x_ordinal = false
x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax)
if #chart_arg.x == 0 then
 
chart_arg.force_x_ordinal = true
else
for _,val in ipairs(chart_arg.x) do
if val == 'x' then
chart_arg.force_x_ordinal = true
break
end
end
end
if chart_arg.force_x_ordinal then chart_arg.x = stringArray(args:value('x')) end
-- get y values (series)
chart_arg.y = args:values_indexed('y')
local yValues = {}
if not chart_arg.y then return '' end --TODO message error mancanza dati per asse y
local seriesTitles = {}
chart_arg.seriesTitles = args:values_indexed('yTitle')
for name, value in pairs(frame.args) do
for pos, y in ipairs(chart_arg.y) do
local yNum
chart_arg.y[pos] = numericArray(y)
if name == "y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end
chart_arg.seriesTitles[pos] = chart_arg.seriesTitles[pos] or ("y" .. tostring(pos))
if yNum then
end
yValues[yNum] = value
-- ignore stacked charts if there is only one series
-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.
if #chart_arg.y == 1 then chart_arg.is_stacked = false end
seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or name
-- read axes arguments
read_ax_arguments(args, 'x', chart_arg)
read_ax_arguments(args, 'y', chart_arg)
-- get marks colors, default palette is category10,
-- if colors is not the name of a predefined palette then read it as an array of colors
--chart_arg.colors = args[cfg.localization.colors] or "category10"
--if not cfg.colors_palette[chart_arg.colors] then
-- chart_arg.colors = stringArray(chart_arg.colors)
--elseif chart_arg.colors ~="category10" and chart_arg.colors ~="category20" then
-- chart_arg.colors = generate_color_palette(chart_arg.colors)
--end
local palette = stringArray(args:value('colors'))
if not palette then palette = stringArray(args:value('color')) end
chart_arg.colors = generate_color_palette(palette, #chart_arg.y)
--if true then return mw.text.jsonEncode(chart_arg.colors) end
-- assure that colors, stroke_thickness and symbols table are at least the same lenght that the number of
-- y series
if isTable(chart_arg.stroke_thickness) then chart_arg.stroke_thickness = extend_table(chart_arg.stroke_thickness, #chart_arg.y) end
if isTable(chart_arg.symbols) then chart_arg.symbols = extend_table(chart_arg.symbols, #chart_arg.y) end
-- if there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale
if isTable(chart_arg.colors) then
alphas = {}
local hasAlpha = false
for i, color in ipairs(chart_arg.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)
chart_arg.colors[i] = "#" .. rgb
else
alphas[i] = "1"
end
end
for i = #chart_arg.colors + 1, #chart_arg.y do alphas[i] = "1" end
if hasAlpha then chart_arg.alphas = alphas end
elseif args[cfg.localization.alpha] then
chart_arg.alphas = stringArray(args[cfg.localization.alpha])
if chart_arg.alphas then
for i,a in ipairs(chart_arg.alphas) do chart_arg.alphas[i] = tostring(tonumber(a, 16) / 255.0) end
chart_arg.alphas = extend_table(chart_arg.alphas, #chart_arg.y)
end
end
chart_arg.colorsByGroup = args:is_yes('colorsByGroup')
local y
chart_arg.debug_json = args:is_yes('debug_json') or false
y, yType, yMin, yMax = deserializeYData(yValues, yType, yMin, yMax)
-- if true then return frame:extensionTag('syntaxhighlight', mw.text.jsonEncode(chart_arg, mw.text.JSON_PRETTY)) end
local chart_json = p.chart_json(chart_arg)
if chart_arg.debug_json then return frame:extensionTag('syntaxhighlight', chart_json) end
local external_legend
if args:value('external_legend') then
external_legend = build_legend(chart_arg.colors, chart_arg.seriesTitles, args:value('external_legend'),
args:value('nCols'))
end
local chart = frame:extensionTag('graph', chart_json)
local align = args:value('thumb')
return wrap_graph(chart, external_legend, align, chart_arg.graphwidth) .. errors_output(args:value('NoTracking'))
end
 
-- ===================================================================================
-- create data tuples, consisting of series index, x value, y value
-- Return a json structure to generate a map chart
local data
-- Imported and modified from de:Modul:Graph revision 142970943 of 10 giugno 2015
if chartType == "pie" then
-- ===================================================================================
-- for pie charts the second second series is merged into the first series as radius values
function p.map_json(args)
data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" })
-- create highlight scale
else
local scales
data = convertXYToManySeries(x, y, xType, yType, seriesTitles)
if args.isNumbers then
scales =
{
{
name = "color",
type = args.scaleType,
___domain = { data = "highlights", field = "v" },
range = args.colorScale,
nice = true,
zero = false
}
}
if args.domainMin then scales[1].domainMin = args.domainMin end
if args.domainMax then scales[1].domainMax = args.domainMax end
 
local exponent = string.match(args.scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent
if exponent then
scales[1].type = "pow"
scales[1].exponent = args.exponent
end
end
 
-- create legend
-- configure stacked charts
if args.legend then
local stacked = false
legend =
local stats
{
if string.sub(chartType, 1, 7) == "stacked" then
chartType = string.sub(chartType, 8)
if #y > 1 then -- ignore stacked charts if there is only one series
stacked = true
-- aggregate data by cumulative y values
stats =
{
namefill = "statscolor", source = "chart", transform =
properties =
{
title = { fontSize = { value = 14 } },
labels = { fontSize = { value = 12 } },
legend =
{
typestroke = { value = "aggregatesilver" },
groupbystrokeWidth = { "x"value = 1.5 },
summarize = { y = "sum" }
}
}
}
end}
end
 
-- createget scalesmap url
local scales = {}basemapUrl
if (string.sub(args.basemap, 1, 7) == "http://") or (string.sub(args.basemap, 1, 8) == "https://") or (string.sub(args.basemap, 1, 2) == "//") then
 
basemapUrl = args.basemap
local xscale = getXScale(chartType, stacked, xMin, xMax, xType)
else
table.insert(scales, xscale)
-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.
local yscale = getYScale(chartType, stacked, yMin, yMax, yType)
local basemap = args.basemap
table.insert(scales, yscale)
if not string.find(basemap, ":") then basemap = cfg.default.base_map_directory .. basemap end
 
basemapUrl = mw.title.new(basemap):fullUrl("action=raw")
 
local colorScale = getColorScale(colors, chartType, #x, #y)
table.insert(scales, colorScale)
 
local alphaScale = getAlphaColorScale(colors, y)
table.insert(scales, alphaScale)
 
local radiusScale
if chartType == "pie" and #y > 1 then
radiusScale = getValueScale("r", 0, outerRadius)
table.insert(scales, radiusScale)
end
 
-- decide if lines (strokes) or areas (fills) should be drawn
local colorField
if chartType == "line" then colorField = "stroke" else colorField = "fill" end
 
-- create chart markings
local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, interpolate)
 
-- text marks
local textmarks
if showValues then textmarks = getTextMarks(chartType, outerRadius, radiusScale) end
 
-- axes
local xAxis, yAxis = getAxes(xTitle, xFormat, xType, yTitle, yFormat, yType, chartType)
 
-- legend
local legend
if legendTitle then legend = getLegend(legendTitle, chartType, outerRadius) end
 
-- construct final output object
local output =
{
version = 2,
width = 1, -- generic value as output size depends solely on map size and scaling factor
width = graphwidth,
height = graphheight1, -- ditto
data = { data, stats },
{
scales = scales,
{
axes = { xAxis, yAxis },
-- data source for the highlights
marks = { chartvis, textmarks },
name = "highlights",
legends = { legend }
values = args.values
},
{
-- data source for map paths data
name = "countries",
url = basemapUrl,
format = { type = "topojson", feature = "countries" },
transform =
{
{
-- geographic transformation ("geopath") of map paths data
type = "geopath",
value = "data", -- data source
scale = args.scale,
translate = { 0, 0 },
projection = args.projection
},
{
-- join ("zip") of mutiple data source: here map paths data and highlights
type = "zip",
key = "id", -- key for map paths data
with = "highlights", -- name of highlight data source
withKey = "id", -- key for highlight data source
as = "zipped", -- name of resulting table
default = { data = { v = args.defaultValue } } -- default value for geographic objects that could not be joined
}
}
}
},
marks =
{
-- output markings (map paths and highlights)
{
type = "path",
from = { data = "countries" },
properties =
{
enter = { path = { field = "path" } },
update = { fill = { field = "zipped.data.v" } },
hover = { fill = { value = "darkgrey" } }
}
}
},
legends = legend
}
if (scales) then
 
output.scales = scales
local flags
output.marks[1].properties.update.fill.scale = "color"
if formatJson then flags = mw.text.JSON_PRETTY end
end
flags = args.debug_json and mw.text.JSON_PRETTY
return mw.text.jsonEncode(output, flags)
end
 
-- ===================================================================================
function p.mapWrapper(frame)
-- Interface function for template:Mappa a colori
return p.map(frame:getParent())
-- ===================================================================================
function p.map(frame)
local args = Args(getArgs(frame, {parentOnly = true}))
map_args = {}
-- map path data for geographic objects
map_args.basemap = args:value('basemap', cfg.default.world_map)
-- scaling factor
map_args.scale = args:number('scale', cfg.default.scale)
-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections
map_args.projection = args:value('projection', "equirectangular")
-- defaultValue for geographic objects without data
map_args.defaultValue = args:value('defaultValue')
map_args.scaleType = args:value('scaleType' , "linear")
-- minimaler Wertebereich (nur für numerische Daten)
map_args.domainMin = args:number('domainMin')
-- maximaler Wertebereich (nur für numerische Daten)
map_args.domainMax = args:number('domainMax')
-- Farbwerte der Farbskala (nur für numerische Daten)
local palette = stringArray(args:value('colors'))
if not palette then palette = stringArray(args:value('color')) end
map_args.colors = generate_color_palette(palette)
-- show legend
map_args.legend = args[cfg.localization.internal_legend]
-- map data are key-value pairs: keys are non-lowercase strings (ideally ISO codes) which need
-- to match the "id" values of the map path data
map_args.values = {}
local isNumbers = nil
for name, value in pairs(args) do
if mw.ustring.find(name, "^[^%l]+$") then
if isNumbers == nil then isNumbers = tonumber(value) end
local data = { id = name, v = value }
if isNumbers then data.v = tonumber(data.v) end
map_args.values[#arguments.values+1] = data
end
end
if not map_args.defaultValue then
if isNumbers then map_args.defaultValue = 0 else map_args.defaultValue = "silver" end
end
map_args.isNumbers = isNumbers
map_args.debug_json = args:is_yes('debug_json')
local output_json = p.map_json(map_args)
if map_args.debug_json then
return frame:extensionTag('syntaxhighlight', output_json)
end
return frame:extensionTag('graph', output_json)
end
 
function p.chartWrapperpalette_list(frame)
local output = { '<table class="wikitable"><tr><th>Nome</th><th>Colori</th></tr>'}
return p.chart(frame:getParent())
local palette_name = {}
for name,colors in pairs(cfg.colors_palette) do
palette_name[#palette_name+1] = name
end
table.sort(palette_name)
for _,name in ipairs(palette_name) do
dump(output, '<tr><td>' .. name .. '</td><td>')
for _,color in ipairs(cfg.colors_palette[name][2]) do
dump(output, string.format('<span style="border:none;background-color:%s;color:%s;">██</span>', color, color))
end
dump(output, '</td></tr>')
end
dump(output, '</table>')
return table.concat(output)
end