Module:FamilyTree: Difference between revisions
From KB Lexicon
No edit summary |
No edit summary |
||
| Line 445: | Line 445: | ||
local function renderChildCards(people, children) | local function renderChildCards(people, children) | ||
local | local wrap = html.create('div') | ||
wrap:addClass('kbft-children') | |||
wrap:css('justify-content', 'center') | |||
wrap:css('width', '100%') | |||
for _, child in ipairs(children) do | for _, child in ipairs(children) do | ||
wrap:node( | |||
renderCard( | |||
people, | |||
child.name, | |||
relationshipBadge(child.relationshipType) | |||
) | |||
) | |||
end | end | ||
return | |||
return wrap | |||
end | end | ||
| Line 569: | Line 580: | ||
gen:addClass('kbft-generation') | gen:addClass('kbft-generation') | ||
local | local row = gen:tag('div') | ||
row:addClass('kbft-row') | |||
for _, group in ipairs(groups) do | for _, group in ipairs(groups) do | ||
local unit = renderFamilyUnit(people, root, group) | |||
-- CENTER ALL UNITS | |||
unit:css('align-items', 'center') | |||
row:node(unit) | |||
end | end | ||
return gen | return gen | ||
end | end | ||
Revision as of 12:23, 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
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 or not person.childLinks then return {} end
local groups = {}
for _, link in ipairs(person.childLinks) 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
local out = {}
for _, group in pairs(groups) do
table.sort(group.children, function(a, b) return a.birthOrder < b.birthOrder 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, bp = a.partner or '', 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
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 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
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 renderFamilyGroupsGeneration(people, root)
local groups = getFamilyGroupsForRoot(people, root)
if #groups == 0 then return nil end
local gen = html.create('div')
gen:addClass('kbft-generation')
local row = gen:tag('div')
row:addClass('kbft-row')
for _, group in ipairs(groups) do
local unit = renderFamilyUnit(people, root, group)
-- CENTER ALL UNITS
unit:css('align-items', 'center')
row:node(unit)
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