Improve indicators and fix hover card age calculation

Draw ancestor/descendant indicators behind card, make them clickable
(navigates to person), use sharper box corners (rx=1). Fix age
calculation to use full birth/death dates instead of just years,
correctly accounting for whether the birthday has passed.
This commit is contained in:
2026-03-15 09:22:04 +01:00
parent c6727ed19c
commit 96d1425036
4 changed files with 112 additions and 66 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,78 @@ export function renderPersonCard(parent, person, config, onClick, containerSelec
onClick({ id: person.id, data });
});
// "More ancestors" indicator — drawn first so card renders on top
if (data.hasMoreAncestors) {
const ig = g.append("g")
.attr("class", "more-ancestors-indicator")
.style("cursor", "pointer")
.on("click", (event) => {
event.stopPropagation();
onClick({ id: person.id, data });
});
const bw = 10, bh = 7, gap = 4;
const cx = w - 25;
const topY = -14;
const leftX = cx - gap / 2 - bw;
const rightX = cx + gap / 2;
const barY = topY + bh;
// Lines first (behind boxes)
ig.append("line")
.attr("x1", leftX + bw / 2).attr("y1", barY)
.attr("x2", rightX + bw / 2).attr("y2", barY);
ig.append("line")
.attr("x1", cx).attr("y1", barY)
.attr("x2", cx).attr("y2", 0);
// Boxes on top of lines
ig.append("rect")
.attr("x", leftX).attr("y", topY)
.attr("width", bw).attr("height", bh)
.attr("rx", 1).attr("ry", 1);
ig.append("rect")
.attr("x", rightX).attr("y", topY)
.attr("width", bw).attr("height", bh)
.attr("rx", 1).attr("ry", 1);
}
// "More descendants" indicator — drawn first so card renders on top
if (data.hasMoreDescendants) {
const ig = g.append("g")
.attr("class", "more-descendants-indicator")
.style("cursor", "pointer")
.on("click", (event) => {
event.stopPropagation();
onClick({ id: person.id, data });
});
const bw = 10, bh = 7, gap = 4;
const cx = w - 25;
const boxTop = h + 7;
const leftX = cx - gap / 2 - bw;
const rightX = cx + gap / 2;
const barY = boxTop;
// Lines first (behind boxes)
ig.append("line")
.attr("x1", cx).attr("y1", h)
.attr("x2", cx).attr("y2", barY);
ig.append("line")
.attr("x1", leftX + bw / 2).attr("y1", barY)
.attr("x2", rightX + bw / 2).attr("y2", barY);
// Boxes on top of lines
ig.append("rect")
.attr("x", leftX).attr("y", boxTop)
.attr("width", bw).attr("height", bh)
.attr("rx", 1).attr("ry", 1);
ig.append("rect")
.attr("x", rightX).attr("y", boxTop)
.attr("width", bw).attr("height", bh)
.attr("rx", 1).attr("ry", 1);
}
// Card background
g.append("rect")
.attr("width", w)
@@ -125,66 +197,6 @@ export function renderPersonCard(parent, person, config, onClick, containerSelec
.text(truncateText(subtitle, maxTextWidth));
}
// "More ancestors" indicator — two small parent boxes at top-right
if (data.hasMoreAncestors) {
const ig = g.append("g").attr("class", "more-ancestors-indicator");
const bw = 10, bh = 7, gap = 4;
const cx = w - 25;
const topY = -14;
const leftX = cx - gap / 2 - bw;
const rightX = cx + gap / 2;
const barY = topY + bh;
// Lines first (behind boxes)
ig.append("line")
.attr("x1", leftX + bw / 2).attr("y1", barY)
.attr("x2", rightX + bw / 2).attr("y2", barY);
ig.append("line")
.attr("x1", cx).attr("y1", barY)
.attr("x2", cx).attr("y2", 0);
// Boxes on top
ig.append("rect")
.attr("x", leftX).attr("y", topY)
.attr("width", bw).attr("height", bh)
.attr("rx", 2).attr("ry", 2);
ig.append("rect")
.attr("x", rightX).attr("y", topY)
.attr("width", bw).attr("height", bh)
.attr("rx", 2).attr("ry", 2);
}
// "More descendants" indicator — two small child boxes at bottom-right
if (data.hasMoreDescendants) {
const ig = g.append("g").attr("class", "more-descendants-indicator");
const bw = 10, bh = 7, gap = 4;
const cx = w - 25;
const boxTop = h + 7; // below card bottom edge
const leftX = cx - gap / 2 - bw;
const rightX = cx + gap / 2;
const barY = boxTop; // bar at top of boxes
// Lines first (behind boxes)
ig.append("line")
.attr("x1", cx).attr("y1", h)
.attr("x2", cx).attr("y2", barY);
ig.append("line")
.attr("x1", leftX + bw / 2).attr("y1", barY)
.attr("x2", rightX + bw / 2).attr("y2", barY);
// Boxes on top
ig.append("rect")
.attr("x", leftX).attr("y", boxTop)
.attr("width", bw).attr("height", bh)
.attr("rx", 2).attr("ry", 2);
ig.append("rect")
.attr("x", rightX).attr("y", boxTop)
.attr("width", bw).attr("height", bh)
.attr("rx", 2).attr("ry", 2);
}
// Attach hover bio card
if (containerSelector) {
attachHoverBioCard(g, data, containerSelector);

View File

@@ -101,6 +101,17 @@ function addFact(container, label, value, place) {
row.append("span").attr("class", "bio-fact-value").text(display);
}
/**
* Try to parse a webtrees display date string into a Date object.
* Handles formats like "15 March 1985", "March 1985", "1985".
*/
function parseDate(dateStr) {
if (!dateStr) return null;
const d = new Date(dateStr);
if (!isNaN(d.getTime())) return d;
return null;
}
function computeAge(data) {
if (!data.birthYear) return "";
@@ -108,18 +119,41 @@ function computeAge(data) {
if (isNaN(birthYear)) return "";
if (data.isDead) {
// Try precise calculation from full dates
const birthDate = parseDate(data.birthDate);
const deathDate = parseDate(data.deathDate);
if (birthDate && deathDate) {
let age = deathDate.getFullYear() - birthDate.getFullYear();
const monthDiff = deathDate.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && deathDate.getDate() < birthDate.getDate())) {
age--;
}
return t("Died at age %s", age);
}
// Fallback to year-based
if (data.deathYear) {
const deathYear = parseInt(data.deathYear, 10);
if (!isNaN(deathYear)) {
const age = deathYear - birthYear;
return t("Died at age %s", age);
return t("Died at age %s", deathYear - birthYear);
}
}
return t("Deceased");
}
const currentYear = new Date().getFullYear();
const age = currentYear - birthYear;
// Living person — try precise calculation
const birthDate = parseDate(data.birthDate);
const now = new Date();
if (birthDate) {
let age = now.getFullYear() - birthDate.getFullYear();
const monthDiff = now.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDate.getDate())) {
age--;
}
return t("Age ~%s", age);
}
// Fallback to year-based approximation
const age = now.getFullYear() - birthYear;
return t("Age ~%s", age);
}