Can customize shown depth
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Pierre HUBERT 2023-08-26 08:47:02 +02:00
parent d3b1028fe4
commit 635fb667e1
4 changed files with 157 additions and 72 deletions

View File

@ -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>

View File

@ -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));
}

View File

@ -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>
); );
} }

View File

@ -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()),
}, },
}); });