documentationfor yFiles for HTML 2.6

Writing a Custom Layout Stage

yFiles for HTML provides many flexible layout algorithms which arrange the graph elements in different styles. A lot of properties and customizable descriptors offer a maximum of flexibility. However, sometimes it is necessary to customize the predefined algorithms even more. In this case, developers can write a custom layout stage or even a complete layout algorithm.

A layout stage encapsulates arbitrary layout functionality, and provides a general means to string together multiple stages into a compound layout process. It is based on the interface ILayoutStage which extends ILayoutAlgorithm. Just like any other ILayoutAlgorithm its layout gets invoked with a call to applyLayout. In addition, it has a coreLayout property, which takes another ILayoutAlgorithm implementation. The coreLayout's applyLayout method is supposed to be invoked somewhere during the stage’s applyLayout method.

It is recommended to derive a custom layout stage from class LayoutStageBase. Its method applyLayoutCore should be invoked to safely call the coreLayout's applyLayout method. Safely means that if no coreLayout is set, applyLayoutCore silently returns, doing nothing.

yFiles for HTML already provides a large number of layout stages. Even the full-fledged layout styles and edge routers are implemented as layout stages, too. Therefore, these layout stages can be plugged together to realize complex layout processes tailored to your requirements.

Common Purposes of a Layout Stage

Layout stages can be used to add additional steps to the layout process. Generally, a layout stage performs preprocessing steps on the input graph before the core layouter’s invocation, and postprocessing steps thereafter.

Basics of a layout stage
class BasicLayoutStage extends LayoutStageBase {
  /**
   * @param {!LayoutGraph} graph
   */
  applyLayout(graph) {
    // preparation

    // core layout
    this.applyLayoutCore(graph)

    // postprocessing
  }
}class BasicLayoutStage extends LayoutStageBase {
  applyLayout(graph: LayoutGraph): void {
    // preparation

    // core layout
    this.applyLayoutCore(graph)

    // postprocessing
  }
}

A few examples (although only a small subset of possible applications are):

  • Temporarily adding group nodes or edges to emphasize relations between elements which are not represented by the graph structure
  • Temporarily hide elements which should not influence the overall layout
  • Post-Process coordinates, e.g. prolong edges from the node’s rectangular bounding box to the real outline of the node

The Custom Layout Stage demo shows different use cases for custom layout stages.

Example: Decorating a Graph

A typical use case for a custom layout stage is to add or hide some elements. In this example we hide some edges which do not represent structural information. This is usually achieved in three steps:

  1. Hiding (i.e. temporarily removing) these edges from the graph
  2. Applying the core layout
  3. un hiding the edges again. In this example, an edge router is applied exclusively to these edges

The following example shows how to achieve this.

Hide edges temporarily
class EdgeHidingStage extends LayoutStageBase {
  /** 
    A data provider key for specifying edges which do not provide structural information
  * @type {EdgeDpKey}
   */
  static get NonStructuralEdgesDpKey() {
    if (typeof EdgeHidingStage.$NonStructuralEdgesDpKey === 'undefined') {
      EdgeHidingStage.$NonStructuralEdgesDpKey = new EdgeDpKey(
        YBoolean.$class,
        EdgeHidingStage.$class,
        'NonStructuralEdgesDpKey'
      )
    }

    return EdgeHidingStage.$NonStructuralEdgesDpKey
  }

  /**
   * @param {!LayoutGraph} graph
   */
  applyLayout(graph) {
    // ------------------------------------------
    // Preparation
    // ------------------------------------------

    const edgesToHideDp = graph.getDataProvider(EdgeHidingStage.NonStructuralEdgesDpKey)
    if (edgesToHideDp === null) {
      // no data provider registered: simply run the core layout and return
      this.applyLayoutCore(graph)
      return
    }

    // hide all edges for which the data provider returns true
    const hider = new LayoutGraphHider(graph)
    hider.hide(new EdgeList(graph.edges.filter((e) => edgesToHideDp.getBoolean(e)).toArray()))

    try {
      // ------------------------------------------
      // Apply the core layout
      // ------------------------------------------
      this.applyLayoutCore(graph)
    } finally {
      // ------------------------------------------
      // Clean up
      // ------------------------------------------
      hider.unhideAll()

      // now route all previously hidden edges
      // re-use the stage's data provider key
      // to tell EdgeRouter which edges to route
      new EdgeRouter({
        affectedEdgesDpKey: EdgeHidingStage.NonStructuralEdgesDpKey,
        scope: EdgeRouterScope.ROUTE_AFFECTED_EDGES
      }).applyLayout(graph)
    }
  }
}class EdgeHidingStage extends LayoutStageBase {
  // A data provider key for specifying edges which do not provide structural information
  static readonly NonStructuralEdgesDpKey = new EdgeDpKey(
    YBoolean.$class,
    EdgeHidingStage.$class,
    'NonStructuralEdgesDpKey'
  )

  public applyLayout(graph: LayoutGraph) {
    // ------------------------------------------
    // Preparation
    // ------------------------------------------

    const edgesToHideDp = graph.getDataProvider(EdgeHidingStage.NonStructuralEdgesDpKey)
    if (edgesToHideDp === null) {
      // no data provider registered: simply run the core layout and return
      this.applyLayoutCore(graph)
      return
    }

    // hide all edges for which the data provider returns true
    const hider = new LayoutGraphHider(graph)
    hider.hide(new EdgeList(graph.edges.filter((e) => edgesToHideDp!.getBoolean(e)).toArray()))

    try {
      // ------------------------------------------
      // Apply the core layout
      // ------------------------------------------
      this.applyLayoutCore(graph)
    } finally {
      // ------------------------------------------
      // Clean up
      // ------------------------------------------
      hider.unhideAll()

      // now route all previously hidden edges
      // re-use the stage's data provider key
      // to tell EdgeRouter which edges to route
      new EdgeRouter({
        affectedEdgesDpKey: EdgeHidingStage.NonStructuralEdgesDpKey,
        scope: EdgeRouterScope.ROUTE_AFFECTED_EDGES
      }).applyLayout(graph)
    }
  }
}

Note that it is good practice to put the clean up step in a finally block.

In this example, it is assumed that there is a data provider which returns true for edges which should be hiddeb. When applying the layout on an IGraph or a GraphComponent the most convenient way to set up such a provider is to use a GenericLayoutData instance. This is shown in example Applying the custom layout on a GraphComponent:

Applying the custom layout on a GraphComponent
// create the layout stage with a hierarchic layout as core layout
const layout = new EdgeHidingStage()
layout.coreLayout = new HierarchicLayout()
// attach the data provider for edges to hide using GenericLayoutData
const data = new GenericLayoutData()
// tie the data provider using the well-known key
const edgesToHide = data.addEdgeItemCollection(EdgeHidingStage.NonStructuralEdgesDpKey)
// hide the selected edges
edgesToHide.source = graphComponent.selection.selectedEdges

// now apply the layout
graphComponent.morphLayout(layout, '0.5s', data)

Example: Pre-Processing

This example shows a pre-processing. Before the actual layout is done by the coreLayout some of the nodes are resized.

Modify the graph before the core layout
class SizeSettingLayoutStage extends LayoutStageBase {
  /** @type {NodeDpKey} */
  static get SizeDpKey() {
    if (typeof SizeSettingLayoutStage.$SizeDpKey === 'undefined') {
      SizeSettingLayoutStage.$SizeDpKey = new NodeDpKey(YNumber.$class, SizeSettingLayoutStage.$class, 'SizeDpKey')
    }

    return SizeSettingLayoutStage.$SizeDpKey
  }

  /** @type {NodeDpKey} */
  static set SizeDpKey(SizeDpKey) {
    SizeSettingLayoutStage.$SizeDpKey = SizeDpKey
  }

  /**
   * @param {!LayoutGraph} graph
   */
  applyLayout(graph) {
    // before the actual layout: adjust the size of the nodes
    const sizeProvider = graph.getDataProvider(SizeSettingLayoutStage.SizeDpKey)
    // only if the data provider is set
    if (sizeProvider != null) {
      for (const node of graph.nodes) {
        // only for nodes which have a value associated
        const size = sizeProvider.getNumber(node)
        if (size > 0) {
          graph.setSize(node, new YDimension(size, size))
        }
      }
    }

    this.applyLayoutCore(graph)
  }
}class SizeSettingLayoutStage extends LayoutStageBase {
  static SizeDpKey = new NodeDpKey(YNumber.$class, SizeSettingLayoutStage.$class, 'SizeDpKey')

  applyLayout(graph: LayoutGraph): void {
    // before the actual layout: adjust the size of the nodes
    const sizeProvider = graph.getDataProvider(SizeSettingLayoutStage.SizeDpKey)
    // only if the data provider is set
    if (sizeProvider != null) {
      for (const node of graph.nodes) {
        // only for nodes which have a value associated
        const size = sizeProvider.getNumber(node)
        if (size > 0) {
          graph.setSize(node, new YDimension(size, size))
        }
      }
    }

    this.applyLayoutCore(graph)
  }
}

In this example it is assumed that there is a data provider which returns the size of the node. For simplicity this is a number which is used for both width and height of the node. If no value is set (default: 0) the current size is not changed.

Providing Data for Custom Layout Stages

Most of the above examples need custom data. It is usual and to provide these data in a data provider. Defining data providers for the layout graph is discussed in section Binding Data to Graph Elements.

If the layout is applied on an IGraph or a GraphComponent as shown in Applying an Automatic Layout setting up data for custom stages is facilitated using class GenericLayoutData.

addNodeItemCollection(dpKey: NodeDpKey<boolean>, itemCollection: ItemCollection<INode>): ItemCollection<INode>
addEdgeItemCollection(dpKey: EdgeDpKey<boolean>, itemCollection: ItemCollection<IEdge>): ItemCollection<IEdge>
addLabelItemCollection(dpKey: ILabelLayoutDpKey<boolean>, itemCollection: ItemCollection<ILabel>): ItemCollection<ILabel>
Creates an ItemCollection<TItem> for the given data provider key.
addNodeItemMapping<TValue>(dpKey: NodeDpKey<TValue>, itemMapping: ItemMapping<INode, TValue>): ItemMapping<INode, TValue>
addEdgeItemMapping<TValue>(dpKey: EdgeDpKey<TValue>, itemMapping: ItemMapping<IEdge, TValue>): ItemMapping<IEdge, TValue>
addLabelItemMapping<TValue>(dpKey: ILabelLayoutDpKey<TValue>, itemMapping: ItemMapping<ILabel, TValue>): ItemMapping<ILabel, TValue>
Creates an ItemMapping<TItem,TValue> for the given data provider key.

Using GenericLayoutData
// an example data provider key for an ItemCollection:
// the corresponding provider returns true for each node which is contained in the collection
const IncludedNodesDpKey = new NodeDpKey(YBoolean.$class, ExampleStage.$class, 'IncludedNodesDpKey')

// an example data provider key for an ItemMapping:
// the corresponding provider returns a number for each edge
const EdgeWeightDpKey = new EdgeDpKey(YNumber.$class, ExampleStage.$class, 'EdgeWeightDpKey')

// generate generic data
const genericData = new GenericLayoutData()

// add an ItemCollection for IncludedNodesDpKey
const includedNodes = genericData.addNodeItemCollection(IncludedNodesDpKey)
includedNodes.delegate = (node) => 'IncludeMe' === node.tag

// add an ItemMapping for EdgeWeightDpKey
const edgeWeights = genericData.addEdgeItemMapping(EdgeWeightDpKey)
edgeWeights.mapper.set(edge1, 10)
edgeWeights.mapper.set(edge2, 20)

graphComponent.morphLayout(layout, '0.5s', genericData)

// an example data provider key for an ItemCollection:
// the corresponding provider returns true for each node which is contained in the collection
const IncludedNodesDpKey = new NodeDpKey(YBoolean.$class, ExampleStage.$class, 'IncludedNodesDpKey')

// an example data provider key for an ItemMapping:
// the corresponding provider returns a number for each edge
const EdgeWeightDpKey = new EdgeDpKey(YNumber.$class, ExampleStage.$class, 'EdgeWeightDpKey')

// generate generic data
const genericData = new GenericLayoutData()

// add an ItemCollection for IncludedNodesDpKey
const includedNodes = genericData.addNodeItemCollection(IncludedNodesDpKey)
includedNodes.delegate = (node: INode) => 'IncludeMe' === node.tag

// add an ItemMapping for EdgeWeightDpKey
const edgeWeights = genericData.addEdgeItemMapping(EdgeWeightDpKey)
edgeWeights.mapper.set(edge1, 10)
edgeWeights.mapper.set(edge2, 20)

graphComponent.morphLayout(layout, '0.5s', genericData)