Skip to content

FamilyTreeGraph レイアウトアルゴリズム

概要

FamilyTreeGraph は、従業員の家族構成を世代ベースの階層グラフとして可視化するコンポーネントである。

  • 目的: 年末調整(年始調整)における扶養親族・配偶者の関係を直感的に把握可能にする
  • 技術: @xyflow/react + カスタムレイアウトフック
  • ドメイン知識: → 親族の基礎概念

設計原則:

  • 本人(従業員)を中心に配置し、世代を上下に展開
  • 現配偶者は右側、前配偶者は左側
  • 子は婚姻ノード直下にセンタリング
  • 直系親族は子孫の上に、傍系親族は右端に配置

データフロー

ドメインデータグラフ構築レイアウト計算描画"FamilyMemberData["FamilyRelationshipData[buildGraphElementscomputeGenerationsresolveRelationshipuseFamilyLayout7フェーズX/Y 座標決定ReactFlow配置済みノードエッジ
ドメインデータグラフ構築レイアウト計算描画"FamilyMemberData["FamilyRelationshipData[buildGraphElementscomputeGenerationsresolveRelationshipuseFamilyLayout7フェーズX/Y 座標決定ReactFlow配置済みノードエッジ

処理の流れ

  1. ドメインデータ入力: FamilyMemberData[]FamilyRelationshipData[] を受け取る
  2. グラフ構築 (buildGraphElements): ドメインデータからノード・エッジを生成(位置は未定)
  3. レイアウト計算 (useFamilyLayout): 7フェーズで全ノードの X/Y 座標を決定
  4. 描画 (ReactFlow): 配置済みノード・エッジを表示

世代計算 (computeGenerations)

BFS(幅優先探索)で本人からの相対的な世代番号を計算する。

デルタルール

関係タイプデルタ説明
spouse0配偶者は同世代
blood / affinal (from→to)+1from=親, to=子
blood / affinal (to→from)-1逆方向(子→親)

世代番号の例

世代 -1世代 0世代 +1blood (delta=-1)blood (delta=-1)blood (delta=+1)blood (delta=+1)spouse (delta=0)父 (gen=-1)母 (gen=-1)本人 (gen=0)配偶者 (gen=0)子1 (gen=+1)子2 (gen=+1)
世代 -1世代 0世代 +1blood (delta=-1)blood (delta=-1)blood (delta=+1)blood (delta=+1)spouse (delta=0)父 (gen=-1)母 (gen=-1)本人 (gen=0)配偶者 (gen=0)子1 (gen=+1)子2 (gen=+1)

アルゴリズム

1. 隣接リストを構築: memberId → [{ neighbor, delta }]
2. BFS キュー初期化: employee を generation=0 で追加
3. キューから取り出し、未訪問の隣接ノードに currentGen + delta を割り当て
4. 全メンバーの世代が確定するまで繰り返し

続柄解決 (resolveRelationship)

各家族メンバーの「続柄」(本人から見た関係名称)を解決する。

2パスアプローチ

Pass 1: 本人直接

  • 本人が from または to である関係から、相手側メンバーに relationship を割り当て

Pass 2: 伝播(繰り返し)

  • 本人を含まない関係について、片方が解決済み・片方が未解決なら、未解決側に relationship を割り当て
  • 変更がなくなるまで繰り返し(多段階チェーン対応: 例 祖父母→親→本人)

規約

各関係の relationship本人視点で遠い方の親族を記述する:

  • 本人→配偶者 (relationship='妻') → 配偶者に「妻」
  • 父→本人 (relationship='父') → 父に「父」
  • 配偶者→連れ子 (relationship='連れ子') → 連れ子に「連れ子」

グラフ構築 (buildGraphElements)

ドメインデータから ReactFlow のノードとエッジを生成する。

メンバーノード

プロパティ
type'familyMember'
width200px
heightcomputeMemberHeight(badgeCount) — バッジなし: 64px, バッジあり: 96px
data{ member, age, relationship, seikeiWoItsuNiSuru, generation }

婚姻ノード

プロパティ
type'marriage'
width / height1px × 1px(不可視コネクタ)
data{ isFormer, side, generation }

生成ルール:

  • 本人の各配偶者関係ごとに1つの婚姻ノード
  • 本人以外の夫婦関係(祖父母等)にも婚姻ノードを生成

エッジ生成

ハンドル配置親子エッジ片親エッジ前配偶者(left-out → right-in)婚姻ノード本人(right → left)婚姻ノード現配偶者(left → right)婚姻ノード (bottom)子A (top)子B (top)親 (bottom)子 (top)
ハンドル配置親子エッジ片親エッジ前配偶者(left-out → right-in)婚姻ノード本人(right → left)婚姻ノード現配偶者(left → right)婚姻ノード (bottom)子A (top)子B (top)親 (bottom)子 (top)

ハンドル配置

ハンドル用途
right / left現配偶者との水平接続
left-out / right-in前配偶者との水平接続
bottom親側から子への垂直接続
top子側から親への垂直接続

親子エッジのルーティング

findParentMarriageNode() により、親子関係を婚姻ノード経由でルーティング:

  • 親が婚姻関係にある場合: 婚姻ノードの bottom → 子の top
  • 片親の場合: 親の bottom → 子の top(婚姻ノードなし)

レイアウトアルゴリズム (useFamilyLayout) — 7フェーズ

useFamilyLayout フックは useMemo 内で applyFamilyLayout を呼び出し、全ノードに X/Y 座標を割り当てる。

Phase 1世代読み取りPhase 2婚姻グループ構築Phase 3世代グルーピングPhase 4Gen-0 X配置Phase 5Gen+N X配置Phase 6Gen-N X配置Phase 7Y座標割り当て
Phase 1世代読み取りPhase 2婚姻グループ構築Phase 3世代グルーピングPhase 4Gen-0 X配置Phase 5Gen+N X配置Phase 6Gen-N X配置Phase 7Y座標割り当て

Phase 1: 世代読み取り

各メンバーノードの data.generation から世代番号を取得する。世代計算は buildGraphElements 内の computeGenerations で完了済み。

Phase 2: 婚姻グループ構築

婚姻ノードごとに MarriageGroup を構築する。

typescript
interface MarriageGroup {
  marriage: Node      // 婚姻コネクタノード
  employee: Node | null  // 本人(gen-0婚姻の場合)
  spouse: Node | null    // 配偶者
  children: Node[]       // 子ノード(dateOfBirth 昇順ソート)
  side: 'right' | 'left' // 配偶者の配置方向
  generation: number     // 婚姻の世代
  subtreeWidth: number   // サブツリー全体幅
}

サブツリー幅の計算:

coupleWidth    = memberW + coupleGap + marriageW + coupleGap + memberW
               = 200 + 40 + 1 + 40 + 200 = 481px

childrenWidth  = N × memberW + (N-1) × nodesep
               = N × 200 + (N-1) × 60

subtreeWidth   = max(coupleWidth, childrenWidth)

追加収集:

  • directChildren: 婚姻ノードを経由せず本人に直接接続された子(片親ケース)
  • individualParents: 婚姻グループに属さない親(片親ケース)

Phase 3: 世代グルーピング

全メンバーノードから存在する世代番号を収集し、昇順ソートする。 gen-0 の婚姻グループを right(現配偶者)と left(前配偶者)に分類する。

Phase 4: Gen 0(本人行)X配置

本人を原点 x=0 に配置し、婚姻グループを左右に展開する。

← 左方向(前配偶者)                    右方向(現配偶者) →

[前配偶者] [gap] [婚姻] [gap] [本人] [gap] [婚姻] [gap] [配偶者]
                                  x=0

右方向展開 (現配偶者):

rightX = memberW + coupleGap  (= 240)
for each gen0Right group:
  marriage.x = rightX
  spouse.x   = rightX + marriageW + coupleGap
  rightX    += spouseBlockW + extraForChildren + nodesep

左方向展開 (前配偶者):

leftX = -(coupleGap + marriageW + coupleGap + memberW)
for each gen0Left group:
  spouse.x   = leftX
  marriage.x = leftX + memberW + coupleGap
  leftX     -= spouseBlockW + extraForChildren + nodesep

extraForChildren: 子のサブツリーがカップル幅を超える場合の追加スペース。

Phase 5: Gen +N(子行)X配置

各 gen-0 婚姻グループの子を、婚姻ノード中心にセンタリング配置する。

         [婚姻]
           |
    +----- + -----+
    |      |      |
  [子1]  [子2]  [子3]
  ←--- centered ---→
marriageCenterX = marriage.x + marriageW / 2
totalChildW     = N × memberW + (N-1) × nodesep
childX          = marriageCenterX - totalChildW / 2
for each child:
  child.x  = childX
  childX  += memberW + nodesep

片親の直接子(directChildren)は本人の中心にセンタリング。

Phase 5b: 子行オーバーラップ解消

左側婚姻グループと右側婚姻グループの子がオーバーラップする場合、ペアワイズシフトで解消する。

手順:

  1. 左婚姻グループ → 直接子 → 右婚姻グループ の順で ChildGroupBounds を構築
  2. 中心X でソート
  3. 隣接ペアのオーバーラップを検出: overlap = left.maxX + nodesep - right.minX
  4. オーバーラップ量の半分ずつ左右に押し出し(関連する婚姻・配偶者ノードも一緒にシフト)

Phase 6: Gen -N(親行)X配置

直系親族の BFS 識別

本人と gen-0 配偶者から BFS で上方探索し、直系(本人の親・義親・祖父母等)の婚姻グループを特定する。

traceQueue = [employee, ...gen0Spouses]
for each memberId in queue:
  if a parentMarriageGroup has this member as child:
    mark as directLineage
    add the spouse pair to queue (for higher generations)

アンカーベースセンタリング

世代の近い方(-1)から順に処理。各直系親婚姻グループを、そのアンカー子孫(配置済みの子)の真上にセンタリングする。

         [親1] [婚姻] [親2]
         ←--- centered ---→
                 |
              [アンカー子]  ← 配置済み (Phase 4/6 で確定)
anchorCenterX = anchorChild.x + memberW / 2
startX        = anchorCenterX - coupleW / 2
spouse1.x     = startX
marriage.x    = startX + memberW + coupleGap
spouse2.x     = startX + memberW + coupleGap + marriageW + coupleGap

同世代オーバーラップ解消

同じ世代に複数の直系親婚姻がある場合(例: 本人の両親と配偶者の両親)、左から右へ走査してオーバーラップを右方向にシフトして解消する。

片親配置

婚姻グループに属さない個人の親は、本人中心にセンタリングして配置する(直系親婚姻がない場合のみ)。

Phase 6b: 傍系親族配置

直系配置で位置が決まらなかったメンバーを右端に順次配置する。

婚姻グループ単位の配置:

for each remaining marriage group:
  rightEdge = max(positioned members in same generation).maxX
  spouse1.x = rightEdge + nodesep
  marriage.x = spouse1.x + memberW + coupleGap
  spouse2.x = marriage.x + marriageW + coupleGap
  center children under marriage

個人メンバーの配置:

for each generation:
  for each unpositioned member (sorted by dateOfBirth):
    member.x = rightEdge + nodesep
    rightEdge = member.x + memberW

Phase 7: Y座標割り当て

全ノードの Y 座標を世代行の累積高さとして計算する。

手順:

  1. 各世代行の最大ノード高さを計算
  2. 世代を昇順(上から下)で走査し、累積 Y を計算
  3. メンバーノード: y = genY[generation]
  4. 婚姻ノード: y = genY[generation] + (rowHeight - marriageH) / 2(行内で垂直中央揃え)
cumulativeY = 0
for each generation (ascending):
  genY[gen] = cumulativeY
  cumulativeY += maxHeight[gen] + ranksep(80px)

定数

定数用途
NODE_DIMENSIONS.member.width200pxメンバーカード幅
NODE_DIMENSIONS.member.baseHeight64pxバッジなしメンバー高さ
NODE_DIMENSIONS.member.badgeSectionHeight32pxバッジあり追加高さ
NODE_DIMENSIONS.marriage.width1px婚姻コネクタ幅
NODE_DIMENSIONS.marriage.height1px婚姻コネクタ高さ
LAYOUT.nodesep60px兄弟間隔
LAYOUT.ranksep80px世代間隔
LAYOUT.coupleGap40px配偶者-婚姻ノード間隔

対応パターン (Storybook ストーリー)

ストーリーパターン世代
NuclearFamily核家族(本人+配偶者+子2人)0, +1
Remarriage再婚(現配偶者+前配偶者+連れ子+実子)0, +1
ThreeGenerations3世代(祖父母+両親+子)-1, 0, +1
FourGenerations4世代(曾祖父母+祖父母+両親+子)-2, -1, 0, +1
SingleParent片親(母のみ+本人+配偶者+子)-1, 0, +1
NonResidentFamily非居住者バッジ付き家族-1, 0, +1
EmployeeOnly本人のみ0
UncleAunt伯叔父母(親の兄弟)-1, 0
NephewNiece甥姪(本人の兄弟の子)-1, 0, +1
InLawParents義父母(配偶者の親)-1, 0
BothSidesParents両家の親(本人側+配偶者側)-1, 0
LargeFamily多子世帯(子5人)0, +1
SingleGrandparent片祖父母-1, 0, +1

ソースコード

ファイル責務
frontend/src/components/ui/family-tree/family-tree-graph.utils.tscomputeGenerations, buildGraphElements, resolveRelationship, getMemberBadges
frontend/src/components/ui/family-tree/use-family-layout.tsuseFamilyLayout — 7フェーズレイアウト
frontend/src/components/ui/family-tree/family-tree-graph.constants.ts定数定義 (NODE_DIMENSIONS, LAYOUT, BADGE_COLORS)
frontend/src/components/ui/family-tree/family-tree-graph.types.tsドメイン型 (FamilyMemberData, FamilyRelationshipData)
frontend/src/components/ui/family-tree/family-tree-graph.stories.tsxStorybook ストーリー(全パターン)