Connectors and labels
On this page
| Category | What you can do |
|---|---|
| Routing | Choose the path between connected elements. |
| Appearance | Set stroke color, width, pattern and arrowheads. |
| Connect elements | Create connections and add labels. |
| Label capabilities | Declare allowed label behavior. |
| Label appearance | Set the appearance of an individual label. |
| Creation defaults | Configure initial connection appearance. |
| Effects | Send particles or a pulse along a connection. |
Change connector style
dart
controller.setConnectorStyle(
connectorId,
color: const DiagramColor(0xFF3355FF),
strokeWidth: 3,
strokeStyle: StrokeStyle.dashed,
);The same method works in Flutter and pure Dart. Only supplied fields change.
| Argument | Type | Meaning |
|---|---|---|
connectorId | String | Existing connector ID. |
color | DiagramColor? | Stroke color. |
strokeWidth | double? | Stroke width in world units. |
strokeStyle | StrokeStyle? | Solid, dashed or dotted stroke. |
Omitted fields retain their current values. Endpoints, routing, arrowheads and labels are preserved. The counterpart through controller.edits returns an allowed or denied EditResult; a successful change creates one undo step. Missing or locked connectors and host admission can deny the edit, and invalid values are denied with DiagramDenialCode.unsupportedOperation. An unchanged style publishes nothing. The inspector performs this same verb.
Arrowheads
Each end marker has its own setter:
dart
controller.edits.setEndArrowhead(connectorId, kind: ArrowheadKind.filledTriangle);
controller.edits.setStartArrowhead(connectorId, kind: ArrowheadKind.none);
controller.edits.setEndArrowhead(connectorId, width: 14, height: 10);| Argument | Type | Meaning |
|---|---|---|
kind | ArrowheadKind? | An installed arrowhead kind, such as none, triangle or filledTriangle. By name ('triangle') through perform. |
width, height | double? | Marker size in world units; nonnegative. |
Only supplied parts change. Each call is one undo step. See verbs.
Reroute connections
Use controller.edits.reroute() to find clear paths for elbow connections. Pass ids to reroute a subset. The verb replaces manual elbow bends and saves all resulting paths in one undoable document edit. If any requested path cannot be found, nothing changes; show the returned result's reason in your application.
Moving unrelated shapes does not change a saved path. Moving an endpoint keeps the saved path while it stays usable and clear of its owners and nearby obstacles. Otherwise the connector falls back to normal routing and its resolved routeDiagnostic reports a stale route (isRouteStale), so an application can offer Reroute connections again. Straight and curved routes are excluded from the default reroute.
Connections rerouted together are spread apart where they would otherwise run along the same channel. Routing does not bundle links, avoid every crossing, or guarantee a globally shortest route. Group interiors are not obstacles, and owned connections stay inside every clipping ancestor. Locked connections, blocked terminals, or no route inside the clips reject the whole edit. Try the crowded network.
Change a route
controller.edits.setConnectorRoute(connectorId, {route, portSpacing, options}) changes how one connector is routed. Use its counterpart through controller.edits for an EditResult. Everything supplied changes together, in one undo step.
| Argument | Type | Meaning |
|---|---|---|
connectorId | String | ID of an existing, unlocked connector. |
route | ConnectorRoute? | New route kind: core's ConnectorRoute.straight, or an installed route such as ConnectorRoutes.orthogonal, quadratic or bezier from the connectors extension. Switching kinds clears the previous kind's control points and options; undo restores them. |
portSpacing | double? | World-space gap between connectors that share a port. Applies to every route. |
options | Map<String, Object?> | Values declared by the route's options schema. The elbow (orthogonal) route declares cornerRadius (default 8) and terminalExtension (default 24). A null value removes the option, restoring its schema default. |
dart
controller.setConnectorRoute('connection', route: ConnectorRoutes.bezier);
controller.setConnectorRoute(
connectorId,
portSpacing: 8,
options: {'terminalExtension': 24, 'cornerRadius': 12},
);Core defines only ConnectorRoute.straight. The elbow and curved identities, ConnectorRoutes.orthogonal, ConnectorRoutes.quadratic and ConnectorRoutes.bezier, come from vyuh_diagram_extension_connectors beside the providers that implement them; a route is available once its extension is installed. Any connector may select any installed route.
An option the route does not declare, or a value its schema rejects, denies the entire edit. The verb preserves endpoints, labels and appearance. Missing or locked connectors and uninstalled routes are denied; an unchanged update is a no-op. The inspector edits routes through the same rules.
Route providers declare typed options in a DiagramRouteOptionsSchema: each DiagramRouteOption has a key, a label, an optional unit and a ValueField, the same declaration a shape uses for its values. Every route, including the elbow route, stores its values in ConnectorElement.routeOptions, which JSON saves verbatim and document admission validates against the installed schema. Absent options take the schema's defaults; read a connector's effective elbow values from connector.routeOptions, falling back to those defaults.
dart
final connector = ConnectorElement(
id: 'connection',
start: start,
end: end,
route: ConnectorRoutes.orthogonal,
routeOptions: const {'cornerRadius': 12, 'terminalExtension': 32},
);
``` A route
that mixes in `DiagramRouteOptionProperties` from the connectors extension shows
one inspector control per option, the same control a shape value of that
field gets, as the elbow route does; see
[properties](../guide/properties).
## Connect and label shapes
Use `controller.edits.connect(from, to)` to connect existing elements. The
controller assigns a fresh connector ID and reports it as the result's
`id`. Shapes and text boxes use
the same connection rules; optional ports select specific anchors.
```dart
final connectionId = controller.connect(
sourceId,
targetId,
fromPort: 'output',
toPort: 'input',
route: ConnectorRoutes.bezier,
);
controller.edits.reconnect(
connectionId,
atStart: false,
endpoint: BoundEndpoint(elementId: anotherTargetId, portId: 'input'),
);
final labelId = controller.addConnectorLabel(connectionId, text: 'Next step', fraction: 0.5);fromPort and toPort default to null; route defaults to ConnectorRoute.straight, and select defaults to true. An accepted connection is one undoable edit. Invalid ports and denied connections return a denied result, with no id, without inserting a connector. Use add(ConnectorElement(...)) for authored IDs, free endpoints, custom anchors, or initial appearance fields.
The example assumes those IDs and ports exist. Omit portId for a shape anchor or use FreeEndpoint for an unbound endpoint. Connection verbs apply the diagram's connection policy; reconnection retains the other connection properties. Routing supports straight, elbow (orthogonal), quadratic and cubic Bezier paths.
Dragging an endpoint onto a portless, whole-shape-connectable object creates a point anchor at the drop position. BoundEndpoint.anchorMode is ConnectorAnchorMode.point, with normalizedAnchor measured relative to the owner's frame. The anchor follows movement, resizing and rotation, and survives undo, copying and save/reopen. The visible stroke and arrowhead are clipped to the contour; selecting the connector reveals its actual interior endpoint. Port-only shapes continue to use declared ports.
connectionSnapRadius is a screen-pixel radius shared by ports and virtual targets; zero disables magnets. Portless shapes offer a center, four side midpoints and four projected corners. Away from magnets, drops preserve the chosen anchor. Port-only shapes require a nearby declared port. Moving the other endpoint preserves the source port.
For the same behavior through the API:
dart
controller.edits.reconnect(
connectionId,
atStart: false,
endpoint: BoundEndpoint(
elementId: targetId,
normalizedAnchor: const DiagramPoint(0.23, 0.71),
anchorMode: ConnectorAnchorMode.point,
),
);Coordinates range from 0 to 1 in each axis: (0, 0) is the frame's top-left and (1, 1) is its bottom-right. Point mode cannot be combined with portId.
Whole-shape bindings default to ConnectorAnchorMode.outline and a center anchor. Straight and curved routes aim at the center and clip at the outline; elbows choose a facing axis. Ports and explicit side anchors keep their directions. Handles show the attachment; visible strokes end at the outline.
Port bindings use the same owner-contour fitting as whole-shape anchors. A port is a routing anchor, not permission to draw through its owner. Straight, quadratic and Bézier paths stop at the first occupied owner contour, with the complete arrowhead and stroke kept outside disjoint endpoint owners. Elbows add orthogonal route negotiation before that shared fitting step. Overlapping owners retain the existing overlap exception.
Labels are attachments owned by the connector. Their fraction ranges from 0 to 1 along resolved path length; they also have offset, size and a shared text story. The label verbs addConnectorLabel, setConnectorLabel and removeConnectorLabel add, change and remove labels; controller.setSelection(ConnectorLabelSelection(connectorId: c, labelId: l)) selects one. The registry's connector labeling capability controls availability and maximum count.
Arrange labels
dart
controller.arrangeLabels();
controller.arrangeLabels(ids: ['connection-id'], spacing: 12);requireAllowed() throws if the editor rejects the edit, for example because a connector is locked. It does not request permission from a user. To show a message instead, handle the returned result:
dart
final result = controller.edits.arrangeLabels();
if (!result.allowed) {
// Display result.reason in your application's feedback area.
}Find nearby positions using measured label geometry, avoiding leaf shapes and other labels. Omit ids to arrange all labels; labels outside a supplied subset stay fixed and remain obstacles. spacing is the minimum gap in world units (default 8; finite and nonnegative).
Connectors owned by expanded frames, lanes, or groups can arrange their labels. Candidates must fit inside every active ancestor clip, including spacing. Clipping disabled on a container imposes no boundary. Rotated and custom outlines use conservative containment checks; if no candidate can be proven to fit, the whole arrangement is rejected without changing content or history.
Bulk label arrangement and rerouting skip connections hidden by collapsed frames. Explicitly targeting a hidden connection asks you to expand its frame first. Visible external connections use the same collapsed-frame endpoint in previews, rerouting and the canvas; their saved child/port bindings stay intact.
Valid placements near their path anchors are retained. Detached labels are returned to the path or placed immediately beside it. The bounded search tries positions along the connector instead of moving labels progressively farther away. All changes share one undo entry and are stored as ordinary label fractions and offsets, so save/load, hit testing and output consume the same positions. A repeated call with nothing to move adds no history.
The bounded search can fail even when a distant placement exists. On failure nothing changes; the result explains that more room is needed. Label movement must be enabled, and requested connectors must exist, be unlocked and have no group owner. Finish active edits first. This verb does not avoid all connector crossings or solve global label optimization. See the crowded-label example.
ConnectorLabelingCapability
The default connector label is Plain Text, centered on its path anchor. It grows to fit its longest line and expands vertically for additional paragraphs. It admits no lists or marks, and it has no resize handles by default.
| Property | Default | Contract |
|---|---|---|
maximumLabels | 2 | One or two attachments on a connector. |
endInset | 0.15 | Default distance from each end, as a fraction of the route. |
defaultFontFamily | sans-serif | Label font, independent of node typography. |
defaultFontSize | 14 | Inherited label text size in logical pixels; explicit run sizes take precedence. |
textType | PlainTextDefinition() | The text filling every label: multiline plain paragraphs. Use maxLines: 1 for Enter to exit editing, or a RichTextDefinition for formatted labels. |
resizable | false | No resize handles, and setConnectorLabel denies size, by default. |
movable | true | Dragging projects the label onto the visible path, between fractions 0 and 1. |
textEditable | true | Double-click creates or edits a label. |
deletable | true | A selected label can be removed independently. |
defaultSize | DiagramSize(120, 32) | Authored box; plain labels fit their longest line, reserving one character when empty. Height grows for additional lines. |
minimumSize | DiagramSize(24, 16) | Admission limit for authored label geometry. |
Select a connector and press Enter, or double-click its path, to create a label when capacity remains. With two-label capacity, defaults place labels near opposite ends: at route fractions endInset and 1 - endInset (0.15 and 0.85). With one-label capacity, the default is the midpoint. Explicit API fraction values remain supported. At capacity, path activation does nothing; it never switches to an arbitrary existing label.
Select a specific label and press Enter, or double-click that label, to edit its text. Label selection retains its identity even when another label exists. maximumLabels accepts 1 or 2. endInset must be finite and in [0, 0.5). defaultFontFamily defaults to sans-serif, independently of the node font; defaultFontSize remains 14. Explicit inline font choices override these defaults.
These interactions are built into the connector labeling capability. Locked connectors, disabled labeling, or textEditable: false prevent entry. Merely entering an existing label creates no document/history change. Enter adds a paragraph; Escape exits editing. The padded editing outline stays visible while typing. Clearing text removes visible ink and the idle hit box; reactivation can reopen the existing empty attachment. Labels accept plain paragraphs, not lists. Use controller.addConnectorLabel explicitly for additional labels, up to the configured limit. Hosts can opt into single-line or resizable labels through a custom capability.
The default inherited font size is 14 px, shared by measurement, painting, caret geometry, the inspector and size shortcuts. Explicit TextRun.fontSize values are preserved. Set ConnectorLabelingCapability(defaultFontSize: ...) to customize the inherited size for a registry.
Style one label
Select a label to show Connector label in the inspector. Its font size, text color and inline marks affect that attachment only. The connector stroke and other labels keep their own styles. Plain labels expose text properties; choosing Badge also exposes fill, border color/style/width, radius and padding. Single-line labels do not show list or paragraph-alignment controls.
Text styling uses the shared TextRun fields (fontSize, color, fontFamily, marks), not a separate label-only text model. For example:
dart
controller.edits.setConnectorLabel(
connectionId,
labelId,
story: TextStory(
id: existingStory.id,
blocks: [
for (final block in existingStory.blocks)
block.copyWith(runs: [
for (final run in block.runs)
run.copyWith(fontSize: 20, color: const DiagramColor(0xff2454d8)),
]),
],
),
presentation: const BadgeLabelPresentation(
fill: DiagramColor(0xffffffff),
stroke: DiagramColor(0xff2454d8),
strokeWidth: 2,
cornerRadius: 8,
padding: 6,
),
);existingStory is the selected attachment's current story. Preserve its IDs and other run properties when restyling. A text change and presentation change in the same call publish and undo together.
controller.setConnectorLabel
dart
EditResult setConnectorLabel(
String connectorId,
String labelId, {
TextStory? story,
double? fraction,
DiagramPoint? offset,
DiagramSize? size,
LabelPresentation? presentation,
})| Argument | Contract |
|---|---|
connectorId, labelId | Required positional identities. Missing or locked connectors reject the operation. |
story | Shared paragraph/run content. Requires text editing capability. |
fraction | Position along resolved path length, from 0 to 1; requires movement capability. |
offset | World-space displacement from the path anchor; requires movement capability. |
size | Authored label size; requires resize capability, disabled by default. |
presentation | Plain Text or badge surface. Omitted fields preserve their current values. |
All named arguments default to null, meaning unchanged. The method returns an EditResult; a missing capability (unsupportedOperation), a missing connector (unavailableTarget) or host admission returns a denied result, and requireAllowed() converts it into DiagramChangeDenied. A missing label (unavailableTarget) or a value the label model rejects (unsupportedOperation) is denied too. No partial update occurs. By name, the verb is perform([connectorId], 'setConnectorLabel', {'label': labelId, ...}).
LabelPresentation
| Production | Contract |
|---|---|
const PlainLabelPresentation() | Text only. No fill, border, radius or padding properties. |
const BadgeLabelPresentation(...) | Rounded backing surface with the fields below. |
| Badge property | Type | Default |
|---|---|---|
fill | DiagramColor | 0xffffffff (white). Use transparent for a border-only badge. |
stroke | DiagramColor | 0xff94a3b8 (muted slate). |
strokeWidth | double | 0 (no border). |
strokeStyle | StrokeStyle | solid; also supports dashed and dotted. |
cornerRadius | double | 4. |
padding | double | 4. |
Dimensions must be finite and nonnegative. Padding must leave a valid text interior. copyWith returns a new badge value; equality includes every field. Border geometry is centered on the backing surface. Resolved visualBounds include its half-width bleed for hit testing, selection and scene bounds; text layout continues to use the padded interior. A visible badge fill or border reserves its area from path effects so particles cannot paint over it.
These styles are saved in JSON and rendered by the shared canvas/PNG painter.
Creation defaults
A DiagramConnectorDefinition(...) contributed through an extension's connectors configures connector creation and labeling. Without a type it replaces the core connector type; DiagramConnectorDefinition(type: 'k', ...) adds another connector type that DiagramConnectorTool(connectorType: 'k') creates.
| Property | Type | Default | Behavior |
|---|---|---|---|
defaultEndOffset | DiagramPoint | (160, 0) | Canvas-space displacement from the start when the connector tool is clicked without dragging. Must be finite and nonzero. Grid snapping still applies. Dragging uses the pointer endpoint. |
labeling | ConnectorLabelingCapability? | Default labeling capability | Configures label creation and editing; null disables labels. |
Inspector editing ranges
DiagramConnectorDefinition.editing accepts DiagramConnectorEditing. These bounds configure the built-in inspector; they do not rewrite authored route values or change the geometry admission rules.
| Property | Default |
|---|---|
minimumStrokeWidth / maximumStrokeWidth | 1 / 32 |
minimumArrowSize / maximumArrowSize | 4 / 64 |
maximumPortSpacing | 64 |
maximumPortExtension | 160 |
maximumCornerRadius | 64 |
All values must be finite and positive, and each minimum must not exceed its maximum. Registry construction validates the declaration. Spacing, extension, and radius controls retain a zero lower bound. Route capabilities determine which controls are shown; quadratic and Bezier routes do not acquire extension controls simply because an editing maximum exists.
addConnectorLabel(text: ...) assigns story and paragraph identities. Supply either plain text or a structured story, never both. Registered label rules still apply: labels accept multiline plain paragraphs, while list content is rejected without publishing a label or changing undo history.
Editing elbow segments
Select an elbow connector to show a circular handle at the midpoint of each nonzero horizontal or vertical segment. Drag a horizontal segment up or down, or a vertical segment left or right. Adjacent parallel runs snap within eight screen pixels and fuse into one segment. Moving outside the snap region during the same drag restores the jog; only pointer release commits the simplified route. Zero-length and collinear vertices are removed before handles are generated, so each continuous run has exactly one circular handle. Rounded corners remain a rendering setting; handles use the unrounded route geometry.
Dragging a terminal segment adds orthogonal return legs instead of moving the bound endpoint. During a drag, invalid positions near a bound terminal clamp to the nearest valid position rather than triggering automatic rerouting. A short terminal run is retained (up to eight world units, limited by the original run); valid fusion into a direct route remains allowed. Moving back from the limit responds immediately from the original gesture baseline. One drag is one undo step; cancelling restores the previous route. Manually edited routes save routedPoints with preserveRoute: true in JSON; set them from code with controller.edits.setRouteGeometry(connectorId, routedPoints: [...], preserveRoute: true), which also sets route, bend and controlPoints. Endpoint movement reconnects the route to the saved interior corridors; moving both endpoints equally translates the route. Explicit Reroute clears manual preservation and computes a new obstacle-avoiding route.
Manual routes express placement intent, not an automatic guarantee of obstacle clearance after arbitrary shape movement. Reroute when the surrounding layout changes and a saved corridor is no longer appropriate. Saved positions are hints: if they would create a crossing or a wrong-way terminal, normal routing takes precedence. Collinear backtracking is removed before corners are rounded. Zero-length segments have no handle, and collapsed connection bundles must be expanded before editing.
Effects
A connector plays transient effects on a mounted canvas: particles or marching dashes that flow along its route (Effect.flow), a pulse or blink of its stroke (Effect.pulse), or a fade to a dim level (Effect.fade). Effects never change the document or history.
dart
final flow = controller.effects.play(
const Effect.flow(particle: EffectParticle.arrow(), count: 4),
on: connectorId,
);
// Blink the route three times, then stop.
controller.effects.play(
const Effect.pulse(
target: EffectTarget.stroke,
waveform: Waveform.step,
period: Duration(milliseconds: 500),
repeat: 3,
),
on: connectorId,
);
flow.stop();See Animation & effects for every effect, its values and custom effects, and the connector effects example.