Add simple tree graph mode #4

Merged
pierre merged 16 commits from simple_tree into master 2023-08-26 14:00:00 +00:00
2 changed files with 75 additions and 21 deletions
Showing only changes of commit ed5bd63bf6 - Show all commits

View File

@ -112,7 +112,7 @@ export class Member implements MemberDataApi {
return firstName.length === 0 return firstName.length === 0
? this.last_name ?? "" ? this.last_name ?? ""
: `${firstName} ${this.last_name ?? ""}`; : `${firstName} ${this.last_name?.toUpperCase() ?? ""}`;
} }
get invertedFullName(): string { get invertedFullName(): string {
@ -159,6 +159,13 @@ export class Member implements MemberDataApi {
}; };
} }
get displayBirthDeath(): string {
let birthDeath = [];
if (this.dateOfBirth) birthDeath.push(fmtDate(this.dateOfBirth));
if (this.dateOfDeath) birthDeath.push(fmtDate(this.dateOfDeath));
return birthDeath.join(" - ");
}
get hasContactInfo(): boolean { get hasContactInfo(): boolean {
return this.email || return this.email ||
this.phone || this.phone ||

View File

@ -1,19 +1,19 @@
import React from "react"; import React from "react";
import { Couple } from "../../api/CoupleApi"; import { Couple } from "../../api/CoupleApi";
import { Member, fmtDate } from "../../api/MemberApi"; import { Member } from "../../api/MemberApi";
import { FamilyTreeNode } from "../../utils/family_tree"; import { FamilyTreeNode } from "../../utils/family_tree";
const FACE_WIDTH = 60; const FACE_WIDTH = 60;
const FACE_HEIGHT = 70; const FACE_HEIGHT = 70;
const CARD_WIDTH = 90; const CARD_HEIGHT = 110;
const CARD_HEIGHT = 80;
const SPOUSE_SPACING = 10; const SPOUSE_SPACING = 10;
const CARD_SPACING = 20; const CARD_SPACING = 20;
const SIBLINGS_SPACING = 20; const SIBLINGS_SPACING = 20;
const LEVEL_SPACING = 20; const LEVEL_SPACING = 20;
const COUPLE_CARDS_WIDTH = 2 * CARD_WIDTH + SPOUSE_SPACING; const NAME_CHAR_W = 7.1428;
const BIRTH_CHAR_W = 5;
interface SimpleTreeSpouseInfo { interface SimpleTreeSpouseInfo {
member: Member; member: Member;
@ -27,6 +27,19 @@ interface SimpleTreeNode {
width: number; width: number;
} }
/**
* Get the width of a member card
*/
function memberCardWidth(m: Member): number {
const nameWidth = m.fullName.length * NAME_CHAR_W;
const birthDeathWidth = m.displayBirthDeath.length * BIRTH_CHAR_W;
return Math.max(FACE_WIDTH, nameWidth, birthDeathWidth);
}
function center(container_width: number, el_width: number): number {
return Math.floor((container_width - el_width) / 2);
}
function buildSimpleDownTreeNode( function buildSimpleDownTreeNode(
tree: FamilyTreeNode, tree: FamilyTreeNode,
depth: number depth: number
@ -59,7 +72,9 @@ function buildSimpleDownTreeNode(
// Compute current level width // Compute current level width
let levelWidth = let levelWidth =
(node.spouse ? COUPLE_CARDS_WIDTH : CARD_WIDTH) + CARD_SPACING; memberCardWidth(node.member) +
(node.spouse ? SPOUSE_SPACING + memberCardWidth(node.spouse.member) : 0) +
CARD_SPACING;
// Compute down level width // Compute down level width
const downWidth = const downWidth =
@ -122,21 +137,42 @@ function NodeArea(p: {
y: number; y: number;
node: SimpleTreeNode; node: SimpleTreeNode;
}): React.ReactElement { }): React.ReactElement {
const couple_x_offset = Math.floor( const couple_x_offset =
(p.node.width - (p.node.spouse ? COUPLE_CARDS_WIDTH : CARD_WIDTH)) / 2 p.x +
Math.floor(
(p.node.width -
(memberCardWidth(p.node.member) +
(p.node.spouse
? SPOUSE_SPACING + memberCardWidth(p.node.spouse.member)
: 0))) /
2
); );
let downXOffset = p.x;
return ( return (
<> <>
<MemberCard x={couple_x_offset} y={p.y} member={p.node.member} /> <MemberCard x={couple_x_offset} y={p.y} member={p.node.member} />
{p.node.spouse && ( {p.node.spouse && (
<MemberCard <MemberCard
x={couple_x_offset + CARD_WIDTH + SPOUSE_SPACING} x={couple_x_offset + memberCardWidth(p.node.member) + SPOUSE_SPACING}
y={p.y} y={p.y}
member={p.node.spouse.member} member={p.node.spouse.member}
/> />
)} )}
{p.node.down.map((n) => {
const el = (
<NodeArea
x={downXOffset}
y={p.y + CARD_HEIGHT + LEVEL_SPACING}
node={n}
/>
);
downXOffset += n.width;
return el;
})}
</> </>
); );
} }
@ -146,31 +182,41 @@ function MemberCard(p: {
y: number; y: number;
member: Member; member: Member;
}): React.ReactElement { }): React.ReactElement {
let birthDeath = []; const w = memberCardWidth(p.member);
if (p.member.dateOfBirth) birthDeath.push(fmtDate(p.member.dateOfBirth));
if (p.member.dateOfDeath) birthDeath.push(fmtDate(p.member.dateOfDeath));
return ( return (
<g transform={`translate(${p.x} ${p.y})`}> <g transform={`translate(${p.x} ${p.y})`} width={w} height={CARD_HEIGHT}>
{/* Member image */} {/* Member image */}
{p.member.hasPhoto ? ( {p.member.hasPhoto ? (
<image <image
x={center(w, FACE_WIDTH)}
href={p.member.thumbnailURL!} href={p.member.thumbnailURL!}
height={FACE_HEIGHT} height={FACE_HEIGHT}
width={FACE_WIDTH} width={FACE_WIDTH}
preserveAspectRatio="xMidYMin slice" preserveAspectRatio="xMidYMin slice"
></image> ></image>
) : ( ) : (
<GenderlessIcon width={FACE_WIDTH} height={FACE_HEIGHT} /> <GenderlessIcon
x={center(w, FACE_WIDTH)}
width={FACE_WIDTH}
height={FACE_HEIGHT}
/>
)} )}
{/* Member text */} {/* Member text */}
<text> <text y={FACE_HEIGHT}>
<tspan x="0" dy="14"> <tspan
x={center(w, p.member.fullName.length * NAME_CHAR_W)}
dy="14"
font-size="13"
>
{p.member.fullName} {p.member.fullName}
</tspan> </tspan>
<tspan x="0" dy="14" font-size="10"> <tspan
{birthDeath.join(" - ")} x={center(w, p.member.displayBirthDeath.length * BIRTH_CHAR_W)}
dy="14"
font-size="10"
>
{p.member.displayBirthDeath}
</tspan> </tspan>
</text> </text>
</g> </g>
@ -178,11 +224,12 @@ function MemberCard(p: {
} }
function GenderlessIcon(p: { function GenderlessIcon(p: {
x?: number;
width: number; width: number;
height: number; height: number;
}): React.ReactElement { }): React.ReactElement {
return ( return (
<g> <g transform={`translate(${p.x ?? 0} 0)`}>
<rect height={p.height} width={p.width} fill="rgb(59, 85, 96)" /> <rect height={p.height} width={p.width} fill="rgb(59, 85, 96)" />
<g transform={`scale(${p.width * 0.001616})`}> <g transform={`scale(${p.width * 0.001616})`}>
<path <path