Skip to main content

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โ€‹

ParameterTypeRequiredDefaultDescription
itemsList<T>โœ…-The list of family members to display
idProviderString Function(T)โœ…-Function that returns a unique ID for each person
fatherProviderString? Function(T)โœ…-Function that returns the father's ID
motherProviderString? Function(T)โœ…-Function that returns the mother's ID
spousesProviderList<String>? Function(T)โœ…-Function that returns spouse IDs
genderProviderint Function(T)โœ…-Function that returns gender (0=male, 1=female)
boxSizeSizeโŒSize(150, 150)Size of each person node
spacingdoubleโŒ30Horizontal spacing between siblings
runSpacingdoubleโŒ60Vertical spacing between generations
orientationGraphOrientationโŒtopToBottomLayout orientation

๐Ÿ”ง Propertiesโ€‹

Read-Only Propertiesโ€‹

PropertyTypeDescription
nodesList<Node<T>>All nodes in the genogram
rootsList<Node<T>>Root nodes (oldest generation without parents)
itemsList<T>All family member data items
uniqueNodeIdStringGenerates a unique node ID
repaintBoundaryKeyGlobalKeyKey for export functionality

Configurable Propertiesโ€‹

PropertyTypeDescription
orientationGraphOrientationChart orientation (vertical/horizontal)
boxSizeSizeSize of person nodes
spacingdoubleHorizontal spacing between siblings
runSpacingdoubleVertical spacing between generations
viewerControllerCustomInteractiveViewerController?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:

ActionDescription
unlinkDescendantsMakes descendants root nodes
connectDescendantsToParentConnects descendants to removed person's parents
removeDescendantsRemoves 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 โ†’