feat: Implement a family tree layout algorithm with generation calculation, member positioning, and relationship line rendering.

This commit is contained in:
Eivind Røsstad Skeie 2025-11-27 11:13:27 +01:00
parent 1ed3ba4056
commit 8613b9f8dd

131
script.js
View file

@ -197,13 +197,68 @@ function renderTree() {
const container = document.getElementById('family-cards-container');
container.innerHTML = '';
// Simple grid layout for now (placeholder for complex tree algo)
familyData.forEach((member, index) => {
if (familyData.length === 0) return;
// 1. Calculate Generations
const memberMap = new Map(familyData.map(m => [m.id, m]));
const generations = {};
const processed = new Set();
function getGeneration(id, depth = 0) {
if (depth > 20) return 0; // Prevent infinite recursion
const member = memberMap.get(id);
if (!member) return 0;
let parentGen = -1;
if (member.motherId) parentGen = Math.max(parentGen, getGeneration(member.motherId, depth + 1));
if (member.fatherId) parentGen = Math.max(parentGen, getGeneration(member.fatherId, depth + 1));
return parentGen + 1;
}
familyData.forEach(member => {
const gen = getGeneration(member.id);
if (!generations[gen]) generations[gen] = [];
generations[gen].push(member);
});
// 2. Position Members
const cardWidth = 220;
const cardHeight = 100; // Approx
const gapX = 40;
const gapY = 150;
const startY = 50;
// Sort generations to ensure we process 0, 1, 2...
const genKeys = Object.keys(generations).sort((a, b) => a - b);
// Store positions for line drawing
const positions = {}; // id -> {x, y, width, height}
genKeys.forEach(genKey => {
const members = generations[genKey];
// Sort members to try and keep spouses together (simple heuristic)
members.sort((a, b) => {
if (a.spouseId === b.id) return -1;
return 0;
});
// Center the row relative to the container
// We'll use a fixed large width for the container to allow scrolling
// Let's assume center is 1000px
const rowWidth = members.length * (cardWidth + gapX) - gapX;
let startX = Math.max(50, 1000 - rowWidth / 2);
members.forEach((member, index) => {
const x = startX + (index * (cardWidth + gapX));
const y = startY + (genKey * gapY);
positions[member.id] = { x, y, width: cardWidth, height: cardHeight };
const card = document.createElement('div');
card.className = 'family-card';
// Simple positioning
card.style.top = `${100 + (Math.floor(index / 5) * 180)}px`;
card.style.left = `${50 + ((index % 5) * 260)}px`;
card.style.left = `${x}px`;
card.style.top = `${y}px`;
const genderIcon = member.gender === 'male' ? '👨' : (member.gender === 'female' ? '👩' : '👤');
@ -218,15 +273,73 @@ function renderTree() {
card.addEventListener('click', () => openEditModal(member));
container.appendChild(card);
});
});
drawLines();
// 3. Draw Lines
drawLines(positions);
}
function drawLines() {
// Placeholder for SVG line drawing logic
function drawLines(positions) {
const svg = document.getElementById('relationship-lines');
svg.innerHTML = '';
// Logic to draw lines between parent/child/spouse would go here
// Set SVG size to match content
const maxX = Math.max(...Object.values(positions).map(p => p.x + p.width)) + 100;
const maxY = Math.max(...Object.values(positions).map(p => p.y + p.height)) + 100;
svg.setAttribute('width', Math.max(2000, maxX)); // Min width 2000 for scrolling
svg.setAttribute('height', Math.max(1000, maxY));
familyData.forEach(member => {
const pos = positions[member.id];
if (!pos) return;
// Parent Connections
[member.motherId, member.fatherId].forEach(parentId => {
if (parentId && positions[parentId]) {
const parentPos = positions[parentId];
// Draw line from bottom of parent to top of child
const x1 = parentPos.x + parentPos.width / 2;
const parentBottom = parentPos.y + 120; // Approx bottom of card
const x2 = pos.x + pos.width / 2;
const y2 = pos.y;
// Bezier curve
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const d = `M ${x1} ${parentBottom} C ${x1} ${parentBottom + 50}, ${x2} ${y2 - 50}, ${x2} ${y2}`;
path.setAttribute("d", d);
path.setAttribute("stroke", "#88a090"); // Moss color
path.setAttribute("stroke-width", "2");
path.setAttribute("fill", "none");
svg.appendChild(path);
}
});
// Spouse Connection
if (member.spouseId && positions[member.spouseId]) {
const spousePos = positions[member.spouseId];
// Only draw once (e.g., if id < spouseId)
if (member.id < member.spouseId) {
const x1 = pos.x + pos.width;
const y1 = pos.y + 60; // Middle of card approx
const x2 = spousePos.x;
const y2 = spousePos.y + 60;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const d = `M ${x1} ${y1} L ${x2} ${y2}`;
path.setAttribute("d", d);
path.setAttribute("stroke", "#1a472a"); // Forest color
path.setAttribute("stroke-width", "2");
path.setAttribute("stroke-dasharray", "5,5"); // Dashed for spouse
path.setAttribute("fill", "none");
svg.appendChild(path);
}
}
});
}
function openEditModal(member) {