Pāriet uz saturu

Modulis:Graph

Vikipēdijas lapa
Dokumentācijas ikona Moduļa dokumentācija[skatīt] [labot] [hronoloģija] [pārlādēt]

Module with helper functions for the Graph extension to display graphs and maps. From de:Modul:Graph.

Functions for templates

map

Creates a JSON object for <graph> to display a political map with colored highlights. In the article namespace the template Veidne:Template should be used instead. See its page for use cases.

Maps can be found at Special:Prefixindex/Template:Graph:Map/Inner/ (for example Worldmap2c-json with country borders) and new maps should also be saved under Module:Graph/.

Parameters:

  • basemap: sets the base map. The map definitions must follow the TopoJSON format and if saved in Wikipedia are available for this module. Maps in the default directory Special:Prefixindex/Template:Graph:Map/Inner/ like Worldmap2c-json should only be referenced by their name while omitting the Modul:Graph/ prefix to allow better portability. The parameter also accepts URLs, e.g. maps from other Wikipedia versions (the link should follow the scheme of //en.wikipedia.org/w/index.php?title=mapname&action=raw, i.e. protocol-relative without leading http/s and a trailing action=raw to fetch the raw content only). URLs to maps on external sites should be avoided for the sake of link stability, performance, security, and she be assumed to be blocked by the software or browser anyway.
  • scale: the scaling factor of the map (default: 100)
  • projection: the map projection to use. Supported values are listed at https://github.com/mbostock/d3/wiki/Geo-Projections. The default value is equirectangular for an equirectangular projection.
  • center: map center (corresponds in the map data to both comma-separated values of the scale field)
  • feature: which geographic objects should be displayed (corresponds in the map data to the name of the field under the objects field). The default is value countries.
  • ids of geographic entities: The actual parameter names depend on the base map and the selected feature. For example, for the above mentioned world map the ids are ISO country codes. The values can be either colors or numbers in case the geographic entities should be associated with numeric data: DE=lightblue marks Germany in light blue color, and DE=80.6 assigns Germany the value 80.6 (population in millions). In the latter case, the actual color depends on the following parameters.
  • colorScale: the color palette to use for the color scale. The palette must be provided as a comma-separated list of color values. The color values must be given either as #rgb/#rrggbb or by a CSS color name. Instead of a list, the built-in color palettes category10 and category20 can also be used.
  • scaleType: supported values are linear for a linear mapping between the data values and the color scale, log for a log mapping, pow for a power mapping (the exponent can be provided as pow 0.5), sqrt for a square-root mapping, and quantize for a quantized scale, i.e. the data is grouped in as many classes as the color palette has colors.
  • domainMin: lower boundary of the data values, i.e. smaller data values are mapped to the lower boundary
  • domainMax: upper boundary of the data values, i.e. larger data values are mapped to the upper boundary
  • legend: show color legend (does not work with quantize)
  • defaultValue: default value for unused geographic entities. In case the id values are colors the default value is silver, in case of numbers it is 0.
  • formatjson: format JSON object for better legibility

chart

Creates a JSON object for <graph> to display charts. In the article namespace the template Template:Graph:Chart should be used instead. See its page for use cases.

Parameters:

  • width: width of the chart
  • height: height of the chart
  • type: type of the chart: line for line charts, area for area charts, and rect for (column) bar charts, and pie for pie charts. Multiple series can stacked using the stacked prefix, e.g. stackedarea.
  • interpolate: interpolation method for line and area charts. It is recommended to use monotone for a monotone cubic interpolation – further supported values are listed at https://github.com/nyurik/vega/wiki/Marks#line.
  • colors: color palette of the chart as a comma-separated list of colors. The color values must be given either as #rgb/#rrggbb/#aarrggbb or by a CSS color name. For #aarrggbb the aa component denotes the alpha channel, i.e. FF=100% opacity, 80=50% opacity/transparency, etc. (The default color palette if n <= 10 is Category10: Veidne:ChartColors else is Category20: Veidne:ChartColors).
  • xAxisTitle and yAxisTitle: captions of the x and y axes
  • xAxisMin, xAxisMax, yAxisMin, and yAxisMax: minimum and maximum values of the x and y axes (not yet supported for bar charts). These parameters can be used to invert the scale of a numeric axis by setting the lowest value to the Max and highest value to the Min.
  • xAxisFormat and yAxisFormat: changes the formatting of the axis labels. Supported values are listed at https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#numbers for numbers. For example, the format % can be used to output percentages. For date/time specification of supported values is https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md , e.g. xAxisFormat=%d-%m-%Y for result 13-01-1977.
  • xAxisAngle: rotates the x axis labels by the specified angle. Recommended values are: -45, +45, -90, +90
  • xType and yType: data types of the values, e.g. integer for integers, number for real numbers, date for dates (e.g. YYYY-MM-DD), and string for ordinal values (use string to prevent axis values from being repeated when there are only a few values). Remarks: Date type doesn't work for bar graphs. For date data input please use ISO date format (e.g. YYYY-MM-DD) acc. to date and time formats used in HTML. Other date format may work but not in all browsers. Date is unfortunately displayed only in en-US format for all Wikipedia languages. Workaround is to use xAxisFormat and yAxisFormat with numerical dates format.
  • xScaleType and yScaleType: scale types of the x and y axes, e.g. linear for linear scale (default), log for logarithmic scale and sqrt for square root scale.
  • x: the x-values as a comma-separated list, for dates and time see remark in xType and yType
  • y or y1, y2, …: the y-values for one or several data series, respectively. For pie charts y2 denotes the radius of the corresponding sectors. For dates and time see remark in xType and yType
  • legend: show legend (only works in case of multiple data series)
  • y1Title, y2Title, …: defines the label of the respective data series in the legend
  • linewidth: line width for line charts or distance between the pie segments for pie charts. Setting to 0 with type=line creates a scatter plot.
  • linewidths: different line widths may be defined for each series of data with csv, if set to 0 with "showSymbols" results with points graph, eg.: linewidths=1, 0, 5, 0.2
  • showSymbols: show symbol on data point for line graphs, if number is provided it's size of symbol, default 2.5. may be defined for each series of data with csv, eg.: showSymbols=1, 2, 3, 4
  • symbolsShape: custom shape for symbol: circle, x, square, cross, diamond, triangle_up, triangle_down, triangle_right, triangle_left. May be defined for each series of data with csv, eg.: symbolsShape= circle, cross, square
  • symbolsNoFill: if true symbol will be without fill (only stroke),
  • symbolsStroke: if "x" symbol is used or option "symbolsNoFill" symbol stroke width, default 2.5
  • showValues: Additionally, output the y values as text. (Currently, only (non-stacked) bar and pie charts are supported.) The output can be configured used the following parameters provided as name1:value1, name2:value2:
  • innerRadius: For pie charts: defines the inner radius to create a doughnut chart.
  • xGrid and yGrid: display grid lines on the x and y axes.
  • Annotations
    • vAnnotationsLine and hAnnotationsLine: display vertical or horizontal annotation lines on specific values e.g. hAnnotationsLine=4, 5, 6
    • vAnnotationsLabel and hAnnotationsLabel: display vertical or horizontal annotation labels for lines e.g. hAnnotationLabel = label1, label2, label3
  • formatjson: format JSON object for better legibility

Template wrappers

The functions mapWrapper and chartWrapper are wrappers to pass all parameters of the calling template to the respective map and chart functions.

Note: In the editor preview the graph extension creates a canvas element with vector graphics. However, when saving the page a PNG raster graphics is generated instead. {{#invoke:Graph|function_wrapper_name}}

local p = {}
 
local baseMapDirectory = "Module:Graph/"
 
local function numericArray(csv)
	if not csv then return end
 
	local list = mw.text.split(mw.ustring.gsub(csv, "%s", ""), ",")
	local result = {}
	for i = 1, #list do
		result[i] = tonumber(list[i])
	end
	return result
end
 
local function stringArray(csv)
	if not csv then return end
 
	return mw.text.split(mw.ustring.gsub(csv, "%s", ""), ",")
end
 
local function isTable(t) return type(t) == "table" end
 
function p.map(frame)
	-- map path data for geographic objects
	local basemap = frame.args.basemap or "WorldMap-iso2.json"
	-- scaling factor
	local scale = tonumber(frame.args.scale) or 100
	-- map projection, see https://github.com/mbostock/d3/wiki/Geo-Projections
	local projection = frame.args.projection or "equirectangular"
	-- defaultValue for geographic objects without data
	local defaultValue = frame.args.defaultValue
	local scaleType = frame.args.scaleType or "linear"
	-- minimaler Wertebereich (nur für numerische Daten)
	local domainMin = tonumber(frame.args.domainMin)
	-- maximaler Wertebereich (nur für numerische Daten)
	local domainMax = tonumber(frame.args.domainMax)
	-- Farbwerte der Farbskala (nur für numerische Daten)
	local colorScale = frame.args.colorScale or "category10"
	-- show legend
	local legend = frame.args.legend
	-- format JSON output
	local formatJSON = frame.args.formatjson
 
	-- map data are key-value pairs: keys are non-lowercase strings (ideally ISO codes) which need to match the "id" values of the map path data
	local values = {}
	local isNumbers = nil
	for name, value in pairs(frame.args) do
		if mw.ustring.find(name, "^[^%l]+$") 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 == "category10" or colorScale == "category20" then else colorScale = stringArray(colorScale) end
		scales =
		{
			{
				name = "color",
				type = scaleType,
				domain = { data = "highlights", field = "v" },
				range = colorScale,
				nice = true
			}
		}
		if domainMin then scales[1].domainMin = domainMin end
		if domainMax then scales[1].domainMax = domainMax end
 
		local exponent = string.match(scaleType, "pow%s+(%d+%.?%d+)") -- check for exponent
		if exponent then
			scales[1].type = "pow"
			scales[1].exponent = exponent
		end
	end
 
	-- create legend
	if legend then
		legend =
		{
			{
				fill = "color",
				properties =
				{
					title = { fontSize = { value = 14 } },
					labels = { fontSize = { value = 12 } },
					legend =
					{
						stroke = { value = "silver" },
						strokeWidth = { value = 1.5 }
					}
				}
			}
		}
	end
 
	-- get map url
	local basemapUrl
	if (string.sub(basemap, 1, 7) == "http://") or (string.sub(basemap, 1, 8) == "https://") or (string.sub(basemap, 1, 2) == "//") 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 = mw.title.new(basemap):fullUrl("action=raw")
	end
 
	local output =
	{
		version = 2,
		width = 1,  -- generic value as output size depends solely on map size and scaling factor
		height = 1, -- ditto
		data = 
		{
			{
				-- data source for the highlights
				name = "highlights",
				values = values
			},
			{
				-- data source for map paths data
				name = "countries",
				url = basemapUrl,
				format = { type = "topojson", feature = "countries" },
				transform =
				{
					{
						-- geographic transformation ("geopath") of map paths data
						type = "geopath",
						value = "data",			-- data source
						scale = scale,
						translate = { 0, 0 },
						projection = projection
					},
					{
						-- join ("zip") of mutiple data source: here map paths data and highlights
						type = "lookup",
						keys = { "id" },      -- key for map paths data
						on = "highlights",    -- name of highlight data source
						onKey = "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)
			{
				type = "path",
				from = { data = "countries" },
				properties = 
				{
					enter = { path = { field = "layout_path" } },
					update = { fill = { field = "zipped.v" } },
					hover = { fill = { value = "darkgrey" } }
				}
			}
		},
		legends = legend
	}
	if (scales) then
		output.scales = scales
		output.marks[1].properties.update.fill.scale = "color"
	end
 
	local flags
	if formatJSON then flags = mw.text.JSON_PRETTY end
	return mw.text.jsonEncode(output, flags)
end
 
function p.chart(frame)
	-- chart width
	local graphwidth = tonumber(frame.args.width)
	-- chart height
	local graphheight = tonumber(frame.args.height)
	-- chart type
	local type = frame.args.type or "line"
	-- interpolation mode: 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 colors = stringArray(frame.args.colors) or "category10"
	-- for line charts, the thickness of the line (strokeWidth)
	local linewidth = tonumber(frame.args.linewidth) or 2.5
	-- x and y axis caption
	local xTitle = frame.args.xAxisTitle
	local yTitle = frame.args.yAxisTitle
	-- override x and y axis minimum and maximum
	local xMin = tonumber(frame.args.xAxisMin)
	local xMax = tonumber(frame.args.xAxisMax)
	local yMin = tonumber(frame.args.yAxisMin)
	local yMax = tonumber(frame.args.yAxisMax)
	-- override x and y axis label formatting
	local xFormat = frame.args.xAxisFormat
	local yFormat = frame.args.yAxisFormat
	-- show legend, optionally caption
	local legend = frame.args.legend
	-- format JSON output
	local formatJSON = frame.args.formatjson
 
	-- get x values
	local x = numericArray(frame.args.x)
 
	-- get y values (series)
	local y = {}
	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
			y[yNum] = numericArray(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 name
		end
	end
 
	-- create data tuples, consisting of series index, x value, y value
	local data = { name = "chart", values = {} }
	for i = 1, #y do
		for j = 1, #x do
			if j <= #y[i] then data.values[#data.values + 1] = { series = seriesTitles[i], x = x[j], y = y[i][j] } end
		end
	end
 
	-- use stacked charts
	local stacked = false
	local stats
	if string.sub(type, 1, 7) == "stacked" then
		type = string.sub(type, 8)
		if #y > 1 then -- ignore stacked charts if there is only one series
			stacked = true
			-- calculate statistics of data as stacking requires cumulative y values
			stats =
			{
				name = "stats", source = "chart", transform = {
					{
						type = "aggregate",
						groupby = { "x" },
						summarize = { y = "sum" }
					}
				}
		}
		end
	end
 
	-- create scales
	local xscale =
	{
		name = "x",
		type = "linear",
		range = "width",
		zero = false, -- do not include zero value
		nice = true,  -- force round numbers for y scale
		domain = { data = "chart", field = "x" }
	}
	if xMin then xscale.domainMin = xMin end
	if xMax then xscale.domainMax = xMax end
	if xMin or xMax then xscale.clamp = true end
	if type == "rect" then xscale.type = "ordinal" end
 
	local yscale =
	{
		name = "y",
		type = "linear",
		range = "height",
		-- area charts have the lower boundary of their filling at y=0 (see marks.properties.enter.y2), therefore these need to start at zero
		zero = type ~= "line",
		nice = true
	}
	if yMin then yscale.domainMin = yMin end
	if yMax then yscale.domainMax = yMax end
	if yMin or yMax then yscale.clamp = true end
	if stacked then
		yscale.domain = { data = "stats", field = "sum_y"  }
	else
		yscale.domain = { data = "chart", field = "y" }
	end
	local colorScale =
	{
		name = "color",
		type = "ordinal",
		range = colors,
		domain = { data = "chart", field = "series" }
	}
	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
	-- for bar charts with multiple series: each series is grouped by the x value, therefore the series need their own scale within each x group
	local groupScale
	if type == "rect" and not stacked and #y > 1 then
		groupScale =
		{
			name = "series",
			type = "ordinal",
			range = "width",
			domain = { field = "series" }
		}
		xscale.padding = 0.2 -- pad each bar group
	end
 
	-- decide if lines (strokes) or areas (fills) should be drawn
	local colorField
	if type == "line" then colorField = "stroke" else colorField = "fill" end
 
	-- create chart markings
	local marks =
	{
		type = type,
		properties =
		{
			-- chart creation event handler
			enter =
			{
				x = { scale = "x", field = "x" },
				y = { scale = "y", field = "y" }
			},
			-- chart update event handler
			update = { },
			-- chart hover event handler
			hover = { }
		}
	}
	if colorField == "stroke" then
		marks.properties.enter["strokeWidth"] = { value = linewidth }
	end
	marks.properties.enter[colorField] = { scale = "color", field = "series" }
	marks.properties.update[colorField] = { scale = "color", field = "series" }
	marks.properties.hover[colorField] = { value = "red" }
	if alphaScale then marks.properties.update[colorField .. "Opacity"] = { scale = "transparency" } end
	-- for bars and area charts set the lower bound of their areas
	if type == "rect" or type == "area" then
		if stacked then
			-- for stacked charts this lower bound is cumulative/stacking
			marks.properties.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 ]]
			marks.properties.enter.y2 = { scale = "y", value = 0 }
		end
	end
	-- for bar charts ...
	if type == "rect" then
		-- set 1 pixel width between the bars
		marks.properties.enter.width = { scale = "x", band = true, offset = -1 }
		-- for multiple series the bar marking need to use the "inner" series scale, whereas the "outer" x scale is used by the grouping
		if not stacked and #y > 1 then
			marks.properties.enter.x.scale = "series"
			marks.properties.enter.x.field = "series"
			marks.properties.enter.width.scale = "series"
		end
	end
	-- stacked charts have their own (stacked) y values
	if stacked then marks.properties.enter.y.field = "layout_start" end
 
	-- set interpolation mode
	if interpolate then marks.properties.enter.interpolate = { value = interpolate } end
 
	if #y == 1 then marks.from = { data = "chart" } else
		-- if there are multiple series, connect colors to series
		marks.properties.update[colorField].field = "series"
		if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "series" end
		-- apply a grouping (facetting) transformation
		marks =
		{
			type = "group",
			marks = { marks },
			from =
			{
				data = "chart", 
				transform =
				{
					{
						type = "facet",
						groupby = { "series" }
					}
				}
			}
		}
		-- for stacked charts apply a stacking transformation
		if stacked then
			table.insert( marks.from.transform, 1, { type = "stack", groupby = { "x" }, sortby = { "series" }, field = "y" } )
		else
			-- for bar charts the series are side-by-side grouped by x
			if type == "rect" then
				marks.from.transform[1].groupby = "x"
				marks.scales = { groupScale }
				marks.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
			end
		end
	end
 
	-- create legend
	if legend then
		legend =
		{
			{
				fill = "color",
				stroke = "color",
				title = legend
			}
		}
	end
 
	-- construct final output object
	local output =
	{
		version = 2,
		width = graphwidth,
		height = graphheight,
		data = { data, stats },
		scales = { xscale, yscale, colorScale, alphaScale },
		axes =
		{
			{
				type = "x",
				scale = "x",
				title = xTitle,
				format = xFormat
			},
			{
				type = "y",
				scale = "y",
				title = yTitle,
				format = yFormat
			}
		},
		marks = { marks },
		legends = legend
	}
 
	local flags
	if formatJSON then flags = mw.text.JSON_PRETTY end
	return mw.text.jsonEncode(output, flags)
end
 
function p.mapWrapper(frame)
	return p.map(frame:getParent())
end
 
function p.chartWrapper(frame)
	return p.chart(frame:getParent())
end
 
return p