Interactive ancestor tree React component built with ReactFlow, featuring expandable generations and customizable UI controls
npm install @ryandymock/ancestor-treeA React library for displaying interactive ancestor trees using ReactFlow.
- Interactive Tree Visualization: Display family trees with nodes for individuals and couples
- Expandable Generations: Click to expand and explore deeper generations
- Customizable Callbacks: Handle clicks on people, couples, and tree interactions
- UI Controls: Show/hide zoom controls, mini-map, background, and more
- TypeScript Support: Fully typed for better development experience
``bash`
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material reactflow
`tsx
import AncestorTree, {
AncestorTreeCallbacks,
AncestorTreeUIControls
} from './components/AncestorTree';
import { Person, PeopleIndex } from './types/person';
function MyApp() {
// Your people data
const people: PeopleIndex = {
"1": {
id: "1",
name: "John Doe",
birth: "1990-01-01",
spouseId: "4",
parentIds: ["2", "3"]
},
"4": {
id: "4",
name: "Jane Doe",
birth: "1991-02-15",
spouseId: "1",
},
// ... more people
};
// Define callbacks for interactions
const callbacks: AncestorTreeCallbacks = {
onPersonClick: (person: Person) => {
console.log("Person clicked:", person);
// Show person details modal, etc.
},
onCoupleClick: (partner1: Person, partner2: Person) => {
console.log("Couple clicked:", partner1, partner2);
// Show couple details modal, etc.
},
onViewportChange: (x: number, y: number, zoom: number) => {
console.log("Viewport changed:", { x, y, zoom });
},
onTreePan: (x: number, y: number) => {
console.log("Tree panned:", { x, y });
},
onTreeZoom: (zoom: number) => {
console.log("Tree zoomed:", zoom);
},
onCoupleExpansion: (coupleId: string | undefined, isExpanded: boolean) => {
console.log("Couple expansion:", { coupleId, isExpanded });
},
};
// Configure UI controls
const uiControls: AncestorTreeUIControls = {
showControls: true, // Show zoom/fit controls
showMiniMap: false, // Show mini-map
showBackground: true, // Show grid background
enablePan: true, // Allow panning
enableZoom: true, // Allow zooming
enableFitView: true, // Auto-fit on load
backgroundColor: "#fafafa", // Custom background color
};
return (
API Reference
$3
| Prop | Type | Required | Description |
|------|------|----------|-------------|
|
people | PeopleIndex | Yes | Object containing all people data indexed by ID |
| rootId | string | Yes | ID of the root person to start the tree from |
| callbacks | AncestorTreeCallbacks | No | Callback functions for various interactions |
| uiControls | AncestorTreeUIControls | No | UI control configuration |$3
| Callback | Type | Description |
|----------|------|-------------|
|
onPersonClick | (person: Person) => void | Called when a person node is clicked |
| onCoupleClick | (partner1: Person, partner2: Person) => void | Called when a couple node is clicked |
| onTreePan | (x: number, y: number) => void | Called when the tree is panned |
| onTreeZoom | (zoom: number) => void | Called when the tree is zoomed |
| onViewportChange | (x: number, y: number, zoom: number) => void | Called when viewport changes (pan or zoom) |
| onCoupleExpansion | (coupleId: string \| undefined, isExpanded: boolean) => void | Called when a couple is expanded/collapsed |$3
| Property | Type | Default | Description |
|----------|------|---------|-------------|
|
showControls | boolean | true | Show/hide zoom and fit controls |
| showMiniMap | boolean | false | Show/hide the mini map |
| showBackground | boolean | true | Show/hide the grid background |
| enablePan | boolean | true | Enable/disable panning |
| enableZoom | boolean | true | Enable/disable zooming |
| enableFitView | boolean | true | Enable/disable fit view on mount |
| backgroundColor | string | "#fafafa" | Custom background color |
| nodeHeight | number | 120 | Height of nodes (affects vertical spacing calculation) |
| verticalGaps | number[] | [0, 325, 100, 325, 100] | Vertical gaps between nodes for each generation |
| defaultVerticalGap | number | 50 | Default vertical gap when generation not specified in verticalGaps |
| coupleNodeWidth | number | 320 | Width of couple nodes (automatically adjusts column spacing) |
| personNodeWidth | number | 160 | Width of person nodes |
| formatPersonSubtitle | (person: Person) => string | undefined | Custom formatter for person subtitle text |$3
`typescript
interface Person {
id: string;
name: string;
birth?: string;
death?: string;
imageUrl?: string;
spouseId?: string;
parentIds?: [string?, string?]; // [fatherId, motherId]
}
`$3
`typescript
type PeopleIndex = Record;
`Example Use Cases
$3
`tsx
const callbacks = {
onPersonClick: (person) => {
setSelectedPerson(person);
setShowPersonModal(true);
},
onCoupleClick: (partner1, partner2) => {
setSelectedCouple([partner1, partner2]);
setShowCoupleModal(true);
},
};
`$3
`tsx
const callbacks = {
onViewportChange: (x, y, zoom) => {
// Save viewport state for user preferences
localStorage.setItem('treeViewport', JSON.stringify({ x, y, zoom }));
},
onCoupleExpansion: (coupleId, isExpanded) => {
// Track which branches users explore
analytics.track('couple_expansion', { coupleId, isExpanded });
},
};
`$3
`tsx
const uiControls = {
showControls: false,
showMiniMap: false,
showBackground: false,
enablePan: false,
enableZoom: false,
};
`$3
`tsx
// If your MUI theme causes card overlapping, adjust vertical spacing
const uiControls = {
nodeHeight: 140, // Increase if cards are taller due to theme
verticalGaps: [0, 400, 150, 400, 150], // Increase gaps between generations
defaultVerticalGap: 75, // Increase default gap for expanded generations
};
`$3
`tsx
// Adjust node widths to accommodate longer names or more content
const uiControls = {
coupleNodeWidth: 400, // Wider couple cards (default: 320px)
personNodeWidth: 200, // Wider person cards (default: 160px)
// Column spacing automatically adjusts based on width changes
// Each generation shifts by (newWidth - defaultWidth) * generationIndex
};// Example: Making nodes narrower for compact display
const compactControls = {
coupleNodeWidth: 280, // 40px narrower than default
personNodeWidth: 140, // 20px narrower than default
// Generation 1 shifts left by 40px, Generation 2 by 80px, etc.
};
`$3
`tsx
// Customize what information appears in the subtitle for each person
const uiControls = {
formatPersonSubtitle: (person) => {
// Show only birth year and location
const birthYear = person.birth ? person.birth.split('-')[0] : '?';
const location = person.location || 'Unknown';
return Born ${birthYear} • ${location};
// Or show age if still alive
// const age = person.death ? null : new Date().getFullYear() - parseInt(person.birth?.split('-')[0] || '0');
// return age ? Age ${age} : ${person.birth} – ${person.death};
// Or show just the ID for minimal display
// return person.id;
},
};
`$3
The library includes a powerful template-based subtitle formatter with many built-in variables:
`tsx
const uiControls = {
formatPersonSubtitle: createSubtitleFormatter("{birthMMM dd, yyyy} – {deathMMM dd, yyyy}"),
};// Helper function to create template-based formatters
function createSubtitleFormatter(template: string) {
return (person: Person) => {
// Implementation handles all variable replacements
return template
.replace(/{name}/g, person.name || "")
.replace(/{birth\*([^}]+)}/g, (match, format) => formatDate(person.birth, format))
// ... (see full implementation in examples)
};
}
`Available Variables:
-
{name} - Full name
- {firstName} - First name only
- {lastName} - Last name(s) only
- {initials} - First letter of each name part (e.g., "J.D.")
- {birth} - Raw birth date string
- {death} - Raw death date string
- {id} - Person ID
- {birthYear} - Birth year only
- {deathYear} - Death year only
- {age} - Age at death (if deceased)
- {currentAge} - Current age (if alive)
- {lifespan} - Formatted as "1950-2020" or "1950-"
- {isAlive} - "Living" or "Deceased"
- {status} - Visual indicator: 🟢 for living, ⚫ for deceasedDate Formatting with Asterisk Syntax:
-
{birth*MM/dd/yyyy} → "03/15/1950" (US format)
- {birth*dd-MM-yyyy} → "15-03-1950" (European format)
- {birth*MMM dd, yyyy} → "Mar 15, 1950" (readable format)
- {birth*MMMM dd, yyyy} → "March 15, 1950" (full month name)
- {death*yyyy-MM-dd} → "2020-12-25" (ISO format)Example Templates:
`tsx
// US date format
"{birthMM/dd/yyyy} – {deathMM/dd/yyyy}"// Readable dates
"{birthdd MMM yyyy} to {deathdd MMM yyyy}"
// Name with status
"{firstName} {lastName} {status}"
// Full month names
"{birth*MMMM dd, yyyy}"
// Initials with lifespan
"{initials} • {lifespan}"
// Current age for living people
"{birthYear} (Age: {currentAge})"
``This library is built with:
- React + TypeScript
- ReactFlow for graph visualization
- Material-UI for components and icons
MIT