mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
Add comprehensive family tree feature expansion
This commit is contained in:
@@ -0,0 +1,657 @@
|
||||
# Family Tree Expansion Implementation Guide
|
||||
|
||||
This comprehensive guide details how to implement and integrate the advanced family tree features for the KJV Study application.
|
||||
|
||||
## Overview
|
||||
|
||||
The family tree expansion includes five major components:
|
||||
|
||||
1. **Advanced Tree Layouts** - Multiple visualization algorithms
|
||||
2. **Enhanced Search & Navigation** - Comprehensive search with highlighting
|
||||
3. **Statistical Analytics** - Interactive charts and demographic insights
|
||||
4. **Mobile Optimization** - Responsive design for all devices
|
||||
5. **Export Capabilities** - Multiple output formats
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
kjvstudy_org/
|
||||
├── static/
|
||||
│ ├── js/
|
||||
│ │ ├── advanced-tree-layouts.js # Multiple layout algorithms
|
||||
│ │ ├── family-tree-search.js # Search and navigation
|
||||
│ │ ├── family-tree-analytics.js # Statistics and insights
|
||||
│ │ └── family-tree-analytics-complete.js # Complete analytics
|
||||
│ └── css/
|
||||
│ └── family-tree-expansions.css # All styling
|
||||
├── templates/
|
||||
│ ├── family_tree.html # Original template
|
||||
│ └── enhanced_family_tree.html # New enhanced template
|
||||
└── FAMILY_TREE_EXPANSION_GUIDE.md # This guide
|
||||
```
|
||||
|
||||
## 🚀 Quick Start Implementation
|
||||
|
||||
### Step 1: Add Dependencies
|
||||
|
||||
Add these dependencies to your existing `family_tree.html` template:
|
||||
|
||||
```html
|
||||
<!-- Add to <head> section -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link href="/static/css/family-tree-expansions.css" rel="stylesheet">
|
||||
|
||||
<!-- Add before closing </body> tag -->
|
||||
<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>
|
||||
```
|
||||
|
||||
### Step 2: Initialize Components
|
||||
|
||||
Add this to your existing JavaScript section:
|
||||
|
||||
```javascript
|
||||
// Initialize advanced components after family data loads
|
||||
let advancedLayouts, searchEngine, analyticsEngine;
|
||||
|
||||
function initializeAdvancedFeatures() {
|
||||
const svg = d3.select("#tree-svg");
|
||||
|
||||
// Initialize layout engine
|
||||
advancedLayouts = new AdvancedTreeLayouts(svg.node(), familyData);
|
||||
advancedLayouts.setSelectPersonCallback(selectPerson);
|
||||
|
||||
// Initialize search
|
||||
searchEngine = new FamilyTreeSearch(familyData, advancedLayouts);
|
||||
|
||||
// Initialize analytics
|
||||
analyticsEngine = new FamilyTreeAnalytics(familyData);
|
||||
}
|
||||
|
||||
// Call this after familyData is loaded
|
||||
initializeAdvancedFeatures();
|
||||
```
|
||||
|
||||
### Step 3: Add Layout Selector
|
||||
|
||||
Insert this HTML where you want the layout controls:
|
||||
|
||||
```html
|
||||
<div class="layout-controls">
|
||||
<button onclick="switchLayout('hierarchical')" class="layout-btn active">
|
||||
<i class="fas fa-sitemap"></i> Hierarchical
|
||||
</button>
|
||||
<button onclick="switchLayout('radial')" class="layout-btn">
|
||||
<i class="fas fa-sun"></i> Radial
|
||||
</button>
|
||||
<button onclick="switchLayout('force-directed')" class="layout-btn">
|
||||
<i class="fas fa-project-diagram"></i> Force-Directed
|
||||
</button>
|
||||
<button onclick="switchLayout('timeline')" class="layout-btn">
|
||||
<i class="fas fa-timeline"></i> Timeline
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🎨 Advanced Tree Layouts
|
||||
|
||||
### Available Layouts
|
||||
|
||||
#### 1. Hierarchical Tree (Default)
|
||||
- **Use Case**: Traditional family tree representation
|
||||
- **Best For**: Clear parent-child relationships
|
||||
- **Implementation**:
|
||||
```javascript
|
||||
// Uses existing D3 tree layout - no changes needed
|
||||
updateD3Tree(person, personId);
|
||||
```
|
||||
|
||||
#### 2. Radial Tree
|
||||
- **Use Case**: Generations in concentric circles
|
||||
- **Best For**: Showing generational patterns
|
||||
- **Implementation**:
|
||||
```javascript
|
||||
advancedLayouts.renderRadialLayout(personId, maxGenerations);
|
||||
```
|
||||
|
||||
#### 3. Force-Directed Layout
|
||||
- **Use Case**: Dynamic relationship visualization
|
||||
- **Best For**: Exploring complex relationships
|
||||
- **Implementation**:
|
||||
```javascript
|
||||
advancedLayouts.renderForceDirectedLayout(personId, includeExtended);
|
||||
```
|
||||
|
||||
#### 4. Timeline Layout
|
||||
- **Use Case**: Chronological family history
|
||||
- **Best For**: Historical context and lifespans
|
||||
- **Implementation**:
|
||||
```javascript
|
||||
advancedLayouts.renderTimelineLayout(personId);
|
||||
```
|
||||
|
||||
#### 5. Circular Pedigree
|
||||
- **Use Case**: Traditional pedigree charts
|
||||
- **Best For**: Ancestry focus
|
||||
- **Implementation**:
|
||||
```javascript
|
||||
advancedLayouts.renderCircularPedigreeLayout(personId);
|
||||
```
|
||||
|
||||
### Layout Switching Function
|
||||
|
||||
```javascript
|
||||
function switchLayout(layoutType) {
|
||||
// Update UI
|
||||
document.querySelectorAll('.layout-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Switch layout
|
||||
switch(layoutType) {
|
||||
case 'radial':
|
||||
advancedLayouts.renderRadialLayout(currentPersonId);
|
||||
break;
|
||||
case 'force-directed':
|
||||
advancedLayouts.renderForceDirectedLayout(currentPersonId);
|
||||
break;
|
||||
case 'timeline':
|
||||
advancedLayouts.renderTimelineLayout(currentPersonId);
|
||||
break;
|
||||
case 'circular-pedigree':
|
||||
advancedLayouts.renderCircularPedigreeLayout(currentPersonId);
|
||||
break;
|
||||
default:
|
||||
updateD3Tree(familyData[currentPersonId], currentPersonId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Enhanced Search & Navigation
|
||||
|
||||
### Search Features
|
||||
|
||||
#### Basic Search
|
||||
- Real-time search with debouncing
|
||||
- Multiple field search (name, title, description, verses)
|
||||
- Gender and generation filtering
|
||||
|
||||
#### Advanced Features
|
||||
- Bookmarking system with localStorage
|
||||
- Navigation breadcrumbs
|
||||
- Search result highlighting in tree
|
||||
- Export search results to CSV
|
||||
|
||||
### Integration Example
|
||||
|
||||
```javascript
|
||||
// The search system auto-integrates when initialized
|
||||
// Customize search behavior:
|
||||
searchEngine.setTreeVisualization(advancedLayouts);
|
||||
|
||||
// Handle search result selection
|
||||
function handleSearchResult(personId) {
|
||||
selectPerson(personId);
|
||||
searchEngine.addToBreadcrumbs(personId);
|
||||
}
|
||||
```
|
||||
|
||||
### Bookmark Management
|
||||
|
||||
```javascript
|
||||
// Add current person to bookmarks
|
||||
function addCurrentBookmark() {
|
||||
searchEngine.addBookmark(currentPersonId);
|
||||
}
|
||||
|
||||
// Navigate to bookmarked person
|
||||
function navigateToBookmark(personId) {
|
||||
selectPerson(personId);
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Statistical Analytics
|
||||
|
||||
### Available Analytics
|
||||
|
||||
#### Overview Statistics
|
||||
- Total persons count
|
||||
- Gender distribution
|
||||
- Generation count
|
||||
- Family metrics
|
||||
- Average children per family
|
||||
|
||||
#### Interactive Charts
|
||||
1. **Demographics** - Gender distribution (pie/bar/doughnut)
|
||||
2. **Generations** - Population by generation
|
||||
3. **Relationships** - Family size distribution
|
||||
4. **Timeline** - Biblical timeline analysis
|
||||
5. **Longevity** - Lifespan distribution
|
||||
|
||||
### Custom Analytics
|
||||
|
||||
```javascript
|
||||
// Add custom insight
|
||||
analyticsEngine.addCustomInsight = function(title, value, description) {
|
||||
const insightsList = document.getElementById('notable-stats');
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<strong>${title}:</strong> ${value} - ${description}`;
|
||||
insightsList.appendChild(li);
|
||||
};
|
||||
|
||||
// Example usage
|
||||
analyticsEngine.addCustomInsight(
|
||||
"Longest Lineage",
|
||||
"10 generations",
|
||||
"From Adam to Noah"
|
||||
);
|
||||
```
|
||||
|
||||
### Chart Customization
|
||||
|
||||
```javascript
|
||||
// Customize chart colors
|
||||
const customColors = {
|
||||
primary: '#007bff',
|
||||
secondary: '#6c757d',
|
||||
success: '#28a745',
|
||||
danger: '#dc3545',
|
||||
warning: '#ffc107',
|
||||
info: '#17a2b8'
|
||||
};
|
||||
|
||||
// Apply to charts
|
||||
analyticsEngine.chartColors = customColors;
|
||||
```
|
||||
|
||||
## 📱 Mobile Optimization
|
||||
|
||||
### Responsive Features
|
||||
|
||||
The CSS includes comprehensive mobile optimizations:
|
||||
|
||||
- **Touch-friendly controls** - Larger tap targets
|
||||
- **Adaptive layouts** - Grid systems that stack on mobile
|
||||
- **Optimized charts** - Reduced heights and simplified legends
|
||||
- **Collapsible sections** - Save screen space
|
||||
- **Swipe gestures** - For navigation (where supported)
|
||||
|
||||
### Mobile-Specific CSS
|
||||
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
.family-search-container {
|
||||
margin: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px; /* Reduced from 300px */
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
grid-template-columns: 1fr; /* Single column on mobile */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 💾 Export Capabilities
|
||||
|
||||
### Available Export Formats
|
||||
|
||||
#### SVG Export
|
||||
```javascript
|
||||
function exportSVG() {
|
||||
const svg = document.getElementById('tree-svg');
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], {type: 'image/svg+xml'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'family-tree.svg';
|
||||
link.click();
|
||||
}
|
||||
```
|
||||
|
||||
#### PNG Export
|
||||
```javascript
|
||||
function exportPNG() {
|
||||
const svg = document.getElementById('tree-svg');
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const data = new XMLSerializer().serializeToString(svg);
|
||||
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.png';
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
};
|
||||
|
||||
img.src = 'data:image/svg+xml;base64,' + btoa(data);
|
||||
}
|
||||
```
|
||||
|
||||
#### Data Export
|
||||
```javascript
|
||||
function exportData() {
|
||||
// Export search results as CSV
|
||||
searchEngine.exportSearchResults();
|
||||
|
||||
// Export analytics report
|
||||
analyticsEngine.exportAnalyticsReport();
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Server-Side Integration
|
||||
|
||||
### Enhanced Route Handler
|
||||
|
||||
Update your `server.py` to include additional endpoints:
|
||||
|
||||
```python
|
||||
@app.get("/family-tree-enhanced", response_class=HTMLResponse)
|
||||
def enhanced_family_tree_page(request: Request):
|
||||
"""Enhanced family tree with advanced features"""
|
||||
books = list(bible.iter_books())
|
||||
|
||||
# Load GEDCOM data (existing logic)
|
||||
static_dir = Path(__file__).parent / "static"
|
||||
gedcom_path = static_dir / "adameve.ged"
|
||||
|
||||
if not gedcom_path.exists():
|
||||
raise HTTPException(status_code=404, detail="GEDCOM file not found")
|
||||
|
||||
try:
|
||||
family_tree_data = parse_gedcom_to_tree_data(gedcom_path)
|
||||
|
||||
# Add enhanced metadata
|
||||
enhanced_data = enhance_family_data(family_tree_data)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to parse GEDCOM: {str(e)}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"enhanced_family_tree.html",
|
||||
{
|
||||
"request": request,
|
||||
"books": books,
|
||||
"family_tree_data": enhanced_data,
|
||||
"analytics_enabled": True,
|
||||
"search_enabled": True
|
||||
}
|
||||
)
|
||||
|
||||
def enhance_family_data(family_data):
|
||||
"""Add enhanced metadata for analytics"""
|
||||
for person_id, person in family_data.items():
|
||||
# Add generation calculation
|
||||
person['generation'] = calculate_generation(person_id, family_data)
|
||||
|
||||
# Add relationship metrics
|
||||
person['relationship_count'] = len(person.get('children', [])) + len(person.get('parents', []))
|
||||
|
||||
# Add search keywords
|
||||
person['search_keywords'] = generate_search_keywords(person)
|
||||
|
||||
return family_data
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Add these endpoints for dynamic data:
|
||||
|
||||
```python
|
||||
@app.get("/api/family-tree/search")
|
||||
def search_family_tree(q: str, filters: str = None):
|
||||
"""Search family tree data"""
|
||||
# Implement server-side search
|
||||
pass
|
||||
|
||||
@app.get("/api/family-tree/analytics")
|
||||
def get_family_analytics():
|
||||
"""Get pre-computed analytics"""
|
||||
# Return analytics data
|
||||
pass
|
||||
|
||||
@app.post("/api/family-tree/bookmark")
|
||||
def save_bookmark(person_id: str, user_id: str = None):
|
||||
"""Save user bookmark"""
|
||||
# Implement bookmark persistence
|
||||
pass
|
||||
```
|
||||
|
||||
## 🎯 Performance Optimization
|
||||
|
||||
### Large Dataset Handling
|
||||
|
||||
For families with 1000+ members:
|
||||
|
||||
```javascript
|
||||
// Implement pagination
|
||||
const CHUNK_SIZE = 100;
|
||||
|
||||
function loadDataInChunks(familyData) {
|
||||
const chunks = Object.keys(familyData).reduce((acc, key, index) => {
|
||||
const chunkIndex = Math.floor(index / CHUNK_SIZE);
|
||||
if (!acc[chunkIndex]) acc[chunkIndex] = {};
|
||||
acc[chunkIndex][key] = familyData[key];
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Lazy loading for search
|
||||
searchEngine.enableLazyLoading = true;
|
||||
searchEngine.chunkSize = CHUNK_SIZE;
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```javascript
|
||||
// Clean up on layout switch
|
||||
function cleanupLayout() {
|
||||
if (advancedLayouts.simulation) {
|
||||
advancedLayouts.simulation.stop();
|
||||
}
|
||||
|
||||
// Clear D3 selections
|
||||
d3.select("#tree-svg").selectAll("*").remove();
|
||||
|
||||
// Clear analytics charts
|
||||
Object.values(analyticsEngine.chartInstances).forEach(chart => {
|
||||
if (chart) chart.destroy();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```javascript
|
||||
// Test layout switching
|
||||
describe('AdvancedTreeLayouts', () => {
|
||||
let layouts;
|
||||
|
||||
beforeEach(() => {
|
||||
layouts = new AdvancedTreeLayouts(mockSvg, mockFamilyData);
|
||||
});
|
||||
|
||||
test('should switch to radial layout', () => {
|
||||
layouts.renderRadialLayout('adam');
|
||||
expect(layouts.getCurrentLayout()).toBe('radial');
|
||||
});
|
||||
|
||||
test('should center view', () => {
|
||||
const spy = jest.spyOn(layouts, 'centerView');
|
||||
layouts.centerView();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Test search functionality
|
||||
describe('FamilyTreeSearch', () => {
|
||||
test('should find persons by name', () => {
|
||||
const results = searchEngine.performSearch('adam');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].name.toLowerCase()).toContain('adam');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
# Test enhanced family tree endpoint
|
||||
def test_enhanced_family_tree_endpoint(client):
|
||||
response = client.get("/family-tree-enhanced")
|
||||
assert response.status_code == 200
|
||||
assert "Enhanced Family Tree" in response.text
|
||||
assert "advanced-tree-layouts.js" in response.text
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Charts Not Rendering
|
||||
```javascript
|
||||
// Check if Chart.js is loaded
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('Chart.js not loaded');
|
||||
// Load Chart.js dynamically
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. D3.js Layout Issues
|
||||
```javascript
|
||||
// Check D3.js version compatibility
|
||||
if (!d3.version || d3.version.split('.')[0] < '7') {
|
||||
console.warn('D3.js v7+ required for full functionality');
|
||||
}
|
||||
|
||||
// Handle missing data gracefully
|
||||
function safeRenderLayout(personId) {
|
||||
if (!familyData[personId]) {
|
||||
console.warn('Person not found:', personId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
advancedLayouts.renderRadialLayout(personId);
|
||||
} catch (error) {
|
||||
console.error('Layout render failed:', error);
|
||||
// Fallback to hierarchical
|
||||
updateD3Tree(familyData[personId], personId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Mobile Performance
|
||||
```javascript
|
||||
// Reduce complexity on mobile
|
||||
function isMobile() {
|
||||
return window.innerWidth < 768;
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
// Disable complex animations
|
||||
advancedLayouts.disableAnimations = true;
|
||||
|
||||
// Reduce max nodes
|
||||
advancedLayouts.maxNodes = 50;
|
||||
|
||||
// Simplify force simulation
|
||||
if (advancedLayouts.simulation) {
|
||||
advancedLayouts.simulation.force("charge").strength(-100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📈 Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **3D Visualization** - Three.js integration for 3D family trees
|
||||
2. **Virtual Reality** - WebXR support for immersive exploration
|
||||
3. **Collaborative Features** - Real-time collaboration on family trees
|
||||
4. **AI Insights** - Machine learning for relationship discovery
|
||||
5. **Blockchain Integration** - Immutable family records
|
||||
|
||||
### Extension Points
|
||||
|
||||
```javascript
|
||||
// Plugin system for custom layouts
|
||||
AdvancedTreeLayouts.registerPlugin = function(name, plugin) {
|
||||
this.plugins[name] = plugin;
|
||||
};
|
||||
|
||||
// Custom analytics modules
|
||||
FamilyTreeAnalytics.addModule = function(name, module) {
|
||||
this.modules[name] = module;
|
||||
};
|
||||
|
||||
// Search providers
|
||||
FamilyTreeSearch.addProvider = function(name, provider) {
|
||||
this.providers[name] = provider;
|
||||
};
|
||||
```
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
### Documentation
|
||||
- [D3.js Documentation](https://d3js.org/)
|
||||
- [Chart.js Documentation](https://www.chartjs.org/)
|
||||
- [SVG Specification](https://www.w3.org/Graphics/SVG/)
|
||||
|
||||
### Examples
|
||||
- See `enhanced_family_tree.html` for complete integration example
|
||||
- Check browser developer tools for debugging layout issues
|
||||
- Use the demo controls to test all features
|
||||
|
||||
### Support
|
||||
- File issues in the project repository
|
||||
- Check console for error messages
|
||||
- Test in latest Chrome/Firefox for best compatibility
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Conclusion
|
||||
|
||||
This expansion transforms the basic family tree into a comprehensive genealogy exploration tool with:
|
||||
|
||||
- **5 different layout algorithms** for varied perspectives
|
||||
- **Advanced search capabilities** with real-time filtering
|
||||
- **Statistical insights** through interactive charts
|
||||
- **Mobile-optimized interface** for all devices
|
||||
- **Export capabilities** for sharing and archiving
|
||||
|
||||
The modular design allows for gradual implementation and easy customization for specific biblical genealogy needs.
|
||||
|
||||
**Next Steps:**
|
||||
1. Implement basic integration following the Quick Start guide
|
||||
2. Customize styling to match your application theme
|
||||
3. Add server-side endpoints for persistence
|
||||
4. Test thoroughly across different devices
|
||||
5. Consider performance optimizations for large datasets
|
||||
|
||||
The enhanced family tree will significantly improve user engagement and provide valuable insights into biblical genealogies and family relationships.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,863 @@
|
||||
/**
|
||||
* Family Tree Analytics and Statistics System for KJV Study
|
||||
* Provides comprehensive statistical analysis and insights with interactive charts
|
||||
*/
|
||||
|
||||
class FamilyTreeAnalytics {
|
||||
constructor(familyData) {
|
||||
this.familyData = familyData;
|
||||
this.chartInstances = {};
|
||||
this.analysisCache = {};
|
||||
|
||||
this.initializeAnalytics();
|
||||
this.calculateStatistics();
|
||||
}
|
||||
|
||||
initializeAnalytics() {
|
||||
this.createAnalyticsInterface();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
createAnalyticsInterface() {
|
||||
const analyticsContainer = document.createElement('div');
|
||||
analyticsContainer.className = 'family-analytics-container';
|
||||
analyticsContainer.innerHTML = `
|
||||
<div class="analytics-header">
|
||||
<h3><i class="fas fa-chart-bar"></i> Family Tree Analytics</h3>
|
||||
<div class="analytics-controls">
|
||||
<button id="refresh-analytics" class="analytics-btn" title="Refresh statistics">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<button id="export-analytics" class="analytics-btn" title="Export analytics report">
|
||||
<i class="fas fa-download"></i> Export Report
|
||||
</button>
|
||||
<button id="toggle-analytics" class="analytics-btn" title="Toggle analytics panel">
|
||||
<i class="fas fa-eye"></i> Toggle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analytics-content">
|
||||
<!-- Overview Statistics -->
|
||||
<div class="stats-overview">
|
||||
<div class="stat-card total-persons">
|
||||
<div class="stat-icon"><i class="fas fa-users"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="total-persons">0</div>
|
||||
<div class="stat-label">Total Persons</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card male-count">
|
||||
<div class="stat-icon"><i class="fas fa-male"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="male-count">0</div>
|
||||
<div class="stat-label">Male</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card female-count">
|
||||
<div class="stat-icon"><i class="fas fa-female"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="female-count">0</div>
|
||||
<div class="stat-label">Female</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card generations-count">
|
||||
<div class="stat-icon"><i class="fas fa-layer-group"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="generations-count">0</div>
|
||||
<div class="stat-label">Generations</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card families-count">
|
||||
<div class="stat-icon"><i class="fas fa-home"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="families-count">0</div>
|
||||
<div class="stat-label">Families</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card avg-children">
|
||||
<div class="stat-icon"><i class="fas fa-baby"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" id="avg-children">0</div>
|
||||
<div class="stat-label">Avg Children</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart Tabs -->
|
||||
<div class="chart-tabs">
|
||||
<button class="tab-btn active" data-tab="demographic">Demographics</button>
|
||||
<button class="tab-btn" data-tab="generational">Generations</button>
|
||||
<button class="tab-btn" data-tab="relationships">Relationships</button>
|
||||
<button class="tab-btn" data-tab="timeline">Timeline</button>
|
||||
<button class="tab-btn" data-tab="longevity">Longevity</button>
|
||||
</div>
|
||||
|
||||
<!-- Chart Panels -->
|
||||
<div class="chart-panels">
|
||||
<!-- Demographics Panel -->
|
||||
<div class="chart-panel active" id="demographic-panel">
|
||||
<div class="panel-header">
|
||||
<h4>Demographic Analysis</h4>
|
||||
<div class="chart-options">
|
||||
<select id="demographic-chart-type">
|
||||
<option value="pie">Pie Chart</option>
|
||||
<option value="bar">Bar Chart</option>
|
||||
<option value="doughnut">Doughnut Chart</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="demographic-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-insights">
|
||||
<div class="insight-item">
|
||||
<strong>Gender Ratio:</strong> <span id="gender-ratio">Loading...</span>
|
||||
</div>
|
||||
<div class="insight-item">
|
||||
<strong>Most Common Names:</strong> <span id="common-names">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generational Panel -->
|
||||
<div class="chart-panel" id="generational-panel">
|
||||
<div class="panel-header">
|
||||
<h4>Generational Distribution</h4>
|
||||
<div class="chart-options">
|
||||
<select id="generational-metric">
|
||||
<option value="count">Person Count</option>
|
||||
<option value="lifespan">Average Lifespan</option>
|
||||
<option value="children">Children per Generation</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="generational-chart"></canvas>
|
||||
</div>
|
||||
<div class="generation-details">
|
||||
<div id="generation-breakdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Relationships Panel -->
|
||||
<div class="chart-panel" id="relationships-panel">
|
||||
<div class="panel-header">
|
||||
<h4>Family Relationships</h4>
|
||||
</div>
|
||||
<div class="relationship-metrics">
|
||||
<div class="metric-grid">
|
||||
<div class="metric-item">
|
||||
<div class="metric-value" id="married-couples">0</div>
|
||||
<div class="metric-label">Married Couples</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-value" id="single-parents">0</div>
|
||||
<div class="metric-label">Single Parents</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-value" id="childless-couples">0</div>
|
||||
<div class="metric-label">Childless Couples</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-value" id="largest-family">0</div>
|
||||
<div class="metric-label">Largest Family</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="relationships-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Panel -->
|
||||
<div class="chart-panel" id="timeline-panel">
|
||||
<div class="panel-header">
|
||||
<h4>Biblical Timeline Analysis</h4>
|
||||
<div class="chart-options">
|
||||
<select id="timeline-view">
|
||||
<option value="births">Birth Timeline</option>
|
||||
<option value="lifespans">Lifespan Overview</option>
|
||||
<option value="generations">Generation Overlap</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container timeline-container">
|
||||
<canvas id="timeline-chart"></canvas>
|
||||
</div>
|
||||
<div class="timeline-insights">
|
||||
<div class="insight-grid">
|
||||
<div class="insight-card">
|
||||
<h5>Longest Lifespan</h5>
|
||||
<div id="longest-lived">Loading...</div>
|
||||
</div>
|
||||
<div class="insight-card">
|
||||
<h5>Shortest Lifespan</h5>
|
||||
<div id="shortest-lived">Loading...</div>
|
||||
</div>
|
||||
<div class="insight-card">
|
||||
<h5>Average Lifespan</h5>
|
||||
<div id="average-lifespan">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Longevity Panel -->
|
||||
<div class="chart-panel" id="longevity-panel">
|
||||
<div class="panel-header">
|
||||
<h4>Longevity Analysis</h4>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="longevity-chart"></canvas>
|
||||
</div>
|
||||
<div class="longevity-trends">
|
||||
<div id="longevity-trends-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Insights Section -->
|
||||
<div class="detailed-insights">
|
||||
<h4>Detailed Insights</h4>
|
||||
<div class="insights-grid">
|
||||
<div class="insight-section">
|
||||
<h5>Family Patterns</h5>
|
||||
<ul id="family-patterns"></ul>
|
||||
</div>
|
||||
<div class="insight-section">
|
||||
<h5>Notable Statistics</h5>
|
||||
<ul id="notable-stats"></ul>
|
||||
</div>
|
||||
<div class="insight-section">
|
||||
<h5>Genealogical Insights</h5>
|
||||
<ul id="genealogical-insights"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert analytics container into the page
|
||||
const familyViewer = document.querySelector('.family-viewer');
|
||||
if (familyViewer) {
|
||||
familyViewer.appendChild(analyticsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
this.switchTab(e.target.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
// Chart type changes
|
||||
document.getElementById('demographic-chart-type')?.addEventListener('change', (e) => {
|
||||
this.updateDemographicChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('generational-metric')?.addEventListener('change', (e) => {
|
||||
this.updateGenerationalChart(e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('timeline-view')?.addEventListener('change', (e) => {
|
||||
this.updateTimelineChart(e.target.value);
|
||||
});
|
||||
|
||||
// Control buttons
|
||||
document.getElementById('refresh-analytics')?.addEventListener('click', () => {
|
||||
this.refreshAnalytics();
|
||||
});
|
||||
|
||||
document.getElementById('export-analytics')?.addEventListener('click', () => {
|
||||
this.exportAnalyticsReport();
|
||||
});
|
||||
|
||||
document.getElementById('toggle-analytics')?.addEventListener('click', () => {
|
||||
this.toggleAnalyticsPanel();
|
||||
});
|
||||
}
|
||||
|
||||
calculateStatistics() {
|
||||
this.stats = {
|
||||
totalPersons: Object.keys(this.familyData).length,
|
||||
genderDistribution: this.calculateGenderDistribution(),
|
||||
generationData: this.calculateGenerationData(),
|
||||
familyStructure: this.calculateFamilyStructure(),
|
||||
lifespanData: this.calculateLifespanData(),
|
||||
relationshipMetrics: this.calculateRelationshipMetrics(),
|
||||
nameAnalysis: this.calculateNameAnalysis(),
|
||||
biblicalTimeline: this.calculateBiblicalTimeline()
|
||||
};
|
||||
|
||||
this.updateOverviewStats();
|
||||
this.generateInsights();
|
||||
this.createCharts();
|
||||
}
|
||||
|
||||
calculateGenderDistribution() {
|
||||
const genders = { male: 0, female: 0, unknown: 0 };
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
const gender = this.determineGender(person);
|
||||
genders[gender]++;
|
||||
});
|
||||
|
||||
return genders;
|
||||
}
|
||||
|
||||
calculateGenerationData() {
|
||||
const generations = {};
|
||||
const visited = new Set();
|
||||
|
||||
const mapGeneration = (personId, generation = 0) => {
|
||||
if (visited.has(personId)) return;
|
||||
visited.add(personId);
|
||||
|
||||
const person = this.familyData[personId];
|
||||
if (!person) return;
|
||||
|
||||
if (!generations[generation]) {
|
||||
generations[generation] = {
|
||||
count: 0,
|
||||
persons: [],
|
||||
totalLifespan: 0,
|
||||
lifespanCount: 0,
|
||||
totalChildren: 0
|
||||
};
|
||||
}
|
||||
|
||||
generations[generation].count++;
|
||||
generations[generation].persons.push(personId);
|
||||
generations[generation].totalChildren += (person.children?.length || 0);
|
||||
|
||||
// Calculate lifespan if available
|
||||
const lifespan = this.calculateLifespan(person);
|
||||
if (lifespan > 0) {
|
||||
generations[generation].totalLifespan += lifespan;
|
||||
generations[generation].lifespanCount++;
|
||||
}
|
||||
|
||||
// Map children to next generation
|
||||
if (person.children) {
|
||||
person.children.forEach(childId => {
|
||||
mapGeneration(childId, generation + 1);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Start with root figures (those without parents)
|
||||
Object.entries(this.familyData).forEach(([id, person]) => {
|
||||
if (!person.parents || person.parents.length === 0) {
|
||||
mapGeneration(id, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return generations;
|
||||
}
|
||||
|
||||
calculateFamilyStructure() {
|
||||
let marriedCouples = 0;
|
||||
let singleParents = 0;
|
||||
let childlessCouples = 0;
|
||||
let largestFamily = 0;
|
||||
const familySizes = [];
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
const childrenCount = person.children?.length || 0;
|
||||
|
||||
if (childrenCount > 0) {
|
||||
familySizes.push(childrenCount);
|
||||
largestFamily = Math.max(largestFamily, childrenCount);
|
||||
|
||||
if (person.spouse) {
|
||||
marriedCouples++;
|
||||
} else {
|
||||
singleParents++;
|
||||
}
|
||||
} else if (person.spouse) {
|
||||
childlessCouples++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
marriedCouples: Math.floor(marriedCouples / 2), // Avoid double counting
|
||||
singleParents,
|
||||
childlessCouples: Math.floor(childlessCouples / 2),
|
||||
largestFamily,
|
||||
familySizes,
|
||||
averageChildren: familySizes.length > 0 ?
|
||||
(familySizes.reduce((a, b) => a + b, 0) / familySizes.length).toFixed(1) : 0
|
||||
};
|
||||
}
|
||||
|
||||
calculateLifespanData() {
|
||||
const lifespans = [];
|
||||
let totalLifespan = 0;
|
||||
let lifespanCount = 0;
|
||||
let longestLived = { name: '', years: 0 };
|
||||
let shortestLived = { name: '', years: Infinity };
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
const lifespan = this.calculateLifespan(person);
|
||||
if (lifespan > 0) {
|
||||
lifespans.push({ name: person.name, years: lifespan });
|
||||
totalLifespan += lifespan;
|
||||
lifespanCount++;
|
||||
|
||||
if (lifespan > longestLived.years) {
|
||||
longestLived = { name: person.name, years: lifespan };
|
||||
}
|
||||
if (lifespan < shortestLived.years) {
|
||||
shortestLived = { name: person.name, years: lifespan };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
lifespans,
|
||||
averageLifespan: lifespanCount > 0 ? (totalLifespan / lifespanCount).toFixed(1) : 0,
|
||||
longestLived: longestLived.years > 0 ? longestLived : null,
|
||||
shortestLived: shortestLived.years < Infinity ? shortestLived : null
|
||||
};
|
||||
}
|
||||
|
||||
calculateRelationshipMetrics() {
|
||||
const relationships = {
|
||||
parentChild: 0,
|
||||
spouses: 0,
|
||||
siblings: 0
|
||||
};
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
relationships.parentChild += person.children?.length || 0;
|
||||
if (person.spouse) relationships.spouses++;
|
||||
});
|
||||
|
||||
relationships.spouses = Math.floor(relationships.spouses / 2); // Avoid double counting
|
||||
|
||||
return relationships;
|
||||
}
|
||||
|
||||
calculateNameAnalysis() {
|
||||
const nameFrequency = {};
|
||||
const nameComponents = {};
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
const name = person.name.toLowerCase();
|
||||
nameFrequency[name] = (nameFrequency[name] || 0) + 1;
|
||||
|
||||
// Analyze name components
|
||||
const parts = name.split(' ');
|
||||
parts.forEach(part => {
|
||||
if (part.length > 2) {
|
||||
nameComponents[part] = (nameComponents[part] || 0) + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const commonNames = Object.entries(nameFrequency)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([name, count]) => ({ name, count }));
|
||||
|
||||
return { nameFrequency, nameComponents, commonNames };
|
||||
}
|
||||
|
||||
calculateBiblicalTimeline() {
|
||||
const timeline = [];
|
||||
|
||||
Object.values(this.familyData).forEach(person => {
|
||||
const birthYear = this.parseBiblicalYear(person.birth_year);
|
||||
const deathYear = this.parseBiblicalYear(person.death_year);
|
||||
|
||||
if (birthYear) {
|
||||
timeline.push({
|
||||
name: person.name,
|
||||
birthYear,
|
||||
deathYear,
|
||||
lifespan: deathYear ? deathYear - birthYear : null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return timeline.sort((a, b) => (b.birthYear || 0) - (a.birthYear || 0));
|
||||
}
|
||||
|
||||
updateOverviewStats() {
|
||||
document.getElementById('total-persons').textContent = this.stats.totalPersons;
|
||||
document.getElementById('male-count').textContent = this.stats.genderDistribution.male;
|
||||
document.getElementById('female-count').textContent = this.stats.genderDistribution.female;
|
||||
document.getElementById('generations-count').textContent = Object.keys(this.stats.generationData).length;
|
||||
document.getElementById('families-count').textContent = this.stats.familyStructure.marriedCouples;
|
||||
document.getElementById('avg-children').textContent = this.stats.familyStructure.averageChildren;
|
||||
}
|
||||
|
||||
createCharts() {
|
||||
// Only create charts if Chart.js is available
|
||||
if (typeof Chart !== 'undefined') {
|
||||
this.createDemographicChart();
|
||||
this.createGenerationalChart();
|
||||
this.createRelationshipsChart();
|
||||
this.createTimelineChart();
|
||||
this.createLongevityChart();
|
||||
}
|
||||
}
|
||||
|
||||
createDemographicChart(type = 'pie') {
|
||||
const ctx = document.getElementById('demographic-chart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.chartInstances.demographic) {
|
||||
this.chartInstances.demographic.destroy();
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: ['Male', 'Female', 'Unknown'],
|
||||
datasets: [{
|
||||
data: [
|
||||
this.stats.genderDistribution.male,
|
||||
this.stats.genderDistribution.female,
|
||||
this.stats.genderDistribution.unknown
|
||||
],
|
||||
backgroundColor: ['#2196F3', '#E91E63', '#9E9E9E'],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
};
|
||||
|
||||
this.chartInstances.demographic = new Chart(ctx, {
|
||||
type: type,
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Gender Distribution'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateDemographicInsights();
|
||||
}
|
||||
|
||||
createGenerationalChart(metric = 'count') {
|
||||
const ctx = document.getElementById('generational-chart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.chartInstances.generational) {
|
||||
this.chartInstances.generational.destroy();
|
||||
}
|
||||
|
||||
const generations = this.stats.generationData;
|
||||
const labels = Object.keys(generations).map(gen => `Generation ${gen}`);
|
||||
let dataValues = [];
|
||||
let label = '';
|
||||
|
||||
switch(metric) {
|
||||
case 'count':
|
||||
dataValues = Object.values(generations).map(gen => gen.count);
|
||||
label = 'Number of Persons';
|
||||
break;
|
||||
case 'lifespan':
|
||||
dataValues = Object.values(generations).map(gen =>
|
||||
gen.lifespanCount > 0 ? (gen.totalLifespan / gen.lifespanCount).toFixed(1) : 0
|
||||
);
|
||||
label = 'Average Lifespan (years)';
|
||||
break;
|
||||
case 'children':
|
||||
dataValues = Object.values(generations).map(gen =>
|
||||
gen.count > 0 ? (gen.totalChildren / gen.count).toFixed(1) : 0
|
||||
);
|
||||
label = 'Average Children per Person';
|
||||
break;
|
||||
}
|
||||
|
||||
this.chartInstances.generational = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: dataValues,
|
||||
backgroundColor: '#4CAF50',
|
||||
borderColor: '#388E3C',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `Generational Analysis - ${label}`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateGenerationBreakdown();
|
||||
}
|
||||
|
||||
createRelationshipsChart() {
|
||||
const ctx = document.getElementById('relationships-chart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.chartInstances.relationships) {
|
||||
this.chartInstances.relationships.destroy();
|
||||
}
|
||||
|
||||
const familySizes = this.stats.familyStructure.familySizes;
|
||||
const distribution = {};
|
||||
|
||||
familySizes.forEach(size => {
|
||||
const key = size > 10 ? '10+' : size.toString();
|
||||
distribution[key] = (distribution[key] || 0) + 1;
|
||||
});
|
||||
|
||||
this.chartInstances.relationships = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(distribution).sort((a, b) => {
|
||||
if (a === '10+') return 1;
|
||||
if (b === '10+') return -1;
|
||||
return parseInt(a) - parseInt(b);
|
||||
}),
|
||||
datasets: [{
|
||||
label: 'Number of Families',
|
||||
data: Object.values(distribution),
|
||||
backgroundColor: '#FF9800',
|
||||
borderColor: '#F57C00',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Children'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Families'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Family Size Distribution'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateRelationshipMetrics();
|
||||
}
|
||||
|
||||
createTimelineChart(view = 'births') {
|
||||
const ctx = document.getElementById('timeline-chart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.chartInstances.timeline) {
|
||||
this.chartInstances.timeline.destroy();
|
||||
}
|
||||
|
||||
const timeline = this.stats.biblicalTimeline.filter(person => person.birthYear);
|
||||
|
||||
if (timeline.length === 0) {
|
||||
ctx.getContext('2d').clearRect(0, 0, ctx.width, ctx.height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple timeline chart showing birth years
|
||||
const labels = timeline.slice(0, 10).map(p => p.name);
|
||||
const data = timeline.slice(0, 10).map(p => p.birthYear);
|
||||
|
||||
this.chartInstances.timeline = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Birth Year',
|
||||
data: data,
|
||||
borderColor: '#9C27B0',
|
||||
backgroundColor: 'rgba(156, 39, 176, 0.1)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
reverse: true, // Earlier years at top
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Years Before Christ'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Biblical Timeline'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateTimelineInsights();
|
||||
}
|
||||
|
||||
createLongevityChart() {
|
||||
const ctx = document.getElementById('longevity-chart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.chartInstances.longevity) {
|
||||
this.chartInstances.longevity.destroy();
|
||||
}
|
||||
|
||||
const lifespans = this.stats.lifespanData.lifespans;
|
||||
if (lifespans.length === 0) return;
|
||||
|
||||
// Create age groups
|
||||
const ageGroups = {
|
||||
'0-100': 0, '101-200': 0, '201-300': 0, '301-400': 0,
|
||||
'401-500': 0, '501-600': 0, '601-700': 0, '700+': 0
|
||||
};
|
||||
|
||||
lifespans.forEach(person => {
|
||||
const age = person.years;
|
||||
if (age <= 100) ageGroups['0-100']++;
|
||||
else if (age <= 200) ageGroups['101-200']++;
|
||||
else if (age <= 300) ageGroups['201-300']++;
|
||||
else if (age <= 400) ageGroups['301-400']++;
|
||||
else if (age <= 500) ageGroups['401-500']++;
|
||||
else if (age <= 600) ageGroups['501-600']++;
|
||||
else if (age <= 700) ageGroups['601-700']++;
|
||||
else ageGroups['700+']++;
|
||||
});
|
||||
|
||||
this.chartInstances.longevity = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: Object.keys(ageGroups),
|
||||
datasets: [{
|
||||
data: Object.values(ageGroups),
|
||||
backgroundColor: [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
|
||||
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Longevity Distribution (Years)'
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateLongevityTrends();
|
||||
}
|
||||
|
||||
// Helper Methods
|
||||
|
||||
determineGender(person) {
|
||||
const name = person.name.toLowerCase();
|
||||
const femaleNames = [
|
||||
'eve', 'sarah', 'sarai', 'rebekah', 'rebecca', 'rachel', 'leah', 'dinah',
|
||||
'tamar', 'miriam', 'deborah', 'ruth', 'naomi', 'bathsheba', 'abigail',
|
||||
'esther', 'mary', 'elizabeth', 'anna', 'hannah', 'martha'
|
||||
];
|
||||
|
||||
if (femaleNames.some(femName => name.includes(femName))) {
|
||||
return 'female';
|
||||
}
|
||||
|
||||
// Check title/description for gender clues
|
||||
const description = (person.description || '').toLowerCase();
|
||||
const title = (person.title || '').toLowerCase();
|
||||
|
||||
if (description.includes('wife') || title.includes('wife') ||
|
||||
description.includes('mother') || title.includes('mother')) {
|
||||
return 'female';
|
||||
}
|
||||
|
||||
return 'male'; // Default for biblical genealogies
|
||||
}
|
||||
|
||||
calculateLifespan(person) {
|
||||
const birthYear = this.parseBiblicalYear(person.birth_year);
|
||||
const deathYear = this.parseBiblicalYear(person.death_year);
|
||||
|
||||
if (birthYear && deathYear && deathYear > birthYear) {
|
||||
return deathYear - birthYear;
|
||||
}
|
||||
|
||||
// Try to extract from age_at_death
|
||||
if (person.age_at_death && person.age_at_death !== "Unknown") {
|
||||
const match = person.age_at_death.match(/(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
parseBiblicalYear(yearString) {
|
||||
if (!yearString || yearString === "Unknown") return null;
|
||||
|
||||
const match = yearString.match(/(\d+)/);
|
||||
return match ? parseInt(match[1]) : null;
|
||||
}
|
||||
|
||||
// UI Update Methods
|
||||
|
||||
switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.chart-panel').forEach(panel => {
|
||||
panel.classList.toggle('active', panel.id === `${tabName}-panel`);
|
||||
});
|
||||
}
|
||||
|
||||
updateDemographicInsights() {
|
||||
const total = this.stats.genderDistribution.male + this.stats.genderDistribution.female;
|
||||
const ratio = total >
|
||||
@@ -0,0 +1,702 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user