BetaTry our live BPMN Workflow EditorSkip to content

Connections

Connections (also called edges or links) connect ports on different nodes, representing relationships or data flow in your graph.

Connection Structure

dart
class Connection {
  final String id;              // Unique identifier
  final String sourceNodeId;    // Source node ID
  final String sourcePortId;    // Source port ID
  final String targetNodeId;    // Target node ID
  final String targetPortId;    // Target port ID

  // Labels (ConnectionLabel objects, not strings)
  ConnectionLabel? startLabel;  // Label at start (anchor 0.0)
  ConnectionLabel? label;       // Label at center (anchor 0.5)
  ConnectionLabel? endLabel;    // Label at end (anchor 1.0)

  // Styling
  final ConnectionStyle? style;       // Custom style override
  final ConnectionEndPoint? startPoint; // Custom start marker
  final ConnectionEndPoint? endPoint;   // Custom end marker
  final double? startGap;             // Gap from source port
  final double? endGap;               // Gap from target port

  // State
  bool animated;                // Whether to show animation
  bool selected;                // Whether currently selected
  final bool locked;            // Whether deletion is prevented
}

Connection Anatomy

Connection Anatomy Diagram

A connection consists of the following visual elements:

Path Elements:

  • Connection Path - The line itself rendered using ConnectionStyle (bezier, smoothstep, step, straight)
  • Path Stroke - Line styling using ConnectionTheme.color and strokeWidth
  • Path Curvature - Bezier curve control via ConnectionTheme.bezierCurvature
  • Corner Radius - Rounded corners for step styles via ConnectionTheme.cornerRadius

Endpoint Elements:

  • Start Endpoint - Marker at source port using ConnectionEndPoint (none, triangle, circle, diamond, rectangle, capsuleHalf)
  • End Endpoint - Marker at target port using ConnectionEndPoint
  • Endpoint Colors - Fill and border using ConnectionTheme.endpointColor and endpointBorderColor
  • Start/End Gaps - Space between port and endpoint via startGap and endGap

Label Elements:

  • Start Label - Text at anchor 0.0 (source port)
  • Center Label - Text at anchor 0.5 (midpoint)
  • End Label - Text at anchor 1.0 (target port)
  • Label Styling - Background, border, text via LabelTheme

Animation Elements:

  • Animation Effect - Visual effect (FlowingDashEffect, ParticleEffect, GradientFlowEffect, PulseEffect)
  • Dash Pattern - Static dashes via ConnectionTheme.dashPattern

State Colors:

  • Default State - Normal color using ConnectionTheme.color
  • Selected State - When selected using ConnectionTheme.selectedColor and selectedStrokeWidth
  • Highlight State - On hover using ConnectionTheme.highlightColor

Control Points (Editable Paths):

  • Waypoints - User-defined control points stored in Connection.controlPoints
  • Control Point Handles - Interactive handles for path editing :::

Creating Connections

dart
final connection = Connection(
  id: 'conn-1',
  sourceNodeId: 'node-1',
  sourcePortId: 'node-1-out',
  targetNodeId: 'node-2',
  targetPortId: 'node-2-in',
);

controller.addConnection(connection);
dart
final connection = Connection(
  id: 'conn-2',
  sourceNodeId: 'node-1',
  sourcePortId: 'node-1-out',
  targetNodeId: 'node-2',
  targetPortId: 'node-2-in',
  startLabel: ConnectionLabel.start(text: 'Send'),
  label: ConnectionLabel.center(text: 'Data Flow'),
  endLabel: ConnectionLabel.end(text: 'Receive'),
);

controller.addConnection(connection);
dart
final connection = Connection(
  id: 'conn-3',
  sourceNodeId: 'node-1',
  sourcePortId: 'node-1-out',
  targetNodeId: 'node-2',
  targetPortId: 'node-2-in',
  animated: true,
  style: ConnectionStyles.smoothstep,
);

controller.addConnection(connection);

Connection Styles

Connection Styles Comparison Four-panel comparison showing the same

two connected nodes with different connection styles: (1) Smoothstep - smooth orthogonal paths with rounded corners, (2) Bezier - flowing curved S-shape, (3) Step - sharp 90-degree right angles, (4) Straight - direct diagonal line. Each labeled with style name. :::

Vyuh Node Flow supports multiple connection rendering styles via ConnectionStyles:

dart
connectionTheme: ConnectionTheme.light.copyWith(
  style: ConnectionStyles.smoothstep,
)

Smooth orthogonal paths with rounded corners. This is the default.

Bezier

dart
connectionTheme: ConnectionTheme.light.copyWith(
  style: ConnectionStyles.bezier,
)

Curved Bezier paths for a flowing appearance.

Step

dart
connectionTheme: ConnectionTheme.light.copyWith(
  style: ConnectionStyles.step,
)

Sharp right-angle paths with clear horizontal and vertical segments.

Straight

dart
connectionTheme: ConnectionTheme.light.copyWith(
  style: ConnectionStyles.straight,
)

Direct straight lines between ports.

Connection Theme

Customize connection appearance via ConnectionTheme:

dart
theme: NodeFlowTheme.light.copyWith(
  connectionTheme: ConnectionTheme(
    style: ConnectionStyles.smoothstep,
    color: Colors.blue,
    selectedColor: Colors.blue.shade700,
    highlightColor: Colors.blue.shade400,
    highlightBorderColor: Colors.blue.shade800,
    strokeWidth: 2.0,
    selectedStrokeWidth: 3.0,
    startPoint: ConnectionEndPoint.none,
    endPoint: ConnectionEndPoint.triangle,
    endpointColor: Colors.blue,
    endpointBorderColor: Colors.blue.shade800,
    endpointBorderWidth: 0.0,
    bezierCurvature: 0.5,
    cornerRadius: 4.0,
    portPlugin: 20.0,
    backEdgeGap: 20.0,
    hitTolerance: 8.0,
    dashPattern: null,  // Solid line (default)
  ),
)
dart
connectionTheme: ConnectionTheme.light.copyWith(
  dashPattern: [8, 4], // 8px dash, 4px gap
)
dart
// Triangle arrow at end (predefined)
endPoint: ConnectionEndPoint.triangle,

// Circle at start (predefined)
startPoint: ConnectionEndPoint.circle,

// Custom endpoint with colors
endPoint: ConnectionEndPoint(
  shape: MarkerShapes.triangle,
  size: Size.square(10),
  color: Colors.blue,
  borderColor: Colors.blue.shade800,
  borderWidth: 1.0,
),

// No endpoints
startPoint: ConnectionEndPoint.none,
endPoint: ConnectionEndPoint.none,
dart
// Available predefined endpoints:
ConnectionEndPoint.none        // No marker
ConnectionEndPoint.circle      // Circular dot
ConnectionEndPoint.triangle    // Arrow head
ConnectionEndPoint.rectangle   // Solid rectangle
ConnectionEndPoint.diamond     // Diamond shape
ConnectionEndPoint.capsuleHalf // Rounded arrow

Temporary Connections

When creating connections by dragging, a temporary connection is shown. Configure via temporaryConnectionTheme:

dart
theme: NodeFlowTheme.light.copyWith(
  temporaryConnectionTheme: ConnectionTheme.light.copyWith(
    color: Colors.grey,
    strokeWidth: 2,
    dashPattern: [5, 5],
    endPoint: ConnectionEndPoint.capsuleHalf,
  ),
)

Connection Events

Handle connection lifecycle and interactions using ConnectionEvents. See Event System for complete documentation.

dart
NodeFlowEditor<MyData, dynamic>(
  controller: controller,
  events: NodeFlowEvents(
    connection: ConnectionEvents(
      onCreated: (connection) {
        print('Created: ${connection.id}');
        saveConnection(connection);
      },
      onDeleted: (connection) {
        print('Deleted: ${connection.id}');
        deleteConnection(connection.id);
      },
      onSelected: (connection) {
        print('Selected: ${connection?.id}');
      },
    ),
  ),
)
dart
NodeFlowEditor<MyData, dynamic>(
  controller: controller,
  events: NodeFlowEvents(
    connection: ConnectionEvents(
      onBeforeStart: (context) {
        // Prevent connections from disabled nodes
        if (context.sourceNode.data.isDisabled) {
          return ConnectionValidationResult.deny(
            reason: 'Cannot connect from disabled node',
            showMessage: true,
          );
        }
        return ConnectionValidationResult.allow();
      },
      onBeforeComplete: (context) {
        // Prevent self-connections
        if (context.isSelfConnection) {
          return ConnectionValidationResult.deny(
            reason: 'Cannot connect to same node',
            showMessage: true,
          );
        }
        return ConnectionValidationResult.allow();
      },
    ),
  ),
)
dart
NodeFlowEditor<MyData, dynamic>(
  controller: controller,
  events: NodeFlowEvents(
    connection: ConnectionEvents(
      onTap: (connection) => _selectConnection(connection),
      onDoubleTap: (connection) => _editConnection(connection),
      onContextMenu: (connection, screenPosition) {
        _showConnectionMenu(connection, screenPosition);
      },
      onConnectStart: (sourceNode, sourcePort) {
        print('Starting connection from ${sourceNode.id}:${sourcePort.id}');
      },
      onConnectEnd: (targetNode, targetPort, position) {
        if (targetNode != null) {
          print('Connected to ${targetNode.id}');
        } else {
          print('Connection cancelled at $position');
        }
      },
    ),
  ),
)

Use onBeforeComplete for validation instead of removing connections

after creation. This provides better UX with visual feedback before the connection is made. :::

Connection Operations

dart
controller.addConnection(connection);
dart
controller.removeConnection('conn-1');
dart
final connection = controller.getConnection('conn-1');
dart
final allConnections = controller.connections;
dart
// Get all connections for a node (built-in method)
final nodeConnections = controller.getConnectionsForNode('node-1');

// Get connections from a specific port
final fromPort = controller.getConnectionsFromPort('node-1', 'out-1');

// Get connections to a specific port
final toPort = controller.getConnectionsToPort('node-2', 'in-1');

Connection Validation

The ConnectionValidationResult class is used to control connection creation:

dart
// Allow connection
ConnectionValidationResult.allow()

// Deny with reason
ConnectionValidationResult.deny(
  reason: 'Cannot connect input to input',
  showMessage: true,  // Show visual feedback to user
)

// Custom result
ConnectionValidationResult(
  allowed: isValid,
  reason: validationMessage,
  showMessage: true,
)

Validation Contexts

Two context objects provide information during validation:

dart
// ConnectionStartContext - when starting a drag
class ConnectionStartContext<T> {
  final Node<T> sourceNode;
  final Port sourcePort;
  final List<String> existingConnections;

  bool get isOutputPort;
  bool get isInputPort;
}

// ConnectionCompleteContext - when completing a connection
class ConnectionCompleteContext<T> {
  final Node<T> sourceNode;
  final Port sourcePort;
  final Node<T> targetNode;
  final Port targetPort;
  final List<String> existingSourceConnections;
  final List<String> existingTargetConnections;

  bool get isOutputToInput;
  bool get isInputToOutput;
  bool get isSelfConnection;
  bool get isSamePort;
}
dart
onBeforeComplete: (context) {
  if (context.isSelfConnection) {
    return ConnectionValidationResult.deny(
      reason: 'Cannot connect node to itself',
    );
  }
  return ConnectionValidationResult.allow();
}
dart
onBeforeComplete: (context) {
  // Only allow output-to-input connections
  if (!context.isOutputToInput) {
    return ConnectionValidationResult.deny(
      reason: 'Must connect output to input',
      showMessage: true,
    );
  }
  return ConnectionValidationResult.allow();
}
dart
bool wouldCreateCycle(Connection newConnection) {
  // Build adjacency list
  final adjacency = <String, Set<String>>{};

  // Add existing connections
  for (final conn in controller.connections) {
    adjacency.putIfAbsent(conn.sourceNodeId, () => {})
        .add(conn.targetNodeId);
  }

  // Add the new connection temporarily
  adjacency.putIfAbsent(newConnection.sourceNodeId, () => {})
      .add(newConnection.targetNodeId);

  // Check for cycle using DFS
  final visited = <String>{};
  final recStack = <String>{};

  bool hasCycle(String node) {
    if (!visited.contains(node)) {
      visited.add(node);
      recStack.add(node);

      final neighbors = adjacency[node] ?? {};
      for (final neighbor in neighbors) {
        if (!visited.contains(neighbor) && hasCycle(neighbor)) {
          return true;
        } else if (recStack.contains(neighbor)) {
          return true;
        }
      }
    }
    recStack.remove(node);
    return false;
  }

  return hasCycle(newConnection.sourceNodeId);
}

// Use built-in cycle detection
final hasCycles = controller.hasCycles();
final cycles = controller.getCycles();

Connection Labels

Labels use the ConnectionLabel class with anchor positioning:

dart
// Start label (anchor 0.0 - at source)
ConnectionLabel.start(text: 'Send')

// Center label (anchor 0.5 - at midpoint)
ConnectionLabel.center(text: 'Data Flow')

// End label (anchor 1.0 - at target)
ConnectionLabel.end(text: 'Receive')

// With perpendicular offset
ConnectionLabel.center(text: 'Flow', offset: 10.0)
dart
// Custom position (0.0 to 1.0)
ConnectionLabel(
  text: 'Custom',
  anchor: 0.25,  // 25% along the path
  offset: -5.0,  // Offset perpendicular to path
)
dart
// Labels are reactive - changes trigger UI updates
connection.label = ConnectionLabel.center(text: 'Updated');
connection.startLabel = null;  // Remove label

// Or update existing label
connection.label?.updateText('New Text');
connection.label?.updateAnchor(0.75);
dart
theme: NodeFlowTheme.light.copyWith(
  labelTheme: LabelTheme(
    textStyle: TextStyle(
      fontSize: 12,
      color: Colors.black87,
      fontWeight: FontWeight.w500,
    ),
    backgroundColor: Colors.white,
    border: Border.all(color: Colors.grey.shade300),
    borderRadius: BorderRadius.circular(4),
    padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    maxWidth: 150,    // Wrap text after 150px
    maxLines: 2,      // Maximum 2 lines
    offset: 0.0,      // Default perpendicular offset
    labelGap: 8.0,    // Minimum gap from endpoints
  ),
)

Connection Selection

dart
// Select connection
controller.selectConnection('conn-1');

// Toggle selection
controller.selectConnection('conn-1', toggle: true);

// Clear connection selection
controller.clearConnectionSelection();

// Check if selected
final isSelected = controller.isConnectionSelected('conn-1');

// Get selected connection IDs
final selectedIds = controller.selectedConnectionIds;

// Select all connections
controller.selectAllConnections();

Interactive Connections

Handle connection interactions through the events API:

dart
events: NodeFlowEvents(
  connection: ConnectionEvents(
    onTap: (connection) {
      showDialog(
        context: context,
        builder: (_) => ConnectionPropertiesDialog(connection: connection),
      );
    },
    onDoubleTap: (connection) => _editConnection(connection),
    onContextMenu: (connection, screenPosition) {
      _showMenu(connection, screenPosition);
    },
    onMouseEnter: (connection) => _highlightConnection(connection),
    onMouseLeave: (connection) => _unhighlightConnection(connection),
  ),
)

Connection Serialization

Connections are automatically serialized with the graph:

dart
// Export graph (includes nodes, connections, annotations)
final graph = controller.exportGraph();
final json = graph.toJson((data) => data.toJson());

// Load graph
final loadedGraph = NodeGraph.fromJson(json, (map) => MyData.fromJson(map));
controller.loadGraph(loadedGraph);

Best Practices

  1. Unique IDs: Use unique, meaningful connection IDs
  2. Validation: Use onBeforeComplete for validation to provide immediate feedback
  3. Cleanup: Connections are automatically removed when nodes are deleted
  4. Visual Feedback: Use different endpoint styles for different connection types
  5. Labels: Use labels sparingly to avoid clutter
  6. Performance: Limit the number of connections for smooth rendering
  7. Cycles: Use controller.hasCycles() to detect cycles in your graph

Common Patterns

dart
class ConnectionFactory {
  static String generateId() {
    return 'conn-${DateTime.now().millisecondsSinceEpoch}';
  }

  static Connection create({
    required String sourceNodeId,
    required String sourcePortId,
    required String targetNodeId,
    required String targetPortId,
    String? labelText,
  }) {
    return Connection(
      id: generateId(),
      sourceNodeId: sourceNodeId,
      sourcePortId: sourcePortId,
      targetNodeId: targetNodeId,
      targetPortId: targetPortId,
      label: labelText != null
          ? ConnectionLabel.center(text: labelText)
          : null,
    );
  }
}
dart
void autoConnect(String sourceNodeId, String targetNodeId) {
  final sourceNode = controller.getNode(sourceNodeId);
  final targetNode = controller.getNode(targetNodeId);

  if (sourceNode == null || targetNode == null) return;
  if (sourceNode.outputPorts.isEmpty || targetNode.inputPorts.isEmpty) return;

  // Connect first available ports
  final connection = Connection(
    id: ConnectionFactory.generateId(),
    sourceNodeId: sourceNodeId,
    sourcePortId: sourceNode.outputPorts.first.id,
    targetNodeId: targetNodeId,
    targetPortId: targetNode.inputPorts.first.id,
  );

  controller.addConnection(connection);
}
dart
// Create a connection that cannot be deleted
final connection = Connection(
  id: 'required-conn',
  sourceNodeId: 'node-1',
  sourcePortId: 'out',
  targetNodeId: 'node-2',
  targetPortId: 'in',
  locked: true,  // Prevents deletion
);

Next Steps