From 8db5c897916a95a668093e15db931449d0228faa Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:43:02 -0500 Subject: [PATCH] Add pedigree helper utilities for data transformation --- client/src/utils/pedigreeHelpers.js | 183 ++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 client/src/utils/pedigreeHelpers.js diff --git a/client/src/utils/pedigreeHelpers.js b/client/src/utils/pedigreeHelpers.js new file mode 100644 index 0000000..1fda70e --- /dev/null +++ b/client/src/utils/pedigreeHelpers.js @@ -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)) +} \ No newline at end of file