tva
← Insights

家系図の可視化のための ReactFlow

A family tree is a graph. The data is a set of nodes — people — connected by typed edges: parent-child, spouse, sibling. The visualisation problem is how to lay that graph out intelligibly at different scales, from a three-generation view to a deep lineage spanning many generations and hundreds of individuals. ReactFlow provides the rendering and interaction infrastructure. The interesting decisions are in the custom node types, the Dagre layout algorithm, the edge styling for relationship types, and the performance strategies that become necessary as trees grow.

This post covers the implementation decisions we made, the trade-offs involved, and the places where ReactFlow’s abstractions both helped and constrained what we could build.


なぜ ReactFlow か

The alternative we considered seriously was D3. D3’s tree layout algorithms are mature, well-documented, and have been used for genealogy visualisation for years. The problem is that D3 requires managing the DOM directly, which conflicts with React’s reconciliation model if you want React components for the interactive elements. Hybrid approaches work but are awkward to maintain.

ReactFlow keeps everything in React’s component model. Nodes and edges are plain JavaScript objects in state. The library handles SVG rendering, pan and zoom, and interaction events. Custom node types are just React components. This makes it straightforward to embed rich interactive content in each node — photo placeholders, expandable detail sections, action buttons — without stepping outside React’s model.

The trade-off is that ReactFlow is more opinionated than D3. The node and edge data model has a specific shape, the layout is handled externally (ReactFlow does not include a layout algorithm), and performance at very large node counts requires more explicit management than a pure D3 approach. For a project in the few-hundred-node range, these trade-offs are favourable.

カスタムノードタイプ:人物カード

ReactFlow’s custom node system is one of its strongest features. A custom node type is a React component that receives the node’s data as props and renders whatever you want. The only requirement is that it renders the Handle components at the connection points where edges attach.

The PersonNode component renders a card with the person’s name, birth year, death year (if applicable), and a photo placeholder that shows the person’s initials when no photo is available. The card has a subtle border that changes colour based on generation depth — a visual cue that helps orient the viewer in a deep tree. Living people and deceased people use distinct visual treatments.

One design decision that significantly improved clarity: the node has two distinct handle positions on the top and bottom, not the default four sides. Parent-child edges connect bottom-to-top, so the tree flows vertically with a consistent direction. Spouse edges connect side-to-side at the same generation level. This separation of edge types by handle position makes the directionality of relationships immediately readable.

The node component also handles the “collapsed” state for deep subtrees. When a subtree is collapsed, the node renders a badge showing the number of hidden descendants. Clicking the badge expands the subtree. This interaction model is essential for making large trees usable — a flat view of four hundred nodes is unreadable, but a view where most of the tree is collapsed and you expand branches on demand is navigable.

階層配置のための Dagre レイアウト

ReactFlow does not calculate node positions. You provide positions as part of the node data, and the library renders them where you tell it to. For a family tree, calculating those positions manually would require reimplementing a hierarchical layout algorithm. The standard choice is Dagre, a directed acyclic graph layout library that produces clean, minimally-crossing hierarchical layouts.

The Dagre integration sits outside the ReactFlow render cycle. When the node or edge data changes, a layout function runs synchronously, assigns positions to each node, and returns the positioned node array that ReactFlow renders. The function takes the raw nodes and edges, constructs a Dagre graph, runs the layout, and reads back the computed x and y values.

import dagre from '@dagrejs/dagre';

function layoutTree(nodes, edges, nodeWidth, nodeHeight) {
  const g = new dagre.graphlib.Graph();
  g.setDefaultEdgeLabel(() => ({}));
  g.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 40 });

  nodes.forEach(node => {
    g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  });
  edges.forEach(edge => {
    g.setEdge(edge.source, edge.target);
  });

  dagre.layout(g);

  return nodes.map(node => {
    const { x, y } = g.node(node.id);
    return { ...node, position: { x: x - nodeWidth / 2, y: y - nodeHeight / 2 } };
  });
}

Dagre’s rankdir parameter controls the layout direction. “TB” (top-to-bottom) is the natural choice for a family tree where time flows downward across generations. ranksep controls vertical spacing between generations; nodesep controls horizontal spacing between siblings. These values need tuning for the specific node sizes you are using.

The limitation of Dagre for family trees is that it treats the graph as a DAG. Spouse edges — which connect two nodes at the same rank and are undirected — are not natural DAG edges. The workaround is to run the Dagre layout using only parent-child edges to establish generation ranks and horizontal positions, then draw spouse edges separately as straight lines at the same y coordinate. This produces the correct visual result: spouses appear side by side at the same level with a horizontal edge between them.

関係タイプのためのエッジスタイリング

ReactFlow supports custom edge components in the same way as custom node components. Each relationship type — parent-child, spouse, adoptive, step — uses a distinct edge style. Parent-child edges are solid lines. Spouse edges are thin horizontal connectors. Adoptive relationships use a dashed line. Step relationships use a dotted line.

The edge component receives the source and target node positions as props and renders an SVG path. For straight edges this is trivial. For curved edges between nodes at different positions, ReactFlow provides a set of path helper functions (getStraightPath, getBezierPath, getSmoothStepPath) that generate the correct SVG path string for the given endpoint coordinates.

One interaction the custom edge components enable is an edge label that appears on hover, showing the relationship description in full. This is useful for complex family structures where the visual style alone may be ambiguous — hovering an edge between two nodes shows “Adoptive parent — 1987” or “Married 1975, divorced 1989.” The label renders as a foreignObject in the SVG, which allows using standard HTML and CSS for the tooltip style.

大規模な系譜でのパフォーマンス

ReactFlow handles moderate-sized trees — up to a few hundred nodes — without specific optimisation. Beyond that, a few strategies become necessary.

ReactFlow includes a built-in virtualisation renderer that only renders nodes within the current viewport. This is enabled by default and handles most of the performance concern for large trees, provided the node components themselves are not expensive to render. A PersonNode that triggers external data fetches or heavy computations on every render will cause jank regardless of viewport culling. Keep node components pure and fast.

The second strategy is memoising the nodes and edges arrays. Because ReactFlow re-renders whenever the node or edge arrays change by reference, passing newly-constructed arrays on every parent render causes unnecessary work. Wrapping the layout computation in useMemo with a dependency on the raw data — not on derived state — ensures the expensive layout computation and the associated re-render only happen when the underlying data actually changes.

The third strategy is the collapse/expand interaction described in the node type section. A tree with 400 nodes that displays only 40 at any given time performs identically to a 40-node tree. The collapse state is managed in React state at the visualisation level, not in the underlying data model. Collapsing a subtree filters it from the nodes and edges arrays passed to ReactFlow; expanding it adds it back. The Dagre layout re-runs with the filtered set.

インタラクションデザインの考慮事項

Beyond the layout and rendering mechanics, the interaction model for a family tree visualisation requires careful thought. Pan and zoom are provided by ReactFlow out of the box, but the initial view and zoom level matter significantly for usability. Calling fitView() after the layout renders ensures the tree is visible and appropriately scaled on load.

Navigation between individuals — clicking a person to centre them in the viewport, following edges to adjacent generations — requires the ReactFlow instance methods setCenter() and fitBounds(). These are accessed via the useReactFlow() hook and should be called with smooth animation enabled to give the user a sense of spatial continuity.

Search and filter — finding a specific person by name and jumping to their position in the tree — is a common requirement for any non-trivial family tree. The implementation highlight-pulses the matched node after centering the viewport on it. This is done by adding a CSS animation class to the matching node for a short duration, then removing it. The ReactFlow updateNode method handles this without triggering a full tree re-layout.

関連インサイト

関連記事