Module:Climate chart

Permanently protected module
From Wikipedia, the free encyclopedia

local p = {}
local cfg = mw.loadData('Module:Climate chart/configuration')

-- from https://lua-users.org/wiki/SimpleRound
local function round(num, decimal_places)
	local mult = 10^(decimal_places or 0)
	return math.floor(num * mult + 0.5) / mult
end

local function arg_or_default(args, from_arg, default)
	local arg = mw.text.trim(args[from_arg] or '')
	if arg ~= '' then
		return arg
	else
		return default
	end
end

-- we only draw using the metric numbers
local function compute_column_draw_data(metric_year, max_precipitation)
	local column_draw_data = {}
	-- so many magic constants
	local precipitation_scale = math.max(1, max_precipitation / 750) -- 750 mm is the maximum for height
	
	for _, month in ipairs(metric_year) do
		local precipitation_bar_height = month.precipitation / 50 / precipitation_scale -- 50 is a magic constant
		local temperature_bar_displacement = month.minimum / 5 + 8
		local temperature_bar_height = (month.maximum - month.minimum) / 5
		local temperature_high_displacement = month.maximum / 5 + 8
		local temperature_low_displacement = month.minimum / 5 + 6.5
		
		table.insert(column_draw_data, {
			precipitation_height = precipitation_bar_height,
			temperature_height = temperature_bar_height,
			temperature_displacement = temperature_bar_displacement,
			temperature_high_displacement = temperature_high_displacement,
			temperature_low_displacement = temperature_low_displacement
		})
	end
	
	return column_draw_data
end

local function present_monthly_temperature(temperature)
	local rounded_temp = round(temperature, 0)
	local temperature_sign = ''
	if rounded_temp < 0 then temperature_sign = '&minus;' end
	local abs_temp = math.abs(rounded_temp)
	
	return temperature_sign .. abs_temp
end

local function draw_column(month_draw_data, month_data)
	
	local precipitation = month_data.precipitation
	local precipitation_decimal_places = precipitation < 10 and 1 or 0
	local rounded_precipitation = round(precipitation, precipitation_decimal_places)
	
	local high_temp = present_monthly_temperature(month_data.maximum)
	local low_temp = present_monthly_temperature(month_data.minimum)
	
	local column = mw.html.create('div')
	column:addClass('climate-chart-column')
	:tag('div')
		:addClass('climate-chart-column-spacer')
		:wikitext('&nbsp;')
		:done()
	:tag('div')
		:addClass('climate-chart-column-precip-bar')
		:wikitext('&nbsp;')
		:css('height', month_draw_data.precipitation_height .. 'em')
		:css('print-color-adjust', 'exact') -- css sanitizer doesn't accept yet
		:done()
	:tag('div')
		:addClass('climate-chart-column-value climate-chart-column-precip')
		:tag('span')
			:wikitext(rounded_precipitation)
			:done()
		:done()
	:tag('div')
		:addClass('climate-chart-column-spacer2')
		:wikitext('&nbsp;')
		:done()
	:tag('div')
		:addClass('climate-chart-column-temp-bar')
		:wikitext('&nbsp;')
		:css('bottom', month_draw_data.temperature_displacement .. 'em' )
		:css('height', month_draw_data.temperature_height .. 'em')
		:css('print-color-adjust', 'exact') -- css sanitizer doesn't accept yet
		:done()
	:tag('div')
		:addClass('climate-chart-column-value climate-chart-column-high-temp')
		:css('bottom', month_draw_data.temperature_high_displacement .. 'em')
		:tag('span')
			:wikitext(high_temp)
			:done()
		:done()
	:tag('div')
		:addClass('climate-chart-column-value climate-chart-column-low-temp')
		:css('bottom', month_draw_data.temperature_low_displacement .. 'em')
		:tag('span')
			:wikitext(low_temp)
			:done()
		:done()
	:done()
	return column
end

local function header_row()
	local month_row = mw.html.create('tr')
	for _, month in ipairs(cfg.i18n.months) do
		month_row:tag('th')
			:attr('scope', 'col')
			:wikitext(month)
			:done()
	end
	
	return month_row:allDone()
end

local function fill_nice_tables(args)
	local primary_table = {}
	local secondary_table = {}
	for n = 2, 37, 3 do
		local minimum = tonumber(args[n])
		local maximum = tonumber(args[n+1])
		local precipitation = tonumber(args[n+2])
		 -- we use the fact that `tonumber` returns nil if it gets not_a_string
		 -- _OR_ the empty string later, since the defaults are unit-specific
		table.insert(primary_table, {
			minimum = minimum,
			maximum = maximum,
			precipitation = precipitation
		})
		table.insert(secondary_table, {
			minimum = minimum,
			maximum = maximum,
			precipitation = precipitation
		})
	end
	return primary_table, secondary_table
end

local function c_to_f(temperature_in_c)
	return temperature_in_c * 1.8 + 32
end

local function f_to_c(temperature_in_f)
	return (temperature_in_f - 32) * 5/9
end

local function mm_to_in(precipitation_in_mm)
	return precipitation_in_mm / 25.4	
end

local function in_to_mm(precipitation_in_in)
	return precipitation_in_in * 25.4
end

local function convert_inplace(t, convert_temperature, convert_precipitation)
	for _, month in ipairs(t) do
		month.minimum = convert_temperature(month.minimum)
		month.maximum = convert_temperature(month.maximum)
		month.precipitation = convert_precipitation(month.precipitation)
	end
end

local function fill_in_nils(year_t, default_t)
	for _, month in ipairs(year_t) do
		if not month.precipitation then month.precipitation = default_t.precipitation end
		if not month.maximum then month.maximum = default_t.temperature_high end
		if not month.minimum then month.minimum = default_t.temperature_low end
	end
end

local function chart_rows(args, imperial)
	local metric_t
	local imperial_t
	local maximum_precipitation = tonumber(args.maxprecip)
	local imperial_max_precipitation
	local metric_max_precipitation
	local default_max_precipitation = 1
	
	if imperial then
		imperial_t, metric_t = fill_nice_tables(args)
		fill_in_nils(metric_t, cfg.metric_default)
		fill_in_nils(imperial_t, cfg.imperial_default)
		convert_inplace(metric_t, f_to_c, in_to_mm, imperial)
		if maximum_precipitation then
			imperial_max_precipitation = maximum_precipitation
			metric_max_precipitation = in_to_mm(maximum_precipitation)
		else
			imperial_max_precipitation = default_max_precipitation
			metric_max_precipitation = default_max_precipitation
		end
	else
		metric_t, imperial_t = fill_nice_tables(args)
		fill_in_nils(metric_t, cfg.metric_default)
		fill_in_nils(imperial_t, cfg.imperial_default)
		convert_inplace(imperial_t, c_to_f, mm_to_in, imperial)
		if maximum_precipitation then
			metric_max_precipitation = maximum_precipitation
			imperial_max_precipitation = mm_to_in(maximum_precipitation)
		else
			metric_max_precipitation = default_max_precipitation
			imperial_max_precipitation = default_max_precipitation
		end
	end
	
	local column_draw_data = compute_column_draw_data(metric_t, metric_max_precipitation)
	
	local metric_row = mw.html.create('tr')
	local imperial_row = mw.html.create('tr')
	local function add_columns(row, year)
		for i = 1, 12 do
			row:tag('td')
				:node(draw_column(column_draw_data[i], year[i]))
				:done()
		end
	end
	
	add_columns(metric_row, metric_t)
	add_columns(imperial_row, imperial_t)
	
	return metric_row, imperial_row
	
end

local function present_chart_content(args, imperial)
	
	local primary_row, secondary_row
	if imperial then
		secondary_row, primary_row = chart_rows(args, imperial)
	else
		primary_row, secondary_row = chart_rows(args, imperial)
	end
	
	local primary = mw.html.create('table')
		:addClass('climate-chart-primary climate-chart-internal')
		:node(header_row())
		:node(primary_row)
		:done()
		
	local secondary_chart = mw.html.create('table')
		:addClass('climate-chart-secondary climate-chart-internal')
		:node(header_row())
		:node(secondary_row)
		:done()
	
	local secondary_title = imperial and cfg.i18n.secondary_title_metric or cfg.i18n.secondary_title_imperial
	-- primary has html_chart
	
	return primary, {
		title = secondary_title,
		chart = secondary_chart
	}
end

local function wrap_secondary_content(chart_content, temp_explanation, precip_explanation)
	local ret = mw.html.create('div')
	ret:addClass('climate-chart-secondary mw-collapsible mw-collapsed')
		:tag('div')
			:addClass('climate-chart-secondary-title')
			:wikitext(chart_content.title)
			:done()
		:tag('div')
			:addClass('mw-collapsible-content')
			:node(chart_content.chart)
			:node(temp_explanation)
			:node(precip_explanation)
			:done()
	return ret
end

local function explain_bar(bar_type, text)
	local ret = mw.html.create('p')
	ret:addClass('climate-change-explain-bar-' .. bar_type)
		:tag('span')
			:wikitext(cfg.i18n.explainer_key)
			:done()
		:wikitext(text)
		:done()
	return ret
end

local function explain(imperial, bar_type, imperial_explanation, metric_explanation)
	if imperial then
		return explain_bar(bar_type, imperial_explanation),
			explain_bar(bar_type, metric_explanation)
	else
		return explain_bar(bar_type, metric_explanation),
			explain_bar(bar_type, imperial_explanation)
	end
end

local function add_source(source)
	if not source then return end
	return mw.html.create('p'):wikitext(string.format(cfg.i18n.source, source))
end

local function add_title_content(title)
	local ret = mw.html.create()
	ret:tag('div')
		:addClass('climate-chart-title')
		:wikitext(title)
		:done()
	:tag('div')
		:addClass('climate-chart-explainer')
		:wikitext(cfg.i18n.explainer)
		:done()
	return ret
end

function p._main(args)
	
	local float = arg_or_default(args, cfg.arg.float, nil)
	local float_class = nil
	if float then
		if float == 'right' then
			float_class = 'climate-chart-right'
		elseif float == 'left' then
			float_class = 'climate-chart-left'
		end
	end
	
	local clear = arg_or_default(args, cfg.arg.clear, nil) or float
	local units = string.lower(arg_or_default(args, cfg.arg.units, ''))
	local is_imperial_primary = units == cfg.keyword.imperial
	local title = add_title_content(arg_or_default(args, cfg.arg.title, ''))
	local primary_chart, secondary_chart_content = present_chart_content(
		args,
		is_imperial_primary
	)
	local primary_temp_explanation, secondary_temp_explanation = explain(
		is_imperial_primary,
		'temp',
		cfg.i18n.explainer_fahrenheit,
		cfg.i18n.explainer_celsius
	)
	local primary_precip_explanation, secondary_precip_explanation = explain(
		is_imperial_primary,
		'precip',
		cfg.i18n.explainer_in,
		cfg.i18n.explainer_mm
	)
	
	local source = add_source(arg_or_default(args, 'source', nil))
	
	local secondary_content = wrap_secondary_content(
		secondary_chart_content,
		secondary_temp_explanation,
		secondary_precip_explanation
	)
	
	local climate_chart = mw.html.create('div')
	climate_chart:addClass('climate-chart')
		:addClass(float_class)
		:css('clear', clear)
	
	climate_chart:node(title)
		:node(primary_chart)
		:node(primary_temp_explanation)
		:node(primary_precip_explanation)
		:node(source)
		:node(secondary_content)
		:allDone()
	return mw.getCurrentFrame():extensionTag{
		name = 'templatestyles', args = { src = 'Module:Climate chart/styles.css' }
	} .. tostring(climate_chart)
end

function p.main(frame)
	return p._main(frame:getParent().args)
end

return p