wpu.nu

Skillnad mellan versioner av "Modul:Graph"

Från wpu.nu

(v an h-line annotations, fix for bug Template_talk:Graph:Chart#Repeated y-values - special thanks to User:Timbaaa)
m (1 version importerades: Importerar för Graph-funktion)
Rad 1: Rad 1:
 
+
-- ATTENTION: Please edit this code at https://de.wikipedia.org/wiki/Modul: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!
--         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_):
 
-- Version History (_PLEASE UPDATE when modifying anything_):
--  2020-09-01 Vertical and horizontal line annotations
 
--  2020-08-08 New logic for "nice" for x axis (problem with scale when xType = "date") and grid
 
--  2020-06-21 Serializes symbol size
 
--              transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line)
 
--              Linewidth serialized with "linewidths"
 
--              Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0
 
--              p.chartDebuger(frame) for easy debug and JSON output
 
--  2020-06-07 Allow lowercase variables for use with [[Template:Wikidata list]]
 
--  2020-05-27 Map: allow specification which feature to display and changing the map center
 
--  2020-04-08 Change default showValues.fontcolor from black to persistentGrey
 
--  2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true
 
--  2020-03-11 Allow user-defined scale types, e.g. logarithmic scale
 
--  2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid
 
 
--  2019-01-24 Allow comma-separated lists to contain values with commas
 
--  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-10-13 Fix browser color-inversion issues via #54595d per [[mw:Template:Graph:PageViews]]
Rad 44: Rad 15:
  
 
local p = {}
 
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 baseMapDirectory = "Module:Graph/"
local persistentGrey = "#54595d"
 
 
local shapes = {}
 
shapes = {
 
circle = "circle", x= "M-.5,-.5L.5,.5M.5,-.5L-.5,.5" , square = "square",
 
cross = "cross", diamond = "diamond", triangle_up = "triangle-up",
 
triangle_down = "triangle-down", triangle_right = "triangle-right",
 
triangle_left = "triangle-left",
 
banana = "m -0.5281,0.2880 0.0020,0.0192 m 0,0 c 0.1253,0.0543 0.2118,0.0679 0.3268,0.0252 0.1569,-0.0582 0.3663,-0.1636 0.4607,-0.3407 0.0824,-0.1547 0.1202,-0.2850 0.0838,-0.4794 l 0.0111,-0.1498 -0.0457,-0.0015 c -0.0024,0.3045 -0.1205,0.5674 -0.3357,0.7414 -0.1409,0.1139 -0.3227,0.1693 -0.5031,0.1856 m 0,0 c 0.1804,-0.0163 0.3622,-0.0717 0.5031,-0.1856 0.2152,-0.1739 0.3329,-0.4291 0.3357,-0.7414 l -0.0422,0.0079 c 0,0 -0.0099,0.1111 -0.0227,0.1644 -0.0537,0.1937 -0.1918,0.3355 -0.3349,0.4481 -0.1393,0.1089 -0.2717,0.2072 -0.4326,0.2806 l -0.0062,0.0260"
 
  }
 
 
  
 
local function numericArray(csv)
 
local function numericArray(csv)
Rad 80: Rad 36:
 
end
 
end
 
end
 
end
 
+
return result, isInteger
    return result, isInteger
 
 
end
 
end
  
Rad 108: Rad 63:
 
function p.map(frame)
 
function p.map(frame)
 
-- map path data for geographic objects
 
-- map path data for geographic objects
local basemap = frame.args.basemap or "Template:Graph:Map/Inner/Worldmap2c-json" -- WorldMap name and/or location may vary from wiki to wiki
+
local basemap = frame.args.basemap or "WorldMap-iso2.json"
 
-- scaling factor
 
-- scaling factor
 
local scale = tonumber(frame.args.scale) or 100
 
local scale = tonumber(frame.args.scale) or 100
Rad 114: Rad 69:
 
local projection = frame.args.projection or "equirectangular"
 
local projection = frame.args.projection or "equirectangular"
 
-- defaultValue for geographic objects without data
 
-- defaultValue for geographic objects without data
local defaultValue = frame.args.defaultValue or frame.args.defaultvalue
+
local defaultValue = frame.args.defaultValue
local scaleType = frame.args.scaleType or frame.args.scaletype or "linear"
+
local scaleType = frame.args.scaleType or "linear"
 
-- minimaler Wertebereich (nur für numerische Daten)
 
-- minimaler Wertebereich (nur für numerische Daten)
local domainMin = tonumber(frame.args.domainMin or frame.args.domainmin)
+
local domainMin = tonumber(frame.args.domainMin)
 
-- maximaler Wertebereich (nur für numerische Daten)
 
-- maximaler Wertebereich (nur für numerische Daten)
local domainMax = tonumber(frame.args.domainMax or frame.args.domainmax)
+
local domainMax = tonumber(frame.args.domainMax)
 
-- Farbwerte der Farbskala (nur für numerische Daten)
 
-- Farbwerte der Farbskala (nur für numerische Daten)
local colorScale = frame.args.colorScale or frame.args.colorscale or "category10"
+
local colorScale = frame.args.colorScale or "category10"
 
-- show legend
 
-- show legend
 
local legend = frame.args.legend
 
local legend = frame.args.legend
-- the map feature to display
 
    local feature = frame.args.feature or "countries"
 
    -- map center
 
    local center = numericArray(frame.args.center)
 
 
-- format JSON output
 
-- format JSON output
 
local formatJson = frame.args.formatjson
 
local formatJson = frame.args.formatjson
Rad 192: Rad 143:
 
}
 
}
 
end
 
end
+
 
 
-- get map url
 
-- get map url
 
local basemapUrl
 
local basemapUrl
Rad 217: Rad 168:
 
{
 
{
 
-- data source for map paths data
 
-- data source for map paths data
name = feature,
+
name = "countries",
 
url = basemapUrl,
 
url = basemapUrl,
format = { type = "topojson", feature = feature },
+
format = { type = "topojson", feature = "countries" },
 
transform =
 
transform =
 
{
 
{
Rad 227: Rad 178:
 
value = "data", -- data source
 
value = "data", -- data source
 
scale = scale,
 
scale = scale,
                        translate = { 0, 0 },
+
translate = { 0, 0 },
                        center = center,
 
 
projection = projection
 
projection = projection
 
},
 
},
Rad 248: Rad 198:
 
{
 
{
 
type = "path",
 
type = "path",
from = { data = feature },
+
from = { data = "countries" },
 
properties =
 
properties =
 
{
 
{
Rad 289: Rad 239:
 
if not xType then xType = "string" end
 
if not xType then xType = "string" end
 
end
 
end
 +
 
return x, xType, xMin, xMax
 
return x, xType, xMin, xMax
 
end
 
end
Rad 360: Rad 311:
 
end
 
end
  
local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)
+
local function getXScale(chartType, stacked, xMin, xMax, xType)
 
if chartType == "pie" then return end
 
if chartType == "pie" then return end
  
Rad 366: Rad 317:
 
{
 
{
 
name = "x",
 
name = "x",
 +
type = "linear",
 
range = "width",
 
range = "width",
 
zero = false, -- do not include zero value
 
zero = false, -- do not include zero value
 +
nice = true,  -- force round numbers for y scale
 
domain = { data = "chart", field = "x" }
 
domain = { data = "chart", field = "x" }
 
}
 
}
if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end
 
 
if xMin then xscale.domainMin = xMin end
 
if xMin then xscale.domainMin = xMin end
 
if xMax then xscale.domainMax = xMax end
 
if xMax then xscale.domainMax = xMax end
Rad 380: Rad 332:
 
xscale.type = "ordinal"
 
xscale.type = "ordinal"
 
if not stacked then xscale.padding = 0.2 end -- pad each bar group
 
if not stacked then xscale.padding = 0.2 end -- pad each bar group
else  
+
else
if xType == "date" then  
+
if xType == "date" then xscale.type = "time"
xscale.type = "time"
 
 
elseif xType == "string" then
 
elseif xType == "string" then
 
xscale.type = "ordinal"
 
xscale.type = "ordinal"
Rad 388: Rad 339:
 
end
 
end
 
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
 
return xscale
 
end
 
end
  
local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)
+
local function getYScale(chartType, stacked, yMin, yMax, yType)
 
if chartType == "pie" then return end
 
if chartType == "pie" then return end
  
Rad 398: Rad 349:
 
{
 
{
 
name = "y",
 
name = "y",
--type = yScaleType or "linear",
+
type = "linear",
 
range = "height",
 
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
 
-- 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",
 
zero = chartType ~= "line",
nice = yScaleType ~= "log" -- force round numbers for y scale, but log scale outputs a wrong "nice" scale
+
nice = true
 
}
 
}
if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end
 
 
if yMin then yscale.domainMin = yMin end
 
if yMin then yscale.domainMin = yMin end
 
if yMax then yscale.domainMax = yMax end
 
if yMax then yscale.domainMax = yMax end
Rad 455: Rad 405:
 
end
 
end
 
return alphaScale
 
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
 
end
  
Rad 553: Rad 463:
 
end
 
end
  
local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate)
+
local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, interpolate)
 
if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end
 
if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end
  
Rad 572: Rad 482:
 
if colorField == "stroke" then
 
if colorField == "stroke" then
 
chartvis.properties.enter.strokeWidth = { value = linewidth or 2.5 }
 
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
 
end
  
Rad 620: Rad 522:
 
chartvis.properties.update[colorField].field = "series"
 
chartvis.properties.update[colorField].field = "series"
 
if alphaScale then chartvis.properties.update[colorField .. "Opacity"].field = "series" end
 
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
 
-- apply a grouping (facetting) transformation
 
chartvis =
 
chartvis =
Rad 689: Rad 584:
 
else
 
else
 
properties.align.value = "left"
 
properties.align.value = "left"
properties.fill.value = showValues.fontcolor or persistentGrey
+
properties.fill.value = showValues.fontcolor or "black"
 
end
 
end
 
elseif chartType == "pie" then
 
elseif chartType == "pie" then
Rad 698: Rad 593:
 
radius = { offset = tonumber(showValues.offset) or -4 },
 
radius = { offset = tonumber(showValues.offset) or -4 },
 
theta = { field = "layout_mid" },
 
theta = { field = "layout_mid" },
fill = { value = showValues.fontcolor or persistentGrey },
+
fill = { value = showValues.fontcolor or "black" },
 
baseline = { },
 
baseline = { },
 
angle = { },
 
angle = { },
Rad 757: Rad 652:
 
end
 
end
  
local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale)
+
local function getSymbolMarks(chartvis)
 
+
local symbolmarks =
local symbolmarks
 
symbolmarks =
 
 
{
 
{
 
type = "symbol",
 
type = "symbol",
Rad 769: Rad 662:
 
x = { scale = "x", field = "x" },
 
x = { scale = "x", field = "x" },
 
y = { scale = "y", field = "y" },
 
y = { scale = "y", field = "y" },
strokeWidth = { value = symStroke },
 
stroke = { scale = "color", field = "series" },
 
 
fill = { scale = "color", field = "series" },
 
fill = { scale = "color", field = "series" },
 +
shape = "circle",
 +
size = { value = 49 }
 
}
 
}
 
}
 
}
 
}
 
}
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
 
if chartvis.from then symbolmarks.from = copy(chartvis.from) end
   
+
 
 
return symbolmarks
 
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
 
end
  
Rad 887: Rad 683:
 
title = xTitle,
 
title = xTitle,
 
format = xAxisFormat,
 
format = xAxisFormat,
grid = xGrid
+
grid = xGrid ~= "0"
 
}
 
}
 
if xAxisAngle then
 
if xAxisAngle then
Rad 896: Rad 692:
 
title =
 
title =
 
{
 
{
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
labels =
 
labels =
Rad 902: Rad 698:
 
angle = { value = xAxisAngle },
 
angle = { value = xAxisAngle },
 
align = { value = xAxisAlign },
 
align = { value = xAxisAlign },
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
ticks =
 
ticks =
 
{
 
{
stroke = { value = persistentGrey }
+
stroke = { value = "#54595d" }
 
},
 
},
 
axis =
 
axis =
 
{
 
{
stroke = { value = persistentGrey },
+
stroke = { value = "#54595d" },
 
strokeWidth = { value = 2 }
 
strokeWidth = { value = 2 }
},
 
grid =
 
{
 
stroke = { value = persistentGrey }
 
 
}
 
}
 
}
 
}
Rad 923: Rad 715:
 
title =
 
title =
 
{
 
{
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
labels =
 
labels =
 
{
 
{
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
ticks =
 
ticks =
 
{
 
{
stroke = { value = persistentGrey }
+
stroke = { value = "#54595d" }
 
},
 
},
 
axis =
 
axis =
 
{
 
{
stroke = { value = persistentGrey },
+
stroke = { value = "#54595d" },
 
strokeWidth = { value = 2 }
 
strokeWidth = { value = 2 }
},
 
grid =
 
{
 
stroke = { value = persistentGrey }
 
 
}
 
}
 
}
 
}
Rad 952: Rad 740:
 
title = yTitle,
 
title = yTitle,
 
format = yAxisFormat,
 
format = yAxisFormat,
grid = yGrid
+
grid = yGrid ~= "0"
 
}
 
}
 
yAxis.properties =
 
yAxis.properties =
Rad 958: Rad 746:
 
title =
 
title =
 
{
 
{
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
labels =
 
labels =
 
{
 
{
fill = { value = persistentGrey }
+
fill = { value = "#54595d" }
 
},
 
},
 
ticks =
 
ticks =
 
{
 
{
stroke = { value = persistentGrey }
+
stroke = { value = "#54595d" }
 
},
 
},
 
axis =
 
axis =
 
{
 
{
stroke = { value = persistentGrey },
+
stroke = { value = "#54595d" },
 
strokeWidth = { value = 2 }
 
strokeWidth = { value = 2 }
 
},
 
},
 
grid =
 
grid =
 
{
 
{
stroke = { value = persistentGrey }
+
stroke = { value = "#54595d" }
 
}
 
}
 
}
 
}
 
 
end
 
end
  
Rad 990: Rad 777:
 
stroke = "color",
 
stroke = "color",
 
title = legendTitle,
 
title = legendTitle,
}
 
legend.properties = {
 
title = {
 
fill = { value = persistentGrey },
 
},
 
labels = {
 
fill = { value = persistentGrey },
 
},
 
 
}
 
}
 
if chartType == "pie" then
 
if chartType == "pie" then
legend.properties = {
+
-- move legend from center position to top
-- move legend from center position to top
+
legend.properties = { legend = { y = { value = -outerRadius } } }
legend = {
 
y = { value = -outerRadius },
 
},
 
title = {
 
fill = { value = persistentGrey }
 
},
 
labels = {
 
fill = { value = persistentGrey },
 
},
 
}
 
 
end
 
end
 
return legend
 
return legend
Rad 1 030: Rad 799:
 
-- for line charts, the thickness of the line; for pie charts the gap between each slice
 
-- for line charts, the thickness of the line; for pie charts the gap between each slice
 
local linewidth = tonumber(frame.args.linewidth)
 
local linewidth = tonumber(frame.args.linewidth)
local linewidthsString = frame.args.linewidths
 
local linewidths
 
if linewidthsString and linewidthsString ~= "" then linewidths = numericArray(linewidthsString) or false end
 
 
-- x and y axis caption
 
-- x and y axis caption
local xTitle = frame.args.xAxisTitle or frame.args.xaxistitle
+
local xTitle = frame.args.xAxisTitle
local yTitle = frame.args.yAxisTitle or frame.args.yaxistitle
+
local yTitle = frame.args.yAxisTitle
 
-- x and y value types
 
-- x and y value types
local xType = frame.args.xType or frame.args.xtype
+
local xType = frame.args.xType
local yType = frame.args.yType or frame.args.ytype
+
local yType = frame.args.yType
 
-- override x and y axis minimum and maximum
 
-- override x and y axis minimum and maximum
local xMin = frame.args.xAxisMin or frame.args.xaxismin
+
local xMin = frame.args.xAxisMin
local xMax = frame.args.xAxisMax or frame.args.xaxismax
+
local xMax = frame.args.xAxisMax
local yMin = frame.args.yAxisMin or frame.args.yaxismin
+
local yMin = frame.args.yAxisMin
local yMax = frame.args.yAxisMax or frame.args.yaxismax
+
local yMax = frame.args.yAxisMax
 
-- override x and y axis label formatting
 
-- override x and y axis label formatting
local xAxisFormat = frame.args.xAxisFormat or frame.args.xaxisformat
+
local xAxisFormat = frame.args.xAxisFormat
local yAxisFormat = frame.args.yAxisFormat or frame.args.yaxisformat
+
local yAxisFormat = frame.args.yAxisFormat
local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle)
+
local xAxisAngle = tonumber(frame.args.xAxisAngle)
-- x and y scale types
 
local xScaleType = frame.args.xScaleType or frame.args.xscaletype
 
local yScaleType = frame.args.yScaleType or frame.args.yscaletype 
 
-- log scale require minimum > 0, for now it's no possible to plot negative values on log - TODO see: https://www.mathworks.com/matlabcentral/answers/1792-log-scale-graphic-with-negative-value
 
-- if xScaleType == "log" then
 
-- if (not xMin or tonumber(xMin) <= 0) then xMin = 0.1 end
 
-- if not xType then xType = "number" end
 
-- end
 
-- if yScaleType == "log" then
 
-- if (not yMin or tonumber(yMin) <= 0) then yMin = 0.1 end
 
-- if not yType then yType = "number" end
 
-- end
 
 
 
 
-- show grid
 
-- show grid
local xGrid = frame.args.xGrid or frame.args.xgrid or false
+
local xGrid = frame.args.xGrid or "0"
local yGrid = frame.args.yGrid or frame.args.ygrid or false
+
local yGrid = frame.args.yGrid or "0"
 
-- for line chart, show a symbol at each data point
 
-- for line chart, show a symbol at each data point
local showSymbols = frame.args.showSymbols or frame.args.showsymbols
+
local showSymbols = 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
 
-- show legend with given title
 
local legendTitle = frame.args.legend
 
local legendTitle = frame.args.legend
 
-- show values as text
 
-- show values as text
local showValues = frame.args.showValues or frame.args.showvalues
+
local showValues = frame.args.showValues
-- show v- and h-line annotations
 
local v_annoLineString = frame.args.vAnnotatonsLine or frame.args.vannotatonsline
 
local h_annoLineString = frame.args.hAnnotatonsLine or frame.args.hannotatonsline
 
local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel
 
local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel
 
 
 
 
 
 
 
 
 
 
 
-- decode annotations cvs
 
local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel
 
if v_annoLineString and v_annoLineString ~= "" then
 
 
 
if xType == "number" or xType == "integer" then
 
v_annoLine = numericArray(v_annoLineString)
 
 
 
else
 
v_annoLine = stringArray(v_annoLineString)
 
 
 
end
 
v_annoLabel = stringArray(v_annoLabelString)
 
end
 
if h_annoLineString and h_annoLineString ~= "" then
 
 
 
if yType == "number" or yType == "integer" then
 
h_annoLine = numericArray(h_annoLineString)
 
 
 
else
 
h_annoLine = stringArray(h_annoLineString)
 
 
 
end
 
h_annoLabel = stringArray(h_annoLabelString)
 
end
 
 
 
 
 
 
 
 
 
 
 
 
-- pie chart radiuses
 
-- pie chart radiuses
local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0
+
local innerRadius = tonumber(frame.args.innerRadius) or 0
 
local outerRadius = math.min(graphwidth, graphheight)
 
local outerRadius = math.min(graphwidth, graphheight)
 
-- format JSON output
 
-- format JSON output
Rad 1 131: Rad 842:
 
yValues[yNum] = value
 
yValues[yNum] = value
 
-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.
 
-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.
seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or frame.args["y" .. yNum .. "title"] or name
+
seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or name
 
end
 
end
 
end
 
end
Rad 1 167: Rad 878:
 
end
 
end
 
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
 
-- create scales
 
local scales = {}
 
local scales = {}
  
local xscale = getXScale(chartType, stacked, xMin, xMax, xType, xScaleType)
+
local xscale = getXScale(chartType, stacked, xMin, xMax, xType)
 
table.insert(scales, xscale)
 
table.insert(scales, xscale)
local yscale = getYScale(chartType, stacked, yMin, yMax, yType, yScaleType)
+
local yscale = getYScale(chartType, stacked, yMin, yMax, yType)
 
table.insert(scales, yscale)
 
table.insert(scales, yscale)
  
Rad 1 200: Rad 892:
 
local alphaScale = getAlphaColorScale(colors, y)
 
local alphaScale = getAlphaColorScale(colors, y)
 
table.insert(scales, alphaScale)
 
table.insert(scales, alphaScale)
 
local lineScale
 
if (linewidths) and (chartType == "line") then
 
lineScale = getLineScale(linewidths, chartType)
 
table.insert(scales, lineScale)
 
end
 
  
 
local radiusScale
 
local radiusScale
Rad 1 216: Rad 902:
 
local colorField
 
local colorField
 
if chartType == "line" then colorField = "stroke" else colorField = "fill" end
 
if chartType == "line" then colorField = "stroke" else colorField = "fill" end
 
 
  
 
-- create chart markings
 
-- create chart markings
local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate)
+
local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, interpolate)
 
local marks = { chartvis }
 
local marks = { chartvis }
 
 
Rad 1 243: Rad 927:
 
end
 
end
 
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
 
-- symbol marks
if showSymbols and chartType ~= "rect" then
+
if chartType == "line" and showSymbols then
 
local chartmarks = chartvis
 
local chartmarks = chartvis
 
if chartmarks.marks then chartmarks = chartmarks.marks[1] end
 
if chartmarks.marks then chartmarks = chartmarks.marks[1] end
 
+
local symbolmarks = getSymbolMarks(chartmarks)
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
 
if chartmarks ~= chartvis then
 
table.insert(chartvis.marks, symbolmarks)
 
table.insert(chartvis.marks, symbolmarks)
Rad 1 359: Rad 938:
 
table.insert(marks, symbolmarks)
 
table.insert(marks, symbolmarks)
 
end
 
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
 
end
  
 
-- axes
 
-- axes
 
local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)
 
local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType)
+
 
 
-- legend
 
-- legend
 
local legend
 
local legend
 
if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end
 
if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end
 +
 
-- construct final output object
 
-- construct final output object
 
local output =
 
local output =
Rad 1 384: Rad 953:
 
width = graphwidth,
 
width = graphwidth,
 
height = graphheight,
 
height = graphheight,
data = { data },
+
data = { data, stats },
 
scales = scales,
 
scales = scales,
 
axes = { xAxis, yAxis },
 
axes = { xAxis, yAxis },
Rad 1 390: Rad 959:
 
legends = { legend }
 
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
 
local flags
Rad 1 406: Rad 972:
 
return p.chart(frame:getParent())
 
return p.chart(frame:getParent())
 
end
 
end
 
function p.chartDebuger(frame)
 
return  "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog
 
end
 
 
  
 
-- Given an HTML-encoded title as first argument, e.g. one produced with {{ARTICLEPAGENAME}},
 
-- Given an HTML-encoded title as first argument, e.g. one produced with {{ARTICLEPAGENAME}},

Versionen från 17 januari 2021 kl. 11.53

Dokumentationen för denna modul kan skapas på Modul:Graph/dok

-- ATTENTION:  Please edit this code at https://de.wikipedia.org/wiki/Modul:Graph
--             This way all wiki languages can stay in sync. Thank you!
--
-- Version History (_PLEASE UPDATE when modifying anything_):
--   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 = {}

local baseMapDirectory = "Module:Graph/"

local function numericArray(csv)
	if not csv then return end

	local list = mw.text.split(csv, "%s*,%s*")
	local result = {}
	local isInteger = true
	for i = 1, #list do
		if list[i] == "" then
			result[i] = nil
		else
			result[i] = tonumber(list[i])
			if not result[i] then return end
			if isInteger then
				local int, frac = math.modf(result[i])
				isInteger = frac == 0.0
			end
		end
	end
	return result, isInteger
end

local function stringArray(text)
	if not text then return end

	local list = mw.text.split(mw.ustring.gsub(tostring(text), "\\,", "<COMMA>"), ",", true)
	for i = 1, #list do
		list[i] = mw.ustring.gsub(mw.text.trim(list[i]), "<COMMA>", ",")
	end
	return list
end

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

local function copy(x)
	if type(x) == "table" then
		local result = {}
		for key, value in pairs(x) do result[key] = copy(value) end
		return result
	else
		return x
	end
end

function p.map(frame)
	-- map path data for geographic objects
	local basemap = frame.args.basemap or "WorldMap-iso2.json"
	-- scaling factor
	local scale = tonumber(frame.args.scale) or 100
	-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections
	local projection = frame.args.projection or "equirectangular"
	-- defaultValue for geographic objects without data
	local defaultValue = frame.args.defaultValue
	local scaleType = frame.args.scaleType or "linear"
	-- minimaler Wertebereich (nur für numerische Daten)
	local domainMin = tonumber(frame.args.domainMin)
	-- maximaler Wertebereich (nur für numerische Daten)
	local domainMax = tonumber(frame.args.domainMax)
	-- Farbwerte der Farbskala (nur für numerische Daten)
	local colorScale = frame.args.colorScale or "category10"
	-- show legend
	local legend = frame.args.legend
	-- format JSON output
	local formatJson = frame.args.formatjson

	-- map data are key-value pairs: keys are non-lowercase strings (ideally ISO codes) which need to match the "id" values of the map path data
	local values = {}
	local isNumbers = nil
	for name, value in pairs(frame.args) do
		if mw.ustring.find(name, "^[^%l]+$") and value and value ~= "" then
			if isNumbers == nil then isNumbers = tonumber(value) end
			local data = { id = name, v = value }
			if isNumbers then data.v = tonumber(data.v) end
			table.insert(values, data)
		end
	end
	if not defaultValue then
		if isNumbers then defaultValue = 0 else defaultValue = "silver" end
	end

	-- create highlight scale
	local scales
	if isNumbers then
		if colorScale then colorScale = string.lower(colorScale) end
		if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end
		scales =
		{
			{
				name = "color",
				type = scaleType,
				domain = { data = "highlights", field = "v" },
				range = colorScale,
				nice = true,
				zero = false
			}
		}
		if domainMin then scales[1].domainMin = domainMin end
		if domainMax then scales[1].domainMax = domainMax end

		local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent
		if exponent then
			scales[1].type = "pow"
			scales[1].exponent = exponent
		end
	end

	-- create legend
	if legend then
		legend =
		{
			{
				fill = "color",
				offset = 120,
				properties =
				{
					title = { fontSize = { value = 14 } },
					labels = { fontSize = { value = 12 } },
					legend =
					{
						stroke = { value = "silver" },
						strokeWidth = { value = 1.5 }
					}
				}
			}
		}
	end

	-- get map url
	local basemapUrl
	if (string.sub(basemap, 1, 10) == "wikiraw://") then
		basemapUrl = basemap
	else
		-- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name.
		if not string.find(basemap, ":") then basemap = baseMapDirectory .. basemap end
		basemapUrl = "wikiraw:///" .. mw.uri.encode(mw.title.new(basemap).prefixedText, "PATH")
	end

	local output =
	{
		version = 2,
		width = 1,  -- generic value as output size depends solely on map size and scaling factor
		height = 1, -- ditto
		data =
		{
			{
				-- data source for the highlights
				name = "highlights",
				values = values
			},
			{
				-- data source for map paths data
				name = "countries",
				url = basemapUrl,
				format = { type = "topojson", feature = "countries" },
				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 = "countries" },
				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)
	if chartType == "pie" then return end

	local xscale =
	{
		name = "x",
		type = "linear",
		range = "width",
		zero = false, -- do not include zero value
		nice = true,  -- force round numbers for y scale
		domain = { data = "chart", field = "x" }
	}
	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

	return xscale
end

local function getYScale(chartType, stacked, yMin, yMax, yType)
	if chartType == "pie" then return end

	local yscale =
	{
		name = "y",
		type = "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 = true
	}
	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 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, 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 }
	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
		-- apply a grouping (facetting) transformation
		chartvis =
		{
			type = "group",
			marks = { chartvis },
			from =
			{
				data = "chart",
				transform =
				{
					{
						type = "facet",
						groupby = { "series" }
					}
				}
			}
		}
		-- for stacked charts apply a stacking transformation
		if stacked then
			table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } )
		else
			-- for bar charts the series are side-by-side grouped by x
			if chartType == "rect" then
				-- for bar charts with multiple series: each serie is grouped by the x value, therefore the series need their own scale within each x group
				local groupScale =
				{
					name = "series",
					type = "ordinal",
					range = "width",
					domain = { field = "series" }
				}

				chartvis.from.transform[1].groupby = "x"
				chartvis.scales = { groupScale }
				chartvis.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
			end
		end
	end

	return chartvis
end

local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues)
	local properties
	if chartType == "rect" then
		properties =
		{
			x = { scale = chartvis.properties.enter.x.scale, field = chartvis.properties.enter.x.field },
			y = { scale = chartvis.properties.enter.y.scale, field = chartvis.properties.enter.y.field, offset = -(tonumber(showValues.offset) or -4) },
			--dx = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text
			dy = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for vertical text
			align = { },
			baseline = { value = "middle" },
			fill = { },
			angle = { value = -90 },
			fontSize = { value = tonumber(showValues.fontsize) or 11 }
		}
		if properties.y.offset >= 0 then
			properties.align.value = "right"
			properties.fill.value = showValues.fontcolor or "white"
		else
			properties.align.value = "left"
			properties.fill.value = showValues.fontcolor or "black"
		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 "black" },
			baseline = { },
			angle = { },
			fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) }
		}
		if (showValues.angle or "midangle") == "midangle" then
			properties.align = { value = "center" }
			properties.angle = { field = "layout_mid", mult = 180.0 / math.pi }

			if properties.radius.offset >= 0 then
				properties.baseline.value = "bottom"
			else
				if not showValues.fontcolor then properties.fill.value = "white" end
				properties.baseline.value = "top"
			end
		elseif tonumber(showValues.angle) then
			-- qunatize scale for aligning text left on right half-circle and right on left half-circle
			local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "left", "right" } }
			table.insert(scales, alignScale)

			properties.align = { scale = alignScale.name, field = "layout_mid" }
			properties.angle = { value = tonumber(showValues.angle) }
			properties.baseline.value = "middle"
			if not tonumber(showValues.offset) then properties.radius.offset = 4 end
		end

		if radiusScale then
			properties.radius.scale = radiusScale.name
			properties.radius.field = radiusScale.domain.field
		else
			properties.radius.value = outerRadius
		end
	end

	if properties then
		if showValues.format then
			local template = "datum.y"
			if yType == "integer" or yType == "number" then template = template .. "|number:'" .. showValues.format .. "'"
			elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'"
			end
			properties.text = { template = "{{" .. template .. "}}" }
		else
			properties.text = { field = "y" }
		end

		local textmarks =
		{
			type = "text",
			properties =
			{
				enter = properties
			}
		}
		if chartvis.from then textmarks.from = copy(chartvis.from) end

		return textmarks
	end
end

local function getSymbolMarks(chartvis)
	local symbolmarks =
	{
		type = "symbol",
		properties =
		{
			enter = 
			{
				x = { scale = "x", field = "x" },
				y = { scale = "y", field = "y" },
				fill = { scale = "color", field = "series" },
				shape = "circle",
				size = { value = 49 }
			}
		}
	}
	if chartvis.from then symbolmarks.from = copy(chartvis.from) end

	return symbolmarks
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 ~= "0"
		}
		if xAxisAngle then
			local xAxisAlign
			if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end
			xAxis.properties =
			{
				title =
				{
					fill = { value = "#54595d" }
				},
				labels =
				{
					angle = { value = xAxisAngle },
					align = { value = xAxisAlign },
					fill = { value = "#54595d" }
				},
				ticks =
				{
					stroke = { value = "#54595d" }
				},
				axis =
				{
					stroke = { value = "#54595d" },
					strokeWidth = { value = 2 }
				}
			}
		else
			xAxis.properties =
			{
				title =
				{
					fill = { value = "#54595d" }
				},
				labels =
				{
					fill = { value = "#54595d" }
				},
				ticks =
				{
					stroke = { value = "#54595d" }
				},
				axis =
				{
					stroke = { value = "#54595d" },
					strokeWidth = { value = 2 }
				}
			}
		end

		if yType == "integer" and not yAxisFormat then yAxisFormat = "d" end
		yAxis =
		{
			type = "y",
			scale = "y",
			title = yTitle,
			format = yAxisFormat,
			grid = yGrid ~= "0"
		}
		yAxis.properties =
		{
			title =
			{
				fill = { value = "#54595d" }
			},
			labels =
			{
				fill = { value = "#54595d" }
			},
			ticks =
			{
				stroke = { value = "#54595d" }
			},
			axis =
			{
				stroke = { value = "#54595d" },
				strokeWidth = { value = 2 }
			},
			grid =
			{
				stroke = { value = "#54595d" }
			}
		}
	end

	return xAxis, yAxis
end

local function getLegend(legendTitle, chartType, outerRadius)
	local legend =
	{
		fill = "color",
		stroke = "color",
		title = legendTitle,
	}
	if chartType == "pie" then
		-- move legend from center position to top
		legend.properties = { legend = { y = { value = -outerRadius } } }
	end
	return legend
end

function p.chart(frame)
	-- chart width and height
	local graphwidth = tonumber(frame.args.width) or 200
	local graphheight = tonumber(frame.args.height) or 200
	-- chart type
	local chartType = frame.args.type or "line"
	-- interpolation mode for line and area charts: linear, step-before, step-after, basis, basis-open, basis-closed (type=line only), bundle (type=line only), cardinal, cardinal-open, cardinal-closed (type=line only), monotone
	local interpolate = frame.args.interpolate
	-- mark colors (if no colors are given, the default 10 color palette is used)
	local colorString = frame.args.colors
	if colorString then colorString = string.lower(colorString) end
	local colors = stringArray(colorString)
	-- for line charts, the thickness of the line; for pie charts the gap between each slice
	local linewidth = tonumber(frame.args.linewidth)
	-- x and y axis caption
	local xTitle = frame.args.xAxisTitle
	local yTitle = frame.args.yAxisTitle
	-- x and y value types
	local xType = frame.args.xType
	local yType = frame.args.yType
	-- override x and y axis minimum and maximum
	local xMin = frame.args.xAxisMin
	local xMax = frame.args.xAxisMax
	local yMin = frame.args.yAxisMin
	local yMax = frame.args.yAxisMax
	-- override x and y axis label formatting
	local xAxisFormat = frame.args.xAxisFormat
	local yAxisFormat = frame.args.yAxisFormat
	local xAxisAngle = tonumber(frame.args.xAxisAngle)
	-- show grid
	local xGrid = frame.args.xGrid or "0"
	local yGrid = frame.args.yGrid or "0"
	-- for line chart, show a symbol at each data point
	local showSymbols = frame.args.showSymbols
	-- show legend with given title
	local legendTitle = frame.args.legend
	-- show values as text
	local showValues = frame.args.showValues
	-- pie chart radiuses
	local innerRadius = tonumber(frame.args.innerRadius) or 0
	local outerRadius = math.min(graphwidth, graphheight)
	-- format JSON output
	local formatJson = frame.args.formatjson

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

	-- get y values (series)
	local yValues = {}
	local seriesTitles = {}
	for name, value in pairs(frame.args) do
		local yNum
		if name == "y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end
		if yNum then
			yValues[yNum] = value
			-- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters.
			seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or 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

	-- create scales
	local scales = {}

	local xscale = getXScale(chartType, stacked, xMin, xMax, xType)
	table.insert(scales, xscale)
	local yscale = getYScale(chartType, stacked, yMin, yMax, yType)
	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 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, 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

	-- symbol marks
	if chartType == "line" and showSymbols then
		local chartmarks = chartvis
		if chartmarks.marks then chartmarks = chartmarks.marks[1] end
		local symbolmarks = getSymbolMarks(chartmarks)
		if chartmarks ~= chartvis then
			table.insert(chartvis.marks, symbolmarks)
		else
			table.insert(marks, symbolmarks)
		end
	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, stats },
		scales = scales,
		axes = { xAxis, yAxis },
		marks = marks,
		legends = { legend }
	}

	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

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

return p