GenogramController
The GenogramController
is the heart of your family tree visualization. It manages family relationships, handles genealogical data, and provides methods for navigating and modifying the family structure.
๐ Constructorโ
GenogramController<T>({
required List<T> items,
required String Function(T) idProvider,
required String? Function(T) fatherProvider,
required String? Function(T) motherProvider,
required List<String>? Function(T) spousesProvider,
required int Function(T) genderProvider,
Size boxSize = const Size(150, 150),
double spacing = 30,
double runSpacing = 60,
GraphOrientation orientation = GraphOrientation.topToBottom,
})
Constructor Parametersโ
Parameter | Type | Required | Default | Description |
---|---|---|---|---|
items | List<T> | โ | - | The list of family members to display |
idProvider | String Function(T) | โ | - | Function that returns a unique ID for each person |
fatherProvider | String? Function(T) | โ | - | Function that returns the father's ID |
motherProvider | String? Function(T) | โ | - | Function that returns the mother's ID |
spousesProvider | List<String>? Function(T) | โ | - | Function that returns spouse IDs |
genderProvider | int Function(T) | โ | - | Function that returns gender (0=male, 1=female) |
boxSize | Size | โ | Size(150, 150) | Size of each person node |
spacing | double | โ | 30 | Horizontal spacing between siblings |
runSpacing | double | โ | 60 | Vertical spacing between generations |
orientation | GraphOrientation | โ | topToBottom | Layout orientation |
๐ง Propertiesโ
Read-Only Propertiesโ
Property | Type | Description |
---|---|---|
nodes | List<Node<T>> | All nodes in the genogram |
roots | List<Node<T>> | Root nodes (oldest generation without parents) |
items | List<T> | All family member data items |
uniqueNodeId | String | Generates a unique node ID |
repaintBoundaryKey | GlobalKey | Key for export functionality |
Configurable Propertiesโ
Property | Type | Description |
---|---|---|
orientation | GraphOrientation | Chart orientation (vertical/horizontal) |
boxSize | Size | Size of person nodes |
spacing | double | Horizontal spacing between siblings |
runSpacing | double | Vertical spacing between generations |
viewerController | CustomInteractiveViewerController? | Controller for zoom/pan |
๐ Methodsโ
Data Managementโ
addItem
โ
Adds a single family member to the genogram.
void addItem(
T item, {
bool recalculatePosition = true,
bool centerGraph = false,
})
Example:
controller.addItem(
FamilyMember(
id: 'child-001',
name: 'John Smith Jr.',
fatherId: 'father-001',
motherId: 'mother-001',
gender: 0, // male
),
centerGraph: true,
);
addItems
โ
Adds multiple family members to the genogram.
void addItems(
List<T> items, {
bool recalculatePosition = true,
bool centerGraph = false,
})
removeItem
โ
Removes a family member and handles their descendants.
void removeItem(
String? id,
ActionOnNodeRemoval action, {
bool recalculatePosition = true,
bool centerGraph = false,
})
ActionOnNodeRemoval Options:
Action | Description |
---|---|
unlinkDescendants | Makes descendants root nodes |
connectDescendantsToParent | Connects descendants to removed person's parents |
removeDescendants | Removes person and all descendants |
Example:
// Remove person but keep their children
controller.removeItem(
'person-005',
ActionOnNodeRemoval.unlinkDescendants,
);
// Remove entire family branch
controller.removeItem(
'person-003',
ActionOnNodeRemoval.removeDescendants,
);
updateItem
โ
Updates an existing family member's information.
void updateItem(
T item, {
bool recalculatePosition = true,
bool centerGraph = false,
})
replaceAll
โ
Replaces all family members in the genogram.
void replaceAll(
List<T> items, {
bool recalculatePosition = true,
bool centerGraph = false,
})
clearItems
โ
Removes all family members from the genogram.
void clearItems({
bool recalculatePosition = true,
bool centerGraph = false,
})
Relationship Queriesโ
getParents
โ
Gets the parents of a specific person.
List<Node<T>> getParents(Node<T> node)
Example:
final childNode = controller.nodes.firstWhere(
(n) => controller.idProvider(n.data) == 'child-001'
);
final parents = controller.getParents(childNode);
// Returns list containing father and/or mother nodes
getSpouseList
โ
Gets all spouses of a specific person.
List<Node<T>> getSpouseList(Node<T> node)
Example:
final personNode = controller.nodes.firstWhere(
(n) => controller.idProvider(n.data) == 'person-001'
);
final spouses = controller.getSpouseList(personNode);
getChildren
โ
Gets all children of a specific person.
List<Node<T>> getChildren(Node<T> node)
Example:
final parentNode = controller.nodes.firstWhere(
(n) => controller.idProvider(n.data) == 'parent-001'
);
final children = controller.getChildren(parentNode);
getSiblings
โ
Gets all siblings of a specific person.
List<Node<T>> getSiblings(Node<T> node)
isSubNode
โ
Checks if one person is a descendant of another.
bool isSubNode(Node<T> child, Node<T> parent)
getLevel
โ
Gets the generation level of a person (1 = oldest generation).
int getLevel(Node<T> node)
Example:
final node = controller.nodes.first;
final generation = controller.getLevel(node); // 1 for grandparents, 2 for parents, etc.
Layout and Positioningโ
calculatePosition
โ
Recalculates node positions using the genogram layout algorithm.
void calculatePosition({bool center = true})
switchOrientation
โ
Switches between vertical and horizontal layouts.
void switchOrientation({
GraphOrientation? orientation,
bool center = true,
})
Example:
// Toggle orientation
controller.switchOrientation();
// Set specific orientation
controller.switchOrientation(
orientation: GraphOrientation.leftToRight,
);
centerNode
โ
Centers the view on a specific family member.
Future<void> centerNode(
String nodeId, {
double? scale,
bool animate = true,
Duration duration = const Duration(milliseconds: 300),
Curve curve = Curves.easeInOut,
})
Example:
// Focus on a specific family member
await controller.centerNode(
'person-001',
scale: 1.5,
animate: true,
);
Export Functionsโ
exportAsImage
โ
Exports the genogram as a PNG image.
Future<Uint8List?> exportAsImage()
Example:
final imageBytes = await controller.exportAsImage();
if (imageBytes != null) {
// Save to file or share
await saveImageToGallery(imageBytes);
}
exportAsPdf
โ
Exports the genogram as a PDF document.
Future<pw.Document?> exportAsPdf()
๐ฏ Complete Exampleโ
Here's a comprehensive example showing the controller in action:
class FamilyTreeManager extends StatefulWidget {
_FamilyTreeManagerState createState() => _FamilyTreeManagerState();
}
class _FamilyTreeManagerState extends State<FamilyTreeManager> {
late GenogramController<FamilyMember> controller;
final viewerController = CustomInteractiveViewerController();
void initState() {
super.initState();
// Initialize controller
controller = GenogramController<FamilyMember>(
items: loadFamilyMembers(),
idProvider: (member) => member.id,
fatherProvider: (member) => member.fatherId,
motherProvider: (member) => member.motherId,
spousesProvider: (member) => member.spouseIds,
genderProvider: (member) => member.gender, // 0 = male, 1 = female
boxSize: Size(180, 120),
spacing: 40,
runSpacing: 100,
orientation: GraphOrientation.topToBottom,
);
// Set viewer controller for zoom/pan
controller.setViewerController(viewerController);
}
// Add new family member
void addFamilyMember(FamilyMember newMember) {
setState(() {
controller.addItem(newMember, centerGraph: true);
});
}
// Remove family member with options
void removeFamilyMember(String memberId) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Remove Family Member'),
content: Text('What should happen to their descendants?'),
actions: [
TextButton(
onPressed: () {
controller.removeItem(
memberId,
ActionOnNodeRemoval.unlinkDescendants,
);
Navigator.pop(context);
},
child: Text('Keep Descendants'),
),
TextButton(
onPressed: () {
controller.removeItem(
memberId,
ActionOnNodeRemoval.removeDescendants,
);
Navigator.pop(context);
},
child: Text('Remove All Descendants'),
),
],
),
);
}
// Update family relationships
void updateRelationships(FamilyMember member) {
controller.updateItem(member);
}
// Search and focus on family member
void searchFamilyMember(String query) async {
final member = controller.items.firstWhere(
(m) => m.name.toLowerCase().contains(query.toLowerCase()),
);
if (member != null) {
await controller.centerNode(
member.id,
scale: 2.0,
animate: true,
);
}
}
// Get family statistics
Map<String, dynamic> getFamilyStats() {
final stats = <String, dynamic>{};
// Count generations
int maxGeneration = 0;
for (final node in controller.nodes) {
final level = controller.getLevel(node);
if (level > maxGeneration) maxGeneration = level;
}
stats['generations'] = maxGeneration;
// Count family members by gender
int males = 0, females = 0;
for (final member in controller.items) {
if (controller.genderProvider(member) == 0) {
males++;
} else {
females++;
}
}
stats['males'] = males;
stats['females'] = females;
// Count marriages
int marriages = 0;
final counted = <String>{};
for (final member in controller.items) {
final spouses = controller.spousesProvider(member) ?? [];
for (final spouseId in spouses) {
final pairKey = [member.id, spouseId].sorted().join('-');
if (!counted.contains(pairKey)) {
counted.add(pairKey);
marriages++;
}
}
}
stats['marriages'] = marriages;
return stats;
}
// Export family tree
void exportFamilyTree() async {
final action = await showDialog<String>(
context: context,
builder: (context) => SimpleDialog(
title: Text('Export Family Tree'),
children: [
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'image'),
child: Text('Export as Image'),
),
SimpleDialogOption(
onPressed: () => Navigator.pop(context, 'pdf'),
child: Text('Export as PDF'),
),
],
),
);
if (action == 'image') {
final bytes = await controller.exportAsImage();
// Handle image bytes
} else if (action == 'pdf') {
final pdf = await controller.exportAsPdf();
// Handle PDF document
}
}
Widget build(BuildContext context) {
final stats = getFamilyStats();
return Scaffold(
appBar: AppBar(
title: Text('Family Tree'),
actions: [
IconButton(
icon: Icon(Icons.rotate_90_degrees_ccw),
onPressed: () => controller.switchOrientation(),
),
IconButton(
icon: Icon(Icons.download),
onPressed: exportFamilyTree,
),
],
),
body: Column(
children: [
// Statistics bar
Container(
padding: EdgeInsets.all(8),
color: Theme.of(context).primaryColor.withOpacity(0.1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('Generations: ${stats['generations']}'),
Text('Members: ${controller.items.length}'),
Text('Males: ${stats['males']}'),
Text('Females: ${stats['females']}'),
Text('Marriages: ${stats['marriages']}'),
],
),
),
// Genogram widget
Expanded(
child: Genogram<FamilyMember>(
controller: controller,
viewerController: viewerController,
// ... rest of configuration
),
),
],
),
);
}
}
๐ก Best Practicesโ
1. Efficient Family Updatesโ
// Bad: Multiple individual updates
familyMembers.forEach((member) => controller.addItem(member));
// Good: Batch update
controller.addItems(familyMembers);
2. Relationship Validationโ
// Validate relationships before adding
bool validateFamilyMember(FamilyMember member) {
// Check if parents exist
if (member.fatherId != null) {
final fatherExists = controller.items.any((m) => m.id == member.fatherId);
if (!fatherExists) return false;
}
if (member.motherId != null) {
final motherExists = controller.items.any((m) => m.id == member.motherId);
if (!motherExists) return false;
}
return true;
}
3. Performance Optimizationโ
// Disable recalculation for batch operations
controller.addItem(member1, recalculatePosition: false);
controller.addItem(member2, recalculatePosition: false);
controller.addItem(member3, recalculatePosition: false);
controller.calculatePosition(); // Calculate once at the end
4. Lazy Loading for Large Family Treesโ
class LazyGenogramController<T> extends GenogramController<T> {
final Future<List<T>> Function(String parentId) loadDescendants;
Future<void> expandBranch(String nodeId) async {
final descendants = await loadDescendants(nodeId);
addItems(descendants);
}
}
Next: Learn about the Genogram Widget โ