มอดูล:Graphical timeline

local getArgs = require('Module:Arguments').getArgs
local compressSparseArray = require('Module:TableTools').compressSparseArray
local p = {}
-- =============**
-- 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  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 "black"
	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)
	-- 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("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("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")
		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")
		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+)$"},
								)
	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 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- Units default to em (typography)em, the height and width of an 'M'.
See Graphical timeline for full documentation.
}}=
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+((widthUnit == "em" and tonumber(args.width)) or 10)
	if aw > args.computedWidth then
		args.computedWidth = aw
	end
	caption:css("width",checkDim(aw,"em",true))
	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
		argsattr = ignoreBlank(argsattr)
	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"))
	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