183 lines
4.7 KiB
JavaScript
183 lines
4.7 KiB
JavaScript
/**
|
|
* 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))
|
|
} |