@ryandymock/ancestor-tree
Version:
Interactive ancestor tree React component built with ReactFlow, featuring expandable generations and customizable UI controls
324 lines (270 loc) • 10.2 kB
Markdown
A 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 (
<div style={{ width: "100vw", height: "100vh" }}>
<AncestorTree
people={people}
rootId="1"
callbacks={callbacks}
uiControls={uiControls}
/>
</div>
);
}
```
| 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 |
| 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 |
| 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 |
```typescript
interface Person {
id: string;
name: string;
birth?: string;
death?: string;
imageUrl?: string;
spouseId?: string;
parentIds?: [string?, string?]; // [fatherId, motherId]
}
```
```typescript
type PeopleIndex = Record<string, Person>;
```
```tsx
const callbacks = {
onPersonClick: (person) => {
setSelectedPerson(person);
setShowPersonModal(true);
},
onCoupleClick: (partner1, partner2) => {
setSelectedCouple([partner1, partner2]);
setShowCoupleModal(true);
},
};
```
```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 });
},
};
```
```tsx
const uiControls = {
showControls: false,
showMiniMap: false,
showBackground: false,
enablePan: false,
enableZoom: false,
};
```
```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
};
```
```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.
};
```
```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;
},
};
```
The library includes a powerful template-based subtitle formatter with many built-in variables:
```tsx
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:**
```tsx
// 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})"
```
This library is built with:
- React + TypeScript
- ReactFlow for graph visualization
- Material-UI for components and icons
MIT