Skillnad mellan versioner av "Modul:Graph"
Från wpu.nu
mediawiki>AKlapper (WMF) m (5 revisions imported from meta:Module:Graph: See phab:T231001) |
(v an h-line annotations, fix for bug Template_talk:Graph:Chart#Repeated y-values - special thanks to User:Timbaaa) |
||
Rad 1: | Rad 1: | ||
− | -- ATTENTION: | + | |
− | -- | + | -- 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_): | -- 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 15: | Rad 44: | ||
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 36: | Rad 80: | ||
end | end | ||
end | end | ||
− | + | ||
+ | return result, isInteger | ||
end | end | ||
Rad 63: | Rad 108: | ||
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 " | + | local basemap = frame.args.basemap or "Template:Graph:Map/Inner/Worldmap2c-json" -- WorldMap name and/or location may vary from wiki to wiki |
-- scaling factor | -- scaling factor | ||
local scale = tonumber(frame.args.scale) or 100 | local scale = tonumber(frame.args.scale) or 100 | ||
Rad 69: | Rad 114: | ||
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 | + | local defaultValue = frame.args.defaultValue or frame.args.defaultvalue |
− | local scaleType = frame.args.scaleType or "linear" | + | local scaleType = frame.args.scaleType or 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) | + | local domainMin = tonumber(frame.args.domainMin or frame.args.domainmin) |
-- maximaler Wertebereich (nur für numerische Daten) | -- maximaler Wertebereich (nur für numerische Daten) | ||
− | local domainMax = tonumber(frame.args.domainMax) | + | local domainMax = tonumber(frame.args.domainMax or 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 "category10" | + | local colorScale = frame.args.colorScale or 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 143: | Rad 192: | ||
} | } | ||
end | end | ||
− | + | ||
-- get map url | -- get map url | ||
local basemapUrl | local basemapUrl | ||
Rad 168: | Rad 217: | ||
{ | { | ||
-- data source for map paths data | -- data source for map paths data | ||
− | name = | + | name = feature, |
url = basemapUrl, | url = basemapUrl, | ||
− | format = { type = "topojson", feature = | + | format = { type = "topojson", feature = feature }, |
transform = | transform = | ||
{ | { | ||
Rad 178: | Rad 227: | ||
value = "data", -- data source | value = "data", -- data source | ||
scale = scale, | scale = scale, | ||
− | + | translate = { 0, 0 }, | |
+ | center = center, | ||
projection = projection | projection = projection | ||
}, | }, | ||
Rad 198: | Rad 248: | ||
{ | { | ||
type = "path", | type = "path", | ||
− | from = { data = | + | from = { data = feature }, |
properties = | properties = | ||
{ | { | ||
Rad 239: | Rad 289: | ||
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 311: | Rad 360: | ||
end | end | ||
− | local function getXScale(chartType, stacked, xMin, xMax, xType) | + | local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType) |
if chartType == "pie" then return end | if chartType == "pie" then return end | ||
Rad 317: | Rad 366: | ||
{ | { | ||
name = "x", | name = "x", | ||
− | |||
range = "width", | range = "width", | ||
zero = false, -- do not include zero value | zero = false, -- do not include zero value | ||
− | |||
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 332: | Rad 380: | ||
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 xscale.type = "time" | + | if xType == "date" then |
+ | xscale.type = "time" | ||
elseif xType == "string" then | elseif xType == "string" then | ||
xscale.type = "ordinal" | xscale.type = "ordinal" | ||
Rad 339: | Rad 388: | ||
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) | + | local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) |
if chartType == "pie" then return end | if chartType == "pie" then return end | ||
Rad 349: | Rad 398: | ||
{ | { | ||
name = "y", | name = "y", | ||
− | + | --type = yScaleType or "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 = | + | 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 yMin then yscale.domainMin = yMin end | ||
if yMax then yscale.domainMax = yMax end | if yMax then yscale.domainMax = yMax end | ||
Rad 405: | Rad 455: | ||
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 463: | Rad 553: | ||
end | end | ||
− | local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, interpolate) | + | 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 | if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end | ||
Rad 482: | Rad 572: | ||
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 522: | Rad 620: | ||
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 584: | Rad 689: | ||
else | else | ||
properties.align.value = "left" | properties.align.value = "left" | ||
− | properties.fill.value = showValues.fontcolor or | + | properties.fill.value = showValues.fontcolor or persistentGrey |
end | end | ||
elseif chartType == "pie" then | elseif chartType == "pie" then | ||
Rad 593: | Rad 698: | ||
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 | + | fill = { value = showValues.fontcolor or persistentGrey }, |
baseline = { }, | baseline = { }, | ||
angle = { }, | angle = { }, | ||
Rad 652: | Rad 757: | ||
end | end | ||
− | local function getSymbolMarks(chartvis) | + | local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale) |
− | local symbolmarks = | + | |
+ | local symbolmarks | ||
+ | symbolmarks = | ||
{ | { | ||
type = "symbol", | type = "symbol", | ||
Rad 662: | Rad 769: | ||
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" }, | ||
− | |||
− | |||
} | } | ||
} | } | ||
} | } | ||
+ | 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 | ||
+ | end | ||
− | return | + | 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 683: | Rad 887: | ||
title = xTitle, | title = xTitle, | ||
format = xAxisFormat, | format = xAxisFormat, | ||
− | grid = xGrid | + | grid = xGrid |
} | } | ||
if xAxisAngle then | if xAxisAngle then | ||
Rad 692: | Rad 896: | ||
title = | title = | ||
{ | { | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
labels = | labels = | ||
Rad 698: | Rad 902: | ||
angle = { value = xAxisAngle }, | angle = { value = xAxisAngle }, | ||
align = { value = xAxisAlign }, | align = { value = xAxisAlign }, | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
ticks = | ticks = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey } |
}, | }, | ||
axis = | axis = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey }, |
strokeWidth = { value = 2 } | strokeWidth = { value = 2 } | ||
+ | }, | ||
+ | grid = | ||
+ | { | ||
+ | stroke = { value = persistentGrey } | ||
} | } | ||
} | } | ||
Rad 715: | Rad 923: | ||
title = | title = | ||
{ | { | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
labels = | labels = | ||
{ | { | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
ticks = | ticks = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey } |
}, | }, | ||
axis = | axis = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey }, |
strokeWidth = { value = 2 } | strokeWidth = { value = 2 } | ||
+ | }, | ||
+ | grid = | ||
+ | { | ||
+ | stroke = { value = persistentGrey } | ||
} | } | ||
} | } | ||
Rad 740: | Rad 952: | ||
title = yTitle, | title = yTitle, | ||
format = yAxisFormat, | format = yAxisFormat, | ||
− | grid = yGrid | + | grid = yGrid |
} | } | ||
yAxis.properties = | yAxis.properties = | ||
Rad 746: | Rad 958: | ||
title = | title = | ||
{ | { | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
labels = | labels = | ||
{ | { | ||
− | fill = { value = | + | fill = { value = persistentGrey } |
}, | }, | ||
ticks = | ticks = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey } |
}, | }, | ||
axis = | axis = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey }, |
strokeWidth = { value = 2 } | strokeWidth = { value = 2 } | ||
}, | }, | ||
grid = | grid = | ||
{ | { | ||
− | stroke = { value = | + | stroke = { value = persistentGrey } |
} | } | ||
} | } | ||
+ | |||
end | end | ||
Rad 777: | Rad 990: | ||
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 | ||
− | -- move legend from center position to top | + | legend.properties = { |
− | + | -- move legend from center position to top | |
+ | legend = { | ||
+ | y = { value = -outerRadius }, | ||
+ | }, | ||
+ | title = { | ||
+ | fill = { value = persistentGrey } | ||
+ | }, | ||
+ | labels = { | ||
+ | fill = { value = persistentGrey }, | ||
+ | }, | ||
+ | } | ||
end | end | ||
return legend | return legend | ||
Rad 799: | Rad 1 030: | ||
-- 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 | + | local xTitle = frame.args.xAxisTitle or frame.args.xaxistitle |
− | local yTitle = frame.args.yAxisTitle | + | local yTitle = frame.args.yAxisTitle or frame.args.yaxistitle |
-- x and y value types | -- x and y value types | ||
− | local xType = frame.args.xType | + | local xType = frame.args.xType or frame.args.xtype |
− | local yType = frame.args.yType | + | local yType = frame.args.yType or frame.args.ytype |
-- override x and y axis minimum and maximum | -- override x and y axis minimum and maximum | ||
− | local xMin = frame.args.xAxisMin | + | local xMin = frame.args.xAxisMin or frame.args.xaxismin |
− | local xMax = frame.args.xAxisMax | + | local xMax = frame.args.xAxisMax or frame.args.xaxismax |
− | local yMin = frame.args.yAxisMin | + | local yMin = frame.args.yAxisMin or frame.args.yaxismin |
− | local yMax = frame.args.yAxisMax | + | local yMax = frame.args.yAxisMax or frame.args.yaxismax |
-- override x and y axis label formatting | -- override x and y axis label formatting | ||
− | local xAxisFormat = frame.args.xAxisFormat | + | local xAxisFormat = frame.args.xAxisFormat or frame.args.xaxisformat |
− | local yAxisFormat = frame.args.yAxisFormat | + | local yAxisFormat = frame.args.yAxisFormat or frame.args.yaxisformat |
− | local xAxisAngle = tonumber(frame.args.xAxisAngle) | + | local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle) |
+ | -- x and y scale types | ||
+ | local xScaleType = frame.args.xScaleType or frame.args.xscaletype | ||
+ | local yScaleType = frame.args.yScaleType or frame.args.yscaletype | ||
+ | -- log scale require minimum > 0, for now it's no possible to plot negative values on log - TODO see: https://www.mathworks.com/matlabcentral/answers/1792-log-scale-graphic-with-negative-value | ||
+ | -- if xScaleType == "log" then | ||
+ | -- if (not xMin or tonumber(xMin) <= 0) then xMin = 0.1 end | ||
+ | -- if not xType then xType = "number" end | ||
+ | -- end | ||
+ | -- if yScaleType == "log" then | ||
+ | -- if (not yMin or tonumber(yMin) <= 0) then yMin = 0.1 end | ||
+ | -- if not yType then yType = "number" end | ||
+ | -- end | ||
+ | |||
-- show grid | -- show grid | ||
− | local xGrid = frame.args.xGrid or | + | local xGrid = frame.args.xGrid or frame.args.xgrid or false |
− | local yGrid = frame.args.yGrid or | + | local yGrid = frame.args.yGrid or frame.args.ygrid or false |
-- 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 | + | local showSymbols = frame.args.showSymbols or frame.args.showsymbols |
+ | local symbolsShape = frame.args.symbolsShape or frame.args.symbolsshape | ||
+ | local symbolsNoFill = frame.args.symbolsNoFill or frame.args.symbolsnofill | ||
+ | local symbolsStroke = tonumber(frame.args.symbolsStroke or frame.args.symbolsstroke) | ||
-- show legend with given title | -- 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 | + | local showValues = frame.args.showValues or frame.args.showvalues |
+ | -- show v- and h-line annotations | ||
+ | local v_annoLineString = frame.args.vAnnotatonsLine or frame.args.vannotatonsline | ||
+ | local h_annoLineString = frame.args.hAnnotatonsLine or frame.args.hannotatonsline | ||
+ | local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel | ||
+ | local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | -- decode annotations cvs | ||
+ | local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel | ||
+ | if v_annoLineString and v_annoLineString ~= "" then | ||
+ | |||
+ | if xType == "number" or xType == "integer" then | ||
+ | v_annoLine = numericArray(v_annoLineString) | ||
+ | |||
+ | else | ||
+ | v_annoLine = stringArray(v_annoLineString) | ||
+ | |||
+ | end | ||
+ | v_annoLabel = stringArray(v_annoLabelString) | ||
+ | end | ||
+ | if h_annoLineString and h_annoLineString ~= "" then | ||
+ | |||
+ | if yType == "number" or yType == "integer" then | ||
+ | h_annoLine = numericArray(h_annoLineString) | ||
+ | |||
+ | else | ||
+ | h_annoLine = stringArray(h_annoLineString) | ||
+ | |||
+ | end | ||
+ | h_annoLabel = stringArray(h_annoLabelString) | ||
+ | end | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
-- pie chart radiuses | -- pie chart radiuses | ||
− | local innerRadius = tonumber(frame.args.innerRadius) or 0 | + | local innerRadius = tonumber(frame.args.innerRadius) or 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 842: | Rad 1 131: | ||
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 name | + | seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or frame.args["y" .. yNum .. "title"] or name |
end | end | ||
end | end | ||
Rad 878: | Rad 1 167: | ||
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) | + | local xscale = getXScale(chartType, stacked, xMin, xMax, xType, xScaleType) |
table.insert(scales, xscale) | table.insert(scales, xscale) | ||
− | local yscale = getYScale(chartType, stacked, yMin, yMax, yType) | + | local yscale = getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) |
table.insert(scales, yscale) | table.insert(scales, yscale) | ||
Rad 892: | Rad 1 200: | ||
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 902: | Rad 1 216: | ||
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, interpolate) | + | local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) |
local marks = { chartvis } | local marks = { chartvis } | ||
Rad 927: | Rad 1 243: | ||
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 chartType | + | if showSymbols and chartType ~= "rect" 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 938: | Rad 1 359: | ||
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 953: | Rad 1 384: | ||
width = graphwidth, | width = graphwidth, | ||
height = graphheight, | height = graphheight, | ||
− | data = { data | + | data = { data }, |
scales = scales, | scales = scales, | ||
axes = { xAxis, yAxis }, | axes = { xAxis, yAxis }, | ||
Rad 959: | Rad 1 390: | ||
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 972: | Rad 1 406: | ||
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 1 september 2020 kl. 17.51
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! -- -- BUGS: X-Axis label format bug? (xAxisFormat =) https://en.wikipedia.org/wiki/Template_talk:Graph:Chart#X-Axis_label_format_bug?_(xAxisFormat_=) -- linewidths - doesnt work for two values (eg 0, 1) but work if added third value of both are zeros? Same for marksStroke - probably bug in Graph extension -- clamp - "clamp" used to avoid marks outside marks area, "clip" should be use instead but not working in Graph extension, see https://phabricator.wikimedia.org/T251709 -- TODO: -- marks: -- - line strokeDash + serialization, -- - symStroke serialization -- - symbolsNoFill serialization -- - arbitrary SVG path symbol shape as symbolsShape argument -- - annotations -- - vertical / horizontal line at specific values [DONE] 2020-09-01 -- - rectangle shape for x,y data range -- - graph type serialization (deep rebuild reqired) -- - second axis (deep rebuild required - assignment of series to one of two axies) -- Version History (_PLEASE UPDATE when modifying anything_): -- 2020-09-01 Vertical and horizontal line annotations -- 2020-08-08 New logic for "nice" for x axis (problem with scale when xType = "date") and grid -- 2020-06-21 Serializes symbol size -- transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line) -- Linewidth serialized with "linewidths" -- Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0 -- p.chartDebuger(frame) for easy debug and JSON output -- 2020-06-07 Allow lowercase variables for use with [[Template:Wikidata list]] -- 2020-05-27 Map: allow specification which feature to display and changing the map center -- 2020-04-08 Change default showValues.fontcolor from black to persistentGrey -- 2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true -- 2020-03-11 Allow user-defined scale types, e.g. logarithmic scale -- 2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid -- 2019-01-24 Allow comma-separated lists to contain values with commas -- 2018-10-13 Fix browser color-inversion issues via #54595d per [[mw:Template:Graph:PageViews]] -- 2018-09-16 Allow disabling the legend for templates -- 2018-09-10 Allow grid lines -- 2018-08-26 Use user-defined order for stacked charts -- 2018-02-11 Force usage of explicitely provided x minimum and/or maximum values, rotation of x labels -- 2017-08-08 Added showSymbols param to show symbols on line charts -- 2016-05-16 Added encodeTitleForPath() to help all path-based APIs graphs like pageviews -- 2016-03-20 Allow omitted data for charts, labels for line charts with string (ordinal) scale at point location -- 2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon. local p = {} --add debug text to this string with eg. debuglog = debuglog .. "" .. "\n\n" .. "- " .. debug.traceback() .. "result type: ".. type(result) .. " result: \n\n" .. mw.dumpObject(result) --invoke chartDebuger() to get graph JSON and this string debuglog = "Debug " .. "\n\n" local baseMapDirectory = "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) if not csv then return end local list = mw.text.split(csv, "%s*,%s*") local result = {} local isInteger = true for i = 1, #list do if list[i] == "" then result[i] = nil else result[i] = tonumber(list[i]) if not result[i] then return end if isInteger then local int, frac = math.modf(result[i]) isInteger = frac == 0.0 end end end return result, isInteger end local function stringArray(text) if not text then return end local list = mw.text.split(mw.ustring.gsub(tostring(text), "\\,", "<COMMA>"), ",", true) for i = 1, #list do list[i] = mw.ustring.gsub(mw.text.trim(list[i]), "<COMMA>", ",") end return list end local function isTable(t) return type(t) == "table" end local function copy(x) if type(x) == "table" then local result = {} for key, value in pairs(x) do result[key] = copy(value) end return result else return x end end function p.map(frame) -- map path data for geographic objects local basemap = frame.args.basemap or "Template:Graph:Map/Inner/Worldmap2c-json" -- WorldMap name and/or location may vary from wiki to wiki -- scaling factor local scale = tonumber(frame.args.scale) or 100 -- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections local projection = frame.args.projection or "equirectangular" -- defaultValue for geographic objects without data local defaultValue = frame.args.defaultValue or frame.args.defaultvalue local scaleType = frame.args.scaleType or frame.args.scaletype or "linear" -- minimaler Wertebereich (nur für numerische Daten) local domainMin = tonumber(frame.args.domainMin or frame.args.domainmin) -- maximaler Wertebereich (nur für numerische Daten) local domainMax = tonumber(frame.args.domainMax or frame.args.domainmax) -- Farbwerte der Farbskala (nur für numerische Daten) local colorScale = frame.args.colorScale or frame.args.colorscale or "category10" -- show legend local legend = frame.args.legend -- the map feature to display local feature = frame.args.feature or "countries" -- map center local center = numericArray(frame.args.center) -- format JSON output local formatJson = frame.args.formatjson -- map data are key-value pairs: keys are non-lowercase strings (ideally ISO codes) which need to match the "id" values of the map path data local values = {} local isNumbers = nil for name, value in pairs(frame.args) do if mw.ustring.find(name, "^[^%l]+$") and value and value ~= "" then if isNumbers == nil then isNumbers = tonumber(value) end local data = { id = name, v = value } if isNumbers then data.v = tonumber(data.v) end table.insert(values, data) end end if not defaultValue then if isNumbers then defaultValue = 0 else defaultValue = "silver" end end -- create highlight scale local scales if isNumbers then if colorScale then colorScale = string.lower(colorScale) end if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end scales = { { name = "color", type = scaleType, domain = { data = "highlights", field = "v" }, range = colorScale, nice = true, zero = false } } if domainMin then scales[1].domainMin = domainMin end if domainMax then scales[1].domainMax = domainMax end local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent if exponent then scales[1].type = "pow" scales[1].exponent = exponent end end -- create legend if legend then legend = { { fill = "color", offset = 120, properties = { title = { fontSize = { value = 14 } }, labels = { fontSize = { value = 12 } }, legend = { stroke = { value = "silver" }, strokeWidth = { value = 1.5 } } } } } end -- get map url local basemapUrl if (string.sub(basemap, 1, 10) == "wikiraw://") then basemapUrl = basemap else -- if not a (supported) url look for a colon as namespace separator. If none prepend default map directory name. if not string.find(basemap, ":") then basemap = baseMapDirectory .. basemap end basemapUrl = "wikiraw:///" .. mw.uri.encode(mw.title.new(basemap).prefixedText, "PATH") end local output = { version = 2, width = 1, -- generic value as output size depends solely on map size and scaling factor height = 1, -- ditto data = { { -- data source for the highlights name = "highlights", values = values }, { -- data source for map paths data name = feature, url = basemapUrl, format = { type = "topojson", feature = feature }, transform = { { -- geographic transformation ("geopath") of map paths data type = "geopath", value = "data", -- data source scale = scale, translate = { 0, 0 }, center = center, projection = projection }, { -- join ("zip") of mutiple data source: here map paths data and highlights type = "lookup", keys = { "id" }, -- key for map paths data on = "highlights", -- name of highlight data source onKey = "id", -- key for highlight data source as = { "zipped" }, -- name of resulting table default = { v = defaultValue } -- default value for geographic objects that could not be joined } } } }, marks = { -- output markings (map paths and highlights) { type = "path", from = { data = feature }, properties = { enter = { path = { field = "layout_path" } }, update = { fill = { field = "zipped.v" } }, hover = { fill = { value = "darkgrey" } } } } }, legends = legend } if (scales) then output.scales = scales output.marks[1].properties.update.fill.scale = "color" end local flags if formatJson then flags = mw.text.JSON_PRETTY end return mw.text.jsonEncode(output, flags) end local function deserializeXData(serializedX, xType, xMin, xMax) local x if not xType or xType == "integer" or xType == "number" then local isInteger x, isInteger = numericArray(serializedX) if x then xMin = tonumber(xMin) xMax = tonumber(xMax) if not xType then if isInteger then xType = "integer" else xType = "number" end end else if xType then error("Numbers expected for parameter 'x'") end end end if not x then x = stringArray(serializedX) if not xType then xType = "string" end end return x, xType, xMin, xMax end local function deserializeYData(serializedYs, yType, yMin, yMax) local y = {} local areAllInteger = true for yNum, value in pairs(serializedYs) do local yValues if not yType or yType == "integer" or yType == "number" then local isInteger yValues, isInteger = numericArray(value) if yValues then areAllInteger = areAllInteger and isInteger else if yType then error("Numbers expected for parameter '" .. name .. "'") else return deserializeYData(serializedYs, "string", yMin, yMax) end end end if not yValues then yValues = stringArray(value) end y[yNum] = yValues end if not yType then if areAllInteger then yType = "integer" else yType = "number" end end if yType == "integer" or yType == "number" then yMin = tonumber(yMin) yMax = tonumber(yMax) end return y, yType, yMin, yMax end local function convertXYToManySeries(x, y, xType, yType, seriesTitles) local data = { name = "chart", format = { type = "json", parse = { x = xType, y = yType } }, values = {} } for i = 1, #y do local yLen = table.maxn(y[i]) for j = 1, #x do if j <= yLen and y[i][j] then table.insert(data.values, { series = seriesTitles[i], x = x[j], y = y[i][j] }) end end end return data end local function convertXYToSingleSeries(x, y, xType, yType, yNames) local data = { name = "chart", format = { type = "json", parse = { x = xType } }, values = {} } for j = 1, #y do data.format.parse[yNames[j]] = yType end for i = 1, #x do local item = { x = x[i] } for j = 1, #y do item[yNames[j]] = y[j][i] end table.insert(data.values, item) end return data end local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType) if chartType == "pie" then return end local xscale = { name = "x", range = "width", zero = false, -- do not include zero value domain = { data = "chart", field = "x" } } if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end if xMin then xscale.domainMin = xMin end if xMax then xscale.domainMax = xMax end if xMin or xMax then xscale.clamp = true xscale.nice = false end if chartType == "rect" then xscale.type = "ordinal" if not stacked then xscale.padding = 0.2 end -- pad each bar group else if xType == "date" then xscale.type = "time" elseif xType == "string" then xscale.type = "ordinal" xscale.points = true end end if xType and xType ~= "date" and xScaleType ~= "log" then xscale.nice = true end -- force round numbers for x scale, but "log" and "date" scale outputs a wrong "nice" scale return xscale end local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) if chartType == "pie" then return end local yscale = { name = "y", --type = yScaleType or "linear", range = "height", -- area charts have the lower boundary of their filling at y=0 (see marks.properties.enter.y2), therefore these need to start at zero zero = chartType ~= "line", nice = yScaleType ~= "log" -- force round numbers for y scale, but log scale outputs a wrong "nice" scale } if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end if yMin then yscale.domainMin = yMin end if yMax then yscale.domainMax = yMax end if yMin or yMax then yscale.clamp = true end if yType == "date" then yscale.type = "time" elseif yType == "string" then yscale.type = "ordinal" end if stacked then yscale.domain = { data = "stats", field = "sum_y" } else yscale.domain = { data = "chart", field = "y" } end return yscale end local function getColorScale(colors, chartType, xCount, yCount) if not colors then if (chartType == "pie" and xCount > 10) or yCount > 10 then colors = "category20" else colors = "category10" end end local colorScale = { name = "color", type = "ordinal", range = colors, domain = { data = "chart", field = "series" } } if chartType == "pie" then colorScale.domain.field = "x" end return colorScale end local function getAlphaColorScale(colors, y) local alphaScale -- if there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale if isTable(colors) then local alphas = {} local hasAlpha = false for i = 1, #colors do local a, rgb = string.match(colors[i], "#(%x%x)(%x%x%x%x%x%x)") if a then hasAlpha = true alphas[i] = tostring(tonumber(a, 16) / 255.0) colors[i] = "#" .. rgb else alphas[i] = "1" end end for i = #colors + 1, #y do alphas[i] = "1" end if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end end return alphaScale end local function getLineScale(linewidths, chartType) local lineScale = {} lineScale = { name = "line", type = "ordinal", range = linewidths, domain = { data = "chart", field = "series" } } return lineScale end local function getSymSizeScale(symSize) local SymSizeScale = {} SymSizeScale = { name = "symSize", type = "ordinal", range = symSize, domain = { data = "chart", field = "series" } } return SymSizeScale end local function getSymShapeScale(symShape) local SymShapeScale = {} SymShapeScale = { name = "symShape", type = "ordinal", range = symShape, domain = { data = "chart", field = "series" } } return SymShapeScale end local function getValueScale(fieldName, min, max, type) local valueScale = { name = fieldName, type = type or "linear", domain = { data = "chart", field = fieldName }, range = { min, max } } return valueScale end local function addInteractionToChartVisualisation(plotMarks, colorField, dataField) -- initial setup if not plotMarks.properties.enter then plotMarks.properties.enter = {} end plotMarks.properties.enter[colorField] = { scale = "color", field = dataField } -- action when cursor is over plot mark: highlight if not plotMarks.properties.hover then plotMarks.properties.hover = {} end plotMarks.properties.hover[colorField] = { value = "red" } -- action when cursor leaves plot mark: reset to initial setup if not plotMarks.properties.update then plotMarks.properties.update = {} end plotMarks.properties.update[colorField] = { scale = "color", field = dataField } end local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) local chartvis = { type = "arc", from = { data = "chart", transform = { { field = "y", type = "pie" } } }, properties = { enter = { innerRadius = { value = innerRadius }, outerRadius = { }, startAngle = { field = "layout_start" }, endAngle = { field = "layout_end" }, stroke = { value = "white" }, strokeWidth = { value = linewidth or 1 } } } } if radiusScale then chartvis.properties.enter.outerRadius.scale = radiusScale.name chartvis.properties.enter.outerRadius.field = radiusScale.domain.field else chartvis.properties.enter.outerRadius.value = outerRadius end addInteractionToChartVisualisation(chartvis, "fill", "x") return chartvis end local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end local chartvis = { type = chartType, properties = { -- chart creation event handler enter = { x = { scale = "x", field = "x" }, y = { scale = "y", field = "y" } } } } addInteractionToChartVisualisation(chartvis, colorField, "series") if colorField == "stroke" then chartvis.properties.enter.strokeWidth = { value = linewidth or 2.5 } if type(lineScale) =="table" then chartvis.properties.enter.strokeWidth.value = nil chartvis.properties.enter.strokeWidth = { scale = "line", field= "series" } end end if interpolate then chartvis.properties.enter.interpolate = { value = interpolate } end if alphaScale then chartvis.properties.update[colorField .. "Opacity"] = { scale = "transparency" } end -- for bars and area charts set the lower bound of their areas if chartType == "rect" or chartType == "area" then if stacked then -- for stacked charts this lower bound is the end of the last stacking element chartvis.properties.enter.y2 = { scale = "y", field = "layout_end" } else --[[ for non-stacking charts the lower bound is y=0 TODO: "yscale.zero" is currently set to "true" for this case, but "false" for all other cases. For the similar behavior "y2" should actually be set to where y axis crosses the x axis, if there are only positive or negative values in the data ]] chartvis.properties.enter.y2 = { scale = "y", value = 0 } end end -- for bar charts ... if chartType == "rect" then -- set 1 pixel width between the bars chartvis.properties.enter.width = { scale = "x", band = true, offset = -1 } -- for multiple series the bar marking needs to use the "inner" series scale, whereas the "outer" x scale is used by the grouping if not stacked and yCount > 1 then chartvis.properties.enter.x.scale = "series" chartvis.properties.enter.x.field = "series" chartvis.properties.enter.width.scale = "series" end end -- stacked charts have their own (stacked) y values if stacked then chartvis.properties.enter.y.field = "layout_start" end -- if there are multiple series group these together if yCount == 1 then chartvis.from = { data = "chart" } else -- if there are multiple series, connect colors to series chartvis.properties.update[colorField].field = "series" if alphaScale then chartvis.properties.update[colorField .. "Opacity"].field = "series" end -- if there are multiple series, connect linewidths to series if chartype == "line" then chartvis.properties.update["strokeWidth"].field = "series" end -- apply a grouping (facetting) transformation chartvis = { type = "group", marks = { chartvis }, from = { data = "chart", transform = { { type = "facet", groupby = { "series" } } } } } -- for stacked charts apply a stacking transformation if stacked then table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } ) else -- for bar charts the series are side-by-side grouped by x if chartType == "rect" then -- for bar charts with multiple series: each serie is grouped by the x value, therefore the series need their own scale within each x group local groupScale = { name = "series", type = "ordinal", range = "width", domain = { field = "series" } } chartvis.from.transform[1].groupby = "x" chartvis.scales = { groupScale } chartvis.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } } end end end return chartvis end local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues) local properties if chartType == "rect" then properties = { x = { scale = chartvis.properties.enter.x.scale, field = chartvis.properties.enter.x.field }, y = { scale = chartvis.properties.enter.y.scale, field = chartvis.properties.enter.y.field, offset = -(tonumber(showValues.offset) or -4) }, --dx = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text dy = { scale = chartvis.properties.enter.x.scale, band = true, mult = 0.5 }, -- for vertical text align = { }, baseline = { value = "middle" }, fill = { }, angle = { value = -90 }, fontSize = { value = tonumber(showValues.fontsize) or 11 } } if properties.y.offset >= 0 then properties.align.value = "right" properties.fill.value = showValues.fontcolor or "white" else properties.align.value = "left" properties.fill.value = showValues.fontcolor or persistentGrey end elseif chartType == "pie" then properties = { x = { group = "width", mult = 0.5 }, y = { group = "height", mult = 0.5 }, radius = { offset = tonumber(showValues.offset) or -4 }, theta = { field = "layout_mid" }, fill = { value = showValues.fontcolor or persistentGrey }, baseline = { }, angle = { }, fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) } } if (showValues.angle or "midangle") == "midangle" then properties.align = { value = "center" } properties.angle = { field = "layout_mid", mult = 180.0 / math.pi } if properties.radius.offset >= 0 then properties.baseline.value = "bottom" else if not showValues.fontcolor then properties.fill.value = "white" end properties.baseline.value = "top" end elseif tonumber(showValues.angle) then -- qunatize scale for aligning text left on right half-circle and right on left half-circle local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "left", "right" } } table.insert(scales, alignScale) properties.align = { scale = alignScale.name, field = "layout_mid" } properties.angle = { value = tonumber(showValues.angle) } properties.baseline.value = "middle" if not tonumber(showValues.offset) then properties.radius.offset = 4 end end if radiusScale then properties.radius.scale = radiusScale.name properties.radius.field = radiusScale.domain.field else properties.radius.value = outerRadius end end if properties then if showValues.format then local template = "datum.y" if yType == "integer" or yType == "number" then template = template .. "|number:'" .. showValues.format .. "'" elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'" end properties.text = { template = "{{" .. template .. "}}" } else properties.text = { field = "y" } end local textmarks = { type = "text", properties = { enter = properties } } if chartvis.from then textmarks.from = copy(chartvis.from) end return textmarks end end local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale) local symbolmarks symbolmarks = { type = "symbol", properties = { enter = { x = { scale = "x", field = "x" }, y = { scale = "y", field = "y" }, strokeWidth = { value = symStroke }, stroke = { scale = "color", field = "series" }, fill = { scale = "color", field = "series" }, } } } if type(symShape) == "string" then symbolmarks.properties.enter.shape = { value = symShape } end if type(symShape) == "table" then symbolmarks.properties.enter.shape = { scale = "symShape", field = "series" } end if type(symSize) == "number" then symbolmarks.properties.enter.size = { value = symSize } end if type(symSize) == "table" then symbolmarks.properties.enter.size = { scale = "symSize", field = "series" } end if noFill then symbolmarks.properties.enter.fill = nil end if alphaScale then symbolmarks.properties.enter.fillOpacity = { scale = "transparency", field = "series" } symbolmarks.properties.enter.strokeOpacity = { scale = "transparency", field = "series" } end if chartvis.from then symbolmarks.from = copy(chartvis.from) end return symbolmarks end local function getAnnoMarks(chartvis, stroke, fill, opacity) local vannolines, hannolines, vannoLabels, vannoLabels vannolines = { type = "rule", from = { data = "v_anno" }, properties = { update = { x = { scale = "x", field = "x" }, y = { value = 0 }, y2 = { field = { group = "height" } }, strokeWidth = { value = stroke }, stroke = { value = persistentGrey }, opacity = { value = opacity } } } } vannolabels = { type = "text", from = { data = "v_anno" }, properties = { update = { x = { scale = "x", field = "x", offset = 3 }, y = { field = { group = "height" }, offset = -3 }, text = { field = "label" }, baseline = { value = "top" }, angle = { value = -90 }, fill = { value = persistentGrey }, opacity = { value = opacity } } } } hannolines = { type = "rule", from = { data = "h_anno" }, properties = { update = { y = { scale = "y", field = "y" }, x = { value = 0 }, x2 = { field = { group = "width" } }, strokeWidth = { value = stroke }, stroke = { value = persistentGrey }, opacity = { value = opacity } } } } hannolabels = { type = "text", from = { data = "h_anno" }, properties = { update = { y = { scale = "y", field = "y", offset = 3 }, x = { value = 0 , offset = 3 }, text = { field = "label" }, baseline = { value = "top" }, angle = { value = 0 }, fill = { value = persistentGrey }, opacity = { value = opacity } } } } return vannolines, vannolabels, hannolines, hannolabels end local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) local xAxis, yAxis if chartType ~= "pie" then if xType == "integer" and not xAxisFormat then xAxisFormat = "d" end xAxis = { type = "x", scale = "x", title = xTitle, format = xAxisFormat, grid = xGrid } if xAxisAngle then local xAxisAlign if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end xAxis.properties = { title = { fill = { value = persistentGrey } }, labels = { angle = { value = xAxisAngle }, align = { value = xAxisAlign }, fill = { value = persistentGrey } }, ticks = { stroke = { value = persistentGrey } }, axis = { stroke = { value = persistentGrey }, strokeWidth = { value = 2 } }, grid = { stroke = { value = persistentGrey } } } else xAxis.properties = { title = { fill = { value = persistentGrey } }, labels = { fill = { value = persistentGrey } }, ticks = { stroke = { value = persistentGrey } }, axis = { stroke = { value = persistentGrey }, strokeWidth = { value = 2 } }, grid = { stroke = { value = persistentGrey } } } end if yType == "integer" and not yAxisFormat then yAxisFormat = "d" end yAxis = { type = "y", scale = "y", title = yTitle, format = yAxisFormat, grid = yGrid } yAxis.properties = { title = { fill = { value = persistentGrey } }, labels = { fill = { value = persistentGrey } }, ticks = { stroke = { value = persistentGrey } }, axis = { stroke = { value = persistentGrey }, strokeWidth = { value = 2 } }, grid = { stroke = { value = persistentGrey } } } end return xAxis, yAxis end local function getLegend(legendTitle, chartType, outerRadius) local legend = { fill = "color", stroke = "color", title = legendTitle, } legend.properties = { title = { fill = { value = persistentGrey }, }, labels = { fill = { value = persistentGrey }, }, } if chartType == "pie" then legend.properties = { -- move legend from center position to top legend = { y = { value = -outerRadius }, }, title = { fill = { value = persistentGrey } }, labels = { fill = { value = persistentGrey }, }, } end return legend end function p.chart(frame) -- chart width and height local graphwidth = tonumber(frame.args.width) or 200 local graphheight = tonumber(frame.args.height) or 200 -- chart type local chartType = frame.args.type or "line" -- interpolation mode for line and area charts: linear, step-before, step-after, basis, basis-open, basis-closed (type=line only), bundle (type=line only), cardinal, cardinal-open, cardinal-closed (type=line only), monotone local interpolate = frame.args.interpolate -- mark colors (if no colors are given, the default 10 color palette is used) local colorString = frame.args.colors if colorString then colorString = string.lower(colorString) end local colors = stringArray(colorString) -- for line charts, the thickness of the line; for pie charts the gap between each slice local linewidth = tonumber(frame.args.linewidth) local linewidthsString = frame.args.linewidths local linewidths if linewidthsString and linewidthsString ~= "" then linewidths = numericArray(linewidthsString) or false end -- x and y axis caption local xTitle = frame.args.xAxisTitle or frame.args.xaxistitle local yTitle = frame.args.yAxisTitle or frame.args.yaxistitle -- x and y value types local xType = frame.args.xType or frame.args.xtype local yType = frame.args.yType or frame.args.ytype -- override x and y axis minimum and maximum local xMin = frame.args.xAxisMin or frame.args.xaxismin local xMax = frame.args.xAxisMax or frame.args.xaxismax local yMin = frame.args.yAxisMin or frame.args.yaxismin local yMax = frame.args.yAxisMax or frame.args.yaxismax -- override x and y axis label formatting local xAxisFormat = frame.args.xAxisFormat or frame.args.xaxisformat local yAxisFormat = frame.args.yAxisFormat or frame.args.yaxisformat local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle) -- x and y scale types local xScaleType = frame.args.xScaleType or frame.args.xscaletype local yScaleType = frame.args.yScaleType or frame.args.yscaletype -- log scale require minimum > 0, for now it's no possible to plot negative values on log - TODO see: https://www.mathworks.com/matlabcentral/answers/1792-log-scale-graphic-with-negative-value -- if xScaleType == "log" then -- if (not xMin or tonumber(xMin) <= 0) then xMin = 0.1 end -- if not xType then xType = "number" end -- end -- if yScaleType == "log" then -- if (not yMin or tonumber(yMin) <= 0) then yMin = 0.1 end -- if not yType then yType = "number" end -- end -- show grid local xGrid = frame.args.xGrid or frame.args.xgrid or false local yGrid = frame.args.yGrid or frame.args.ygrid or false -- for line chart, show a symbol at each data point local showSymbols = frame.args.showSymbols or frame.args.showsymbols local symbolsShape = frame.args.symbolsShape or frame.args.symbolsshape local symbolsNoFill = frame.args.symbolsNoFill or frame.args.symbolsnofill local symbolsStroke = tonumber(frame.args.symbolsStroke or frame.args.symbolsstroke) -- show legend with given title local legendTitle = frame.args.legend -- show values as text local showValues = frame.args.showValues or frame.args.showvalues -- show v- and h-line annotations local v_annoLineString = frame.args.vAnnotatonsLine or frame.args.vannotatonsline local h_annoLineString = frame.args.hAnnotatonsLine or frame.args.hannotatonsline local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel -- decode annotations cvs local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel if v_annoLineString and v_annoLineString ~= "" then if xType == "number" or xType == "integer" then v_annoLine = numericArray(v_annoLineString) else v_annoLine = stringArray(v_annoLineString) end v_annoLabel = stringArray(v_annoLabelString) end if h_annoLineString and h_annoLineString ~= "" then if yType == "number" or yType == "integer" then h_annoLine = numericArray(h_annoLineString) else h_annoLine = stringArray(h_annoLineString) end h_annoLabel = stringArray(h_annoLabelString) end -- pie chart radiuses local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0 local outerRadius = math.min(graphwidth, graphheight) -- format JSON output local formatJson = frame.args.formatjson -- get x values local x x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax) -- get y values (series) local yValues = {} local seriesTitles = {} for name, value in pairs(frame.args) do local yNum if name == "y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end if yNum then yValues[yNum] = value -- name the series: default is "y<number>". Can be overwritten using the "y<number>Title" parameters. seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or frame.args["y" .. yNum .. "title"] or name end end local y y, yType, yMin, yMax = deserializeYData(yValues, yType, yMin, yMax) -- create data tuples, consisting of series index, x value, y value local data if chartType == "pie" then -- for pie charts the second second series is merged into the first series as radius values data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" }) else data = convertXYToManySeries(x, y, xType, yType, seriesTitles) end -- configure stacked charts local stacked = false local stats if string.sub(chartType, 1, 7) == "stacked" then chartType = string.sub(chartType, 8) if #y > 1 then -- ignore stacked charts if there is only one series stacked = true -- aggregate data by cumulative y values stats = { name = "stats", source = "chart", transform = { { type = "aggregate", groupby = { "x" }, summarize = { y = "sum" } } } } end end -- add annotations to data local vannoData, hannoData if v_annoLine then vannoData = { name = "v_anno", format = { type = "json", parse = { x = xType } }, values = {} } for i = 1, #v_annoLine do local item = { x = v_annoLine[i], label = v_annoLabel[i] } table.insert(vannoData.values, item) end end if h_annoLine then hannoData = { name = "h_anno", format = { type = "json", parse = { y = yType } }, values = {} } for i = 1, #h_annoLine do local item = { y = h_annoLine[i], label = h_annoLabel[i] } table.insert(hannoData.values, item) end end -- create scales local scales = {} local xscale = getXScale(chartType, stacked, xMin, xMax, xType, xScaleType) table.insert(scales, xscale) local yscale = getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) table.insert(scales, yscale) local colorScale = getColorScale(colors, chartType, #x, #y) table.insert(scales, colorScale) local alphaScale = getAlphaColorScale(colors, y) table.insert(scales, alphaScale) local lineScale if (linewidths) and (chartType == "line") then lineScale = getLineScale(linewidths, chartType) table.insert(scales, lineScale) end local radiusScale if chartType == "pie" and #y > 1 then radiusScale = getValueScale("r", 0, outerRadius) table.insert(scales, radiusScale) end -- decide if lines (strokes) or areas (fills) should be drawn local colorField if chartType == "line" then colorField = "stroke" else colorField = "fill" end -- create chart markings local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) local marks = { chartvis } -- text marks if showValues then if type(showValues) == "string" then -- deserialize as table local keyValues = mw.text.split(showValues, "%s*,%s*") showValues = {} for _, kv in ipairs(keyValues) do local key, value = mw.ustring.match(kv, "^%s*(.-)%s*:%s*(.-)%s*$") if key then showValues[key] = value end end end local chartmarks = chartvis if chartmarks.marks then chartmarks = chartmarks.marks[1] end local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues) if chartmarks ~= chartvis then table.insert(chartvis.marks, textmarks) else table.insert(marks, textmarks) end end -- grids if xGrid then if xGrid == "0" then xGrid = false elseif xGrid == 0 then xGrid = false elseif xGrid == "false" then xGrid = false elseif xGrid == "n" then xGrid = false else xGrid = true end end if yGrid then if yGrid == "0" then yGrid = false elseif yGrid == 0 then yGrid = false elseif yGrid == "false" then yGrid = false elseif yGrid == "n" then yGrid = false else yGrid = true end end -- symbol marks if showSymbols and chartType ~= "rect" then local chartmarks = chartvis if chartmarks.marks then chartmarks = chartmarks.marks[1] end if type(showSymbols) == "string" then if showSymbols == "" then showSymbols = true else showSymbols = numericArray(showSymbols) end else showSymbols = tonumber(showSymbols) end -- custom size local symSize if type(showSymbols) == "number" then symSize = tonumber(showSymbols*showSymbols*8.5) elseif type(showSymbols) == "table" then symSize = {} for k, v in pairs(showSymbols) do symSize[k]=v*v*8.5 -- "size" acc to Vega syntax is area of symbol end else symSize = 50 end -- symSizeScale local symSizeScale = {} if type(symSize) == "table" then symSizeScale = getSymSizeScale(symSize) table.insert(scales, symSizeScale) end -- custom shape if stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end local symShape = " " if type(symbolsShape) == "string" and shapes[symbolsShape] then symShape = shapes[symbolsShape] elseif type(symbolsShape) == "table" then symShape = {} for k, v in pairs(symbolsShape) do if symbolsShape[k] and shapes[symbolsShape[k]] then symShape[k]=shapes[symbolsShape[k]] else symShape[k] = "circle" end end else symShape = "circle" end -- symShapeScale local symShapeScale = {} if type(symShape) == "table" then symShapeScale = getSymShapeScale(symShape) table.insert(scales, symShapeScale) end -- custom stroke local symStroke if (type(symbolsStroke) == "number") then symStroke = tonumber(symbolsStroke) -- TODO symStroke serialization -- elseif type(symbolsStroke) == "table" then -- symStroke = {} -- for k, v in pairs(symbolsStroke) do -- symStroke[k]=symbolsStroke[k] -- --always draw x with stroke -- if symbolsShape[k] == "x" then symStroke[k] = 2.5 end --always draw x with stroke -- if symbolsNoFill[k] then symStroke[k] = 2.5 end -- end else symStroke = 0 --always draw x with stroke if symbolsShape == "x" then symStroke = 2.5 end --always draw x with stroke if symbolsNoFill then symStroke = 2.5 end end -- TODO -- symStrokeScale -- local symStrokeScale = {} -- if type(symStroke) == "table" then -- symStrokeScale = getSymStrokeScale(symStroke) -- table.insert(scales, symStrokeScale) -- end local symbolmarks = getSymbolMarks(chartmarks, symSize, symShape, symStroke, symbolsNoFill, alphaScale) if chartmarks ~= chartvis then table.insert(chartvis.marks, symbolmarks) else table.insert(marks, symbolmarks) end end local vannolines, vannolabels, hannolines, hannolabels = getAnnoMarks(chartmarks, persistentGrey, persistentGrey, 0.75) if vannoData then table.insert(marks, vannolines) table.insert(marks, vannolabels) end if hannoData then table.insert(marks, hannolines) table.insert(marks, hannolabels) end -- axes local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) -- legend local legend if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end -- construct final output object local output = { version = 2, width = graphwidth, height = graphheight, data = { data }, scales = scales, axes = { xAxis, yAxis }, marks = marks, legends = { legend } } if vannoData then table.insert(output.data, vannoData) end if hannoData then table.insert(output.data, hannoData) end if stats then table.insert(output.data, stats) end local flags if formatJson then flags = mw.text.JSON_PRETTY end return mw.text.jsonEncode(output, flags) end function p.mapWrapper(frame) return p.map(frame:getParent()) end function p.chartWrapper(frame) return p.chart(frame:getParent()) end function p.chartDebuger(frame) return "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog end -- Given an HTML-encoded title as first argument, e.g. one produced with {{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