Module:Sandbox/pietrasagh/Graph explained

-- ATTENTION: Please edit this code at https://de.wikipedia.org/wiki/Modul:Graph-- This way all wiki languages can stay in sync. Thank you!---- Version History (_PLEASE UPDATE when modifying anything_):-- 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 socond of both zeros???-- TODO: -- marks:-- - line strokeDash + serialization,-- - symStroke serialization-- - symbolsNoFill serialization-- - arbitrary SVG path symbol shape as symbolsShape argument-- - annotations-- - vertical / horizontal line at specific values -- - rectangle shape for x,y data range-- - graph type serialization (deep rebuild reqired)-- - axis-- - second axis (deep rebuild required - assignment of series to one of two axies)-- - native time format depending on project language-- - if x axis text vertical format to middle of tick-- 2020-05-10 Serializes symbol size-- 2020-05-10 transparent symbosls (from line colour) - buggy-- 2020-05-10 Linewidth serialized with "linewidths"-- 2020-05-10 Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0-- 2020-05-10 p.chartDebuger(frame) for easy debug and JSON output -- 2020-04-08 Change default showValues.fontcolor from black to persistentGrey-- 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]

"" 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, isIntegerend

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

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

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 endend

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 local scaleType = frame.args.scaleType or "linear" -- minimaler Wertebereich (nur für numerische Daten) local domainMin = tonumber(frame.args.domainMin) -- maximaler Wertebereich (nur für numerische Daten) local domainMax = tonumber(frame.args.domainMax) -- Farbwerte der Farbskala (nur für numerische Daten) local colorScale = frame.args.colorScale or "category10" -- show legend local legend = frame.args.legend -- format JSON output local formatJson = frame.args.formatjson

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

nil then isNumbers = tonumber(value) end local data = 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 = 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 = 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 = 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, 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

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

"pie" then return end

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

"rect" then xscale.type = "ordinal" if not stacked then xscale.padding = 0.2 end -- pad each bar group else if xType

"date" then xscale.type = "time" elseif xType

"string" then xscale.type = "ordinal" xscale.points = true end end

return xscaleend

local function getYScale(chartType, stacked, yMin, yMax, yType, yScaleType) if chartType

"pie" then return end

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

"date" then yscale.type = "time" elseif 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

"pie" and xCount > 10) or yCount > 10 then colors = "category20" else colors = "category10" end end

local colorScale = if chartType

"pie" then colorScale.domain.field = "x" end return colorScaleend

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 = 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 if not plotMarks.properties.enter then plotMarks.properties.enter = end plotMarks.properties.enter[colorField] =

-- action when cursor is over plot mark: highlight if not plotMarks.properties.hover then plotMarks.properties.hover = end plotMarks.properties.hover[colorField] =

-- action when cursor leaves plot mark: reset to initial setup if not plotMarks.properties.update then plotMarks.properties.update = end 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

"pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end

local chartvis = addInteractionToChartVisualisation(chartvis, colorField, "series") if colorField

"stroke" then chartvis.properties.enter.strokeWidth = if type(lineScale)

"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

"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 = else -- chartvis.properties.enter.y2 = end end -- for bar charts ... if chartType

"rect" then -- set 1 pixel width between the bars chartvis.properties.enter.width = -- 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 = 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 -- test -- if there are multiple series, connect linewidths to series if chartype

"line" then chartvis.properties.update["strokeWidth"].field = "series" end -- test

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

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

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

"rect" then properties = 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 = if (showValues.angle or "midangle")

"midangle" then properties.align = properties.angle =

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

"integer" or yType

"number" then template = template .. "|number:'" .. showValues.format .. "'" elseif yType

"date" then template = template .. "|time:" .. showValues.format .. "'" end properties.text = else properties.text = end

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)

"string" then symbolmarks.properties.enter.shape = end if type(symShape)

"table" then symbolmarks.properties.enter.shape = end if type(symSize)

"number" then symbolmarks.properties.enter.size = 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 getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) local xAxis, yAxis if chartType ~= "pie" then if xType

"integer" then xAxisFormat = "d" end if not xAxisFormat then xAxisFormat = "d" end xAxis = if xAxisAngle then local xAxisAlign if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end xAxis.properties = else xAxis.properties = end

if yType

"integer" then yAxisFormat = "d" end if not yAxisFormat then yAxisFormat = "g" end yAxis = yAxis.properties = end

return xAxis, yAxisend

local function getLegend(legendTitle, chartType, outerRadius) local legend = legend.properties = if chartType

"pie" then legend.properties = end return legendend

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 then linewidths = numericArray(linewidthsString) end -- x and y axis caption local xTitle = frame.args.xAxisTitle local yTitle = frame.args.yAxisTitle -- x and y value types local xType = frame.args.xType local yType = frame.args.yType -- override x and y axis minimum and maximum local xMin = frame.args.xAxisMin local xMax = frame.args.xAxisMax local yMin = frame.args.yAxisMin local yMax = frame.args.yAxisMax -- override x and y axis label formatting local xAxisFormat = frame.args.xAxisFormat local yAxisFormat = frame.args.yAxisFormat local xAxisAngle = tonumber(frame.args.xAxisAngle) -- x and y scale types local xScaleType = frame.args.xScaleType local yScaleType = 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 false local yGrid = frame.args.yGrid or false -- for line chart, show a symbol at each data point local showSymbols = frame.args.showSymbols local symbolsShape = frame.args.symbolsShape local symbolsNoFill = frame.args.symbolsNoFill local symbolsStroke = tonumber(frame.args.symbolsStroke) -- show legend with given title local legendTitle = frame.args.legend -- show values as text local showValues = frame.args.showValues -- pie chart radiuses local innerRadius = tonumber(frame.args.innerRadius) or 0 local outerRadius = math.min(graphwidth, graphheight) -- format JSON output local formatJson = frame.args.formatjson

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

-- get y values (series) local yValues = local seriesTitles = for name, value in pairs(frame.args) do local yNum if name

"y" then yNum = 1 else yNum = tonumber(string.match(name, "^y(%d+)$")) end if yNum then yValues[yNum] = value -- name the series: default is "y". Can be overwritten using the "yTitle" parameters. seriesTitles[yNum] = frame.args["y" .. yNum .. "Title"] or name end end local y y, yType, yMin, yMax = deserializeYData(yValues, yType, yMin, yMax)

-- create data tuples, consisting of series index, x value, y value local data if chartType

"pie" then -- for pie charts the second second series is merged into the first series as radius values data = convertXYToSingleSeries(x, y, xType, yType,) 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 = 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 = -- text marks if showValues then if type(showValues)

"string" then -- deserialize as table local keyValues = mw.text.split(showValues, "%s*,%s*") showValues = for _, kv in ipairs(keyValues) do local key, value = mw.ustring.match(kv, "^%s*(.-)%s*:%s*(.-)%s*$") if key then showValues[key] = value end end end

local chartmarks = chartvis if chartmarks.marks then chartmarks = chartmarks.marks[1] end local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues) if chartmarks ~= chartvis then table.insert(chartvis.marks, textmarks) else table.insert(marks, textmarks) end end

-- symbol marks if showSymbols then local chartmarks = chartvis if chartmarks.marks then chartmarks = chartmarks.marks[1] end

if type(showSymbols)

"string" then showSymbols = numericArray(showSymbols) 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 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

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

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