/** * Bio card tooltip on hover for the sidebar graph. * * Shows full details: name, photo, birth, death, marriage, occupation, age. * Uses window.familyNavGraphI18n for translated labels. */ import { select } from "../d3.js"; let activeTooltip = null; let hideTimer = null; function t(key, ...args) { const i18n = window.familyNavGraphI18n || {}; let str = i18n[key] || key; for (const arg of args) { str = str.replace("__AGE__", arg); } return str; } export function showBioCard(data, cardElement, containerSelector) { hideTooltip(); const container = select(containerSelector); const containerRect = container.node().getBoundingClientRect(); const cardRect = cardElement.getBoundingClientRect(); // Position tooltip below the card, centered const left = cardRect.left - containerRect.left + cardRect.width / 2; const top = cardRect.bottom - containerRect.top + 6; const tooltip = container .append("div") .attr("class", "bio-card") .style("left", `${left}px`) .style("top", `${top}px`) .style("transform", "translateX(-50%)") .on("mouseenter", () => clearTimeout(hideTimer)) .on("mouseleave", () => scheduleHide()); // Header const header = tooltip.append("div").attr("class", "bio-header"); if (data.avatar) { header .append("img") .attr("src", data.avatar) .attr("alt", data.fullName || "") .attr("class", "bio-photo"); } const headerText = header.append("div").attr("class", "bio-header-text"); headerText.append("div").attr("class", "bio-name").text(data.fullName || "???"); const ageText = computeAge(data); if (ageText) { headerText.append("div").attr("class", "bio-age").text(ageText); } // Facts const facts = tooltip.append("div").attr("class", "bio-facts"); addFact(facts, t("Born"), data.birthDate, data.birthPlace); addFact(facts, t("Baptism"), data.baptismDate); addFact(facts, t("Marriage"), data.marriageDate); addFact(facts, t("Died"), data.deathDate, data.deathPlace); addFact(facts, t("Occupation"), data.occupation); addFact(facts, t("Residence"), data.residence); // Profile link tooltip .append("a") .attr("href", data.url) .attr("class", "bio-link") .text(t("View profile") + " \u2192"); activeTooltip = tooltip; } function addFact(container, label, value, place) { if (!value && !place) return; const row = container.append("div").attr("class", "bio-fact"); row.append("span").attr("class", "bio-fact-label").text(label); let display = value || ""; if (place) { display += display ? `, ${place}` : place; } row.append("span").attr("class", "bio-fact-value").text(display); } 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 ""; const birthYear = parseInt(data.birthYear, 10); if (isNaN(birthYear)) return ""; if (data.isDead) { 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); } if (data.deathYear) { const deathYear = parseInt(data.deathYear, 10); if (!isNaN(deathYear)) { return t("Died at age %s", deathYear - birthYear); } } return t("Deceased"); } 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); } const age = now.getFullYear() - birthYear; return t("Age ~%s", age); } function scheduleHide() { hideTimer = setTimeout(hideTooltip, 300); } export function hideTooltip() { clearTimeout(hideTimer); if (activeTooltip) { activeTooltip.remove(); activeTooltip = null; } } export function attachHoverBioCard(cardGroup, data, containerSelector) { cardGroup .on("mouseenter", function () { clearTimeout(hideTimer); showBioCard(data, this, containerSelector); }) .on("mouseleave", () => { scheduleHide(); }); }