-- 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 -- 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 -- 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 stringdebuglog = "Debug " .. "\n\n"
local baseMapDirectory = "Module:Graph/"local persistentGrey = "#54595d"
local shapes = shapes =
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]
0.0 end end end
return result, isIntegerend
local function stringArray(text) if not text then return end
local list = mw.text.split(mw.ustring.gsub(tostring(text), "\\,", "
local function isTable(t) return type(t)
local function copy(x) if type(x)
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
-- create highlight scale local scales if isNumbers then if colorScale then colorScale = string.lower(colorScale) end if colorScale ~= "category10" and colorScale ~= "category20" then colorScale = stringArray(colorScale) end scales = if domainMin then scales[1].domainMin = domainMin end if domainMax then scales[1].domainMax = domainMax end
local exponent = scaleType:match("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 = end -- get map url local basemapUrl if (basemap:sub(1, 10)
local output = 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
"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 elseif xType then error("Numbers expected for parameter 'x'") end end if not x then x = stringArray(serializedX) xType = xType or "string" end return x, xType, xMin, xMaxend
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
"number" then local isInteger yValues, isInteger = numericArray(value) if yValues then areAllInteger = areAllInteger and isInteger elseif yType then error("Numbers expected for parameter '" .. name .. "'") else return deserializeYData(serializedYs, "string", yMin, yMax) end end yValues = yValues or stringArray(value)
y[yNum] = yValues end if not yType then if areAllInteger then yType = "integer" else yType = "number" end end if yType
"number" then yMin = tonumber(yMin) yMax = tonumber(yMax) end
return y, yType, yMin, yMaxend
local function convertXYToManySeries(x, y, xType, yType, seriesTitles) local data = 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,) end end end return dataend
local function convertXYToSingleSeries(x, y, xType, yType, yNames) local data =
for j = 1, #y do data.format.parse[yNames[j]] = yType end
for i = 1, #x do local item = for j = 1, #y do item[yNames[j]] = y[j][i] end
table.insert(data.values, item) end return dataend
local function getXScale(chartType, stacked, xMin, xMax, xType, xScaleType) if chartType
local xscale = 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
"date" then xscale.type = "time" elseif xType
local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) if chartType
local yscale = 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
"string" then yscale.type = "ordinal" end if stacked then yscale.domain = else yscale.domain = end
return yscaleend
local function getColorScale(colors, chartType, xCount, yCount) if not colors then if (chartType
local colorScale = if chartType
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 = colors[i]:match("#(%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 = end end return alphaScaleend
local function getLineScale(linewidths, chartType) local lineScale = lineScale =
return lineScaleend
local function getSymSizeScale(symSize) local SymSizeScale = SymSizeScale =
return SymSizeScaleend
local function getSymShapeScale(symShape) local SymShapeScale = SymShapeScale =
return SymShapeScaleend
local function getValueScale(fieldName, min, max, type) local valueScale = return valueScaleend
local function addInteractionToChartVisualisation(plotMarks, colorField, dataField) -- initial setup plotMarks.properties.enter = plotMarks.properties.enter or plotMarks.properties.enter[colorField] =
-- action when cursor is over plot mark: highlight plotMarks.properties.hover = plotMarks.properties.hover or plotMarks.properties.hover[colorField] =
-- action when cursor leaves plot mark: reset to initial setup plotMarks.properties.update = plotMarks.properties.update or plotMarks.properties.update[colorField] = end
local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) local chartvis =
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 chartvisend
local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) if chartType
local chartvis = addInteractionToChartVisualisation(chartvis, colorField, "series") if colorField
"table" then chartvis.properties.enter.strokeWidth.value = nil chartvis.properties.enter.strokeWidth = end end
if interpolate then chartvis.properties.enter.interpolate = end
if alphaScale then chartvis.properties.update[colorField .. "Opacity"] = end -- for bars and area charts set the lower bound of their areas if chartType
"area" then if stacked then -- for stacked charts this lower bound is the end of the last stacking element chartvis.properties.enter.y2 = else -- chartvis.properties.enter.y2 = end end -- for bar charts ... if chartType
-- if there are multiple series group these together if yCount
"line" then chartvis.properties.update["strokeWidth"].field = "series" end
-- apply a grouping (facetting) transformation chartvis = -- for stacked charts apply a stacking transformation if stacked then table.insert(chartvis.from.transform, 1,) else -- for bar charts the series are side-by-side grouped by x if chartType
chartvis.from.transform[1].groupby = "x" chartvis.scales = chartvis.properties = end end end
return chartvisend
local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues) local properties if chartType
"pie" then properties = if (showValues.angle or "midangle")
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 = table.insert(scales, alignScale)
properties.align = properties.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
"number" then template = template .. "|number:'" .. showValues.format .. "'" elseif yType
local textmarks = if chartvis.from then textmarks.from = copy(chartvis.from) end
return textmarks endend
local function getSymbolMarks(chartvis, symSize, symShape, symStroke, noFill, alphaScale)
local symbolmarks symbolmarks = if type(symShape)
"table" then symbolmarks.properties.enter.shape = end if type(symSize)
"table" then symbolmarks.properties.enter.size = end if noFill then symbolmarks.properties.enter.fill = nil end if alphaScale then symbolmarks.properties.enter.fillOpacity = symbolmarks.properties.enter.strokeOpacity = end if chartvis.from then symbolmarks.from = copy(chartvis.from) end return symbolmarksend
local function getAnnoMarks(chartvis, stroke, fill, opacity)
local vannolines, hannolines, vannoLabels, vannoLabels vannolines = vannolabels = hannolines = hannolabels = return vannolines, vannolabels, hannolines, hannolabelsend
local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) local xAxis, yAxis if chartType ~= "pie" then if xType
if yType
return xAxis, yAxisend
local function getLegend(legendTitle, chartType, outerRadius) local legend = legend.properties = if chartType
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" mw.log('test') mw.log(chartType) -- 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 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
"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
"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
-- create data tuples, consisting of series index, x value, y value local data if chartType
-- configure stacked charts local stacked = false local stats if chartType:sub(1, 7)
-- 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
local radiusScale if chartType
-- decide if lines (strokes) or areas (fills) should be drawn local colorField if chartType
-- create chart markings local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) local marks = -- text marks if showValues then if type(showValues)
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 xGrid = not (xGrid
0 or xGrid
"n") end if yGrid then yGrid = not (yGrid
0 or yGrid
"n") end -- symbol marks if showSymbols and chartType ~= "rect" then local chartmarks = chartvis if chartmarks.marks then chartmarks = chartmarks.marks[1] end
if type(showSymbols)
"" then showSymbols = true else showSymbols = numericArray(showSymbols) end else showSymbols = tonumber(showSymbols) end
-- custom size local symSize if 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)
-- custom shape if stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end local symShape = " " if 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)
"number") then symStroke = tonumber(symbolsStroke)-- TODO symStroke serialization-- elseif type(symbolsStroke)
"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
-- TODO -- symStrokeScale -- local symStrokeScale = -- if type(symStroke)
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 = 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.chartWrapper2(frame) return p.chart(frame)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,-- convert it into a properly URL path-encoded string-- This function is critical for any graph that uses path-based APIs, e.g. PageViews graphfunction p.encodeTitleForPath(frame) return mw.uri.encode(mw.text.decode(mw.text.trim(frame.args[1])), 'PATH')end
return p