Module:Graph

 -- 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 - &quot;clamp&quot; used to avoid marks outside marks area, &quot;clip&quot; 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 &quot;nice&quot; for x axis (problem with scale when xType = &quot;date&quot;) and grid
 --   2020-06-21 Serializes symbol size
 --              transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line)
 --              Linewidth serialized with &quot;linewidths&quot;
 --              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 &quot;nice&quot;=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 .. &quot;&quot; .. &quot;\n\n&quot;  .. &quot;- &quot; .. debug.traceback() .. &quot;result type: &quot;.. type(result) ..  &quot; result: \n\n&quot; .. mw.dumpObject(result) 
 --invoke chartDebuger() to get graph JSON and this string
 debuglog = &quot;Debug &quot; .. &quot;\n\n&quot; 
 
 local baseMapDirectory = &quot;Module:Graph/&quot;
 local persistentGrey = &quot;#54595d&quot;
 
 local shapes = {}
 shapes = { 
 	circle = &quot;circle&quot;, x= &quot;M-.5,-.5L.5,.5M.5,-.5L-.5,.5&quot; , square = &quot;square&quot;, 
 	cross = &quot;cross&quot;, diamond = &quot;diamond&quot;, triangle_up = &quot;triangle-up&quot;, 
 	triangle_down = &quot;triangle-down&quot;, triangle_right = &quot;triangle-right&quot;, 
 	triangle_left = &quot;triangle-left&quot;, 
 	banana = &quot;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&quot; 
 	   	}
 
 
 local function numericArray(csv)
 	if not csv then return end
 
 	local list = mw.text.split(csv, &quot;%s*,%s*&quot;)
 	local result = {}
 	local isInteger = true
 	for i = 1, #list do
 		if list[i] == &quot;&quot; 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), &quot;\\,&quot;, &quot;&lt;COMMA&gt;&quot;), &quot;,&quot;, true)
 	for i = 1, #list do
 		list[i] = mw.ustring.gsub(mw.text.trim(list[i]), &quot;&lt;COMMA&gt;&quot;, &quot;,&quot;)
 	end
 	return list
 end
 
 local function isTable(t) return type(t) == &quot;table&quot; end
 
 local function copy(x)
 	if type(x) == &quot;table&quot; 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 &quot;Template:Graph:Map/Inner/Worldmap2c-json&quot; -- 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 &quot;equirectangular&quot;
 	-- 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 &quot;linear&quot;
 	-- 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 &quot;category10&quot;
 	-- show legend
 	local legend = frame.args.legend
 	-- the map feature to display
     local feature = frame.args.feature or &quot;countries&quot;
     -- 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 &quot;id&quot; values of the map path data
 	local values = {}
 	local isNumbers = nil
 	for name, value in pairs(frame.args) do
 		if mw.ustring.find(name, &quot;^[^%l]+$&quot;) and value and value ~= &quot;&quot; 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 = &quot;silver&quot; end
 	end
 
 	-- create highlight scale
 	local scales
 	if isNumbers then
 		if colorScale then colorScale = string.lower(colorScale) end
 		if colorScale == &quot;category10&quot; or colorScale == &quot;category20&quot; then else colorScale = stringArray(colorScale) end
 		scales =
 		{
 			{
 				name = &quot;color&quot;,
 				type = scaleType,
 				domain = { data = &quot;highlights&quot;, field = &quot;v&quot; },
 				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, &quot;pow%s+(%d+%.?%d+)&quot;) -- check for exponent
 		if exponent then
 			scales[1].type = &quot;pow&quot;
 			scales[1].exponent = exponent
 		end
 	end
 
 	-- create legend
 	if legend then
 		legend =
 		{
 			{
 				fill = &quot;color&quot;,
 				offset = 120,
 				properties =
 				{
 					title = { fontSize = { value = 14 } },
 					labels = { fontSize = { value = 12 } },
 					legend =
 					{
 						stroke = { value = &quot;silver&quot; },
 						strokeWidth = { value = 1.5 }
 					}
 				}
 			}
 		}
 	end
  
 	-- get map url
 	local basemapUrl
 	if (string.sub(basemap, 1, 10) == &quot;wikiraw://&quot;) 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, &quot;:&quot;) then basemap = baseMapDirectory .. basemap end
 		basemapUrl = &quot;wikiraw:///&quot; .. mw.uri.encode(mw.title.new(basemap).prefixedText, &quot;PATH&quot;)
 	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 = &quot;highlights&quot;,
 				values = values
 			},
 			{
 				-- data source for map paths data
 				name = feature,
 				url = basemapUrl,
 				format = { type = &quot;topojson&quot;, feature = feature },
 				transform =
 				{
 					{
 						-- geographic transformation (&quot;geopath&quot;) of map paths data
 						type = &quot;geopath&quot;,
 						value = &quot;data&quot;,			-- data source
 						scale = scale,
                         translate = { 0, 0 },
                         center = center,
 						projection = projection
 					},
 					{
 						-- join (&quot;zip&quot;) of mutiple data source: here map paths data and highlights
 						type = &quot;lookup&quot;,
 						keys = { &quot;id&quot; },      -- key for map paths data
 						on = &quot;highlights&quot;,    -- name of highlight data source
 						onKey = &quot;id&quot;,         -- key for highlight data source
 						as = { &quot;zipped&quot; },    -- 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 = &quot;path&quot;,
 				from = { data = feature },
 				properties =
 				{
 					enter = { path = { field = &quot;layout_path&quot; } },
 					update = { fill = { field = &quot;zipped.v&quot; } },
 					hover = { fill = { value = &quot;darkgrey&quot; } }
 				}
 			}
 		},
 		legends = legend
 	}
 	if (scales) then
 		output.scales = scales
 		output.marks[1].properties.update.fill.scale = &quot;color&quot;
 	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 == &quot;integer&quot; or xType == &quot;number&quot; then
 		local isInteger
 		x, isInteger = numericArray(serializedX)
 		if x then
 			xMin = tonumber(xMin)
 			xMax = tonumber(xMax)
 			if not xType then
 				if isInteger then xType = &quot;integer&quot; else xType = &quot;number&quot; end
 			end
 		else
 			if xType then error(&quot;Numbers expected for parameter 'x'&quot;) end
 		end
 	end
 	if not x then
 		x = stringArray(serializedX)
 		if not xType then xType = &quot;string&quot; 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 == &quot;integer&quot; or yType == &quot;number&quot; then
 			local isInteger
 			yValues, isInteger = numericArray(value)
 			if yValues then
 				areAllInteger = areAllInteger and isInteger
 			else
 				if yType then
 					error(&quot;Numbers expected for parameter '&quot; .. name .. &quot;'&quot;)
 				else
 					return deserializeYData(serializedYs, &quot;string&quot;, 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 = &quot;integer&quot; else yType = &quot;number&quot; end
 	end
 	if yType == &quot;integer&quot; or yType == &quot;number&quot; 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 = &quot;chart&quot;,
 		format =
 		{
 			type = &quot;json&quot;,
 			parse = { x = xType, y = yType }
 		},
 		values = {}
 	}
 	for i = 1, #y do
 		local yLen = table.maxn(y[i])
 		for j = 1, #x do
 			if j &lt;= 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 = &quot;chart&quot;, format = { type = &quot;json&quot;, 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 == &quot;pie&quot; then return end
 
 	local xscale =
 	{
 		name = &quot;x&quot;,
 		range = &quot;width&quot;,
 		zero = false, -- do not include zero value
 		domain = { data = &quot;chart&quot;, field = &quot;x&quot; }
 	}
 	if xScaleType then xscale.type = xScaleType else xscale.type = &quot;linear&quot; 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 == &quot;rect&quot; then
 		xscale.type = &quot;ordinal&quot;
 		if not stacked then xscale.padding = 0.2 end -- pad each bar group
 	else 
 		if xType == &quot;date&quot; then 
 			xscale.type = &quot;time&quot;
 		elseif xType == &quot;string&quot; then
 			xscale.type = &quot;ordinal&quot;
 			xscale.points = true
 		end
 	end
 	if xType and xType ~= &quot;date&quot; and xScaleType ~= &quot;log&quot; then xscale.nice = true end -- force round numbers for x scale, but &quot;log&quot; and &quot;date&quot; scale outputs a wrong &quot;nice&quot; scale
 	return xscale
 end
 
 local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)
 	if chartType == &quot;pie&quot; then return end
 
 	local yscale =
 	{
 		name = &quot;y&quot;,
 		 --type = yScaleType or &quot;linear&quot;,
 		range = &quot;height&quot;,
 		-- 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 ~= &quot;line&quot;,
 		nice = yScaleType ~= &quot;log&quot; -- force round numbers for y scale, but log scale outputs a wrong &quot;nice&quot; scale
 	}
 	if yScaleType then yscale.type = yScaleType else yscale.type = &quot;linear&quot; 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 == &quot;date&quot; then yscale.type = &quot;time&quot;
 	elseif yType == &quot;string&quot; then yscale.type = &quot;ordinal&quot; end
 	if stacked then
 		yscale.domain = { data = &quot;stats&quot;, field = &quot;sum_y&quot; }
 	else
 		yscale.domain = { data = &quot;chart&quot;, field = &quot;y&quot; }
 	end
 
 	return yscale
 end
 
 local function getColorScale(colors, chartType, xCount, yCount)
 	if not colors then
 		if (chartType == &quot;pie&quot; and xCount &gt; 10) or yCount &gt; 10 then colors = &quot;category20&quot; else colors = &quot;category10&quot; end
 	end
 
 	local colorScale =
 	{
 		name = &quot;color&quot;,
 		type = &quot;ordinal&quot;,
 		range = colors,
 		domain = { data = &quot;chart&quot;, field = &quot;series&quot; }
 	}
 	if chartType == &quot;pie&quot; then colorScale.domain.field = &quot;x&quot; end
 	return colorScale
 end
 
 local function getAlphaColorScale(colors, y)
 	local alphaScale
 	-- if there is at least one color in the format &quot;#aarrggbb&quot;, 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], &quot;#(%x%x)(%x%x%x%x%x%x)&quot;)
 			if a then
 				hasAlpha = true
 				alphas[i] = tostring(tonumber(a, 16) / 255.0)
 				colors[i] = &quot;#&quot; .. rgb
 			else
 				alphas[i] = &quot;1&quot;
 			end
 		end
 		for i = #colors + 1, #y do alphas[i] = &quot;1&quot; end
 		if hasAlpha then alphaScale = { name = &quot;transparency&quot;, type = &quot;ordinal&quot;, range = alphas } end
 	end
 	return alphaScale
 end
 
 local function getLineScale(linewidths, chartType)
 	local lineScale = {}
  
 	lineScale =
     	{
         name = &quot;line&quot;,
         type = &quot;ordinal&quot;,
         range = linewidths,
         domain = { data = &quot;chart&quot;, field = &quot;series&quot; }
     	}
 
 	return lineScale
 end
 
 local function getSymSizeScale(symSize)
 	local SymSizeScale = {}
 	SymSizeScale =
        	{
         name = &quot;symSize&quot;,
         type = &quot;ordinal&quot;,
         range = symSize,
         domain = { data = &quot;chart&quot;, field = &quot;series&quot; }
         }
 
 	return SymSizeScale
 end
 
 local function getSymShapeScale(symShape)
 	local SymShapeScale = {}
 	SymShapeScale =
        	{
         name = &quot;symShape&quot;,
         type = &quot;ordinal&quot;,
         range = symShape,
         domain = { data = &quot;chart&quot;, field = &quot;series&quot; }
         }
 
 	return SymShapeScale
 end
 
 local function getValueScale(fieldName, min, max, type)
 	local valueScale =
 	{
 		name = fieldName,
 		type = type or &quot;linear&quot;,
 		domain = { data = &quot;chart&quot;, 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 = &quot;color&quot;, 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 = &quot;red&quot; }
 
 	-- 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 = &quot;color&quot;, field = dataField }
 end
 
 local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale)
 	local chartvis =
 	{
 		type = &quot;arc&quot;,
 		from = { data = &quot;chart&quot;, transform = { { field = &quot;y&quot;, type = &quot;pie&quot; } } },
 
 		properties =
 		{
 			enter = {
 				innerRadius = { value = innerRadius },
 				outerRadius = { },
 				startAngle = { field = &quot;layout_start&quot; },
 				endAngle = { field = &quot;layout_end&quot; },
 				stroke = { value = &quot;white&quot; },
 				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, &quot;fill&quot;, &quot;x&quot;)
 
 	return chartvis
 end
 
 local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate)
 	if chartType == &quot;pie&quot; then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end
 
 	local chartvis =
 	{
 		type = chartType,
 		properties =
 		{
 			-- chart creation event handler
 			enter =
 			{
 				x = { scale = &quot;x&quot;, field = &quot;x&quot; },
 				y = { scale = &quot;y&quot;, field = &quot;y&quot; }
 			}
 		}
 	}
 	addInteractionToChartVisualisation(chartvis, colorField, &quot;series&quot;)
 	if colorField == &quot;stroke&quot; then
 		chartvis.properties.enter.strokeWidth = { value = linewidth or 2.5 }
 		if type(lineScale) ==&quot;table&quot;  then 
 			chartvis.properties.enter.strokeWidth.value = nil
 			chartvis.properties.enter.strokeWidth = 
 			{
 				scale = &quot;line&quot;,
 				field= &quot;series&quot;
 			} 
 		end
 	end
 
 	if interpolate then chartvis.properties.enter.interpolate = { value = interpolate } end
 
 	if alphaScale then chartvis.properties.update[colorField .. &quot;Opacity&quot;] = { scale = &quot;transparency&quot; } end
 	-- for bars and area charts set the lower bound of their areas
 	if chartType == &quot;rect&quot; or chartType == &quot;area&quot; then
 		if stacked then
 			-- for stacked charts this lower bound is the end of the last stacking element
 			chartvis.properties.enter.y2 = { scale = &quot;y&quot;, field = &quot;layout_end&quot; }
 		else
 			--[[
 			for non-stacking charts the lower bound is y=0
 			TODO: &quot;yscale.zero&quot; is currently set to &quot;true&quot; for this case, but &quot;false&quot; for all other cases.
 			For the similar behavior &quot;y2&quot; 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 = &quot;y&quot;, value = 0 }
 		end
 	end
 	-- for bar charts ...
 	if chartType == &quot;rect&quot; then
 		-- set 1 pixel width between the bars
 		chartvis.properties.enter.width = { scale = &quot;x&quot;, band = true, offset = -1 }
 		-- for multiple series the bar marking needs to use the &quot;inner&quot; series scale, whereas the &quot;outer&quot; x scale is used by the grouping
 		if not stacked and yCount &gt; 1 then
 			chartvis.properties.enter.x.scale = &quot;series&quot;
 			chartvis.properties.enter.x.field = &quot;series&quot;
 			chartvis.properties.enter.width.scale = &quot;series&quot;
 		end
 	end
 	-- stacked charts have their own (stacked) y values
 	if stacked then chartvis.properties.enter.y.field = &quot;layout_start&quot; end
 
 	-- if there are multiple series group these together
 	if yCount == 1 then
 		chartvis.from = { data = &quot;chart&quot; }
 	else
 		-- if there are multiple series, connect colors to series
 		chartvis.properties.update[colorField].field = &quot;series&quot;
 		if alphaScale then chartvis.properties.update[colorField .. &quot;Opacity&quot;].field = &quot;series&quot; end
 		
 	    -- if there are multiple series, connect linewidths to series
 		if chartype == &quot;line&quot; then
 			chartvis.properties.update[&quot;strokeWidth&quot;].field = &quot;series&quot;
 		end
 
 		
 		-- apply a grouping (facetting) transformation
 		chartvis =
 		{
 			type = &quot;group&quot;,
 			marks = { chartvis },
 			from =
 			{
 				data = &quot;chart&quot;,
 				transform =
 				{
 					{
 						type = &quot;facet&quot;,
 						groupby = { &quot;series&quot; }
 					}
 				}
 			}
 		}
 		-- for stacked charts apply a stacking transformation
 		if stacked then
 			table.insert(chartvis.from.transform, 1, { type = &quot;stack&quot;, groupby = { &quot;x&quot; }, sortby = { &quot;-_id&quot; }, field = &quot;y&quot; } )
 		else
 			-- for bar charts the series are side-by-side grouped by x
 			if chartType == &quot;rect&quot; 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 = &quot;series&quot;,
 					type = &quot;ordinal&quot;,
 					range = &quot;width&quot;,
 					domain = { field = &quot;series&quot; }
 				}
 
 				chartvis.from.transform[1].groupby = &quot;x&quot;
 				chartvis.scales = { groupScale }
 				chartvis.properties = { enter = { x = { field = &quot;key&quot;, scale = &quot;x&quot; }, width = { scale = &quot;x&quot;, band = true } } }
 			end
 		end
 	end
 
 	return chartvis
 end
 
 local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues)
 	local properties
 	if chartType == &quot;rect&quot; 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 = &quot;middle&quot; },
 			fill = { },
 			angle = { value = -90 },
 			fontSize = { value = tonumber(showValues.fontsize) or 11 }
 		}
 		if properties.y.offset &gt;= 0 then
 			properties.align.value = &quot;right&quot;
 			properties.fill.value = showValues.fontcolor or &quot;white&quot;
 		else
 			properties.align.value = &quot;left&quot;
 			properties.fill.value = showValues.fontcolor or persistentGrey
 		end
 	elseif chartType == &quot;pie&quot; then
 		properties =
 		{
 			x = { group = &quot;width&quot;, mult = 0.5 },
 			y = { group = &quot;height&quot;, mult = 0.5 },
 			radius = { offset = tonumber(showValues.offset) or -4 },
 			theta = { field = &quot;layout_mid&quot; },
 			fill = { value = showValues.fontcolor or persistentGrey },
 			baseline = { },
 			angle = { },
 			fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) }
 		}
 		if (showValues.angle or &quot;midangle&quot;) == &quot;midangle&quot; then
 			properties.align = { value = &quot;center&quot; }
 			properties.angle = { field = &quot;layout_mid&quot;, mult = 180.0 / math.pi }
 
 			if properties.radius.offset &gt;= 0 then
 				properties.baseline.value = &quot;bottom&quot;
 			else
 				if not showValues.fontcolor then properties.fill.value = &quot;white&quot; end
 				properties.baseline.value = &quot;top&quot;
 			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 = &quot;align&quot;, type = &quot;quantize&quot;, domainMin = 0.0, domainMax = math.pi * 2, range = { &quot;left&quot;, &quot;right&quot; } }
 			table.insert(scales, alignScale)
 
 			properties.align = { scale = alignScale.name, field = &quot;layout_mid&quot; }
 			properties.angle = { value = tonumber(showValues.angle) }
 			properties.baseline.value = &quot;middle&quot;
 			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 = &quot;datum.y&quot;
 			if yType == &quot;integer&quot; or yType == &quot;number&quot; then template = template .. &quot;|number:'&quot; .. showValues.format .. &quot;'&quot;
 			elseif yType == &quot;date&quot; then template = template .. &quot;|time:&quot; .. showValues.format .. &quot;'&quot;
 			end
 			properties.text = { template = &quot;{{&quot; .. template .. &quot;}}&quot; }
 		else
 			properties.text = { field = &quot;y&quot; }
 		end
 
 		local textmarks =
 		{
 			type = &quot;text&quot;,
 			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 = &quot;symbol&quot;,
 		properties =
 		{
 			enter = 
 			{
 				x = { scale = &quot;x&quot;, field = &quot;x&quot; },
 				y = { scale = &quot;y&quot;, field = &quot;y&quot; },
 				strokeWidth = { value = symStroke },
 				stroke = { scale = &quot;color&quot;, field = &quot;series&quot; },
 				fill = { scale = &quot;color&quot;, field = &quot;series&quot; },
 			}
 		}
 	}
 	if type(symShape) == &quot;string&quot; then 
 		symbolmarks.properties.enter.shape = { value = symShape }
 	end
 	if type(symShape) == &quot;table&quot; then 
 		symbolmarks.properties.enter.shape = { scale = &quot;symShape&quot;, field = &quot;series&quot; }
 	end
 	if type(symSize) == &quot;number&quot; then 
 		symbolmarks.properties.enter.size = { value = symSize }
 	end
 	if type(symSize) == &quot;table&quot; then 
 		symbolmarks.properties.enter.size = { scale = &quot;symSize&quot;, field = &quot;series&quot; }
 	end
 	if noFill then 
 		symbolmarks.properties.enter.fill = nil
 	end
 	if alphaScale then 
 		symbolmarks.properties.enter.fillOpacity = 
 		{ scale = &quot;transparency&quot;, field = &quot;series&quot; } 
 		symbolmarks.properties.enter.strokeOpacity = 
 		{ scale = &quot;transparency&quot;, field = &quot;series&quot; } 
 	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 = &quot;rule&quot;,
 		from = { data = &quot;v_anno&quot; },
 		properties =
 		{
 			update = 
 			{
 				x = { scale = &quot;x&quot;, field = &quot;x&quot; },
 				y = { value = 0 },
 				y2 = {  field = { group = &quot;height&quot; } },
 				strokeWidth = { value = stroke },
 				stroke = { value = persistentGrey },
 				opacity = { value = opacity }
 			}
 		}
 	}
 	vannolabels =
 	{
 		type = &quot;text&quot;,
 		from = { data = &quot;v_anno&quot; },
 		properties =
 		{
 			update = 
 			{
 				x = { scale = &quot;x&quot;, field = &quot;x&quot;, offset = 3 },
 				y = {  field = { group = &quot;height&quot; }, offset = -3 },
 				text = { field = &quot;label&quot; },
 				baseline = { value = &quot;top&quot; },
 		        angle = { value = -90 },
 		        fill = { value = persistentGrey },
 		        opacity = { value = opacity }
 			}
 		}
 	}	
 	hannolines =
 	{
 		type = &quot;rule&quot;,
 		from = { data = &quot;h_anno&quot; },
 		properties =
 		{
 			update = 
 			{
 				y = { scale = &quot;y&quot;, field = &quot;y&quot; },
 				x = { value = 0 },
 				x2 = {  field = { group = &quot;width&quot; } },
 				strokeWidth = { value = stroke },
 				stroke = { value = persistentGrey },
 				opacity = { value = opacity }
 			}
 		}
 	}
 	hannolabels =
 	{
 		type = &quot;text&quot;,
 		from = { data = &quot;h_anno&quot; },
 		properties =
 		{
 			update = 
 			{
 				y = { scale = &quot;y&quot;, field = &quot;y&quot;, offset = 3 },
 				x = {  value = 0 , offset = 3 },
 				text = { field = &quot;label&quot; },
 				baseline = { value = &quot;top&quot; },
 		        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 ~= &quot;pie&quot; then
 		if xType == &quot;integer&quot; and not xAxisFormat then xAxisFormat = &quot;d&quot; end
 		xAxis =
 		{
 			type = &quot;x&quot;,
 			scale = &quot;x&quot;,
 			title = xTitle,
 			format = xAxisFormat,
 			grid = xGrid
 		}
 		if xAxisAngle then
 			local xAxisAlign
 			if xAxisAngle &lt; 0 then xAxisAlign = &quot;right&quot; else xAxisAlign = &quot;left&quot; 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 == &quot;integer&quot; and not yAxisFormat then yAxisFormat = &quot;d&quot; end
 		yAxis =
 		{
 			type = &quot;y&quot;,
 			scale = &quot;y&quot;,
 			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 = &quot;color&quot;,
 		stroke = &quot;color&quot;,
 		title = legendTitle,
 	}
 	legend.properties = {
 		title = {
 			fill = { value = persistentGrey },
 		},
 		labels = {
 			fill = { value = persistentGrey },
 		},
 	}
 	if chartType == &quot;pie&quot; 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 &quot;line&quot;
 	-- 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 ~= &quot;&quot; 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 &gt; 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 == &quot;log&quot; then
 --		if (not xMin or tonumber(xMin) &lt;= 0) then xMin = 0.1 end
 --		if not xType then xType = &quot;number&quot; end
 --	end
 --	if yScaleType == &quot;log&quot; then
 --		if (not yMin or tonumber(yMin) &lt;= 0) then yMin = 0.1 end
 --		if not yType then yType = &quot;number&quot; 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 ~= &quot;&quot; then
 
 		if xType == &quot;number&quot; or xType == &quot;integer&quot; 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 ~= &quot;&quot; then
 
 		if yType == &quot;number&quot; or yType == &quot;integer&quot; 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 == &quot;y&quot; then yNum = 1 else yNum = tonumber(string.match(name, &quot;^y(%d+)$&quot;)) end
 		if yNum then
 			yValues[yNum] = value
 			-- name the series: default is &quot;y&lt;number&gt;&quot;. Can be overwritten using the &quot;y&lt;number&gt;Title&quot; parameters.
 			seriesTitles[yNum] = frame.args[&quot;y&quot; .. yNum .. &quot;Title&quot;] or frame.args[&quot;y&quot; .. yNum .. &quot;title&quot;] 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 == &quot;pie&quot; then
 		-- for pie charts the second second series is merged into the first series as radius values
 		data = convertXYToSingleSeries(x, y, xType, yType, { &quot;y&quot;, &quot;r&quot; })
 	else
 		data = convertXYToManySeries(x, y, xType, yType, seriesTitles)
 	end
 
 	-- configure stacked charts
 	local stacked = false
 	local stats
 	if string.sub(chartType, 1, 7) == &quot;stacked&quot; then
 		chartType = string.sub(chartType, 8)
 		if #y &gt; 1 then -- ignore stacked charts if there is only one series
 		stacked = true
 		-- aggregate data by cumulative y values
 		stats =
 		{
 			name = &quot;stats&quot;, source = &quot;chart&quot;, transform =
 		{
 			{
 				type = &quot;aggregate&quot;,
 				groupby = { &quot;x&quot; },
 				summarize = { y = &quot;sum&quot; }
 			}
 		}
 		}
 		end
 	end
 	
 	-- add annotations to data
 	local vannoData, hannoData
 	
 	if v_annoLine then
 		vannoData = { name = &quot;v_anno&quot;, format = { type = &quot;json&quot;, 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 = &quot;h_anno&quot;, format = { type = &quot;json&quot;, 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 == &quot;line&quot;) then
 		lineScale = getLineScale(linewidths, chartType)
 		table.insert(scales, lineScale)
 	end
 
 	local radiusScale
 	if chartType == &quot;pie&quot; and #y &gt; 1 then
 		radiusScale = getValueScale(&quot;r&quot;, 0, outerRadius)
 		table.insert(scales, radiusScale)
 	end
 
 	-- decide if lines (strokes) or areas (fills) should be drawn
 	local colorField
 	if chartType == &quot;line&quot; then colorField = &quot;stroke&quot; else colorField = &quot;fill&quot; 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) == &quot;string&quot; then -- deserialize as table
 			local keyValues = mw.text.split(showValues, &quot;%s*,%s*&quot;)
 			showValues = {}
 			for _, kv in ipairs(keyValues) do
 				local key, value = mw.ustring.match(kv, &quot;^%s*(.-)%s*:%s*(.-)%s*$&quot;)
 				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 == &quot;0&quot; then xGrid = false
     	elseif xGrid == 0 then xGrid = false 
     	elseif xGrid == &quot;false&quot; then xGrid = false 
     	elseif xGrid == &quot;n&quot; then xGrid = false 
     	else xGrid = true 
     	end
     end
     if yGrid then 
     	if yGrid == &quot;0&quot; then yGrid = false
     	elseif yGrid == 0 then yGrid = false 
     	elseif yGrid == &quot;false&quot; then yGrid = false 
     	elseif yGrid == &quot;n&quot; then yGrid = false 
     	else yGrid = true 
     	end
     end
     
 	-- symbol marks
 	if showSymbols and chartType ~= &quot;rect&quot; then
 		local chartmarks = chartvis
 		if chartmarks.marks then chartmarks = chartmarks.marks[1] end
 
 		if type(showSymbols) == &quot;string&quot; then
 			if showSymbols == &quot;&quot; then showSymbols = true
 			else showSymbols = numericArray(showSymbols)
 			end
 		else
 			showSymbols = tonumber(showSymbols)
 		end
 
 		-- custom size
 		local symSize
 		if type(showSymbols) == &quot;number&quot; then 
 			symSize = tonumber(showSymbols*showSymbols*8.5)
 	 	elseif type(showSymbols) == &quot;table&quot; then 
 	 		symSize = {}
 	 		for k, v in pairs(showSymbols) do
                 symSize[k]=v*v*8.5 -- &quot;size&quot; acc to Vega syntax is area of symbol
             end
         else
 	 		symSize = 50
 	 	end
 		-- symSizeScale 
 	 	local symSizeScale = {}
 		if type(symSize) == &quot;table&quot; then
 			symSizeScale = getSymSizeScale(symSize)
 			table.insert(scales, symSizeScale)
 		end
  	
 
     	-- custom shape
     	if  stringArray(symbolsShape) and #stringArray(symbolsShape) &gt; 1 then symbolsShape = stringArray(symbolsShape) end
     	
     	local symShape = &quot; &quot;
 		
 		if type(symbolsShape) == &quot;string&quot; and shapes[symbolsShape] then
 			symShape = shapes[symbolsShape]
 	 	elseif type(symbolsShape) == &quot;table&quot; 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] = &quot;circle&quot;
                 end
             end
        	else
 			symShape = &quot;circle&quot;
 		end
 		-- symShapeScale 
 	 	local symShapeScale = {}
 		if type(symShape) == &quot;table&quot; then
 			symShapeScale = getSymShapeScale(symShape)
 			table.insert(scales, symShapeScale)
 		end 
  
 		-- custom stroke
 		local symStroke
 		if (type(symbolsStroke) == &quot;number&quot;) then 
 			symStroke = tonumber(symbolsStroke)
 -- TODO symStroke serialization
 --		elseif type(symbolsStroke) == &quot;table&quot; then 
 --	 		symStroke = {}
 --	 		for k, v in pairs(symbolsStroke) do
 --                symStroke[k]=symbolsStroke[k]
 --                		--always draw x with stroke
 --				if symbolsShape[k] == &quot;x&quot; 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 == &quot;x&quot; 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) == &quot;table&quot; 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   &quot;\n\nchart JSON\n &quot;.. p.chart(frame) .. &quot; \n\n&quot; .. 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.args[1])), 'PATH')
 end