From 7a16918d665f7e110ef1025eda877900f9683c8b Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:08:46 -0500 Subject: [PATCH] Add interactive pedigree tree visualization component --- client/src/components/PedigreeView.jsx | 234 +++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 client/src/components/PedigreeView.jsx diff --git a/client/src/components/PedigreeView.jsx b/client/src/components/PedigreeView.jsx new file mode 100644 index 0000000..bba0b0b --- /dev/null +++ b/client/src/components/PedigreeView.jsx @@ -0,0 +1,234 @@ +import { useState, useEffect, useCallback } from 'react' +import { X, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react' +import Tree from 'react-d3-tree' +import axios from 'axios' +import './PedigreeView.css' + +function PedigreeView({ dogId, onClose }) { + const [treeData, setTreeData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [translate, setTranslate] = useState({ x: 0, y: 0 }) + const [zoom, setZoom] = useState(0.8) + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + + useEffect(() => { + fetchPedigree() + }, [dogId]) + + useEffect(() => { + const updateDimensions = () => { + const container = document.querySelector('.pedigree-container') + if (container) { + const width = container.offsetWidth + const height = container.offsetHeight + setDimensions({ width, height }) + setTranslate({ x: width / 4, y: height / 2 }) + } + } + + updateDimensions() + window.addEventListener('resize', updateDimensions) + return () => window.removeEventListener('resize', updateDimensions) + }, []) + + const fetchPedigree = async () => { + try { + setLoading(true) + const response = await axios.get(`/api/pedigree/${dogId}?generations=5`) + const formatted = formatTreeData(response.data) + setTreeData(formatted) + } catch (err) { + setError(err.response?.data?.error || 'Failed to load pedigree') + } finally { + setLoading(false) + } + } + + const formatTreeData = (dog) => { + if (!dog) return null + + const children = [] + if (dog.sire) children.push(formatTreeData(dog.sire)) + if (dog.dam) children.push(formatTreeData(dog.dam)) + + return { + name: dog.name, + attributes: { + sex: dog.sex, + birth_date: dog.birth_date, + registration: dog.registration_number, + breed: dog.breed, + color: dog.color, + generation: dog.generation + }, + children: children.length > 0 ? children : undefined + } + } + + const handleNodeClick = useCallback((nodeData) => { + console.log('Node clicked:', nodeData) + }, []) + + const handleZoomIn = () => { + setZoom(prev => Math.min(prev + 0.2, 2)) + } + + const handleZoomOut = () => { + setZoom(prev => Math.max(prev - 0.2, 0.4)) + } + + const handleReset = () => { + setZoom(0.8) + setTranslate({ x: dimensions.width / 4, y: dimensions.height / 2 }) + } + + const renderCustomNode = ({ nodeDatum, toggleNode }) => ( + + + + {nodeDatum.attributes.sex === 'male' ? '♂' : '♀'} + + + {nodeDatum.name} + + {nodeDatum.attributes.registration && ( + + {nodeDatum.attributes.registration} + + )} + {nodeDatum.attributes.birth_date && ( + + Born: {new Date(nodeDatum.attributes.birth_date).getFullYear()} + + )} + + ) + + if (loading) { + return ( +
+
+
Loading pedigree...
+
+
+ ) + } + + if (error) { + return ( +
+
e.stopPropagation()}> +
+

Pedigree Tree

+ +
+
{error}
+
+
+ ) + } + + return ( +
+
e.stopPropagation()}> +
+

Pedigree Tree - {treeData?.name}

+
+ + + + +
+
+ +
+
+ + Male +
+
+ + Female +
+
+ +
+ {treeData && dimensions.width > 0 && ( + + )} +
+ +
+

+ Tip: Use mouse wheel to zoom, click and drag to pan. + Click on nodes to view details. +

+
+
+
+ ) +} + +export default PedigreeView