Module:Timeline
Jump to navigation
Jump to search
-- Module: Timeline
-- Author: [[User:DuckeyD]]
-- Inspired by: EasyTimeline by Erik Zachte
-- Version: 1.1
-- <nowiki>
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('<br>\'\'\''..string.gsub(conf.style.label_format, '$name', label.name)..'\'\'\'')
else
oasis_elem:wikitext((assert(label.name, 'No "name" on label '.._)))
mercury_elem:wikitext('<br>\'\'\''..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..':<br>'..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
-- </nowiki>