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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user