Add pedigree helper utilities for data transformation

This commit is contained in:
2026-03-09 00:43:02 -05:00
parent dca3c5709b
commit 8db5c89791

View File

@@ -0,0 +1,183 @@
/**
* Transform API pedigree data to react-d3-tree format
* @param {Object} dog - Dog object from API with nested sire/dam
* @param {number} maxGenerations - Maximum generations to display (default 5)
* @returns {Object} Tree data in react-d3-tree format
*/
export const transformPedigreeData = (dog, maxGenerations = 5) => {
if (!dog) return null
const buildTree = (dogData, generation = 0) => {
if (!dogData || generation >= maxGenerations) {
return null
}
const node = {
name: dogData.name || 'Unknown',
attributes: {
id: dogData.id,
sex: dogData.sex,
registration: dogData.registration_number || '',
birth_year: dogData.birth_date ? new Date(dogData.birth_date).getFullYear() : ''
},
children: []
}
// Add sire (father) to children
if (dogData.sire) {
const sireNode = buildTree(dogData.sire, generation + 1)
if (sireNode) {
node.children.push(sireNode)
}
}
// Add dam (mother) to children
if (dogData.dam) {
const damNode = buildTree(dogData.dam, generation + 1)
if (damNode) {
node.children.push(damNode)
}
}
// Remove empty children array
if (node.children.length === 0) {
delete node.children
}
return node
}
return buildTree(dog)
}
/**
* Calculate total ancestors in pedigree
* @param {Object} treeData - Tree data structure
* @returns {number} Total number of ancestors
*/
export const countAncestors = (treeData) => {
if (!treeData) return 0
let count = 1
if (treeData.children) {
treeData.children.forEach(child => {
count += countAncestors(child)
})
}
return count - 1 // Exclude the root dog
}
/**
* Get generation counts
* @param {Object} treeData - Tree data structure
* @returns {Object} Generation counts { 1: count, 2: count, ... }
*/
export const getGenerationCounts = (treeData) => {
const counts = {}
const traverse = (node, generation = 0) => {
if (!node) return
counts[generation] = (counts[generation] || 0) + 1
if (node.children) {
node.children.forEach(child => traverse(child, generation + 1))
}
}
traverse(treeData)
delete counts[0] // Remove the root dog
return counts
}
/**
* Check if pedigree is complete for given generations
* @param {Object} treeData - Tree data structure
* @param {number} generations - Number of generations to check
* @returns {boolean} True if complete
*/
export const isPedigreeComplete = (treeData, generations = 3) => {
const expectedCount = Math.pow(2, generations) - 1
const actualCount = countAncestors(treeData)
return actualCount >= expectedCount
}
/**
* Find common ancestors between two dogs
* @param {Object} dog1Tree - First dog's pedigree tree
* @param {Object} dog2Tree - Second dog's pedigree tree
* @returns {Array} Array of common ancestor IDs
*/
export const findCommonAncestors = (dog1Tree, dog2Tree) => {
const getAncestorIds = (tree) => {
const ids = new Set()
const traverse = (node) => {
if (!node) return
if (node.attributes?.id) ids.add(node.attributes.id)
if (node.children) {
node.children.forEach(traverse)
}
}
traverse(tree)
return ids
}
const ids1 = getAncestorIds(dog1Tree)
const ids2 = getAncestorIds(dog2Tree)
return Array.from(ids1).filter(id => ids2.has(id))
}
/**
* Format COI value with risk level
* @param {number} coi - Coefficient of Inbreeding
* @returns {Object} { value, level, color, description }
*/
export const formatCOI = (coi) => {
if (coi === null || coi === undefined) {
return {
value: 'N/A',
level: 'unknown',
color: '#6b7280',
description: 'COI cannot be calculated'
}
}
const value = coi.toFixed(2)
if (coi <= 5) {
return {
value: `${value}%`,
level: 'low',
color: '#10b981',
description: 'Low inbreeding - Excellent genetic diversity'
}
} else if (coi <= 10) {
return {
value: `${value}%`,
level: 'medium',
color: '#f59e0b',
description: 'Moderate inbreeding - Acceptable with caution'
}
} else {
return {
value: `${value}%`,
level: 'high',
color: '#ef4444',
description: 'High inbreeding - Consider genetic diversity'
}
}
}
/**
* Get pedigree completeness percentage
* @param {Object} treeData - Tree data structure
* @param {number} targetGenerations - Target generations
* @returns {number} Percentage complete (0-100)
*/
export const getPedigreeCompleteness = (treeData, targetGenerations = 5) => {
const expectedTotal = Math.pow(2, targetGenerations) - 1
const actualCount = countAncestors(treeData)
return Math.min(100, Math.round((actualCount / expectedTotal) * 100))
}