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