feat: Implement a family tree layout algorithm with generation calculation, member positioning, and relationship line rendering.
This commit is contained in:
parent
1ed3ba4056
commit
8613b9f8dd
1 changed files with 134 additions and 21 deletions
155
script.js
155
script.js
|
|
@ -197,36 +197,149 @@ function renderTree() {
|
||||||
const container = document.getElementById('family-cards-container');
|
const container = document.getElementById('family-cards-container');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// Simple grid layout for now (placeholder for complex tree algo)
|
if (familyData.length === 0) return;
|
||||||
familyData.forEach((member, index) => {
|
|
||||||
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`;
|
|
||||||
|
|
||||||
const genderIcon = member.gender === 'male' ? '👨' : (member.gender === 'female' ? '👩' : '👤');
|
// 1. Calculate Generations
|
||||||
|
const memberMap = new Map(familyData.map(m => [m.id, m]));
|
||||||
|
const generations = {};
|
||||||
|
const processed = new Set();
|
||||||
|
|
||||||
card.innerHTML = `
|
function getGeneration(id, depth = 0) {
|
||||||
<div class="card-avatar">${genderIcon}</div>
|
if (depth > 20) return 0; // Prevent infinite recursion
|
||||||
<div class="card-name">${member.firstName} ${member.lastName}</div>
|
const member = memberMap.get(id);
|
||||||
<div class="card-dates">
|
if (!member) return 0;
|
||||||
${new Date(member.birthDate).getFullYear()} - ${member.deathDate ? new Date(member.deathDate).getFullYear() : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
card.addEventListener('click', () => openEditModal(member));
|
let parentGen = -1;
|
||||||
container.appendChild(card);
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawLines();
|
// 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';
|
||||||
|
card.style.left = `${x}px`;
|
||||||
|
card.style.top = `${y}px`;
|
||||||
|
|
||||||
|
const genderIcon = member.gender === 'male' ? '👨' : (member.gender === 'female' ? '👩' : '👤');
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="card-avatar">${genderIcon}</div>
|
||||||
|
<div class="card-name">${member.firstName} ${member.lastName}</div>
|
||||||
|
<div class="card-dates">
|
||||||
|
${new Date(member.birthDate).getFullYear()} - ${member.deathDate ? new Date(member.deathDate).getFullYear() : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.addEventListener('click', () => openEditModal(member));
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Draw Lines
|
||||||
|
drawLines(positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawLines() {
|
function drawLines(positions) {
|
||||||
// Placeholder for SVG line drawing logic
|
|
||||||
const svg = document.getElementById('relationship-lines');
|
const svg = document.getElementById('relationship-lines');
|
||||||
svg.innerHTML = '';
|
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) {
|
function openEditModal(member) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue