Annotations
Add sticky notes, groups, and markers to enrich your node flows
Annotations
Annotations are visual overlays that add context to your node flows without affecting the underlying graph logic. Use them for documentation, organization, and semantic indicators.
Sticky Notes
Free-floating notes for comments and documentation
Groups
Visual containers that surround related nodes
Markers
Compact icons for status and semantic indicators
Three Annotation Types
Sticky Notes
Free-floating notes that can be placed anywhere on the canvas. Perfect for comments, reminders, and documentation.
controller.createStickyNote(
position: const Offset(400, 50),
text: 'This is a reminder!\n\nMulti-line text supported.',
width: 200,
height: 120,
color: Colors.yellow.shade200,
);Properties:
| Property | Type | Default | Description |
|---|---|---|---|
text | String | required | The note content |
width | double | 200.0 | Width in pixels |
height | double | 100.0 | Height in pixels |
color | Color | Colors.yellow | Background color |
Groups
Visual containers that automatically surround a set of nodes. Groups resize and reposition as their contained nodes move.
controller.createGroupAnnotation(
title: 'Data Processing',
nodeIds: {'node1', 'node2', 'node3'},
color: Colors.blue.shade300,
padding: const EdgeInsets.all(30),
);Properties:
| Property | Type | Default | Description |
|---|---|---|---|
title | String | required | Header label |
nodeIds | Set<String> | required | Nodes to contain |
color | Color | Colors.blue | Header and tint color |
padding | EdgeInsets | EdgeInsets.all(20) | Space around nodes |
Groups have a default z-index of -1, placing them behind nodes. Use sendAnnotationToBack() to layer multiple groups.
Markers
Compact circular badges with icons. Use them for BPMN-style workflow indicators, status badges, and semantic tags.
controller.createMarker(
position: const Offset(80, 80),
markerType: MarkerType.warning,
color: Colors.orange,
tooltip: 'Check prerequisites before proceeding',
);Marker Types:
| Type | Icon | Use Case |
|---|---|---|
MarkerType.error | Error icon | Errors, failures |
MarkerType.warning | Warning icon | Warnings, cautions |
MarkerType.info | Info icon | Information, tips |
MarkerType.risk | Problem icon | Risk indicators |
| Type | Icon | Use Case |
|---|---|---|
MarkerType.user | Person icon | Human tasks |
MarkerType.script | Code icon | Automated scripts |
MarkerType.service | Settings icon | Service calls |
MarkerType.manual | Hand icon | Manual steps |
| Type | Icon | Use Case |
|---|---|---|
MarkerType.timer | Timer icon | Time-based events |
MarkerType.message | Message icon | Communications |
MarkerType.decision | Question icon | Decision points |
MarkerType.milestone | Flag icon | Checkpoints |
MarkerType.subprocess | Arrow icon | Sub-workflows |
MarkerType.compliance | Verified icon | Regulatory items |
Node-Following Annotations
Annotations can follow nodes, moving automatically when the node moves. This is powerful for attaching persistent notes or markers to specific nodes.
Create the Annotation
final note = controller.createStickyNote(
position: const Offset(0, 0), // Will be overridden
text: 'Always visible next to this node',
offset: const Offset(50, 100), // Offset from node center
);Link to a Node
controller.annotations.addNodeDependency(note.id, 'target-node-id');The Annotation Follows
When the node moves, the annotation moves with it, maintaining its offset.
The offset property determines the annotation's position relative to the dependent node's center:
StickyAnnotation(
id: 'linked-note',
position: Offset.zero,
text: 'I follow the node!',
offset: const Offset(80, 50), // 80px right, 50px down from node center
);Managing Annotations
Access the Annotation Controller
// Via controller property
controller.annotations.addAnnotation(myAnnotation);
controller.annotations.removeAnnotation(id);
// Convenience methods on main controller
controller.createStickyNote(...);
controller.createGroupAnnotation(...);
controller.createMarker(...);
controller.removeAnnotation(id);Visibility Control
// Individual annotation
controller.annotations.setAnnotationVisible(id, false);
// All at once
controller.hideAllAnnotations();
controller.showAllAnnotations();Z-Index and Layering
Annotations are rendered in z-index order. Lower values appear behind higher values.
// Groups typically use negative z-index to appear behind nodes
controller.annotations.sendAnnotationToBack(groupId);
// Bring a sticky note to front
controller.annotations.bringAnnotationToFront(noteId);Selection
Annotations can be selected like nodes:
// Programmatic selection
controller.selectAnnotation(id);
controller.clearSelection(); // Clears nodes AND annotations
// Check selection state
final isSelected = annotation.currentSelected;Set isInteractive: false on an annotation to make it purely decorative - it won't respond to clicks or selection.
Configuration
Grid Snapping
Annotations can snap to grid independently of nodes:
NodeFlowController(
config: NodeFlowConfig(
snapToGrid: true,
snapAnnotationsToGrid: false, // Annotations move freely
),
);Theme Integration
Annotations respect the AnnotationTheme in your NodeFlowTheme:
NodeFlowTheme(
annotationTheme: AnnotationTheme(
selectionBorderColor: Colors.blue,
selectionBorderWidth: 2.0,
// Additional theming options...
),
);Complete Example
Here's a workflow with all three annotation types:
class AnnotatedWorkflow extends StatefulWidget {
@override
State<AnnotatedWorkflow> createState() => _AnnotatedWorkflowState();
}
class _AnnotatedWorkflowState extends State<AnnotatedWorkflow> {
late final NodeFlowController<Map<String, dynamic>> controller;
@override
void initState() {
super.initState();
controller = NodeFlowController();
_setupWorkflow();
}
void _setupWorkflow() {
// Add nodes
controller.addNode(Node(
id: 'start',
type: 'start',
position: const Offset(100, 100),
size: const Size(120, 60),
data: {'label': 'Start'},
outputPorts: const [Port(id: 'out', position: PortPosition.right)],
));
controller.addNode(Node(
id: 'process',
type: 'process',
position: const Offset(280, 100),
size: const Size(140, 80),
data: {'label': 'Process Data'},
inputPorts: const [Port(id: 'in', position: PortPosition.left)],
outputPorts: const [Port(id: 'out', position: PortPosition.right)],
));
controller.addNode(Node(
id: 'end',
type: 'end',
position: const Offset(480, 100),
size: const Size(120, 60),
data: {'label': 'End'},
inputPorts: const [Port(id: 'in', position: PortPosition.left)],
));
// Connect nodes
controller.addConnection(Connection(
id: 'c1',
sourceNodeId: 'start',
sourcePortId: 'out',
targetNodeId: 'process',
targetPortId: 'in',
));
controller.addConnection(Connection(
id: 'c2',
sourceNodeId: 'process',
sourcePortId: 'out',
targetNodeId: 'end',
targetPortId: 'in',
));
// Add a group around the workflow
controller.createGroupAnnotation(
title: 'Main Workflow',
nodeIds: {'start', 'process', 'end'},
color: Colors.indigo.shade200,
padding: const EdgeInsets.all(40),
);
// Add a documentation sticky note
controller.createStickyNote(
position: const Offset(100, 220),
text: 'This workflow processes incoming data and outputs results.',
width: 220,
height: 80,
color: Colors.amber.shade100,
);
// Add status markers
controller.createMarker(
position: const Offset(260, 80),
markerType: MarkerType.timer,
color: Colors.blue,
tooltip: 'Estimated time: 5 minutes',
);
controller.createMarker(
position: const Offset(460, 80),
markerType: MarkerType.milestone,
color: Colors.green,
tooltip: 'Completion checkpoint',
);
}
@override
Widget build(BuildContext context) {
return NodeFlowEditor<Map<String, dynamic>>(
controller: controller,
nodeBuilder: (context, node) => Center(
child: Text(node.data['label'] ?? ''),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}Creating Custom Annotations
Extend the Annotation base class to create custom annotation types:
class BadgeAnnotation extends Annotation {
final String label;
final Color badgeColor;
final double radius;
BadgeAnnotation({
required super.id,
required Offset position,
required this.label,
this.badgeColor = Colors.purple,
this.radius = 30,
}) : super(
type: 'badge',
initialPosition: position,
);
@override
Size get size => Size(radius * 2, radius * 2);
@override
Widget buildWidget(BuildContext context) {
return Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
color: badgeColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: badgeColor.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: Center(
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
@override
Map<String, dynamic> toJson() => {
'id': id,
'type': type,
'x': currentPosition.dx,
'y': currentPosition.dy,
'label': label,
'badgeColor': badgeColor.value,
'radius': radius,
};
@override
void fromJson(Map<String, dynamic> json) {
setPosition(Offset(
(json['x'] as num).toDouble(),
(json['y'] as num).toDouble(),
));
}
}Required Overrides
| Method | Purpose |
|---|---|
Size get size | Dimensions for hit testing |
Widget buildWidget(BuildContext) | Visual representation |
Map<String, dynamic> toJson() | Serialization |
void fromJson(Map<String, dynamic>) | Deserialization |
The framework automatically handles positioning, selection feedback, drag interactions, and reactivity. You just define the visual appearance and serialization.
Serialization
Annotations are automatically included when you serialize the graph:
// Export includes annotations
final json = controller.exportGraph(
nodeDataConverter: (data) => data,
);
// Import restores annotations
await controller.loadGraph(
json,
nodeDataConverter: (json) => json as Map<String, dynamic>,
);For custom annotation types, register them in Annotation.fromJsonByType() or handle deserialization manually.
Best Practices
- Use groups sparingly - Too many overlapping groups create visual clutter
- Match markers to semantics - Use consistent marker types for consistent meanings
- Keep notes concise - Sticky notes work best for short reminders, not documentation
- Layer thoughtfully - Put groups behind nodes, markers at the same level, notes on top
- Consider interactivity - Set
isInteractive: falsefor purely decorative annotations
See Also
- Serialization - Save and load workflows with annotations
- Theming Overview - Customize annotation appearance
- Controller - Full annotation controller API