reverse pedigree
This commit is contained in:
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import PedigreeTree from '../components/PedigreeTree'
|
||||
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
||||
import { transformPedigreeData, transformDescendantData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
||||
|
||||
function PedigreeView() {
|
||||
const { id } = useParams()
|
||||
@@ -14,28 +14,39 @@ function PedigreeView() {
|
||||
const [pedigreeData, setPedigreeData] = useState(null)
|
||||
const [coiData, setCoiData] = useState(null)
|
||||
const [generations, setGenerations] = useState(5)
|
||||
const [viewMode, setViewMode] = useState('ancestors')
|
||||
|
||||
useEffect(() => {
|
||||
fetchPedigreeData()
|
||||
}, [id, generations])
|
||||
}, [id, generations, viewMode])
|
||||
|
||||
const fetchPedigreeData = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
||||
const dogData = pedigreeRes.data
|
||||
setDog(dogData)
|
||||
if (viewMode === 'ancestors') {
|
||||
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
||||
const dogData = pedigreeRes.data
|
||||
setDog(dogData)
|
||||
|
||||
const treeData = transformPedigreeData(dogData, generations)
|
||||
setPedigreeData(treeData)
|
||||
const treeData = transformPedigreeData(dogData, generations)
|
||||
setPedigreeData(treeData)
|
||||
|
||||
try {
|
||||
const coiRes = await axios.get(`/api/pedigree/${id}/coi`)
|
||||
setCoiData(coiRes.data)
|
||||
} catch (coiError) {
|
||||
console.warn('COI calculation unavailable:', coiError)
|
||||
try {
|
||||
const coiRes = await axios.get(`/api/pedigree/${id}/coi`)
|
||||
setCoiData(coiRes.data)
|
||||
} catch (coiError) {
|
||||
console.warn('COI calculation unavailable:', coiError)
|
||||
setCoiData(null)
|
||||
}
|
||||
} else {
|
||||
const descendantRes = await axios.get(`/api/pedigree/${id}/descendants?generations=${generations}`)
|
||||
const dogData = descendantRes.data
|
||||
setDog(dogData)
|
||||
|
||||
const treeData = transformDescendantData(dogData, generations)
|
||||
setPedigreeData(treeData)
|
||||
setCoiData(null)
|
||||
}
|
||||
|
||||
@@ -86,7 +97,7 @@ function PedigreeView() {
|
||||
return (
|
||||
<div className="container">
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => navigate(`/dogs/${id}`)}
|
||||
@@ -96,10 +107,10 @@ function PedigreeView() {
|
||||
Back to Profile
|
||||
</button>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
|
||||
{dog?.name}'s Pedigree
|
||||
{dog?.name}'s {viewMode === 'ancestors' ? 'Pedigree' : 'Descendants'}
|
||||
</h1>
|
||||
{dog?.registration_number && (
|
||||
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
|
||||
@@ -107,65 +118,86 @@ function PedigreeView() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', padding: '4px', borderRadius: 'var(--radius)' }}>
|
||||
<button
|
||||
className={`btn ${viewMode === 'ancestors' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => setViewMode('ancestors')}
|
||||
style={{ padding: '0.5rem 1rem' }}
|
||||
>
|
||||
Ancestors
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${viewMode === 'descendants' ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => setViewMode('descendants')}
|
||||
style={{ padding: '0.5rem 1rem' }}
|
||||
>
|
||||
Descendants
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
|
||||
|
||||
{/* COI */}
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
||||
Coefficient of Inbreeding
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: coiInfo.color }}>
|
||||
{coiInfo.value}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
background: coiInfo.color + '20',
|
||||
color: coiInfo.color,
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600'
|
||||
}}>
|
||||
{coiInfo.level}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
|
||||
{coiInfo.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Completeness */}
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
||||
Pedigree Completeness
|
||||
</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--text-primary)' }}>
|
||||
{completeness}%
|
||||
</div>
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<div style={{
|
||||
height: '6px',
|
||||
background: 'var(--bg-tertiary)',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid var(--border)'
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: `${completeness}%`,
|
||||
background: barColor,
|
||||
borderRadius: '3px',
|
||||
transition: 'width 0.4s ease',
|
||||
boxShadow: `0 0 6px ${barColor}`
|
||||
}} />
|
||||
{viewMode === 'ancestors' && (
|
||||
<>
|
||||
{/* COI */}
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
||||
Coefficient of Inbreeding
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: coiInfo.color }}>
|
||||
{coiInfo.value}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
background: coiInfo.color + '20',
|
||||
color: coiInfo.color,
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600'
|
||||
}}>
|
||||
{coiInfo.level}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
|
||||
{coiInfo.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Completeness */}
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
||||
Pedigree Completeness
|
||||
</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--text-primary)' }}>
|
||||
{completeness}%
|
||||
</div>
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<div style={{
|
||||
height: '6px',
|
||||
background: 'var(--bg-tertiary)',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid var(--border)'
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: `${completeness}%`,
|
||||
background: barColor,
|
||||
borderRadius: '3px',
|
||||
transition: 'width 0.4s ease',
|
||||
boxShadow: `0 0 6px ${barColor}`
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Generations */}
|
||||
<div>
|
||||
|
||||
@@ -180,4 +180,48 @@ 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API descendant data to react-d3-tree format
|
||||
* @param {Object} dog - Dog object from API with nested offspring array
|
||||
* @param {number} maxGenerations - Maximum generations to display (default 3)
|
||||
* @returns {Object} Tree data in react-d3-tree format
|
||||
*/
|
||||
export const transformDescendantData = (dog, maxGenerations = 3) => {
|
||||
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: []
|
||||
}
|
||||
|
||||
if (dogData.offspring && dogData.offspring.length > 0) {
|
||||
dogData.offspring.forEach(child => {
|
||||
const childNode = buildTree(child, generation + 1)
|
||||
if (childNode) {
|
||||
node.children.push(childNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (node.children.length === 0) {
|
||||
delete node.children
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
return buildTree(dog)
|
||||
}
|
||||
Reference in New Issue
Block a user