diff --git a/kjvstudy_org/static/js/advanced-tree-layouts.js b/kjvstudy_org/static/js/advanced-tree-layouts.js
new file mode 100644
index 0000000..223bf11
--- /dev/null
+++ b/kjvstudy_org/static/js/advanced-tree-layouts.js
@@ -0,0 +1,542 @@
+/**
+ * Advanced Tree Layout Options for KJV Study Family Tree
+ * Provides multiple visualization modes beyond the standard hierarchical tree
+ */
+
+class AdvancedTreeLayouts {
+ constructor(svg, familyData) {
+ this.svg = svg;
+ this.familyData = familyData;
+ this.currentLayout = 'hierarchical';
+ this.width = 800;
+ this.height = 600;
+ this.simulation = null;
+ this.zoom = null;
+ this.g = null;
+
+ this.initializeZoom();
+ }
+
+ initializeZoom() {
+ this.zoom = d3.zoom()
+ .scaleExtent([0.1, 3])
+ .on("zoom", (event) => {
+ if (this.g) {
+ this.g.attr("transform", event.transform);
+ }
+ });
+
+ this.svg.call(this.zoom);
+ this.g = this.svg.append("g");
+ }
+
+ updateDimensions() {
+ const rect = this.svg.node().getBoundingClientRect();
+ this.width = rect.width;
+ this.height = rect.height;
+ }
+
+ /**
+ * Radial Tree Layout - Shows generations in concentric circles
+ */
+ renderRadialLayout(rootPersonId, maxGenerations = 4) {
+ this.currentLayout = 'radial';
+ this.updateDimensions();
+
+ const treeData = this.buildRadialTreeData(rootPersonId, maxGenerations);
+ if (!treeData) return;
+
+ this.g.selectAll("*").remove();
+
+ const centerX = this.width / 2;
+ const centerY = this.height / 2;
+ const maxRadius = Math.min(this.width, this.height) / 2 - 60;
+
+ // Calculate positions for each generation
+ const generations = this.getGenerationLevels(treeData);
+ const radiusStep = maxRadius / Math.max(generations.length - 1, 1);
+
+ // Position nodes in concentric circles
+ generations.forEach((generation, genIndex) => {
+ const radius = genIndex * radiusStep;
+ const angleStep = (2 * Math.PI) / Math.max(generation.length, 1);
+
+ generation.forEach((node, nodeIndex) => {
+ const angle = nodeIndex * angleStep - Math.PI / 2; // Start from top
+ node.x = centerX + radius * Math.cos(angle);
+ node.y = centerY + radius * Math.sin(angle);
+ });
+ });
+
+ this.drawRadialConnections(treeData);
+ this.drawNodes(treeData.nodes, rootPersonId);
+
+ // Add generation labels
+ this.addGenerationLabels(generations, centerX, centerY, radiusStep);
+ }
+
+ /**
+ * Force-Directed Layout - Dynamic positioning based on relationships
+ */
+ renderForceDirectedLayout(rootPersonId, includeExtended = true) {
+ this.currentLayout = 'force-directed';
+ this.updateDimensions();
+
+ const graphData = this.buildGraphData(rootPersonId, includeExtended);
+ if (!graphData.nodes.length) return;
+
+ this.g.selectAll("*").remove();
+
+ // Create force simulation
+ this.simulation = d3.forceSimulation(graphData.nodes)
+ .force("link", d3.forceLink(graphData.links)
+ .id(d => d.id)
+ .distance(d => this.getLinkDistance(d))
+ .strength(d => this.getLinkStrength(d)))
+ .force("charge", d3.forceManyBody()
+ .strength(-300)
+ .distanceMax(200))
+ .force("center", d3.forceCenter(this.width / 2, this.height / 2))
+ .force("collision", d3.forceCollide().radius(25));
+
+ // Draw links
+ const links = this.g.selectAll('.force-link')
+ .data(graphData.links)
+ .enter()
+ .append('line')
+ .attr('class', d => `force-link ${d.type}`)
+ .attr('stroke-width', d => d.type === 'marriage' ? 3 : 2)
+ .attr('stroke', d => this.getLinkColor(d.type));
+
+ // Draw nodes
+ const nodes = this.g.selectAll('.force-node')
+ .data(graphData.nodes)
+ .enter()
+ .append('g')
+ .attr('class', d => `force-node ${d.gender} ${d.id === rootPersonId ? 'current' : ''}`)
+ .call(d3.drag()
+ .on("start", (event, d) => this.dragStarted(event, d))
+ .on("drag", (event, d) => this.dragged(event, d))
+ .on("end", (event, d) => this.dragEnded(event, d)))
+ .on('click', (event, d) => {
+ this.selectPerson(d.id);
+ });
+
+ nodes.append('circle')
+ .attr('r', d => d.id === rootPersonId ? 12 : 8)
+ .attr('fill', d => this.getNodeColor(d, rootPersonId));
+
+ nodes.append('text')
+ .attr('dy', '.35em')
+ .attr('text-anchor', 'middle')
+ .style('font-size', '10px')
+ .style('font-weight', d => d.id === rootPersonId ? 'bold' : 'normal')
+ .text(d => this.truncateName(d.name, 12));
+
+ // Update positions on simulation tick
+ this.simulation.on("tick", () => {
+ links
+ .attr("x1", d => d.source.x)
+ .attr("y1", d => d.source.y)
+ .attr("x2", d => d.target.x)
+ .attr("y2", d => d.target.y);
+
+ nodes.attr("transform", d => `translate(${d.x},${d.y})`);
+ });
+ }
+
+ /**
+ * Timeline Tree Layout - Shows family relationships across biblical timeline
+ */
+ renderTimelineLayout(rootPersonId) {
+ this.currentLayout = 'timeline';
+ this.updateDimensions();
+
+ const timelineData = this.buildTimelineData(rootPersonId);
+ if (!timelineData.length) return;
+
+ this.g.selectAll("*").remove();
+
+ // Sort by estimated birth year
+ timelineData.sort((a, b) => (a.estimatedYear || 0) - (b.estimatedYear || 0));
+
+ const padding = 50;
+ const timelineWidth = this.width - 2 * padding;
+ const timelineHeight = this.height - 2 * padding;
+
+ // Create time scale
+ const years = timelineData.map(d => d.estimatedYear || 0).filter(y => y > 0);
+ const minYear = Math.min(...years) || 4000;
+ const maxYear = Math.max(...years) || 1;
+
+ const xScale = d3.scaleLinear()
+ .domain([minYear, maxYear])
+ .range([padding, this.width - padding]);
+
+ // Group by generation for y-positioning
+ const generations = this.groupByGeneration(timelineData);
+ const yScale = d3.scaleBand()
+ .domain(Object.keys(generations))
+ .range([padding, this.height - padding])
+ .paddingInner(0.2);
+
+ // Draw timeline axis
+ this.drawTimelineAxis(xScale, minYear, maxYear);
+
+ // Position and draw nodes
+ Object.entries(generations).forEach(([generation, persons]) => {
+ const y = yScale(generation) + yScale.bandwidth() / 2;
+
+ persons.forEach((person, index) => {
+ const x = person.estimatedYear ?
+ xScale(person.estimatedYear) :
+ padding + (index * 30); // Fallback positioning
+
+ person.x = x;
+ person.y = y;
+ });
+ });
+
+ this.drawTimelineConnections(timelineData);
+ this.drawNodes(timelineData, rootPersonId);
+ this.addGenerationLabels(generations, 20, 0, yScale.bandwidth());
+ }
+
+ /**
+ * Circular Pedigree Layout - Traditional pedigree chart in circular form
+ */
+ renderCircularPedigreeLayout(rootPersonId) {
+ this.currentLayout = 'circular-pedigree';
+ this.updateDimensions();
+
+ const pedigreeData = this.buildPedigreeData(rootPersonId, 5);
+ if (!pedigreeData) return;
+
+ this.g.selectAll("*").remove();
+
+ const centerX = this.width / 2;
+ const centerY = this.height / 2;
+ const maxRadius = Math.min(this.width, this.height) / 2 - 60;
+
+ // Calculate positions for pedigree chart
+ this.positionPedigreeNodes(pedigreeData, centerX, centerY, maxRadius);
+
+ this.drawPedigreeConnections(pedigreeData);
+ this.drawNodes([pedigreeData], rootPersonId);
+ }
+
+ // Helper Methods
+
+ buildRadialTreeData(rootPersonId, maxGenerations) {
+ const nodes = [];
+ const visited = new Set();
+
+ const traverse = (personId, generation) => {
+ if (!personId || visited.has(personId) || generation > maxGenerations) return null;
+
+ visited.add(personId);
+ const person = this.familyData[personId];
+ if (!person) return null;
+
+ const node = {
+ id: personId,
+ name: person.name,
+ data: person,
+ gender: this.determineGender(person),
+ generation: generation,
+ children: []
+ };
+
+ nodes.push(node);
+
+ // Add children
+ if (person.children && generation < maxGenerations) {
+ person.children.forEach(childId => {
+ const child = traverse(childId, generation + 1);
+ if (child) node.children.push(child);
+ });
+ }
+
+ return node;
+ };
+
+ const rootNode = traverse(rootPersonId, 0);
+ return { root: rootNode, nodes: nodes };
+ }
+
+ buildGraphData(rootPersonId, includeExtended) {
+ const nodes = [];
+ const links = [];
+ const visited = new Set();
+ const maxDepth = includeExtended ? 3 : 2;
+
+ const traverse = (personId, depth) => {
+ if (!personId || visited.has(personId) || depth > maxDepth) return;
+
+ visited.add(personId);
+ const person = this.familyData[personId];
+ if (!person) return;
+
+ const node = {
+ id: personId,
+ name: person.name,
+ data: person,
+ gender: this.determineGender(person)
+ };
+ nodes.push(node);
+
+ // Add family links
+ if (person.spouse) {
+ const spouseId = this.findPersonIdByName(person.spouse);
+ if (spouseId && !visited.has(spouseId)) {
+ traverse(spouseId, depth);
+ links.push({
+ source: personId,
+ target: spouseId,
+ type: 'marriage'
+ });
+ }
+ }
+
+ // Add parent-child links
+ if (person.children && depth < maxDepth) {
+ person.children.forEach(childId => {
+ traverse(childId, depth + 1);
+ links.push({
+ source: personId,
+ target: childId,
+ type: 'parent-child'
+ });
+ });
+ }
+
+ // Add parent links
+ if (person.parents && depth < maxDepth) {
+ person.parents.forEach(parentId => {
+ traverse(parentId, depth + 1);
+ links.push({
+ source: parentId,
+ target: personId,
+ type: 'parent-child'
+ });
+ });
+ }
+ };
+
+ traverse(rootPersonId, 0);
+ return { nodes, links };
+ }
+
+ buildTimelineData(rootPersonId) {
+ const persons = [];
+ const visited = new Set();
+
+ const traverse = (personId, generation = 0) => {
+ if (!personId || visited.has(personId)) return;
+
+ visited.add(personId);
+ const person = this.familyData[personId];
+ if (!person) return;
+
+ const estimatedYear = this.estimateBirthYear(person);
+ persons.push({
+ id: personId,
+ name: person.name,
+ data: person,
+ gender: this.determineGender(person),
+ generation: generation,
+ estimatedYear: estimatedYear
+ });
+
+ // Traverse family members
+ if (person.children) {
+ person.children.forEach(childId => traverse(childId, generation + 1));
+ }
+ if (person.parents) {
+ person.parents.forEach(parentId => traverse(parentId, generation - 1));
+ }
+ };
+
+ traverse(rootPersonId);
+ return persons;
+ }
+
+ getGenerationLevels(treeData) {
+ const generations = {};
+
+ const addToGeneration = (node) => {
+ const gen = node.generation || 0;
+ if (!generations[gen]) generations[gen] = [];
+ generations[gen].push(node);
+
+ if (node.children) {
+ node.children.forEach(addToGeneration);
+ }
+ };
+
+ if (treeData.nodes) {
+ treeData.nodes.forEach(addToGeneration);
+ }
+
+ return Object.values(generations);
+ }
+
+ drawRadialConnections(treeData) {
+ // Draw curved connections between generations
+ const connections = [];
+
+ const addConnections = (node) => {
+ if (node.children) {
+ node.children.forEach(child => {
+ connections.push({ source: node, target: child });
+ addConnections(child);
+ });
+ }
+ };
+
+ if (treeData.root) addConnections(treeData.root);
+
+ this.g.selectAll('.radial-link')
+ .data(connections)
+ .enter()
+ .append('path')
+ .attr('class', 'radial-link')
+ .attr('d', d => {
+ const midX = (d.source.x + d.target.x) / 2;
+ const midY = (d.source.y + d.target.y) / 2;
+ return `M${d.source.x},${d.source.y} Q${midX},${midY} ${d.target.x},${d.target.y}`;
+ })
+ .attr('stroke', '#666')
+ .attr('stroke-width', 2)
+ .attr('fill', 'none');
+ }
+
+ drawNodes(nodes, currentPersonId) {
+ const nodeSelection = this.g.selectAll('.tree-node')
+ .data(nodes)
+ .enter()
+ .append('g')
+ .attr('class', d => `tree-node ${d.gender} ${d.id === currentPersonId ? 'current' : ''}`)
+ .attr('transform', d => `translate(${d.x},${d.y})`)
+ .on('click', (event, d) => {
+ if (this.selectPerson) this.selectPerson(d.id);
+ });
+
+ nodeSelection.append('circle')
+ .attr('r', d => d.id === currentPersonId ? 10 : 6)
+ .attr('fill', d => this.getNodeColor(d, currentPersonId));
+
+ nodeSelection.append('text')
+ .attr('dy', '.35em')
+ .attr('text-anchor', 'middle')
+ .style('font-size', '10px')
+ .style('font-weight', d => d.id === currentPersonId ? 'bold' : 'normal')
+ .text(d => this.truncateName(d.name, 12));
+ }
+
+ // Utility Methods
+
+ determineGender(person) {
+ const name = person.name.toLowerCase();
+ const femaleNames = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'mary', 'elizabeth'];
+ return femaleNames.includes(name) ? 'female' : 'male';
+ }
+
+ getNodeColor(node, currentPersonId) {
+ if (node.id === currentPersonId) return '#007bff';
+ return node.gender === 'female' ? '#e91e63' : '#2196f3';
+ }
+
+ getLinkColor(type) {
+ switch(type) {
+ case 'marriage': return '#4caf50';
+ case 'parent-child': return '#666';
+ default: return '#999';
+ }
+ }
+
+ getLinkDistance(link) {
+ return link.type === 'marriage' ? 80 : 120;
+ }
+
+ getLinkStrength(link) {
+ return link.type === 'marriage' ? 0.8 : 0.5;
+ }
+
+ truncateName(name, maxLength) {
+ return name.length > maxLength ? name.substring(0, maxLength) + '...' : name;
+ }
+
+ estimateBirthYear(person) {
+ // Simple estimation based on biblical timeline
+ // This would need more sophisticated logic based on actual biblical data
+ const birthYear = person.birth_year;
+ if (birthYear && birthYear !== "Unknown") {
+ const match = birthYear.match(/\d+/);
+ return match ? parseInt(match[0]) : null;
+ }
+ return null;
+ }
+
+ findPersonIdByName(name) {
+ return Object.keys(this.familyData).find(id =>
+ this.familyData[id].name === name
+ );
+ }
+
+ // Force simulation event handlers
+ dragStarted(event, d) {
+ if (!event.active) this.simulation.alphaTarget(0.3).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ dragged(event, d) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ dragEnded(event, d) {
+ if (!event.active) this.simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+
+ // Public API
+ setSelectPersonCallback(callback) {
+ this.selectPerson = callback;
+ }
+
+ getCurrentLayout() {
+ return this.currentLayout;
+ }
+
+ centerView() {
+ const bounds = this.g.node().getBBox();
+ const parent = this.svg.node().getBoundingClientRect();
+ const fullWidth = parent.width;
+ const fullHeight = parent.height;
+ const width = bounds.width;
+ const height = bounds.height;
+ const midX = bounds.x + width / 2;
+ const midY = bounds.y + height / 2;
+
+ const scale = 0.8 / Math.max(width / fullWidth, height / fullHeight);
+ const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
+
+ this.svg.transition()
+ .duration(750)
+ .call(this.zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
+ }
+
+ destroy() {
+ if (this.simulation) {
+ this.simulation.stop();
+ }
+ }
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = AdvancedTreeLayouts;
+}
\ No newline at end of file
diff --git a/kjvstudy_org/static/js/family-tree-analytics.js b/kjvstudy_org/static/js/family-tree-analytics.js
new file mode 100644
index 0000000..5c56f30
--- /dev/null
+++ b/kjvstudy_org/static/js/family-tree-analytics.js
@@ -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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Gender Ratio: Loading...
+
+
+ Most Common Names: Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
Childless Couples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Longest Lifespan
+
Loading...
+
+
+
Shortest Lifespan
+
Loading...
+
+
+
Average Lifespan
+
Loading...
+
+
+
+
+
+
+
+
+
+
+
+
Detailed Insights
+
+
+
+
+
Genealogical Insights
+
+
+
+
+
+ `;
+
+ // 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() {
+ this.createDemographicChart();
+ this.createGenerationalChart();
+ this.createRelationshipsChart();
+ this.createTimelineChart();
+ this.createLongevityChart();
+ }
+
+ createDemographicChart(type = 'pie') {
+ const ctx = document.getElementById('demographic-chart');
+ if (!ctx) 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) 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) 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) 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;
+ }
+
+ let chartData = {};
+
+ switch(view) {
+ case 'births':
+ chartData = this.createBirthTimelineData(timeline);
+ break;
+ case 'lifespans':
+ chartData = this.createLifespanTimelineData(timeline);
+ break;
+ case 'generations':
+ chartData = this.createGenerationOverlapData(timeline);
+ break;
+ }
+
+ this.chartInstances.timeline = new Chart(ctx, chartData);
+ this.updateTimelineInsights();
+ }
+
+ createLongevityChart() {
+ const ctx = document.getElementById('longevity-chart');
+ if (!ctx) 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) {
+ // Update tab buttons
+ document.querySelectorAll('.tab-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
+ });
+
+ // Update panels
+ document.querySelectorAll('.chart-panel').forEach(panel => {
+ panel.classList.toggle('active', panel.id === `${tabName}-panel`);
+ });
+ }
+
+ updateDemographicChart(type) {
+ this.createDemographicChart(type);
+ }
+
+ updateGenerationalChart(metric) {
+ this.createGenerationalChart(metric);
+ }
+
+ updateTimelineChart(view) {
+ this.createTimelineChart(view);
+ }
+
+ updateDemographicInsights() {
+ const total = this.stats.genderDistribution.male + this.stats.genderDistribution.female;
+ const ratio = total > 0 ?
+ `${(this.stats.genderDistribution.male / total * 100).toFixed(1)}% Male, ${(this.stats.genderDistribution.female / total * 100).toFixed(1)}% Female` :
+ 'No data';
+
+ document.getElementById('gender-ratio').textContent = ratio;
+
+ const commonNames = this.stats.nameAnalysis.commonNames
+ .slice(0, 3)
+ .map(item => item.name)
+ .join(', ');
+
+ document.getElementById('
\ No newline at end of file
diff --git a/kjvstudy_org/static/js/family-tree-search.js b/kjvstudy_org/static/js/family-tree-search.js
new file mode 100644
index 0000000..5ac8691
--- /dev/null
+++ b/kjvstudy_org/static/js/family-tree-search.js
@@ -0,0 +1,916 @@
+/**
+ * Advanced Search and Navigation System for KJV Study Family Tree
+ * Provides comprehensive search capabilities with tree highlighting and breadcrumb navigation
+ */
+
+class FamilyTreeSearch {
+ constructor(familyData, treeVisualization) {
+ this.familyData = familyData;
+ this.treeVisualization = treeVisualization;
+ this.searchResults = [];
+ this.currentHighlights = [];
+ this.searchHistory = [];
+ this.bookmarks = JSON.parse(localStorage.getItem('familyTreeBookmarks')) || [];
+ this.breadcrumbs = [];
+
+ this.initializeSearch();
+ this.initializeNavigation();
+ this.loadBookmarks();
+ }
+
+ initializeSearch() {
+ // Create search interface
+ this.createSearchInterface();
+ this.setupSearchEventListeners();
+ this.buildSearchIndex();
+ }
+
+ createSearchInterface() {
+ const searchContainer = document.createElement('div');
+ searchContainer.className = 'family-search-container';
+ searchContainer.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Access
+
+
+
+
+
+
+
+
+
Bookmarks
+
+
+
+
+
+
+
+
+ `;
+
+ // Insert search container into the page
+ const familyViewer = document.querySelector('.family-viewer');
+ if (familyViewer) {
+ familyViewer.insertBefore(searchContainer, familyViewer.firstChild);
+ }
+ }
+
+ setupSearchEventListeners() {
+ const searchInput = document.getElementById('family-search-input');
+ const clearBtn = document.getElementById('search-clear-btn');
+ const advancedToggle = document.getElementById('advanced-search-toggle');
+ const highlightBtn = document.getElementById('highlight-all-btn');
+ const exportBtn = document.getElementById('export-results-btn');
+
+ // Search input with debouncing
+ let searchTimeout;
+ searchInput.addEventListener('input', (e) => {
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ this.performSearch(e.target.value);
+ }, 300);
+ });
+
+ // Clear search
+ clearBtn.addEventListener('click', () => {
+ searchInput.value = '';
+ this.clearSearch();
+ });
+
+ // Toggle advanced filters
+ advancedToggle.addEventListener('click', () => {
+ const filters = document.querySelector('.search-filters');
+ filters.classList.toggle('visible');
+ });
+
+ // Filter change events
+ document.querySelectorAll('input[name="searchFields"], input[name="genderFilter"]').forEach(input => {
+ input.addEventListener('change', () => {
+ if (searchInput.value) {
+ this.performSearch(searchInput.value);
+ }
+ });
+ });
+
+ document.getElementById('generation-filter').addEventListener('change', () => {
+ if (searchInput.value) {
+ this.performSearch(searchInput.value);
+ }
+ });
+
+ // Result actions
+ highlightBtn.addEventListener('click', () => this.highlightAllResults());
+ exportBtn.addEventListener('click', () => this.exportSearchResults());
+ }
+
+ buildSearchIndex() {
+ this.searchIndex = {};
+ Object.entries(this.familyData).forEach(([id, person]) => {
+ this.searchIndex[id] = {
+ id: id,
+ name: person.name.toLowerCase(),
+ title: (person.title || '').toLowerCase(),
+ description: (person.description || '').toLowerCase(),
+ verses: this.extractVerseText(person.verses || []).toLowerCase(),
+ gender: this.determineGender(person),
+ searchText: [
+ person.name,
+ person.title || '',
+ person.description || '',
+ this.extractVerseText(person.verses || [])
+ ].join(' ').toLowerCase()
+ };
+ });
+ }
+
+ performSearch(query) {
+ if (!query.trim()) {
+ this.clearSearch();
+ return;
+ }
+
+ const searchFields = this.getSelectedSearchFields();
+ const genderFilter = this.getSelectedGenderFilter();
+ const generationFilter = this.getGenerationFilter();
+
+ this.searchResults = this.executeSearch(query, searchFields, genderFilter, generationFilter);
+ this.displaySearchResults();
+ this.updateResultsCount();
+
+ // Add to search history
+ this.addToSearchHistory(query);
+ }
+
+ executeSearch(query, searchFields, genderFilter, generationFilter) {
+ const queryLower = query.toLowerCase();
+ const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0);
+
+ return Object.values(this.searchIndex).filter(person => {
+ // Gender filter
+ if (genderFilter !== 'all' && person.gender !== genderFilter) {
+ return false;
+ }
+
+ // Generation filter (would need current person context)
+ if (generationFilter !== 'all') {
+ // Implementation depends on current tree state
+ // This is a placeholder for generation-based filtering
+ }
+
+ // Text search
+ const matchScore = this.calculateMatchScore(person, queryTerms, searchFields);
+ return matchScore > 0;
+ }).sort((a, b) => {
+ // Sort by relevance score
+ const scoreA = this.calculateMatchScore(a, queryTerms, searchFields);
+ const scoreB = this.calculateMatchScore(b, queryTerms, searchFields);
+ return scoreB - scoreA;
+ });
+ }
+
+ calculateMatchScore(person, queryTerms, searchFields) {
+ let score = 0;
+
+ queryTerms.forEach(term => {
+ searchFields.forEach(field => {
+ const fieldValue = person[field] || '';
+ if (fieldValue.includes(term)) {
+ // Exact name matches get highest score
+ if (field === 'name' && fieldValue === term) {
+ score += 10;
+ }
+ // Name contains term gets high score
+ else if (field === 'name') {
+ score += 5;
+ }
+ // Other fields get standard score
+ else {
+ score += 1;
+ }
+ }
+ });
+ });
+
+ return score;
+ }
+
+ displaySearchResults() {
+ const resultsList = document.getElementById('search-results-list');
+ resultsList.innerHTML = '';
+
+ if (this.searchResults.length === 0) {
+ resultsList.innerHTML = 'No results found
';
+ return;
+ }
+
+ this.searchResults.forEach((result, index) => {
+ const person = this.familyData[result.id];
+ const resultElement = document.createElement('div');
+ resultElement.className = 'search-result-item';
+ resultElement.innerHTML = `
+
+
${person.name}
+
${person.title || 'Biblical Figure'}
+
${this.createSearchSnippet(person)}
+
+
+
+
+
+
+ `;
+
+ // Add event listeners
+ resultElement.querySelector('.select-person-btn').addEventListener('click', () => {
+ this.selectPerson(result.id);
+ });
+
+ resultElement.querySelector('.highlight-person-btn').addEventListener('click', () => {
+ this.highlightPersonInTree(result.id);
+ });
+
+ resultElement.querySelector('.bookmark-person-btn').addEventListener('click', () => {
+ this.addBookmark(result.id);
+ });
+
+ resultsList.appendChild(resultElement);
+ });
+ }
+
+ createSearchSnippet(person) {
+ const snippetLength = 100;
+ let snippet = person.description || '';
+
+ if (snippet.length > snippetLength) {
+ snippet = snippet.substring(0, snippetLength) + '...';
+ }
+
+ return snippet || 'Biblical figure in genealogy';
+ }
+
+ // Navigation Methods
+
+ initializeNavigation() {
+ this.navigationHistory = [];
+ this.navigationIndex = -1;
+ this.setupNavigationEventListeners();
+ }
+
+ setupNavigationEventListeners() {
+ document.getElementById('nav-root').addEventListener('click', () => this.navigateToRoot());
+ document.getElementById('nav-back').addEventListener('click', () => this.navigateBack());
+ document.getElementById('nav-forward').addEventListener('click', () => this.navigateForward());
+ document.getElementById('clear-breadcrumbs').addEventListener('click', () => this.clearBreadcrumbs());
+ document.getElementById('add-bookmark').addEventListener('click', () => this.addCurrentBookmark());
+ document.getElementById('manage-bookmarks').addEventListener('click', () => this.openBookmarkManager());
+ }
+
+ addToBreadcrumbs(personId) {
+ const person = this.familyData[personId];
+ if (!person) return;
+
+ // Avoid duplicate consecutive entries
+ if (this.breadcrumbs.length > 0 && this.breadcrumbs[this.breadcrumbs.length - 1].id === personId) {
+ return;
+ }
+
+ this.breadcrumbs.push({
+ id: personId,
+ name: person.name,
+ timestamp: Date.now()
+ });
+
+ // Limit breadcrumb history
+ if (this.breadcrumbs.length > 10) {
+ this.breadcrumbs.shift();
+ }
+
+ this.updateBreadcrumbDisplay();
+ }
+
+ updateBreadcrumbDisplay() {
+ const breadcrumbTrail = document.getElementById('breadcrumb-trail');
+ breadcrumbTrail.innerHTML = '';
+
+ this.breadcrumbs.forEach((crumb, index) => {
+ const crumbElement = document.createElement('div');
+ crumbElement.className = 'breadcrumb-item';
+ crumbElement.innerHTML = `
+ ${crumb.name}
+
+ `;
+
+ crumbElement.querySelector('.breadcrumb-select').addEventListener('click', () => {
+ this.selectPerson(crumb.id);
+ });
+
+ breadcrumbTrail.appendChild(crumbElement);
+
+ // Add separator
+ if (index < this.breadcrumbs.length - 1) {
+ const separator = document.createElement('span');
+ separator.className = 'breadcrumb-separator';
+ separator.textContent = '→';
+ breadcrumbTrail.appendChild(separator);
+ }
+ });
+ }
+
+ // Bookmark Management
+
+ addBookmark(personId) {
+ const person = this.familyData[personId];
+ if (!person) return;
+
+ const bookmark = {
+ id: personId,
+ name: person.name,
+ title: person.title || 'Biblical Figure',
+ timestamp: Date.now()
+ };
+
+ // Check if already bookmarked
+ if (!this.bookmarks.find(b => b.id === personId)) {
+ this.bookmarks.push(bookmark);
+ this.saveBookmarks();
+ this.updateBookmarksDisplay();
+ this.showNotification(`${person.name} added to bookmarks`);
+ } else {
+ this.showNotification(`${person.name} is already bookmarked`);
+ }
+ }
+
+ removeBookmark(personId) {
+ this.bookmarks = this.bookmarks.filter(b => b.id !== personId);
+ this.saveBookmarks();
+ this.updateBookmarksDisplay();
+ }
+
+ saveBookmarks() {
+ localStorage.setItem('familyTreeBookmarks', JSON.stringify(this.bookmarks));
+ }
+
+ loadBookmarks() {
+ this.updateBookmarksDisplay();
+ }
+
+ updateBookmarksDisplay() {
+ const bookmarksList = document.getElementById('bookmarks-list');
+ bookmarksList.innerHTML = '';
+
+ if (this.bookmarks.length === 0) {
+ bookmarksList.innerHTML = 'No bookmarks yet
';
+ return;
+ }
+
+ this.bookmarks.forEach(bookmark => {
+ const bookmarkElement = document.createElement('div');
+ bookmarkElement.className = 'bookmark-item';
+ bookmarkElement.innerHTML = `
+
+
${bookmark.name}
+
${bookmark.title}
+
+
+
+
+
+ `;
+
+ bookmarkElement.querySelector('.bookmark-select').addEventListener('click', () => {
+ this.selectPerson(bookmark.id);
+ });
+
+ bookmarkElement.querySelector('.bookmark-remove').addEventListener('click', () => {
+ this.removeBookmark(bookmark.id);
+ });
+
+ bookmarksList.appendChild(bookmarkElement);
+ });
+ }
+
+ // Tree Highlighting
+
+ highlightPersonInTree(personId) {
+ // Remove existing highlights
+ this.clearHighlights();
+
+ // Add new highlight
+ this.currentHighlights.push(personId);
+
+ // Update tree visualization
+ if (this.treeVisualization && this.treeVisualization.highlightNode) {
+ this.treeVisualization.highlightNode(personId);
+ }
+ }
+
+ highlightAllResults() {
+ this.clearHighlights();
+
+ this.currentHighlights = this.searchResults.map(result => result.id);
+
+ if (this.treeVisualization && this.treeVisualization.highlightNodes) {
+ this.treeVisualization.highlightNodes(this.currentHighlights);
+ }
+ }
+
+ clearHighlights() {
+ this.currentHighlights = [];
+
+ if (this.treeVisualization && this.treeVisualization.clearHighlights) {
+ this.treeVisualization.clearHighlights();
+ }
+ }
+
+ // Utility Methods
+
+ getSelectedSearchFields() {
+ const checkboxes = document.querySelectorAll('input[name="searchFields"]:checked');
+ return Array.from(checkboxes).map(cb => cb.value);
+ }
+
+ getSelectedGenderFilter() {
+ const radio = document.querySelector('input[name="genderFilter"]:checked');
+ return radio ? radio.value : 'all';
+ }
+
+ getGenerationFilter() {
+ const select = document.getElementById('generation-filter');
+ return select ? select.value : 'all';
+ }
+
+ extractVerseText(verses) {
+ if (!Array.isArray(verses)) return '';
+ return verses.map(verse => verse.text || '').join(' ');
+ }
+
+ determineGender(person) {
+ const name = person.name.toLowerCase();
+ const femaleNames = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'mary', 'elizabeth'];
+ return femaleNames.includes(name) ? 'female' : 'male';
+ }
+
+ updateResultsCount() {
+ const countElement = document.querySelector('.results-count');
+ if (countElement) {
+ countElement.textContent = `${this.searchResults.length} result${this.searchResults.length !== 1 ? 's' : ''}`;
+ }
+ }
+
+ clearSearch() {
+ this.searchResults = [];
+ this.displaySearchResults();
+ this.updateResultsCount();
+ this.clearHighlights();
+ }
+
+ addToSearchHistory(query) {
+ if (!this.searchHistory.includes(query)) {
+ this.searchHistory.unshift(query);
+ if (this.searchHistory.length > 20) {
+ this.searchHistory.pop();
+ }
+ }
+ }
+
+ showNotification(message) {
+ // Simple notification system
+ const notification = document.createElement('div');
+ notification.className = 'search-notification';
+ notification.textContent = message;
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.remove();
+ }, 3000);
+ }
+
+ exportSearchResults() {
+ if (this.searchResults.length === 0) {
+ this.showNotification('No search results to export');
+ return;
+ }
+
+ const csvContent = this.generateSearchResultsCSV();
+ this.downloadCSV(csvContent, 'family_tree_search_results.csv');
+ }
+
+ generateSearchResultsCSV() {
+ const headers = ['Name', 'Title', 'Description', 'Birth Year', 'Death Year'];
+ const rows = [headers];
+
+ this.searchResults.forEach(result => {
+ const person = this.familyData[result.id];
+ rows.push([
+ person.name,
+ person.title || '',
+ person.description || '',
+ person.birth_year || '',
+ person.death_year || ''
+ ]);
+ });
+
+ return rows.map(row => row.map(field => `"${field}"`).join(',')).join('\n');
+ }
+
+ downloadCSV(content, filename) {
+ const blob = new Blob([content], { type: 'text/csv' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+
+ // Public API
+
+ selectPerson(personId) {
+ this.addToBreadcrumbs(personId);
+
+ // Call the external select person function
+ if (window.selectPerson) {
+ window.selectPerson(personId);
+ }
+ }
+
+ getCurrentHighlights() {
+ return [...this.currentHighlights];
+ }
+
+ getSearchResults() {
+ return [...this.searchResults];
+ }
+
+ setTreeVisualization(treeViz) {
+ this.treeVisualization = treeViz;
+ }
+}
+
+// CSS Styles for Search Interface
+const searchStyles = `
+
+`;
+
+// Inject styles
+document.head.insertAdjacentHTML('beforeend', searchStyles);
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = FamilyTreeSearch;
+}
\ No newline at end of file