# 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 - "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 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  
-- 2020-04-08 Change default showValues.fontcolor from black to persistentGrey  
-- 2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true  
-- 2020-03-11 Allow user-defined scale types, e.g. logarithmic scale  
-- 2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid  
-- 2019-01-24 Allow comma-separated lists to contain values with commas  
-- 2018-10-13 Fix browser color-inversion issues via #54595d per mw:Template:Graph:PageViews  
-- 2018-09-16 Allow disabling the legend for templates  
-- 2018-09-10 Allow grid lines  
-- 2018-08-26 Use user-defined order for stacked charts  
-- 2018-02-11 Force usage of explicitely provided x minimum and/or maximum values, rotation of x labels  
-- 2017-08-08 Added showSymbols param to show symbols on line charts  
-- 2016-05-16 Added encodeTitleForPath to help all path-based APIs graphs like pageviews  
-- 2016-03-20 Allow omitted data for charts, labels for line charts with string (ordinal) scale at point location  
-- 2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon.  

local p = {}  

--add debug text to this string with eg. debuglog = debuglog .. "" .. "\n\n" .. "- " .. debug.traceback .. "result type: ".. type(result) .. " result: \n\n" .. mw.dumpObject(result)  
--invoke chartDebuger to get graph JSON and this string  
debuglog = "Debug " .. "\n\n"  

local baseMapDirectory = "Module:Graph/"  
local persistentGrey = "#54595d"  

local shapes = {}  
shapes = {  
circle = "circle", x= "M-.5,-.5L.5,.5M.5,-.5L-.5,.5" , square = "square",  
cross = "cross", diamond = "diamond", triangle_up = "triangle-up",  
triangle_down = "triangle-down", triangle_right = "triangle-",  
triangle_left = "triangle-",  
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  
-- map path data for geographic objects  
local basemap = .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(.args.scale) or 100  
-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections  
local projection = .args.projection or "equirectangular"  
-- defaultValue for geographic objects without data  
local defaultValue = .args.defaultValue or .args.defaultvalue  
local scaleType = .args.scaleType or .args.scaletype or "linear"  
-- minimaler Wertebereich (nur für numerische Daten)  
local domainMin = tonumber(.args.domainMin or .args.domainmin)  
-- maximaler Wertebereich (nur für numerische Daten)  
local domainMax = tonumber(.args.domainMax or .args.domainmax)  
-- Farbwerte der Farbskala (nur für numerische Daten)  
local colorScale = .args.colorScale or .args.colorscale or "category10"  
-- show legend  
local legend = .args.legend  
-- the map feature to display  
local feature = .args.feature or "countries"  
-- map  
local = numericArray(.args.)  
-- format JSON output  
local formatJson = .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(.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 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 },  
= ,  
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 = ""  
properties.fill.value = showValues.fontcolor or "white"  
else  
properties.align.value = ""  
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 = "" }  
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 on half-circle and on half-circle  
local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "", "" } }  
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 xAxis else xAxis 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 position to top  
legend = {  
y = { value = -outerRadius },  
},  
title = {  
fill = { value = persistentGrey }  
},  
labels = {  
fill = { value = persistentGrey },  
},  
}  
end  
return legend  
end  

function p.chart  
-- chart width and height  
local graphwidth = tonumber(.args.width) or 200  
local graphheight = tonumber(.args.height) or 200  
-- chart type  
local chartType = .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 = .args.interpolate  
-- mark colors (if no colors are given, the default 10 color palette is used)  
local colorString = .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(.args.linewidth)  
local linewidthsString = .args.linewidths  
local linewidths  
if linewidthsString and linewidthsString ~= "" then linewidths = numericArray(linewidthsString) or false end  
-- x and y axis caption  
local xTitle = .args.xAxisTitle or .args.xaxistitle  
local yTitle = .args.yAxisTitle or .args.yaxistitle  
-- x and y value types  
local xType = .args.xType or .args.xtype  
local yType = .args.yType or .args.ytype  
-- override x and y axis minimum and maximum  
local xMin = .args.xAxisMin or .args.xaxismin  
local xMax = .args.xAxisMax or .args.xaxismax  
local yMin = .args.yAxisMin or .args.yaxismin  
local yMax = .args.yAxisMax or .args.yaxismax  
-- override x and y axis label formatting  
local xAxisFormat = .args.xAxisFormat or .args.xaxisformat  
local yAxisFormat = .args.yAxisFormat or .args.yaxisformat  
local xAxisAngle = tonumber(.args.xAxisAngle) or tonumber(.args.xaxisangle)  
-- x and y scale types  
local xScaleType = .args.xScaleType or .args.xscaletype  
local yScaleType = .args.yScaleType or .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 = .args.xGrid or .args.xgrid or false  
local yGrid = .args.yGrid or .args.ygrid or false  
-- for line chart, show a symbol at each data point  
local showSymbols = .args.showSymbols or .args.showsymbols  
local symbolsShape = .args.symbolsShape or .args.symbolsshape  
local symbolsNoFill = .args.symbolsNoFill or .args.symbolsnofill  
local symbolsStroke = tonumber(.args.symbolsStroke or .args.symbolsstroke)  
-- show legend with given title  
local legendTitle = .args.legend  
-- show values as text  
local showValues = .args.showValues or .args.showvalues  
-- show v- and h-line annotations  
local v_annoLineString = .args.vAnnotatonsLine or .args.vannotatonsline  
local h_annoLineString = .args.hAnnotatonsLine or .args.hannotatonsline  
local v_annoLabelString = .args.vAnnotatonsLabel or .args.vannotatonslabel  
local h_annoLabelString = .args.hAnnotatonsLabel or .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(.args.innerRadius) or tonumber(.args.innerradius) or 0  
local outerRadius = math.min(graphwidth, graphheight)  
-- format JSON output  
local formatJson = .args.formatjson  

-- get x values  
local x  
x, xType, xMin, xMax = deserializeXData(.args.x, xType, xMin, xMax)  

-- get y values (series)  
local yValues = {}  
local seriesTitles = {}  
for name, value in pairs(.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] = .args["y" .. yNum .. "Title"] or .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  
return p.map(:getParent)  
end  

function p.chartWrapper  
return p.chart(:getParent)  
end  

function p.chartDebuger  
return "\n\nchart JSON\n ".. p.chart .. " \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  
return mw.uri.encode(mw.text.decode(mw.text.trim(.args[1])), 'PATH')  
end  

return p