Modulo:Graph

Vai alla navigazione Vai alla ricerca

Modulo che implementa i template {{Grafico}} e {{Grafico a torta}}.

Ha una sottopagina di configurazione: Modulo:Graph/Configurazione.


local p = {}
local cfg = mw.loadData( 'Module:Graph/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 message to errors list, mgs_key must be a error key listed in
-- 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

-- ===============================================================================
-- Consolidate errors messages 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 t is a table
-- ===============================================================================
local function isTable(t)
	return type(t) == "table"
end

-- ===============================================================================
-- 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

-- ===============================================================================
-- 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

-- ===============================================================================
-- Return value of parameter name as number
-- ===============================================================================
function Args:number(name, default)
	local val = self:value(name)
	return (val and tonumber(val)) or default
end


-- ===============================================================================
-- 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 index = 1
		if self.args[base_name_localized] then
		values[1] = self.args[base_name_localized]
		index = 2
	end
	while true do
		local val = self.args[base_name_localized .. tostring(index)]
		if not val then break end
		values[index] = val
		index = index + 1
	end
	return values
end

-- ===============================================================================
-- Return true if parameter arg is present and is a yes value (a valor
-- in the array cfg.yes_values
-- ===============================================================================
function Args:is_yes(name)
	local val = self:value(name)
	return val and cfg.yes_values[mw.ustring.lower(val)]
end

-- ===============================================================================
-- Return true if parameter arg is present and is a yes value (a valor
-- in the array cfg.yes_values
-- ===============================================================================
function Args:is_no(name)
	local val = self:value(name)
	return val and cfg.no_values[mw.ustring.lower(val)]
end

setmetatable(Args, { __call = function(_, ...) return Args.new(...) end })

-- ===============================================================================
-- Return an array of numbers splitting a string at ","
-- For localization purpose check for the presence of an alternative separator
-- and alternative symbol for decimal separator
-- ===============================================================================
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

-- ===============================================================================
-- 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


-- ==============================================================================
-- 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 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
	end
	local new_len = new_len or palette_len
	
	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

-- ===================================================================================
-- 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 = math.floor(args.graphwidth / 3),
		height = math.floor(args.graphwidth / 3),
		data = {
			{
				name = "table",
				values = data,
				transform = { { type = "pie", value = "x" } }
			}
		},
		marks = {
			{
				type = "arc",
				from =  { data = "table", 
						 transform = { { field = "y", type = "pie"} }
						},
				properties = {
					enter = {
						innerRadius = {value = args.inner_radius},
						startAngle = { field = "layout_start"},
						outerRadius = {value = args.outer_radius },
						endAngle = {field = "layout_end"},
						stroke = {value = "#fff"},
						fill = { field = "x", scale = "color"},
					},
				},
			 }
		},
		scales = {
			{
			  name = "color",
			  range = args.colors,
			  domain = { data = "table", field = "x"},
			  ["type"] = "ordinal"
			}
	  	}
	}
	if args.internal_legend then
		data[#data] = { { fill = "color", stroke = "color", title = args.internal_legend } }
	end	
	local flags = args.debug_json and mw.text.JSON_PRETTY
	return mw.text.jsonEncode(graph, flags)
end

-- ===================================================================================
-- Interface function for template:pie_chart
-- ===================================================================================
function p.pie_chart(frame)
	local args = Args(getArgs(frame, {parentOnly = true}))
	local pie_args = { }
	pie_args.name = args:value('name', 'grafico a torta')
	pie_args.values = numericArray(args:value('values'))
	-- Se non trova valori validi termina
	if not pie_args.values or #pie_args.values == 0 then
		add_error('no_values')
		return errors_output(args.NoTracking)
	end
	pie_args.labels = args:values_indexed('label')
	-- 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
	local palette = stringArray(args:value('colors'))
	if not palette then palette = 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
		pie_args.labels[pos] = txt .. ' (' .. mw.language.getContentLanguage():formatNum(pie_args.values[pos] or 0) .. '%)'
	end
	pie_args.debug_json = args:is_yes('debug_json')
	local json_code = p.pie_chart_json(pie_args)
	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)
	local align = args:value('thumb')
	return wrap_graph(chart, external_legend, align, pie_args.graphwidth ) .. errors_output(args:value('NoTracking'))
end

-- ===================================================================================
-- Generate data structure for x and y axes
-- ===================================================================================
local function build_ax(args, ax_name)

	local ax = {
		type = ax_name,
		scale = ax_name,
		title = args[ax_name .. 'title'],
		format = args[ax_name .. 'format'],
		grid = args[ax_name .. 'grid'],
		layer = "back"
	}
	if isTable(args[ax_name .. 'AxisPrimaryTicks']) then
		ax.values = args[ax_name .. 'AxisPrimaryTicks']
	elseif args[ax_name .. 'nTicks'] then
		ax.ticks = args[ax_name .. 'nTicks']
	end
	if args[ax_name .. 'SecondaryTicks'] then ax.subdivide = args[ax_name .. 'SecondaryTicks'] end
	return ax
end

-- ===================================================================================
-- Return a json structure to generate a a line/area/bar chart
-- 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
	local data = { name = "chart", values = {} }
	for i, yserie in ipairs(args.y) do
		for j = 1, math.min(#yserie, #args.x) do
			if yserie[j] ~= 'x' then data.values[#data.values + 1] = { series = args.seriesTitles[i], x = args.x[j], y = yserie[j] } end
		end
	end
	-- calculate statistics of data as stacking requires cumulative y values
	local stats
	if args.is_stacked then
		stats =
		{
			name = "stats", source = "chart", transform = {
				{
					type = "aggregate",
					summarize = { y = "sum" },
					groupby = { "x" }
				}
			}
		}
	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 = "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 args.graph_type == "rect" or args.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 = args.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
		yscale.domain = { data = "stats", field = "sum_y"  }
	else
		yscale.domain = { data = "chart", field = "y" }
	end
	-- Color scale
	local colorScale = {
		name = "color",
		type = "ordinal",
		range = args.colors,
		domain = { data = "chart", field = "series" }
	}
	local alphaScale
	if args.alphas then alphaScale = { name = "transparency", graph_type = "ordinal", range = args.alphas } end
	-- Symbols scale
	local symbolsScale
	if type(args.symbols) == 'table' then
		symbolsScale = { name = "symShape", type = "ordinal", range = args.symbols, domain = { data = "chart", field = "series" } }
	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 args.graph_type == "rect" and not args.is_stacked and #args.y > 1 then
		groupScale = { name = "series", type = "ordinal", range = "width", domain = { field = "series" } }
		xscale.padding = 0.2 -- pad each bar group
	end

	-- decide if lines (strokes) or areas (fills) should be drawn
	local colorField
	if args.graph_type == "line" then colorField = "stroke" else colorField = "fill" end

	-- create chart markings
	local marks =
	{
		type = args.graph_type,
		properties =
		{
			-- chart creation event handler
			enter =
			{
				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" }
	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 args.graph_type == "rect" or args.graph_type == "area" then
		if args.is_stacked then
			-- for stacked charts this lower bound is cumulative/stacking
			marks.properties.enter.y2 = { scale = "y", field = "layout_end" }
		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 args.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 args.is_stacked and #args.y > 1 then
			marks.properties.enter.x.scale = "series"
			marks.properties.enter.x.field = "series"
			marks.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 args.is_stacked then marks.properties.enter.y.field = "layout_start" end
	-- set interpolation mode
	if args.interpolate then marks.properties.enter.interpolate = { value = args.interpolate } end
	local symbolsMarks
	if symbolsScale then
		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
			marks.properties.update[colorField].field = "x"
		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
		marks =
		{
			type = "group",
			marks = { marks, symbolsMarks },
			from =
			{
				data = "chart",
				transform =
				{
					{
						type = "facet",
						groupby = { "series" }
					}
				}
			}
		}
		-- for stacked charts apply a stacking transformation
		if args.is_stacked then
			table.insert(marks.from.transform, 1, {
				field = "y",
				type = "stack",
				sortby = { "-_id" },
				groupby = { "x" }
			})
		else
			-- for bar charts the series are side-by-side grouped by x
			if args.graph_type == "rect" then
				marks.from.transform[1].groupby = "x"
				marks.scales = { groupScale }
				marks.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
			end
		end
		marks = { marks }
	end

	-- create legend
	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

-- ===================================================================================
-- Interface function for template:Grafico a linee
-- ===================================================================================
function p.chart(frame)

	-- Read ax arguments
	local function read_ax_arguments(args, ax_name, chart_arg)
		chart_arg[ax_name .. 'title'] = args:value(ax_name .. 'AxisTitle')
		chart_arg[ax_name .. 'format'] = args:value(ax_name .. 'AxisFormat')
		local grid = cfg.default[ax_name .. 'Grid']
		if grid then
		   grid = not args:is_no(ax_name .. 'Grid')
		else
			grid = args:is_yes(ax_name .. 'Grid')
		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

	-- get graph type
	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('type_unknown', {graph_string})
	end

	local args = Args(getArgs(frame, {parentOnly = true}))
	-- analyze and build data to build the chart
	local chart_arg = { }
	chart_arg.graphwidth = args:number('width', cfg.default.width) 
	chart_arg.graphheight = args:number('height', cfg.default.height) 
	chart_arg.graph_type, chart_arg.is_stacked = get_graph_type(args:value('type'))
	chart_arg.interpolate = args:value('interpolate')
	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.
	if chart_arg.graph_type == "line" and not args:is_no('symbols') then
		chart_arg.symbols = stringArray(args:value('symbols') or cfg.default.symbol)
		chart_arg.symbol_size = args:number('symbolSize', cfg.default.symbol_size)
	end
	if chart_arg.graph_type =="line" then
		chart_arg.stroke_thickness =  args:number('strokeThickness', cfg.default.stroke_thickness)
	end
	-- show legend, optionally caption
	chart_arg.internal_legend = args:value('internal_legend')
	-- get x values
	chart_arg.x = numericArray(args:value('x'))
	chart_arg.force_x_ordinal = false
	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')
	if not chart_arg.y then return '' end --TODO message error mancanza dati per asse y
	chart_arg.seriesTitles = args:values_indexed('yTitle')	
	for pos, y in ipairs(chart_arg.y) do
		chart_arg.y[pos] = numericArray(y)
		chart_arg.seriesTitles[pos] = chart_arg.seriesTitles[pos] or ("y" .. tostring(pos))
	end
	-- ignore stacked charts if there is only one series
	if #chart_arg.y == 1 then chart_arg.is_stacked = false end
	-- 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')
	chart_arg.debug_json = args:is_yes('debug_json') or false
	-- 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[cfg.localization.thumb]
	return wrap_graph(chart, external_legend, align, chart_arg.graphwidth) .. errors_output(args:value('NoTracking'))
end

-- ===================================================================================
-- Return a json structure to generate a map chart
-- Imported and modified from de:Modul:Graph revision 142970943 of 10 giugno 2015
-- ===================================================================================
function p.map_json(args)
	-- create highlight scale
	local scales
	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
	if args.legend then
		legend =
		{
			{
				fill = "color",
				properties =
				{
					title = { fontSize = { value = 14 } },
					labels = { fontSize = { value = 12 } },
					legend =
					{
						stroke = { value = "silver" },
						strokeWidth = { value = 1.5 }
					}
				}
			}
		}
	end

	-- get map url
	local 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
	else
		-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.
		local basemap = args.basemap
		if not string.find(basemap, ":") then basemap = cfg.default.base_map_directory .. basemap end
		basemapUrl = mw.title.new(basemap):fullUrl("action=raw")
	end
	local output =
	{
		version = 2,
		width = 1,  -- generic value as output size depends solely on map size and scaling factor
		height = 1, -- ditto
		data =
		{
			{
				-- data source for the highlights
				name = "highlights",
				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
		output.marks[1].properties.update.fill.scale = "color"
	end
	flags = args.debug_json and mw.text.JSON_PRETTY
	return mw.text.jsonEncode(output, flags)
end

-- ===================================================================================
-- Interface function for template:Mappa a colori
-- ===================================================================================
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.palette_list(frame)
	local output = { '<table class="wikitable"><tr><th>Nome</th><th>Colori</th></tr>'}
	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

return p