Module:Climate chart
Appearance
This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is semi-protected from editing. |
This module depends on the following other modules: |
This module uses TemplateStyles: |
Implements {{climate chart}}. At some point.
Usage
{{#invoke:Climate chart|function_name}}
local p = {}
local cfg = mw.loadData('Module:Climate chart/configuration')
-- we import this function to strip whitespace and normalize minus signs
-- in arguments, which can be done hyphen-minus (-), unicode minus (−), or html
-- reference minus − or similar. this is behavior preserving from the
-- wikitext version of the template
local pfexpr = mw.ext.ParserFunctions.expr
-- 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 = '−' 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(' ')
:done()
:tag('div')
:addClass('climate-chart-column-precip-bar')
:wikitext(' ')
: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(' ')
:done()
:tag('div')
:addClass('climate-chart-column-temp-bar')
:wikitext(' ')
: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(pfexpr(args[n]))
local maximum = tonumber(pfexpr(args[n+1]))
local precipitation = tonumber(pfexpr(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