<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://attuproject.org/wiki?action=history&amp;feed=atom&amp;title=Module%3ATimeline</id>
	<title>Module:Timeline - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://attuproject.org/wiki?action=history&amp;feed=atom&amp;title=Module%3ATimeline"/>
	<link rel="alternate" type="text/html" href="https://attuproject.org/wiki?title=Module:Timeline&amp;action=history"/>
	<updated>2026-10-30T06:37:37Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.44.6</generator>
	<entry>
		<id>https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28843&amp;oldid=prev</id>
		<title>DoomBot: load_modules: sync</title>
		<link rel="alternate" type="text/html" href="https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28843&amp;oldid=prev"/>
		<updated>2026-05-31T23:02:15Z</updated>

		<summary type="html">&lt;p&gt;load_modules: sync&lt;/p&gt;
&lt;a href=&quot;https://attuproject.org/wiki?title=Module:Timeline&amp;amp;diff=28843&amp;amp;oldid=28827&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>DoomBot</name></author>
	</entry>
	<entry>
		<id>https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28827&amp;oldid=prev</id>
		<title>DoomBot: load_modules: sync</title>
		<link rel="alternate" type="text/html" href="https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28827&amp;oldid=prev"/>
		<updated>2026-05-31T21:38:21Z</updated>

		<summary type="html">&lt;p&gt;load_modules: sync&lt;/p&gt;
&lt;a href=&quot;https://attuproject.org/wiki?title=Module:Timeline&amp;amp;diff=28827&amp;amp;oldid=28817&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>DoomBot</name></author>
	</entry>
	<entry>
		<id>https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28817&amp;oldid=prev</id>
		<title>DoomBot: load_modules: sync</title>
		<link rel="alternate" type="text/html" href="https://attuproject.org/wiki?title=Module:Timeline&amp;diff=28817&amp;oldid=prev"/>
		<updated>2026-05-31T20:42:48Z</updated>

		<summary type="html">&lt;p&gt;load_modules: sync&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Module:Timeline&lt;br /&gt;
-- CSS-div timeline renderer for the Attu Project wiki.&lt;br /&gt;
-- Replaces EasyTimeline (PNG) with inline HTML/CSS that survives MediaWiki&amp;#039;s sanitizer.&lt;br /&gt;
--&lt;br /&gt;
-- Invoke:&lt;br /&gt;
--   {{#invoke:Timeline|main&lt;br /&gt;
--    |data=List_of_IgRS_Speakers_by_time_as_Speaker/Timeline&lt;br /&gt;
--    |period_start=1-1 1 PC&lt;br /&gt;
--    |period_end=present&lt;br /&gt;
--   }}&lt;br /&gt;
&lt;br /&gt;
local Cal = require(&amp;quot;Module:AttuCalendar&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
local M = {}&lt;br /&gt;
&lt;br /&gt;
-- Layout constants (mirror EasyTimeline PlotArea / barincrement).&lt;br /&gt;
local LABEL_W  = 150   -- px: left label column&lt;br /&gt;
local BAR_W    = 1150  -- px: bar area width&lt;br /&gt;
local CANVAS_W = LABEL_W + BAR_W&lt;br /&gt;
local ROW_H    = 20    -- px: row height (barincrement)&lt;br /&gt;
local BAR_H    = 16    -- px: full bar (ROW_H - 2*2)&lt;br /&gt;
local NARROW_H = 6     -- px: narrow overlay height&lt;br /&gt;
&lt;br /&gt;
-- Nation colour map — muted palette derived from flag hues.&lt;br /&gt;
local COLORS = {&lt;br /&gt;
    utlia      = &amp;quot;#C0392B&amp;quot;,  -- deep red&lt;br /&gt;
    akaria     = &amp;quot;#148F77&amp;quot;,  -- dark teal&lt;br /&gt;
    okrit      = &amp;quot;#1E8449&amp;quot;,  -- forest green&lt;br /&gt;
    tietero    = &amp;quot;#1A5276&amp;quot;,  -- dark blue&lt;br /&gt;
    niueyjar   = &amp;quot;#7D3C98&amp;quot;,  -- purple&lt;br /&gt;
    deysachin  = &amp;quot;#D35400&amp;quot;,  -- burnt orange&lt;br /&gt;
    nongba     = &amp;quot;#E87C8A&amp;quot;,  -- rose&lt;br /&gt;
    eee        = &amp;quot;#C9A800&amp;quot;,  -- dark amber&lt;br /&gt;
    casea      = &amp;quot;#2E86C1&amp;quot;,  -- medium sky blue&lt;br /&gt;
    faltir     = &amp;quot;#9E7B5A&amp;quot;,  -- tan/brown&lt;br /&gt;
    kel        = &amp;quot;#2E4057&amp;quot;,  -- dark slate&lt;br /&gt;
    spyron     = &amp;quot;#7F8C8D&amp;quot;,  -- steel gray&lt;br /&gt;
    joy        = &amp;quot;#B7770D&amp;quot;,  -- dark gold&lt;br /&gt;
    larossa    = &amp;quot;#1A237E&amp;quot;,  -- navy&lt;br /&gt;
    kalam      = &amp;quot;#0E9AA7&amp;quot;,  -- cyan-teal&lt;br /&gt;
    hapsaw     = &amp;quot;#B03A7A&amp;quot;,  -- deep rose/magenta&lt;br /&gt;
    steam      = &amp;quot;#6B6B00&amp;quot;,  -- olive&lt;br /&gt;
    tvaqi      = &amp;quot;#2AA198&amp;quot;,  -- solarized cyan (distinct from akaria/kalam)&lt;br /&gt;
    walst      = &amp;quot;#7B241C&amp;quot;,  -- dark maroon&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Nation display names in legend order.&lt;br /&gt;
local NATIONS = {&lt;br /&gt;
    { id = &amp;quot;utlia&amp;quot;,     name = &amp;quot;Utlia&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;akaria&amp;quot;,    name = &amp;quot;Akaria&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;okrit&amp;quot;,     name = &amp;quot;Okrit&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;tietero&amp;quot;,   name = &amp;quot;Tietero&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;niueyjar&amp;quot;,  name = &amp;quot;Niueyjar&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;deysachin&amp;quot;, name = &amp;quot;Deysachin&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;nongba&amp;quot;,    name = &amp;quot;Nongba&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;eee&amp;quot;,       name = &amp;quot;eee&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;casea&amp;quot;,     name = &amp;quot;Casea&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;faltir&amp;quot;,    name = &amp;quot;Faltir&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;kel&amp;quot;,       name = &amp;quot;Kelelemi&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;spyron&amp;quot;,    name = &amp;quot;Spyron&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;joy&amp;quot;,       name = &amp;quot;Joy&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;larossa&amp;quot;,   name = &amp;quot;La Rossa&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;kalam&amp;quot;,     name = &amp;quot;Kalam&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;hapsaw&amp;quot;,    name = &amp;quot;Hapshaw&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;steam&amp;quot;,     name = &amp;quot;Steamworks&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;tvaqi&amp;quot;,     name = &amp;quot;T&amp;#039;vaqi&amp;quot; },&lt;br /&gt;
    { id = &amp;quot;walst&amp;quot;,     name = &amp;quot;Wälstanland&amp;quot; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- ---------- data subpage ----------&lt;br /&gt;
&lt;br /&gt;
local function get_page_content(title)&lt;br /&gt;
    local t = mw.title.new(title)&lt;br /&gt;
    return t and t:getContent() or nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Parse {{TimelineBar|bar|label|color|start|end[|narrow]}} lines from raw wikitext.&lt;br /&gt;
-- Returns:&lt;br /&gt;
--   rows  — list of {id, label} in document order (unique by bar id)&lt;br /&gt;
--   segs  — {bar_id → list of {color, start, stop, narrow}}&lt;br /&gt;
local function parse_data(content)&lt;br /&gt;
    local rows    = {}&lt;br /&gt;
    local row_idx = {}&lt;br /&gt;
    local segs    = {}&lt;br /&gt;
&lt;br /&gt;
    for line in content:gmatch(&amp;quot;[^\n]+&amp;quot;) do&lt;br /&gt;
        -- Try 6-arg form first, then 5-arg.&lt;br /&gt;
        local bar, label, color, start_s, end_s, narrow =&lt;br /&gt;
            line:match(&amp;quot;^%s*{{TimelineBar|([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|}]+)|([^|}]*)}}%s*$&amp;quot;)&lt;br /&gt;
        if not bar then&lt;br /&gt;
            bar, label, color, start_s, end_s =&lt;br /&gt;
                line:match(&amp;quot;^%s*{{TimelineBar|([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|}]+)}}%s*$&amp;quot;)&lt;br /&gt;
            narrow = &amp;quot;&amp;quot;&lt;br /&gt;
        end&lt;br /&gt;
        if bar then&lt;br /&gt;
            bar     = mw.text.trim(bar)&lt;br /&gt;
            label   = mw.text.trim(label)&lt;br /&gt;
            color   = mw.text.trim(color):lower()&lt;br /&gt;
            start_s = mw.text.trim(start_s)&lt;br /&gt;
            end_s   = mw.text.trim(end_s)&lt;br /&gt;
            narrow  = mw.text.trim(narrow or &amp;quot;&amp;quot;):lower()&lt;br /&gt;
            if not row_idx[bar] then&lt;br /&gt;
                table.insert(rows, { id = bar, label = label })&lt;br /&gt;
                row_idx[bar] = #rows&lt;br /&gt;
            end&lt;br /&gt;
            if not segs[bar] then segs[bar] = {} end&lt;br /&gt;
            table.insert(segs[bar], {&lt;br /&gt;
                color  = color,&lt;br /&gt;
                start  = start_s,&lt;br /&gt;
                stop   = end_s,&lt;br /&gt;
                narrow = (narrow == &amp;quot;narrow&amp;quot;),&lt;br /&gt;
            })&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return rows, segs&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ---------- pixel helpers ----------&lt;br /&gt;
&lt;br /&gt;
-- Convert a fractional position [0,1] to absolute x in the bar area.&lt;br /&gt;
local function frac_to_x(frac)&lt;br /&gt;
    return LABEL_W + math.floor(frac * BAR_W)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Minimum 2px bar width to keep short terms visible.&lt;br /&gt;
local function frac_to_w(f1, f2)&lt;br /&gt;
    return math.max(2, math.floor((f2 - f1) * BAR_W))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Convert a date string to a fraction of [ps_num, pe_num] without re-parsing bounds.&lt;br /&gt;
local function frac_of(date_str, ps_num, pe_num)&lt;br /&gt;
    local d = Cal.parse_date(date_str)&lt;br /&gt;
    local span = pe_num - ps_num&lt;br /&gt;
    if span == 0 then return 0 end&lt;br /&gt;
    return (d - ps_num) / span&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ---------- renderers ----------&lt;br /&gt;
&lt;br /&gt;
local function render_segs(seg_list, row_top, ps_num, pe_num)&lt;br /&gt;
    -- Render full bars before narrow bars so narrow overlays draw on top.&lt;br /&gt;
    local full = {}&lt;br /&gt;
    local narrow = {}&lt;br /&gt;
    for _, seg in ipairs(seg_list) do&lt;br /&gt;
        local ok, f1 = pcall(frac_of, seg.start, ps_num, pe_num)&lt;br /&gt;
        local ok2, f2 = pcall(frac_of, seg.stop,  ps_num, pe_num)&lt;br /&gt;
        if ok and ok2 and f2 &amp;gt; f1 then&lt;br /&gt;
            f1 = math.max(0, math.min(1, f1))&lt;br /&gt;
            f2 = math.max(0, math.min(1, f2))&lt;br /&gt;
            local x   = frac_to_x(f1)&lt;br /&gt;
            local w   = frac_to_w(f1, f2)&lt;br /&gt;
            local bg  = COLORS[seg.color] or &amp;quot;#CCCCCC&amp;quot;&lt;br /&gt;
            local h, top_off&lt;br /&gt;
            if seg.narrow then&lt;br /&gt;
                h       = NARROW_H&lt;br /&gt;
                top_off = row_top + math.floor((ROW_H - NARROW_H) / 2)&lt;br /&gt;
            else&lt;br /&gt;
                h       = BAR_H&lt;br /&gt;
                top_off = row_top + 2&lt;br /&gt;
            end&lt;br /&gt;
            local div = string.format(&lt;br /&gt;
                &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:%dpx;top:%dpx;width:%dpx;height:%dpx;background:%s;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
                x, top_off, w, h, bg&lt;br /&gt;
            )&lt;br /&gt;
            if seg.narrow then narrow[#narrow + 1] = div&lt;br /&gt;
            else               full[#full + 1]   = div&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return table.concat(full) .. table.concat(narrow)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function render_label(label, row_top)&lt;br /&gt;
    return string.format(&lt;br /&gt;
        &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:0;top:%dpx;width:%dpx;height:%dpx;&amp;#039;&lt;br /&gt;
        .. &amp;#039;overflow:hidden;white-space:nowrap;font-size:11px;line-height:%dpx;&amp;quot;&amp;gt;%s&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
        row_top, LABEL_W - 4, ROW_H, ROW_H, label&lt;br /&gt;
    )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Render year axis ticks and major labels.&lt;br /&gt;
-- Returns the HTML string and the additional height consumed by the axis.&lt;br /&gt;
local function render_axis(ps_num, pe_num, bars_h)&lt;br /&gt;
    local out      = {}&lt;br /&gt;
    local axis_top = bars_h + 4&lt;br /&gt;
&lt;br /&gt;
    -- Draw from the first whole PC year inside the period to the last.&lt;br /&gt;
    local y_first = math.ceil(ps_num)&lt;br /&gt;
    local y_last  = math.floor(pe_num)&lt;br /&gt;
    local span    = pe_num - ps_num&lt;br /&gt;
&lt;br /&gt;
    for y = y_first, y_last do&lt;br /&gt;
        local frac = (y - ps_num) / span&lt;br /&gt;
        if frac &amp;gt;= 0 and frac &amp;lt;= 1 then&lt;br /&gt;
            local x        = frac_to_x(frac)&lt;br /&gt;
            local is_major = (y % 5 == 0)&lt;br /&gt;
            local tick_h   = is_major and 6 or 3&lt;br /&gt;
&lt;br /&gt;
            -- Tick mark&lt;br /&gt;
            out[#out + 1] = string.format(&lt;br /&gt;
                &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:%dpx;top:%dpx;width:1px;height:%dpx;background:#555;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
                x, axis_top, tick_h&lt;br /&gt;
            )&lt;br /&gt;
&lt;br /&gt;
            -- Year label (major ticks only); centred by subtracting half of a 40px box&lt;br /&gt;
            if is_major then&lt;br /&gt;
                out[#out + 1] = string.format(&lt;br /&gt;
                    &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:%dpx;top:%dpx;width:40px;&amp;#039;&lt;br /&gt;
                    .. &amp;#039;font-size:10px;text-align:center;white-space:nowrap;&amp;quot;&amp;gt;%d&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
                    x - 20, axis_top + 8, y&lt;br /&gt;
                )&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return table.concat(out), 28  -- axis consumes 28px below bar area&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function render_legend(used_colors)&lt;br /&gt;
    local items = {}&lt;br /&gt;
    for _, n in ipairs(NATIONS) do&lt;br /&gt;
        if used_colors[n.id] then&lt;br /&gt;
            items[#items + 1] = string.format(&lt;br /&gt;
                &amp;#039;&amp;lt;span style=&amp;quot;display:inline-block;width:12px;height:12px;&amp;#039;&lt;br /&gt;
                .. &amp;#039;background:%s;margin-right:4px;vertical-align:middle;&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;%s&amp;#039;,&lt;br /&gt;
                COLORS[n.id] or &amp;quot;#ccc&amp;quot;, n.name&lt;br /&gt;
            )&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cols    = 4&lt;br /&gt;
    local col_w   = 180   -- content width per column&lt;br /&gt;
    local gap     = 8     -- gap between columns&lt;br /&gt;
    local slot_w  = col_w + gap&lt;br /&gt;
    local n_rows  = math.ceil(#items / cols)&lt;br /&gt;
    local leg_h   = n_rows * 20&lt;br /&gt;
    local leg_w   = cols * slot_w - gap&lt;br /&gt;
    local out     = {&lt;br /&gt;
        string.format(&lt;br /&gt;
            &amp;#039;&amp;lt;div style=&amp;quot;position:relative;width:%dpx;height:%dpx;margin-top:8px;&amp;quot;&amp;gt;&amp;#039;,&lt;br /&gt;
            leg_w, leg_h&lt;br /&gt;
        )&lt;br /&gt;
    }&lt;br /&gt;
    for i, item in ipairs(items) do&lt;br /&gt;
        local col = (i - 1) % cols&lt;br /&gt;
        local row = math.floor((i - 1) / cols)&lt;br /&gt;
        out[#out + 1] = string.format(&lt;br /&gt;
            &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:%dpx;top:%dpx;width:%dpx;&amp;#039;&lt;br /&gt;
            .. &amp;#039;font-size:11px;white-space:nowrap;&amp;quot;&amp;gt;%s&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
            col * slot_w, row * 20, col_w, item&lt;br /&gt;
        )&lt;br /&gt;
    end&lt;br /&gt;
    out[#out + 1] = &amp;#039;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
    return table.concat(out)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ---------- main ----------&lt;br /&gt;
&lt;br /&gt;
function M.main(frame)&lt;br /&gt;
    local parent = frame:getParent()&lt;br /&gt;
    local args = (parent and next(parent.args)) and parent.args or frame.args&lt;br /&gt;
&lt;br /&gt;
    local data_page    = mw.text.trim(args.data         or &amp;quot;&amp;quot;)&lt;br /&gt;
    local period_start = mw.text.trim(args.period_start or &amp;quot;1-1 1 PC&amp;quot;)&lt;br /&gt;
    local period_end   = mw.text.trim(args.period_end   or &amp;quot;present&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    if data_page == &amp;quot;&amp;quot; then&lt;br /&gt;
        return &amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;Timeline: missing |data= parameter&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local content = get_page_content(data_page)&lt;br /&gt;
    if not content then&lt;br /&gt;
        return &amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;Timeline: data page not found: &amp;#039;&lt;br /&gt;
            .. mw.text.nowiki(data_page) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local rows, segs = parse_data(content)&lt;br /&gt;
    if #rows == 0 then&lt;br /&gt;
        return &amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;Timeline: no TimelineBar entries found&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Parse period bounds once.&lt;br /&gt;
    local ok1, ps_num = pcall(Cal.parse_date, period_start)&lt;br /&gt;
    local ok2, pe_num = pcall(Cal.parse_date, period_end)&lt;br /&gt;
    if not (ok1 and ok2) then&lt;br /&gt;
        return &amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;Timeline: invalid period_start or period_end&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Collect used colours for the legend.&lt;br /&gt;
    local used_colors = {}&lt;br /&gt;
    for _, row in ipairs(rows) do&lt;br /&gt;
        for _, seg in ipairs(segs[row.id] or {}) do&lt;br /&gt;
            used_colors[seg.color] = true&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Build the bar canvas.&lt;br /&gt;
    local n_rows  = #rows&lt;br /&gt;
    local bars_h  = n_rows * ROW_H&lt;br /&gt;
    -- Stripes rendered first so bars and labels draw on top.&lt;br /&gt;
    local stripes = {}&lt;br /&gt;
    local bar_parts = {}&lt;br /&gt;
&lt;br /&gt;
    for i, row in ipairs(rows) do&lt;br /&gt;
        local top = (i - 1) * ROW_H&lt;br /&gt;
        -- Alternating light stripe; even rows (0-indexed) get shading.&lt;br /&gt;
        if i % 2 == 0 then&lt;br /&gt;
            stripes[#stripes + 1] = string.format(&lt;br /&gt;
                &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:0;top:%dpx;width:%dpx;height:%dpx;background:rgba(80,130,200,0.18);&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
                top, CANVAS_W, ROW_H&lt;br /&gt;
            )&lt;br /&gt;
        end&lt;br /&gt;
        bar_parts[#bar_parts + 1] = render_label(row.label, top)&lt;br /&gt;
        bar_parts[#bar_parts + 1] = render_segs(segs[row.id] or {}, top, ps_num, pe_num)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local parts = stripes&lt;br /&gt;
    for _, v in ipairs(bar_parts) do parts[#parts + 1] = v end&lt;br /&gt;
&lt;br /&gt;
    -- Vertical line at the TT/PC boundary (start of 1 PC = fractional year 1.0).&lt;br /&gt;
    -- Note: parse_date maps TT→negative, PC→positive with no year 0; a chart&lt;br /&gt;
    -- spanning both eras has ~1 phantom unit of gap between -1 and 1 in pixel space.&lt;br /&gt;
    local span = pe_num - ps_num&lt;br /&gt;
    if span &amp;gt; 0 and ps_num &amp;lt;= 1 and pe_num &amp;gt;= 1 then&lt;br /&gt;
        local boundary_x = frac_to_x((1.0 - ps_num) / span)&lt;br /&gt;
        parts[#parts + 1] = string.format(&lt;br /&gt;
            &amp;#039;&amp;lt;div style=&amp;quot;position:absolute;left:%dpx;top:0;width:2px;height:%dpx;background:#2C3E50;&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;#039;,&lt;br /&gt;
            boundary_x, bars_h&lt;br /&gt;
        )&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local axis_html, axis_extra = render_axis(ps_num, pe_num, bars_h)&lt;br /&gt;
    parts[#parts + 1] = axis_html&lt;br /&gt;
&lt;br /&gt;
    local total_h = bars_h + axis_extra&lt;br /&gt;
    local canvas = string.format(&lt;br /&gt;
        &amp;#039;&amp;lt;div style=&amp;quot;position:relative;width:%dpx;height:%dpx;font-family:sans-serif;font-size:12px;&amp;quot;&amp;gt;&amp;#039;,&lt;br /&gt;
        CANVAS_W, total_h&lt;br /&gt;
    ) .. table.concat(parts) .. &amp;#039;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
    return &amp;#039;&amp;lt;div style=&amp;quot;overflow-x:auto;padding-bottom:16px;&amp;quot;&amp;gt;&amp;#039; .. canvas .. render_legend(used_colors) .. &amp;#039;&amp;lt;/div&amp;gt;&amp;#039;&lt;br /&gt;
        .. &amp;#039;[[Category:Pages with timelines]]&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return M&lt;/div&gt;</summary>
		<author><name>DoomBot</name></author>
	</entry>
</feed>