# 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

```bash
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material reactflow
```

## Basic Usage

```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>
  );
}
```

## 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

```typescript
interface Person {
  id: string;
  name: string;
  birth?: string;
  death?: string;
  imageUrl?: string;
  spouseId?: string;
  parentIds?: [string?, string?]; // [fatherId, motherId]
}
```

### PeopleIndex Type

```typescript
type PeopleIndex = Record<string, Person>;
```

## Example Use Cases

### 1. Basic Tree with Click Handlers

```tsx
const callbacks = {
  onPersonClick: (person) => {
    setSelectedPerson(person);
    setShowPersonModal(true);
  },
  onCoupleClick: (partner1, partner2) => {
    setSelectedCouple([partner1, partner2]);
    setShowCoupleModal(true);
  },
};
```

### 2. Tracking User Interactions

```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. Minimal UI for Embedding

```tsx
const uiControls = {
  showControls: false,
  showMiniMap: false,
  showBackground: false,
  enablePan: false,
  enableZoom: false,
};
```

### 4. Custom Spacing for Theme Compatibility

```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
};
```

### 5. Configurable Node Widths

```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.
};
```

### 6. Custom Subtitle Formatting

```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;
  },
};
```

### 7. Advanced Subtitle Formatting with Template Variables

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})"
```

## Development

This library is built with:
- React + TypeScript
- ReactFlow for graph visualization
- Material-UI for components and icons

## License

MIT 