Permanently protected module
From Wikipedia, the free encyclopedia


-- ATTENTION:	Please edit this code at https://de.wikipedia.org/wiki/Modul:Graph

--          	This way all wiki languages can stay in sync. Thank you!

--

--	BUGS:	X-Axis label format bug? (xAxisFormat =) /info/en/?search=Template_talk:Graph:Chart#X-Axis_label_format_bug?_(xAxisFormat_=)

--			linewidths - doesnt work for two values (eg 0, 1) but work if added third value of both are zeros? Same for marksStroke - probably bug in Graph extension

--			clamp - "clamp" used to avoid marks outside marks area, "clip" should be use instead but not working in Graph extension, see https://phabricator.wikimedia.org/T251709

--	TODO:  

--			marks:

--				- line strokeDash + serialization,

--				- symStroke serialization

--				- symbolsNoFill serialization

--				- arbitrary SVG path symbol shape as symbolsShape argument

--				- annotations

--					- vertical / horizontal line at specific values [DONE] 2020-09-01

--					- rectangle shape for x,y data range

--				- graph type serialization (deep rebuild reqired)

--	     - second axis (deep rebuild required - assignment of series to one of two axies) 



-- Version History (_PLEASE UPDATE when modifying anything_):

--   2020-09-01 Vertical and horizontal line annotations

--   2020-08-08 New logic for "nice" for x axis (problem with scale when xType = "date") and grid

--   2020-06-21 Serializes symbol size

--              transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line)

--              Linewidth serialized with "linewidths"

--              Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0

--              p.chartDebuger(frame) for easy debug and JSON output 

--   2020-06-07 Allow lowercase variables for use with [[Template:Wikidata list]]

--   2020-05-27 Map: allow specification which feature to display and changing the map center

--   2020-04-08 Change default showValues.fontcolor from black to persistentGrey

--   2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true

--   2020-03-11 Allow user-defined scale types, e.g. logarithmic scale

--   2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid

--   2019-01-24 Allow comma-separated lists to contain values with commas

--   2018-10-13 Fix browser color-inversion issues via #54595d per [[mw:Template:Graph:PageViews]]

--   2018-09-16 Allow disabling the legend for templates

--   2018-09-10 Allow grid lines

--   2018-08-26 Use user-defined order for stacked charts

--   2018-02-11 Force usage of explicitely provided x minimum and/or maximum values, rotation of x labels

--   2017-08-08 Added showSymbols param to show symbols on line charts

--   2016-05-16 Added encodeTitleForPath() to help all path-based APIs graphs like pageviews

--   2016-03-20 Allow omitted data for charts, labels for line charts with string (ordinal) scale at point location

--   2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon.



local p = {}



--add debug text to this string with eg. 	debuglog = debuglog .. "" .. "\n\n"  .. "- " .. debug.traceback() .. "result type: ".. type(result) ..  " result: \n\n" .. mw.dumpObject(result) 

--invoke chartDebuger() to get graph JSON and this string

debuglog = "Debug " .. "\n\n" 



local baseMapDirectory = "Module:Graph/"

local persistentGrey = "#54595d"



local shapes = {}

shapes = { 

	circle = "circle", x= "M-.5,-.5L.5,.5M.5,-.5L-.5,.5" , square = "square", 

	cross = "cross", diamond = "diamond", triangle_up = "triangle-up", 

	triangle_down = "triangle-down", triangle_right = "triangle-right", 

	triangle_left = "triangle-left", 

	banana = "m -0.5281,0.2880 0.0020,0.0192 m 0,0 c 0.1253,0.0543 0.2118,0.0679 0.3268,0.0252 0.1569,-0.0582 0.3663,-0.1636 0.4607,-0.3407 0.0824,-0.1547 0.1202,-0.2850 0.0838,-0.4794 l 0.0111,-0.1498 -0.0457,-0.0015 c -0.0024,0.3045 -0.1205,0.5674 -0.3357,0.7414 -0.1409,0.1139 -0.3227,0.1693 -0.5031,0.1856 m 0,0 c 0.1804,-0.0163 0.3622,-0.0717 0.5031,-0.1856 0.2152,-0.1739 0.3329,-0.4291 0.3357,-0.7414 l -0.0422,0.0079 c 0,0 -0.0099,0.1111 -0.0227,0.1644 -0.0537,0.1937 -0.1918,0.3355 -0.3349,0.4481 -0.1393,0.1089 -0.2717,0.2072 -0.4326,0.2806 l -0.0062,0.0260" 

	   	}





local function numericArray(csv)

	if not csv then return end



	local list = mw.text.split(csv, "%s*,%s*")

	local result = {}

	local isInteger = true

	for i = 1, #list do

		if listi == "" then

			resulti = nil

		else

			resulti = tonumber(listi])

			if not resulti then return end

			if isInteger then

				local int, frac = math.modf(resulti])

				isInteger = frac == 0.0

			end

		end

	end



    return result, isInteger

end



local function stringArray(text)

	if not text then return end



	local list = mw.text.split(mw.ustring.gsub(tostring(text), "\\,", "<COMMA>"), ",", true)

	for i = 1, #list do

		listi = mw.ustring.gsub(mw.text.trim(listi]), "<COMMA>", ",")

	end

	return list

end



local function isTable(t) return type(t) == "table" end



local function copy(x)

	if type(x) == "table" then

		local result = {}

		for key, value in pairs(x) do resultkey = copy(value) end

		return result

	else

		return x

	end

end



function p.map(frame)

	-- map path data for geographic objects

	local basemap = frame.args.basemap or "Template:Graph:Map/Inner/Worldmap2c-json" -- WorldMap name and/or location may vary from wiki to wiki

	-- scaling factor

	local scale = tonumber(frame.args.scale) or 100

	-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections

	local projection = frame.args.projection or "equirectangular"

	-- defaultValue for geographic objects without data

	local defaultValue = frame.args.defaultValue or frame.args.defaultvalue

	local scaleType = frame.args.scaleType or frame.args.scaletype or "linear"

	-- minimaler Wertebereich (nur für numerische Daten)

	local domainMin = tonumber(frame.args.domainMin or frame.args.domainmin)

	-- maximaler Wertebereich (nur für numerische Daten)

	local domainMax = tonumber(frame.args.domainMax or frame.args.domainmax)

	-- Farbwerte der Farbskala (nur für numerische Daten)

	local colorScale = frame.args.colorScale or frame.args.colorscale or "category10"

	-- show legend

	local legend = frame.args.legend

	-- the map feature to display

    local feature = frame.args.feature or "countries"

    -- map center

    local center = numericArray(frame.args.center)

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

	local values = {}

	local isNumbers = nil

	for name, value in pairs(frame.args) do

		if mw.ustring.find(name, "^[^%l]+$") and value and value ~= "" then

			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

	if not defaultValue then

		if isNumbers then defaultValue = 0 else defaultValue = "silver" end

	end



	-- create highlight scale

	local scales

	if isNumbers then

		if colorScale then colorScale = string.lower(colorScale) end

		if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end

		scales =

		{

			{

				name = "color",

				type = scaleType,

				domain = { data = "highlights", field = "v" },

				range = colorScale,

				nice = true,

				zero = false

			}

		}

		if domainMin then scales1].domainMin = domainMin end

		if domainMax then scales1].domainMax = domainMax end



		local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent

		if exponent then

			scales1].type = "pow"

			scales1].exponent = exponent

		end

	end



	-- create legend

	if legend then

		legend =

		{

			{

				fill = "color",

				offset = 120,

				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(basemap, 1, 10) == "wikiraw://") then

		basemapUrl = basemap

	else

		-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.

		if not string.find(basemap, ":") then basemap = baseMapDirectory .. basemap end

		basemapUrl = "wikiraw:///" .. mw.uri.encode(mw.title.new(basemap).prefixedText, "PATH")

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

			},

			{

				-- data source for map paths data

				name = feature,

				url = basemapUrl,

				format = { type = "topojson", feature = feature },

				transform =

				{

					{

						-- geographic transformation ("geopath") of map paths data

						type = "geopath",

						value = "data",			-- data source

						scale = scale,

                        translate = { 0, 0 },

                        center = center,

						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

					}

				}

			}

		},

		marks =

		{

			-- output markings (map paths and highlights)

			{

				type = "path",

				from = { data = feature },

				properties =

				{

					enter = { path = { field = "layout_path" } },

					update = { fill = { field = "zipped.v" } },

					hover = { fill = { value = "darkgrey" } }

				}

			}

		},

		legends = legend

	}

	if (scales) then

		output.scales = scales

		output.marks1].properties.update.fill.scale = "color"

	end



	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)

	local x



	if not xType or xType == "integer" or xType == "number" then

		local isInteger

		x, isInteger = numericArray(serializedX)

		if x then

			xMin = tonumber(xMin)

			xMax = tonumber(xMax)

			if not xType then

				if isInteger then xType = "integer" else xType = "number" end

			end

		else

			if xType then error("Numbers expected for parameter 'x'") end

		end

	end

	if not x then

		x = stringArray(serializedX)

		if not xType then xType = "string" end

	end

	return x, xType, xMin, xMax

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



		yyNum = yValues

	end

	if not yType then

		if areAllInteger then yType = "integer" else yType = "number" end

	end

	if yType == "integer" or yType == "number" then

		yMin = tonumber(yMin)

		yMax = tonumber(yMax)

	end



	return y, yType, yMin, yMax

end



local function convertXYToManySeries(x, y, xType, yType, seriesTitles)

	local data =

	{

		name = "chart",

		format =

		{

			type = "json",

			parse = { x = xType, y = yType }

		},

		values = {}

	}

	for i = 1, #y do

		local yLen = table.maxn(yi])

		for j = 1, #x do

			if j <= yLen and yi][j then table.insert(data.values, { series = seriesTitlesi], x = xj], y = yi][j }) end

		end

	end

	return data

end



local function convertXYToSingleSeries(x, y, xType, yType, yNames)

	local data = { name = "chart", format = { type = "json", parse = { x = xType } }, values = {} }



	for j = 1, #y do data.format.parseyNamesj]] = yType end



	for i = 1, #x do

		local item = { x = xi }

		for j = 1, #y do itemyNamesj]] = yj][i end



		table.insert(data.values, item)

	end

	return data

end



local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)

	if chartType == "pie" then return end



	local xscale =

	{

		name = "x",

		range = "width",

		zero = false, -- do not include zero value

		domain = { data = "chart", field = "x" }

	}

	if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end

	if xMin then xscale.domainMin = xMin end

	if xMax then xscale.domainMax = xMax end

	if xMin or xMax then

		xscale.clamp = true

		xscale.nice = false

	end

	if chartType == "rect" then

		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"

			xscale.points = true

		end

	end

	if xType and xType ~= "date" and xScaleType ~= "log" then xscale.nice = true end -- force round numbers for x scale, but "log" and "date" scale outputs a wrong "nice" scale

	return xscale

end



local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)

	if chartType == "pie" then return end



	local yscale =

	{

		name = "y",

		 --type = yScaleType or "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 = chartType ~= "line",

		nice = yScaleType ~= "log" -- force round numbers for y scale, but log scale outputs a wrong "nice" scale

	}

	if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end

	if yMin then yscale.domainMin = yMin end

	if yMax then yscale.domainMax = yMax end

	if yMin or yMax then yscale.clamp = true end

	if yType == "date" then yscale.type = "time"

	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



	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 = 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 there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale

	if isTable(colors) then

		local alphas = {}

		local hasAlpha = false

		for i = 1, #colors do

			local a, rgb = string.match(colorsi], "#(%x%x)(%x%x%x%x%x%x)")

			if a then

				hasAlpha = true

				alphasi = tostring(tonumber(a, 16) / 255.0)

				colorsi = "#" .. rgb

			else

				alphasi = "1"

			end

		end

		for i = #colors + 1, #y do alphasi = "1" end

		if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end

	end

	return alphaScale

end



local function getLineScale(linewidths, chartType)

	local lineScale = {}

 

	lineScale =

    	{

        name = "line",

        type = "ordinal",

        range = linewidths,

        domain = { data = "chart", field = "series" }

    	}



	return lineScale

end



local function getSymSizeScale(symSize)

	local SymSizeScale = {}

	SymSizeScale =

       	{

        name = "symSize",

        type = "ordinal",

        range = symSize,

        domain = { data = "chart", field = "series" }

        }



	return SymSizeScale

end



local function getSymShapeScale(symShape)

	local SymShapeScale = {}

	SymShapeScale =

       	{

        name = "symShape",

        type = "ordinal",

        range = symShape,

        domain = { data = "chart", field = "series" }

        }



	return SymShapeScale

end



local function getValueScale(fieldName, min, max, type)

	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.entercolorField = { scale = "color", field = dataField }



	-- action when cursor is over plot mark: highlight

	if not plotMarks.properties.hover then plotMarks.properties.hover = {} end

	plotMarks.properties.hovercolorField = { value = "red" }



	-- action when cursor leaves plot mark: reset to initial setup

	if not plotMarks.properties.update then plotMarks.properties.update = {} end

	plotMarks.properties.updatecolorField = { 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



	addInteractionToChartVisualisation(chartvis, "fill", "x")



	return chartvis

end



local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate)

	if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end



	local chartvis =

	{

		type = chartType,

		properties =

		{

			-- chart creation event handler

			enter =

			{

				x = { scale = "x", field = "x" },

				y = { scale = "y", field = "y" }

			}

		}

	}

	addInteractionToChartVisualisation(chartvis, colorField, "series")

	if colorField == "stroke" then

		chartvis.properties.enter.strokeWidth = { value = linewidth or 2.5 }

		if type(lineScale) =="table"  then 

			chartvis.properties.enter.strokeWidth.value = nil

			chartvis.properties.enter.strokeWidth = 

			{

				scale = "line",

				field= "series"

			} 

		end

	end



	if interpolate then chartvis.properties.enter.interpolate = { value = interpolate } end



	if alphaScale then chartvis.properties.updatecolorField .. "Opacity" = { scale = "transparency" } end

	-- for bars and area charts set the lower bound of their areas

	if chartType == "rect" or chartType == "area" then

		if stacked then

			-- for stacked charts this lower bound is the end of the last stacking element

			chartvis.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 ]]

			chartvis.properties.enter.y2 = { scale = "y", value = 0 }

		end

	end

	-- for bar charts ...

	if chartType == "rect" then

		-- set 1 pixel width between the bars

		chartvis.properties.enter.width = { scale = "x", band = true, offset = -1 }

		-- for multiple series the bar marking needs to use the "inner" series scale, whereas the "outer" x scale is used by the grouping

		if not stacked and yCount > 1 then

			chartvis.properties.enter.x.scale = "series"

			chartvis.properties.enter.x.field = "series"

			chartvis.properties.enter.width.scale = "series"

		end

	end

	-- stacked charts have their own (stacked) y values

	if stacked then chartvis.properties.enter.y.field = "layout_start" end



	-- if there are multiple series group these together

	if yCount == 1 then

		chartvis.from = { data = "chart" }

	else

		-- if there are multiple series, connect colors to series

		chartvis.properties.updatecolorField].field = "series"

		if alphaScale then chartvis.properties.updatecolorField .. "Opacity"].field = "series" end

		

	    -- if there are multiple series, connect linewidths to series

		if chartype == "line" then

			chartvis.properties.update"strokeWidth"].field = "series"

		end



		

		-- apply a grouping (facetting) transformation

		chartvis =

		{

			type = "group",

			marks = { chartvis },

			from =

			{

				data = "chart",

				transform =

				{

					{

						type = "facet",

						groupby = { "series" }

					}

				}

			}

		}

		-- for stacked charts apply a stacking transformation

		if stacked then

			table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } )

		else

			-- for bar charts the series are side-by-side grouped by x

			if chartType == "rect" then

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

				local groupScale =

				{

					name = "series",

					type = "ordinal",

					range = "width",

					domain = { field = "series" }

				}



				chartvis.from.transform1].groupby = "x"

				chartvis.scales = { groupScale }

				chartvis.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }

			end

		end

	end



	return chartvis

end



local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues)

	local properties

	if chartType == "rect" then

		properties =

		{

			x = { scale = chartvis.properties.enter.x.scale, field = chartvis.properties.enter.x.field },

			y = { scale = chartvis.properties.enter.y.scale, field = chartvis.properties.enter.y.field, offset = -(tonumber(showValues.offset) or -4) },

			--dx = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text

			dy = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for vertical text

			align = { },

			baseline = { value = "middle" },

			fill = { },

			angle = { value = -90 },

			fontSize = { value = tonumber(showValues.fontsize) or 11 }

		}

		if properties.y.offset >= 0 then

			properties.align.value = "right"

			properties.fill.value = showValues.fontcolor or "white"

		else

			properties.align.value = "left"

			properties.fill.value = showValues.fontcolor or persistentGrey

		end

	elseif chartType == "pie" then

		properties =

		{

			x = { group = "width", mult = 0.5 },

			y = { group = "height", mult = 0.5 },

			radius = { offset = tonumber(showValues.offset) or -4 },

			theta = { field = "layout_mid" },

			fill = { value = showValues.fontcolor or persistentGrey },

			baseline = { },

			angle = { },

			fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) }

		}

		if (showValues.angle or "midangle") == "midangle" then

			properties.align = { value = "center" }

			properties.angle = { field = "layout_mid", mult = 180.0 / math.pi }



			if properties.radius.offset >= 0 then

				properties.baseline.value = "bottom"

			else

				if not showValues.fontcolor then properties.fill.value = "white" end

				properties.baseline.value = "top"

			end

		elseif tonumber(showValues.angle) then

			-- qunatize scale for aligning text left on right half-circle and right on left half-circle

			local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "left", "right" } }

			table.insert(scales, alignScale)



			properties.align = { scale = alignScale.name, field = "layout_mid" }

			properties.angle = { value = tonumber(showValues.angle) }

			properties.baseline.value = "middle"

			if not tonumber(showValues.offset) then properties.radius.offset = 4 end

		end



		if radiusScale then

			properties.radius.scale = radiusScale.name

			properties.radius.field = radiusScale.domain.field

		else

			properties.radius.value = outerRadius

		end

	end



	if properties then

		if showValues.format then

			local template = "datum.y"

			if yType == "integer" or yType == "number" then template = template .. "|number:'" .. showValues.format .. "'"

			elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'"

			end

			properties.text = { template = "{{" .. template .. "}}" }

		else

			properties.text = { field = "y" }

		end



		local textmarks =

		{

			type = "text",

			properties =

			{

				enter = properties

			}

		}

		if chartvis.from then textmarks.from = copy(chartvis.from) end



		return textmarks

	end

end



local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale)



	local symbolmarks 

	symbolmarks =

	{

		type = "symbol",

		properties =

		{

			enter = 

			{

				x = { scale = "x", field = "x" },

				y = { scale = "y", field = "y" },

				strokeWidth = { value = symStroke },

				stroke = { scale = "color", field = "series" },

				fill = { scale = "color", field = "series" },

			}

		}

	}

	if type(symShape) == "string" then 

		symbolmarks.properties.enter.shape = { value = symShape }

	end

	if type(symShape) == "table" then 

		symbolmarks.properties.enter.shape = { scale = "symShape", field = "series" }

	end

	if type(symSize) == "number" then 

		symbolmarks.properties.enter.size = { value = symSize }

	end

	if type(symSize) == "table" then 

		symbolmarks.properties.enter.size = { scale = "symSize", field = "series" }

	end

	if noFill then 

		symbolmarks.properties.enter.fill = nil

	end

	if alphaScale then 

		symbolmarks.properties.enter.fillOpacity = 

		{ scale = "transparency", field = "series" } 

		symbolmarks.properties.enter.strokeOpacity = 

		{ scale = "transparency", field = "series" } 

	end

	if chartvis.from then symbolmarks.from = copy(chartvis.from) end

    

	return symbolmarks

end



local function getAnnoMarks(chartvis, stroke, fill, opacity)



	local vannolines, hannolines, vannoLabels, vannoLabels 

	vannolines =

	{

		type = "rule",

		from = { data = "v_anno" },

		properties =

		{

			update = 

			{

				x = { scale = "x", field = "x" },

				y = { value = 0 },

				y2 = {  field = { group = "height" } },

				strokeWidth = { value = stroke },

				stroke = { value = persistentGrey },

				opacity = { value = opacity }

			}

		}

	}

	vannolabels =

	{

		type = "text",

		from = { data = "v_anno" },

		properties =

		{

			update = 

			{

				x = { scale = "x", field = "x", offset = 3 },

				y = {  field = { group = "height" }, offset = -3 },

				text = { field = "label" },

				baseline = { value = "top" },

		        angle = { value = -90 },

		        fill = { value = persistentGrey },

		        opacity = { value = opacity }

			}

		}

	}	

	hannolines =

	{

		type = "rule",

		from = { data = "h_anno" },

		properties =

		{

			update = 

			{

				y = { scale = "y", field = "y" },

				x = { value = 0 },

				x2 = {  field = { group = "width" } },

				strokeWidth = { value = stroke },

				stroke = { value = persistentGrey },

				opacity = { value = opacity }

			}

		}

	}

	hannolabels =

	{

		type = "text",

		from = { data = "h_anno" },

		properties =

		{

			update = 

			{

				y = { scale = "y", field = "y", offset = 3 },

				x = {  value = 0 , offset = 3 },

				text = { field = "label" },

				baseline = { value = "top" },

		        angle = { value = 0 },

		        fill = { value = persistentGrey },

		        opacity = { value = opacity }

			}

		}

	}

	return vannolines, vannolabels, hannolines, hannolabels

end



local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)

	local xAxis, yAxis

	if chartType ~= "pie" then

		if xType == "integer" and not xAxisFormat then xAxisFormat = "d" end

		xAxis =

		{

			type = "x",

			scale = "x",

			title = xTitle,

			format = xAxisFormat,

			grid = xGrid

		}

		if xAxisAngle then

			local xAxisAlign

			if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end

			xAxis.properties =

			{

				title =

				{

					fill = { value = persistentGrey }

				},

				labels =

				{

					angle = { value = xAxisAngle },

					align = { value = xAxisAlign },

					fill = { value = persistentGrey }

				},

				ticks =

				{

					stroke = { value = persistentGrey }

				},

				axis =

				{

					stroke = { value = persistentGrey },

					strokeWidth = { value = 2 }

				},

				grid =

				{

					stroke = { value = persistentGrey }

				}

			}

		else

			xAxis.properties =

			{

				title =

				{

					fill = { value = persistentGrey }

				},

				labels =

				{

					fill = { value = persistentGrey }

				},

				ticks =

				{

					stroke = { value = persistentGrey }

				},

				axis =

				{

					stroke = { value = persistentGrey },

					strokeWidth = { value = 2 }

				},

				grid =

				{

					stroke = { value = persistentGrey }

				}

			}

		end



		if yType == "integer" and not yAxisFormat then yAxisFormat = "d" end

		yAxis =

		{

			type = "y",

			scale = "y",

			title = yTitle,

			format = yAxisFormat,

			grid = yGrid

		}

		yAxis.properties =

		{

			title =

			{

				fill = { value = persistentGrey }

			},

			labels =

			{

				fill = { value = persistentGrey }

			},

			ticks =

			{

				stroke = { value = persistentGrey }

			},

			axis =

			{

				stroke = { value = persistentGrey },

				strokeWidth = { value = 2 }

			},

			grid =

			{

				stroke = { value = persistentGrey }

			}

		}

	

	end



	return xAxis, yAxis

end



local function getLegend(legendTitle, chartType, outerRadius)

	local legend =

	{

		fill = "color",

		stroke = "color",

		title = legendTitle,

	}

	legend.properties = {

		title = {

			fill = { value = persistentGrey },

		},

		labels = {

			fill = { value = persistentGrey },

		},

	}

	if chartType == "pie" then

		legend.properties = {

			-- move legend from center position to top

			legend = {

				y = { value = -outerRadius },

			},

			title = {

				fill = { value = persistentGrey }

			},

			labels = {

				fill = { value = persistentGrey },

			},

		}

	end

	return 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 colorString = frame.args.colors

	if colorString then colorString = string.lower(colorString) end

	local colors = stringArray(colorString)

	-- for line charts, the thickness of the line; for pie charts the gap between each slice

	local linewidth = tonumber(frame.args.linewidth)

	local linewidthsString = frame.args.linewidths

	local linewidths

	if linewidthsString and linewidthsString ~= "" then linewidths = numericArray(linewidthsString) or false end

	-- x and y axis caption

	local xTitle = frame.args.xAxisTitle  or frame.args.xaxistitle

	local yTitle = frame.args.yAxisTitle  or frame.args.yaxistitle

	-- x and y value types

	local xType = frame.args.xType or frame.args.xtype

	local yType = frame.args.yType or frame.args.ytype

	-- override x and y axis minimum and maximum

	local xMin = frame.args.xAxisMin or frame.args.xaxismin

	local xMax = frame.args.xAxisMax or frame.args.xaxismax

	local yMin = frame.args.yAxisMin or frame.args.yaxismin

	local yMax = frame.args.yAxisMax or frame.args.yaxismax

	-- override x and y axis label formatting

	local xAxisFormat = frame.args.xAxisFormat or frame.args.xaxisformat

	local yAxisFormat = frame.args.yAxisFormat or frame.args.yaxisformat

	local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle)

	-- x and y scale types

	local xScaleType = frame.args.xScaleType or frame.args.xscaletype 

	local yScaleType = frame.args.yScaleType or frame.args.yscaletype  

-- log scale require minimum > 0, for now it's no possible to plot negative values on log - TODO see: https://www.mathworks.com/matlabcentral/answers/1792-log-scale-graphic-with-negative-value

--	if xScaleType == "log" then

--		if (not xMin or tonumber(xMin) <= 0) then xMin = 0.1 end

--		if not xType then xType = "number" end

--	end

--	if yScaleType == "log" then

--		if (not yMin or tonumber(yMin) <= 0) then yMin = 0.1 end

--		if not yType then yType = "number" end

--	end



	-- show grid

	local xGrid = frame.args.xGrid or frame.args.xgrid or false

	local yGrid = frame.args.yGrid or frame.args.ygrid or false

	-- for line chart, show a symbol at each data point

	local showSymbols = frame.args.showSymbols or frame.args.showsymbols

	local symbolsShape = frame.args.symbolsShape or frame.args.symbolsshape

	local symbolsNoFill = frame.args.symbolsNoFill or frame.args.symbolsnofill 

	local symbolsStroke = tonumber(frame.args.symbolsStroke or frame.args.symbolsstroke)

	-- show legend with given title

	local legendTitle = frame.args.legend

	-- show values as text

	local showValues = frame.args.showValues or frame.args.showvalues 

	-- show v- and h-line annotations

	local v_annoLineString = frame.args.vAnnotatonsLine or frame.args.vannotatonsline

	local h_annoLineString = frame.args.hAnnotatonsLine or frame.args.hannotatonsline

	local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel

	local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel











	-- decode annotations cvs

	local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel

	if v_annoLineString and v_annoLineString ~= "" then



		if xType == "number" or xType == "integer" then 

		v_annoLine = numericArray(v_annoLineString)



		else 

			v_annoLine = stringArray(v_annoLineString)



		end

		v_annoLabel = stringArray(v_annoLabelString)

	end

	if h_annoLineString and h_annoLineString ~= "" then



		if yType == "number" or yType == "integer" then 

			h_annoLine = numericArray(h_annoLineString)



		else 

			h_annoLine = stringArray(h_annoLineString)



		end

		h_annoLabel = stringArray(h_annoLabelString)

	end











	-- pie chart radiuses

	local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0

	local outerRadius = math.min(graphwidth, graphheight)

	-- format JSON output

	local formatJson = frame.args.formatjson



	-- get x values

	local x

	x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax)



	-- get y values (series)

	local yValues = {}

	local seriesTitles = {}

	for name, value in pairs(frame.args) do

		local yNum

		if name == "y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end

		if yNum then

			yValuesyNum = value

			-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.

			seriesTitlesyNum = frame.args"y" .. yNum .. "Title" or frame.args"y" .. yNum .. "title" or name

		end

	end

	local y

	y, yType, yMin, yMax = deserializeYData(yValues, yType, yMin, yMax)



	-- create data tuples, consisting of series index, x value, y value

	local data

	if chartType == "pie" then

		-- for pie charts the second second series is merged into the first series as radius values

		data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" })

	else

		data = convertXYToManySeries(x, y, xType, yType, seriesTitles)

	end



	-- configure stacked charts

	local stacked = false

	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 =

		{

			name = "stats", source = "chart", transform =

		{

			{

				type = "aggregate",

				groupby = { "x" },

				summarize = { y = "sum" }

			}

		}

		}

		end

	end

	

	-- add annotations to data

	local vannoData, hannoData

	

	if v_annoLine then

		vannoData = { name = "v_anno", format = { type = "json", parse = { x = xType } }, values = {} }

		for i = 1, #v_annoLine do

			local item = { x = v_annoLinei], label = v_annoLabeli }

			table.insert(vannoData.values, item)

		end

	end	

	if h_annoLine then

		hannoData = { name = "h_anno", format = { type = "json", parse = { y = yType } }, values = {} }

		for i = 1, #h_annoLine do

			local item = { y = h_annoLinei], label = h_annoLabeli }

			table.insert(hannoData.values, item)

		end

	end	





	-- create scales

	local scales = {}



	local xscale = getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)

	table.insert(scales, xscale)

	local yscale = getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)

	table.insert(scales, yscale)



	local colorScale = getColorScale(colors, chartType, #x, #y)

	table.insert(scales, colorScale)



	local alphaScale = getAlphaColorScale(colors, y)

	table.insert(scales, alphaScale)



	local lineScale

	if (linewidths) and (chartType == "line") then

		lineScale = getLineScale(linewidths, chartType)

		table.insert(scales, lineScale)

	end



	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, lineScale, interpolate)

	local marks = { chartvis }

	

	-- text marks

	if showValues then

		if type(showValues) == "string" then -- deserialize as table

			local keyValues = mw.text.split(showValues, "%s*,%s*")

			showValues = {}

			for _, kv in ipairs(keyValues) do

				local key, value = mw.ustring.match(kv, "^%s*(.-)%s*:%s*(.-)%s*$")

				if key then showValueskey = value end

			end

		end



		local chartmarks = chartvis

		if chartmarks.marks then chartmarks = chartmarks.marks1 end

		local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues)

		if chartmarks ~= chartvis then

			table.insert(chartvis.marks, textmarks)

		else

			table.insert(marks, textmarks)

		end

	end

	

    -- grids

    if xGrid then 

    	if xGrid == "0" then xGrid = false

    	elseif xGrid == 0 then xGrid = false 

    	elseif xGrid == "false" then xGrid = false 

    	elseif xGrid == "n" then xGrid = false 

    	else xGrid = true 

    	end

    end

    if yGrid then 

    	if yGrid == "0" then yGrid = false

    	elseif yGrid == 0 then yGrid = false 

    	elseif yGrid == "false" then yGrid = false 

    	elseif yGrid == "n" then yGrid = false 

    	else yGrid = true 

    	end

    end

    

	-- symbol marks

	if showSymbols and chartType ~= "rect" then

		local chartmarks = chartvis

		if chartmarks.marks then chartmarks = chartmarks.marks1 end



		if type(showSymbols) == "string" then

			if showSymbols == "" then showSymbols = true

			else showSymbols = numericArray(showSymbols)

			end

		else

			showSymbols = tonumber(showSymbols)

		end



		-- custom size

		local symSize

		if type(showSymbols) == "number" then 

			symSize = tonumber(showSymbols*showSymbols*8.5)

	 	elseif type(showSymbols) == "table" then 

	 		symSize = {}

	 		for k, v in pairs(showSymbols) do

                symSizek=v*v*8.5 -- "size" acc to Vega syntax is area of symbol

            end

        else

	 		symSize = 50

	 	end

		-- symSizeScale 

	 	local symSizeScale = {}

		if type(symSize) == "table" then

			symSizeScale = getSymSizeScale(symSize)

			table.insert(scales, symSizeScale)

		end

 	



    	-- custom shape

    	if  stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end

    	

    	local symShape = " "

		

		if type(symbolsShape) == "string" and shapessymbolsShape then

			symShape = shapessymbolsShape

	 	elseif type(symbolsShape) == "table" then 

	 		symShape = {}

	 		for k, v in pairs(symbolsShape) do

                if symbolsShapek and shapessymbolsShapek]] then 

                	symShapek=shapessymbolsShapek]]

                else

                	symShapek = "circle"

                end

            end

       	else

			symShape = "circle"

		end

		-- symShapeScale 

	 	local symShapeScale = {}

		if type(symShape) == "table" then

			symShapeScale = getSymShapeScale(symShape)

			table.insert(scales, symShapeScale)

		end 

 

		-- custom stroke

		local symStroke

		if (type(symbolsStroke) == "number") then 

			symStroke = tonumber(symbolsStroke)

-- TODO symStroke serialization

--		elseif type(symbolsStroke) == "table" then 

--	 		symStroke = {}

--	 		for k, v in pairs(symbolsStroke) do

--                symStroke[k]=symbolsStroke[k]

--                		--always draw x with stroke

--				if symbolsShape[k] == "x" then symStroke[k] = 2.5 end

				--always draw x with stroke

--				if symbolsNoFill[k] then symStroke[k] = 2.5 end

--            end

		else 

			symStroke = 0

		--always draw x with stroke

			if symbolsShape == "x" then symStroke = 2.5 end

		--always draw x with stroke

			if symbolsNoFill then symStroke = 2.5 end

	 	end 		





--	TODO	-- symStrokeScale 

--	 	local symStrokeScale = {}

--		if type(symStroke) == "table" then

--			symStrokeScale = getSymStrokeScale(symStroke)

--			table.insert(scales, symStrokeScale)

--		end





		

		local symbolmarks = getSymbolMarks(chartmarks, symSize, symShape, symStroke, symbolsNoFill, alphaScale)

		if chartmarks ~= chartvis then

			table.insert(chartvis.marks, symbolmarks)

		else

			table.insert(marks, symbolmarks)

		end

	end





	local vannolines, vannolabels, hannolines, hannolabels = getAnnoMarks(chartmarks, persistentGrey, persistentGrey, 0.75)

	if vannoData then

		table.insert(marks, vannolines)

		table.insert(marks, vannolabels)

	end

	if hannoData then

		table.insert(marks, hannolines)

		table.insert(marks, hannolabels)

	end



	-- axes

	local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)

	

	-- legend

	local legend

	if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end

	-- construct final output object

	local output =

	{

		version = 2,

		width = graphwidth,

		height = graphheight,

		data = { data },

		scales = scales,

		axes = { xAxis, yAxis },

		marks = marks,

		legends = { legend }

	}

	if vannoData then table.insert(output.data, vannoData) end

	if hannoData then table.insert(output.data, hannoData) end

	if stats then table.insert(output.data, stats) end



	local flags

	if formatJson then flags = mw.text.JSON_PRETTY end

	return mw.text.jsonEncode(output, flags)

end



function p.mapWrapper(frame)

	return p.map(frame:getParent())

end



function p.chartWrapper(frame)

	return p.chart(frame:getParent())

end



function p.chartDebuger(frame)

	return   "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog 

end





-- Given an HTML-encoded title as first argument, e.g. one produced with {{ARTICLEPAGENAME}},

-- convert it into a properly URL path-encoded string

-- This function is critical for any graph that uses path-based APIs, e.g. PageViews graph

function p.encodeTitleForPath(frame)

	return mw.uri.encode(mw.text.decode(mw.text.trim(frame.args1])), 'PATH')

end



return p
Permanently protected module
From Wikipedia, the free encyclopedia


-- ATTENTION:	Please edit this code at https://de.wikipedia.org/wiki/Modul:Graph

--          	This way all wiki languages can stay in sync. Thank you!

--

--	BUGS:	X-Axis label format bug? (xAxisFormat =) /info/en/?search=Template_talk:Graph:Chart#X-Axis_label_format_bug?_(xAxisFormat_=)

--			linewidths - doesnt work for two values (eg 0, 1) but work if added third value of both are zeros? Same for marksStroke - probably bug in Graph extension

--			clamp - "clamp" used to avoid marks outside marks area, "clip" should be use instead but not working in Graph extension, see https://phabricator.wikimedia.org/T251709

--	TODO:  

--			marks:

--				- line strokeDash + serialization,

--				- symStroke serialization

--				- symbolsNoFill serialization

--				- arbitrary SVG path symbol shape as symbolsShape argument

--				- annotations

--					- vertical / horizontal line at specific values [DONE] 2020-09-01

--					- rectangle shape for x,y data range

--				- graph type serialization (deep rebuild reqired)

--	     - second axis (deep rebuild required - assignment of series to one of two axies) 



-- Version History (_PLEASE UPDATE when modifying anything_):

--   2020-09-01 Vertical and horizontal line annotations

--   2020-08-08 New logic for "nice" for x axis (problem with scale when xType = "date") and grid

--   2020-06-21 Serializes symbol size

--              transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line)

--              Linewidth serialized with "linewidths"

--              Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0

--              p.chartDebuger(frame) for easy debug and JSON output 

--   2020-06-07 Allow lowercase variables for use with [[Template:Wikidata list]]

--   2020-05-27 Map: allow specification which feature to display and changing the map center

--   2020-04-08 Change default showValues.fontcolor from black to persistentGrey

--   2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true

--   2020-03-11 Allow user-defined scale types, e.g. logarithmic scale

--   2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid

--   2019-01-24 Allow comma-separated lists to contain values with commas

--   2018-10-13 Fix browser color-inversion issues via #54595d per [[mw:Template:Graph:PageViews]]

--   2018-09-16 Allow disabling the legend for templates

--   2018-09-10 Allow grid lines

--   2018-08-26 Use user-defined order for stacked charts

--   2018-02-11 Force usage of explicitely provided x minimum and/or maximum values, rotation of x labels

--   2017-08-08 Added showSymbols param to show symbols on line charts

--   2016-05-16 Added encodeTitleForPath() to help all path-based APIs graphs like pageviews

--   2016-03-20 Allow omitted data for charts, labels for line charts with string (ordinal) scale at point location

--   2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon.



local p = {}



--add debug text to this string with eg. 	debuglog = debuglog .. "" .. "\n\n"  .. "- " .. debug.traceback() .. "result type: ".. type(result) ..  " result: \n\n" .. mw.dumpObject(result) 

--invoke chartDebuger() to get graph JSON and this string

debuglog = "Debug " .. "\n\n" 



local baseMapDirectory = "Module:Graph/"

local persistentGrey = "#54595d"



local shapes = {}

shapes = { 

	circle = "circle", x= "M-.5,-.5L.5,.5M.5,-.5L-.5,.5" , square = "square", 

	cross = "cross", diamond = "diamond", triangle_up = "triangle-up", 

	triangle_down = "triangle-down", triangle_right = "triangle-right", 

	triangle_left = "triangle-left", 

	banana = "m -0.5281,0.2880 0.0020,0.0192 m 0,0 c 0.1253,0.0543 0.2118,0.0679 0.3268,0.0252 0.1569,-0.0582 0.3663,-0.1636 0.4607,-0.3407 0.0824,-0.1547 0.1202,-0.2850 0.0838,-0.4794 l 0.0111,-0.1498 -0.0457,-0.0015 c -0.0024,0.3045 -0.1205,0.5674 -0.3357,0.7414 -0.1409,0.1139 -0.3227,0.1693 -0.5031,0.1856 m 0,0 c 0.1804,-0.0163 0.3622,-0.0717 0.5031,-0.1856 0.2152,-0.1739 0.3329,-0.4291 0.3357,-0.7414 l -0.0422,0.0079 c 0,0 -0.0099,0.1111 -0.0227,0.1644 -0.0537,0.1937 -0.1918,0.3355 -0.3349,0.4481 -0.1393,0.1089 -0.2717,0.2072 -0.4326,0.2806 l -0.0062,0.0260" 

	   	}





local function numericArray(csv)

	if not csv then return end



	local list = mw.text.split(csv, "%s*,%s*")

	local result = {}

	local isInteger = true

	for i = 1, #list do

		if listi == "" then

			resulti = nil

		else

			resulti = tonumber(listi])

			if not resulti then return end

			if isInteger then

				local int, frac = math.modf(resulti])

				isInteger = frac == 0.0

			end

		end

	end



    return result, isInteger

end



local function stringArray(text)

	if not text then return end



	local list = mw.text.split(mw.ustring.gsub(tostring(text), "\\,", "<COMMA>"), ",", true)

	for i = 1, #list do

		listi = mw.ustring.gsub(mw.text.trim(listi]), "<COMMA>", ",")

	end

	return list

end



local function isTable(t) return type(t) == "table" end



local function copy(x)

	if type(x) == "table" then

		local result = {}

		for key, value in pairs(x) do resultkey = copy(value) end

		return result

	else

		return x

	end

end



function p.map(frame)

	-- map path data for geographic objects

	local basemap = frame.args.basemap or "Template:Graph:Map/Inner/Worldmap2c-json" -- WorldMap name and/or location may vary from wiki to wiki

	-- scaling factor

	local scale = tonumber(frame.args.scale) or 100

	-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections

	local projection = frame.args.projection or "equirectangular"

	-- defaultValue for geographic objects without data

	local defaultValue = frame.args.defaultValue or frame.args.defaultvalue

	local scaleType = frame.args.scaleType or frame.args.scaletype or "linear"

	-- minimaler Wertebereich (nur für numerische Daten)

	local domainMin = tonumber(frame.args.domainMin or frame.args.domainmin)

	-- maximaler Wertebereich (nur für numerische Daten)

	local domainMax = tonumber(frame.args.domainMax or frame.args.domainmax)

	-- Farbwerte der Farbskala (nur für numerische Daten)

	local colorScale = frame.args.colorScale or frame.args.colorscale or "category10"

	-- show legend

	local legend = frame.args.legend

	-- the map feature to display

    local feature = frame.args.feature or "countries"

    -- map center

    local center = numericArray(frame.args.center)

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

	local values = {}

	local isNumbers = nil

	for name, value in pairs(frame.args) do

		if mw.ustring.find(name, "^[^%l]+$") and value and value ~= "" then

			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

	if not defaultValue then

		if isNumbers then defaultValue = 0 else defaultValue = "silver" end

	end



	-- create highlight scale

	local scales

	if isNumbers then

		if colorScale then colorScale = string.lower(colorScale) end

		if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end

		scales =

		{

			{

				name = "color",

				type = scaleType,

				domain = { data = "highlights", field = "v" },

				range = colorScale,

				nice = true,

				zero = false

			}

		}

		if domainMin then scales1].domainMin = domainMin end

		if domainMax then scales1].domainMax = domainMax end



		local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent

		if exponent then

			scales1].type = "pow"

			scales1].exponent = exponent

		end

	end



	-- create legend

	if legend then

		legend =

		{

			{

				fill = "color",

				offset = 120,

				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(basemap, 1, 10) == "wikiraw://") then

		basemapUrl = basemap

	else

		-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.

		if not string.find(basemap, ":") then basemap = baseMapDirectory .. basemap end

		basemapUrl = "wikiraw:///" .. mw.uri.encode(mw.title.new(basemap).prefixedText, "PATH")

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

			},

			{

				-- data source for map paths data

				name = feature,

				url = basemapUrl,

				format = { type = "topojson", feature = feature },

				transform =

				{

					{

						-- geographic transformation ("geopath") of map paths data

						type = "geopath",

						value = "data",			-- data source

						scale = scale,

                        translate = { 0, 0 },

                        center = center,

						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

					}

				}

			}

		},

		marks =

		{

			-- output markings (map paths and highlights)

			{

				type = "path",

				from = { data = feature },

				properties =

				{

					enter = { path = { field = "layout_path" } },

					update = { fill = { field = "zipped.v" } },

					hover = { fill = { value = "darkgrey" } }

				}

			}

		},

		legends = legend

	}

	if (scales) then

		output.scales = scales

		output.marks1].properties.update.fill.scale = "color"

	end



	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)

	local x



	if not xType or xType == "integer" or xType == "number" then

		local isInteger

		x, isInteger = numericArray(serializedX)

		if x then

			xMin = tonumber(xMin)

			xMax = tonumber(xMax)

			if not xType then

				if isInteger then xType = "integer" else xType = "number" end

			end

		else

			if xType then error("Numbers expected for parameter 'x'") end

		end

	end

	if not x then

		x = stringArray(serializedX)

		if not xType then xType = "string" end

	end

	return x, xType, xMin, xMax

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



		yyNum = yValues

	end

	if not yType then

		if areAllInteger then yType = "integer" else yType = "number" end

	end

	if yType == "integer" or yType == "number" then

		yMin = tonumber(yMin)

		yMax = tonumber(yMax)

	end



	return y, yType, yMin, yMax

end



local function convertXYToManySeries(x, y, xType, yType, seriesTitles)

	local data =

	{

		name = "chart",

		format =

		{

			type = "json",

			parse = { x = xType, y = yType }

		},

		values = {}

	}

	for i = 1, #y do

		local yLen = table.maxn(yi])

		for j = 1, #x do

			if j <= yLen and yi][j then table.insert(data.values, { series = seriesTitlesi], x = xj], y = yi][j }) end

		end

	end

	return data

end



local function convertXYToSingleSeries(x, y, xType, yType, yNames)

	local data = { name = "chart", format = { type = "json", parse = { x = xType } }, values = {} }



	for j = 1, #y do data.format.parseyNamesj]] = yType end



	for i = 1, #x do

		local item = { x = xi }

		for j = 1, #y do itemyNamesj]] = yj][i end



		table.insert(data.values, item)

	end

	return data

end



local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)

	if chartType == "pie" then return end



	local xscale =

	{

		name = "x",

		range = "width",

		zero = false, -- do not include zero value

		domain = { data = "chart", field = "x" }

	}

	if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end

	if xMin then xscale.domainMin = xMin end

	if xMax then xscale.domainMax = xMax end

	if xMin or xMax then

		xscale.clamp = true

		xscale.nice = false

	end

	if chartType == "rect" then

		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"

			xscale.points = true

		end

	end

	if xType and xType ~= "date" and xScaleType ~= "log" then xscale.nice = true end -- force round numbers for x scale, but "log" and "date" scale outputs a wrong "nice" scale

	return xscale

end



local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)

	if chartType == "pie" then return end



	local yscale =

	{

		name = "y",

		 --type = yScaleType or "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 = chartType ~= "line",

		nice = yScaleType ~= "log" -- force round numbers for y scale, but log scale outputs a wrong "nice" scale

	}

	if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end

	if yMin then yscale.domainMin = yMin end

	if yMax then yscale.domainMax = yMax end

	if yMin or yMax then yscale.clamp = true end

	if yType == "date" then yscale.type = "time"

	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



	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 = 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 there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale

	if isTable(colors) then

		local alphas = {}

		local hasAlpha = false

		for i = 1, #colors do

			local a, rgb = string.match(colorsi], "#(%x%x)(%x%x%x%x%x%x)")

			if a then

				hasAlpha = true

				alphasi = tostring(tonumber(a, 16) / 255.0)

				colorsi = "#" .. rgb

			else

				alphasi = "1"

			end

		end

		for i = #colors + 1, #y do alphasi = "1" end

		if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end

	end

	return alphaScale

end



local function getLineScale(linewidths, chartType)

	local lineScale = {}

 

	lineScale =

    	{

        name = "line",

        type = "ordinal",

        range = linewidths,

        domain = { data = "chart", field = "series" }

    	}



	return lineScale

end



local function getSymSizeScale(symSize)

	local SymSizeScale = {}

	SymSizeScale =

       	{

        name = "symSize",

        type = "ordinal",

        range = symSize,

        domain = { data = "chart", field = "series" }

        }



	return SymSizeScale

end



local function getSymShapeScale(symShape)

	local SymShapeScale = {}

	SymShapeScale =

       	{

        name = "symShape",

        type = "ordinal",

        range = symShape,

        domain = { data = "chart", field = "series" }

        }



	return SymShapeScale

end



local function getValueScale(fieldName, min, max, type)

	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.entercolorField = { scale = "color", field = dataField }



	-- action when cursor is over plot mark: highlight

	if not plotMarks.properties.hover then plotMarks.properties.hover = {} end

	plotMarks.properties.hovercolorField = { value = "red" }



	-- action when cursor leaves plot mark: reset to initial setup

	if not plotMarks.properties.update then plotMarks.properties.update = {} end

	plotMarks.properties.updatecolorField = { 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



	addInteractionToChartVisualisation(chartvis, "fill", "x")



	return chartvis

end



local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate)

	if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end



	local chartvis =

	{

		type = chartType,

		properties =

		{

			-- chart creation event handler

			enter =

			{

				x = { scale = "x", field = "x" },

				y = { scale = "y", field = "y" }

			}

		}

	}

	addInteractionToChartVisualisation(chartvis, colorField, "series")

	if colorField == "stroke" then

		chartvis.properties.enter.strokeWidth = { value = linewidth or 2.5 }

		if type(lineScale) =="table"  then 

			chartvis.properties.enter.strokeWidth.value = nil

			chartvis.properties.enter.strokeWidth = 

			{

				scale = "line",

				field= "series"

			} 

		end

	end



	if interpolate then chartvis.properties.enter.interpolate = { value = interpolate } end



	if alphaScale then chartvis.properties.updatecolorField .. "Opacity" = { scale = "transparency" } end

	-- for bars and area charts set the lower bound of their areas

	if chartType == "rect" or chartType == "area" then

		if stacked then

			-- for stacked charts this lower bound is the end of the last stacking element

			chartvis.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 ]]

			chartvis.properties.enter.y2 = { scale = "y", value = 0 }

		end

	end

	-- for bar charts ...

	if chartType == "rect" then

		-- set 1 pixel width between the bars

		chartvis.properties.enter.width = { scale = "x", band = true, offset = -1 }

		-- for multiple series the bar marking needs to use the "inner" series scale, whereas the "outer" x scale is used by the grouping

		if not stacked and yCount > 1 then

			chartvis.properties.enter.x.scale = "series"

			chartvis.properties.enter.x.field = "series"

			chartvis.properties.enter.width.scale = "series"

		end

	end

	-- stacked charts have their own (stacked) y values

	if stacked then chartvis.properties.enter.y.field = "layout_start" end



	-- if there are multiple series group these together

	if yCount == 1 then

		chartvis.from = { data = "chart" }

	else

		-- if there are multiple series, connect colors to series

		chartvis.properties.updatecolorField].field = "series"

		if alphaScale then chartvis.properties.updatecolorField .. "Opacity"].field = "series" end

		

	    -- if there are multiple series, connect linewidths to series

		if chartype == "line" then

			chartvis.properties.update"strokeWidth"].field = "series"

		end



		

		-- apply a grouping (facetting) transformation

		chartvis =

		{

			type = "group",

			marks = { chartvis },

			from =

			{

				data = "chart",

				transform =

				{

					{

						type = "facet",

						groupby = { "series" }

					}

				}

			}

		}

		-- for stacked charts apply a stacking transformation

		if stacked then

			table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } )

		else

			-- for bar charts the series are side-by-side grouped by x

			if chartType == "rect" then

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

				local groupScale =

				{

					name = "series",

					type = "ordinal",

					range = "width",

					domain = { field = "series" }

				}



				chartvis.from.transform1].groupby = "x"

				chartvis.scales = { groupScale }

				chartvis.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }

			end

		end

	end



	return chartvis

end



local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues)

	local properties

	if chartType == "rect" then

		properties =

		{

			x = { scale = chartvis.properties.enter.x.scale, field = chartvis.properties.enter.x.field },

			y = { scale = chartvis.properties.enter.y.scale, field = chartvis.properties.enter.y.field, offset = -(tonumber(showValues.offset) or -4) },

			--dx = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text

			dy = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for vertical text

			align = { },

			baseline = { value = "middle" },

			fill = { },

			angle = { value = -90 },

			fontSize = { value = tonumber(showValues.fontsize) or 11 }

		}

		if properties.y.offset >= 0 then

			properties.align.value = "right"

			properties.fill.value = showValues.fontcolor or "white"

		else

			properties.align.value = "left"

			properties.fill.value = showValues.fontcolor or persistentGrey

		end

	elseif chartType == "pie" then

		properties =

		{

			x = { group = "width", mult = 0.5 },

			y = { group = "height", mult = 0.5 },

			radius = { offset = tonumber(showValues.offset) or -4 },

			theta = { field = "layout_mid" },

			fill = { value = showValues.fontcolor or persistentGrey },

			baseline = { },

			angle = { },

			fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) }

		}

		if (showValues.angle or "midangle") == "midangle" then

			properties.align = { value = "center" }

			properties.angle = { field = "layout_mid", mult = 180.0 / math.pi }



			if properties.radius.offset >= 0 then

				properties.baseline.value = "bottom"

			else

				if not showValues.fontcolor then properties.fill.value = "white" end

				properties.baseline.value = "top"

			end

		elseif tonumber(showValues.angle) then

			-- qunatize scale for aligning text left on right half-circle and right on left half-circle

			local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "left", "right" } }

			table.insert(scales, alignScale)



			properties.align = { scale = alignScale.name, field = "layout_mid" }

			properties.angle = { value = tonumber(showValues.angle) }

			properties.baseline.value = "middle"

			if not tonumber(showValues.offset) then properties.radius.offset = 4 end

		end



		if radiusScale then

			properties.radius.scale = radiusScale.name

			properties.radius.field = radiusScale.domain.field

		else

			properties.radius.value = outerRadius

		end

	end



	if properties then

		if showValues.format then

			local template = "datum.y"

			if yType == "integer" or yType == "number" then template = template .. "|number:'" .. showValues.format .. "'"

			elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'"

			end

			properties.text = { template = "{{" .. template .. "}}" }

		else

			properties.text = { field = "y" }

		end



		local textmarks =

		{

			type = "text",

			properties =

			{

				enter = properties

			}

		}

		if chartvis.from then textmarks.from = copy(chartvis.from) end



		return textmarks

	end

end



local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale)



	local symbolmarks 

	symbolmarks =

	{

		type = "symbol",

		properties =

		{

			enter = 

			{

				x = { scale = "x", field = "x" },

				y = { scale = "y", field = "y" },

				strokeWidth = { value = symStroke },

				stroke = { scale = "color", field = "series" },

				fill = { scale = "color", field = "series" },

			}

		}

	}

	if type(symShape) == "string" then 

		symbolmarks.properties.enter.shape = { value = symShape }

	end

	if type(symShape) == "table" then 

		symbolmarks.properties.enter.shape = { scale = "symShape", field = "series" }

	end

	if type(symSize) == "number" then 

		symbolmarks.properties.enter.size = { value = symSize }

	end

	if type(symSize) == "table" then 

		symbolmarks.properties.enter.size = { scale = "symSize", field = "series" }

	end

	if noFill then 

		symbolmarks.properties.enter.fill = nil

	end

	if alphaScale then 

		symbolmarks.properties.enter.fillOpacity = 

		{ scale = "transparency", field = "series" } 

		symbolmarks.properties.enter.strokeOpacity = 

		{ scale = "transparency", field = "series" } 

	end

	if chartvis.from then symbolmarks.from = copy(chartvis.from) end

    

	return symbolmarks

end



local function getAnnoMarks(chartvis, stroke, fill, opacity)



	local vannolines, hannolines, vannoLabels, vannoLabels 

	vannolines =

	{

		type = "rule",

		from = { data = "v_anno" },

		properties =

		{

			update = 

			{

				x = { scale = "x", field = "x" },

				y = { value = 0 },

				y2 = {  field = { group = "height" } },

				strokeWidth = { value = stroke },

				stroke = { value = persistentGrey },

				opacity = { value = opacity }

			}

		}

	}

	vannolabels =

	{

		type = "text",

		from = { data = "v_anno" },

		properties =

		{

			update = 

			{

				x = { scale = "x", field = "x", offset = 3 },

				y = {  field = { group = "height" }, offset = -3 },

				text = { field = "label" },

				baseline = { value = "top" },

		        angle = { value = -90 },

		        fill = { value = persistentGrey },

		        opacity = { value = opacity }

			}

		}

	}	

	hannolines =

	{

		type = "rule",

		from = { data = "h_anno" },

		properties =

		{

			update = 

			{

				y = { scale = "y", field = "y" },

				x = { value = 0 },

				x2 = {  field = { group = "width" } },

				strokeWidth = { value = stroke },

				stroke = { value = persistentGrey },

				opacity = { value = opacity }

			}

		}

	}

	hannolabels =

	{

		type = "text",

		from = { data = "h_anno" },

		properties =

		{

			update = 

			{

				y = { scale = "y", field = "y", offset = 3 },

				x = {  value = 0 , offset = 3 },

				text = { field = "label" },

				baseline = { value = "top" },

		        angle = { value = 0 },

		        fill = { value = persistentGrey },

		        opacity = { value = opacity }

			}

		}

	}

	return vannolines, vannolabels, hannolines, hannolabels

end



local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)

	local xAxis, yAxis

	if chartType ~= "pie" then

		if xType == "integer" and not xAxisFormat then xAxisFormat = "d" end

		xAxis =

		{

			type = "x",

			scale = "x",

			title = xTitle,

			format = xAxisFormat,

			grid = xGrid

		}

		if xAxisAngle then

			local xAxisAlign

			if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end

			xAxis.properties =

			{

				title =

				{

					fill = { value = persistentGrey }

				},

				labels =

				{

					angle = { value = xAxisAngle },

					align = { value = xAxisAlign },

					fill = { value = persistentGrey }

				},

				ticks =

				{

					stroke = { value = persistentGrey }

				},

				axis =

				{

					stroke = { value = persistentGrey },

					strokeWidth = { value = 2 }

				},

				grid =

				{

					stroke = { value = persistentGrey }

				}

			}

		else

			xAxis.properties =

			{

				title =

				{

					fill = { value = persistentGrey }

				},

				labels =

				{

					fill = { value = persistentGrey }

				},

				ticks =

				{

					stroke = { value = persistentGrey }

				},

				axis =

				{

					stroke = { value = persistentGrey },

					strokeWidth = { value = 2 }

				},

				grid =

				{

					stroke = { value = persistentGrey }

				}

			}

		end



		if yType == "integer" and not yAxisFormat then yAxisFormat = "d" end

		yAxis =

		{

			type = "y",

			scale = "y",

			title = yTitle,

			format = yAxisFormat,

			grid = yGrid

		}

		yAxis.properties =

		{

			title =

			{

				fill = { value = persistentGrey }

			},

			labels =

			{

				fill = { value = persistentGrey }

			},

			ticks =

			{

				stroke = { value = persistentGrey }

			},

			axis =

			{

				stroke = { value = persistentGrey },

				strokeWidth = { value = 2 }

			},

			grid =

			{

				stroke = { value = persistentGrey }

			}

		}

	

	end



	return xAxis, yAxis

end



local function getLegend(legendTitle, chartType, outerRadius)

	local legend =

	{

		fill = "color",

		stroke = "color",

		title = legendTitle,

	}

	legend.properties = {

		title = {

			fill = { value = persistentGrey },

		},

		labels = {

			fill = { value = persistentGrey },

		},

	}

	if chartType == "pie" then

		legend.properties = {

			-- move legend from center position to top

			legend = {

				y = { value = -outerRadius },

			},

			title = {

				fill = { value = persistentGrey }

			},

			labels = {

				fill = { value = persistentGrey },

			},

		}

	end

	return 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 colorString = frame.args.colors

	if colorString then colorString = string.lower(colorString) end

	local colors = stringArray(colorString)

	-- for line charts, the thickness of the line; for pie charts the gap between each slice

	local linewidth = tonumber(frame.args.linewidth)

	local linewidthsString = frame.args.linewidths

	local linewidths

	if linewidthsString and linewidthsString ~= "" then linewidths = numericArray(linewidthsString) or false end

	-- x and y axis caption

	local xTitle = frame.args.xAxisTitle  or frame.args.xaxistitle

	local yTitle = frame.args.yAxisTitle  or frame.args.yaxistitle

	-- x and y value types

	local xType = frame.args.xType or frame.args.xtype

	local yType = frame.args.yType or frame.args.ytype

	-- override x and y axis minimum and maximum

	local xMin = frame.args.xAxisMin or frame.args.xaxismin

	local xMax = frame.args.xAxisMax or frame.args.xaxismax

	local yMin = frame.args.yAxisMin or frame.args.yaxismin

	local yMax = frame.args.yAxisMax or frame.args.yaxismax

	-- override x and y axis label formatting

	local xAxisFormat = frame.args.xAxisFormat or frame.args.xaxisformat

	local yAxisFormat = frame.args.yAxisFormat or frame.args.yaxisformat

	local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle)

	-- x and y scale types

	local xScaleType = frame.args.xScaleType or frame.args.xscaletype 

	local yScaleType = frame.args.yScaleType or frame.args.yscaletype  

-- log scale require minimum > 0, for now it's no possible to plot negative values on log - TODO see: https://www.mathworks.com/matlabcentral/answers/1792-log-scale-graphic-with-negative-value

--	if xScaleType == "log" then

--		if (not xMin or tonumber(xMin) <= 0) then xMin = 0.1 end

--		if not xType then xType = "number" end

--	end

--	if yScaleType == "log" then

--		if (not yMin or tonumber(yMin) <= 0) then yMin = 0.1 end

--		if not yType then yType = "number" end

--	end



	-- show grid

	local xGrid = frame.args.xGrid or frame.args.xgrid or false

	local yGrid = frame.args.yGrid or frame.args.ygrid or false

	-- for line chart, show a symbol at each data point

	local showSymbols = frame.args.showSymbols or frame.args.showsymbols

	local symbolsShape = frame.args.symbolsShape or frame.args.symbolsshape

	local symbolsNoFill = frame.args.symbolsNoFill or frame.args.symbolsnofill 

	local symbolsStroke = tonumber(frame.args.symbolsStroke or frame.args.symbolsstroke)

	-- show legend with given title

	local legendTitle = frame.args.legend

	-- show values as text

	local showValues = frame.args.showValues or frame.args.showvalues 

	-- show v- and h-line annotations

	local v_annoLineString = frame.args.vAnnotatonsLine or frame.args.vannotatonsline

	local h_annoLineString = frame.args.hAnnotatonsLine or frame.args.hannotatonsline

	local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel

	local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel











	-- decode annotations cvs

	local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel

	if v_annoLineString and v_annoLineString ~= "" then



		if xType == "number" or xType == "integer" then 

		v_annoLine = numericArray(v_annoLineString)



		else 

			v_annoLine = stringArray(v_annoLineString)



		end

		v_annoLabel = stringArray(v_annoLabelString)

	end

	if h_annoLineString and h_annoLineString ~= "" then



		if yType == "number" or yType == "integer" then 

			h_annoLine = numericArray(h_annoLineString)



		else 

			h_annoLine = stringArray(h_annoLineString)



		end

		h_annoLabel = stringArray(h_annoLabelString)

	end











	-- pie chart radiuses

	local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0

	local outerRadius = math.min(graphwidth, graphheight)

	-- format JSON output

	local formatJson = frame.args.formatjson



	-- get x values

	local x

	x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax)



	-- get y values (series)

	local yValues = {}

	local seriesTitles = {}

	for name, value in pairs(frame.args) do

		local yNum

		if name == "y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end

		if yNum then

			yValuesyNum = value

			-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.

			seriesTitlesyNum = frame.args"y" .. yNum .. "Title" or frame.args"y" .. yNum .. "title" or name

		end

	end

	local y

	y, yType, yMin, yMax = deserializeYData(yValues, yType, yMin, yMax)



	-- create data tuples, consisting of series index, x value, y value

	local data

	if chartType == "pie" then

		-- for pie charts the second second series is merged into the first series as radius values

		data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" })

	else

		data = convertXYToManySeries(x, y, xType, yType, seriesTitles)

	end



	-- configure stacked charts

	local stacked = false

	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 =

		{

			name = "stats", source = "chart", transform =

		{

			{

				type = "aggregate",

				groupby = { "x" },

				summarize = { y = "sum" }

			}

		}

		}

		end

	end

	

	-- add annotations to data

	local vannoData, hannoData

	

	if v_annoLine then

		vannoData = { name = "v_anno", format = { type = "json", parse = { x = xType } }, values = {} }

		for i = 1, #v_annoLine do

			local item = { x = v_annoLinei], label = v_annoLabeli }

			table.insert(vannoData.values, item)

		end

	end	

	if h_annoLine then

		hannoData = { name = "h_anno", format = { type = "json", parse = { y = yType } }, values = {} }

		for i = 1, #h_annoLine do

			local item = { y = h_annoLinei], label = h_annoLabeli }

			table.insert(hannoData.values, item)

		end

	end	





	-- create scales

	local scales = {}



	local xscale = getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)

	table.insert(scales, xscale)

	local yscale = getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)

	table.insert(scales, yscale)



	local colorScale = getColorScale(colors, chartType, #x, #y)

	table.insert(scales, colorScale)



	local alphaScale = getAlphaColorScale(colors, y)

	table.insert(scales, alphaScale)



	local lineScale

	if (linewidths) and (chartType == "line") then

		lineScale = getLineScale(linewidths, chartType)

		table.insert(scales, lineScale)

	end



	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, lineScale, interpolate)

	local marks = { chartvis }

	

	-- text marks

	if showValues then

		if type(showValues) == "string" then -- deserialize as table

			local keyValues = mw.text.split(showValues, "%s*,%s*")

			showValues = {}

			for _, kv in ipairs(keyValues) do

				local key, value = mw.ustring.match(kv, "^%s*(.-)%s*:%s*(.-)%s*$")

				if key then showValueskey = value end

			end

		end



		local chartmarks = chartvis

		if chartmarks.marks then chartmarks = chartmarks.marks1 end

		local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues)

		if chartmarks ~= chartvis then

			table.insert(chartvis.marks, textmarks)

		else

			table.insert(marks, textmarks)

		end

	end

	

    -- grids

    if xGrid then 

    	if xGrid == "0" then xGrid = false

    	elseif xGrid == 0 then xGrid = false 

    	elseif xGrid == "false" then xGrid = false 

    	elseif xGrid == "n" then xGrid = false 

    	else xGrid = true 

    	end

    end

    if yGrid then 

    	if yGrid == "0" then yGrid = false

    	elseif yGrid == 0 then yGrid = false 

    	elseif yGrid == "false" then yGrid = false 

    	elseif yGrid == "n" then yGrid = false 

    	else yGrid = true 

    	end

    end

    

	-- symbol marks

	if showSymbols and chartType ~= "rect" then

		local chartmarks = chartvis

		if chartmarks.marks then chartmarks = chartmarks.marks1 end



		if type(showSymbols) == "string" then

			if showSymbols == "" then showSymbols = true

			else showSymbols = numericArray(showSymbols)

			end

		else

			showSymbols = tonumber(showSymbols)

		end



		-- custom size

		local symSize

		if type(showSymbols) == "number" then 

			symSize = tonumber(showSymbols*showSymbols*8.5)

	 	elseif type(showSymbols) == "table" then 

	 		symSize = {}

	 		for k, v in pairs(showSymbols) do

                symSizek=v*v*8.5 -- "size" acc to Vega syntax is area of symbol

            end

        else

	 		symSize = 50

	 	end

		-- symSizeScale 

	 	local symSizeScale = {}

		if type(symSize) == "table" then

			symSizeScale = getSymSizeScale(symSize)

			table.insert(scales, symSizeScale)

		end

 	



    	-- custom shape

    	if  stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end

    	

    	local symShape = " "

		

		if type(symbolsShape) == "string" and shapessymbolsShape then

			symShape = shapessymbolsShape

	 	elseif type(symbolsShape) == "table" then 

	 		symShape = {}

	 		for k, v in pairs(symbolsShape) do

                if symbolsShapek and shapessymbolsShapek]] then 

                	symShapek=shapessymbolsShapek]]

                else

                	symShapek = "circle"

                end

            end

       	else

			symShape = "circle"

		end

		-- symShapeScale 

	 	local symShapeScale = {}

		if type(symShape) == "table" then

			symShapeScale = getSymShapeScale(symShape)

			table.insert(scales, symShapeScale)

		end 

 

		-- custom stroke

		local symStroke

		if (type(symbolsStroke) == "number") then 

			symStroke = tonumber(symbolsStroke)

-- TODO symStroke serialization

--		elseif type(symbolsStroke) == "table" then 

--	 		symStroke = {}

--	 		for k, v in pairs(symbolsStroke) do

--                symStroke[k]=symbolsStroke[k]

--                		--always draw x with stroke

--				if symbolsShape[k] == "x" then symStroke[k] = 2.5 end

				--always draw x with stroke

--				if symbolsNoFill[k] then symStroke[k] = 2.5 end

--            end

		else 

			symStroke = 0

		--always draw x with stroke

			if symbolsShape == "x" then symStroke = 2.5 end

		--always draw x with stroke

			if symbolsNoFill then symStroke = 2.5 end

	 	end 		





--	TODO	-- symStrokeScale 

--	 	local symStrokeScale = {}

--		if type(symStroke) == "table" then

--			symStrokeScale = getSymStrokeScale(symStroke)

--			table.insert(scales, symStrokeScale)

--		end





		

		local symbolmarks = getSymbolMarks(chartmarks, symSize, symShape, symStroke, symbolsNoFill, alphaScale)

		if chartmarks ~= chartvis then

			table.insert(chartvis.marks, symbolmarks)

		else

			table.insert(marks, symbolmarks)

		end

	end





	local vannolines, vannolabels, hannolines, hannolabels = getAnnoMarks(chartmarks, persistentGrey, persistentGrey, 0.75)

	if vannoData then

		table.insert(marks, vannolines)

		table.insert(marks, vannolabels)

	end

	if hannoData then

		table.insert(marks, hannolines)

		table.insert(marks, hannolabels)

	end



	-- axes

	local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)

	

	-- legend

	local legend

	if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end

	-- construct final output object

	local output =

	{

		version = 2,

		width = graphwidth,

		height = graphheight,

		data = { data },

		scales = scales,

		axes = { xAxis, yAxis },

		marks = marks,

		legends = { legend }

	}

	if vannoData then table.insert(output.data, vannoData) end

	if hannoData then table.insert(output.data, hannoData) end

	if stats then table.insert(output.data, stats) end



	local flags

	if formatJson then flags = mw.text.JSON_PRETTY end

	return mw.text.jsonEncode(output, flags)

end



function p.mapWrapper(frame)

	return p.map(frame:getParent())

end



function p.chartWrapper(frame)

	return p.chart(frame:getParent())

end



function p.chartDebuger(frame)

	return   "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog 

end





-- Given an HTML-encoded title as first argument, e.g. one produced with {{ARTICLEPAGENAME}},

-- convert it into a properly URL path-encoded string

-- This function is critical for any graph that uses path-based APIs, e.g. PageViews graph

function p.encodeTitleForPath(frame)

	return mw.uri.encode(mw.text.decode(mw.text.trim(frame.args1])), 'PATH')

end



return p

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook