From e62c2bcd32f851c05585f784587a980d0dffb012 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:40:56 -0500 Subject: [PATCH] Add interactive PedigreeTree component with D3 visualization --- client/src/components/PedigreeTree.jsx | 167 +++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 client/src/components/PedigreeTree.jsx diff --git a/client/src/components/PedigreeTree.jsx b/client/src/components/PedigreeTree.jsx new file mode 100644 index 0000000..ee72413 --- /dev/null +++ b/client/src/components/PedigreeTree.jsx @@ -0,0 +1,167 @@ +import { useState, useCallback, useEffect } from 'react' +import Tree from 'react-d3-tree' +import { ZoomIn, ZoomOut, Maximize2, Download } from 'lucide-react' +import './PedigreeTree.css' + +const PedigreeTree = ({ dogId, pedigreeData, coi }) => { + const [translate, setTranslate] = useState({ x: 0, y: 0 }) + const [zoom, setZoom] = useState(0.8) + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + + useEffect(() => { + const updateDimensions = () => { + const container = document.getElementById('tree-container') + if (container) { + setDimensions({ + width: container.offsetWidth, + height: container.offsetHeight + }) + setTranslate({ + x: container.offsetWidth / 4, + y: container.offsetHeight / 2 + }) + } + } + + updateDimensions() + window.addEventListener('resize', updateDimensions) + return () => window.removeEventListener('resize', updateDimensions) + }, []) + + const handleZoomIn = () => setZoom(z => Math.min(z + 0.2, 2)) + const handleZoomOut = () => setZoom(z => Math.max(z - 0.2, 0.2)) + const handleReset = () => { + setZoom(0.8) + setTranslate({ + x: dimensions.width / 4, + y: dimensions.height / 2 + }) + } + + const renderCustomNode = ({ nodeDatum, toggleNode }) => { + const isMale = nodeDatum.attributes?.sex === 'male' + const nodeColor = isMale ? '#3b82f6' : '#ec4899' + + return ( + + { + if (nodeDatum.attributes?.id) { + window.location.href = `/dogs/${nodeDatum.attributes.id}` + } + }} + /> + + {isMale ? '♂' : '♀'} + + + {nodeDatum.name} + + {nodeDatum.attributes?.registration && ( + + {nodeDatum.attributes.registration} + + )} + {nodeDatum.attributes?.birth_year && ( + + ({nodeDatum.attributes.birth_year}) + + )} + + ) + } + + return ( +
+
+
+ + + +
+ {coi !== null && coi !== undefined && ( +
+ COI: + 10 ? 'high' : coi > 5 ? 'medium' : 'low'}`}> + {coi.toFixed(2)}% + +
+ )} +
+ +
+
+
+ Male +
+
+
+ Female +
+
+ +
+ {pedigreeData && dimensions.width > 0 && ( + { + setZoom(zoom) + setTranslate(translate) + }} + orientation="horizontal" + pathFunc="step" + separation={{ siblings: 1.5, nonSiblings: 2 }} + nodeSize={{ x: 200, y: 150 }} + renderCustomNodeElement={renderCustomNode} + enableLegacyTransitions + transitionDuration={300} + /> + )} +
+
+ ) +} + +export default PedigreeTree \ No newline at end of file