편집 요약 없음 |
편집 요약 없음 |
||
1번째 줄: | 1번째 줄: | ||
-- ATTENTION: Please edit this code at https:// | |||
-- ATTENTION: Please edit this code at https://www.mediawiki.org/w/index.php?title=Module:Graph | |||
-- This way all wiki languages can stay in sync. Thank you! | -- This way all wiki languages can stay in sync. Thank you! | ||
-- Changes history and TODO's moved to end of script | |||
-- | |||
local p = {} | local p = {} | ||
47번째 줄: | 8번째 줄: | ||
--add debug text to this string with eg. debuglog = debuglog .. "" .. "\n\n" .. "- " .. debug.traceback() .. "result type: ".. type(result) .. " result: \n\n" .. mw.dumpObject(result) | --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 | --invoke chartDebuger() to get graph JSON and this string | ||
debuglog = "Debug " .. "\n\n" | local debuglog = "Debug " .. "\n\n" | ||
local baseMapDirectory = "Module:Graph/" | local baseMapDirectory = "Module:Graph/" | ||
103번째 줄: | 64번째 줄: | ||
return x | return x | ||
end | end | ||
end | end | ||
271번째 줄: | 69번째 줄: | ||
local x | local x | ||
if not xType | if not xType or xType == "number" then | ||
local isInteger | local isInteger | ||
x, isInteger = numericArray(serializedX) | x, isInteger = numericArray(serializedX) | ||
278번째 줄: | 76번째 줄: | ||
xMax = tonumber(xMax) | xMax = tonumber(xMax) | ||
if not xType then | if not xType then | ||
if isInteger then | if isInteger then xType = "number" end | ||
end | end | ||
else | else | ||
297번째 줄: | 95번째 줄: | ||
for yNum, value in pairs(serializedYs) do | for yNum, value in pairs(serializedYs) do | ||
local yValues | local yValues | ||
if not yType | if not yType or yType == "number" then | ||
local isInteger | local isInteger | ||
yValues, isInteger = numericArray(value) | yValues, isInteger = numericArray(value) | ||
315번째 줄: | 113번째 줄: | ||
end | end | ||
if not yType then | if not yType then | ||
if areAllInteger then | if areAllInteger then yType = "number" end | ||
end | end | ||
if | if yType == "number" then | ||
yMin = tonumber(yMin) | yMin = tonumber(yMin) | ||
yMax = tonumber(yMax) | yMax = tonumber(yMax) | ||
334번째 줄: | 132번째 줄: | ||
parse = { x = xType, y = yType } | parse = { x = xType, y = yType } | ||
}, | }, | ||
values = {} | values = {}, | ||
transform = {} | |||
} | } | ||
for i = 1, #y do | for i = 1, #y do | ||
local yLen = table.maxn(y[i]) | local yLen = table.maxn(y[i]) | ||
for j = 1, #x do | 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 | if j <= yLen and y[i][j] then table.insert(data.values, { series = seriesTitles[i], index = i , x = x[j], y = y[i][j] }) end | ||
end | end | ||
end | end | ||
346번째 줄: | 145번째 줄: | ||
local function convertXYToSingleSeries(x, y, xType, yType, yNames) | local function convertXYToSingleSeries(x, y, xType, yType, yNames) | ||
local data = { name = "chart", format = { type = "json", parse = { x = xType } }, values = {} } | local data = { | ||
name = "chart", | |||
format = { type = "json", parse = { x = xType } }, | |||
values = {}, | |||
transform = {} } | |||
for j = 1, #y do data.format.parse[yNames[j]] = yType end | for j = 1, #y do data.format.parse[yNames[j]] = yType end | ||
367번째 줄: | 170번째 줄: | ||
range = "width", | range = "width", | ||
zero = false, -- do not include zero value | zero = false, -- do not include zero value | ||
nice = true, | |||
domain = { data = "chart", field = "x" } | domain = { data = "chart", field = "x" } | ||
} | } | ||
if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end | if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end | ||
if xMin then xscale.domainMin = xMin end | if xMin then xscale.domainMin = tonumber(xMin) end | ||
if xMax then xscale.domainMax = xMax end | if xMax then xscale.domainMax = tonumber(xMax) end | ||
if xMin or xMax then | if xMin or xMax then | ||
xscale.clamp = true | xscale.clamp = true | ||
377번째 줄: | 181번째 줄: | ||
end | end | ||
if chartType == "rect" then | if chartType == "rect" then | ||
xscale.type = " | xscale.type = "band" | ||
xscale.zero = nil | |||
xscale.nice = nil | |||
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 | ||
398번째 줄: | 204번째 줄: | ||
name = "y", | name = "y", | ||
range = "height", | range = "height", | ||
-- area charts have the lower boundary of their filling at y=0 (see marks. | -- area charts have the lower boundary of their filling at y=0 (see marks.encode.enter.y2), therefore these need to start at zero | ||
zero = chartType ~= "line", | zero = chartType ~= "line", | ||
nice = | nice = true, | ||
} | } | ||
if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end | if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end | ||
if yMin then yscale.domainMin = yMin end | if yMin then yscale.domainMin = tonumber(yMin) end | ||
if yMax then yscale.domainMax = yMax end | if yMax then yscale.domainMax = tonumber(yMax) end | ||
if yMin or yMax then yscale.clamp = true end | if yMin or yMax then yscale.clamp = true end | ||
if yType == "date" then yscale.type = "time" | if yType == "date" then yscale.type = "time" | ||
elseif yType == "string" then yscale.type = "ordinal" end | elseif yType == "string" then yscale.type = "ordinal" end | ||
if stacked then | if stacked then | ||
yscale.domain = { data = " | yscale.domain = { data = "chart", field = "y1" } | ||
else | else | ||
yscale.domain = { data = "chart", field = "y" } | yscale.domain = { data = "chart", field = "y" } | ||
419번째 줄: | 226번째 줄: | ||
local function getColorScale(colors, chartType, xCount, yCount) | local function getColorScale(colors, chartType, xCount, yCount) | ||
if not colors then | if not colors then | ||
if (chartType == "pie" and xCount > 10) or yCount > 10 then colors = "category20" else colors = "category10" end | colors = {scheme = "category10"} | ||
if (chartType == "pie" and xCount > 10) or yCount > 10 then colors = {scheme = "category20"} else colors = {scheme = "category10"} end | |||
end | end | ||
508번째 줄: | 316번째 줄: | ||
local function addInteractionToChartVisualisation(plotMarks, colorField, dataField) | local function addInteractionToChartVisualisation(plotMarks, colorField, dataField) | ||
-- initial setup | -- initial setup | ||
if not plotMarks. | if not plotMarks.encode.enter then plotMarks.encode.enter = {} end | ||
plotMarks. | plotMarks.encode.enter[colorField] = { scale = "color", field = dataField } | ||
-- action when cursor is over plot mark: highlight | -- action when cursor is over plot mark: highlight | ||
if not plotMarks. | if not plotMarks.encode.hover then plotMarks.encode.hover = {} end | ||
plotMarks. | plotMarks.encode.hover[colorField] = { value = "red" } | ||
-- action when cursor leaves plot mark: reset to initial setup | -- action when cursor leaves plot mark: reset to initial setup | ||
if not plotMarks. | if not plotMarks.encode.update then plotMarks.encode.update = {} end | ||
plotMarks. | plotMarks.encode.update[colorField] = { scale = "color", field = dataField } | ||
end | end | ||
local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) | local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale, graphwidth, graphheight) | ||
local chartvis = | local chartvis = | ||
{ | { | ||
type = "arc", | type = "arc", | ||
from = { data = "chart" | from = { data = "chart"} , | ||
encode = | |||
{ | { enter = { | ||
x = { value = graphwidth / 2}, | |||
y = { value = graphheight / 2} | |||
}, | |||
update = { | |||
innerRadius = { value = innerRadius }, | innerRadius = { value = innerRadius }, | ||
outerRadius = { }, | outerRadius = { value = outerRadius }, | ||
startAngle = { field = "startAngle" }, | startAngle = { field = "startAngle" }, | ||
endAngle = { field = "endAngle" }, | endAngle = { field = "endAngle" }, | ||
540번째 줄: | 351번째 줄: | ||
if radiusScale then | if radiusScale then | ||
chartvis. | chartvis.encode.update.outerRadius.scale = radiusScale.name | ||
chartvis. | chartvis.encode.update.outerRadius.field = radiusScale.domain.field | ||
else | else | ||
chartvis. | chartvis.encode.update.outerRadius.value = outerRadius | ||
end | end | ||
551번째 줄: | 362번째 줄: | ||
end | end | ||
local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) | local function getChartVisualisation(chartType, stacked, colorField, yCount, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate, graphwidth, graphheight) | ||
if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale) end | if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale, graphwidth, graphheight) end | ||
local chartvis = | local chartvis = | ||
{ | { | ||
type = chartType, | type = chartType, | ||
encode = | |||
{ | { | ||
-- chart creation event handler | -- chart creation event handler | ||
569번째 줄: | 380번째 줄: | ||
addInteractionToChartVisualisation(chartvis, colorField, "series") | addInteractionToChartVisualisation(chartvis, colorField, "series") | ||
if colorField == "stroke" then | if colorField == "stroke" then | ||
chartvis. | chartvis.encode.enter.strokeWidth = { value = linewidth or 2.5 } | ||
if type(lineScale) =="table" then | if type(lineScale) =="table" then | ||
chartvis. | chartvis.encode.enter.strokeWidth.value = nil | ||
chartvis. | chartvis.encode.enter.strokeWidth = | ||
{ | { | ||
scale = "line", | scale = "line", | ||
580번째 줄: | 391번째 줄: | ||
end | end | ||
if interpolate then chartvis. | if interpolate then chartvis.encode.enter.interpolate = { value = interpolate } end | ||
if alphaScale then chartvis. | if alphaScale then chartvis.encode.update[colorField .. "Opacity"] = { scale = "transparency" } end | ||
-- for bars and area charts set the lower bound of their areas | -- for bars and area charts set the lower bound of their areas | ||
if chartType == "rect" or chartType == "area" then | if chartType == "rect" or chartType == "area" then | ||
if stacked then | if stacked then | ||
-- for stacked charts this lower bound is the end of the last stacking element | -- for stacked charts this lower bound is the end of the last stacking element | ||
chartvis. | chartvis.encode.enter.y2 = { scale = "y", field = "layout_end" } | ||
else | else | ||
--[[ | --[[ | ||
594번째 줄: | 405번째 줄: | ||
For the similar behavior "y2" should actually be set to where y axis crosses the x axis, | 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 ]] | if there are only positive or negative values in the data ]] | ||
chartvis. | chartvis.encode.enter.y2 = { scale = "y", value = 0 } | ||
end | end | ||
end | end | ||
600번째 줄: | 411번째 줄: | ||
if chartType == "rect" then | if chartType == "rect" then | ||
-- set 1 pixel width between the bars | -- set 1 pixel width between the bars | ||
chartvis. | chartvis.encode.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 | -- 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 | if not stacked and yCount > 1 then | ||
chartvis. | chartvis.encode.enter.x.scale = "x" | ||
chartvis. | chartvis.encode.enter.x.field = "x" | ||
chartvis. | chartvis.encode.enter.width.scale = "series" | ||
end | end | ||
end | end | ||
-- stacked charts have their own (stacked) y values | -- stacked charts have their own (stacked) y values | ||
if stacked then chartvis. | if stacked then chartvis.encode.enter.y.field = "layout_start" end | ||
-- if there are multiple series group these together | -- if there are multiple series group these together | ||
615번째 줄: | 426번째 줄: | ||
chartvis.from = { data = "chart" } | chartvis.from = { data = "chart" } | ||
else | else | ||
chartvis.from = { data = "facet" } | |||
-- if there are multiple series, connect colors to series | -- if there are multiple series, connect colors to series | ||
chartvis. | chartvis.encode.enter[colorField].field = "series" | ||
if alphaScale then chartvis. | if alphaScale then chartvis.encode.update[colorField .. "Opacity"].field = "series" end | ||
---- TODO check? if there are multiple series, connect linewidths to series | |||
-- if chartType == "line" then | |||
-- chartvis.encode.update.strokeWidth.field = "series" | |||
--end | |||
-- apply a grouping (facetting) transformation | -- apply a grouping (facetting) transformation | ||
chartvis = | chartvis = | ||
{ | { | ||
type = "group", | type = "group", | ||
from = | from = | ||
{ | { facet= | ||
{ | { | ||
data = "chart", | |||
name = "facet", | |||
groupby = "series" | |||
} | } | ||
} | }, | ||
marks = chartvis | |||
} | } | ||
-- for stacked charts apply a stacking transformation | -- for stacked charts apply a stacking transformation | ||
if stacked then | if stacked then -- TODO must check for non bar | ||
table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } ) | --table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } ) | ||
-- transform goes to data in vega 5 | |||
chartvis.marks.encode.enter.y = { | |||
scale = "y"; field = "y1" | |||
} | |||
chartvis.marks.encode.enter.y2 = { | |||
scale = "y"; field = "y0" | |||
} | |||
chartvis.marks = {chartvis.marks} | |||
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 | ||
if chartType == "rect" then | 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 | -- 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 | ||
chartvis.from.facet.groupby = "x" | |||
{ | chartvis.signals = {{name = "width", update = "bandwidth('x')"}} -- calculation or width for each group od bars | ||
name = " | chartvis.scales = {{ | ||
name = "facet_index", type = "band", range = "width", | |||
domain = { data = "facet", field = "series" }}} | |||
domain = { field = "series" } | chartvis.encode = {enter = {x = { scale = "x", field = "x"}}} | ||
chartvis.marks.encode.enter.x = { field = "series", scale = "facet_index" } | |||
chartvis.marks.encode.enter.width = { scale = "facet_index", band = true } | |||
chartvis. | chartvis.marks = {chartvis.marks} | ||
else chartvis.marks = {chartvis.marks} end | |||
chartvis. | |||
end | |||
end | end | ||
end | end | ||
667번째 줄: | 481번째 줄: | ||
end | end | ||
local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues) | local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues, graphwidth, graphheight) | ||
local | local encode | ||
if chartType == "rect" then | if chartType == "rect" then | ||
encode = | |||
{ | { | ||
x = { scale = chartvis. | x = { scale = chartvis.encode.enter.x.scale, field = chartvis.encode.enter.x.field }, | ||
y = { scale = chartvis. | y = { scale = chartvis.encode.enter.y.scale, field = chartvis.encode.enter.y.field, offset = -(tonumber(showValues.offset) or -4) }, | ||
--dx = { scale = chartvis. | --dx = { scale = chartvis.encode.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text | ||
dy = { scale = chartvis. | dy = { scale = chartvis.encode.enter.x.scale, band = true, mult = 0.5 }, -- for vertical text | ||
align = { }, | align = { }, | ||
baseline = { value = "middle" }, | baseline = { value = "middle" }, | ||
682번째 줄: | 496번째 줄: | ||
fontSize = { value = tonumber(showValues.fontsize) or 11 } | fontSize = { value = tonumber(showValues.fontsize) or 11 } | ||
} | } | ||
if | if encode.y.offset >= 0 then | ||
encode.align.value = "right" | |||
encode.fill.value = showValues.fontcolor or "white" | |||
else | else | ||
encode.align.value = "left" | |||
encode.fill.value = showValues.fontcolor or persistentGrey | |||
end | end | ||
elseif chartType == "pie" then | elseif chartType == "pie" then | ||
encode = | |||
{ | { | ||
x = { value = graphwidth / 2}, | |||
y = { value = graphheight / 2}, | |||
radius = { offset = tonumber(showValues.offset) or - | radius = { offset = tonumber(showValues.offset) or -15 }, | ||
theta = { | theta = {signal = "(datum.startAngle + datum.endAngle)/2"}, | ||
fill = { value = showValues.fontcolor or persistentGrey }, | fill = { value = showValues.fontcolor or persistentGrey }, | ||
baseline = { }, | baseline = { }, | ||
701번째 줄: | 515번째 줄: | ||
fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) } | fontSize = { value = tonumber(showValues.fontsize) or math.ceil(outerRadius / 10) } | ||
} | } | ||
if (showValues.angle or "midangle") == "midangle" then | if (showValues.angle or "midangle") == "midangle" then -- TODO check always true ? | ||
encode.align = { value = "center" } | |||
encode.angle = { field = "layout_mid", mult = 180.0 / math.pi } | |||
if | if encode.radius.offset >= 0 then | ||
encode.baseline.value = "bottom" | |||
else | else | ||
if not showValues.fontcolor then | if not showValues.fontcolor then encode.fill.value = "white" end | ||
encode.baseline.value = "middle" | |||
end | end | ||
elseif tonumber(showValues.angle) then | elseif tonumber(showValues.angle) then -- Todo check | ||
-- qunatize scale for aligning text left on right half-circle and right on left half-circle | -- 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" } } | local alignScale = { name = "align", type = "quantize", domainMin = 0.0, domainMax = math.pi * 2, range = { "left", "right" } } | ||
table.insert(scales, alignScale) | table.insert(scales, alignScale) | ||
encode.align = { scale = alignScale.name, field = "layout_mid" } | |||
encode.angle = { value = tonumber(showValues.angle) } | |||
encode.baseline.value = "middle" | |||
if not tonumber(showValues.offset) then | if not tonumber(showValues.offset) then encode.radius.offset = 4 end | ||
end | end | ||
if radiusScale then | if radiusScale then | ||
encode.radius.scale = radiusScale.name | |||
encode.radius.field = radiusScale.domain.field | |||
else | else | ||
encode.radius.value = outerRadius | |||
end | end | ||
end | end | ||
if | if encode then | ||
if showValues.format then | if showValues.format then | ||
local template = "datum.y" | local template = "datum.y" | ||
if | if yType == "number" then template = template .. "|number:'" .. showValues.format .. "'" | ||
elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'" | elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'" | ||
end | end | ||
encode.text = { template = "{{" .. template .. "}}" } | |||
else | else | ||
encode.text = { field = "y" } | |||
end | end | ||
744번째 줄: | 558번째 줄: | ||
{ | { | ||
type = "text", | type = "text", | ||
encode = | |||
{ | { | ||
enter = | enter = encode | ||
} | } | ||
} | } | ||
761번째 줄: | 575번째 줄: | ||
{ | { | ||
type = "symbol", | type = "symbol", | ||
encode = | |||
{ | { | ||
enter = | enter = | ||
774번째 줄: | 588번째 줄: | ||
} | } | ||
if type(symShape) == "string" then | if type(symShape) == "string" then | ||
symbolmarks. | symbolmarks.encode.enter.shape = { value = symShape } | ||
end | end | ||
if type(symShape) == "table" then | if type(symShape) == "table" then | ||
symbolmarks. | symbolmarks.encode.enter.shape = { scale = "symShape", field = "series" } | ||
end | end | ||
if type(symSize) == "number" then | if type(symSize) == "number" then | ||
symbolmarks. | symbolmarks.encode.enter.size = { value = symSize } | ||
end | end | ||
if type(symSize) == "table" then | if type(symSize) == "table" then | ||
symbolmarks. | symbolmarks.encode.enter.size = { scale = "symSize", field = "series" } | ||
end | end | ||
if noFill then | if noFill then | ||
symbolmarks. | symbolmarks.encode.enter.fill = nil | ||
end | end | ||
if alphaScale then | if alphaScale then | ||
symbolmarks. | symbolmarks.encode.enter.fillOpacity = | ||
{ scale = "transparency", field = "series" } | { scale = "transparency", field = "series" } | ||
symbolmarks. | symbolmarks.encode.enter.strokeOpacity = | ||
{ scale = "transparency", field = "series" } | { scale = "transparency", field = "series" } | ||
end | end | ||
801번째 줄: | 615번째 줄: | ||
local function getAnnoMarks(chartvis, stroke, fill, opacity) | local function getAnnoMarks(chartvis, stroke, fill, opacity) | ||
local vannolines, hannolines, | local vannolines, hannolines, vannolabels, hannolabels | ||
vannolines = | vannolines = | ||
{ | { | ||
type = "rule", | type = "rule", | ||
from = { data = "v_anno" }, | from = { data = "v_anno" }, | ||
encode = | |||
{ | { | ||
update = | update = | ||
824번째 줄: | 638번째 줄: | ||
type = "text", | type = "text", | ||
from = { data = "v_anno" }, | from = { data = "v_anno" }, | ||
encode = | |||
{ | { | ||
update = | update = | ||
843번째 줄: | 657번째 줄: | ||
type = "rule", | type = "rule", | ||
from = { data = "h_anno" }, | from = { data = "h_anno" }, | ||
encode = | |||
{ | { | ||
update = | update = | ||
861번째 줄: | 675번째 줄: | ||
type = "text", | type = "text", | ||
from = { data = "h_anno" }, | from = { data = "h_anno" }, | ||
encode = | |||
{ | { | ||
update = | update = | ||
879번째 줄: | 693번째 줄: | ||
end | end | ||
local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType) | local function getAxes(xTitle, xAxisFormat, xAxisAngle, xType, xGrid, yTitle, yAxisFormat, yType, yGrid, chartType, graphheight) | ||
local xAxis, yAxis | local xAxis, yAxis | ||
if chartType ~= "pie" then | if chartType ~= "pie" then | ||
if xType == " | if xType == "number" and not xAxisFormat then xAxisFormat = "d" end | ||
xAxis = | xAxis = | ||
{ | { | ||
scale = "x", | scale = "x", | ||
title = xTitle, | title = xTitle, | ||
format = xAxisFormat, | format = xAxisFormat, | ||
grid = xGrid | grid = xGrid, | ||
-- hard coding required orient values | |||
orient = "bottom" | |||
} | } | ||
if xAxisFormat == "d" then xAxis.tickMinStep = 1 end | |||
if xAxisAngle then | if xAxisAngle then | ||
local xAxisAlign | local xAxisAlign | ||
if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end | if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end | ||
xAxis. | xAxis.encode = | ||
{ | { | ||
title = | title = | ||
921번째 줄: | 738번째 줄: | ||
} | } | ||
else | else | ||
xAxis. | xAxis.encode = | ||
{ | { | ||
title = | title = | ||
947번째 줄: | 764번째 줄: | ||
end | end | ||
if yType == " | if yType == "number" and not yAxisFormat then yAxisFormat = "d" end | ||
yAxis = | yAxis = | ||
{ | { | ||
scale = "y", | scale = "y", | ||
title = yTitle, | title = yTitle, | ||
format = yAxisFormat, | format = yAxisFormat, | ||
grid = yGrid | grid = yGrid, | ||
-- hard coding required orient values | |||
orient = "left" | |||
} | } | ||
yAxis. | if yAxisFormat == "d" then | ||
yAxis.tickMinStep = 1 | |||
if graphheight < 151 then yAxis.tickCount = math.max(math.floor(graphheight/12), 2) end | |||
end | |||
yAxis.encode = | |||
{ | { | ||
title = | title = | ||
988번째 줄: | 811번째 줄: | ||
local function getLegend(legendTitle, chartType, outerRadius) | local function getLegend(legendTitle, chartType, outerRadius) | ||
local legend = | local legend = | ||
{ | { | ||
fill = "color", | fill = "color", | ||
stroke = "color", | stroke = "color", | ||
title = legendTitle, | title = legendTitle, | ||
} | } | ||
legend. | legend.titleColor = persistentGrey | ||
legend.labelColor = persistentGrey | |||
fill = { value = | |||
if chartType == "pie" then | |||
legend.orient = "top-right" | |||
legend.titleColor = persistentGrey | |||
legend.labelColor = persistentGrey | |||
end | |||
return legend | |||
end | |||
function p.map(frame) | |||
-- map path data for geographic objects | |||
local basemap = frame.args.basemap or "WorldMap-iso2.json" -- WorldMap name and/or location may vary from wiki to wiki | |||
-- scaling factor | |||
local scale = tonumber(frame.args.scale) or 60 | |||
-- 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 | |||
-- 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 | |||
colorScale = {scheme = colorScale} | |||
else colorScale = stringArray(colorScale) end | |||
scales = | |||
{ | |||
{ | |||
name = "color", | |||
type = scaleType or "linear", | |||
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 | |||
else | |||
if colorScale == "category10" or colorScale == "category20" then colorScale = {scheme = colorScale} | |||
else colorScale = stringArray(colorScale) end | |||
scales = | |||
{ | |||
{ | |||
name = "color", | |||
type = scaleType or "ordinal", | |||
domain = { data = "highlights", field = "v" }, | |||
range = colorScale, | |||
} | |||
} | |||
end | |||
-- create legend | |||
if legend then | |||
legend = | |||
{ | |||
fill = "color", | |||
stroke = "color", | |||
title = legendTitle, | |||
} | |||
legend.titleColor = persistentGrey | |||
legend.labelColor = persistentGrey | |||
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 = | |||
{ | |||
schema = "https://vega.github.io/schema/vega/v5.json", | |||
width = 190, | |||
height = 60, -- fit nicely world map with scale 60 | |||
projections = | |||
{ | |||
{ name = "projection", | |||
value = "data", -- data source | |||
scale = scale, | |||
translate = { 0, 0 }, | |||
center = center, | |||
type = projection | |||
}, | |||
}, | |||
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 = | |||
{ | |||
{ | |||
-- join ("zip") of mutiple data source: here map paths data and highlights | |||
type = "lookup", | |||
fields = { "id" }, -- key for map paths data | |||
from = "highlights", -- name of highlight data source | |||
key = "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 = | |||
fill = { value = persistentGrey } | { | ||
-- output markings (map paths and highlights) | |||
{ transform = {{type = "geoshape", projection = "projection"}}, | |||
type = "shape", | |||
from = { data = feature }, | |||
encode = | |||
{ | |||
enter = {stroke = { value = persistentGrey } }, | |||
update = { fill = { field = "zipped.v" } }, | |||
hover = { fill = { value = persistentGrey } } | |||
} | |||
} | |||
}, | }, | ||
legends = { legend} | |||
} | } | ||
if | if (scales) then | ||
output.scales = scales | |||
output.marks[1].encode.update.fill.scale = "color" | |||
end | end | ||
return | |||
local flags | |||
if formatJson then flags = mw.text.JSON_PRETTY end | |||
JSONtemp = mw.text.jsonEncode(output, flags) | |||
-- $ is not allowed in variable name so it need to be added in JSON string | |||
JSON = string.gsub(JSONtemp, '\"schema\":\"https://vega.github.io/schema/vega/v5.json\"','\"$schema\":\"https://vega.github.io/schema/vega/v5.json\"',1) | |||
return JSON | |||
end | end | ||
1,051번째 줄: | 1,041번째 줄: | ||
local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle) | local xAxisAngle = tonumber(frame.args.xAxisAngle) or tonumber(frame.args.xaxisangle) | ||
-- x and y scale types | -- x and y scale types | ||
local xScaleType = frame.args.xScaleType or frame.args.xscaletype | local xScaleType = frame.args.xScaleType or frame.args.xscaletype | ||
local yScaleType = frame.args.yScaleType or frame.args.yscaletype | 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 | -- 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 xScaleType == "log" then | ||
1,066번째 줄: | 1,056번째 줄: | ||
local xGrid = frame.args.xGrid or frame.args.xgrid or false | local xGrid = frame.args.xGrid or frame.args.xgrid or false | ||
local yGrid = frame.args.yGrid or frame.args.ygrid or false | local yGrid = frame.args.yGrid or frame.args.ygrid or false | ||
-- grids fail-safe | |||
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 | |||
-- for line chart, show a symbol at each data point | -- for line chart, show a symbol at each data point | ||
local showSymbols = frame.args.showSymbols or frame.args.showsymbols | local showSymbols = frame.args.showSymbols or frame.args.showsymbols | ||
1,080번째 줄: | 1,087번째 줄: | ||
local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel | local v_annoLabelString = frame.args.vAnnotatonsLabel or frame.args.vannotatonslabel | ||
local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel | local h_annoLabelString = frame.args.hAnnotatonsLabel or frame.args.hannotatonslabel | ||
-- decode annotations cvs | -- decode annotations cvs | ||
local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel | local v_annoLine, v_annoLabel, h_annoLine, h_annoLabel | ||
if v_annoLineString and v_annoLineString ~= "" then | if v_annoLineString and v_annoLineString ~= "" then | ||
if xType == "number" then v_annoLine = numericArray(v_annoLineString) | |||
if xType == "number | else v_annoLine = stringArray(v_annoLineString) end | ||
else | |||
v_annoLabel = stringArray(v_annoLabelString) | v_annoLabel = stringArray(v_annoLabelString) | ||
end | end | ||
if h_annoLineString and h_annoLineString ~= "" then | if h_annoLineString and h_annoLineString ~= "" then | ||
if yType == "number" then h_annoLine = numericArray(h_annoLineString) | |||
if yType == "number | else h_annoLine = stringArray(h_annoLineString) end | ||
else | |||
h_annoLabel = stringArray(h_annoLabelString) | h_annoLabel = stringArray(h_annoLabelString) | ||
end | end | ||
-- pie chart radiuses | -- pie chart radiuses | ||
local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0 | local innerRadius = tonumber(frame.args.innerRadius) or tonumber(frame.args.innerradius) or 0 | ||
local outerRadius = math.min(graphwidth, graphheight) | local outerRadius = math.min(graphwidth, graphheight) / 2; | ||
-- format JSON output | -- format JSON output | ||
local formatJson = frame.args.formatjson | local formatJson = frame.args.formatjson | ||
-- get x values | -- get x values | ||
local x | local x | ||
x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax) | x, xType, xMin, xMax = deserializeXData(frame.args.x, xType, xMin, xMax) | ||
-- get y values (series) | -- get y values (series) | ||
local yValues = {} | local yValues = {} | ||
1,140번째 줄: | 1,123번째 줄: | ||
-- create data tuples, consisting of series index, x value, y value | -- create data tuples, consisting of series index, x value, y value | ||
local data | local data, transform | ||
if chartType == "pie" then | if chartType == "pie" then | ||
-- for pie charts the second second series is merged into the first series as radius values | -- for pie charts the second second series is merged into the first series as radius values | ||
data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" }) | data = convertXYToSingleSeries(x, y, xType, yType, { "y", "r" }) | ||
else | else | ||
data = convertXYToManySeries(x, y, xType, yType, seriesTitles) | data = convertXYToManySeries(x, y, xType, yType, seriesTitles) | ||
end | end | ||
-- configure stacked charts | -- configure transform in data for stacked charts and pie | ||
local stacked = false | local stacked = false | ||
if string.sub(chartType, 1, 7) == "stacked" then | if string.sub(chartType, 1, 7) == "stacked" then | ||
chartType = string.sub(chartType, 8) | chartType = string.sub(chartType, 8) | ||
1,156번째 줄: | 1,140번째 줄: | ||
stacked = true | stacked = true | ||
-- aggregate data by cumulative y values | -- aggregate data by cumulative y values | ||
transform = | |||
{ | {{ | ||
type = "stack", | |||
groupby = {"x"}, | |||
sort = { field = "index"}, | |||
field = "y" | |||
}} | |||
else transform = {} end | |||
end | |||
} | if chartType == "pie" then | ||
transform = { { field = "y", type = "pie" } } | |||
end | end | ||
1,218번째 줄: | 1,201번째 줄: | ||
local colorField | local colorField | ||
if chartType == "line" then colorField = "stroke" else colorField = "fill" end | if chartType == "line" then colorField = "stroke" else colorField = "fill" end | ||
-- create chart markings | -- create chart markings | ||
local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate) | local chartvis = getChartVisualisation(chartType, stacked, colorField, #y, innerRadius, outerRadius, linewidth, alphaScale, radiusScale, lineScale, interpolate, graphwidth, graphheight) | ||
local marks = { chartvis } | local marks = { chartvis } | ||
1,238번째 줄: | 1,219번째 줄: | ||
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 textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues) | local textmarks = getTextMarks(chartmarks, chartType, outerRadius, scales, radiusScale, yType, showValues, graphwidth, graphheight) | ||
if chartmarks ~= chartvis then | if chartmarks ~= chartvis then | ||
table.insert(chartvis.marks, textmarks) | table.insert(chartvis.marks, textmarks) | ||
1,246번째 줄: | 1,227번째 줄: | ||
end | end | ||
-- symbol marks | -- symbol marks | ||
1,277번째 줄: | 1,242번째 줄: | ||
end | end | ||
-- custom size | -- custom symbol size | ||
local symSize | local symSize | ||
if type(showSymbols) == "number" then | if type(showSymbols) == "number" then | ||
1,296번째 줄: | 1,261번째 줄: | ||
end | end | ||
-- custom shape | -- custom shape | ||
if stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end | if stringArray(symbolsShape) and #stringArray(symbolsShape) > 1 then symbolsShape = stringArray(symbolsShape) end | ||
local symShape = " " | local symShape --= " " | ||
if type(symbolsShape) == "string" and shapes[symbolsShape] then | if type(symbolsShape) == "string" and shapes[symbolsShape] then | ||
1,353번째 줄: | 1,317번째 줄: | ||
-- end | -- end | ||
local symbolmarks = getSymbolMarks(chartmarks, symSize, symShape, symStroke, symbolsNoFill, alphaScale) | local symbolmarks = getSymbolMarks(chartmarks, symSize, symShape, symStroke, symbolsNoFill, alphaScale) | ||
if chartmarks ~= chartvis then | if chartmarks ~= chartvis then | ||
1,363번째 줄: | 1,325번째 줄: | ||
end | end | ||
local vannolines, vannolabels, hannolines, hannolabels = getAnnoMarks(chartmarks, | local vannolines, vannolabels, hannolines, hannolabels = getAnnoMarks(chartmarks, 2.5, persistentGrey, 0.75) | ||
if vannoData then | if vannoData then | ||
table.insert(marks, vannolines) | table.insert(marks, vannolines) | ||
1,376번째 줄: | 1,338번째 줄: | ||
-- 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, graphheight) | ||
-- 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 | ||
if legend and chartType == "pie" and outerRadius < graphwidth/2+100 then graphwidth = graphwidth + 100 end | |||
-- construct final output object | -- construct final output object | ||
local output = | local output = | ||
{ | { | ||
schema = "https://vega.github.io/schema/vega/v5.json", | |||
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 = marks, | ||
legends = { legend } | legends = { legend } | ||
1,395번째 줄: | 1,358번째 줄: | ||
if vannoData then table.insert(output.data, vannoData) end | if vannoData then table.insert(output.data, vannoData) end | ||
if hannoData then table.insert(output.data, hannoData) end | if hannoData then table.insert(output.data, hannoData) end | ||
if | if transform then data.transform = transform end -- table.insert(output.data.transform, transform) end | ||
local flags | local flags | ||
if formatJson then flags = mw.text.JSON_PRETTY end | if formatJson then flags = mw.text.JSON_PRETTY end | ||
JSONtemp = mw.text.jsonEncode(output, flags) | |||
-- $ is not allowed in variable name so it need to be added in JSON string | |||
JSON = string.gsub(JSONtemp, '\"schema\":\"https://vega.github.io/schema/vega/v5.json\"','\"$schema\":\"https://vega.github.io/schema/vega/v5.json\"',1) | |||
return JSON | |||
end | end | ||
1,413번째 줄: | 1,379번째 줄: | ||
return "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog | return "\n\nchart JSON\n ".. p.chart(frame) .. " \n\n" .. debuglog | ||
end | 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}}, | ||
1,423번째 줄: | 1,388번째 줄: | ||
return p | return p | ||
-- BUGS: [check if still exist in Vega 5] | |||
-- 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 | |||
-- Reordering even strings like integers - see https://en.wikipedia.org/wiki/Template_talk:Graph:Chart#Reordering_even_strings_like_integers | |||
-- TODO: | |||
-- - bugs from Vega 2 - check if still exist in Vega 5 | |||
-- - marks: | |||
-- - line strokeDash + serialization, | |||
-- - symStroke serialization | |||
-- - symbolsNoFill serialization | |||
-- - arbitrary SVG path symbol shape as symbolsShape argument | |||
-- - annotations | |||
-- - 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_): | |||
-- 2023-09-10 Update to Vega 5 (except maps) | |||
-- 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. |
2024년 4월 24일 (수) 16:10 기준 최신판
이 모듈에 대한 설명문서는 모듈:Graph/설명문서에서 만들 수 있습니다
-- ATTENTION: Please edit this code at https://www.mediawiki.org/w/index.php?title=Module:Graph
-- This way all wiki languages can stay in sync. Thank you!
-- Changes history and TODO's moved to end of script
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
local 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
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 = "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 == "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 = "number" end
end
if 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 = {},
transform = {}
}
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], index = 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 = {},
transform = {} }
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
nice = true,
domain = { data = "chart", field = "x" }
}
if xScaleType then xscale.type = xScaleType else xscale.type = "linear" end
if xMin then xscale.domainMin = tonumber(xMin) end
if xMax then xscale.domainMax = tonumber(xMax) end
if xMin or xMax then
xscale.clamp = true
xscale.nice = false
end
if chartType == "rect" then
xscale.type = "band"
xscale.zero = nil
xscale.nice = nil
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",
range = "height",
-- area charts have the lower boundary of their filling at y=0 (see marks.encode.enter.y2), therefore these need to start at zero
zero = chartType ~= "line",
nice = true,
}
if yScaleType then yscale.type = yScaleType else yscale.type = "linear" end
if yMin then yscale.domainMin = tonumber(yMin) end
if yMax then yscale.domainMax = tonumber(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 = "chart", field = "y1" }
else
yscale.domain = { data = "chart", field = "y" }
end
return yscale
end
local function getColorScale(colors, chartType, xCount, yCount)
if not colors then
colors = {scheme = "category10"}
if (chartType == "pie" and xCount > 10) or yCount > 10 then colors = {scheme = "category20"} else colors = {scheme = "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.encode.enter then plotMarks.encode.enter = {} end
plotMarks.encode.enter[colorField] = { scale = "color", field = dataField }
-- action when cursor is over plot mark: highlight
if not plotMarks.encode.hover then plotMarks.encode.hover = {} end
plotMarks.encode.hover[colorField] = { value = "red" }
-- action when cursor leaves plot mark: reset to initial setup
if not plotMarks.encode.update then plotMarks.encode.update = {} end
plotMarks.encode.update[colorField] = { scale = "color", field = dataField }
end
local function getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale, graphwidth, graphheight)
local chartvis =
{
type = "arc",
from = { data = "chart"} ,
encode =
{ enter = {
x = { value = graphwidth / 2},
y = { value = graphheight / 2}
},
update = {
innerRadius = { value = innerRadius },
outerRadius = { value = outerRadius },
startAngle = { field = "startAngle" },
endAngle = { field = "endAngle" },
stroke = { value = "white" },
strokeWidth = { value = linewidth or 1 }
}
}
}
if radiusScale then
chartvis.encode.update.outerRadius.scale = radiusScale.name
chartvis.encode.update.outerRadius.field = radiusScale.domain.field
else
chartvis.encode.update.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, graphwidth, graphheight)
if chartType == "pie" then return getPieChartVisualisation(yCount, innerRadius, outerRadius, linewidth, radiusScale, graphwidth, graphheight) end
local chartvis =
{
type = chartType,
encode =
{
-- chart creation event handler
enter =
{
x = { scale = "x", field = "x" },
y = { scale = "y", field = "y" }
}
}
}
addInteractionToChartVisualisation(chartvis, colorField, "series")
if colorField == "stroke" then
chartvis.encode.enter.strokeWidth = { value = linewidth or 2.5 }
if type(lineScale) =="table" then
chartvis.encode.enter.strokeWidth.value = nil
chartvis.encode.enter.strokeWidth =
{
scale = "line",
field= "series"
}
end
end
if interpolate then chartvis.encode.enter.interpolate = { value = interpolate } end
if alphaScale then chartvis.encode.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.encode.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.encode.enter.y2 = { scale = "y", value = 0 }
end
end
-- for bar charts ...
if chartType == "rect" then
-- set 1 pixel width between the bars
chartvis.encode.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.encode.enter.x.scale = "x"
chartvis.encode.enter.x.field = "x"
chartvis.encode.enter.width.scale = "series"
end
end
-- stacked charts have their own (stacked) y values
if stacked then chartvis.encode.enter.y.field = "layout_start" end
-- if there are multiple series group these together
if yCount == 1 then
chartvis.from = { data = "chart" }
else
chartvis.from = { data = "facet" }
-- if there are multiple series, connect colors to series
chartvis.encode.enter[colorField].field = "series"
if alphaScale then chartvis.encode.update[colorField .. "Opacity"].field = "series" end
---- TODO check? if there are multiple series, connect linewidths to series
-- if chartType == "line" then
-- chartvis.encode.update.strokeWidth.field = "series"
--end
-- apply a grouping (facetting) transformation
chartvis =
{
type = "group",
from =
{ facet=
{
data = "chart",
name = "facet",
groupby = "series"
}
},
marks = chartvis
}
-- for stacked charts apply a stacking transformation
if stacked then -- TODO must check for non bar
--table.insert(chartvis.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "-_id" }, field = "y" } )
-- transform goes to data in vega 5
chartvis.marks.encode.enter.y = {
scale = "y"; field = "y1"
}
chartvis.marks.encode.enter.y2 = {
scale = "y"; field = "y0"
}
chartvis.marks = {chartvis.marks}
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
chartvis.from.facet.groupby = "x"
chartvis.signals = {{name = "width", update = "bandwidth('x')"}} -- calculation or width for each group od bars
chartvis.scales = {{
name = "facet_index", type = "band", range = "width",
domain = { data = "facet", field = "series" }}}
chartvis.encode = {enter = {x = { scale = "x", field = "x"}}}
chartvis.marks.encode.enter.x = { field = "series", scale = "facet_index" }
chartvis.marks.encode.enter.width = { scale = "facet_index", band = true }
chartvis.marks = {chartvis.marks}
else chartvis.marks = {chartvis.marks} end
end
end
return chartvis
end
local function getTextMarks(chartvis, chartType, outerRadius, scales, radiusScale, yType, showValues, graphwidth, graphheight)
local encode
if chartType == "rect" then
encode =
{
x = { scale = chartvis.encode.enter.x.scale, field = chartvis.encode.enter.x.field },
y = { scale = chartvis.encode.enter.y.scale, field = chartvis.encode.enter.y.field, offset = -(tonumber(showValues.offset) or -4) },
--dx = { scale = chartvis.encode.enter.x.scale, band = true, mult = 0.5 }, -- for horizontal text
dy = { scale = chartvis.encode.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 encode.y.offset >= 0 then
encode.align.value = "right"
encode.fill.value = showValues.fontcolor or "white"
else
encode.align.value = "left"
encode.fill.value = showValues.fontcolor or persistentGrey
end
elseif chartType == "pie" then
encode =
{
x = { value = graphwidth / 2},
y = { value = graphheight / 2},
radius = { offset = tonumber(showValues.offset) or -15 },
theta = {signal = "(datum.startAngle + datum.endAngle)/2"},
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 -- TODO check always true ?
encode.align = { value = "center" }
encode.angle = { field = "layout_mid", mult = 180.0 / math.pi }
if encode.radius.offset >= 0 then
encode.baseline.value = "bottom"
else
if not showValues.fontcolor then encode.fill.value = "white" end
encode.baseline.value = "middle"
end
elseif tonumber(showValues.angle) then -- Todo check
-- 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)
encode.align = { scale = alignScale.name, field = "layout_mid" }
encode.angle = { value = tonumber(showValues.angle) }
encode.baseline.value = "middle"
if not tonumber(showValues.offset) then encode.radius.offset = 4 end
end
if radiusScale then
encode.radius.scale = radiusScale.name
encode.radius.field = radiusScale.domain.field
else
encode.radius.value = outerRadius
end
end
if encode then
if showValues.format then
local template = "datum.y"
if yType == "number" then template = template .. "|number:'" .. showValues.format .. "'"
elseif yType == "date" then template = template .. "|time:" .. showValues.format .. "'"
end
encode.text = { template = "{{" .. template .. "}}" }
else
encode.text = { field = "y" }
end
local textmarks =
{
type = "text",
encode =
{
enter = encode
}
}
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",
encode =
{
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.encode.enter.shape = { value = symShape }
end
if type(symShape) == "table" then
symbolmarks.encode.enter.shape = { scale = "symShape", field = "series" }
end
if type(symSize) == "number" then
symbolmarks.encode.enter.size = { value = symSize }
end
if type(symSize) == "table" then
symbolmarks.encode.enter.size = { scale = "symSize", field = "series" }
end
if noFill then
symbolmarks.encode.enter.fill = nil
end
if alphaScale then
symbolmarks.encode.enter.fillOpacity =
{ scale = "transparency", field = "series" }
symbolmarks.encode.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, hannolabels
vannolines =
{
type = "rule",
from = { data = "v_anno" },
encode =
{
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" },
encode =
{
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" },
encode =
{
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" },
encode =
{
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, graphheight)
local xAxis, yAxis
if chartType ~= "pie" then
if xType == "number" and not xAxisFormat then xAxisFormat = "d" end
xAxis =
{
scale = "x",
title = xTitle,
format = xAxisFormat,
grid = xGrid,
-- hard coding required orient values
orient = "bottom"
}
if xAxisFormat == "d" then xAxis.tickMinStep = 1 end
if xAxisAngle then
local xAxisAlign
if xAxisAngle < 0 then xAxisAlign = "right" else xAxisAlign = "left" end
xAxis.encode =
{
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.encode =
{
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 == "number" and not yAxisFormat then yAxisFormat = "d" end
yAxis =
{
scale = "y",
title = yTitle,
format = yAxisFormat,
grid = yGrid,
-- hard coding required orient values
orient = "left"
}
if yAxisFormat == "d" then
yAxis.tickMinStep = 1
if graphheight < 151 then yAxis.tickCount = math.max(math.floor(graphheight/12), 2) end
end
yAxis.encode =
{
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.titleColor = persistentGrey
legend.labelColor = persistentGrey
if chartType == "pie" then
legend.orient = "top-right"
legend.titleColor = persistentGrey
legend.labelColor = persistentGrey
end
return legend
end
function p.map(frame)
-- map path data for geographic objects
local basemap = frame.args.basemap or "WorldMap-iso2.json" -- WorldMap name and/or location may vary from wiki to wiki
-- scaling factor
local scale = tonumber(frame.args.scale) or 60
-- 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
-- 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
colorScale = {scheme = colorScale}
else colorScale = stringArray(colorScale) end
scales =
{
{
name = "color",
type = scaleType or "linear",
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
else
if colorScale == "category10" or colorScale == "category20" then colorScale = {scheme = colorScale}
else colorScale = stringArray(colorScale) end
scales =
{
{
name = "color",
type = scaleType or "ordinal",
domain = { data = "highlights", field = "v" },
range = colorScale,
}
}
end
-- create legend
if legend then
legend =
{
fill = "color",
stroke = "color",
title = legendTitle,
}
legend.titleColor = persistentGrey
legend.labelColor = persistentGrey
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 =
{
schema = "https://vega.github.io/schema/vega/v5.json",
width = 190,
height = 60, -- fit nicely world map with scale 60
projections =
{
{ name = "projection",
value = "data", -- data source
scale = scale,
translate = { 0, 0 },
center = center,
type = projection
},
},
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 =
{
{
-- join ("zip") of mutiple data source: here map paths data and highlights
type = "lookup",
fields = { "id" }, -- key for map paths data
from = "highlights", -- name of highlight data source
key = "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)
{ transform = {{type = "geoshape", projection = "projection"}},
type = "shape",
from = { data = feature },
encode =
{
enter = {stroke = { value = persistentGrey } },
update = { fill = { field = "zipped.v" } },
hover = { fill = { value = persistentGrey } }
}
}
},
legends = { legend}
}
if (scales) then
output.scales = scales
output.marks[1].encode.update.fill.scale = "color"
end
local flags
if formatJson then flags = mw.text.JSON_PRETTY end
JSONtemp = mw.text.jsonEncode(output, flags)
-- $ is not allowed in variable name so it need to be added in JSON string
JSON = string.gsub(JSONtemp, '\"schema\":\"https://vega.github.io/schema/vega/v5.json\"','\"$schema\":\"https://vega.github.io/schema/vega/v5.json\"',1)
return JSON
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
-- grids fail-safe
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
-- 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" 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" 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) / 2;
-- 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, transform
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 transform in data for stacked charts and pie
local stacked = false
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
transform =
{{
type = "stack",
groupby = {"x"},
sort = { field = "index"},
field = "y"
}}
else transform = {} end
end
if chartType == "pie" then
transform = { { field = "y", type = "pie" } }
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, graphwidth, graphheight)
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, graphwidth, graphheight)
if chartmarks ~= chartvis then
table.insert(chartvis.marks, textmarks)
else
table.insert(marks, textmarks)
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 symbol 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, 2.5, 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, graphheight)
-- legend
local legend
if legendTitle and tonumber(legendTitle) ~= 0 then legend = getLegend(legendTitle, chartType, outerRadius) end
if legend and chartType == "pie" and outerRadius < graphwidth/2+100 then graphwidth = graphwidth + 100 end
-- construct final output object
local output =
{
schema = "https://vega.github.io/schema/vega/v5.json",
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 transform then data.transform = transform end -- table.insert(output.data.transform, transform) end
local flags
if formatJson then flags = mw.text.JSON_PRETTY end
JSONtemp = mw.text.jsonEncode(output, flags)
-- $ is not allowed in variable name so it need to be added in JSON string
JSON = string.gsub(JSONtemp, '\"schema\":\"https://vega.github.io/schema/vega/v5.json\"','\"$schema\":\"https://vega.github.io/schema/vega/v5.json\"',1)
return JSON
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
-- BUGS: [check if still exist in Vega 5]
-- 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
-- Reordering even strings like integers - see https://en.wikipedia.org/wiki/Template_talk:Graph:Chart#Reordering_even_strings_like_integers
-- TODO:
-- - bugs from Vega 2 - check if still exist in Vega 5
-- - marks:
-- - line strokeDash + serialization,
-- - symStroke serialization
-- - symbolsNoFill serialization
-- - arbitrary SVG path symbol shape as symbolsShape argument
-- - annotations
-- - 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_):
-- 2023-09-10 Update to Vega 5 (except maps)
-- 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.