UNPKG

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