diff --git a/kjvstudy_org/templates/family_tree.html b/kjvstudy_org/templates/family_tree.html index bcfde98..24cd30e 100644 --- a/kjvstudy_org/templates/family_tree.html +++ b/kjvstudy_org/templates/family_tree.html @@ -229,6 +229,30 @@ stroke-width: 4px; } + .tree-node.male circle { + fill: #2d3436; + stroke: #74b9ff; + stroke-width: 3px; + } + + .tree-node.female circle { + fill: #2d3436; + stroke: #fd79a8; + stroke-width: 3px; + } + + .tree-node.current.male circle { + fill: #74b9ff; + stroke: #74b9ff; + stroke-width: 4px; + } + + .tree-node.current.female circle { + fill: #fd79a8; + stroke: #fd79a8; + stroke-width: 4px; + } + .tree-node.spouse circle { fill: #2d3436; stroke: #28a745; @@ -262,6 +286,16 @@ font-weight: bold; } + .tree-node.current.male text { + fill: white; + font-weight: bold; + } + + .tree-node.current.female text { + fill: white; + font-weight: bold; + } + .tree-link { fill: none; stroke: #636e72; @@ -285,6 +319,58 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } + .gender-legend { + position: absolute; + top: 1rem; + right: 1rem; + background: #2d3436; + border: 1px solid #636e72; + border-radius: 8px; + padding: 0.75rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + font-size: 0.8rem; + color: #f8f9fa; + } + + .legend-title { + font-weight: bold; + margin-bottom: 0.5rem; + color: #ffc107; + } + + .legend-item { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + } + + .legend-item:last-child { + margin-bottom: 0; + } + + .legend-circle { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid; + } + + .legend-circle.male { + background: #2d3436; + border-color: #74b9ff; + } + + .legend-circle.female { + background: #2d3436; + border-color: #fd79a8; + } + + .legend-circle.current { + background: #ffc107; + border-color: #ffc107; + } + .tree-controls button { margin: 0.25rem; padding: 0.25rem 0.5rem; @@ -319,6 +405,11 @@ font-size: 0.7rem; padding: 0.2rem 0.4rem; } + + .gender-legend { + font-size: 0.7rem; + padding: 0.5rem; + } } .welcome-message { @@ -623,6 +714,22 @@ + +
+
Gender
+
+
+ Male +
+
+
+ Female +
+
+
+ Current Person +
+
@@ -1025,6 +1132,62 @@ tree = d3.tree().size([height - 100, width - 200]); } + function determineGender(person) { + const name = person.name.toLowerCase(); + + // Known biblical female names (expanded list) + const femaleNames = [ + 'eve', 'sarah', 'sarai', 'rebekah', 'rebecca', 'rachel', 'leah', 'dinah', + 'tamar', 'miriam', 'deborah', 'ruth', 'naomi', 'bathsheba', 'bath-sheba', + 'abigail', 'esther', 'judith', 'mary', 'elizabeth', 'anna', 'hannah', + 'martha', 'mary magdalene', 'dorcas', 'lydia', 'priscilla', 'prisca', + 'naamah', 'adah', 'zillah', 'milcah', 'iscah', 'hagar', 'keturah', + 'asenath', 'potipherah', 'zipporah', 'gomer', 'rizpah', 'maacah', + 'haggith', 'abital', 'eglah', 'michal', 'ahinoam', 'abigail', + 'jezebel', 'athaliah', 'huldah', 'vashti', 'zeresh', 'peninnah', + 'orpah', 'ruth', 'nomi', 'mara', 'rahab', 'jael', 'sisera', + 'gideon', 'samson', 'delilah', 'hannah', 'abigail', 'bathsheba' + ]; + + // Remove duplicates and clean up the list + const uniqueFemaleNames = [...new Set(femaleNames)].filter(n => n && n.length > 0); + + // Check if it's a known female name + if (uniqueFemaleNames.includes(name)) { + return 'female'; + } + + // Check for name endings that suggest female names + if (name.endsWith('ah') || name.endsWith('iah') || name.endsWith('beth') || name.endsWith('anna')) { + // But exclude male names that also end this way + const maleExceptions = ['noah', 'judah', 'micah', 'hosea', 'nehemiah', 'jeremiah', 'isaiah', 'obadiah']; + if (!maleExceptions.includes(name)) { + return 'female'; + } + } + + // If person has a spouse, try to determine from relationship context + if (person.spouse) { + // Look for contextual clues in description or title + const description = (person.description || '').toLowerCase(); + const title = (person.title || '').toLowerCase(); + + if (description.includes('wife') || title.includes('wife') || + description.includes('mother') || title.includes('mother') || + description.includes('daughter') || title.includes('daughter')) { + return 'female'; + } + if (description.includes('husband') || title.includes('husband') || + description.includes('father') || title.includes('father') || + description.includes('son') || title.includes('son')) { + return 'male'; + } + } + + // Default to male for biblical genealogies (most recorded names are male) + return 'male'; + } + function buildTreeData(rootPersonId, maxDepth = 3) { const visited = new Set(); @@ -1039,6 +1202,7 @@ id: personId, name: person.name, data: person, + gender: determineGender(person), children: [], spouse: null }; @@ -1047,10 +1211,12 @@ if (person.spouse) { const spouseId = Object.keys(familyData).find(id => familyData[id].name === person.spouse); if (spouseId && !visited.has(spouseId)) { + const spouseData = familyData[spouseId]; node.spouse = { id: spouseId, name: person.spouse, - data: familyData[spouseId] + data: spouseData, + gender: determineGender(spouseData) }; } } @@ -1121,7 +1287,7 @@ .data(treeLayout.descendants()) .enter() .append('g') - .attr('class', d => `tree-node ${d.data.id === personId ? 'current' : ''}`) + .attr('class', d => `tree-node ${d.data.gender} ${d.data.id === personId ? 'current' : ''}`) .attr('transform', d => `translate(${d.y},${d.x})`) .on('click', (event, d) => { selectPerson(d.data.id); @@ -1141,7 +1307,7 @@ .data(treeLayout.descendants().filter(d => d.data.spouse)) .enter() .append('g') - .attr('class', 'tree-node spouse') + .attr('class', d => `tree-node spouse ${d.data.spouse.gender}`) .attr('transform', d => `translate(${d.y + 50},${d.x})`) .on('click', (event, d) => { selectPerson(d.data.spouse.id);