Module:Timeline

-- Module: Timeline -- Author: User:DuckeyD -- Inspired by: EasyTimeline by Erik Zachte -- Version: 1.1

--

local Timeline = {}

-- Dependencies local colors = require('Dev:Colors') local entrypoint = require('Dev:Entrypoint')

-- Constants local YEAR_LENGTH = 60 * 60 * 24 * 365.2 local MONTH_LENGTH = 60 * 60 * 24 * 30.44

-- Template function Timeline.main = entrypoint(Timeline)

-- Main invokable function function Timeline.create(frame) -- Load config from a given module local conf = mw.loadData('Module:'..assert(frame.args[1], 'Config module not passed as a frame argument')) local container = mw.html.create('div')

-- Style parameters local background_color = colors.parse('$color-text'):invert local text_color = colors.params['color-text'] local timeline_padding = 4 local labels_width = 120 local bar_height = 16 local bar_margin = 8 local chart_margin = 10 local chart_major = colors.params['color-text'] local chart_minor = colors.parse('$color-text'):alpha(50):hex local bar_background = background_color local bar_alpha = 40 local legend_columns = 3

-- Visibility parameters local timeline_hidden = false local legend_hidden = false local background_hidden = false

-- Custom styling from config if conf.style then if conf.style.background_color then background_color = colors.parse(conf.style.background_color) end -- CSS Color if conf.style.text_color then text_color = conf.style.text_color end -- CSS Color if conf.style.timeline_padding then timeline_padding = conf.style.timeline_padding end -- number if conf.style.labels_width then labels_width = conf.style.labels_width end -- number if conf.style.bar_height then bar_height = conf.style.bar_height end -- number if conf.style.bar_margin then bar_margin = conf.style.bar_margin end -- number if conf.style.chart_margin then chart_margin = conf.style.chart_margin end -- number if conf.style.chart_major then chart_major = conf.style.chart_major end -- CSS Color if conf.style.chart_minor then chart_minor = conf.style.chart_minor end -- CSS Color if conf.style.bar_background then bar_background = conf.style.bar_background end -- CSS Color if conf.style.bar_alpha then bar_alpha = conf.style.bar_alpha end -- number if conf.style.legend_columns then legend_columns = conf.style.legend_columns end -- number -- conf.style.label_format -- string end

-- Custom visibility settings from config if conf.hidden then if conf.hidden.timeline then timeline_hidden = conf.hidden.timeline end if conf.hidden.legend then legend_hidden = conf.hidden.legend end if conf.hidden.background then background_hidden = conf.hidden.background end end

local chart_width = 700 - (labels_width + timeline_padding*2 + chart_margin)

-- Root element styling container:css({       ['box-sizing'] = 'border-box',        ['display'] = 'flex',        ['width'] = '700px',        ['padding'] = timeline_padding..'px',        ['background-color'] = background_color:hex,        ['color'] = text_color,        ['flex-wrap'] = 'wrap',        ['margin'] = (timeline_padding*3)..'px 0'    })

-- Separator for Mercury container:node(mercuryOnly(mw.html.create('hr')))

-- Labels local labels = mw.html.create('div') labels:css({       ['width'] = labels_width..'px',        ['text-align'] = 'right',        ['line-height'] = bar_height..'px',        ['font-size'] = (0.75 * bar_height)..'px'    })

for _, label in pairs(assert(conf.dataset, 'No dataset found in config')) do       local label_elem = mw.html.create('div'):css({            ['height'] = bar_height..'px',            ['margin'] = bar_margin..'px 0'        }) local oasis_elem = oasisOnly(mw.html.create('span')) local mercury_elem = mercuryOnly(mw.html.create('span'))

-- Custom label formatting - add style.label_format string containing "$name" to config if conf.style and conf.style.label_format then oasis_elem:wikitext((string.gsub(conf.style.label_format, '$name', (assert(label.name, 'No "name" on label '.._))))) mercury_elem:wikitext(' \'\'\..string.gsub(conf.style.label_format, '$name', label.name)..'\'\'\) else oasis_elem:wikitext((assert(label.name, 'No "name" on label '.._))) mercury_elem:wikitext(' \'\'\..label.name..'\'\'\) end label_elem:node(oasis_elem) label_elem:node(mercury_elem) -- Main Mercury design local mercury_list = mercuryOnly(mw.html.create('ul')) for _, bar in ipairs(assert(label.bars, 'No "bars" on label '..label.name)) do           if assert(assert(conf.bar_types, 'No bar_types defined in config')[assert(bar.bar_type, 'No "bar_type" key on a bar in label '..label.name)], 'Bar type '..bar.bar_type..' not found').legend then local mercury_from = os.date('%d/%m/%Y', dateToTimestamp(assert(bar.from, 'No "from" key on a bar in label '..label.name), conf)) local mercury_till = os.date('%d/%m/%Y', dateToTimestamp(assert(bar.till, 'No "till" key on a bar in label '..label.name), conf)) local mercury_label = conf.bar_types[bar.bar_type].legend mercury_list:node(mw.html.create('li'):wikitext(mercury_label..': '..mercury_from..' – '..mercury_till)) end end label_elem:node(mercury_list)

labels:node(label_elem) end

container:node(labels)

-- Chart local chart = oasisOnly(mw.html.create('div'))

chart:css({       ['width'] = chart_width..'px',        ['margin-left'] = (chart_margin-1)..'px',        ['border-left'] = '1px solid '..chart_major,        ['border-bottom'] = '1px solid '..chart_major    })

if not background_hidden then local chart_bg, chart_offset = generateBackground(           assert(conf.from, 'No "from" key found in config'),            assert(conf.till, 'No "till" key found in config'),            chart_width, chart_major, chart_minor        ) chart:css({           ['background-image'] = chart_bg,            ['background-position-x'] = chart_offset        }) end

local chart_from = dateToTimestamp(conf.from) local chart_till = dateToTimestamp(conf.till)

local chart_diff = chart_till - chart_from

bar_background:alpha(bar_alpha)

-- Chart bars for _, label in pairs(conf.dataset) do       local bar_container = mw.html.create('div'):css({            ['height'] = bar_height..'px',            ['margin'] = bar_margin..'px 0',            ['background-color'] = bar_background:rgb,            ['position'] = 'relative'        })

for _, bar in ipairs(label.bars) do           local bar_from = dateToTimestamp(bar.from, conf) local bar_till = dateToTimestamp(bar.till, conf) bar_container:node(mw.html.create('div'):css({ ['height'] = bar_height..'px', ['background'] = assert(conf.bar_types[bar.bar_type].color, 'No color on bar type '..bar.bar_type), ['position'] = 'absolute', ['left'] = (((bar_from - chart_from)/chart_diff)*chart_width)..'px', ['right'] = (((chart_till - bar_till)/chart_diff)*chart_width)..'px' }))       end chart:node(bar_container) end

container:node(chart)

-- Timeline if not timeline_hidden then local chart_timeline = mw.html.create('div'):css({           ['width'] = chart_width..'px',            ['height'] = (0.75 * bar_height)..'px',            ['line-height'] = (0.75 * bar_height)..'px',            ['margin-left'] = (labels_width + chart_margin)..'px',            ['font-size'] = (0.625 * bar_height)..'px',            ['position'] = 'relative'        })

for i = math.floor(chart_from/YEAR_LENGTH) + 1, math.floor(chart_till/YEAR_LENGTH), 1 do           chart_timeline:node(mw.html.create('div'):wikitext(1970+i):css({ ['position'] = 'absolute', ['left'] = (((dateToTimestamp('01/01/'..(1970+i)) - chart_from)/chart_diff)*chart_width)..'px', ['transform'] = 'translate(-50%, 0)' }))       end

container:node(oasisOnly(chart_timeline)) end

-- Legend if not legend_hidden then local legend = mw.html.create('div'):css({           ['margin-top'] = bar_height..'px',            ['margin-left'] = (labels_width + chart_margin)..'px',            ['font-size'] = (0.75*bar_height)..'px',            ['width'] = chart_width..'px',            ['display'] = 'flex',            ['flex-wrap'] = 'wrap'        })

for _, bar_type in pairs(conf.bar_types) do           if bar_type.legend then local label_elem = mw.html.create('div'):css({                   ['display'] = 'flex',                    ['align-items'] = 'center',                    ['height'] = bar_height..'px',                    ['width'] = (100/legend_columns)..'%'                }) label_elem:node(mw.html.create('div'):css({ ['width'] = (0.75*bar_height)..'px', ['height'] = (0.75*bar_height)..'px', ['background'] = bar_type.color, ['margin-right'] = (bar_margin/2)..'px' }))               if bar_type.order then label_elem:css('order', bar_type.order) end label_elem:wikitext(bar_type.legend) legend:node(label_elem) end end

container:node(oasisOnly(legend)) end

-- Separator for Mercury container:node(mercuryOnly(mw.html.create('br'))) container:node(mercuryOnly(mw.html.create('hr')))

return container end

-- Helper functions function generateBackground(from, till, width, year_color, month_color) local start_date = dateToTimestamp(from) local end_date = dateToTimestamp(till)

local diff = end_date - start_date local background = 'repeating-linear-gradient(to right, transparent'

local month_multiplier = (MONTH_LENGTH*width)/diff

for i = 1, 11, 1 do       background = background..generateBar(i * month_multiplier, month_color) end background = background..generateBar(12 * month_multiplier, year_color)..')'   local offset = (start_date % YEAR_LENGTH)*width/diff    offset = '-'..offset..'px'    return background, offset end

function generateBar(pos, bar) pos = math.floor(pos) return ', transparent '..pos..'px, '..bar..' '..pos..'px, '..bar..' '..(pos+1)..'px, transparent '..(pos+1)..'px' end

function mercuryOnly(elem) return elem:css('display', 'none !important') end

function oasisOnly(elem) return elem:addClass('mobile-hidden') end

function dateToTimestamp(d, conf) if d == 'now' then return os.time end if d == 'start' and conf.from then return dateToTimestamp(conf.from) end if d == 'end' and conf.till then return dateToTimestamp(conf.till) end return os.time({       year = d:sub(7, 10),        month = d:sub(4, 5),        day = d:sub(1, 2),        hour = 0, min = 0, sec = 0    }) end

return Timeline

--