@ryandymock/ancestor-tree
v0.1.5
Published
Interactive ancestor tree React component built with ReactFlow, featuring expandable generations and customizable UI controls
Maintainers
Readme
Ancestor Tree Library
A React library for displaying interactive ancestor trees using ReactFlow.
Features
- 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
Installation
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material reactflowBasic Usage
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 (
<div style={{ width: "100vw", height: "100vh" }}>
<AncestorTree
people={people}
rootId="1"
callbacks={callbacks}
uiControls={uiControls}
/>
</div>
);
}API Reference
Props
| 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 |
AncestorTreeCallbacks
| 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 |
AncestorTreeUIControls
| 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 |
Person Type
interface Person {
id: string;
name: string;
birth?: string;
death?: string;
imageUrl?: string;
spouseId?: string;
parentIds?: [string?, string?]; // [fatherId, motherId]
}PeopleIndex Type
type PeopleIndex = Record<string, Person>;Example Use Cases
1. Basic Tree with Click Handlers
const callbacks = {
onPersonClick: (person) => {
setSelectedPerson(person);
setShowPersonModal(true);
},
onCoupleClick: (partner1, partner2) => {
setSelectedCouple([partner1, partner2]);
setShowCoupleModal(true);
},
};2. Tracking User Interactions
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. Minimal UI for Embedding
const uiControls = {
showControls: false,
showMiniMap: false,
showBackground: false,
enablePan: false,
enableZoom: false,
};4. Custom Spacing for Theme Compatibility
// 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
};5. Configurable Node Widths
// 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.
};6. Custom Subtitle Formatting
// 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;
},
};7. Advanced Subtitle Formatting with Template Variables
The library includes a powerful template-based subtitle formatter with many built-in variables:
const uiControls = {
formatPersonSubtitle: createSubtitleFormatter("{birth*MMM dd, yyyy} – {death*MMM 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 deceased
Date 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:
// US date format
"{birth*MM/dd/yyyy} – {death*MM/dd/yyyy}"
// Readable dates
"{birth*dd MMM yyyy} to {death*dd 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})"Development
This library is built with:
- React + TypeScript
- ReactFlow for graph visualization
- Material-UI for components and icons
License
MIT
