יחידה:Graph – הבדלי גרסאות
חלוקת קונטרסים (שיחה | תרומות) מויקיפדיה: https://he.wikipedia.org/wiki/%D7%99%D7%97%D7%99%D7%93%D7%94:Graph |
עדכון מוויקיפדיה |
||
| שורה 1: | שורה 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 | |||
-- Reordering even strings like integers - see https://en.wikipedia.org/wiki/Template_talk:Graph:Chart#Reordering_even_strings_like_integers | |||
-- 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. | -- 2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon. | ||
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) | ||
if not csv then return end | if not csv then return end | ||
local list = mw.text.split(csv, "%s*,%s*") | local list = mw.text.split(csv, "%s*,%s*") | ||
local result = {} | local result = {} | ||
local isInteger = true | local isInteger = true | ||
for i = 1, #list do | for i = 1, #list do | ||
if list[i] == "" then result[i] = nil else | if list[i] == "" then | ||
result[i] = nil | |||
else | |||
result[i] = tonumber(list[i]) | result[i] = tonumber(list[i]) | ||
if not result[i] then return end | if not result[i] then return end | ||
| שורה 28: | שורה 79: | ||
end | end | ||
end | end | ||
return result, isInteger | |||
end | end | ||
local function stringArray( | local function stringArray(text) | ||
if not | 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 | end | ||
| שורה 51: | שורה 107: | ||
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 "WorldMap-iso2.json" | local basemap = frame.args.basemap or "WorldMap-iso2.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 | ||
| שורה 57: | שורה 113: | ||
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 | ||
| שורה 74: | שורה 134: | ||
local isNumbers = nil | local isNumbers = nil | ||
for name, value in pairs(frame.args) do | for name, value in pairs(frame.args) do | ||
if mw.ustring.find(name, "^[^%l]+$") then | if mw.ustring.find(name, "^[^%l]+$") and value and value ~= "" then | ||
if isNumbers == nil then isNumbers = tonumber(value) end | if isNumbers == nil then isNumbers = tonumber(value) end | ||
local data = { id = name, v = value } | local data = { id = name, v = value } | ||
| שורה 88: | שורה 148: | ||
local scales | local scales | ||
if isNumbers then | if isNumbers then | ||
if colorScale then colorScale = string.lower(colorScale) end | |||
if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end | if colorScale == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end | ||
scales = | scales = | ||
| שורה 96: | שורה 157: | ||
domain = { data = "highlights", field = "v" }, | domain = { data = "highlights", field = "v" }, | ||
range = colorScale, | range = colorScale, | ||
nice = true | nice = true, | ||
zero = false | |||
} | } | ||
} | } | ||
| שורה 129: | שורה 191: | ||
} | } | ||
end | end | ||
-- get map url | -- get map url | ||
local basemapUrl | local basemapUrl | ||
| שורה 154: | שורה 216: | ||
{ | { | ||
-- 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 = | ||
{ | { | ||
| שורה 164: | שורה 226: | ||
value = "data", -- data source | value = "data", -- data source | ||
scale = scale, | scale = scale, | ||
translate = { 0, 0 }, | |||
center = center, | |||
projection = projection | projection = projection | ||
}, | }, | ||
| שורה 184: | שורה 247: | ||
{ | { | ||
type = "path", | type = "path", | ||
from = { data = | from = { data = feature }, | ||
properties = | properties = | ||
{ | { | ||
| שורה 225: | שורה 288: | ||
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 | ||
| שורה 297: | שורה 359: | ||
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 | ||
| שורה 303: | שורה 365: | ||
{ | { | ||
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 | ||
if xMin or xMax then xscale.clamp = true end | if xMin or xMax then | ||
xscale.clamp = true | |||
xscale.nice = false | |||
end | |||
if chartType == "rect" then | if chartType == "rect" then | ||
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" | ||
| שורה 322: | שורה 387: | ||
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 | ||
| שורה 332: | שורה 397: | ||
{ | { | ||
name = "y", | name = "y", | ||
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 | ||
| שורה 388: | שורה 453: | ||
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 | ||
| שורה 446: | שורה 551: | ||
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 | ||
| שורה 465: | שורה 570: | ||
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 | ||
| שורה 505: | שורה 618: | ||
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 = | ||
| שורה 524: | שורה 644: | ||
-- for stacked charts apply a stacking transformation | -- for stacked charts apply a stacking transformation | ||
if stacked then | if stacked then | ||
table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { " | table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } ) | ||
else | else | ||
-- for bar charts the series are side-by-side grouped by x | -- for bar charts the series are side-by-side grouped by x | ||
| שורה 567: | שורה 687: | ||
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 | ||
| שורה 576: | שורה 696: | ||
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 = { }, | ||
| שורה 635: | שורה 755: | ||
end | end | ||
local function getAxes(xTitle, xAxisFormat, xType, yTitle, yAxisFormat, yType, chartType) | 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 | local xAxis, yAxis | ||
if chartType ~= "pie" then | if chartType ~= "pie" then | ||
| שורה 644: | שורה 888: | ||
scale = "x", | scale = "x", | ||
title = xTitle, | title = xTitle, | ||
format = xAxisFormat | 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 | if yType == "integer" and not yAxisFormat then yAxisFormat = "d" end | ||
| שורה 653: | שורה 953: | ||
scale = "y", | scale = "y", | ||
title = yTitle, | title = yTitle, | ||
format = yAxisFormat | 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 | end | ||
| שורה 666: | שורה 992: | ||
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 | ||
| שורה 683: | שורה 1,027: | ||
local interpolate = frame.args.interpolate | local interpolate = frame.args.interpolate | ||
-- mark colors (if no colors are given, the default 10 color palette is used) | -- mark colors (if no colors are given, the default 10 color palette is used) | ||
local | 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 | -- 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) 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 | -- 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 | ||
| שורה 723: | שורה 1,133: | ||
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 | ||
| שורה 759: | שורה 1,169: | ||
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) | ||
| שורה 773: | שורה 1,202: | ||
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 | ||
| שורה 783: | שורה 1,218: | ||
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 } | |||
-- text marks | -- text marks | ||
if showValues then | if showValues then | ||
if type(showValues) == "string" then -- deserialize as table | 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 | end | ||
local chartmarks = chartvis | local chartmarks = chartvis | ||
if chartmarks.marks then chartmarks = chartmarks.marks[1] end | if chartmarks.marks then chartmarks = chartmarks.marks[1] end | ||
textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues) | local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues) | ||
if chartmarks ~= chartvis then | if chartmarks ~= chartvis then | ||
table.insert(chartvis.marks, textmarks) | table.insert(chartvis.marks, textmarks) | ||
textmarks | else | ||
table.insert(marks, textmarks) | |||
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 | |||
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 | -- axes | ||
local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xType, yTitle, yAxisFormat, yType, chartType) | local xAxis, yAxis = getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) | ||
-- legend | -- legend | ||
local legend | local legend | ||
if legendTitle 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 = | ||
| שורה 821: | שורה 1,387: | ||
width = graphwidth, | width = graphwidth, | ||
height = graphheight, | height = graphheight, | ||
data = { data | data = { data }, | ||
scales = scales, | scales = scales, | ||
axes = { xAxis, yAxis }, | axes = { xAxis, yAxis }, | ||
marks = | marks = marks, | ||
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 | ||
if formatJson then flags = mw.text.JSON_PRETTY end | if formatJson then flags = mw.text.JSON_PRETTY end | ||
| שורה 840: | שורה 1,409: | ||
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}}, | ||