Module:Timeline

From The Satanic Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Timeline/doc

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

-- <nowiki>

local Timeline = {}

-- Dependencies
local colors = require('Module:Colors')
local entrypoint = require('Module: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>