Toggle menu
15
236
74
27.6K
Kenshi Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.


Smooth piechart module. Accessed via Template:Piechart.

Usage

Draws charts in HTML with an accessible legend (optional). A list of all features is in the "TODO" section of the main `p.pie` function.

Most of the time you should use with a helper template that adds required CSS: {{Piechart}}.

Examples

Minimalistic

Note that you don't need to provide the second value as it's calculated (assuming they sum up to 100).

{{Piechart| [ {"value":33.3},  {} ] }}

Labels and Legend

Here we add some custom labels. Also note that we add a meta option to add legend on the side.

{{Piechart| [
 {"label": "women: $v", "value": 33.3},
 {"label": "men: $v"}
]
| meta = {"legend":true}
}}
  1. women: 33.3%
  2. men: 66.7%

Automatic Scaling

In cases where you don't have calculated percentages, you can use automatic scaling. Just provide both values in this case.

{{Piechart| [
 {"label": "women: $v", "value": 750},
 {"label": "men: $v", "value": 250}
]
| meta = {"legend":true}
}}
  1. women: 750 (75.0%)
  2. men: 250 (25.0%)

Multiple Values

The module allows displaying multiple values, not just 2.

{{Piechart| [
 {"label": "sweets: $v", "value": 5, "color":"darkred"},
 {"label": "sandwiches: $v", "value": 3, "color":"wheat"},
 {"label": "cookies: $v", "value": 2, "color":"goldenrod"},
 {"label": "drinks: $v", "value": 1, "color":"#ccf"}
]
|meta={"autoscale":true, "legend":true}
}}
  1. sweets: 5 (45.5%)
  2. sandwiches: 3 (27.3%)
  3. cookies: 2 (18.2%)
  4. drinks: 1 (9.09%)

Note that in this case, it was necessary to provide the additional option "autoscale":true. This is necessary when the sum is less than 100.

Links

  1. sweets: 5 (45.5%)
  2. sandwiches: 3 (27.3%)
  3. cookies: 2 (18.2%)
  4. drinks: 1 (9.09%)

Legend and Its Position

The legend is added using the meta property legend as shown. However, you can also change the order using direction. Possible values include:

  • row (default) – order is list, chart;
  • row-reverse – reverse order, i.e., chart, list;
  • column – column layout (vertical).
  • column-reverse – column layout, reversed (chart on top).
{{Piechart| [
 {"label": "cookies: $v", "value": 2, "color":"goldenrod"},
 {"label": "drinks: $v", "value": 1, "color":"#ccf"},
 {"label": "sweets: $v", "value": 5, "color":"darkred"},
 {"label": "sandwiches: $v", "value": 3, "color":"wheat"}
]
|meta={"autoscale":true, "legend":true, "direction":"row-reverse"}
}}

row (default direction)

  1. cookies: 2 (18.2%)
  2. drinks: 1 (9.09%)
  3. sweets: 5 (45.5%)
  4. sandwiches: 3 (27.3%)

row-reverse

  1. cookies: 2 (18.2%)
  2. drinks: 1 (9.09%)
  3. sweets: 5 (45.5%)
  4. sandwiches: 3 (27.3%)

column

  1. cookies: 2 (18.2%)
  2. drinks: 1 (9.09%)
  3. sweets: 5 (45.5%)
  4. sandwiches: 3 (27.3%)

column-reverse

  1. cookies: 2 (18.2%)
  2. drinks: 1 (9.09%)
  3. sweets: 5 (45.5%)
  4. sandwiches: 3 (27.3%)

Green frames added for clarity in examples. They are not normally added.

Direct functions

In case you want to use without the {{Piechart}} template, you can use this main functions:

  • {{#invoke:Piechart|pie|json_data|meta=json_options}}
  • {{#invoke:Piechart|color|number}}

Note that direct calls to the pie function require adding CSS:

<templatestyles src="Piechart/style.css"/>
{{#invoke:Piechart|pie| [ {"value":33.3},  {} ] }}

Example of json_data:

[
		{ "label": "pie: $v", "color": "wheat", "value": 40 },
		{ "label": "cheese pizza $v", "color": "#fc0", "value": 20 },
		{ "label": "mixed pizza: $v", "color": "#f60", "value": 20 },
		{ "label": "raw pizza $v", "color": "#f30" }
]
  • Note that the last value is missing. The last value is optional as long as the values are intended to sum up to 100 (as in 100%).
  • Notice $v label, this is a formatted number (see `function prepareLabel`).
  • Colors are hex or names. Default palette is in shades of green.

Example of meta=json_options:

|meta = {"size":200, "autoscale":false, "legend":true}

All meta options are optional (see `function p.setupOptions`).

Feature requests

For feature requests and bugs write to me, the author of the piecharte module: Maciej Nux.

See also

  • {{Pie chart}}, also pie, but with classic template parameters (enumerated); that chart is shown on the right (like a thumbnail image).



local p = {}
-- require exact colors for printing
local forPrinting = "-webkit-print-color-adjust: exact; print-color-adjust: exact;"
--[[
	Smooth piechart module.

	Draws charts in HTML with an accessible legend (optional).
	A list of all features is in the "TODO" section of the main `p.pie` function.

	Use with a helper template that adds required CSS.

	{{{1}}}:
	[
		{ "label": "pie: $v", "color": "wheat", "value": 40 },
		{ "label": "cheese pizza $v", "color": "#fc0", "value": 20 },
		{ "label": "mixed pizza: $v", "color": "#f60", "value": 20 },
		{ "label": "raw pizza $v", "color": "#f30" }
	]
    Where $v is a formatted number (see `function prepareLabel`).

	{{{meta}}}:
		{"size":200, "autoscale":false, "legend":true}
	All meta options are optional (see `function p.setupOptions`).
]]
-- Author: [[User:Nux|Maciej Nux]] (pl.wikipedia.org).

--[[
	Debug:
	
	-- labels and auto-value
	local json_data = '[{"label": "k: $v", "value": 33.1}, {"label": "m: $v", "value": -1}]'
	local html = p.renderPie(json_data)
	mw.logObject(html)
	
	-- autoscale values
	local json_data = '[{"value": 700}, {"value": 300}]'
	local html = p.renderPie(json_data, options)
	mw.logObject(html)	
	
	-- size option
	local json_data = '[{"label": "k: $v", "value": 33.1}, {"label": "m: $v", "value": -1}]'
	local options = '{"size":200}'
	local html = p.renderPie(json_data, options)
	mw.logObject(html)	

	-- custom colors
	local json_data = '[{"label": "k: $v", "value": 33.1, "color":"black"}, {"label": "m: $v", "value": -1, "color":"green"}]'
	local html = p.renderPie(json_data)
	mw.logObject(html)
	
	-- 4-cuts
	local entries = {
	    '{"label": "ciastka: $v", "value": 2, "color":"goldenrod"}',
	    '{"label": "słodycze: $v", "value": 4, "color":"darkred"}',
	    '{"label": "napoje: $v", "value": 1, "color":"lightblue"}',
	    '{"label": "kanapki: $v", "value": 3, "color":"wheat"}'
	}
	local json_data = '['..table.concat(entries, ',')..']'
	local html = p.renderPie(json_data, '{"autoscale":true}')
	mw.logObject(html)

	-- colors
	local fr = { args = { " 123 " } }
	local ret = p.color(fr)
]]

--[[
	Color for a slice (defaults).

	{{{1}}}: slice number
]]
function p.color(frame)
	local index = tonumber(trim(frame.args[1]))
	return ' ' .. defaultColor(index)
end

--[[
	Piechart.
	
    TODO:
    - [x] basic 2-element pie chart
        - read json
        - calculate value with -1
        - generate html
        - new css + tests
        - provide dumb labels (just v%)
    - [x] colors in json
    - [x] 1st value >= 50%
    - [x] custom labels support
    - [x] pie size from 'meta' param (options json)
    - [x] pl formatting for numbers?
    - [x] support undefined value (instead of -1)
    - [x] undefined in any order
    - [x] scale values to 100% (autoscale)
    - [x] order values clockwise (not left/right)
    - [x] multi-cut pie
    - [x] sanitize user values
    - [x] auto colors
    - [x] function to get color by number (for custom legend)
	- [x] remember and show autoscaled data
    - [x] generate a legend
	- [x] simple legend positioning by (flex-)direction
    - legend2: customization
		- (?) itemTpl support
			- replace default item with tpl
			- can I / should I sanitize it?
			- support for $v, $d, $p
		- (?) custom head
    - (?) validation of input
		- check if required values are present
		- message showing whole entry, when entry is invalid
		- pre-sanitize values?
		- sane info when JSON fails? Maybe dump JSON and show example with quotes-n-all...
    - (?) option to sort entries by value
]] 
function p.pie(frame)
	local json_data = trim(frame.args[1])
	local options = {}
	if (frame.args.meta) then
		options.meta = trim(frame.args.meta)
	end
	if (frame.args.caption) then
		options.caption = trim(frame.args.caption)
	end
	
	local html = p.renderPie(json_data, options)
	return trim(html)
end

-- Setup chart options.
function p.setupOptions(user_options)
	local options = {
		-- circle size in [px]
		size = 100,
		-- autoscale values (otherwise assume they sum up to 100)
		autoscale = false,
		-- hide chart for screen readers (when you have a table, forced for legend)
		ariahidechart = false,
		-- show legend (defaults to the left side)
		legend = false,
		-- direction of legend-chart flexbox (flex-direction)
		direction = "",
		-- width of the main container
		-- when direction is used defaults to max-width, otherwise it's not added
		width = "",
		-- caption above the labels
		caption = "",
	}
	-- internals
	options.style = ""
	if user_options.caption then
		options.caption = user_options.caption
	end
	if user_options.meta then
		local rawOptions = mw.text.jsonDecode(user_options.meta, mw.text.JSON_TRY_FIXING)
		if rawOptions then
			if type(rawOptions.size) == "number" then
				options.size = math.floor(rawOptions.size)
			end
			options.autoscale = rawOptions.autoscale or false 
			if rawOptions.legend then
				options.legend = true
			end
			if rawOptions.ariahidechart then
				options.ariahidechart = true
			end
			if (type(rawOptions.direction) == "string") then
				-- Remove unsafe/invalid characters
				local sanitized = rawOptions.direction:gsub("[^a-z0-9%-]", "")
				-- also adjust width so that row-reverse won't push things to the right
				options.direction = 'flex-direction: ' .. sanitized .. ';'
				options.width = 'width: max-content;'
			end
			if (type(rawOptions.width) == "string") then
				-- note, this intentionaly overwrites what was set for direction
				local sanitized = rawOptions.width:gsub("[^a-z0-9%-]", "")
				options.width = 'width: ' .. sanitized .. ';'
			end
			if (type(rawOptions.caption) == "string") then
				options.caption = rawOptions.caption
			end
		end
		-- build style
		if options.width ~= "" then
			options.style = options.style .. options.width
		end
		if options.direction ~= "" then
			options.style = options.style .. options.direction
		end
	end
	if (options.legend) then
		options.ariahidechart = true
	end
	return options
end

--[[
	Render piechart.
	
	@param json_data JSON string with pie data.
]]
function p.renderPie(json_data, user_options)
	local data = mw.text.jsonDecode(json_data, mw.text.JSON_TRY_FIXING)
	local options = p.setupOptions(user_options)

	-- prepare
	local ok, total = p.prepareEntries(data, options)

	-- init render
	local html = "<div class='smooth-pie-container' style='"..options.style.."'>"

	-- error info
	if not ok then
		html = html .. renderErrors(data)
	end

	-- render legend
	if options.legend then
		html = html .. p.renderLegend(data, options)
	end

	-- render items
	local header, items, footer = p.renderEntries(ok, total, data, options)
	html = html .. header .. items .. footer

	-- end .smooth-pie-container
	html = html .. "\n</div>"

	return html
end

-- Prepare data (slices etc)
function p.prepareEntries(data, options)
	local sum = sumValues(data);
	-- force autoscale when over 100
	if (sum > 100) then
		options.autoscale = true
	end
	-- pre-format entries
	local ok = true
	local no = 0
	local total = #data
	for index, entry in ipairs(data) do
		no = no + 1
		if not prepareSlice(entry, no, sum, total, options) then
			no = no - 1
			ok = false
		end
	end
	total = no -- total valid

	return ok, total
end

function sumValues(data)
	local sum = 0;
	for _, entry in ipairs(data) do
		local value = entry.value
		if not (type(value) ~= "number" or value < 0) then
		    sum = sum + value
		end
	end
	return sum
end

-- render error info
function renderErrors(data)
	local html = "\n<ol class='chart-errors' style='display:none'>"
	for _, entry in ipairs(data) do
		if entry.error then
			entryJson = mw.text.jsonEncode(entry)
			html = html .. "\n<li>".. entryJson .."</li>"
		end
	end
	return html .. "\n</ol>\n"
end

-- Prepare single slice data (modifies entry).
-- @param no = 1..total
function prepareSlice(entry, no, sum, total, options)
	local autoscale = options.autoscale
	local value = entry.value
	if (type(value) ~= "number" or value < 0) then
		if autoscale then
			entry.error = "cannot autoscale unknown value"
			return false
		end
		value = 100 - sum
	end
	-- entry.raw only when scaled
	if autoscale then
		entry.raw = value
		value = (value / sum) * 100
	end
	entry.value = value

	-- prepare final label
	entry.label = prepareLabel(entry.label, entry)
	-- background, but also color for MW syntax linter
	entry.bcolor = backColor(entry, no, total) .. ";color:#000"

	return true
end

-- render legend for pre-processed entries
function p.renderLegend(data, options)
	local html = ""
	if options.caption ~= "" then
		html = "\n<div class='smooth-pie-legend-container'>"
		html = html .. "<div class='smooth-pie-caption'>" .. options.caption .. "</div>"
	end
	html = html .. "\n<ol class='smooth-pie-legend'>"
	for _, entry in ipairs(data) do
		if not entry.error then
			html = html .. renderLegendItem(entry, options)
		end
	end
	html = html .. "\n</ol>\n"
	if options.caption ~= "" then
		html = html .. "</div>\n"
	end
	return html
end
-- render legend item
function renderLegendItem(entry, options)
	-- invisible value (for a11y reasons this should not be used for important values!)
	if entry.visible ~= nil and entry.visible == false then
		return ""
	end

	local label = entry.label
	local bcolor = entry.bcolor
	local html = "\n<li>"
	html = html .. '<span class="l-color" style="'..forPrinting..bcolor..'"></span>'
	html = html .. '<span class="l-label">'..label..'</span>'
	return html .. "</li>"
end

-- Prepare data (slices etc)
function p.renderEntries(ok, total, data, options)
	-- cache for some items (small slices)
	p.cuts = mw.loadJsonData('Module:Piechart/cuts.json')

	local first = true
	local previous = 0
	local no = 0
	local items = ""
	local header = ""
	for index, entry in ipairs(data) do
		if not entry.error then
			no = no + 1
			if no == total then
				header = renderFinal(entry, options)
			else
				items = items .. renderOther(previous, entry, options)
			end
			previous = previous + entry.value
		end
	end
	local footer = '\n</div>'

	return header, items, footer
end
-- final, but header...
function renderFinal(entry, options)
	local label = entry.label
	local bcolor = entry.bcolor
	local size = options.size

	-- hide chart for readers, especially when legend is there
	local aria = ""
	if (options.ariahidechart) then
		aria = 'aria-hidden="true"'
	end

	-- slices container and last slice
	local style = 'width:'..size..'px;height:'..size..'px;'..bcolor..';'..forPrinting
	local html = [[
<div class="smooth-pie"
	style="]]..style..[["
	title="]]..p.extract_text(label)..[["
	]]..aria..[[
>]]
	return html
end
-- any other then final
function renderOther(previous, entry, options)
	local value = entry.value
	local label = entry.label
	local bcolor = entry.bcolor

	-- value too small to see
	if (value < 0.03) then
		mw.log('value too small', value, label)
		return ""
	end
	
	local html =  ""
	
	local size = ''
	-- mw.logObject({'v,p,l', value, previous, label})
	if (value >= 50) then
		html = sliceWithClass('pie50', 50, value, previous, bcolor, label)
	elseif (value >= 25) then
		html = sliceWithClass('pie25', 25, value, previous, bcolor, label)
	elseif (value >= 12.5) then
		html = sliceWithClass('pie12-5', 12.5, value, previous, bcolor, label)
	elseif (value >= 7) then
		html = sliceWithClass('pie7', 7, value, previous, bcolor, label)
	elseif (value >= 5) then
		html = sliceWithClass('pie5', 5, value, previous, bcolor, label)
	else
		-- 0-5%
		local cutIndex = round(value*10)
		if cutIndex < 1 then
		    cutIndex = 1
		end
		local cut = p.cuts[cutIndex]
		local transform = rotation(previous)
		html = sliceX(cut, transform, bcolor, label)
	end	
	-- mw.log(html)

	return html
end

-- round to int
function round(number)
    return math.floor(number + 0.5)
end

-- render full slice with specific class
function sliceWithClass(sizeClass, sizeStep, value, previous, bcolor, label)
	local transform = rotation(previous)
	local html =  ""
	html = html .. sliceBase(sizeClass, transform, bcolor, label)
	-- mw.logObject({'sliceWithClass:', sizeClass, sizeStep, value, previous, bcolor, label})
	if (value > sizeStep) then
		local extra = value - sizeStep
		transform = rotation(previous + extra)
		-- mw.logObject({'sliceWithClass; extra, transform', extra, transform})
		html = html .. sliceBase(sizeClass, transform, bcolor, label)
	end
	return html
end

-- render single slice
function sliceBase(sizeClass, transform, bcolor, label)
	local style = bcolor
	if transform ~= "" then
        style = style .. '; ' .. transform
    end
	return '\n\t<div class="'..sizeClass..'" style="'..style..'" title="'..p.extract_text(label)..'"></div>'
end

-- small slice cut to fluid size.
-- range in theory: 0 to 24.(9)% reaching 24.(9)% for cut = +inf
-- range in practice: 0 to 5%
function sliceX(cut, transform, bcolor, label)
	local path = 'clip-path: polygon(0% 0%, '..cut..'% 0%, 0 100%)'
	return '\n\t<div style="'..transform..'; '..bcolor..'; '..path..'" title="'..p.extract_text(label)..'"></div>'
end

-- translate value to turn rotation
function rotation(value)
	if (value > 0) then
		return string.format("transform: rotate(%.3fturn)", value/100)
	end
	return ''
end

-- Language sensitive float.
function formatNum(value)
	local lang = mw.language.getContentLanguage()
	
	-- doesn't do precision :(
	-- local v = lang:formatNum(value)
	
	local v = ""
	if (value < 10) then
		v = string.format("%.2f", value)
	else
		v = string.format("%.1f", value)
	end
	if (lang:getCode() == 'pl') then
		v = v:gsub("%.", ",")
	end
	return v
end
p._formatNum = formatNum

--[[
	Prepare final label.

	Typical tpl:
		"Abc: $v"
	will result in:
		"Abc: 23%" -- when values are percentages
		"Abc: 1234 (23%)" -- when values are autoscaled
	
	Advanced tpl:
		"Abc: $d ($p)" -- only works with autoscale
]]
function prepareLabel(tpl, entry)
	-- static tpl
	if tpl and not string.find(tpl, '$') then
		return tpl
	end

	-- format % value without %
	local p = formatNum(entry.value)

	-- default template
	if not tpl then
		tpl = "$v"
	end

	local label = "" 
	if entry.raw then
		label = tpl:gsub("%$p", p .. "%%"):gsub("%$d", entry.raw):gsub("%$v", entry.raw .. " (" .. p .. "%%)")
	else
		label = tpl:gsub("%$v", p .. "%%")
	end
	return label
end

-- default colors
-- source: https://colorbrewer2.org/#type=diverging&scheme=PRGn&n=6
local colorGroupSize = 3 -- must be at least 3
local colorGroups = 4
local colorPalette = {
-- green (from dark)
'#1b7837',
'#7fbf7b',
'#d9f0d3',
-- violet
'#762a83',
'#af8dc3',
'#e7d4e8',
-- red
'#d73027',
'#fc8d59',
'#fee090',
-- blue
'#4575b4',
'#91bfdb',
'#e0f3f8',
}
local lastColor = '#fff'
-- background color from entry or the default colors
function backColor(entry, no, total)
    if (type(entry.color) == "string") then
    	-- Remove unsafe characters from entry.color
    	local sanitizedColor = entry.color:gsub("[^a-zA-Z0-9#%-]", "")
        return 'background:' .. sanitizedColor
    else
    	local color = defaultColor(no, total)
        return 'background:' .. color
    end
end
-- color from the default colors
function defaultColor(no, total)
	local color = lastColor
	if no <= 0 then
		return color
	end
	local size = #colorPalette
	if not total or total == 0 then
		total = size + 1
	end
	local colorNo = defaultColorNo(no, total, size)
	if colorNo > 0 then
		color = colorPalette[colorNo]
	end
	return color
end
-- gets color number from default colors
-- trys to return a light color as the last one
-- 0 means white-ish color should be used
function defaultColorNo(no, total, size)
	local color = 0 -- special, lastColor
	if total == 1 then
		color = 1
	elseif total <= colorGroupSize * (colorGroups - 1) then
		if no < total then
			color = no
		else
			local groupIndex = ((no - 1) % colorGroupSize)
			if groupIndex == 0 then -- dark
				color = no+1
			elseif groupIndex == 1 then -- med
				color = no+1
			else
				color = no
			end
		end
	elseif no < total then
		color = ((no - 1) % size) + 1
	end
	return color
end
--[[
	Testing defaultColorNo:
	p.test_defaultColorNo(1, 12)
	p.test_defaultColorNo(2, 12)
	p.test_defaultColorNo(3, 12)
	p.test_defaultColorNo(4, 12)
	p.test_defaultColorNo(5, 12)
	p.test_defaultColorNo(6, 12)
]]
function p.test_defaultColorNo(total, size)
	for no=1,total do
		local color = defaultColorNo(no, total, size)
		mw.logObject({no=no, color=color})
	end
end

--[[
	trim string
	
	note:
	`(s:gsub(...))` returns only a string
	`s:gsub(...)` returns a string and a number
]]
function trim(s)
	return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

--[[
  Extract text from simple wikitext.
  
  For now only works with links.
]]
-- Tests:
-- mw.log(p.extract_text("[[candy|sweets]]: $v"))
-- mw.log(p.extract_text("[[sandwich]]es: $v"))
-- mw.log(p.extract_text("sandwich]]es: $v"))
-- mw.log(p.extract_text("sandwiches: $v"))
function p.extract_text(label)
    -- quick death mode
    if not label:find("%[%[") then
        return label
    end
    -- replace links with pipe (e.g., [[candy|sweets]])
    label = label:gsub("%[%[[^|%]]+|(.-)%]%]", "%1")
    -- replace simple links without pipe (e.g., [[sandwich]])
    label = label:gsub("%[%[(.-)%]%]", "%1")
    
    return label
end

--[[
  Parse classic template params into JSON.

From:  
|label1=cookies: $v |value1=11 |color1=goldenrod
|label2=sweets: $v |value2=20 |color2=darkred

To:
{"value":11,"color":"goldenrod","label":"cookies: $v"},
{"value":20,"color":"darkred","label":"sweets: $v"},

]]
function p.parseEnumParams(frame)
    local args = frame:getParent().args
    local result = {}
    
    local i = 1
	local sum = 0.0
	local hasCustomColor = false -- has last custom color
    while args["value" .. i] do
        -- value is required in this mode; it's also assumed to be 0..100
        local entry = { value = tonumber(args["value" .. i]) or 0 }
        -- label and color is optional
        if args["label" .. i] and args["label" .. i] ~= "" then
            entry.label = args["label" .. i] .. " ("..entry.value.."%)"
        end
        hasCustomColor = false
        if args["color" .. i] and args["color" .. i] ~= "" then
            entry.color = args["color" .. i]
            hasCustomColor = true
        end
        table.insert(result, entry)
        sum = sum + entry.value
        i = i + 1
    end
    
    -- support other value mapping
	local lang = mw.language.getContentLanguage()
	local langOther = "Other"
	if (lang:getCode() == 'pl') then
		langOther = "Inne"
	end
	local colorOther = "#FEFDFD" -- white-ish for custom colors for best chance and contrast
	
    if args["other"] and args["other"] ~= "" then
		local value = 100 - sum
		if value < 0 then
			value = 0
		end
        local otherEntry = { label = (args["other-label"] or langOther) .. " ("..formatNum(value).."%)" }
        if args["other-color"] and args["other-color"] ~= "" then
            otherEntry.color = args["other-color"]
        else
        	otherEntry.color = colorOther
        end
        table.insert(result, otherEntry)
    elseif sum < 100 then
    	if hasCustomColor then
        	table.insert(result, {visible = false, label = langOther .. " ($v)", color = colorOther})
        else
        	table.insert(result, {visible = false, label = langOther .. " ($v)"})
        end
	end
    
    local jsonString = mw.text.jsonEncode(result)
    return jsonString
end

return p