Module:Climate chart

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(' ') :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(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