documentationfor yFiles for HTML 2.6

Port Placement

The points where an edge connects to its source and target nodes are called ports. In some applications they are of importance and have a fixed position, sometimes even a visual representation. In other applications they can be completely ignored. Similarly, some layout algorithms take care to distribute the ports in a way which fits their criteria best, whereas other algorithms simply place them in the middle of the node.

Example Diagrams with and without restricted port locations
Logic gates: ports have fixed positions and are not altered by the layout algorithm
Social network: ports are placed by the layout algorithm

Internally, the layout algorithms are not aware of ports. They might place the start and end points of edges, though. Further, these locations might be restricted or some rules can be defined to restrict them

In this section we describe how we can control the port placement of automatic layout algorithms. Note that the layout executor can already apply many rules with simple settings.

Automatic Port Handling by LayoutExecutor

While ports are part of the model in the view part the layout algorithms don’t handle them automatically. LayoutExecutor, however, provides some settings to make the layout algorithms aware of them.

Keeping the Port Location

Setting fixPorts to true keeps the location of the ports fixed (relative to the node layout). This can be used in cases where the ports are already placed in a fixed location on the nodes and should not be altered. Technically, if this mode is enabled the LayoutExecutor automatically creates strong port constraints at the current location.

This setting is ignored if port constraints are already registered.

This feature has no effect for layout algorithms which don’t support port constraints. For these algorithms a FixPortLocationStage has to be added. See also Using fixPorts with organic layout.

This feature is disabled by default

Effect of fixPorts
Before layout
Layout with fixPorts disabled. The edges are equally distributed, losing their port positions.
Layout with fixPorts enabled. The port locations are kept.

Grouping Edges which Connect to the Same Port

One single port can be source or target of an arbitrary number of edges. However, since the layout algorithms are not aware of ports they might place these points at different locations. Setting automaticEdgeGrouping to true automatically groups edges that are connected to the same port.

This setting is ignored if edge groups are already registered.

This feature is enabled by default.

Effect of automaticEdgeGrouping
automaticEdgeGrouping disabled (what the layout algorithm sees): The layout algorithm sees separate end points
automaticEdgeGrouping disabled (after applying to the original graph): Both edges are forced to start at the same port, messing up the layout
Layout with automaticEdgeGrouping enabled. The layout algorithm considers both edges starting from the same port

Placing Ports on the Node’s Outline

The layout algorithms treat nodes as rectangular objects whose bounds are defined by their layout. Therefore, the layout algorithms place the ports either at the border or the center of a node. Nodes, however, might be visualized by more complex shapes. Setting the portAdjustmentPolicy enables LayoutExecutor to place ports at the outline of a node. The first or last segment of the connecting edge will be prolonged or shortened to connect to that port.

This setting is ignored if strong port constraints are set.

This feature is disabled by default (i.e. set to NEVER).

Effect of portAdjustmentPolicy
Disabled (set to NEVER): ports placed on the node’s bounding box (red)
Enabled (set to ALWAYS): ports placed on the node’s outline (black)

Placing Port Labels

Since the layout algorithms are not aware of ports the automatic label placement is not aware of port labels, either. However, ports are bound to nodes and the end points of edges. Therefore, their labels can be mimicked either as node labels or as edge labels. LayoutExecutor automatically handles this conversion task. Since the best way to convert port labels might vary from port to port the policy for that is defined in an ItemMapping<TItem,TValue> for each single port. The mapping is set using property portLabelPolicies.

Restricting Port Locations

To restrict the location where an edge connects to its source or target node to specific locations during automatic layout, yFiles for HTML provides the concept of port candidates. This concept is implemented by the PortCandidate class. Port candidates can be used in conjunction with both nodes and edges.

Port Candidates for Nodes

When used in conjunction with nodes, port candidates provide a means to:

  • Restrict anchor locations at nodes. You can specify either exact anchor locations at a node (called location constraints or strong constraints) or restrict the anchor to any location at a specific side of a node (called side constraints or weak constraints).
  • Associate costs with a given anchor location. This establishes an order of precedence among a given set of anchor locations. Anchor locations with low cost are favored over anchor locations with higher costs.
  • Limit the number of connecting edges at a given anchor location.

A typical example for the use of port candidates is a flow diagram as shown in Using port candidates to control connection points: The diamond shape, which is the visualization of a switch, should have its incoming edge connecting at the top. The first outgoing edge should connect at the bottom (first image), the second at the right, the third at the left (second image). If there are more outgoing edges, these edges should connect at the bottom as well as more than one incoming edge should connect at the top (third image).

Using port candidates to control connection points
Incoming edges connect at the top, the first outgoing edge at the bottom…​
…​ more outgoing edges occupy the right and left corners…​
…​ when all corners are occupied

To define the set of side constraints and location constraints at a node, multiple port candidates can easily be combined using the services of the PortCandidateSet class. When a PortCandidate object is added to an instance of PortCandidateSet, the capacity of the port candidate can optionally be configured. The capacity of a given port candidate (sometimes also referred to as cardinality) specifies the allowed number of connecting edges at that side or anchor location.

Matching Port Candidates and Edges

Matching port candidates means the process of distributing a node’s edges to the available port candidates. If a node is associated with a set of port candidates via a PortCandidateSet object, all edges connecting to that node are distributed among the available port candidates with respect to:

  • the costs of a given port candidate
  • the number of edges that are allowed to connect to a given port candidate

For example, when the limit of allowed edges for a given port candidate with costs k is reached, i.e., the given port candidate is said to be saturated, then the next least expensive port candidate among the remaining ones is chosen to connect edges to.

Defining a port candidate set demonstrates how to create a candidate set for the diamond node shown in Using port candidates to control connection points: First, port candidates for the four corners of the diamond are defined. The number of connecting edges for these candidates is limited to 1.

Further port candidates, which can take an unlimited number (Integer.MAX_VALUE) of edges, are defined to handle any additional edges. To let the edges first connect to the corners before the additional candidates are occupied, a higher cost is associated with these latter candidates.

Defining a port candidate set
// define a PortCandidateSet
const pcs = new PortCandidateSet()

// the node has the size (30, 30) with the point (0, 0) at the center
// so the coordinates for the top corner are (0, -15)

// create a candidate at the top corner with direction North and cost 0
const candidate = PortCandidate.createCandidate(0, -15, PortDirections.NORTH, 0)
// add it to the set and allow only one edge to connect to it
pcs.add(candidate, 1)
// do the same for the other three corners
pcs.add(PortCandidate.createCandidate(0, 15, PortDirections.SOUTH, 0), 1)
pcs.add(PortCandidate.createCandidate(15, 0, PortDirections.EAST, 0), 1)
pcs.add(PortCandidate.createCandidate(-15, 0, PortDirections.WEST, 0), 1)

// to allow more edges to connect at the top and bottom
// create extra candidates and allow Integer.MAX_VALUE edges to connect

// to avoid that these candidates are occupied before the others
// associate a cost of 1 with them
pcs.add(PortCandidate.createCandidate(0, -15, PortDirections.NORTH, 1), Number.MAX_VALUE)
pcs.add(PortCandidate.createCandidate(0, 15, PortDirections.SOUTH, 1), Number.MAX_VALUE)

To influence the matching process, a subset of the PortCandidate objects used for a node can additionally be associated with the respective ports of its connecting edges. The subset then defines a restricted set of desired port candidates an edge prefers to connect to. The PortCandidate objects can be combined using ICollection<T> objects which are stored by means of layout data or data providers. The data providers are registered with the graph using the look-up keys SOURCE_PORT_CANDIDATE_COLLECTION_DP_KEY and TARGET_PORT_CANDIDATE_COLLECTION_DP_KEY.

Layout support for port candidates lists the layout algorithms that provide support for port candidates.

Layout support for port candidates
Layout Style Class Name Note
HierarchicalHierarchicLayoutHierarchicLayout supports port candidates as soon as they are set. See the description of incremental hierarchical layout for more information.
Polyline Edge RoutingEdgeRouterPolyline EdgeRouter supports port candidates as soon as they are set. See the description of polyline edge routing for more information.
Bus-style Edge RoutingBusRouterBusRouter supports port candidates as soon as they are set. See the description of bus-style edge routing for more information.

Declaring port candidates for a node is done by associating a corresponding PortCandidateSet object with the node via layout data or the data provider key NODE_PORT_CANDIDATE_SET_DP_KEY. All layout and edge routing styles that support port candidates respect these restrictions without further configuration.

Performing a layout with node port candidates
/**
 * @param {!IGraph} graph
 * @param {!PortCandidateSet} portCandidateSet
 */
function performLayoutWithNodePortCandidates(graph, portCandidateSet) {
  // use LayoutData to configure the port candidates
  const layoutData = new HierarchicLayoutData({
    nodePortCandidateSets: (node) =>
      node.tag === 'diamond' ? portCandidateSet : null
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}function performLayoutWithNodePortCandidates(graph: IGraph, portCandidateSet: PortCandidateSet): void {
  // use LayoutData to configure the port candidates
  const layoutData = new HierarchicLayoutData({
    nodePortCandidateSets: (node: INode) => (node.tag === 'diamond' ? portCandidateSet : null)
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}

Port Candidates for Edges

Similar to nodes, when used in conjunction with edges, port candidates provide a means to:

  • Restrict anchor locations at the source or target node of an edge. You can specify either exact anchor locations at a node (called location constraints or strong constraints) or restrict the anchor to any location at a specific side of a node (called side constraints or weak constraints).
  • Associate costs with a given anchor location. This establishes an order of precedence among a given set of anchor locations. Anchor locations with low cost are favored over anchor locations with higher costs.

Unlike node port candidates, there is no point in specifying the number of edges at an anchor location since edge port candidates are defined per single edge. Consequently, edge port candidates are combined with regular Collection objects instead of PortCandidateSet.

The following layout style supports edge port candidates:

Layout support for enhanced port candidates
Layout Style Class Name Note
HierarchicalHierarchicLayoutHierarchicLayout supports port candidates as soon as they are set. See the description of incremental hierarchical layout for more information.
PartialPartialLayout, ClearAreaLayoutBoth PartialLayout and ClearAreaLayout support port candidates for re-routed edges.

A special case is RecursiveGroupLayout which delegates to other layout algorithms and therefore supports port candidates if these do.

In particular, this scheme is supported by edge routing algorithms, and it allows to conveniently specify side constraints comprising two or three sides, for example.

Routing support for port candidates lists the edge routing algorithms that provide support for port candidates modeling enhanced port constraints.

Routing support for port candidates
Routing Style Class Name Note
Orthogonal / PolylineEdgeRouter, ChannelEdgeRouter, and BusRouterAll classes by default support port candidates as soon as they are set. See the descriptions of EdgeRouter, ChannelEdgeRouter, and BusRouter, for more information.

Declaring port candidates for the source or target of an edge is done by associating a corresponding Collection of PortCandidate objects with the edge via layout data or the data provider key SOURCE_PORT_CANDIDATE_COLLECTION_DP_KEY or TARGET_PORT_CANDIDATE_COLLECTION_DP_KEY, respectively. All layout and edge routing styles that support port candidates respect these restrictions without further configuration.

Performing a layout with edge port candidates
/**
 * @param {!IGraph} graph
 */
function performLayoutWithEdgePortCandidates(graph) {
  // blue edges should start near the right upper corner and end near the left upper corner
  const blueSourcePC = new List({
    items: [PortCandidate.createCandidate(15, -10, PortDirections.EAST)]
  })
  const blueTargetPC = new List({
    items: [PortCandidate.createCandidate(-15, -10, PortDirections.WEST)]
  })
  // green edges should start near the right lower corner and end near the left lower corner
  const greenSourcePC = new List({
    items: [PortCandidate.createCandidate(15, 10, PortDirections.EAST)]
  })
  const greenTargetPC = new List({
    items: [PortCandidate.createCandidate(-15, 10, PortDirections.WEST)]
  })

  // use LayoutData to apply the source and target port candidates
  const layoutData = new HierarchicLayoutData({
    sourcePortCandidates: (edge) =>
      edge.tag === 'blue'
        ? blueSourcePC
        : edge.tag === 'green'
          ? greenSourcePC
          : null,
    targetPortCandidates: (edge) =>
      edge.tag === 'blue'
        ? blueTargetPC
        : edge.tag === 'green'
          ? greenTargetPC
          : null
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}function performLayoutWithEdgePortCandidates(graph: IGraph): void {
  // blue edges should start near the right upper corner and end near the left upper corner
  const blueSourcePC = new List<PortCandidate>({
    items: [PortCandidate.createCandidate(15, -10, PortDirections.EAST)]
  })
  const blueTargetPC = new List<PortCandidate>({
    items: [PortCandidate.createCandidate(-15, -10, PortDirections.WEST)]
  })
  // green edges should start near the right lower corner and end near the left lower corner
  const greenSourcePC = new List<PortCandidate>({
    items: [PortCandidate.createCandidate(15, 10, PortDirections.EAST)]
  })
  const greenTargetPC = new List<PortCandidate>({
    items: [PortCandidate.createCandidate(-15, 10, PortDirections.WEST)]
  })

  // use LayoutData to apply the source and target port candidates
  const layoutData = new HierarchicLayoutData({
    sourcePortCandidates: (edge) => (edge.tag === 'blue' ? blueSourcePC : edge.tag === 'green' ? greenSourcePC : null),
    targetPortCandidates: (edge) => (edge.tag === 'blue' ? blueTargetPC : edge.tag === 'green' ? greenTargetPC : null)
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}
Layout with edge port candidates
A default hierarchic layout
…and a hierarchic layout with edge port candidates

Port Constraints

A port constraint pinpoints an edge’s end at its source node or target node. This concept is implemented by the PortConstraint class.

Port Constraints are superseded by Port Candidates which are more powerful and cover all use cases of port constraints. They are listed here for completeness.

There are two kinds of port constraints available:

Weak constraint
Determines the node’s side at which an edge path’s end should connect.
Strong constraint
Determines the exact coordinates where the edge path’s end should be located. The coordinates are interpreted relative to the node’s center.

In contrast to a strong port candidate, the anchor location of a strong port constraint is not specified as part of the port constraint instance but defined by the actual location of the port with such a constraint.

Layout support for port constraints lists the layout algorithms that provide support for port constraints.

Layout support for port constraints
Layout Style Class Name Note
HierarchicalHierarchicLayoutHierarchicLayout by default obeys weak and strong port constraints as soon as they are set. See the description of the hierarchical layout style for more information.
PartialPartialLayout, ClearAreaLayoutBoth PartialLayout and ClearAreaLayout obey weak and strong port constraints for re-routed edges.
TreeTreeLayoutNearly all of the predefined node placer implementations that can be used with the generic tree layout algorithm by default obey strong and weak port constraints as soon as they are set. See the description of generic tree layout for more information.

A special case is RecursiveGroupLayout: the algorithm converts port constraints into port candidates. Also, it delegates to other layout algorithms. Therefore, RecursiveGroupLayout supports port constraints if the applied layout algorithms support port candidates.

Routing support for port constraints lists the edge routing algorithms that provide support for port constraints.

Routing support for port constraints
Routing Style Class Name Note
Orthogonal / PolylineEdgeRouter, ChannelEdgeRouter, and BusRouterAll classes by default obey weak and strong port constraints as soon as they are set. See the descriptions of EdgeRouter, ChannelEdgeRouter, and BusRouter for more information.

Declaring port constraints for the source or target of an edge is done by associating a corresponding PortConstraint object with the edge via layout data or the data provider key SOURCE_PORT_CONSTRAINT_DP_KEY or TARGET_PORT_CONSTRAINT_DP_KEY, respectively. All layout and edge routing styles that support port constraints respect these restrictions without further configuration.

Performing a layout with port constraints
/**
 * @param {!IGraph} graph
 */
function performLayoutWithPortConstraints(graph) {
  // use LayoutData to apply the source and target port constraints
  const layoutData = new HierarchicLayoutData({
    // edges should start at the right side at blue nodes and at the left side at green nodes
    sourcePortConstraints: (edge) =>
      edge.sourceNode.tag === 'blue'
        ? PortConstraint.create(PortSide.EAST)
        : edge.sourceNode.tag === 'green'
          ? PortConstraint.create(PortSide.WEST)
          : null,
    targetPortConstraints: (edge) =>
      edge.targetNode.tag === 'blue'
        ? PortConstraint.create(PortSide.WEST)
        : edge.targetNode.tag === 'green'
          ? PortConstraint.create(PortSide.EAST)
          : null
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}function performLayoutWithPortConstraints(graph: IGraph): void {
  // use LayoutData to apply the source and target port constraints
  const layoutData = new HierarchicLayoutData({
    // edges should start at the right side at blue nodes and at the left side at green nodes
    sourcePortConstraints: (edge) =>
      edge.sourceNode!.tag === 'blue'
        ? PortConstraint.create(PortSide.EAST)
        : edge.sourceNode!.tag === 'green'
          ? PortConstraint.create(PortSide.WEST)
          : null,
    targetPortConstraints: (edge) =>
      edge.targetNode!.tag === 'blue'
        ? PortConstraint.create(PortSide.WEST)
        : edge.targetNode!.tag === 'green'
          ? PortConstraint.create(PortSide.EAST)
          : null
  })

  // and perform a layout
  const layout = new HierarchicLayout()
  graph.applyLayout(layout, layoutData)
}
Layout with port constraints
A default hierarchic layout
…and a hierarchic layout with port constraints

The Logic Gates demo application shows in detail how to use PortConstraint with the HierarchicLayout.

Restricting Ports for Layout Algorithms which Do Not Support Port Constraints

Some layout algorithms do not support strong port candidates. However, one might still want to keep the ports in a fixed location, e.g. by enabling fixPorts. For such cases a FixPortLocationStage can be prepended.

Using fixPorts with organic layout
// organic layout does not support port constraints
const coreLayout = new OrganicLayout({ preferredEdgeLength: 100 })

// We want to keep the ports at their current location, though
// Therefore, we prepend the FixPortLocationStage
// which applies PortConstraints after the actual layout
const layout = new FixPortLocationStage({
  coreLayout: coreLayout,
  keepCalculatedPorts: false
})

// enable FixPorts which internally adds
// strong port constraints at the current location
const ex = new LayoutExecutor({
  graphComponent: graphComponent,
  layout: layout,
  fixPorts: true
})
await ex.start()

// organic layout does not support port constraints
const coreLayout = new OrganicLayout({ preferredEdgeLength: 100 })

// We want to keep the ports at their current location, though
// Therefore, we prepend the FixPortLocationStage
// which applies PortConstraints after the actual layout
const layout = new FixPortLocationStage({ coreLayout: coreLayout, keepCalculatedPorts: false })

// enable FixPorts which internally adds
// strong port constraints at the current location
const ex = new LayoutExecutor({ graphComponent: graphComponent, layout: layout, fixPorts: true })
await ex.start()

In this example the port constraints are created automatically with fixPorts enabled. Individual port constraints can be applied, though, using FixPortLocationStageData's properties sourcePortConstraints and targetPortConstraints.

Using port constraints with organic layout
const layout = new FixPortLocationStage(
  new OrganicLayout({
    preferredEdgeLength: 100
  })
)
layout.keepCalculatedPorts = false

// create port constraints only for nodes which are no circles
const data = new FixPortLocationStageData({
  sourcePortConstraints: (edge) =>
    edge.sourceNode.style instanceof ShapeNodeStyle &&
    edge.sourceNode.style.shape !== ShapeNodeShape.ELLIPSE
      ? PortConstraint.create(PortSide.ANY, true)
      : null
})

await graphComponent.morphLayout(layout, '0.3s', data)

const layout = new FixPortLocationStage(
  new OrganicLayout({
    preferredEdgeLength: 100
  })
)
layout.keepCalculatedPorts = false

// create port constraints only for nodes which are no circles
const data = new FixPortLocationStageData({
  sourcePortConstraints: (edge) =>
    edge.sourceNode!.style instanceof ShapeNodeStyle && edge.sourceNode!.style.shape !== ShapeNodeShape.ELLIPSE
      ? PortConstraint.create(PortSide.ANY, true)
      : null
})

await graphComponent.morphLayout(layout, '0.3s', data)

Note that, technically, the ports are moved after the actual layout algorithm has finished. This might result in strange edge paths, especially if there are bends. In such cases it might help to set keepCalculatedPorts to true (the default). With this setting enabled a bend is inserted at the location where the layout algorithm places the port while the actual port is kept at its original location. For straight edge paths, however, it is better to set keepCalculatedPorts to false. In this mode the port location itself is set to the original location.

keepCalculatedPorts should be set depending on the layout algorithm. Usually algorithms which produce straight edges (like organic layout) should be used with keepCalculatedPorts set to false.