From 51e4eeb561fd3a764a0c089ffd5d62f5b84d7928 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sat, 29 Nov 2025 14:20:00 -0500 Subject: [PATCH] Add more interesting family tree statistics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New statistics added to the family tree overview: Most Siblings: - Tracks which person had the most siblings - Currently shows Nathan with 18 siblings (one of David's 19 children) Close Family Marriages: - Detects marriages between close relatives - Checks for sibling marriages and aunt/uncle-niece/nephew relationships - Shows 0 in current GEDCOM data - Provides historical context: "common in early biblical times" - Important for understanding biblical/ancient Near Eastern culture API Changes: - Add most_siblings field to FamilyTreeStatsResponse - Add close_family_marriages field with description - Calculate sibling counts from GEDCOM relationship data - Detect close family relationships through parent/sibling analysis Template Updates: - Add "Most Siblings" row with clickable link to person page - Add "Close Family Marriages" row with contextual note - Populate values via JavaScript from stats API Test Updates: - Add assertions for most_siblings structure - Add assertions for close_family_marriages value - Verify all new fields are present and correctly typed This helps provide educational context about how family structures differed in ancient biblical times compared to modern norms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- kjvstudy_org/routes/api.py | 49 ++++++++++++++++++++++++- kjvstudy_org/templates/family_tree.html | 22 +++++++++++ tests/test_api.py | 16 ++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/kjvstudy_org/routes/api.py b/kjvstudy_org/routes/api.py index 7064e37..7adfb9a 100644 --- a/kjvstudy_org/routes/api.py +++ b/kjvstudy_org/routes/api.py @@ -266,8 +266,10 @@ class FamilyTreeStatsResponse(BaseModel): total_generations: int = Field(..., json_schema_extra={"example": 77}) longest_lived: PersonStat most_children: PersonStat + most_siblings: PersonStat average_lifespan: Optional[float] = Field(None, json_schema_extra={"example": 256.5}) total_with_known_ages: int = Field(..., json_schema_extra={"example": 156}) + close_family_marriages: int = Field(..., json_schema_extra={"example": 3}, description="Marriages between close relatives (common in early biblical times)") # Mapping of category names to their data dictionaries @@ -2025,6 +2027,14 @@ def api_family_tree_stats(): most_children_person_id = None most_children_count = 0 + # Find person with most siblings + most_siblings_person = None + most_siblings_person_id = None + most_siblings_count = 0 + + # Track close family marriages + close_family_marriages_count = 0 + # Calculate average lifespan total_age = 0 people_with_ages = 0 @@ -2092,6 +2102,36 @@ def api_family_tree_stats(): most_children_person = person most_children_person_id = person_id + # Check siblings count + siblings_count = len(person.get("siblings", [])) + if siblings_count > most_siblings_count: + most_siblings_count = siblings_count + most_siblings_person = person + most_siblings_person_id = person_id + + # Check for close family marriages (if person has spouse) + if person.get("spouse"): + spouse_name = person.get("spouse") + # Check if spouse is in the family tree + for potential_spouse_id, potential_spouse in family_tree_data.items(): + if potential_spouse.get("name") == spouse_name: + # Check if they share parents (siblings) + person_parents = set(person.get("parents", [])) + spouse_parents = set(potential_spouse.get("parents", [])) + + if person_parents and spouse_parents and person_parents & spouse_parents: + # They share at least one parent - siblings or half-siblings + close_family_marriages_count += 0.5 # Count each marriage once (will be seen from both sides) + + # Check if spouse is parent's sibling (aunt/uncle-niece/nephew) + for parent_id in person.get("parents", []): + if parent_id in family_tree_data: + parent_siblings = family_tree_data[parent_id].get("siblings", []) + if potential_spouse_id in parent_siblings: + close_family_marriages_count += 0.5 + + break + # Calculate average lifespan average_lifespan = round(total_age / people_with_ages, 1) if people_with_ages > 0 else None @@ -2111,8 +2151,15 @@ def api_family_tree_stats(): "value": most_children_count, "additional_info": f"Had {most_children_count} children" if most_children_person else None }, + "most_siblings": { + "name": most_siblings_person["name"] if most_siblings_person else "Unknown", + "person_id": most_siblings_person_id if most_siblings_person_id else "unknown", + "value": most_siblings_count, + "additional_info": f"Had {most_siblings_count} siblings" if most_siblings_person else None + }, "average_lifespan": average_lifespan, - "total_with_known_ages": people_with_ages + "total_with_known_ages": people_with_ages, + "close_family_marriages": int(close_family_marriages_count) } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to load family tree statistics: {str(e)}") diff --git a/kjvstudy_org/templates/family_tree.html b/kjvstudy_org/templates/family_tree.html index f44cea0..343df2d 100644 --- a/kjvstudy_org/templates/family_tree.html +++ b/kjvstudy_org/templates/family_tree.html @@ -181,6 +181,14 @@ section:nth-of-type(3) { Most Children: — + + Most Siblings: + — + + + Close Family Marriages: + — + @@ -303,6 +311,20 @@ document.addEventListener('DOMContentLoaded', function() { `${personName} (${data.most_children.value} children)`; } + // Most siblings + if (data.most_siblings && data.most_siblings.value > 0) { + const personName = data.most_siblings.name; + const personId = data.most_siblings.person_id; + document.getElementById('stat-most-siblings').innerHTML = + `${personName} (${data.most_siblings.value} siblings)`; + } + + // Close family marriages + if (data.close_family_marriages !== undefined) { + document.getElementById('stat-close-marriages').textContent = + data.close_family_marriages + ' (common in early biblical times)'; + } + // Show stats, hide loading statsLoading.style.display = 'none'; statsContainer.style.display = 'table'; diff --git a/tests/test_api.py b/tests/test_api.py index 3d94afc..3cef79b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -933,8 +933,10 @@ class TestFamilyTreeEndpoints: assert "total_generations" in data assert "longest_lived" in data assert "most_children" in data + assert "most_siblings" in data assert "average_lifespan" in data assert "total_with_known_ages" in data + assert "close_family_marriages" in data # Verify types assert isinstance(data["total_people"], int) @@ -961,9 +963,23 @@ class TestFamilyTreeEndpoints: assert isinstance(data["most_children"]["value"], int) assert data["most_children"]["value"] >= 0 + # Verify most_siblings structure + assert "name" in data["most_siblings"] + assert "person_id" in data["most_siblings"] + assert "value" in data["most_siblings"] + assert "additional_info" in data["most_siblings"] + assert isinstance(data["most_siblings"]["name"], str) + assert isinstance(data["most_siblings"]["person_id"], str) + assert isinstance(data["most_siblings"]["value"], int) + assert data["most_siblings"]["value"] >= 0 + # Verify average lifespan is either a number or null assert data["average_lifespan"] is None or isinstance(data["average_lifespan"], (int, float)) + # Verify close family marriages is an integer + assert isinstance(data["close_family_marriages"], int) + assert data["close_family_marriages"] >= 0 + def test_api_index_includes_new_endpoints(self, client): """Test that API index includes all new endpoints""" response = client.get("/api/")