Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 3: Line 3:
local cargo = mw.ext.cargo
local cargo = mw.ext.cargo
local html = mw.html
local html = mw.html
-- =========================================
-- Helpers
-- =========================================


local function trim(s)
local function trim(s)
Line 85: Line 89:
end
end


-- =========================================
-- Data loading
-- Data loading
-- =========================================


local function loadCharacters()
local function loadCharacters()
Line 106: Line 112:
end
end
end
end
return people
return people
end
end
Line 131: Line 138:
addUnique(people[child].parents, p1)
addUnique(people[child].parents, p1)
addUnique(people[p1].children, child)
addUnique(people[p1].children, child)
table.insert(people[p1].childLinks, {
table.insert(people[p1].childLinks, {
child = child,
child = child,
Line 144: Line 152:
addUnique(people[child].parents, p2)
addUnique(people[child].parents, p2)
addUnique(people[p2].children, child)
addUnique(people[p2].children, child)
table.insert(people[p2].childLinks, {
table.insert(people[p2].childLinks, {
child = child,
child = child,
Line 217: Line 226:
end
end


-- =========================================
-- Relationship helpers
-- Relationship helpers
-- =========================================


local function relationshipBadge(relType)
local function relationshipBadge(relType)
Line 256: Line 267:
local out = {}
local out = {}
local parents = getParents(people, root)
local parents = getParents(people, root)
for _, parentName in ipairs(parents) do
for _, parentName in ipairs(parents) do
local parent = people[parentName]
local parent = people[parentName]
Line 264: Line 276:
end
end
end
end
out = uniq(out)
out = uniq(out)
sortNames(people, out)
sortNames(people, out)
Line 329: Line 342:
local function getFamilyGroupsForRoot(people, root)
local function getFamilyGroupsForRoot(people, root)
local person = people[root]
local person = people[root]
if not person or not person.childLinks then return {} end
if not person then return {} end


local groups = {}
local groups = {}


for _, link in ipairs(person.childLinks) do
-- child-based groups
for _, link in ipairs(person.childLinks or {}) do
local key
local key
if isRealValue(link.unionID) then
if isRealValue(link.unionID) then
Line 356: Line 370:
birthOrder = tonumber(link.birthOrder) or 999
birthOrder = tonumber(link.birthOrder) or 999
})
})
end
-- partner-only groups, so spouses show even without children
for _, partner in ipairs(person.partners or {}) do
if isRealValue(partner) then
local found = false
for _, group in pairs(groups) do
if group.partner == partner then
found = true
break
end
end
if not found then
local union = findUnionBetween(people, root, partner)
local key = (union and union.unionID and ('union::' .. union.unionID)) or ('partner::' .. partner)
groups[key] = {
unionID = union and union.unionID or nil,
partner = partner,
children = {}
}
end
end
end
end


local out = {}
local out = {}
for _, group in pairs(groups) do
for _, group in pairs(groups) do
table.sort(group.children, function(a, b) return a.birthOrder < b.birthOrder end)
table.sort(group.children, function(a, b)
return (a.birthOrder or 999) < (b.birthOrder or 999)
end)
table.insert(out, group)
table.insert(out, group)
end
end
Line 367: Line 407:
local aSingle = not isRealValue(a.partner)
local aSingle = not isRealValue(a.partner)
local bSingle = not isRealValue(b.partner)
local bSingle = not isRealValue(b.partner)
if aSingle ~= bSingle then return aSingle end


local ap, bp = a.partner or '', b.partner or ''
if aSingle ~= bSingle then
return aSingle
end
 
local ap = a.partner or ''
local bp = b.partner or ''
local ad = (people[ap] and people[ap].displayName) or ap
local ad = (people[ap] and people[ap].displayName) or ap
local bd = (people[bp] and people[bp].displayName) or bp
local bd = (people[bp] and people[bp].displayName) or bp
Line 378: Line 422:
end
end


-- Rendering
-- =========================================
-- Rendering helpers
-- =========================================


local function renderCard(people, name, badgeText, extraClass)
local function renderCard(people, name, badgeText, extraClass)
Line 389: Line 435:
card:addClass(extraClass)
card:addClass(extraClass)
end
end
card:wikitext(makeLink(person.name, person.displayName))
card:wikitext(makeLink(person.name, person.displayName))


Line 438: Line 485:
local row = html.create('div')
local row = html.create('div')
row:addClass('kbft-row')
row:addClass('kbft-row')
for _, unit in ipairs(units) do
for _, unit in ipairs(units) do
if unit then row:node(unit) end
if unit then row:node(unit) end
end
end
return row
return row
end
local function renderChildCards(people, children)
local wrap = html.create('div')
wrap:addClass('kbft-children')
wrap:css('justify-content', 'center')
wrap:css('width', '100%')
for _, child in ipairs(children) do
wrap:node(
renderCard(
people,
child.name,
relationshipBadge(child.relationshipType)
)
)
end
return wrap
end
local function renderFamilyUnit(people, root, group)
local unit = html.create('div')
unit:addClass('kbft-sibling-unit')
local top = unit:tag('div')
top:addClass('kbft-family-main-wrap')
-- ONLY render partner, NOT root again
if isRealValue(group.partner) then
local union = findUnionBetween(people, root, group.partner)
local marriageYear = getMarriageYear(union)
top:node(renderCouple(people, nil, group.partner, marriageYear))
else
-- solo parent: render nothing on top
-- (root is already shown above)
end
-- children
if #group.children > 0 then
unit:tag('div'):addClass('kbft-child-down')
local childRow = unit:tag('div')
childRow:addClass('kbft-children')
for _, child in ipairs(group.children) do
childRow:node(
renderCard(
people,
child.name,
relationshipBadge(child.relationshipType)
)
)
end
end
return unit
end
end


local function renderUpperCoupleGeneration(people, couples)
local function renderUpperCoupleGeneration(people, couples)
if #couples == 0 then return nil end
if #couples == 0 then return nil end
local gen = html.create('div')
local gen = html.create('div')
gen:addClass('kbft-generation')
gen:addClass('kbft-generation')
Line 571: Line 562:


return gen
return gen
end
local function renderBranchChildren(people, children)
local childWrap = html.create('div')
childWrap:addClass('kbft-children')
for _, child in ipairs(children) do
childWrap:node(
renderCard(
people,
child.name,
relationshipBadge(child.relationshipType)
)
)
end
return childWrap
end
end


Line 579: Line 587:
local gen = html.create('div')
local gen = html.create('div')
gen:addClass('kbft-generation')
gen:addClass('kbft-generation')
gen:addClass('kbft-branches-generation')
local row = gen:tag('div')
row:addClass('kbft-branch-row')


-- ROW: ROOT + PARTNERS
for _, group in ipairs(groups) do
local partnerRow = gen:tag('div')
local unit = row:tag('div')
partnerRow:addClass('kbft-row')
unit:addClass('kbft-branch-unit')


-- root in center
local top = unit:tag('div')
partnerRow:node(renderSingleCard(people, root, 'kbft-focus-card'))
top:addClass('kbft-branch-top')


for _, group in ipairs(groups) do
if isRealValue(group.partner) then
if isRealValue(group.partner) then
partnerRow:node(renderSingleCard(people, group.partner))
top:node(renderSingleCard(people, group.partner))
else
-- solo branch spacer so root is not duplicated
top:tag('div')
:addClass('kbft-branch-spacer')
end
end
end


-- CHILDREN ROW
if #group.children > 0 then
local childrenRow = gen:tag('div')
unit:tag('div')
childrenRow:addClass('kbft-row')
:addClass('kbft-branch-drop')
childrenRow:css('margin-top', '10px')


for _, group in ipairs(groups) do
unit:node(renderBranchChildren(people, group.children))
local childUnit = html.create('div')
childUnit:addClass('kbft-sibling-unit')
 
childUnit:tag('div')
:addClass('kbft-child-down')
 
local childWrap = childUnit:tag('div')
childWrap:addClass('kbft-children')
 
for _, child in ipairs(group.children) do
childWrap:node(
renderCard(
people,
child.name,
relationshipBadge(child.relationshipType)
)
)
end
end
childrenRow:node(childUnit)
end
end


Line 624: Line 618:
end
end


-- =========================================
-- Public renderers
-- Public renderers
-- =========================================


local function renderConnectedForRoot(people, root)
local function renderConnectedForRoot(people, root)
Line 728: Line 724:
end
end


-- =========================================
-- Public functions
-- Public functions
-- =========================================


function p.tree(frame)
function p.tree(frame)

Revision as of 12:39, 30 March 2026

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

local p = {}

local cargo = mw.ext.cargo
local html = mw.html

-- =========================================
-- Helpers
-- =========================================

local function trim(s)
	if s == nil then return nil end
	s = tostring(s)
	s = mw.text.trim(s)
	if s == '' then return nil end
	return s
end

local function isRealValue(v)
	v = trim(v)
	if not v then return false end
	local lowered = mw.ustring.lower(v)
	return lowered ~= 'unknown' and lowered ~= 'none' and lowered ~= 'n/a'
end

local function addUnique(list, value)
	if not isRealValue(value) then return end
	for _, existing in ipairs(list) do
		if existing == value then return end
	end
	table.insert(list, value)
end

local function uniq(list)
	local out, seen = {}, {}
	for _, v in ipairs(list or {}) do
		if isRealValue(v) and not seen[v] then
			seen[v] = true
			table.insert(out, v)
		end
	end
	return out
end

local function getArg(frame, key)
	local v = frame.args[key]
	if isRealValue(v) then return trim(v) end
	local parent = frame:getParent()
	if parent then
		v = parent.args[key]
		if isRealValue(v) then return trim(v) end
	end
	return nil
end

local function getRoot(frame)
	return getArg(frame, 'root') or getArg(frame, 'person') or mw.title.getCurrentTitle().text
end

local function makeLink(name, displayName)
	if not isRealValue(name) then return '' end
	displayName = trim(displayName) or name
	return string.format('[[%s|%s]]', name, displayName)
end

local function ensurePerson(people, name)
	name = trim(name)
	if not isRealValue(name) then return nil end

	if not people[name] then
		people[name] = {
			name = name,
			displayName = name,
			parents = {},
			children = {},
			partners = {},
			unions = {},
			childLinks = {}
		}
	end
	return people[name]
end

local function sortNames(people, names)
	table.sort(names, function(a, b)
		local ad = (people[a] and people[a].displayName) or a
		local bd = (people[b] and people[b].displayName) or b
		return mw.ustring.lower(ad) < mw.ustring.lower(bd)
	end)
end

-- =========================================
-- Data loading
-- =========================================

local function loadCharacters()
	local results = cargo.query('Characters', 'Page,DisplayName', { limit = 5000 })
	local people = {}

	for _, row in ipairs(results) do
		local page = trim(row.Page)
		local displayName = trim(row.DisplayName)
		if isRealValue(page) then
			people[page] = {
				name = page,
				displayName = displayName or page,
				parents = {},
				children = {},
				partners = {},
				unions = {},
				childLinks = {}
			}
		end
	end

	return people
end

local function loadParentChild(people)
	local results = cargo.query(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{ limit = 5000 }
	)

	for _, row in ipairs(results) do
		local child = trim(row.Child)
		local p1 = trim(row.Parent1)
		local p2 = trim(row.Parent2)
		local unionID = trim(row.UnionID)
		local relationshipType = trim(row.RelationshipType)
		local birthOrder = tonumber(trim(row.BirthOrder)) or 999

		if isRealValue(child) then
			ensurePerson(people, child)

			if isRealValue(p1) then
				ensurePerson(people, p1)
				addUnique(people[child].parents, p1)
				addUnique(people[p1].children, child)

				table.insert(people[p1].childLinks, {
					child = child,
					otherParent = p2,
					unionID = unionID,
					relationshipType = relationshipType,
					birthOrder = birthOrder
				})
			end

			if isRealValue(p2) then
				ensurePerson(people, p2)
				addUnique(people[child].parents, p2)
				addUnique(people[p2].children, child)

				table.insert(people[p2].childLinks, {
					child = child,
					otherParent = p1,
					unionID = unionID,
					relationshipType = relationshipType,
					birthOrder = birthOrder
				})
			end
		end
	end
end

local function loadUnions(people)
	local results = cargo.query(
		'Unions',
		'UnionID,Partner1,Partner2,UnionType,Status,StartDate,EndDate,MarriageDate,DivorceDate,EngagementDate',
		{ limit = 5000 }
	)

	for _, row in ipairs(results) do
		local p1 = trim(row.Partner1)
		local p2 = trim(row.Partner2)

		if isRealValue(p1) then ensurePerson(people, p1) end
		if isRealValue(p2) then ensurePerson(people, p2) end

		if isRealValue(p1) and isRealValue(p2) then
			addUnique(people[p1].partners, p2)
			addUnique(people[p2].partners, p1)

			table.insert(people[p1].unions, {
				unionID = trim(row.UnionID),
				partner = p2,
				unionType = trim(row.UnionType),
				status = trim(row.Status),
				startDate = trim(row.StartDate),
				endDate = trim(row.EndDate),
				marriageDate = trim(row.MarriageDate),
				divorceDate = trim(row.DivorceDate),
				engagementDate = trim(row.EngagementDate)
			})

			table.insert(people[p2].unions, {
				unionID = trim(row.UnionID),
				partner = p1,
				unionType = trim(row.UnionType),
				status = trim(row.Status),
				startDate = trim(row.StartDate),
				endDate = trim(row.EndDate),
				marriageDate = trim(row.MarriageDate),
				divorceDate = trim(row.DivorceDate),
				engagementDate = trim(row.EngagementDate)
			})
		end
	end
end

local function finalizePeople(people)
	for _, person in pairs(people) do
		person.parents = uniq(person.parents)
		person.children = uniq(person.children)
		person.partners = uniq(person.partners)
	end
end

local function loadData()
	local people = loadCharacters()
	loadParentChild(people)
	loadUnions(people)
	finalizePeople(people)
	return people
end

-- =========================================
-- Relationship helpers
-- =========================================

local function relationshipBadge(relType)
	if not isRealValue(relType) then return nil end
	local t = mw.ustring.lower(relType)
	if t:find('adopt') then return 'adopted' end
	if t:find('step') then return 'step' end
	if t:find('bio') then return nil end
	return relType
end

local function findUnionBetween(people, name1, name2)
	if not isRealValue(name1) or not isRealValue(name2) then return nil end
	local person = people[name1]
	if not person or not person.unions then return nil end
	for _, union in ipairs(person.unions) do
		if union.partner == name2 then return union end
	end
	return nil
end

local function getMarriageYear(union)
	if not union then return nil end
	local raw = union.marriageDate or union.engagementDate or union.startDate
	if not isRealValue(raw) then return nil end
	return tostring(raw):match('^(%d%d%d%d)') or tostring(raw)
end

local function getParents(people, root)
	local person = people[root]
	if not person then return {} end
	local parents = uniq(person.parents)
	sortNames(people, parents)
	return parents
end

local function getGrandparents(people, root)
	local out = {}
	local parents = getParents(people, root)

	for _, parentName in ipairs(parents) do
		local parent = people[parentName]
		if parent then
			for _, gp in ipairs(parent.parents) do
				addUnique(out, gp)
			end
		end
	end

	out = uniq(out)
	sortNames(people, out)
	return out
end

local function getSiblings(people, root)
	local out, seen = {}, {}
	local person = people[root]
	if not person then return out end

	for _, parentName in ipairs(person.parents) do
		local parent = people[parentName]
		if parent then
			for _, childName in ipairs(parent.children) do
				if childName ~= root and not seen[childName] then
					seen[childName] = true
					table.insert(out, childName)
				end
			end
		end
	end

	out = uniq(out)
	sortNames(people, out)
	return out
end

local function getConnectedPeople(people, root)
	local out = {}
	local person = people[root]
	if not person then return out end

	for _, v in ipairs(person.parents) do addUnique(out, v) end
	for _, v in ipairs(person.children) do addUnique(out, v) end
	for _, v in ipairs(person.partners) do addUnique(out, v) end
	for _, v in ipairs(getSiblings(people, root)) do addUnique(out, v) end
	for _, v in ipairs(getGrandparents(people, root)) do addUnique(out, v) end

	out = uniq(out)
	sortNames(people, out)
	return out
end

local function getRootSiblingSequence(people, root)
	local siblings = getSiblings(people, root)
	local seq, inserted = {}, false
	local midpoint = math.floor(#siblings / 2) + 1

	for i, sib in ipairs(siblings) do
		if i == midpoint then
			table.insert(seq, root)
			inserted = true
		end
		table.insert(seq, sib)
	end

	if not inserted then
		table.insert(seq, root)
	end

	return seq
end

local function getFamilyGroupsForRoot(people, root)
	local person = people[root]
	if not person then return {} end

	local groups = {}

	-- child-based groups
	for _, link in ipairs(person.childLinks or {}) do
		local key
		if isRealValue(link.unionID) then
			key = 'union::' .. link.unionID
		elseif isRealValue(link.otherParent) then
			key = 'partner::' .. link.otherParent
		else
			key = 'single::' .. root
		end

		if not groups[key] then
			groups[key] = {
				unionID = link.unionID,
				partner = link.otherParent,
				children = {}
			}
		end

		table.insert(groups[key].children, {
			name = link.child,
			relationshipType = link.relationshipType,
			birthOrder = tonumber(link.birthOrder) or 999
		})
	end

	-- partner-only groups, so spouses show even without children
	for _, partner in ipairs(person.partners or {}) do
		if isRealValue(partner) then
			local found = false
			for _, group in pairs(groups) do
				if group.partner == partner then
					found = true
					break
				end
			end

			if not found then
				local union = findUnionBetween(people, root, partner)
				local key = (union and union.unionID and ('union::' .. union.unionID)) or ('partner::' .. partner)

				groups[key] = {
					unionID = union and union.unionID or nil,
					partner = partner,
					children = {}
				}
			end
		end
	end

	local out = {}
	for _, group in pairs(groups) do
		table.sort(group.children, function(a, b)
			return (a.birthOrder or 999) < (b.birthOrder or 999)
		end)
		table.insert(out, group)
	end

	table.sort(out, function(a, b)
		local aSingle = not isRealValue(a.partner)
		local bSingle = not isRealValue(b.partner)

		if aSingle ~= bSingle then
			return aSingle
		end

		local ap = a.partner or ''
		local bp = b.partner or ''
		local ad = (people[ap] and people[ap].displayName) or ap
		local bd = (people[bp] and people[bp].displayName) or bp
		return mw.ustring.lower(ad) < mw.ustring.lower(bd)
	end)

	return out
end

-- =========================================
-- Rendering helpers
-- =========================================

local function renderCard(people, name, badgeText, extraClass)
	if not isRealValue(name) then return nil end
	local person = people[name] or { name = name, displayName = name }

	local card = html.create('div')
	card:addClass('kbft-card')
	if isRealValue(extraClass) then
		card:addClass(extraClass)
	end

	card:wikitext(makeLink(person.name, person.displayName))

	if isRealValue(badgeText) then
		card:tag('div')
			:addClass('kbft-years')
			:wikitext(badgeText)
	end

	return card
end

local function renderSingleCard(people, name, extraClass)
	local wrap = html.create('div')
	wrap:addClass('kbft-single')
	wrap:node(renderCard(people, name, nil, extraClass))
	return wrap
end

local function renderCouple(people, leftName, rightName, marriageYear, leftClass, rightClass)
	if isRealValue(leftName) and isRealValue(rightName) then
		local wrap = html.create('div')
		wrap:addClass('kbft-couple')

		wrap:node(renderCard(people, leftName, nil, leftClass))

		local marriage = wrap:tag('div')
		marriage:addClass('kbft-marriage')

		if isRealValue(marriageYear) then
			marriage:tag('div')
				:addClass('kbft-marriage-year')
				:wikitext(marriageYear)
		end

		marriage:tag('div')
			:addClass('kbft-marriage-line')

		wrap:node(renderCard(people, rightName, nil, rightClass))
		return wrap
	end

	if isRealValue(leftName) then return renderSingleCard(people, leftName, leftClass) end
	if isRealValue(rightName) then return renderSingleCard(people, rightName, rightClass) end
	return nil
end

local function renderGenerationRow(units)
	local row = html.create('div')
	row:addClass('kbft-row')

	for _, unit in ipairs(units) do
		if unit then row:node(unit) end
	end

	return row
end

local function renderUpperCoupleGeneration(people, couples)
	if #couples == 0 then return nil end

	local gen = html.create('div')
	gen:addClass('kbft-generation')

	local units = {}
	for _, pair in ipairs(couples) do
		table.insert(units, renderCouple(people, pair[1], pair[2], nil))
	end

	gen:node(renderGenerationRow(units))
	return gen
end

local function buildGrandparentCouples(people, root)
	local parents = getParents(people, root)
	local couples = {}

	for _, parentName in ipairs(parents) do
		local parent = people[parentName]
		if parent then
			local gp = uniq(parent.parents)
			sortNames(people, gp)
			if #gp > 0 then
				table.insert(couples, { gp[1], gp[2] })
			end
		end
	end

	return couples
end

local function buildParentCouples(people, root)
	local parents = getParents(people, root)
	if #parents == 0 then return {} end
	return { { parents[1], parents[2] } }
end

local function renderFocalGeneration(people, root)
	local sequence = getRootSiblingSequence(people, root)

	local gen = html.create('div')
	gen:addClass('kbft-generation')

	local groupWrap = gen:tag('div')
	groupWrap:addClass('kbft-siblings')

	groupWrap:tag('div')
		:addClass('kbft-sibling-spine')

	local row = groupWrap:tag('div')
	row:addClass('kbft-sibling-row')

	for _, name in ipairs(sequence) do
		local unit = row:tag('div')
		unit:addClass('kbft-sibling-unit')

		unit:tag('div')
			:addClass('kbft-sibling-up')

		if name == root then
			unit:node(renderSingleCard(people, name, 'kbft-focus-card'))
		else
			unit:node(renderSingleCard(people, name))
		end
	end

	return gen
end

local function renderBranchChildren(people, children)
	local childWrap = html.create('div')
	childWrap:addClass('kbft-children')

	for _, child in ipairs(children) do
		childWrap:node(
			renderCard(
				people,
				child.name,
				relationshipBadge(child.relationshipType)
			)
		)
	end

	return childWrap
end

local function renderFamilyGroupsGeneration(people, root)
	local groups = getFamilyGroupsForRoot(people, root)
	if #groups == 0 then return nil end

	local gen = html.create('div')
	gen:addClass('kbft-generation')
	gen:addClass('kbft-branches-generation')

	local row = gen:tag('div')
	row:addClass('kbft-branch-row')

	for _, group in ipairs(groups) do
		local unit = row:tag('div')
		unit:addClass('kbft-branch-unit')

		local top = unit:tag('div')
		top:addClass('kbft-branch-top')

		if isRealValue(group.partner) then
			top:node(renderSingleCard(people, group.partner))
		else
			-- solo branch spacer so root is not duplicated
			top:tag('div')
				:addClass('kbft-branch-spacer')
		end

		if #group.children > 0 then
			unit:tag('div')
				:addClass('kbft-branch-drop')

			unit:node(renderBranchChildren(people, group.children))
		end
	end

	return gen
end

-- =========================================
-- Public renderers
-- =========================================

local function renderConnectedForRoot(people, root)
	local person = people[root]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
	end

	local connected = getConnectedPeople(people, root)
	local node = html.create('div')
	node:addClass('kbft-tree')

	node:tag('div')
		:addClass('kbft-title')
		:wikitext('Connected to ' .. makeLink(person.name, person.displayName))

	local gen = node:tag('div')
	gen:addClass('kbft-generation')

	local units = {}
	for _, name in ipairs(connected) do
		table.insert(units, renderSingleCard(people, name))
	end
	gen:node(renderGenerationRow(units))

	return tostring(node)
end

local function renderProfileForRoot(people, root)
	local person = people[root]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
	end

	local node = html.create('div')
	node:addClass('kbft-tree')

	node:tag('div')
		:addClass('kbft-title')
		:wikitext(makeLink(person.name, person.displayName))

	local function addSection(label, names)
		names = uniq(names)
		if #names == 0 then return end
		sortNames(people, names)

		node:tag('div')
			:addClass('kbft-title')
			:css('margin-top', '22px')
			:wikitext(label)

		local gen = node:tag('div')
		gen:addClass('kbft-generation')

		local units = {}
		for _, name in ipairs(names) do
			table.insert(units, renderSingleCard(people, name))
		end
		gen:node(renderGenerationRow(units))
	end

	addSection('Parents', person.parents)
	addSection('Partners', person.partners)
	addSection('Children', person.children)

	return tostring(node)
end

local function renderTreeForRoot(people, root)
	local person = people[root]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
	end

	local node = html.create('div')
	node:addClass('kbft-tree')

	node:tag('div')
		:addClass('kbft-title')
		:wikitext('Family Tree: ' .. makeLink(person.name, person.displayName))

	local gpGen = renderUpperCoupleGeneration(people, buildGrandparentCouples(people, root))
	if gpGen then
		node:node(gpGen)
		node:tag('div'):addClass('kbft-connector')
	end

	local parentGen = renderUpperCoupleGeneration(people, buildParentCouples(people, root))
	if parentGen then
		node:node(parentGen)
		node:tag('div'):addClass('kbft-connector')
	end

	node:node(renderFocalGeneration(people, root))

	local familyGen = renderFamilyGroupsGeneration(people, root)
	if familyGen then
		node:tag('div'):addClass('kbft-connector')
		node:node(familyGen)
	end

	return tostring(node)
end

-- =========================================
-- Public functions
-- =========================================

function p.tree(frame)
	local root = getRoot(frame)
	local people = loadData()
	return renderTreeForRoot(people, root)
end

function p.profile(frame)
	local root = getRoot(frame)
	local people = loadData()
	return renderProfileForRoot(people, root)
end

function p.connected(frame)
	local root = getRoot(frame)
	local people = loadData()
	return renderConnectedForRoot(people, root)
end

return p