Module:Graphical timeline

local getArgs = require('Module:Arguments').getArgs

local compressSparseArray = require('Module:TableTools').compressSparseArray

local p = {}

-- Note for translators of this module:

-- This module depends on :Template:period color, :template:period start, and :template:period end.

-- Those templates must be implemented on the wiki. If the names are changed, they need to be changed here:

local periodColor = "period color"

local periodStart = "period start"

local periodEnd = "period end"

-- =================

-- UTILITY FUNCTIONS

-- =================

-- Default colors for first 28 bars/periods

local defaultColor = {"#6ca","#ff9","#6cf","#c96","#fcc","#9f9","#96c","#cc6","#ccc","#f66","#6c6","#99f","#c66","#f9c",

"#396","#ff3","#06c","#963","#c9c","#9c6","#c63","#c96","#999","#c03","#393","#939","#996","#f69"}

-- The default width of annotations (in em)

local defaultAW = 8

-- Previous version default width (in em)

local oldDefaultAW = 7

-- Function to turn blank arguments back into nil

-- Parameters:

-- s = a string argument

-- Returns

-- if s is empty, turn back into nil (considered false by Lua)

local function ignoreBlank(s)

if s == "" then

return nil

end

return s

end

-- Function to suppress incorrect CSS values

-- Parameters:

-- val = dimensional value

-- unit = unit of value

-- nonneg = [bool] value needs to be non-negative

-- formatstr = optional format string

-- Returns:

-- correct string for html, or nil if val is negative

local function checkDim(val, unit, nonneg, formatstr)

if not val then

return nil

end

val = tonumber(val)

if not val or (nonneg and val < 0) then

return nil

end

if formatstr then

return mw.ustring.format(formatstr,val)..unit

end

return val..unit

end

-- function to scan argument list for pattern

-- Parameters:

-- args = an argument dict that will be scanned for one or more patterns

-- patterns = a list of Lua string patters to scan for

-- other = a list of other argument specification lists

-- each element o corresponds to a new argument to produce in the results

-- o[1] = key in new argument list

-- o[2] = prefix of old argument

-- o[3] = suffix of old argument

-- Returns:

-- new argument list that matches patterns specified, with new key names

--

-- This function makes the Lua module scalable, by specifying a list of string patterns that

-- contain relevant arguments for a single graphical element, e.g., "period(%d+)". These

-- patterns should have exactly one capture that returns a number.

--

-- When such a pattern is detected, the number is extracted and then other arguments

-- with the same number is searched for. Thus, if "period57" is detected, other relevant

-- arguments like "period57-text" are searched for and, if non-empty, are copied to the

-- output list with a new argument key. Thus, there is {"text","period","-text"}, and

-- "period(%d+)" detects period57, the code will look for "period57-text" in the input

-- and copy it's value to "text" on the output.

--

-- This function thus pulls all relevant arguments for a single graphical item out, and

-- makes an argument list to call a function to produce a single element (such as a bar or note)

function p._scanArgs(args,patterns,other)

local result = {}

for _, p in pairs(patterns) do

for k, v in pairs(args) do

local m = tonumber(mw.ustring.match(k,p))

-- if there is a matching argument, and it's not blank

-- and we haven't handled that match yet, then find other

-- arguments and copy them into output arg list.

-- We have to handle blank arguments for backward compatibility with the template

-- we check for an existing output with item m to save time

if m and v ~= "" and not result[m] then

local singleResult = {}

for _, o in ipairs(other) do

local foundVal = args[(o[2] or "")..m..(o[3] or "")]

if foundVal then

singleResult[o[1]] = foundVal

end

end

-- A hack: for any argument number m, there is a magic list of default

-- colors. We copy that default color for m into the new argument list, in

-- case it's useful. After this, m is discarded

singleResult.defaultColor = defaultColor[m]

result[m] = singleResult

end

end

end

-- Squeeze out all skipped values. Thus, continguous argument numbers are not

-- required: the module can get called with bar3, bar17, bar59 and it will only produce

-- three bars, in numerical order that they were called (3, 17, 59)

return compressSparseArray(result)

end

-- Function to compute the numeric step in the timescale

-- Parameters:

-- p1, p2 = lower and upper bounds of timescale

-- Returns:

-- round step size that produces ~10 steps between p1 and p2

--

-- Implements Template:Calculate increment, except with a slight tweak:

-- The round value (0.1, 0.2, 0.5, 1.0) is selected based on minimum log

-- distance, so the thresholds are slightly tweaked

function p._calculateIncrement(p1, p2)

local d = math.abs(p1-p2)

if d < 1e-10 then

return 1e-10

end

local logd = math.log10(d)

local n = math.floor(logd)

local frac = logd-n

local prevPower = math.pow(10,n-1)

if frac < 0.5*math.log10(2) then

return prevPower

elseif frac < 0.5 then

return 2*prevPower

elseif frac < 0.5*math.log10(50) then

return 5*prevPower

else

return 10*prevPower

end

end

-- Signed power function for squashing timeline to be more readable

function p._signedPow(x,p)

if x < 0 then

return -math.pow(-x,p)

end

return math.pow(x,p)

end

-- Function to convert from time to location in HTML

-- Arguments:

-- t = time

-- from = earliest time in timeline

-- to = latest time in timeline

-- height = height of timeline (in some units)

-- scaling = method of scaling ('linear' or 'sqrt' or 'pow')

-- power = power law of scaling (if scaling='pow')

function p._scaleTime(t, from, to, height, scaling, power)

if scaling == 'pow' then

from = p._signedPow(from,power)

to = p._signedPow(to,power)

t = p._signedPow(t,power)

end

return height*(to-t)/(to-from)

end

-- Utility function to create HTML container for entire graphical timeline

-- Parameters:

-- container = HTML container for title

-- args = arguments passed to main

-- args["instance-id"] = unique string per Graphical timeline per page

-- args.embedded = is timeline embedded in another infobox?

-- args.align = float of timeline (default=right)

-- args.margin = uniform margin around timeline

-- args.bodyclass = CSS class for whole container

-- args.collapsible = make timeline collapsible

-- args.state = set collapse state

-- Returns;

-- html div object that is root of DOM for graphical timeline

--

-- CSS taken from previous version of Template:Grpahical timeline

local function createContainer(args)

args.align = args.align or "right"

local container = mw.html.create('table')

container:attr("id","Container"..(args["instance-id"] or ""))

container:attr("role","presentation")

container:addClass(args.bodyclass)

container:addClass("toccolours")

container:addClass("searchaux")

if not args.embedded then

if args.state == "collapsed" then

args.collapsible = true

container:addClass("mw-collapsed")

container:addClass("nomobile")

elseif args.state == "autocollapse" then

args.collapsible = true

container:addClass("autocollapse")

container:addClass("nomobile")

end

if args.collapsible then

container:addClass("mw-collapsible")

end

end

container:css("text-align","left")

container:css("padding","0 0.5em")

container:css("border-style",args.embedded and "none" or "solid")

if args.embedded then

container:css("margin","auto")

else

container:css("float",args.align)

if args.align == "right" or args.align == "left" then

container:css("clear",args.align)

end

local margins = {}

margins[1] = args.margin or "0.3em"

margins[2] = (args.align == "right" and 0) or args.margin or "1.4em"

margins[3] = args.margin or "0.8em"

margins[4] = (args.align == "left" and 0) or args.margin or "1.4em"

container:css("margin",table.concat(margins," "))

end

container:css("overflow","hidden")

return container

end

-- Utility function to create title for graphical timeline

-- Parameters:

-- args = arguments passed to main

-- args["instance-id"] = unique string per Graphical timeline per page

-- args["title-color"] = background color for title

-- args.title = title of timeline

-- Returns;

-- html div object that is the title

--

-- CSS taken from previous version of Template:Grpahical timeline

local function createTitle(container,args)

container:attr("id","Title"..(args["instance-id"] or ""))

local bottomPadding = args["link-to"] and (not args.embedded)

and (not args.collapsible) and "0" or "1em"

container:css("padding","1em 1em "..bottomPadding.." 1em")

local title = container:tag('div')

title:css("background-color",ignoreBlank(args["title-colour"] or args["title-color"] or "#77bb77"))

title:css("padding","0 0.2em 0 0.2em")

title:css("font-weight","bold")

title:css("text-align","center")

title:wikitext(args.title)

end

-- Utility function to create optional navbox header for timeline

-- Parameters:

-- container = container for navbox header

-- args = arguments passed to main

-- args.title = title of timeline

-- args["link-to"] = name of parent template (without namespace)

-- Returns;

-- html div object that is the navbox header

--

-- CSS taken from previous version of Template:Grpahical timeline

local function navboxHeader(container,args)

local frame = mw.getCurrentFrame()

container:attr("id","Navbox"..(args["instance-id"] or ""))

local topMargin = args.title and "0" or "0.2em"

container:css("padding","0")

container:css("margin",topMargin.." 1em 0 0")

container:css("text-align","right")

container:wikitext(frame:expandTemplate{title="Navbar",args={"Template:"..args["link-to"]}})

end

-- ==================

-- TIME AXIS AND BARS

-- ==================

--Function to create HTML time axis on left side of timeline

--Arguments:

-- container = HTML parent object

-- args = arguments passed to main

-- args.from = beginning (earliest) time of timeline

-- args.to = ending (latest) time of timeline

-- args.height = height of timeline

-- args["height-unit"] = unit of height (default args.unit)

-- args.unit = unit of measurement (default em)

-- args["instance-id"] = unique string per Graphical timeline per page

-- args["scale-increment"] = gap between time ticks (default=automatically computed)

-- args.scaling = method of scaling (linear or sqrt, linear by default)

-- args["label-freq"] = frequency of labels (per major tick)

-- Returns;

-- html div object for the time axis

--

-- CSS taken from previous version of Template:Grpahical timeline

function p._scalemarkers(container,args)

local height = tonumber(args.height) or 36

local unit = args["height-unit"] or args.unit or "em"

container:attr("id","Scale"..(args["instance-id"] or ""))

container:css("width","4.2em")

args.computedWidth = args.computedWidth+4.2

container:css("position","relative")

container:css("float","left")

container:css("font-size","100%")

container:css("height",checkDim(height,unit,true))

container:css("border-right","1px solid #242020")

local incr = args["scale-increment"] or p._calculateIncrement(args.from,args.to)

-- step through by half the desired increment, alternating small and large ticks

-- put labels every args["label-freq"] large ticks

local labelFreq = args["label-freq"] or 1

labelFreq = labelFreq*2 -- account for minor ticks

local halfIncr = incr/2

local tIndex = math.ceil(args.from/incr)*2 -- always start on a label

local toIndex = math.floor(args.to/halfIncr)

local tickCount = 0

while tIndex <= toIndex do

local t = tIndex*halfIncr

local div = container:tag("div")

div:css("float","right")

div:css("position","absolute")

div:css("right","-1px")

div:css("top",checkDim(p._scaleTime(t,args.from,args.to,height,args.scaling,args.power),

unit,nil,"%.2f"))

div:css("transform","translateY(-50%)")

local span = div:tag("span")

span:css("font-size","90%")

local text = ""

if tickCount%labelFreq == 0 then

if t < 0 then

text = mw.ustring.format("−%g ",-t)

else

text = mw.ustring.format("%g ",t)

end

end

if tickCount%2 == 0 then

text = text.."—"

else

text = text.."–"

end

span:wikitext(text)

tIndex = tIndex + 1

tickCount = tickCount + 1

end

end

-- Function to create timeline container div

-- Arguments:

-- container = HTML parent object

-- args = arguments passed to main

-- args["plot-colour"] = background color for timeline

-- args["instance-id"] = unique string per graphical timeline per page

-- args.height = height of timeline (36 by default)

-- args.width = width of timeline (10 by default)

-- args["height-unit"] = unit of height measurement (args.unit by default)

-- args["width-unit"] = unit of width measurement (args.unit by default)

-- args.unit = unit of measurement (em by default)

-- Returns:

-- timeline HTML object created

local function createTimeline(container,args)

local color = ignoreBlank(args["plot-colour"] or args["plot-color"])

container:attr("id","Timeline"..(args["instance-id"] or ""))

container:addClass("toccolours")

container:css("position","relative")

container:css("font-size","100%")

container:css("width","100%")

container:css("height",checkDim(args.height or 36,args["height-unit"] or args.unit or "em",true))

container:css("padding","0px")

container:css("float","left")

local width = args.width or 10

local widthUnit = args["width-unit"] or args.unit or "em"

container:css("width",checkDim(width,widthUnit,true))

if widthUnit == "em" then

args.timelineWidth = width

elseif widthUnit == "px" then

args.timelineWidth = width/13.3

else

args.timelineWidth = 10

end

args.computedWidth = args.computedWidth+args.timelineWidth

container:css("border","none")

container:css("background-color",color)

container:addClass("notheme")

return container

end

-- Function to draw single bar (or box)

-- Arguments:

-- container = parent HTML object for bar

-- args = arguments for this box

-- args.text = text to display

-- args.nudgedown = distance to nudge text down (in em)

-- args.nudgeup = distance to nudge text up (in em)

-- args.nudgeright = distance to nudge text right (in em)

-- args.nudgeleft = distance to nudge text left (in em)

-- args.colour = color of bar (default to color assigned to bar number)

-- args.left = fraction of timeline width for left edge of bar (default 0)

-- args.right = fraction of timeline width for right edge of bar (default 1)

-- args.to = beginning (bottom) of bar, in time units (default timeline begin)

-- args.from = end (top) of bar, in time units (default timeline end)

-- args.height = timeline height

-- args.width = timeline width

-- args["height-unit"] = units of timeline height (default args.unit)

-- args["width-unit"] = units of timeline width (default args.unit)

-- args.unit = units for timeline dimensions (default em)

-- args.border-style = CSS style for top/bottom of border (default "solid" if args.border)

function p._singleBar(container,args)

args.text = args.text or " "

args.nudgedown = (tonumber(args.nudgedown) or 0) - (tonumber(args.nudgeup) or 0)

args.nudgeright = (tonumber(args.nudgeright) or 0) - (tonumber(args.nudgeleft) or 0)

args.colour = args.colour or args.defaultColor

args.left = tonumber(args.left) or 0

args.right = tonumber(args.right) or 1

args.to = tonumber(args.to) or args["tl-to"]

args.from = tonumber(args.from) or args["tl-from"]

args.height = tonumber(args.height) or 36

args.width = tonumber(args.width) or 10

args["height-unit"] = args["height-unit"] or args.unit or "em"

args["width-unit"] = args["width-unit"] or args.unit or "em"

args.border = tonumber(args.border)

args["border-style"] = args["border-style"] or ((args.border or args["border-colour"]) and "solid") or "none"

-- the HTML element for the box/bar itself

local bar = container:tag('div')

bar:css("font-size","100%")

bar:css("background-color",ignoreBlank(args.colour or "#aaccff"))

bar:css("border-width",checkDim(args.border,args["height-unit"],true))

bar:css("border-color",ignoreBlank(args["border-colour"]))

bar:css("border-style",args["border-style"].." none")

bar:css("position","absolute")

bar:css("text-align","center")

bar:css("margin","0")

bar:css("padding","0")

bar:css("pointer-events", "none")

bar:addClass("notheme")

local bar_top = p._scaleTime(args.to,args["tl-from"],args["tl-to"],args.height,args.scaling,args.power)

local bar_bottom = p._scaleTime(args.from,args["tl-from"],args["tl-to"],args.height,args.scaling,args.power)

local bar_height = bar_bottom-bar_top

bar:css("top",checkDim(bar_top,args["height-unit"],nil,"%.3f"))

if args["border-style"] ~= "none" and args.border then

bar_height = bar_height-2*args.border

end

bar:css("height",checkDim(bar_height,args["height-unit"],true,"%.3f"))

bar:css("left",checkDim(args.left*args.width,args["width-unit"],nil,"%.3f"))

bar:css("width",checkDim((args.right-args.left)*args.width,args["width-unit"],true,"%.3f"))

-- within the bar, use a div to nudge text away from center

local textParent = bar

if not args.alignBoxText then

local nudge = bar:tag('div')

nudge:css("font-size","100%")

nudge:css("position","relative")

nudge:css("top",checkDim(args.nudgedown,"em",nil))

nudge:css("left",checkDim(args.nudgeright,"em",nil))

nudge:css("pointer-events", "none")

textParent = nudge

end

-- put text div as child of nudge div (if exists)

local text = textParent:tag('div')

text:css("position","relative")

text:css("text-align","center")

text:css("font-size",ignoreBlank(args.textsize))

text:css("vertical-align","middle")

text:addClass("notheme")

local text_bottom = -0.5*bar_height

text:css("display","block")

text:css("bottom",checkDim(text_bottom,args["height-unit"],nil,"%.3f"))

text:css("transform","translateY(-50%)")

text:css("z-index","5")

text:css("pointer-events", "initial")

text:wikitext(ignoreBlank(args.text))

end

-- Function to render all bars/boxes in timeline

-- Arguments:

-- container = parent HTML object

-- args = arguments to main function

--

-- Global (main) arguments are parsed, individual box arguments are picked out

-- and passed to p._singleBar() above

--

-- The function looks for bar*-left, bar*-right, bar*-from, or bar*-to,

-- where * is a string of digits. That string of digits is then used to

-- find corresponding parameters of the individual bar.

-- For example, if bar23-left is found, then bar23-colour turns into local colour,

-- bar23-left turns into local left, bar23-from turns into local from, etc.

function p._bars(container,args)

local barArgs = p._scanArgs(args,{"^bar(%d+)-left$","^bar(%d+)-right$","^bar(%d+)-from","^bar(%d+)-to"},

{{"text","bar","-text"},

{"textsize","bar","-font-size"},

{"nudgedown","bar","-nudge-down"},

{"nudgeup","bar","-nudge-up"},

{"nudgeright","bar","-nudge-right"},

{"nudgeleft","bar","-nudge-left"},

{"colour","bar","-colour"},

{"colour","bar","-color"},

{"border","bar","-border-width"},

{"border-colour","bar","-border-colour"},

{"border-colour","bar","-border-color"},

{"border-style","bar","-border-style"},

{"left","bar","-left"},

{"right","bar","-right"},

{"from","bar","-from"},

{"to","bar","-to"}})

-- The individual bar arguments are placed into the barArgs table

-- Iterating through barArgs picks out the

for _, barg in ipairs(barArgs) do

-- barg is a table with the local arguments for one bar.

-- barg needs to have some global arguments copied into it:

barg["tl-from"] = args.from

barg["tl-to"] = args.to

barg.height = args.height

barg.width = args.width

barg["height-unit"] = args["height-unit"]

barg["width-unit"] = args["width-unit"]

barg.unit = args.unit

barg.scaling = args.scaling

barg.power = args.power

barg.alignBoxText = not args["disable-box-align"]

-- call _singleBar with the local arguments for one bar

p._singleBar(container,barg)

end

end

-- Function to draw a bar corresponding to a geological period

-- Arguments:

-- container = parent HTML object

-- args = global arguments passed to main

--

-- This function is just like _bars(), above, except with defaults for periods:

-- a period bar is triggered by period* (* = string of digits)

-- all other parameters start with "period", not "bar"

-- colour, from, and to parameters default to data from named period

-- text is a wikilink to period article

function p._periods(container,args)

local frame = mw.getCurrentFrame()

local periodArgs = p._scanArgs(args,{"^period(%d+)$"},

{{"text","period","-text"},

{"textsize","period","-font-size"},

{"period","period"},

{"nudgedown","period","-nudge-down"},

{"nudgeup","period","-nudge-up"},

{"nudgeright","period","-nudge-right"},

{"nudgeleft","period","-nudge-left"},

{"colour","period","-colour"},

{"colour","period","-color"},

{"border-width","period","-border-width"},

{"border-colour","period","-border-colour"},

{"border-colour","period","-border-color"},

{"border-style","period","-border-style"},

{"left","period","-left"},

{"right","period","-right"},

{"from","period","-from"},

{"to","period","-to"}})

-- Iterate through period* arguments, translating much like bar* arguments

-- Supply period defaults to local arguments, also

for _, parg in ipairs(periodArgs) do

parg.text = parg.text or (""..parg.period.."")

parg.textsize = "90%"

parg.colour = parg.colour or frame:expandTemplate{title=periodColor,args={parg.period}}

parg.from = parg.from or tonumber("-"..frame:expandTemplate{title=periodStart,args={parg.period}})

parg.to = parg.to or tonumber("-"..frame:expandTemplate{title=periodEnd,args={parg.period}})

if tonumber(parg.from) < tonumber(args.from) then

parg.from = args.from

end

if tonumber(parg.to) > tonumber(args.to) then

parg.to = args.to

end

parg["tl-from"] = args.from

parg["tl-to"] = args.to

parg.height = args.height

parg.width = args.width

parg["height-unit"] = args["height-unit"]

parg["width-unit"] = args["width-unit"]

parg.unit = args.unit

parg.scaling = args.scaling

parg.power = args.power

parg.alignBoxText = not args["disable-box-align"]

p._singleBar(container,parg)

end

end

-- ===========

-- ANNOTATIONS

-- ===========

-- Function to render a single note (annotation)

-- Arguments:

-- container = parent HTML object

-- args = arguments for this single note

-- args.text = text to display in note

-- args.noarr = bool, true if no arrow should be used

-- args.height = height of timeline

-- args.unit = height units

-- args.at = position of annotation (in time units)

-- args.colour = color of text in note

-- args.textsize = size of text (default 90%)

-- args.nudgeright = nudge text (and arrow) to right (in em)

-- args.nudgeleft = nudge text (and arrow) to left (in em)

-- Following parameters are only applicable to "no arrow" case or when

-- args.alignArrow is false:

-- args.nudgedown = nudge text down (in em)

-- args.nudgeup = nudge text up (in em)

-- args.aw = annotation width (in em)

function p._singleNote(container,args)

-- Ensure some parameters default to sensible values

args.height = tonumber(args.height) or 36

args.at = tonumber(args.at) or 0.5*(args.to+args.from)

args.colour = args.colour or "var( --color-base, #000)"

args.aw = tonumber(args.aw)

-- if string is centering, use old width to not break it

or mw.ustring.find(args.text,"center",1,true) and oldDefaultAW

or defaultAW

args.textsize = args.textsize or "90%"

-- Convert 4 nudge arguments to 2 numeric signed nudge dimensions (right, down)

args.nudgeright = (tonumber(args.nudgeright) or 0)-(tonumber(args.nudgeleft) or 0)

args.nudgedown = (tonumber(args.nudgedown) or 0)-(tonumber(args.nudgeup) or 0)

-- Container should have no pointer events, only the text should.

-- This prevents issues with containers overlapping and blocking pointer events on links.

container:css("pointer-events", "none")

-- Two cases: no arrow, and arrow

-- For no arrow case, use previous CSS which works well to position text

if args.noarr then

-- First, place a bar that pushes annotation down to right spot

local bar = container:tag('div')

bar:addClass("annot-bar")

bar:css("width","auto")

bar:css("font-size","100%")

bar:css("position","absolute")

bar:css("text-align","center")

bar:css("pointer-events", "none")

bar:css("margin-top",checkDim(p._scaleTime(args.at,args.from,args.to,args.height,args.scaling,args.power),

args.unit,nil,"%.3f"))

-- Now, nudge the text per nudge dimensions

local nudge = bar:tag('div')

nudge:addClass("annot-nudge")

nudge:css("font-size","100%")

nudge:css("float","left")

nudge:css("position","relative")

nudge:css("text-align","left")

nudge:css("pointer-events", "none")

nudge:css("top",checkDim(args.nudgedown-0.75,"em",nil))

nudge:css("left",checkDim(args.nudgeright,"em",nil))

nudge:css("width",checkDim(args.aw,"em",true))

-- Finally, place a dev for the text

local text = nudge:tag('div')

text:css("position","relative")

text:css("width","auto")

text:css("z-index","10")

text:css("font-size",ignoreBlank(args.textsize))

text:css("color",ignoreBlank(args.colour))

text:css("vertical-align","middle")

text:css("line-height","105%")

text:css("bottom","0")

-- Ensure that the text can be interacted with:

text:css("pointer-events","initial")

text:wikitext(ignoreBlank(args.text))

else

-- In the arrow case, previous code didn't correctly line up the text

-- Now that we're in Lua, it's easy to use a table to hold the arrow against the text

-- One row: first td is arrow, second td is text

-- Table gets placed directly using top CSS and absolute position

local tbl = container:tag('table')

tbl:attr("role","presentation") -- warn screen readers this table is for layout only

-- choose a reasonable height for table, then position middle of that height in the timeline

tbl:css("position","absolute")

tbl:css("z-index","15")

local at_location = p._scaleTime(args.at,args.from,args.to,args.height,args.scaling,args.power)

tbl:css("top",checkDim(at_location,args.unit,nil,"%.3f"))

tbl:css("left",checkDim(args.nudgeright,"em",nil))

tbl:css("transform","translateY(-50%)")

tbl:css("padding","0")

tbl:css("margin","0")

tbl:css("font-size","100%")

local row = tbl:tag('tr')

local arrowCell = row:tag('td')

arrowCell:css("padding","0")

arrowCell:css("text-align","left")

arrowCell:css("vertical-align","middle")

local arrowSpan = arrowCell:tag('span')

arrowSpan:css("color",args.colour)

arrowSpan:wikitext("←") --- HTML for left-pointing arrow

local textCell = row:tag('td')

textCell:css("padding","0")

textCell:css("text-align","left")

textCell:css("vertical-align","middle")

local textParent = textCell

-- If disable-arrow-align is true, nudge the text per nudge dimensions:

if not args.alignArrow then

local nudge = textCell:tag('div')

nudge:addClass("annot-nudge")

nudge:css("font-size","100%")

nudge:css("float","left")

nudge:css("position","relative")

nudge:css("top",checkDim(args.nudgedown,"em",nil))

textParent = nudge

end

local text = textParent:tag('div')

text:css("z-index","10")

text:css("font-size",ignoreBlank(args.textsize))

text:css("color",ignoreBlank(args.colour))

text:css("display","block")

text:css("line-height","105%") --- don't crunch multiple lines of text

text:css("bottom","0")

-- Ensure that the text can be interacted with:

text:css("pointer-events","initial")

text:wikitext(ignoreBlank(args.text))

end

end

-- Function to render all annotations in timeline

-- Arguments:

-- container = parent HTML object

-- args = arguments to main function

--

-- Global (main) arguments are parsed, individual box arguments are picked out

-- and passed to p._singleNote() above

--

-- The function looks for note*, where * is a string of digits

-- That string of digits is then used to find corresponding parameters of the individual note.

-- For example, if note23 is found, then note23-colour turns into local colour,

-- note-at turns into local at, note-texdt turns into local text, etc.

--

-- args["annotation-width"] overrides automatically determined width of annotation div

function p._annotations(container,args)

local noteArgs = p._scanArgs(args,{"^note(%d+)$"},

{{"text","note"},

{"noarr","note","-remove-arrow"},

{"noarr","note","-no-arrow"},

{"textsize","note","-size"},

{"textsize","note","-font-size"},

{"nudgedown","note","-nudge-down"},

{"nudgeup","note","-nudge-up"},

{"nudgeright","note","-nudge-right"},

{"nudgeleft","note","-nudge-left"},

{"colour","note","-colour"},

{"colour","note","-color"},

{"at","note","-at"}})

if #noteArgs == 0 then

return

end

-- a div to hold all of the notes

local notes= container:tag('td')

notes:attr("id","Annotations"..(args["instance-id"] or ""))

notes:css("padding","0")

notes:css("margin","0.7em 0 0.7em 0")

notes:css("float","left")

notes:css("position","relative")

-- Is there a "real" note? If so, leave room for it

-- real is: is non-empty and (has arrow or isn't nudged left)

local realNote = false

for _, narg in ipairs(noteArgs) do

local left = (tonumber(narg.nudgeleft) or 0)-(tonumber(narg.nudgeright) or 0)

if narg.text ~= "" and (not narg.noarr or left <= 0) then

realNote = true

args.hasRealNote = true -- record realNote boolean in args for further use

break

end

end

-- width of notes holder depends on whethere there are any "real" notes

-- width can be overriden

local aw = tonumber(args["annotations-width"]) or (realNote and defaultAW) or 0

aw = aw+0.22*args.timelineWidth

notes:css("width",checkDim(aw,"em",true,"%.3f"))

args.computedWidth = args.computedWidth+aw

local height = tonumber(args.height) or 36

local unit = args["height-unit"] or args.unit or "em"

notes:css("height",checkDim(height,unit,true))

for _, narg in ipairs(noteArgs) do

--- copy required global parameters to local note args

narg.from = args.from

narg.to = args.to

narg.height = args.height

narg.unit = args["height-unit"] or args["width-unit"] or "em"

narg.aw = args["annotations-width"]

narg.alignArrow = not args["disable-arrow-align"]

narg.scaling = args.scaling

narg.power = args.power

p._singleNote(notes,narg)

end

end

-- ====================

-- LEGENDS AND CAPTIONS

-- ====================

-- Function to render a single legend (below the timeline)

-- Arguments:

-- container = parent HTML object

-- args = argument table for this legend

-- args.colour = color to show in square

-- args.text = text that describes color

function p._singleLegend(container,args)

if not args.text then -- if no text, not a sensible legend

return

end

args.colour = args.colour or args.defaultColor or "transparent"

local row = container:tag('tr')

local squareCell = row:tag('td')

squareCell:css("padding",0)

local square = squareCell:tag('span')

square:css("background",ignoreBlank(args.colour))

square:css("padding","0em .1em")

square:css("border","solid 1px #242020")

square:css("height","1.5em")

square:css("width","1.5em")

square:css("margin",".25em .9em .25em .25em")

square:wikitext(" ")

local textCell = row:tag('td')

textCell:css("padding",0)

local text = textCell:tag('div')

text:wikitext(args.text)

end

function p._legends(container,args)

local legendArgs = p._scanArgs(args,{"^legend(%d+)$"},

{{"text","legend"},

{"colour","bar","-colour"},

{"colour","bar","-color"},

{"colour","legend","-colour"},

{"colour","legend","-color"}

})

if #legendArgs == 0 then

return

end

local legendRow = container:tag('tr')

local legendCell = container:tag('td')

legendCell:attr("id","Legend"..(args["instance-id"] or ""))

legendCell:attr("colspan",3)

legendCell:css("padding","0 0.2em 0.7em 1em")

local legend = legendCell:tag('table')

legend:attr("id","Legend"..(args["instance-id"] or ""))

legend:attr("role","presentation")

legend:addClass("toccolours")

legend:css("margin-left","3.1em")

legend:css("border-style","none")

legend:css("float","left")

legend:css("clear","both")

for _,larg in ipairs(legendArgs) do

p._singleLegend(legend,larg)

end

end

local helpString = [=[

----

Usage instructions

----

Copy the text below, adding multiple bars, legends and notes as required.


Comments, enclosed in - -->, should be removed.

Remember:

  • You must use {{!}} wherever you want a {{!}} to be

: rendered in the timeline

  • Large borders will displace bars in many browsers
  • Text should not be wider than its containing bar,

: as this may cause compatibility issues

See {{tl|Graphical timeline}} for full documentation.

{{Graphical timeline/blank}}}}]=]

local function createCaption(container,args)

local captionRow = container:tag("tr")

local captionCell = captionRow:tag("td")

captionCell:attr("id","Caption"..(args["instance-id"] or ""))

captionCell:attr("colspan",3)

captionCell:css("padding","0")

captionCell:css("margin","0 0.2em 0.7em 0.2em")

local caption = captionCell:tag("div")

caption:attr("id","Caption"..(args["instance-id"] or ""))

caption:addClass("toccolours")

if args.embedded then

caption:css("margin","0 auto")

caption:css("float","left")

else

caption:css("margin","0 0.5em")

end

caption:css("border-style","none")

caption:css("clear","both")

caption:css("text-align","center")

local widthUnit = args["width-unit"] or args.unit or "em"

local aw = tonumber(args["annotations-width"]) or (args.hasRealNote and defaultAW) or -0.25

aw = aw+5+args.timelineWidth

if aw > args.computedWidth then

args.computedWidth = aw

end

caption:css("width",checkDim(aw,"em",true,"%.3f"))

caption:wikitext((args.caption or "")..((args.help and args.help ~= "off" and helpString) or ""))

end

function p._main(args)

-- For backward compatibility with template, all empty arguments are accepted.

-- But, for some parameters, empty will cause a Lua error, so for those, we convert

-- empty to nil.

for _, attr in pairs({"title","link-to","embedded","align","margin",

"height","width","unit","height-unit","width-unit","scale-increment",

"annotations-width","disable-arrow-align","disable-box-align","from","to"}) do

args[attr] = ignoreBlank(args[attr])

end

-- Check that to > from, and that they're both defined

local from = tonumber(args.from) or 0

local to = tonumber(args.to) or 0

if from > to then

args.from = to

args.to = from

else

args.from = from

args.to = to

end

if args.scaling == 'sqrt' then

args.scaling = 'pow'

args.power = 0.5

end

if args.scaling == 'pow' then

args.power = args.power or 0.5

end

args.computedWidth = 1.7

-- Create container table

local container = createContainer(args)

-- TITLE

if args.title and not args.embedded then

local titleRow = container:tag('tr')

local titleCell = titleRow:tag('td')

titleCell:attr("colspan",3)

createTitle(titleCell,args)

end

-- NAVBOX HEADER

if args["link-to"] and not args.embedded then

local navboxRow = container:tag('tr')

local navboxCell = navboxRow:tag('td')

navboxCell:attr("colspan",3)

navboxHeader(navboxCell,args)

end

local centralRow = container:tag('tr')

centralRow:css("vertical-align","top")

-- SCALEBAR

local scaleCell = centralRow:tag('td')

scaleCell:css("padding","0")

scaleCell:css("margin","0.7em 0 0.7em 0")

p._scalemarkers(scaleCell,args)

-- TIMELINE

local timelineCell = centralRow:tag('td')

timelineCell:css("padding","0")

timelineCell:css("margin","0.7em 0 0.7em 0")

local timeline = createTimeline(timelineCell,args)

-- PERIODS

p._periods(timeline,args)

-- BARS

p._bars(timeline,args)

-- ANNOTATIONS

p._annotations(centralRow,args)

-- LEGEND

p._legends(container,args)

-- CAPTION

createCaption(container,args)

container:css("min-width",checkDim(args.computedWidth,"em",true,"%.3f"))

return container

end

function p.main(frame)

local args = getArgs(frame,{frameOnly=false,parentOnly=false,parentFirst=true,removeBlanks=false})

return tostring(p._main(args):allDone())

end

return p