Initial commit - GEDCOM genealogy visualizer
This commit is contained in:
39289
Input/patrickb44_2026-02-01.ged
Normal file
39289
Input/patrickb44_2026-02-01.ged
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Input/patrickb44_2026-02-01.zip
Normal file
BIN
Input/patrickb44_2026-02-01.zip
Normal file
Binary file not shown.
BIN
Resources/patrickb44_2026-02-01.zip
Normal file
BIN
Resources/patrickb44_2026-02-01.zip
Normal file
Binary file not shown.
903
index.html
Normal file
903
index.html
Normal file
@@ -0,0 +1,903 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visualiseur Généalogie - Geneanet</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
header > div:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.person-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.person-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.person-item:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.person-info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.family-tree {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.tree-node.selected {
|
||||
background: #e8eaf6;
|
||||
border-left-color: #764ba2;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tree-selector {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
overflow-x: auto;
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tree-box {
|
||||
display: inline-block;
|
||||
border: 2px solid #667eea;
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
background: white;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree-box.sosa1 {
|
||||
border: 3px solid #764ba2;
|
||||
background: #f3e5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tree-generation {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.generation-title {
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.generation-people {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-header:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #667eea;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.collapsible-content.collapsed {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 2px solid #667eea;
|
||||
border-top: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.suggestions.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: #f3e5f5;
|
||||
}
|
||||
|
||||
.suggestion-item.selected {
|
||||
background: #e8eaf6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>📊 Visualiseur Généalogie Geneanet</h1>
|
||||
<p>Chargez votre fichier GEDCOM pour visualiser votre arbre généalogique</p>
|
||||
<div class="file-upload">
|
||||
<input type="file" id="fileInput" accept=".ged,.gedcom" placeholder="Sélectionnez un fichier GEDCOM">
|
||||
<button onclick="handleFileUpload()">Charger</button>
|
||||
</div>
|
||||
<div id="error" class="error" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button onclick="downloadAsCSV()" id="csvBtn" style="display:none; align-self: flex-start;">📥 Télécharger CSV</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<!-- Stats -->
|
||||
<div class="card">
|
||||
<h2>📈 Statistiques</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Personnes</div>
|
||||
<div class="stat-value" id="statPersonnes">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Familles</div>
|
||||
<div class="stat-value" id="statFamilles">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Naissances</div>
|
||||
<div class="stat-value" id="statNaissances">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Décès</div>
|
||||
<div class="stat-value" id="statDecès">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Mariages</div>
|
||||
<div class="stat-value" id="statMariages">0</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Âge moyen</div>
|
||||
<div class="stat-value" id="statAgesMoyen">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des personnes -->
|
||||
<div class="card">
|
||||
<h2>👥 Personnes</h2>
|
||||
<div class="person-list" id="personList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chronologie -->
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<div class="collapsible-header" onclick="toggleChronology()">
|
||||
<h2>📅 Chronologie (Naissances par année)</h2>
|
||||
<button class="collapse-btn" id="chronologyBtn">▼</button>
|
||||
</div>
|
||||
<div class="collapsible-content" id="chronologyContent" style="height: 300px; position: relative;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Arbre familial -->
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<h2>🌳 Arbre Généalogique (SOSA)</h2>
|
||||
<div class="tree-selector">
|
||||
<label>Rechercher SOSA 1 (point de départ):</label>
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="text"
|
||||
id="personSearch"
|
||||
class="search-input"
|
||||
placeholder="Tapez un nom..."
|
||||
onkeyup="filterSuggestions(this.value)"
|
||||
onkeydown="handleSearchKeydown(event)"
|
||||
>
|
||||
<div class="suggestions" id="suggestions"></div>
|
||||
</div>
|
||||
<label style="margin-top: 10px;">Ou sélectionnez dans la liste:</label>
|
||||
<select id="sosaSelector" onchange="buildFamilyTree()">
|
||||
<option value="">-- Choisir une personne --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="tree-container" id="treeDisplay"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let gedcomData = null;
|
||||
let individuals = {};
|
||||
let families = {};
|
||||
|
||||
async function handleFileUpload() {
|
||||
const file = document.getElementById('fileInput').files[0];
|
||||
if (!file) {
|
||||
showError('Sélectionnez un fichier d\'abord');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
gedcomData = parseGedcom(text);
|
||||
displayData();
|
||||
clearError();
|
||||
document.getElementById('csvBtn').style.display = 'block';
|
||||
} catch (e) {
|
||||
showError('Erreur lors du parsing du fichier: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseGedcom(text) {
|
||||
const lines = text.split('\n');
|
||||
individuals = {};
|
||||
families = {};
|
||||
let currentIndividual = null;
|
||||
let currentFamily = null;
|
||||
let lastEventType = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
const parts = line.split(' ');
|
||||
const level = parseInt(parts[0]);
|
||||
const tag = parts[1];
|
||||
const value = parts.slice(2).join(' ');
|
||||
|
||||
if (level === 0) {
|
||||
if (tag.startsWith('@I') && tag.endsWith('@')) {
|
||||
currentIndividual = {
|
||||
id: tag,
|
||||
name: '',
|
||||
sex: '',
|
||||
birth: { date: null, place: '' },
|
||||
death: { date: null, place: '' }
|
||||
};
|
||||
individuals[tag] = currentIndividual;
|
||||
currentFamily = null;
|
||||
lastEventType = null;
|
||||
} else if (tag.startsWith('@F') && tag.endsWith('@')) {
|
||||
currentFamily = {
|
||||
id: tag,
|
||||
husband: '',
|
||||
wife: '',
|
||||
children: []
|
||||
};
|
||||
families[tag] = currentFamily;
|
||||
currentIndividual = null;
|
||||
lastEventType = null;
|
||||
}
|
||||
} else if (level === 1) {
|
||||
if (currentIndividual) {
|
||||
if (tag === 'NAME') {
|
||||
// Format GEDCOM: "Prenom /Nom/"
|
||||
const nameMatch = value.match(/(.+?)\s*\/(.+?)\//);
|
||||
if (nameMatch) {
|
||||
const firstName = nameMatch[1].trim();
|
||||
const lastName = nameMatch[2].trim();
|
||||
currentIndividual.name = `${lastName} ${firstName}`;
|
||||
} else {
|
||||
currentIndividual.name = value.replace(/\//g, '').trim();
|
||||
}
|
||||
} else if (tag === 'SEX') {
|
||||
currentIndividual.sex = value;
|
||||
} else if (tag === 'BIRT') {
|
||||
lastEventType = 'BIRT';
|
||||
} else if (tag === 'DEAT') {
|
||||
lastEventType = 'DEAT';
|
||||
} else if (tag === 'FAMC') {
|
||||
currentIndividual.familyChild = value;
|
||||
lastEventType = null;
|
||||
} else if (tag === 'FAMS') {
|
||||
currentIndividual.familySpouse = value;
|
||||
lastEventType = null;
|
||||
} else {
|
||||
// Reset pour tous les autres tags (OCCU, RETI, etc)
|
||||
lastEventType = null;
|
||||
}
|
||||
} else if (currentFamily) {
|
||||
if (tag === 'HUSB') {
|
||||
currentFamily.husband = value;
|
||||
} else if (tag === 'WIFE') {
|
||||
currentFamily.wife = value;
|
||||
} else if (tag === 'CHIL') {
|
||||
currentFamily.children.push(value);
|
||||
}
|
||||
}
|
||||
} else if (level === 2 && currentIndividual) {
|
||||
if (tag === 'DATE') {
|
||||
const date = parseDate(value);
|
||||
if (lastEventType === 'BIRT') {
|
||||
currentIndividual.birth.date = date;
|
||||
} else if (lastEventType === 'DEAT') {
|
||||
currentIndividual.death.date = date;
|
||||
}
|
||||
} else if (tag === 'PLAC') {
|
||||
if (lastEventType === 'BIRT') {
|
||||
currentIndividual.birth.place = value;
|
||||
} else if (lastEventType === 'DEAT') {
|
||||
currentIndividual.death.place = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { individuals, families };
|
||||
}
|
||||
|
||||
function parseDate(dateStr) {
|
||||
// Format GEDCOM standard: "jour MOIS année" (ex: "05 NOV 1991" ou "NOV 1991" ou "1991")
|
||||
const months = { JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12 };
|
||||
const parts = dateStr.trim().split(/\s+/);
|
||||
|
||||
let day = 1, month = 1, year = null;
|
||||
|
||||
// Parcourir les parties: nombre, mois (texte), nombre (année)
|
||||
let numberCount = 0;
|
||||
for (const part of parts) {
|
||||
if (months[part]) {
|
||||
month = months[part];
|
||||
} else if (!isNaN(part)) {
|
||||
const num = parseInt(part);
|
||||
if (numberCount === 0) {
|
||||
// Premier nombre pourrait être jour OU année si c'est 4 chiffres
|
||||
if (num > 31) {
|
||||
year = num;
|
||||
} else {
|
||||
day = num;
|
||||
}
|
||||
} else {
|
||||
// Deuxième nombre = année
|
||||
year = num;
|
||||
}
|
||||
numberCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return year ? new Date(year, month - 1, day) : null;
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
if (!gedcomData) return;
|
||||
|
||||
const personnes = Object.values(individuals).filter(p => p.name);
|
||||
const familles = Object.keys(families).length;
|
||||
const naissances = personnes.filter(p => p.birth?.date).length;
|
||||
const décès = personnes.filter(p => p.death?.date).length;
|
||||
const mariages = Object.values(families).filter(f => f.husband || f.wife).length;
|
||||
|
||||
const ages = personnes
|
||||
.filter(p => p.birth?.date && p.death?.date)
|
||||
.map(p => (p.death.date - p.birth.date) / (365.25 * 24 * 60 * 60 * 1000))
|
||||
.filter(a => a > 0 && a < 150);
|
||||
const ageMoyen = ages.length > 0 ? Math.round(ages.reduce((a, b) => a + b) / ages.length) : '-';
|
||||
|
||||
document.getElementById('statPersonnes').textContent = personnes.length;
|
||||
document.getElementById('statFamilles').textContent = familles;
|
||||
document.getElementById('statNaissances').textContent = naissances;
|
||||
document.getElementById('statDecès').textContent = décès;
|
||||
document.getElementById('statMariages').textContent = mariages;
|
||||
document.getElementById('statAgesMoyen').textContent = ageMoyen;
|
||||
|
||||
displayPersonList(personnes);
|
||||
displayChronology(personnes);
|
||||
populateSosaSelector(personnes);
|
||||
console.log(`Chargé: ${personnes.length} personnes, ${familles} familles`);
|
||||
}
|
||||
|
||||
function populateSosaSelector(personnes) {
|
||||
const selector = document.getElementById('sosaSelector');
|
||||
const sorted = personnes.sort((a, b) => (b.birth?.date || new Date()) - (a.birth?.date || new Date()));
|
||||
selector.innerHTML = '<option value="">-- Choisir une personne --</option>' +
|
||||
sorted.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
}
|
||||
|
||||
function buildFamilyTree() {
|
||||
const sosaId = document.getElementById('sosaSelector').value;
|
||||
if (!sosaId) return;
|
||||
|
||||
const sosa1 = individuals[sosaId];
|
||||
if (!sosa1) return;
|
||||
|
||||
const tree = document.getElementById('treeDisplay');
|
||||
tree.innerHTML = '';
|
||||
|
||||
// SOSA 1
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">SOSA 1 - Sujet</div>
|
||||
<div class="generation-people">
|
||||
<div class="tree-box sosa1">${sosa1.name}<br><small>${sosa1.birth?.date?.getFullYear() || '?'}</small></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Ascendants
|
||||
const ascendants = getAscendants(sosa1);
|
||||
if (ascendants.parents.length > 0) {
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">⬆️ Parents (SOSA 2-3)</div>
|
||||
<div class="generation-people">
|
||||
${ascendants.parents.map(p => `<div class="tree-box">${p.name}<br><small>${p.birth?.date?.getFullYear() || '?'} - ${p.death?.date?.getFullYear() || '?'}</small></div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (ascendants.grandparents.length > 0) {
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">⬆️ Grands-parents (SOSA 4-7)</div>
|
||||
<div class="generation-people">
|
||||
${ascendants.grandparents.map(p => `<div class="tree-box">${p.name}<br><small>${p.birth?.date?.getFullYear() || '?'}</small></div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (ascendants.greatgrandparents.length > 0) {
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">⬆️ Arrière-grands-parents (SOSA 8-15)</div>
|
||||
<div class="generation-people">
|
||||
${ascendants.greatgrandparents.map(p => `<div class="tree-box">${p.name}<br><small>${p.birth?.date?.getFullYear() || '?'}</small></div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Descendants
|
||||
const descendants = getDescendants(sosa1);
|
||||
if (descendants.children.length > 0) {
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">⬇️ Enfants</div>
|
||||
<div class="generation-people">
|
||||
${descendants.children.map(p => `<div class="tree-box">${p.name}<br><small>${p.birth?.date?.getFullYear() || '?'}</small></div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (descendants.grandchildren.length > 0) {
|
||||
tree.innerHTML += `<div class="tree-generation">
|
||||
<div class="generation-title">⬇️ Petits-enfants</div>
|
||||
<div class="generation-people">
|
||||
${descendants.grandchildren.map(p => `<div class="tree-box">${p.name}<br><small>${p.birth?.date?.getFullYear() || '?'}</small></div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getAscendants(person) {
|
||||
const parents = [];
|
||||
const grandparents = [];
|
||||
const greatgrandparents = [];
|
||||
|
||||
// Trouver la famille enfant
|
||||
const familyChild = families[person.familyChild];
|
||||
if (familyChild) {
|
||||
if (familyChild.husband) {
|
||||
const father = individuals[familyChild.husband];
|
||||
if (father) {
|
||||
parents.push(father);
|
||||
const fatherFamily = families[father.familyChild];
|
||||
if (fatherFamily) {
|
||||
if (fatherFamily.husband) grandparents.push(individuals[fatherFamily.husband]);
|
||||
if (fatherFamily.wife) grandparents.push(individuals[fatherFamily.wife]);
|
||||
// Arrière-grands-parents
|
||||
[fatherFamily.husband, fatherFamily.wife].forEach(id => {
|
||||
if (id) {
|
||||
const gp = individuals[id];
|
||||
const gpFamily = families[gp.familyChild];
|
||||
if (gpFamily) {
|
||||
if (gpFamily.husband) greatgrandparents.push(individuals[gpFamily.husband]);
|
||||
if (gpFamily.wife) greatgrandparents.push(individuals[gpFamily.wife]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (familyChild.wife) {
|
||||
const mother = individuals[familyChild.wife];
|
||||
if (mother) {
|
||||
parents.push(mother);
|
||||
const motherFamily = families[mother.familyChild];
|
||||
if (motherFamily) {
|
||||
if (motherFamily.husband) grandparents.push(individuals[motherFamily.husband]);
|
||||
if (motherFamily.wife) grandparents.push(individuals[motherFamily.wife]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parents: parents.filter(p => p),
|
||||
grandparents: grandparents.filter(p => p),
|
||||
greatgrandparents: greatgrandparents.filter(p => p)
|
||||
};
|
||||
}
|
||||
|
||||
function getDescendants(person) {
|
||||
const children = [];
|
||||
const grandchildren = [];
|
||||
|
||||
// Trouver les familles où cette personne est parent
|
||||
const familiesAsParent = Object.values(families).filter(f =>
|
||||
f.husband === person.id || f.wife === person.id
|
||||
);
|
||||
|
||||
familiesAsParent.forEach(fam => {
|
||||
fam.children.forEach(childId => {
|
||||
const child = individuals[childId];
|
||||
if (child) {
|
||||
children.push(child);
|
||||
// Petits-enfants
|
||||
const familiesOfChild = Object.values(families).filter(f =>
|
||||
f.husband === childId || f.wife === childId
|
||||
);
|
||||
familiesOfChild.forEach(childFam => {
|
||||
childFam.children.forEach(gcId => {
|
||||
const gc = individuals[gcId];
|
||||
if (gc) grandchildren.push(gc);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
children,
|
||||
grandchildren
|
||||
};
|
||||
}
|
||||
|
||||
function displayPersonList(personnes) {
|
||||
const list = document.getElementById('personList');
|
||||
list.innerHTML = personnes
|
||||
.sort((a, b) => (b.birth?.date || new Date()) - (a.birth?.date || new Date()))
|
||||
.map(p => `
|
||||
<div class="person-item" onclick="showPersonDetails('${p.id}')">
|
||||
<div class="person-name">${p.name} ${p.sex === 'F' ? '👩' : '👨'}</div>
|
||||
<div class="person-info">
|
||||
${p.birth?.date ? p.birth.date.getFullYear() : '?'} - ${p.death?.date ? p.death.date.getFullYear() : '?'}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function displayChronology(personnes) {
|
||||
const years = {};
|
||||
personnes.forEach(p => {
|
||||
if (p.birth?.date) {
|
||||
const year = p.birth.date.getFullYear();
|
||||
years[year] = (years[year] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const sortedYears = Object.entries(years).sort((a, b) => a[0] - b[0]);
|
||||
const maxCount = Math.max(...sortedYears.map(y => y[1]));
|
||||
|
||||
const chronology = document.getElementById('chronologyContent');
|
||||
chronology.innerHTML = sortedYears.map(([year, count]) => `
|
||||
<div style="display: flex; align-items: center; margin: 5px 0;">
|
||||
<div style="width: 50px; font-size: 12px;">${year}</div>
|
||||
<div style="background: #667eea; height: 20px; width: ${(count / maxCount) * 500}px; border-radius: 3px;"></div>
|
||||
<div style="margin-left: 10px; color: #666; font-size: 12px;">${count}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleChronology() {
|
||||
const content = document.getElementById('chronologyContent');
|
||||
const btn = document.getElementById('chronologyBtn');
|
||||
content.classList.toggle('collapsed');
|
||||
btn.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
|
||||
}
|
||||
|
||||
function filterSuggestions(query) {
|
||||
const suggestions = document.getElementById('suggestions');
|
||||
if (!query.trim()) {
|
||||
suggestions.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
const personnes = Object.values(individuals).filter(p => p.name);
|
||||
const matches = personnes.filter(p =>
|
||||
p.name.toLowerCase().includes(query.toLowerCase())
|
||||
).slice(0, 20);
|
||||
|
||||
if (matches.length === 0) {
|
||||
suggestions.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.innerHTML = matches.map((p, idx) => `
|
||||
<div class="suggestion-item" onclick="selectPersonFromSearch('${p.id}', this)">
|
||||
${p.name} <small>(${p.birth?.date?.getFullYear() || '?'} - ${p.death?.date?.getFullYear() || '?'})</small>
|
||||
</div>
|
||||
`).join('');
|
||||
suggestions.classList.add('active');
|
||||
}
|
||||
|
||||
function selectPersonFromSearch(personId, element) {
|
||||
const person = individuals[personId];
|
||||
document.getElementById('personSearch').value = person.name;
|
||||
document.getElementById('sosaSelector').value = personId;
|
||||
document.getElementById('suggestions').classList.remove('active');
|
||||
buildFamilyTree();
|
||||
}
|
||||
|
||||
function handleSearchKeydown(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const suggestions = document.getElementById('suggestions');
|
||||
const items = suggestions.querySelectorAll('.suggestion-item');
|
||||
if (items.length > 0) {
|
||||
items[0].click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAsCSV() {
|
||||
const personnes = Object.values(individuals).filter(p => p.name);
|
||||
|
||||
// En-têtes CSV
|
||||
const headers = ['ID', 'Nom', 'Sexe', 'Date Naissance', 'Lieu Naissance', 'Date Décès', 'Lieu Décès', 'Père', 'Mère'];
|
||||
const rows = [headers.join(',')];
|
||||
|
||||
// Données
|
||||
personnes.forEach(person => {
|
||||
const familyChild = families[person.familyChild];
|
||||
const father = familyChild?.husband ? individuals[familyChild.husband]?.name || '' : '';
|
||||
const mother = familyChild?.wife ? individuals[familyChild.wife]?.name || '' : '';
|
||||
|
||||
const row = [
|
||||
person.id.replace(/@/g, ''),
|
||||
`"${person.name.replace(/"/g, '""')}"`,
|
||||
person.sex === 'F' ? 'F' : person.sex === 'M' ? 'M' : '',
|
||||
person.birth?.date ? person.birth.date.toLocaleDateString('fr-FR') : '',
|
||||
`"${(person.birth?.place || '').replace(/"/g, '""')}"`,
|
||||
person.death?.date ? person.death.date.toLocaleDateString('fr-FR') : '',
|
||||
`"${(person.death?.place || '').replace(/"/g, '""')}"`,
|
||||
`"${father.replace(/"/g, '""')}"`,
|
||||
`"${mother.replace(/"/g, '""')}"`,
|
||||
];
|
||||
rows.push(row.join(','));
|
||||
});
|
||||
|
||||
// Créer le fichier
|
||||
const csv = rows.join('\n');
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'genealogie.csv');
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function showPersonDetails(personId) {
|
||||
const person = individuals[personId];
|
||||
if (!person) return;
|
||||
|
||||
// Sélectionner cette personne comme SOSA 1
|
||||
document.getElementById('sosaSelector').value = personId;
|
||||
buildFamilyTree();
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const error = document.getElementById('error');
|
||||
error.textContent = msg;
|
||||
error.style.display = 'block';
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
document.getElementById('error').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user