Add simple tree graph mode #4
@@ -27,9 +27,11 @@ 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 { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree";
 | 
					import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree";
 | 
				
			||||||
 | 
					import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum CurrTab {
 | 
					enum CurrTab {
 | 
				
			||||||
  BasicTree,
 | 
					  BasicTree,
 | 
				
			||||||
 | 
					  SimpleTree,
 | 
				
			||||||
  AdvancedTree,
 | 
					  AdvancedTree,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,7 +45,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const family = useFamily();
 | 
					  const family = useFamily();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [currTab, setCurrTab] = React.useState(CurrTab.AdvancedTree);
 | 
					  const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree);
 | 
				
			||||||
  const [currMode, setCurrMode] = React.useState(TreeMode.Descending);
 | 
					  const [currMode, setCurrMode] = React.useState(TreeMode.Descending);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const member = family.members.get(Number(memberId));
 | 
					  const member = family.members.get(Number(memberId));
 | 
				
			||||||
@@ -146,6 +148,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
 | 
				
			|||||||
          aria-label="basic tabs example"
 | 
					          aria-label="basic tabs example"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Tab tabIndex={CurrTab.BasicTree} label="Basique" />
 | 
					          <Tab tabIndex={CurrTab.BasicTree} label="Basique" />
 | 
				
			||||||
 | 
					          <Tab tabIndex={CurrTab.SimpleTree} label="Simple" />
 | 
				
			||||||
          <Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
 | 
					          <Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
 | 
				
			||||||
        </Tabs>
 | 
					        </Tabs>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
@@ -154,6 +157,12 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
 | 
				
			|||||||
      <Paper style={{ flex: "1" }}>
 | 
					      <Paper style={{ flex: "1" }}>
 | 
				
			||||||
        {currTab === CurrTab.BasicTree ? (
 | 
					        {currTab === CurrTab.BasicTree ? (
 | 
				
			||||||
          <BasicFamilyTree tree={tree!} depth={currDepth} />
 | 
					          <BasicFamilyTree tree={tree!} depth={currDepth} />
 | 
				
			||||||
 | 
					        ) : currTab === CurrTab.SimpleTree ? (
 | 
				
			||||||
 | 
					          <SimpleFamilyTree
 | 
				
			||||||
 | 
					            tree={tree!}
 | 
				
			||||||
 | 
					            isUp={currMode === TreeMode.Ascending}
 | 
				
			||||||
 | 
					            depth={currDepth}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          <ComplexFamilyTree
 | 
					          <ComplexFamilyTree
 | 
				
			||||||
            tree={tree!}
 | 
					            tree={tree!}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										198
									
								
								geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
				
			|||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { Couple } from "../../api/CoupleApi";
 | 
				
			||||||
 | 
					import { Member, fmtDate } from "../../api/MemberApi";
 | 
				
			||||||
 | 
					import { FamilyTreeNode } from "../../utils/family_tree";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FACE_WIDTH = 60;
 | 
				
			||||||
 | 
					const FACE_HEIGHT = 70;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CARD_WIDTH = 90;
 | 
				
			||||||
 | 
					const CARD_HEIGHT = 80;
 | 
				
			||||||
 | 
					const SPOUSE_SPACING = 10;
 | 
				
			||||||
 | 
					const CARD_SPACING = 20;
 | 
				
			||||||
 | 
					const SIBLINGS_SPACING = 20;
 | 
				
			||||||
 | 
					const LEVEL_SPACING = 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const COUPLE_CARDS_WIDTH = 2 * CARD_WIDTH + SPOUSE_SPACING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SimpleTreeSpouseInfo {
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					  couple: Couple;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SimpleTreeNode {
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					  spouse?: SimpleTreeSpouseInfo;
 | 
				
			||||||
 | 
					  down: SimpleTreeNode[];
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function buildSimpleDownTreeNode(
 | 
				
			||||||
 | 
					  tree: FamilyTreeNode,
 | 
				
			||||||
 | 
					  depth: number
 | 
				
			||||||
 | 
					): SimpleTreeNode {
 | 
				
			||||||
 | 
					  if (depth === 0) throw new Error("Too much recursion reached!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const lastCouple = tree.couples?.[tree.couples?.length ?? 0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Preprocess children
 | 
				
			||||||
 | 
					  let childrenToProcess = tree.down;
 | 
				
			||||||
 | 
					  if (depth > 1)
 | 
				
			||||||
 | 
					    tree.couples?.forEach(
 | 
				
			||||||
 | 
					      (c) => (childrenToProcess = childrenToProcess?.concat(c.down))
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  else childrenToProcess = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const node = {
 | 
				
			||||||
 | 
					    down:
 | 
				
			||||||
 | 
					      childrenToProcess?.map((c) => buildSimpleDownTreeNode(c, depth - 1)) ??
 | 
				
			||||||
 | 
					      [],
 | 
				
			||||||
 | 
					    member: tree.member,
 | 
				
			||||||
 | 
					    spouse: lastCouple
 | 
				
			||||||
 | 
					      ? {
 | 
				
			||||||
 | 
					          couple: lastCouple.couple,
 | 
				
			||||||
 | 
					          member: lastCouple.member,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      : undefined,
 | 
				
			||||||
 | 
					    width: -1,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Compute current level width
 | 
				
			||||||
 | 
					  let levelWidth =
 | 
				
			||||||
 | 
					    (node.spouse ? COUPLE_CARDS_WIDTH : CARD_WIDTH) + CARD_SPACING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Compute down level width
 | 
				
			||||||
 | 
					  const downWidth =
 | 
				
			||||||
 | 
					    SIBLINGS_SPACING * node.down.length +
 | 
				
			||||||
 | 
					    node.down.reduce((prev, n) => prev + n.width, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  node.width = Math.max(levelWidth, downWidth);
 | 
				
			||||||
 | 
					  return node;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO : if useless, remove and merge up and down techniques
 | 
				
			||||||
 | 
					function buildSimpleUpTreeNode(
 | 
				
			||||||
 | 
					  tree: FamilyTreeNode,
 | 
				
			||||||
 | 
					  depth: number
 | 
				
			||||||
 | 
					): SimpleTreeNode {
 | 
				
			||||||
 | 
					  /*if (depth === 0) throw new Error("Too much recursion reached!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    member: tree.member,
 | 
				
			||||||
 | 
					    children:
 | 
				
			||||||
 | 
					      depth === 1
 | 
				
			||||||
 | 
					        ? []
 | 
				
			||||||
 | 
					        : tree.down?.map((c) => buildSimpleUpTreeNode(c, depth - 1)) ?? [],
 | 
				
			||||||
 | 
					  };*/
 | 
				
			||||||
 | 
					  return buildSimpleDownTreeNode(tree, depth);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple family tree
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Only one couple can be shown in this version
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SimpleFamilyTree(p: {
 | 
				
			||||||
 | 
					  tree: FamilyTreeNode;
 | 
				
			||||||
 | 
					  isUp: boolean;
 | 
				
			||||||
 | 
					  depth: number;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const tree = React.useMemo(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      p.isUp
 | 
				
			||||||
 | 
					        ? buildSimpleUpTreeNode(p.tree, p.depth)
 | 
				
			||||||
 | 
					        : buildSimpleDownTreeNode(p.tree, p.depth),
 | 
				
			||||||
 | 
					    [p.tree, p.isUp, p.depth]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const height = p.depth * (CARD_HEIGHT + LEVEL_SPACING) - LEVEL_SPACING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.info(`tree width=${tree.width} height=${height}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <svg width={tree.width} height={height} xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					      <NodeArea node={tree} x={0} y={0} />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function NodeArea(p: {
 | 
				
			||||||
 | 
					  x: number;
 | 
				
			||||||
 | 
					  y: number;
 | 
				
			||||||
 | 
					  node: SimpleTreeNode;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const couple_x_offset = Math.floor(
 | 
				
			||||||
 | 
					    (p.node.width - (p.node.spouse ? COUPLE_CARDS_WIDTH : CARD_WIDTH)) / 2
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <MemberCard x={couple_x_offset} y={p.y} member={p.node.member} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {p.node.spouse && (
 | 
				
			||||||
 | 
					        <MemberCard
 | 
				
			||||||
 | 
					          x={couple_x_offset + CARD_WIDTH + SPOUSE_SPACING}
 | 
				
			||||||
 | 
					          y={p.y}
 | 
				
			||||||
 | 
					          member={p.node.spouse.member}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function MemberCard(p: {
 | 
				
			||||||
 | 
					  x: number;
 | 
				
			||||||
 | 
					  y: number;
 | 
				
			||||||
 | 
					  member: Member;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  let birthDeath = [];
 | 
				
			||||||
 | 
					  if (p.member.dateOfBirth) birthDeath.push(fmtDate(p.member.dateOfBirth));
 | 
				
			||||||
 | 
					  if (p.member.dateOfDeath) birthDeath.push(fmtDate(p.member.dateOfDeath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <g transform={`translate(${p.x} ${p.y})`}>
 | 
				
			||||||
 | 
					      {/* Member image */}
 | 
				
			||||||
 | 
					      {p.member.hasPhoto ? (
 | 
				
			||||||
 | 
					        <image
 | 
				
			||||||
 | 
					          href={p.member.thumbnailURL!}
 | 
				
			||||||
 | 
					          height={FACE_HEIGHT}
 | 
				
			||||||
 | 
					          width={FACE_WIDTH}
 | 
				
			||||||
 | 
					          preserveAspectRatio="xMidYMin slice"
 | 
				
			||||||
 | 
					        ></image>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <GenderlessIcon width={FACE_WIDTH} height={FACE_HEIGHT} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Member text */}
 | 
				
			||||||
 | 
					      <text>
 | 
				
			||||||
 | 
					        <tspan x="0" dy="14">
 | 
				
			||||||
 | 
					          {p.member.fullName}
 | 
				
			||||||
 | 
					        </tspan>
 | 
				
			||||||
 | 
					        <tspan x="0" dy="14" font-size="10">
 | 
				
			||||||
 | 
					          {birthDeath.join(" - ")}
 | 
				
			||||||
 | 
					        </tspan>
 | 
				
			||||||
 | 
					      </text>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function GenderlessIcon(p: {
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <g>
 | 
				
			||||||
 | 
					      <rect height={p.height} width={p.width} fill="rgb(59, 85, 96)" />
 | 
				
			||||||
 | 
					      <g transform={`scale(${p.width * 0.001616})`}>
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					          transform="translate(50,40)"
 | 
				
			||||||
 | 
					          fill="lightgrey"
 | 
				
			||||||
 | 
					          d="M256 288c79.5 0 144-64.5 144-144S335.5 0 256 0 112 
 | 
				
			||||||
 | 
					          64.5 112 144s64.5 144 144 144zm128 32h-55.1c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16H128C57.3 320 0 377.3 
 | 
				
			||||||
 | 
					          0 448v16c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48v-16c0-70.7-57.3-128-128-128z"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user