Concepts

The task of a layout algorithm is a major undertaking that involves arbitrarily complex logic. However, there can be identified a number of well-defined (sub-)tasks that even completely different algorithms do in a similar manner. Factoring out such tasks so that they can be reused in varying contexts, greatly reduces the complexity of any layout algorithm.

The yFiles library allows to formulate complex layout processes by plugging together so-called "layout stages" that, among other things, can be used to handle such well-defined (sub-)tasks.

The Layout Stages Concept

A layout stage serves as a kind of container that encapsulates arbitrary layout functionality, and provides a general means to string together multiple stages into a compound layout process. Interface LayoutStage defines the basis for a layout stage. It is a specialization of interface Layouter, and thus can be used as a stand-alone layout provider as well as a part of a larger layout process.

Figure 5.6. LayoutStage

LayoutStage.

The methods of interface LayoutStage are used to establish a relationship to another Layouter implementation, the so-called "core layouter." The core layouter's invocation is entirely bracketed by the layout stage's logic.

When used in the context of a larger layout process, the layout stage can easily be used to simplify the core layouter's task. It performs preprocessing steps on the input graph before the core layouter's invocation, and postprocessing steps thereafter.

Table 5.1, “Predefined layout stages” lists some of the predefined LayoutStage implementations, most of them being part of the default compound layout process as described below. Further layout stages are described in the section called “Layout Stages”.

Table 5.1. Predefined layout stages

Classname Description
BufferedLayouter Duplicates the graph to be processed, so that the layout cannot destroy the original data. See also the description of buffered layout calculation below.
SelfLoopLayouter Calculates orthogonal edge paths for a graph's self-loops (reflexive edges).
ParallelEdgeLayouter Calculates edge paths for all edges with identical source node and target node. (Self-loops are not processed.)
OrientationLayouter Changes the orientation of a computed layout.
SubgraphLayouter Reduces the original graph to the subgraph that is induced by selected nodes.

Figure 5.7, “Layout stages complex” shows the class hierarchy for the layout stages.

Figure 5.7. Layout stages complex

Layout stages complex.

Default Compound Layout Process

Except for class BufferedLayouter, all layout stages from Table 5.1, “Predefined layout stages” are part of the compound layout process defined by CanonicMultiStageLayouter. The following methods of class CanonicMultiStageLayouter can be used for configuring the layout process:

Using Buffered Layout

With the yFiles 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 doLayout method. Choosing this approach, the layout calculation is performed on the given graph, and is also immediately assigned. Buffered layout, in contrast, utilizes class BufferedLayouter, 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. For instance, invoking a layout algorithm directly on an instance of Graph2D is almost never a good idea, since its nodes and edges are rather "heavy."
  • 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 doLayout() 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 BufferedLayouter

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

The core layouter 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., Layouter 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 layouter 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.
  • Instead of immediately writing back the calculated layout to the given input graph, class BufferedLayouter provides the possibility to return the result as a GraphLayout object, leaving the original graph's layout unmodified. The GraphLayout object then allows to defer coordinate assignment to a later point in time, for example, using class LayoutTool's applyGraphLayout method. By means of class LayoutMorpher, it can also be used to nicely "morph" from one layout to another in an animated fashion. (See the section called “Layout Morphing”.)
  • 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 BufferedLayouter guarantees that the structure of the input graph remains consistent.

Wrapping a layout algorithm with a BufferedLayouter layout stage is as easy as shown in Example 5.3, “Using buffered layout”.

Example 5.3. Using buffered layout

// 'graph' is of type y.layout.LayoutGraph. 

// Run organic layout by implicitly wrapping its invocation using the services 
// of class BufferedLayouter. 
new BufferedLayouter(new SmartOrganicLayouter()).doLayout(graph);

Alternatively, class BufferedLayouter allows to get the calculated graph layout as a separate object. This is demonstrated in Example 5.4, “Buffered layout with deferred coordinate assignment”.

Example 5.4. Buffered layout with deferred coordinate assignment

// 'graph' is of type y.layout.LayoutGraph. 

// Run organic layout by implicitly wrapping its invocation using the services 
// of class BufferedLayouter. 
// The result of the layout is returned separately as a GraphLayout object, 
// i.e., the original graph's layout information is not changed. 
GraphLayout gl = 
    new BufferedLayouter(new SmartOrganicLayouter()).calcLayout(graph);

Providing Supplemental Layout Data

The yFiles layout algorithms support advanced functionality that can take into account even individual information for single graph elements. However, individual setup like, e.g., attaching a preferred edge length to each edge, is beyond the means a graph structure provides. Instead, the data accessor concept as described in the section called “Binding Data to Graph Elements” is utilized to provide the individual setup as supplemental information to a layout algorithm.

The supplemental data for the graph elements is bound to the graph using a so-called "data provider key." During layout calculation, the algorithm then retrieves the data provider that contains the data from the graph by using the same key.

Depending on the data provider key, the algorithm expects the returned data provider to hold values of specific type for either nodes or edges of the graph. Example 5.5, “Binding supplemental data” shows the use of an implicit data provider that returns an integral value for each edge indicating the edge's preferred length to the organic layout algorithm.

Example 5.5. Binding supplemental data

// 'graph' is of type y.layout.LayoutGraph. 

// Create an implicit data provider and register it with the graph using a 
// so-called data provider look-up key. 
// The key is then used by the layout algorithm to retrieve the supplemental 
// data. 
graph.addDataProvider(SmartOrganicLayouter.PREFERRED_EDGE_LENGTH_DATA,
                      new DataProviderAdapter() {
  public int getInt(Object o) {
    return (int)(200 * getLength((Edge)o));
  }
});

// Invoke organic layout on the graph. 
SmartOrganicLayouter sol = new SmartOrganicLayouter();
sol.doLayout(graph);

// Remove the data provider from the graph. 
graph.removeDataProvider(SmartOrganicLayouter.PREFERRED_EDGE_LENGTH_DATA);