documentationfor yFiles for HTML 2.6

The LayoutGraph API - Part 2: Layout

The type LayoutGraph extends Graph and adds positional and dimensional information to the graph model. In particular, this means the location and size of a node, specified by the type INodeLayout, and the location of the source port, target port, and the control points of the path of an edge, specified by type IEdgeLayout. This extended graph type is used by the automatic layouts of yFiles.

Nodes

Interface INodeLayout is used to add the layout information to a node.

This information consists of the coordinates for the upper left corner of the node, and its width and height.

The following convenience methods defined by abstract class LayoutGraph can be used to control the layout information for nodes:

getCenter(node: Node): YPoint
getLocation(node: Node): YPoint
getHeight(node: Node): number
getWidth(node: Node): number
Getter methods for nodes.
setCenter(node: Node, position: YPoint): void
setLocation(node: Node, position: YPoint): void
setSize(node: Node, width: number, height: number): void
Setter methods for nodes.

Setting a node’s layout
// getting a node's bounding box
const nl = graph.getLayout(node)
const x = nl.x
const y = nl.y
const width = nl.width
const height = nl.height

// set the top left corner of the node
graph.setLocation(node, x + 10, y + 10)

// set the center of the node
graph.setCenter(node, 20, 20)

// set the size of the node
graph.setSize(node, width * 2, height * 2)

Edges

Interface IEdgeLayout is used to add the layout information to an edge.

This information consists of the coordinates for both starting point and end point of the edge path, as well as the coordinates for the control points in-between these two.

The coordinates for the edge’s end points are relative to the center coordinates of the edge’s source node and target node, respectively. The coordinates for the control points, in contrast, are absolute.

The following convenience methods defined by abstract class LayoutGraph can be used to control the layout information for edges. Note that the path list for an edge includes its source port, all control points, and also its target port. In this case the source and target port locations are absolute coordinates. The point list, though, holds only the edge’s control points.

Control points are also known as bends.

getPathList(edge: Edge): YList
getPointList(edge: Edge): YList
getSourcePointAbs(edge: Edge): YPoint
getTargetPointAbs(edge: Edge): YPoint
Getter methods for edges.
setPath(edge: Edge, path: YList): void
setPoints(edge: Edge, points: YList): void
setSourcePointAbs(edge: Edge, point: YPoint): void
setTargetPointAbs(edge: Edge, point: YPoint): void
Setter methods for edges.

Setting the edge layout
const edge = graph.createEdge(node1, node2)

// set the edge's source point at the top center of node1
const topCenterAbsolute = new YPoint(graph.getCenterX(node1), graph.getY(node1))
graph.setSourcePointAbs(edge, topCenterAbsolute)

// set the edge's target point at the bottom center of node2
// note that SetSourcePointRel or SetTargetPointRel set the point
// relative to the *center* of the node
const bottomCenterRelative = new YPoint(0, graph.getHeight(node2) / 2)
graph.setTargetPointRel(edge, bottomCenterRelative)

// now set all bends (the path points)
const pointList = new YList([
  new YPoint(graph.getCenterX(node), graph.getY(node1) - 100),
  new YPoint(graph.getX(node1), graph.getY(node1) - 100),
  new YPoint(graph.getX(node1), graph.getY(node2) + graph.getWidth(node2) + 100),
  new YPoint(graph.getCenterX(node2), graph.getY(node2) + graph.getWidth(node2) + 100)
])

Labels

Labels in a LayoutGraph are represented by implementations of the interface ILabelLayout interface. More precisely, an INodeLabelLayout defines properties for node labels and an IEdgeLabelLayout defines properties for edge labels.

To create, add, and remove labels from a LayoutGraph, use the ILabelLayoutFactory. The factory can be obtained by calling LayoutGraphUtilities.getLabelFactory. It is bound to a specific graph instance and can be used to create, add, and remove both node and edge labels in that graph. Adding labels to a LayoutGraph shows how the factory is used to create and add labels.

Adding labels to a LayoutGraph
const graph = getMyLayoutGraph()

// create the label factory
const labelLayoutFactory = LayoutGraphUtilities.getLabelFactory(graph)

// create a node label with a discrete model...
const node = graph.firstNode
const nodeLabelLayoutModel = new DiscreteNodeLabelLayoutModel(DiscreteNodeLabelPositions.INTERNAL_MASK)
const nodeLabel = labelLayoutFactory.createLabelLayout(
  node,
  new YOrientedRectangle(0, 0, 80, 20),
  nodeLabelLayoutModel
)
// ... and add it to the node
labelLayoutFactory.addLabelLayout(node, nodeLabel)

// create an edge label with a slider model...
const edge = graph.firstEdge
const edgeLabelLayoutModel = new SliderEdgeLabelLayoutModel(SliderMode.SIDE)
const edgeLabel = labelLayoutFactory.createLabelLayout(
  edge,
  new YOrientedRectangle(0, 0, 80, 20),
  edgeLabelLayoutModel,
  PreferredPlacementDescriptor.newSharedInstance(LabelPlacements.LEFT_OF_EDGE)
)
// ... and add it to the edge
labelLayoutFactory.addLabelLayout(edge, edgeLabel)
const graph: LayoutGraph = getMyLayoutGraph()

// create the label factory
const labelLayoutFactory = LayoutGraphUtilities.getLabelFactory(graph)

// create a node label with a discrete model...
const node = graph.firstNode!
const nodeLabelLayoutModel = new DiscreteNodeLabelLayoutModel(DiscreteNodeLabelPositions.INTERNAL_MASK)
const nodeLabel = labelLayoutFactory.createLabelLayout(
  node,
  new YOrientedRectangle(0, 0, 80, 20),
  nodeLabelLayoutModel
)
// ... and add it to the node
labelLayoutFactory.addLabelLayout(node, nodeLabel)

// create an edge label with a slider model...
const edge = graph.firstEdge!
const edgeLabelLayoutModel = new SliderEdgeLabelLayoutModel(SliderMode.SIDE)
const edgeLabel = labelLayoutFactory.createLabelLayout(
  edge,
  new YOrientedRectangle(0, 0, 80, 20),
  edgeLabelLayoutModel,
  PreferredPlacementDescriptor.newSharedInstance(LabelPlacements.LEFT_OF_EDGE)
)
// ... and add it to the edge
labelLayoutFactory.addLabelLayout(edge, edgeLabel)

Defining label positions: Label Models

A label placement algorithm uses the label model that is associated with a label to get the available candidate positions. From this set of candidates it then chooses one that best matches the label position it has calculated. The label model’s model parameter is then used to encode this position.

The label models discussed here are based on interfaces INodeLabelLayoutModel and IEdgeLabelLayoutModel.

It is important to understand that the result of a labeling algorithm is one model parameter per processed label, which is directly stored with the label after calculation. This model parameter expresses the calculated label position with respect to the corresponding label model, and it is only valid in the context of this label model. To get the actual location of a label after the labeling algorithm has finished, both model parameter and corresponding label model are necessary.

It is also important to understand that the labeling algorithm can only place labels on positions which are supported by the label model. The label models DiscreteNodeLabelLayoutModel and DiscreteEdgeLabelLayoutModel support only a small set of locations. This might prevent the algorithm from removing overlaps or finding a good position. For automatic label placement it is recommended to use a label model which supports a free placement or a free placement along an edge, e.g. FreeNodeLabelLayoutModel for nodes and FreeEdgeLabelLayoutModel or SliderEdgeLabelLayoutModel for edges.

Preferred placement of a Label

The following property defined in interface IEdgeLabelLayout returns the PreferredPlacementDescriptor instance that is associated with an edge label:

preferredPlacementDescriptor

By default, the PreferredPlacementDescriptor instance that is associated with actual implementations of interface IEdgeLabelLayout, like, e.g., instances of class EdgeLabelLayoutImpl, is immutable. The immutability is a direct consequence of the descriptor instance being shared among newly created edge labels.

Every attempted assignment to a property of such an immutable PreferredPlacementDescriptor instance will yield an exception.

To properly configure individual preferred placement options for an edge label, a dedicated descriptor instance needs to be associated with the label. The following code snippet shows how this can be achieved:

Creating and associating a new PreferredPlacementDescriptor to configure individual preferred placement options for an edge label
const labelLayoutFactory = LayoutGraphUtilities.getLabelFactory(layoutGraph)
const edgeLabelLayoutModel = new SliderEdgeLabelLayoutModel(SliderMode.SIDE)

labelLayoutFactory.addLabelLayout(
  edge,
  labelLayoutFactory.createLabelLayout(
    edge,
    new YOrientedRectangle(0, 0, 80, 20),
    edgeLabelLayoutModel,
    PreferredPlacementDescriptor.newSharedInstance(LabelPlacements.LEFT_OF_EDGE)
  )
)

Using Buffered Layout

This section applies only to yFiles layout packages!The IGraph-based adapter mechanism already creates the layout graph as copy of the original IGraph and thus provides the same conceptual benefits as buffered layout. Therefore, class BufferedLayout must not be used with that mechanism.

With the yFiles for HTML layout algorithms it is possible to have a graph layout calculated using two different approaches, namely unbuffered layout or buffered layout.

Unbuffered layout means to directly invoke a layout algorithm’s applyLayout method. Choosing this approach, the layout calculation is performed on the given graph, and is also immediately assigned.

Buffered layout, in contrast, utilizes class BufferedLayout, which creates a copy of the original graph that is then used for layout calculation.

Unbuffered layout has some severe drawbacks that should be observed:

  • A layout algorithm might perform badly in terms of memory consumption and execution time, due to the implementation of the graph structure. Crucial graph methods might not be optimized for layout tasks.
  • Some layout algorithms need to temporarily add/remove nodes or edges to/from the given graph. Any registered graph listeners will be notified about such structural changes, which subsequently might result in unnecessary or even harmful action on a listener’s behalf.
  • Even though it is guaranteed that a layout algorithm will not change a graph’s node set and edge set, it is not unusual that the ordering of nodes and/or edges is modified.

Consequently, it is not safe to rely on the index() feature of nodes or edges.

  • In rare cases it might happen that a layout algorithm will crash during a calculation (due to a bug, for example). It will then return immediately and generate an exception. The input graph will be left in an intermediate, often broken state and no recovery will be possible for it.
  • Directly invoking a layout algorithm’s applyLayout method will not return the calculated coordinates, but instead assign them right away to the given graph.

Consequently, any other way of coordinate assignment, e.g., in an animated fashion using coordinate interpolation, is defeated.

With these drawbacks in mind, it is almost always a good idea to choose buffered layout instead. It facilitates many sophisticated features, like, e.g., layout morphing, and at the same time increases an application’s robustness.

Class BufferedLayout

The main purpose of class BufferedLayout is to create a copy of the input graph before calling its core layout algorithm. The graph structure that is used for the copied graph is optimized for layout calculation.

Class BufferedLayout must not be used with the IGraph-based adapter mechanisms. Due to its nature, this mechanism already creates the layout graph as copy of the original IGraph

The core layout algorithm subsequently executes on the copy and calculates a new layout, which is then transferred to the original graph. There are several beneficial aspects of this functionality:

  • The structure of the input graph is guaranteed to not change at all.

Usually, layout providers (i.e., ILayoutAlgorithm implementations) make no guarantees on leaving the sequence of nodes or edges unchanged, which may result in unexpected side effects. One such side effect is, for example, that a layout algorithm may assign completely different layouts to a graph when being invoked twice on the same graph.

The reason for such behavior is that a layout provider’s output in general depends on the sequence of elements in the graph, but this sequence has changed with the first layout invocation.

  • Calculating a layout on a copy instead of the original graph proves to be more robust. Even if there should occur an unrecoverable error in the layout process, class BufferedLayout guarantees that the structure of the input graph remains consistent.

Wrapping a layout algorithm with a BufferedLayout layout stage is as easy as shown in Using buffered layout (LayoutGraph API).

Using buffered layout (LayoutGraph API)
const graph = getMyLayoutGraph()

// Run organic layout by implicitly wrapping its invocation using the services
// of class BufferedLayout.
new BufferedLayout(new OrganicLayout()).applyLayout(graph)
const graph: LayoutGraph = getMyLayoutGraph()

// Run organic layout by implicitly wrapping its invocation using the services
// of class BufferedLayout.
new BufferedLayout(new OrganicLayout()).applyLayout(graph)