Aller au contenu

Module:Graph

Cette page fait l’objet d’une mesure de semi-protection étendue.
Une page de Wikipédia, l'encyclopédie libre.
Ceci est une version archivée de cette page, en date du 3 juin 2015 à 15:52 et modifiée en dernier par Mps (discuter | contributions). Elle peut contenir des erreurs, des inexactitudes ou des contenus vandalisés non présents dans la version actuelle.

 Documentation[voir] [modifier] [historique] [purger]

Utilisation

Fonctions exportables :

  • fonction(frame) – description (courte description de fonction(frame) et autres informations pertinentes).
  • fonction2() – description2 (courte description de fonction2() et autres informations pertinentes).

Autres fonctions :

  • fonction() – description2 (courte description de fonction() et autres informations pertinentes).

Modules externes et autres éléments dont ce module a besoin pour fonctionner :

  • mw.title – description (courte description expliquant la dépendance de ce module externe).

Exemples

Pour des exemples, voir la page de test[Où ?] permettant de tester diverses modifications apportées. Voir aussi {{Graph:Lines}} et {{Graph:Chart}}, qui sont les modèles utilisant le module.

Test 1

local p = {}

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)
	-- Kartendaten
	local basemap = mw.title.new(frame.args.basemap or "Wikipedia:Graph/WorldMap-iso2.json")
	-- Kartenskalierung (hängt von den Kartendaten ab)
	local scale = tonumber(frame.args.scale) or 100
	-- Kartenprojektion: siehe https://github.com/mbostock/d3/wiki/Geo-Projections
	local projection = frame.args.projection or "equirectangular"
	-- Standardfarbe für Staaten ohne Daten
	local defaultColor = frame.args.defaultColor or "silver"
	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"
	-- Legende
	local legend = frame.args.legend
	
	-- Kartenmarkierungen als Schlüssel-Wert-Paare: Schlüssel müssen Großbuchstaben, Ziffern oder "-" sein (idealerweise ISO-Codes) und den "id"-Werten der Kartendaten entsprechen
	local values = {}
	local isNumbers = nil
	for name, value in pairs(frame.args) do
		local startPos, endPos = string.find(name, "[A-Z0-9\-]+")
		if startPos == 1 and endPos == string.len(name) 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

	-- Skalen
	local scales
	if isNumbers then
		if colorScale == "category10" or colorScale == "category20" then else colorScale = mw.text.split(colorScale, ",") end
		scales =
		{
			{
				name = "color",
				type = scaleType,
				domain = { data = "highlights", field = "data.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+)") -- auf Exponenten für exponentielle Darstellung prüfen
		if exponent then
			scales[1].type = "pow"
			scales[1].exponent = exponent
		end
	end

	-- Legende
	if legend then
		legend =
		{
			{
				fill = "color",
				properties =
				{
					title = { fontSize = { value = 14 } },
					labels = { fontSize = { value = 12 } },
					legend =
					{
						stroke = { value = "silver" },
						strokeWidth = { value = 1.5 }
					}
				}
			}
		}
	end

	local mapNamespace = basemap.nsText
	if string.len(mapNamespace) > 0 then mapNamespace = mapNamespace .. ":" end
	
	local output =
	{
		width = 1,  -- Dummywert, da Ausgabegröße von den Kartendaten und der Skalierung abhängt
		height = 1, -- dito
		data = 
		{
			{
				-- Datenquelle der Kartenmarkierungen
				name = "highlights",
				values = values
			},
			{
				-- Datenquelle der Kartendaten
				name = "countries",
				url = "/w/index.php?title=" .. mapNamespace .. basemap:partialUrl() .. "&action=raw",
				format = { type = "topojson", feature = "countries" },
				transform =
				{
					{
						-- geografische Transformation ("geopath") der Kartendaten
						type = "geopath",
						value = "data",			-- Datenquelle
						scale = scale,			-- Skalierungsfaktor
						translate = { 0, 0 },	-- Translation
						projection = projection	-- Projektionsmethode
					},
					{
						-- Zusammenführung ("zip") mehrerer Datenquellen: hier Kartendaten mit Kartenmarkierungen ("highlights")
						type = "zip",
						key = "data.id",     -- Schlüssel der Kartendaten
						with = "highlights", -- Name der Datenquelle der Kartenmarkierungen
						withKey = "data.id", -- Schlüssel der Kartenmarkierungen
						as = "zipped",       -- Ergebnistabellenname
						default = { data = { v = defaultColor } } -- Standardaktion für geografische Objekte ohne Entsprechung in unserem Datensatz
					}
				}
			}
		},
		marks =
		{
			-- Markierungen ausgeben
			{
				type = "path",
				from = { data = "countries" },
				properties = 
				{
					enter = { path = { field = "path" } },
					update = { fill = { field = "zipped.data.v" } },
					hover = { fill = { value = "darkgrey" } } -- hat außerhalb der Vorschau keine Wirkung, daher kein Extra-Parameter
				}
			}
		}
	}
	if (scales) then
		output.scales = scales
		output.marks[1].properties.update.fill.scale = "color"
	end
	
	-- Legende
	if legend then output.legends = legend end
	
	return mw.text.jsonEncode(output)
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"
	-- 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)
	-- 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 = "facet", keys = { "data.x" } },
					{ type = "stats", value = "data.y" }
				}
		}
		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 = "data.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",
		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"  }
	else
		yscale.domain = { data = "chart", field = "data.y" }
	end
	local colorScale =
	{
		name = "color",
		type = "ordinal",
		range = colors
	}
	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
	
	-- 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 = "data.x" },
				y = { scale = "y", field = "data.y" }
			},
			-- chart update event handler
			update = { },
			-- chart hover event handler
			hover = { }
		}
	}
	marks.properties.update[colorField] = { scale = "color" }
	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 = "y2" }
		else
			-- for non-stacking charts the lower bound is the lower bound of the y axis
			marks.properties.enter.y2 = { scale = "y", value = 0 }
		end
	end
	-- for bar charts set the width between the bars
	if type == "rect" then marks.properties.enter.width = { scale = "x", band = true, offset = -1 } end
	-- stacked charts have their own (stacked) y values
	if stacked then marks.properties.enter.y.field = "y" 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 = "data.series"
		if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "data.series" end
		-- apply a grouping (facetting) transformation
		marks =
		{
			type = "group",
			marks = { marks },
			from =
			{
				data = "chart", 
				transform =
				{
					{
						type = "facet",
						keys = { "data.series" }
					}
				}
			}
		}
		-- for stacked charts apply a stacking transformation
		if stacked then
			marks.from.transform[2] = { type = "stack", point = "data.x", height = "data.y" }
		end
	end

	-- create legend
	if legend then
		legend =
		{
			{
				fill = "color",
				stroke = "color",
				title = legend
			}
		}
	end

	-- construct final output object
	local output =
	{
		width = graphwidth,
		height = graphheight,
		data = { data, stats },
		scales = { xscale, yscale, colorScale, alphaScale },
		axes =
		{
			{
				type = "x",
				scale = "x",
				title = xTitle
			},
			{
				type = "y",
				scale = "y",
				title = yTitle
			}
		},
		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