Can customize shown depth
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		| @@ -5,22 +5,27 @@ import { | |||||||
|   FormControlLabel, |   FormControlLabel, | ||||||
|   FormLabel, |   FormLabel, | ||||||
|   IconButton, |   IconButton, | ||||||
|  |   InputLabel, | ||||||
|  |   MenuItem, | ||||||
|   Paper, |   Paper, | ||||||
|   Radio, |   Radio, | ||||||
|   RadioGroup, |   RadioGroup, | ||||||
|  |   Select, | ||||||
|   Tab, |   Tab, | ||||||
|   Tabs, |   Tabs, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { useParams } from "react-router-dom"; | import { useParams } from "react-router-dom"; | ||||||
| import { | import { | ||||||
|  |   FamilyTreeNode, | ||||||
|   buildAscendingTree, |   buildAscendingTree, | ||||||
|   buildDescendingTree, |   buildDescendingTree, | ||||||
|  |   treeHeight, | ||||||
| } from "../../utils/family_tree"; | } from "../../utils/family_tree"; | ||||||
| import { useFamily } from "../../widgets/BaseFamilyRoute"; | import { useFamily } from "../../widgets/BaseFamilyRoute"; | ||||||
|  | import { BasicFamilyTree } from "../../widgets/BasicFamilyTree"; | ||||||
| import { MemberItem } from "../../widgets/MemberItem"; | import { MemberItem } from "../../widgets/MemberItem"; | ||||||
| import { RouterLink } from "../../widgets/RouterLink"; | import { RouterLink } from "../../widgets/RouterLink"; | ||||||
| import { BasicFamilyTree } from "../../widgets/BasicFamilyTree"; |  | ||||||
| import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree"; | import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree"; | ||||||
|  |  | ||||||
| enum CurrTab { | enum CurrTab { | ||||||
| @@ -43,15 +48,17 @@ export function FamilyMemberTreeRoute(): React.ReactElement { | |||||||
|  |  | ||||||
|   const member = family.members.get(Number(memberId)); |   const member = family.members.get(Number(memberId)); | ||||||
|  |  | ||||||
|   const tree = React.useMemo( |   const memo: [FamilyTreeNode, number] | null = React.useMemo(() => { | ||||||
|     () => |     if (!member) return null; | ||||||
|       !member |     const tree = | ||||||
|         ? null |       currMode === TreeMode.Ascending | ||||||
|         : currMode === TreeMode.Ascending |  | ||||||
|         ? buildAscendingTree(member.id, family.members, family.couples) |         ? buildAscendingTree(member.id, family.members, family.couples) | ||||||
|         : buildDescendingTree(member.id, family.members, family.couples), |         : buildDescendingTree(member.id, family.members, family.couples); | ||||||
|     [member, currMode, family.members, family.couples] |  | ||||||
|   ); |     return [tree, treeHeight(tree)]; | ||||||
|  |   }, [member, currMode, family.members, family.couples]); | ||||||
|  |  | ||||||
|  |   const [currDepth, setCurrDepth] = React.useState(0); | ||||||
|  |  | ||||||
|   if (!member) { |   if (!member) { | ||||||
|     return ( |     return ( | ||||||
| @@ -61,6 +68,10 @@ export function FamilyMemberTreeRoute(): React.ReactElement { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const [tree, maxDepth] = memo!; | ||||||
|  |  | ||||||
|  |   if (currDepth === 0) setCurrDepth(maxDepth); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       style={{ |       style={{ | ||||||
| @@ -90,7 +101,10 @@ export function FamilyMemberTreeRoute(): React.ReactElement { | |||||||
|           <RadioGroup |           <RadioGroup | ||||||
|             row |             row | ||||||
|             value={currMode} |             value={currMode} | ||||||
|             onChange={(_e, v) => setCurrMode(Number(v))} |             onChange={(_e, v) => { | ||||||
|  |               setCurrDepth(0); | ||||||
|  |               setCurrMode(Number(v)); | ||||||
|  |             }} | ||||||
|           > |           > | ||||||
|             <FormControlLabel |             <FormControlLabel | ||||||
|               value={TreeMode.Descending} |               value={TreeMode.Descending} | ||||||
| @@ -105,7 +119,26 @@ export function FamilyMemberTreeRoute(): React.ReactElement { | |||||||
|           </RadioGroup> |           </RadioGroup> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|  |  | ||||||
|         <div style={{ flex: "2" }}></div> |         <div style={{ flex: "1" }}></div> | ||||||
|  |  | ||||||
|  |         <FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}> | ||||||
|  |           <InputLabel>Profondeur</InputLabel> | ||||||
|  |           <Select | ||||||
|  |             value={currDepth} | ||||||
|  |             onChange={(v) => setCurrDepth(Number(v.target.value))} | ||||||
|  |             label="Profondeur" | ||||||
|  |           > | ||||||
|  |             {Array(maxDepth) | ||||||
|  |               .fill(0) | ||||||
|  |               .map((_v, index) => ( | ||||||
|  |                 <MenuItem key={index} value={index + 1}> | ||||||
|  |                   {index + 1} | ||||||
|  |                 </MenuItem> | ||||||
|  |               ))} | ||||||
|  |           </Select> | ||||||
|  |         </FormControl> | ||||||
|  |  | ||||||
|  |         <div style={{ flex: "1" }}></div> | ||||||
|  |  | ||||||
|         <Tabs |         <Tabs | ||||||
|           value={currTab} |           value={currTab} | ||||||
| @@ -120,11 +153,12 @@ export function FamilyMemberTreeRoute(): React.ReactElement { | |||||||
|       {/* the tree itself */} |       {/* the tree itself */} | ||||||
|       <Paper style={{ flex: "1" }}> |       <Paper style={{ flex: "1" }}> | ||||||
|         {currTab === CurrTab.BasicTree ? ( |         {currTab === CurrTab.BasicTree ? ( | ||||||
|           <BasicFamilyTree tree={tree!} /> |           <BasicFamilyTree tree={tree!} depth={currDepth} /> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <ComplexFamilyTree |           <ComplexFamilyTree | ||||||
|             tree={tree!} |             tree={tree!} | ||||||
|             isUp={currMode === TreeMode.Ascending} |             isUp={currMode === TreeMode.Ascending} | ||||||
|  |             depth={currDepth} | ||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
|       </Paper> |       </Paper> | ||||||
|   | |||||||
| @@ -82,6 +82,9 @@ export function buildDescendingTree( | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Sort family tree children per date of birth | ||||||
|  |  */ | ||||||
| function sortChildren(n: FamilyTreeNode[]): FamilyTreeNode[] { | function sortChildren(n: FamilyTreeNode[]): FamilyTreeNode[] { | ||||||
|   n.sort( |   n.sort( | ||||||
|     (a, b) => |     (a, b) => | ||||||
| @@ -89,3 +92,71 @@ function sortChildren(n: FamilyTreeNode[]): FamilyTreeNode[] { | |||||||
|   ); |   ); | ||||||
|   return n; |   return n; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compute family tree height | ||||||
|  |  */ | ||||||
|  | export function treeHeight(node: FamilyTreeNode): number { | ||||||
|  |   let res = | ||||||
|  |     node.down?.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) ?? 0; | ||||||
|  |  | ||||||
|  |   node.couples?.forEach( | ||||||
|  |     (c) => | ||||||
|  |       (res = Math.max( | ||||||
|  |         res, | ||||||
|  |         c.down.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) | ||||||
|  |       )) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return res + 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compute family tree width | ||||||
|  |  */ | ||||||
|  | export function treeWidth(node: FamilyTreeNode): number { | ||||||
|  |   const values = new Array(treeHeight(node)).fill(0); | ||||||
|  |   treeWidthRecurse(node, values, 0); | ||||||
|  |   return Math.max(...values); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function treeWidthRecurse(node: FamilyTreeNode, vals: number[], level: number) { | ||||||
|  |   vals[level] += | ||||||
|  |     1 + (node.couples?.length ?? 0) + ((node.down?.length ?? 0) > 0 ? 1 : 0); | ||||||
|  |  | ||||||
|  |   node.down?.forEach((n) => treeWidthRecurse(n, vals, level + 1)); | ||||||
|  |  | ||||||
|  |   node.couples?.forEach((c) => | ||||||
|  |     c.down.forEach((n) => treeWidthRecurse(n, vals, level + 1)) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get the list of members to be shown in a tree, | ||||||
|  |  * depending of a specified depth | ||||||
|  |  */ | ||||||
|  | export function getAvailableMembers( | ||||||
|  |   t: FamilyTreeNode, | ||||||
|  |   depth: number | ||||||
|  | ): Set<number> { | ||||||
|  |   const s = new Set<number>(); | ||||||
|  |   getAvailableMembersRecurse(t, depth, s); | ||||||
|  |   return s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getAvailableMembersRecurse( | ||||||
|  |   t: FamilyTreeNode, | ||||||
|  |   depth: number, | ||||||
|  |   s: Set<number> | ||||||
|  | ) { | ||||||
|  |   if (depth < 1) return; | ||||||
|  |  | ||||||
|  |   s.add(t.member.id); | ||||||
|  |  | ||||||
|  |   t.couples?.forEach((c) => { | ||||||
|  |     s.add(c.member.id); | ||||||
|  |     c.down.forEach((e) => getAvailableMembersRecurse(e, depth - 1, s)); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   t.down?.forEach((e) => getAvailableMembersRecurse(e, depth - 1, s)); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import { MemberPhoto } from "./MemberPhoto"; | |||||||
|  |  | ||||||
| export function BasicFamilyTree(p: { | export function BasicFamilyTree(p: { | ||||||
|   tree: FamilyTreeNode; |   tree: FamilyTreeNode; | ||||||
|  |   depth: number; | ||||||
| }): React.ReactElement { | }): React.ReactElement { | ||||||
|   return ( |   return ( | ||||||
|     <TreeView |     <TreeView | ||||||
| @@ -20,12 +21,15 @@ export function BasicFamilyTree(p: { | |||||||
|       defaultExpandIcon={<ChevronRightIcon />} |       defaultExpandIcon={<ChevronRightIcon />} | ||||||
|       sx={{ flexGrow: 1 }} |       sx={{ flexGrow: 1 }} | ||||||
|     > |     > | ||||||
|       <FamilyTreeItem n={p.tree} /> |       <FamilyTreeItem n={p.tree} depth={p.depth} /> | ||||||
|     </TreeView> |     </TreeView> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function FamilyTreeItem(p: { n: FamilyTreeNode }): React.ReactElement { | function FamilyTreeItem(p: { | ||||||
|  |   depth: number; | ||||||
|  |   n: FamilyTreeNode; | ||||||
|  | }): React.ReactElement { | ||||||
|   let children = p.n.down ?? []; |   let children = p.n.down ?? []; | ||||||
|  |  | ||||||
|   if (p.n.couples) { |   if (p.n.couples) { | ||||||
| @@ -55,9 +59,10 @@ function FamilyTreeItem(p: { n: FamilyTreeNode }): React.ReactElement { | |||||||
|         </div> |         </div> | ||||||
|       } |       } | ||||||
|     > |     > | ||||||
|       {children.map((c) => ( |       {p.depth >= 2 && | ||||||
|         <FamilyTreeItem key={c.member.id} n={c} /> |         children.map((c) => ( | ||||||
|       ))} |           <FamilyTreeItem depth={p.depth - 1} key={c.member.id} n={c} /> | ||||||
|  |         ))} | ||||||
|     </TreeItem> |     </TreeItem> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,13 +9,19 @@ import "svg2pdf.js"; | |||||||
| import { Couple } from "../../api/CoupleApi"; | import { Couple } from "../../api/CoupleApi"; | ||||||
| import { Member, fmtDate } from "../../api/MemberApi"; | import { Member, fmtDate } from "../../api/MemberApi"; | ||||||
| import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; | import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; | ||||||
| import { FamilyTreeNode } from "../../utils/family_tree"; | import { | ||||||
|  |   FamilyTreeNode, | ||||||
|  |   getAvailableMembers, | ||||||
|  |   treeHeight, | ||||||
|  |   treeWidth, | ||||||
|  | } from "../../utils/family_tree"; | ||||||
| import { downloadBlob } from "../../utils/files_utils"; | import { downloadBlob } from "../../utils/files_utils"; | ||||||
| import "./family-chart.css"; | import "./family-chart.css"; | ||||||
|  |  | ||||||
| export function ComplexFamilyTree(p: { | export function ComplexFamilyTree(p: { | ||||||
|   tree: FamilyTreeNode; |   tree: FamilyTreeNode; | ||||||
|   isUp: boolean; |   isUp: boolean; | ||||||
|  |   depth: number; | ||||||
| }): React.ReactElement { | }): React.ReactElement { | ||||||
|   const darkTheme = useDarkTheme(); |   const darkTheme = useDarkTheme(); | ||||||
|  |  | ||||||
| @@ -23,7 +29,7 @@ export function ComplexFamilyTree(p: { | |||||||
|     if (!container) return; |     if (!container) return; | ||||||
|  |  | ||||||
|     const store = f3.createStore({ |     const store = f3.createStore({ | ||||||
|       data: treeToF3Data(p.tree, p.isUp), |       data: treeToF3Data(p.tree, p.isUp, p.depth), | ||||||
|       node_separation: 250, |       node_separation: 250, | ||||||
|       level_separation: 150, |       level_separation: 150, | ||||||
|     }); |     }); | ||||||
| @@ -197,41 +203,12 @@ export function ComplexFamilyTree(p: { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function treeHeight(node: FamilyTreeNode): number { | function treeToF3Data( | ||||||
|   let res = |   node: FamilyTreeNode, | ||||||
|     node.down?.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) ?? 0; |   isUp: boolean, | ||||||
|  |   depth: number | ||||||
|   node.couples?.forEach( | ): f3Data[] { | ||||||
|     (c) => |   const availableMembers = getAvailableMembers(node, depth); | ||||||
|       (res = Math.max( |  | ||||||
|         res, |  | ||||||
|         c.down.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) |  | ||||||
|       )) |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   return res + 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function treeWidth(node: FamilyTreeNode): number { |  | ||||||
|   const values = new Array(treeHeight(node)).fill(0); |  | ||||||
|   treeWidthRecurse(node, values, 0); |  | ||||||
|   return Math.max(...values); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function treeWidthRecurse(node: FamilyTreeNode, vals: number[], level: number) { |  | ||||||
|   vals[level] += |  | ||||||
|     1 + (node.couples?.length ?? 0) + ((node.down?.length ?? 0) > 0 ? 1 : 0); |  | ||||||
|  |  | ||||||
|   node.down?.forEach((n) => treeWidthRecurse(n, vals, level + 1)); |  | ||||||
|  |  | ||||||
|   node.couples?.forEach((c) => |  | ||||||
|     c.down.forEach((n) => treeWidthRecurse(n, vals, level + 1)) |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function treeToF3Data(node: FamilyTreeNode, isUp: boolean): f3Data[] { |  | ||||||
|   const availableMembers = new Set<number>(); |  | ||||||
|   getAvailableMembers(node, availableMembers); |  | ||||||
|  |  | ||||||
|   const list: f3Data[] = []; |   const list: f3Data[] = []; | ||||||
|   if (isUp) treeToF3DataUpRecurse(node, list, availableMembers); |   if (isUp) treeToF3DataUpRecurse(node, list, availableMembers); | ||||||
| @@ -239,17 +216,6 @@ function treeToF3Data(node: FamilyTreeNode, isUp: boolean): f3Data[] { | |||||||
|   return list; |   return list; | ||||||
| } | } | ||||||
|  |  | ||||||
| function getAvailableMembers(t: FamilyTreeNode, s: Set<number>) { |  | ||||||
|   s.add(t.member.id); |  | ||||||
|  |  | ||||||
|   t.couples?.forEach((c) => { |  | ||||||
|     s.add(c.member.id); |  | ||||||
|     c.down.forEach((e) => getAvailableMembers(e, s)); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   t.down?.forEach((e) => getAvailableMembers(e, s)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function memberData(m: Member, c?: Couple): f3.f3DataData { | function memberData(m: Member, c?: Couple): f3.f3DataData { | ||||||
|   return { |   return { | ||||||
|     first_name: m.first_name ?? "_", |     first_name: m.first_name ?? "_", | ||||||
| @@ -271,6 +237,8 @@ function treeToF3DataUpRecurse( | |||||||
|   child?: number, |   child?: number, | ||||||
|   spouses?: number[] |   spouses?: number[] | ||||||
| ) { | ) { | ||||||
|  |   if (!availableMembers.has(node.member.id)) return; | ||||||
|  |  | ||||||
|   array.push({ |   array.push({ | ||||||
|     data: memberData(node.member), |     data: memberData(node.member), | ||||||
|     id: node.member.id.toString(), |     id: node.member.id.toString(), | ||||||
| @@ -309,11 +277,13 @@ function treeToF3DataDownRecurse( | |||||||
|   array: f3Data[], |   array: f3Data[], | ||||||
|   availableMembers: Set<number> |   availableMembers: Set<number> | ||||||
| ) { | ) { | ||||||
|  |   if (!availableMembers.has(node.member.id)) return; | ||||||
|  |  | ||||||
|   // Get all members ids |   // Get all members ids | ||||||
|   const children = node?.down?.map((c) => c.member.id.toString()) ?? []; |   let children = node?.down?.map((c) => c.member.id) ?? []; | ||||||
|   node.couples?.map((c) => |   node.couples?.map((c) => c.down.forEach((m) => children.push(m.member.id))); | ||||||
|     c.down.forEach((m) => children.push(m.member.id.toString())) |  | ||||||
|   ); |   children = children.filter((c) => availableMembers.has(c)); | ||||||
|  |  | ||||||
|   array.push({ |   array.push({ | ||||||
|     data: memberData(node.member), |     data: memberData(node.member), | ||||||
| @@ -328,8 +298,10 @@ function treeToF3DataDownRecurse( | |||||||
|           ? node.member.mother.toString() |           ? node.member.mother.toString() | ||||||
|           : undefined, |           : undefined, | ||||||
|  |  | ||||||
|       spouses: node.couples?.map((c) => c.member.id.toString()), |       spouses: node.couples | ||||||
|       children: children, |         ?.filter((s) => availableMembers.has(s.member.id)) | ||||||
|  |         .map((c) => c.member.id.toString()), | ||||||
|  |       children: children.map((c) => c.toString()), | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -339,6 +311,7 @@ function treeToF3DataDownRecurse( | |||||||
|  |  | ||||||
|   if (node.couples) { |   if (node.couples) { | ||||||
|     for (const c of node.couples) { |     for (const c of node.couples) { | ||||||
|  |       if (!availableMembers.has(c.member.id)) continue; | ||||||
|       array.push({ |       array.push({ | ||||||
|         data: memberData(c.member, c.couple), |         data: memberData(c.member, c.couple), | ||||||
|         id: c.member.id.toString(), |         id: c.member.id.toString(), | ||||||
| @@ -352,7 +325,9 @@ function treeToF3DataDownRecurse( | |||||||
|               ? c.member.mother.toString() |               ? c.member.mother.toString() | ||||||
|               : undefined, |               : undefined, | ||||||
|           spouses: [node.member.id.toString()], |           spouses: [node.member.id.toString()], | ||||||
|           children: c.down.map((c) => c.member.id.toString()), |           children: c.down | ||||||
|  |             .filter((c) => availableMembers.has(c.member.id)) | ||||||
|  |             .map((c) => c.member.id.toString()), | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user