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.

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:

PropertyTypeDefaultDescription
textStringrequiredThe note content
widthdouble200.0Width in pixels
heightdouble100.0Height in pixels
colorColorColors.yellowBackground 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:

PropertyTypeDefaultDescription
titleStringrequiredHeader label
nodeIdsSet<String>requiredNodes to contain
colorColorColors.blueHeader and tint color
paddingEdgeInsetsEdgeInsets.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:

TypeIconUse Case
MarkerType.errorError iconErrors, failures
MarkerType.warningWarning iconWarnings, cautions
MarkerType.infoInfo iconInformation, tips
MarkerType.riskProblem iconRisk indicators
TypeIconUse Case
MarkerType.userPerson iconHuman tasks
MarkerType.scriptCode iconAutomated scripts
MarkerType.serviceSettings iconService calls
MarkerType.manualHand iconManual steps
TypeIconUse Case
MarkerType.timerTimer iconTime-based events
MarkerType.messageMessage iconCommunications
MarkerType.decisionQuestion iconDecision points
MarkerType.milestoneFlag iconCheckpoints
MarkerType.subprocessArrow iconSub-workflows
MarkerType.complianceVerified iconRegulatory 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
);
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

MethodPurpose
Size get sizeDimensions 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

  1. Use groups sparingly - Too many overlapping groups create visual clutter
  2. Match markers to semantics - Use consistent marker types for consistent meanings
  3. Keep notes concise - Sticky notes work best for short reminders, not documentation
  4. Layer thoughtfully - Put groups behind nodes, markers at the same level, notes on top
  5. Consider interactivity - Set isInteractive: false for purely decorative annotations

See Also

On this page