Start to build simple tree
This commit is contained in:
parent
635fb667e1
commit
cb79a2755e
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user