Latest revision |
Your text |
Line 1: |
Line 1: |
| | | |
− | -- 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.
| |