-- 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 =) https://en.wikipedia.org/wiki/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 = "মডিউল:গ্রাফ/" 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 list[i] == "" then 			result[i] = nil 		else 			result[i] = tonumber(list[i]) 			if not result[i] then return end 			if isInteger then 				local int, frac = math.modf(result[i]) 				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 		list[i] = mw.ustring.gsub(mw.text.trim(list[i]), "<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 result[key] = 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 scales[1].domainMin = domainMin end 		if domainMax then scales[1].domainMax = domainMax end  		local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent 		if exponent then 			scales[1].type = "pow" 			scales[1].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.marks[1].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  		y[yNum] = 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(y[i]) 		for j = 1, #x do 			if j <= yLen and y[i][j] then table.insert(data.values, { series = seriesTitles[i], x = x[j], y = y[i][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.parse[yNames[j]] = yType end  	for i = 1, #x do 		local item = { x = x[i] } 		for j = 1, #y do item[yNames[j]] = y[j][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(colors[i], "#(%x%x)(%x%x%x%x%x%x)") 			if a then 				hasAlpha = true 				alphas[i] = tostring(tonumber(a, 16) / 255.0) 				colors[i] = "#" .. rgb 			else 				alphas[i] = "1" 			end 		end 		for i = #colors + 1, #y do alphas[i] = "1" end 		if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end 	end 	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.enter[colorField] = { scale = "color", field = dataField }  	-- action when cursor is over plot mark: highlight 	if not plotMarks.properties.hover then plotMarks.properties.hover = {} end 	plotMarks.properties.hover[colorField] = { value = "red" }  	-- action when cursor leaves plot mark: reset to initial setup 	if not plotMarks.properties.update then plotMarks.properties.update = {} end 	plotMarks.properties.update[colorField] = { scale = "color", field = dataField } end  local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) 	local chartvis = 	{ 		type = "arc", 		from = { data = "chart", transform = { { field = "y", type = "pie" } } },  		properties = 		{ 			enter = { 				innerRadius = { value = innerRadius }, 				outerRadius = { }, 				startAngle = { field = "layout_start" }, 				endAngle = { field = "layout_end" }, 				stroke = { value = "white" }, 				strokeWidth = { value = linewidth or 1 } 			} 		} 	}  	if radiusScale then 		chartvis.properties.enter.outerRadius.scale = radiusScale.name 		chartvis.properties.enter.outerRadius.field = radiusScale.domain.field 	else 		chartvis.properties.enter.outerRadius.value = outerRadius 	end  	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.update[colorField .. "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.update[colorField].field = "series" 		if alphaScale then chartvis.properties.update[colorField .. "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.transform[1].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 = "" } 		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 			yValues[yNum] = value 			-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters. 			seriesTitles[yNum] = 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_annoLine[i], label = v_annoLabel[i] } 			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_annoLine[i], label = h_annoLabel[i] } 			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 showValues[key] = value end 			end 		end  		local chartmarks = chartvis 		if chartmarks.marks then chartmarks = chartmarks.marks[1] 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.marks[1] 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                 symSize[k]=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 shapes[symbolsShape] then 			symShape = shapes[symbolsShape] 	 	elseif type(symbolsShape) == "table" then  	 		symShape = {} 	 		for k, v in pairs(symbolsShape) do                 if symbolsShape[k] and shapes[symbolsShape[k]] then                  	symShape[k]=shapes[symbolsShape[k]]                 else                 	symShape[k] = "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 , -- 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.args[1])), 'PATH') end  return p