mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-21 06:50:56 +00:00
702 lines
27 KiB
HTML
702 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Enhanced Family Tree - KJV Study</title>
|
|
|
|
<!-- Core Styles -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
|
|
<!-- D3.js for advanced visualizations -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<!-- Chart.js for analytics -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<!-- Custom Styles -->
|
|
<link href="/static/css/family-tree-expansions.css" rel="stylesheet">
|
|
|
|
<style>
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
padding: 20px;
|
|
}
|
|
|
|
.main-container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header-section {
|
|
background: linear-gradient(135deg, #007bff 0%, #6610f2 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header-section h1 {
|
|
margin: 0;
|
|
font-size: 2.5rem;
|
|
font-weight: 300;
|
|
}
|
|
|
|
.header-section p {
|
|
margin: 10px 0 0 0;
|
|
opacity: 0.9;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.content-wrapper {
|
|
padding: 30px;
|
|
}
|
|
|
|
.layout-selector {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.layout-option {
|
|
padding: 12px 24px;
|
|
border: 2px solid #dee2e6;
|
|
background: white;
|
|
color: #495057;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.layout-option.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
|
}
|
|
|
|
.layout-option:hover {
|
|
border-color: #007bff;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.visualization-container {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 12px;
|
|
min-height: 600px;
|
|
margin-bottom: 30px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tree-svg {
|
|
width: 100%;
|
|
height: 600px;
|
|
}
|
|
|
|
.loading-indicator {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #007bff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 15px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.feature-showcase {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.feature-card {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.feature-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.feature-icon {
|
|
font-size: 2rem;
|
|
color: #007bff;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.feature-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: #495057;
|
|
}
|
|
|
|
.feature-description {
|
|
color: #6c757d;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.demo-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.demo-btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
background: #6c757d;
|
|
color: white;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.demo-btn:hover {
|
|
background: #5a6268;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.demo-btn.primary {
|
|
background: #007bff;
|
|
}
|
|
|
|
.demo-btn.primary:hover {
|
|
background: #0056b3;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="main-container">
|
|
<!-- Header Section -->
|
|
<div class="header-section">
|
|
<h1><i class="fas fa-sitemap"></i> Enhanced Family Tree Visualization</h1>
|
|
<p>Advanced biblical genealogy exploration with multiple layouts, analytics, and interactive features</p>
|
|
</div>
|
|
|
|
<!-- Content Wrapper -->
|
|
<div class="content-wrapper">
|
|
<!-- Layout Selector -->
|
|
<div class="layout-selector">
|
|
<div class="layout-option active" data-layout="hierarchical">
|
|
<i class="fas fa-sitemap"></i> Hierarchical
|
|
</div>
|
|
<div class="layout-option" data-layout="radial">
|
|
<i class="fas fa-sun"></i> Radial
|
|
</div>
|
|
<div class="layout-option" data-layout="force-directed">
|
|
<i class="fas fa-project-diagram"></i> Force-Directed
|
|
</div>
|
|
<div class="layout-option" data-layout="timeline">
|
|
<i class="fas fa-timeline"></i> Timeline
|
|
</div>
|
|
<div class="layout-option" data-layout="circular-pedigree">
|
|
<i class="fas fa-circle-notch"></i> Circular Pedigree
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Demo Controls -->
|
|
<div class="demo-controls">
|
|
<button class="demo-btn primary" id="load-sample-data">
|
|
<i class="fas fa-database"></i> Load Sample Data
|
|
</button>
|
|
<button class="demo-btn" id="center-view">
|
|
<i class="fas fa-crosshairs"></i> Center View
|
|
</button>
|
|
<button class="demo-btn" id="export-view">
|
|
<i class="fas fa-download"></i> Export
|
|
</button>
|
|
<button class="demo-btn" id="toggle-analytics">
|
|
<i class="fas fa-chart-bar"></i> Analytics
|
|
</button>
|
|
<button class="demo-btn" id="toggle-search">
|
|
<i class="fas fa-search"></i> Search
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Visualization Container -->
|
|
<div class="visualization-container" id="viz-container">
|
|
<div class="loading-indicator" id="loading-indicator">
|
|
<div class="loading-spinner"></div>
|
|
<div>Loading family tree data...</div>
|
|
</div>
|
|
<svg class="tree-svg" id="main-tree-svg"></svg>
|
|
</div>
|
|
|
|
<!-- Search Container (Will be dynamically inserted) -->
|
|
<div id="search-container"></div>
|
|
|
|
<!-- Analytics Container (Will be dynamically inserted) -->
|
|
<div id="analytics-container"></div>
|
|
|
|
<!-- Feature Showcase -->
|
|
<div class="feature-showcase">
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-search-plus"></i></div>
|
|
<div class="feature-title">Advanced Search</div>
|
|
<div class="feature-description">
|
|
Comprehensive search capabilities with filtering by name, gender, generation, and biblical references.
|
|
Features real-time highlighting and bookmark management.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-chart-line"></i></div>
|
|
<div class="feature-title">Statistical Analytics</div>
|
|
<div class="feature-description">
|
|
Interactive charts and insights showing demographic patterns, generational trends,
|
|
family relationships, and biblical timeline analysis.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-project-diagram"></i></div>
|
|
<div class="feature-title">Multiple Layouts</div>
|
|
<div class="feature-description">
|
|
Choose from hierarchical, radial, force-directed, timeline, and circular pedigree layouts.
|
|
Each optimized for different exploration patterns.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-route"></i></div>
|
|
<div class="feature-title">Smart Navigation</div>
|
|
<div class="feature-description">
|
|
Breadcrumb trails, navigation history, bookmarking system, and quick access controls
|
|
for efficient family tree exploration.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-mobile-alt"></i></div>
|
|
<div class="feature-title">Responsive Design</div>
|
|
<div class="feature-description">
|
|
Fully responsive interface that adapts to desktop, tablet, and mobile devices
|
|
with touch-friendly controls and optimized layouts.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon"><i class="fas fa-download"></i></div>
|
|
<div class="feature-title">Export Capabilities</div>
|
|
<div class="feature-description">
|
|
Export family trees as high-quality images, PDF documents, or structured data formats.
|
|
Perfect for sharing and printing.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Core Scripts -->
|
|
<script src="/static/js/advanced-tree-layouts.js"></script>
|
|
<script src="/static/js/family-tree-search.js"></script>
|
|
<script src="/static/js/family-tree-analytics.js"></script>
|
|
|
|
<!-- Main Integration Script -->
|
|
<script>
|
|
// Enhanced Family Tree Integration
|
|
class EnhancedFamilyTree {
|
|
constructor() {
|
|
this.familyData = {};
|
|
this.currentLayout = 'hierarchical';
|
|
this.currentPerson = null;
|
|
this.layoutEngine = null;
|
|
this.searchEngine = null;
|
|
this.analyticsEngine = null;
|
|
|
|
this.initialize();
|
|
}
|
|
|
|
async initialize() {
|
|
// Initialize components
|
|
this.setupEventListeners();
|
|
await this.loadSampleData();
|
|
this.initializeComponents();
|
|
this.hideLoading();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Layout selector
|
|
document.querySelectorAll('.layout-option').forEach(option => {
|
|
option.addEventListener('click', (e) => {
|
|
this.switchLayout(e.target.dataset.layout);
|
|
});
|
|
});
|
|
|
|
// Demo controls
|
|
document.getElementById('load-sample-data').addEventListener('click', () => {
|
|
this.loadSampleData();
|
|
});
|
|
|
|
document.getElementById('center-view').addEventListener('click', () => {
|
|
if (this.layoutEngine) {
|
|
this.layoutEngine.centerView();
|
|
}
|
|
});
|
|
|
|
document.getElementById('export-view').addEventListener('click', () => {
|
|
this.exportCurrentView();
|
|
});
|
|
|
|
document.getElementById('toggle-analytics').addEventListener('click', () => {
|
|
this.toggleAnalytics();
|
|
});
|
|
|
|
document.getElementById('toggle-search').addEventListener('click', () => {
|
|
this.toggleSearch();
|
|
});
|
|
}
|
|
|
|
async loadSampleData() {
|
|
// Sample biblical family data
|
|
this.familyData = {
|
|
'adam': {
|
|
name: 'Adam',
|
|
title: 'First Man',
|
|
description: 'The first human being created by God',
|
|
children: ['cain', 'abel', 'seth'],
|
|
parents: [],
|
|
spouse: 'Eve',
|
|
birth_year: '4004 BC',
|
|
death_year: '3074 BC',
|
|
age_at_death: '930 years',
|
|
verses: [
|
|
{ reference: 'Genesis 2:7', text: 'And the LORD God formed man of the dust of the ground...' },
|
|
{ reference: 'Genesis 5:5', text: 'And all the days that Adam lived were nine hundred and thirty years...' }
|
|
]
|
|
},
|
|
'eve': {
|
|
name: 'Eve',
|
|
title: 'First Woman',
|
|
description: 'The first woman, created from Adam\'s rib',
|
|
children: ['cain', 'abel', 'seth'],
|
|
parents: [],
|
|
spouse: 'Adam',
|
|
birth_year: '4004 BC',
|
|
death_year: 'Unknown',
|
|
age_at_death: 'Unknown',
|
|
verses: [
|
|
{ reference: 'Genesis 2:22', text: 'And the rib, which the LORD God had taken from man, made he a woman...' },
|
|
{ reference: 'Genesis 3:20', text: 'And Adam called his wife\'s name Eve; because she was the mother of all living.' }
|
|
]
|
|
},
|
|
'cain': {
|
|
name: 'Cain',
|
|
title: 'First Son',
|
|
description: 'First son of Adam and Eve, farmer and first murderer',
|
|
children: ['enoch'],
|
|
parents: ['adam', 'eve'],
|
|
spouse: null,
|
|
birth_year: '3874 BC',
|
|
death_year: 'Unknown',
|
|
age_at_death: 'Unknown',
|
|
verses: [
|
|
{ reference: 'Genesis 4:1', text: 'And Adam knew Eve his wife; and she conceived, and bare Cain...' }
|
|
]
|
|
},
|
|
'abel': {
|
|
name: 'Abel',
|
|
title: 'Second Son',
|
|
description: 'Second son of Adam and Eve, shepherd, killed by Cain',
|
|
children: [],
|
|
parents: ['adam', 'eve'],
|
|
spouse: null,
|
|
birth_year: '3871 BC',
|
|
death_year: '3796 BC',
|
|
age_at_death: '75 years',
|
|
verses: [
|
|
{ reference: 'Genesis 4:2', text: 'And she again bare his brother Abel. And Abel was a keeper of sheep...' }
|
|
]
|
|
},
|
|
'seth': {
|
|
name: 'Seth',
|
|
title: 'Third Son',
|
|
description: 'Third son of Adam and Eve, ancestor of Noah',
|
|
children: ['enos'],
|
|
parents: ['adam', 'eve'],
|
|
spouse: null,
|
|
birth_year: '3769 BC',
|
|
death_year: '2857 BC',
|
|
age_at_death: '912 years',
|
|
verses: [
|
|
{ reference: 'Genesis 4:25', text: 'And Adam knew his wife again; and she bare a son, and called his name Seth...' }
|
|
]
|
|
},
|
|
'enoch': {
|
|
name: 'Enoch',
|
|
title: 'Son of Cain',
|
|
description: 'Son of Cain, first city builder',
|
|
children: [],
|
|
parents: ['cain'],
|
|
spouse: null,
|
|
birth_year: '3700 BC',
|
|
death_year: 'Unknown',
|
|
age_at_death: 'Unknown',
|
|
verses: []
|
|
},
|
|
'enos': {
|
|
name: 'Enos',
|
|
title: 'Son of Seth',
|
|
description: 'Son of Seth, grandson of Adam',
|
|
children: ['cainan'],
|
|
parents: ['seth'],
|
|
spouse: null,
|
|
birth_year: '3679 BC',
|
|
death_year: '2769 BC',
|
|
age_at_death: '905 years',
|
|
verses: []
|
|
},
|
|
'cainan': {
|
|
name: 'Cainan',
|
|
title: 'Son of Enos',
|
|
description: 'Son of Enos, great-grandson of Adam',
|
|
children: [],
|
|
parents: ['enos'],
|
|
spouse: null,
|
|
birth_year: '3609 BC',
|
|
death_year: '2699 BC',
|
|
age_at_death: '910 years',
|
|
verses: []
|
|
}
|
|
};
|
|
|
|
// Simulate loading delay
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Initialize with Adam as root
|
|
this.currentPerson = 'adam';
|
|
this.renderCurrentLayout();
|
|
}
|
|
|
|
initializeComponents() {
|
|
const svg = d3.select('#main-tree-svg');
|
|
|
|
// Initialize layout engine
|
|
this.layoutEngine = new AdvancedTreeLayouts(svg, this.familyData);
|
|
this.layoutEngine.setSelectPersonCallback((personId) => {
|
|
this.selectPerson(personId);
|
|
});
|
|
|
|
// Initialize search engine
|
|
this.searchEngine = new FamilyTreeSearch(this.familyData, this.layoutEngine);
|
|
|
|
// Initialize analytics engine
|
|
this.analyticsEngine = new FamilyTreeAnalytics(this.familyData);
|
|
|
|
// Initially hide search and analytics
|
|
this.hideSearch();
|
|
this.hideAnalytics();
|
|
}
|
|
|
|
switchLayout(layoutType) {
|
|
// Update UI
|
|
document.querySelectorAll('.layout-option').forEach(option => {
|
|
option.classList.toggle('active', option.dataset.layout === layoutType);
|
|
});
|
|
|
|
this.currentLayout = layoutType;
|
|
this.renderCurrentLayout();
|
|
}
|
|
|
|
renderCurrentLayout() {
|
|
if (!this.layoutEngine || !this.currentPerson) return;
|
|
|
|
this.showLoading();
|
|
|
|
// Small delay to show loading indicator
|
|
setTimeout(() => {
|
|
switch(this.currentLayout) {
|
|
case 'hierarchical':
|
|
// Use existing D3 tree implementation
|
|
this.renderHierarchicalLayout();
|
|
break;
|
|
case 'radial':
|
|
this.layoutEngine.renderRadialLayout(this.currentPerson);
|
|
break;
|
|
case 'force-directed':
|
|
this.layoutEngine.renderForceDirectedLayout(this.currentPerson);
|
|
break;
|
|
case 'timeline':
|
|
this.layoutEngine.renderTimelineLayout(this.currentPerson);
|
|
break;
|
|
case 'circular-pedigree':
|
|
this.layoutEngine.renderCircularPedigreeLayout(this.currentPerson);
|
|
break;
|
|
}
|
|
this.hideLoading();
|
|
}, 300);
|
|
}
|
|
|
|
renderHierarchicalLayout() {
|
|
// Use the existing hierarchical tree implementation
|
|
// This would integrate with the existing family tree code
|
|
const svg = d3.select('#main-tree-svg');
|
|
svg.selectAll('*').remove();
|
|
|
|
// Add sample hierarchical tree
|
|
const g = svg.append('g');
|
|
const nodes = [
|
|
{ name: 'Adam', x: 400, y: 100, id: 'adam' },
|
|
{ name: 'Eve', x: 500, y: 100, id: 'eve' },
|
|
{ name: 'Cain', x: 300, y: 200, id: 'cain' },
|
|
{ name: 'Abel', x: 400, y: 200, id: 'abel' },
|
|
{ name: 'Seth', x: 500, y: 200, id: 'seth' }
|
|
];
|
|
|
|
const nodeGroups = g.selectAll('.node')
|
|
.data(nodes)
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', 'node')
|
|
.attr('transform', d => `translate(${d.x}, ${d.y})`)
|
|
.on('click', (event, d) => this.selectPerson(d.id));
|
|
|
|
nodeGroups.append('circle')
|
|
.attr('r', 8)
|
|
.attr('fill', d => d.id === this.currentPerson ? '#007bff' : '#6c757d');
|
|
|
|
nodeGroups.append('text')
|
|
.attr('dy', -15)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('font-size', '12px')
|
|
.text(d => d.name);
|
|
}
|
|
|
|
selectPerson(personId) {
|
|
this.currentPerson = personId;
|
|
this.renderCurrentLayout();
|
|
|
|
// Update analytics if visible
|
|
if (this.analyticsEngine && !document.getElementById('analytics-container').style.display === 'none') {
|
|
this.analyticsEngine.calculateStatistics();
|
|
}
|
|
}
|
|
|
|
exportCurrentView() {
|
|
const svg = document.getElementById('main-tree-svg');
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const img = new Image();
|
|
|
|
img.onload = function() {
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
const link = document.createElement('a');
|
|
link.download = `family-tree-${new Date().getTime()}.png`;
|
|
link.href = canvas.toDataURL();
|
|
link.click();
|
|
};
|
|
|
|
img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
|
|
}
|
|
|
|
toggleAnalytics() {
|
|
const container = document.getElementById('analytics-container');
|
|
if (container.style.display === 'none') {
|
|
this.showAnalytics();
|
|
} else {
|
|
this.hideAnalytics();
|
|
}
|
|
}
|
|
|
|
toggleSearch() {
|
|
const container = document.getElementById('search-container');
|
|
if (container.style.display === 'none') {
|
|
this.showSearch();
|
|
} else {
|
|
this.hideSearch();
|
|
}
|
|
}
|
|
|
|
showAnalytics() {
|
|
const container = document.getElementById('analytics-container');
|
|
container.style.display = 'block';
|
|
container.innerHTML = '';
|
|
|
|
if (this.analyticsEngine) {
|
|
// Re-initialize analytics in the container
|
|
const analyticsElement = this.analyticsEngine.createAnalyticsInterface();
|
|
container.appendChild(analyticsElement);
|
|
}
|
|
}
|
|
|
|
hideAnalytics() {
|
|
document.getElementById('analytics-container').style.display = 'none';
|
|
}
|
|
|
|
showSearch() {
|
|
const container = document.getElementById('search-container');
|
|
container.style.display = 'block';
|
|
}
|
|
|
|
hideSearch() {
|
|
document.getElementById('search-container').style.display = 'none';
|
|
}
|
|
|
|
showLoading() {
|
|
document.getElementById('loading-indicator').style.display = 'block';
|
|
}
|
|
|
|
hideLoading() {
|
|
document.getElementById('loading-indicator').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Initialize the enhanced family tree when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new EnhancedFamilyTree();
|
|
});
|
|
|
|
// Global helper functions for integration with existing code
|
|
window.selectPerson = function(personId) {
|
|
if (window.familyTreeInstance) {
|
|
window.familyTreeInstance.selectPerson(personId);
|
|
}
|
|
};
|
|
|
|
// Export for global access
|
|
window.EnhancedFamilyTree = EnhancedFamilyTree;
|
|
</script>
|
|
</body>
|
|
</html> |