Add more interesting family tree statistics

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-29 14:20:00 -05:00
parent f7013de63a
commit 51e4eeb561
3 changed files with 86 additions and 1 deletions
+48 -1
View File
@@ -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)}")
+22
View File
@@ -181,6 +181,14 @@ section:nth-of-type(3) {
<td>Most Children:</td>
<td id="stat-most-children"></td>
</tr>
<tr>
<td>Most Siblings:</td>
<td id="stat-most-siblings"></td>
</tr>
<tr>
<td>Close Family Marriages:</td>
<td id="stat-close-marriages"></td>
</tr>
</tbody>
</table>
</section>
@@ -303,6 +311,20 @@ document.addEventListener('DOMContentLoaded', function() {
`<a href="/family-tree/person/${personId}">${personName}</a> (${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 =
`<a href="/family-tree/person/${personId}">${personName}</a> (${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';
+16
View File
@@ -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/")