Add simple tree graph mode #4
@ -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 ||
|
||||||
|
@ -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,9 +137,18 @@ 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 (
|
||||||
<>
|
<>
|
||||||
@ -132,11 +156,23 @@ function NodeArea(p: {
|
|||||||
|
|
||||||
{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
|
||||||
|
Loading…
Reference in New Issue
Block a user