documentationfor yFiles for HTML 2.6

Applying an Automatic Layout

Although the yFiles for HTML layout algorithms are tailored to the dedicated graph model of the layout part, the layouts can be directly applied to an IGraph by means of the adapter mechanisms described in this section. You don’t need to work with the layout graph model unless you implement your own ILayoutAlgorithm.

This section shows how to apply an automatic layout to an IGraph. It does not apply to the yFiles for HTML Layout package which doesn’t provide the IGraph model. Developers who write applications with the yFiles for HTML Layout package should refer to the chapter Customizing Automatic Layout to learn how to use the LayoutGraph API.

A layout can be applied directly to an IGraph or a GraphComponent’s graph with the methods applyLayout and morphLayout, respectively. These methods take care of creating a layout graph copy of the original IGraph, calculate the layout on that copy, and finally transfer the new layout back to the original graph. Besides obvious characteristics of the graph like nodes, edges, and labels, more specific features like groups, tables, and swimlanes are automatically converted as well.

To adopt the new layout without delay as soon as its calculation has finished, apply a layout algorithm to an IGraph using the applyLayout method. This is a synchronous call.

Applying an automatic layout
graph.applyLayout(new HierarchicLayout())

To adopt the new layout in an animated fashion, apply a layout algorithm to a GraphComponent (and its IGraph) using the morphLayout method. We call this layout morphing. Method morphLayout is a non-blocking, asynchronous call. morphLayout returns a Promise which is fulfilled after both the layout calculation and the animation to the new layout have finished, and rejected if an error occurred.

Applying an automatic layout in an animated fashion
// using async/await:
let runningLayout = true
try {
  await graphComponent.morphLayout({
    layout: new HierarchicLayout(),
    morphDuration: '0.2s'
  })
} catch (error) {
  // report error
  alert(error)
} finally {
  // free resources
  runningLayout = false
}

// alternatively simply apply the layout in an animated fashion, ignoring the Promise...
graphComponent.morphLayout({
  layout: new HierarchicLayout(),
  morphDuration: TimeSpan.fromMilliseconds(250)
})

Both these methods provide overloads to specify supplemental layout data.

The animation done by the morphLayout method zooms the viewport to fit the graph bounds. To configure this or other aspects of the layout morphing, use the LayoutExecutor class instead of the morphLayout method. See section Configuring the Layout Morphing for more details.

Both the applyLayout method and the morphLayout method, use the LayoutExecutor class to calculate the layout. However, build optimizers like tree-shaking tools do not recognize such implicit dependencies and might remove the yFiles for HTML modules that contain the implicit dependencies. To avoid this for class LayoutExecutor, use the static Class.ensure method to create an explicit dependency to the class and its module:

Class.ensure(LayoutExecutor)

Layout Data

Besides global configuration settings that specify the overall arrangement of the graph, the layout algorithms can take into account individual information for single graph elements. For example, this includes specifying a minimum edge length for each edge or a preferred location for each label.

This individual information is neither specified on the ILayoutAlgorithm instance nor directly on the graph. Instead, each layout style and each edge routing style has an associated type that provides the element-level options for the layout. These types are called layout data, they extend the LayoutData type.

To take the individual data into consideration, the layout algorithm and the layout data have to be applied together to the graph or GraphComponent:

Applying an automatic layout and layout data in an animated fashion
const layout = new HierarchicLayout()
// configure layout algorithm options ...

const layoutData = new HierarchicLayoutData()
// configure graph specific options ...

// apply both layout and layout data
await graphComponent.morphLayout({ layout, layoutData })

During layout calculation, the individual information for the graph elements is bound to the layout graph using a so-called data provider key. For historical reasons the descriptions of the layout styles often mention these keys instead of the corresponding properties of the newer LayoutData concept. Nevertheless, we strongly recommend to use the LayoutData instead of the data provider keys.

The properties of the various layout specific LayoutData subclasses are commonly either of type ItemMapping<TItem,TValue>, ItemCollection<TItem> or SingleItem<TItem>. The first maps an element (commonly a graph element) to an arbitrary object. The other two define a collection of items or a single item for use in the layout algorithm. This is a convenient and type safe means to add collections and maps to your layout configuration.

ItemMapping
constant: TValue
Sets a single constant on this mapper that is returned for every given element.
delegate: MapperDelegate<TItem, TValue>
Sets a function that is called for each given element which will produce an object of the result type of this instance.
mapper: IMapper<TItem, TValue>
Sets a map containing the actual mapping from the elements to the values.
ItemCollection
items: ICollection<TItem>
Sets the items that this instance should contain.
source: IEnumerable<TItem>
Sets the items that this instance should contain.
delegate: Predicate<TItem>
Sets a predicate that is called for each given element whether this element is contained in this collection.
mapper: IMapper<TItem, boolean>
Sets a map that defines boolean values for each element whether this element is contained in this collection.
SingleItem
item: TItem
Sets the item that this instance should contain.
delegate: Predicate<TItem>
Sets a predicate that is called for each given element. The first item matching the predicate will be used.
source: IEnumerable<TItem>
Sets an IEnumerable<T> whose first item will be used.

For all three types, type conversion allows you to omit these intermediate properties and directly assign the property of the actual LayoutData.

For example, the following code snippet configures a LayoutData object for HierarchicLayout:

Example for a custom LayoutData
/**
 * @returns {!HierarchicLayoutData}
 */
function initLayoutData() {
  // create a new instance of layout data for the hierarchic layout
  return new HierarchicLayoutData({
    // as an example, set the edge thickness for all edges to a small value
    edgeThickness: 0.5,

    // next, we set up node halos for nodes that have a color set as tag
    nodeHalos: (node) =>
      node.tag !== null ? NodeHalo.create(15) : NodeHalo.ZERO_HALO,

    // In this example, we define port constraints for colored nodes:
    // Blue nodes have all their incoming and outgoing edges on the east, and red nodes on the west.
    sourcePortConstraints: (edge) => getPortConstraintForEdge(edge, true),
    targetPortConstraints: (edge) => getPortConstraintForEdge(edge, false)
  })
}

/**
 * @param {!IEdge} edge
 * @param {boolean} source
 * @returns {!PortConstraint}
 */
function getPortConstraintForEdge(edge, source) {
  const color = source ? edge.sourceNode.tag : edge.targetNode.tag
  return color === Fill.RED
    ? PortConstraint.create(PortSide.WEST)
    : color === Fill.BLUE
      ? PortConstraint.create(PortSide.EAST)
      : PortConstraint.create(PortSide.ANY)
}function initLayoutData(): HierarchicLayoutData {
  // create a new instance of layout data for the hierarchic layout
  return new HierarchicLayoutData({
    // as an example, set the edge thickness for all edges to a small value
    edgeThickness: 0.5,

    // next, we set up node halos for nodes that have a color set as tag
    nodeHalos: (node) => (node.tag !== null ? NodeHalo.create(15) : NodeHalo.ZERO_HALO),

    // In this example, we define port constraints for colored nodes:
    // Blue nodes have all their incoming and outgoing edges on the east, and red nodes on the west.
    sourcePortConstraints: (edge: any): PortConstraint => getPortConstraintForEdge(edge, true),
    targetPortConstraints: (edge: any): PortConstraint => getPortConstraintForEdge(edge, false)
  })
}

function getPortConstraintForEdge(edge: IEdge, source: boolean): PortConstraint {
  const color: Fill = source ? edge.sourceNode!.tag : edge.targetNode!.tag
  return color === Fill.RED
    ? PortConstraint.create(PortSide.WEST)
    : color === Fill.BLUE
      ? PortConstraint.create(PortSide.EAST)
      : PortConstraint.create(PortSide.ANY)
}

This configuration changes the edge thickness, node halos and port constraints for various nodes with only a few lines of code. The LayoutData tells the layout algorithm to consider the edges as a bit thicker than usual, to provide node halos for nodes that are colored red or blue and additionally that it should arrange the nodes and route the edges such that blue nodes always have all incoming and outgoing edges on the east side and red nodes always on the west side. The effect of this configuration changes the resulting layout significantly with only a few lines of code, as shown in Layout Data:

A graph laid out with a standard HierarchicLayout.
A graph laid out with the same layout algorithm and the layout data defined in Example for a custom LayoutData.

Combining Layout Data

For some use-cases, it is required to apply several layout algorithms at once. For example, first use the organic layout to place the nodes and then apply an edge routing algorithm as a post-processing step to obtain orthogonal instead of straight-line edge routes.

For such cases, we may also have to combine multiple LayoutData instances, which can be realized by means of class CompositeLayoutData. Note that the LayoutData are applied in the order as they appear in the Items collection.

Configuring the Layout Morphing

To configure and customize either the creation of the layout graph or the animation from old to new layout, you can apply the layout using the LayoutExecutor or LayoutExecutorAsync class instead of using the morphLayout method. With respect to animation, both of these classes have the same interface.

Using the LayoutExecutor class to customize the layout morphing of an automatic layout
const layout = new HierarchicLayout()
// configure layout algorithm options ...

const layoutData = new HierarchicLayoutData()
// configure graph specific options ...

const layoutExecutor = new LayoutExecutor({
  graphComponent,
  layout,
  layoutData
})
// configure the LayoutExecutor ...
await layoutExecutor.start()
const layout = new HierarchicLayout()
// configure layout algorithm options ...

const layoutData = new HierarchicLayoutData()
// configure graph specific options ...

const layoutExecutor = new LayoutExecutor({ graphComponent, layout, layoutData })
// configure the LayoutExecutor ...
await layoutExecutor.start()

The following options affect the layout animation:

duration
The duration of the animation. A duration of 0 disables the animation. Instead, the new layout is adopted without delay as soon as its calculation has finished.
animateViewport
Specifies whether to animate the viewport.
easedAnimation
Specifies whether to use eased animation.
updateContentRect
Specifies whether the content rectangle of the GraphComponent should be updated upon completion.

The following options affect the way the layout graph is created:

automaticEdgeGrouping
Specifies whether edge groups are automatically created for edges that are connected to the same port. See section Automatic Port Handling by LayoutExecutor for details.
fixPorts
Specifies whether strong port constraints are automatically created. See section Automatic Port Handling by LayoutExecutor for details.
portAdjustmentPolicy
Specifies whether to place the ports at the outline of a node. See section Automatic Port Handling by LayoutExecutor for details.
portLabelPolicies
Specifies how labels at Ports should be treated by the layout algorithm. This is a mapping which is queried for each label at each port and defines whether the port label mimics a node or an edge label for the layout. See section Automatic Port Handling by LayoutExecutor for details.
tableLayoutConfigurator
The type that is responsible for adding table information to the layout graph.
hideEdgesAtEdges
Controls whether edges at other edges will be hidden from the layout graph. The default is false in which case temporary nodes are inserted for each source and target port of an edge that is owned by an edge.
abortHandler
The type that provides an abort mechanism for layout calculations. See section Aborting Layout Calculations for details.

Using Asynchronous Layout Calculation

In order not to block the UI thread of a browser for too long, or in order to execute multiple layouts in parallel, you can use class LayoutExecutorAsync and a corresponding LayoutExecutorAsyncWorker instance.

For long-running layouts or calculations, using Web Workers is the recommended approach to avoid a blocked user interface. Web Workers are supported in all modern browsers and allow for parallel execution of JavaScript. However since they run in a separate execution context, message passing needs to be used in order to let the two contexts communicate.

yFiles for HTML does not implement the communication between the invoking context and the worker. Instead the transmission of the data as well as the setup and tear-down of the remote worker context needs to be done outside of the library. Several demos show how this can be implemented in a generic way for different browsers and toolkits. With this approach, even a remote layout server can be implemented with only a few lines of glue code.

Asynchronous layout execution involves two classes for the two parties involved:

LayoutExecutorAsync is the part that typically runs on the UI thread in the browser. It has almost the identical API to LayoutExecutor and can be used just the same. The only difference is that you do not specify an ILayoutAlgorithm, but instead an object that describes the algorithm. This is because you will want to avoid having a dependency on the algorithm on the UI thread if you don’t want to use it, anyway. And since algorithms cannot be serialized, the descriptor will be serialized instead, and the part that runs as a worker will construct the layout algorithm using the information found in the descriptor. Alternatively, the descriptor can be ignored, and the layout can be hard-coded on the worker side.

For graph items only their geometric information is transferred to the worker, so information about the styles, tags etc. are missing. For labels only their layout and - if existing - the content of their ILabelCandidateDescriptor is transferred. When layout algorithms place labels as well as their owner, only free label models are supported as the chosen owner location can’t be considered to determine valid distinct label positions. If no free label models can be used, a pure labeling step should be applied after the main layout has been calculated. By activating sendLabelCandidates, all valid label candidate locations are sent to the worker.

LayoutExecutorAsync takes the descriptor, serializes the graph along with the information stored in the LayoutData and sends it to the LayoutExecutorAsyncWorker:

LayoutExecutorAsyncWorker lives on the other thread, process, or machine that will actually perform the costly operations. Its job is to deserialize the information sent from the UI thread, construct the ILayoutAlgorithm, and apply the layout. Once that is done, the resulting graph will be encoded and serialized again and sent back to the UI thread, where the changes can be applied to the graph, both atomically or in an animated fashion.

Using the LayoutExecutorAsync class to run an automatic layout in another context
// configure layout algorithm options ...
const layoutDescriptor = {
  name: 'HierarchicLayout',
  properties: { nodeToNodeDistance: 20 }
}

const layoutData = new HierarchicLayoutData()
// configure graph specific options ...

// create an asynchronous layout executor that calculates a layout on the worker
const executor = new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent: graphComponent,
  layoutDescriptor: layoutDescriptor,
  layoutData: layoutData,
  duration: '1s'
})

// run the layout
await executor.start()
// configure layout algorithm options ...
const layoutDescriptor: LayoutDescriptor = {
  name: 'HierarchicLayout',
  properties: { nodeToNodeDistance: 20 }
}

const layoutData = new HierarchicLayoutData()
// configure graph specific options ...

// create an asynchronous layout executor that calculates a layout on the worker
const executor = new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent: graphComponent,
  layoutDescriptor: layoutDescriptor,
  layoutData: layoutData,
  duration: '1s'
})

// run the layout
await executor.start()

The above code does not specify explicitly how the two parties communicate with each other. They could reside in different web threads, different browsers, tabs, or even on remote machines. Most of the time, however Web Workers will be used. The configuration is straightforward and conceptually does not differ between these different approaches:

Configuring the LayoutExecutorAsync class to use a Web Worker
function webWorkerMessageHandler(data) {
  return new Promise((resolve) => {
    worker.onmessage = (e) => resolve(e.data)
    worker.postMessage(data)
  })
}
function webWorkerMessageHandler(data: unknown): Promise<any> {
  return new Promise((resolve) => {
    worker.onmessage = (e: any) => resolve(e.data)
    worker.postMessage(data)
  })
}

On the receiving side, class LayoutExecutorAsyncWorker requires a similar configuration approach. The API is simple. Conceptually all that needs to be done is deserialize the message from the UI thread, run the algorithm, and send back the encoded results. The LayoutExecutorAsyncWorker class is responsible for the serialization and deserialization. You will have to manage the receipt of the data from the client and sending back the encoded results. Conceptually, this happens outside the worker class. Your code will receive the message from the thread and pass it to the process method. That method will decode the message and pass the information to the function that was specified in the constructor call. It’s this function that will construct the ILayoutAlgorithm and run it. When the function completes, the worker instance serializes the data and returns it to the client code, which will need to send it back to the UI thread.

This is how a typical function could be implemented:

Configuring the LayoutExecutorAsyncWorker class with a layout function
function applyLayout(graph, layoutDescriptor) {
  // create and configure the layout algorithm
  if (layoutDescriptor.name === 'HierarchicLayout') {
    const layout = new HierarchicLayout(layoutDescriptor.properties)

    // run the layout
    layout.applyLayout(graph)
  }
}

const executor = new LayoutExecutorAsyncWorker(applyLayout)
function applyLayout(graph: LayoutGraph, layoutDescriptor: LayoutDescriptor): void {
  // create and configure the layout algorithm
  if (layoutDescriptor.name === 'HierarchicLayout') {
    const layout = new HierarchicLayout(layoutDescriptor.properties)

    // run the layout
    layout.applyLayout(graph)
  }
}

const executor = new LayoutExecutorAsyncWorker(applyLayout)

And the glue code for running this code in a Web Worker could look like this:

Using the LayoutExecutorAsyncWorker in a Web Worker setup
// when a message is received..
self.addEventListener(
  'message',
  async (e) => {
    // send it to the executor for processing and post the results
    // back to the caller
    try {
      const data = await executor.process(e.data)
      self.postMessage(data)
    } catch (error) {
      self.postMessage(error)
    }
  },
  false
)

Migrating from Synchronous to Asynchronous Layout Calculation

While we do recommend starting with UI-thread-blocking layout execution using the LayoutExecutor, you may want to consider switching to asynchronous layout calculation later during development when it becomes clear that your graphs are too large, the algorithms too complex, and the execution time for your layouts gets too long and prohibits a blocking of the UI thread. Note that for the vast majority of cases, we believe that for a layout, it is OK to block the UI thread even for up to several seconds. This is especially true if the layouts don’t happen unexpectedly but get triggered by user-interaction and when there is a "waiting" animation, which, when implemented properly, will also work with a blocked UI thread.

Migrating from a layout running on the main thread to an external multi-threaded execution model is straightforward from the API perspective: LayoutExecutorAsync has almost the identical API as LayoutExecutor, and a migration from code using the latter to the former will allow you to reuse the code that you have written before.

For many applications, converting to asynchronous layout calculation should not be necessary. If you keep the number of elements in your graphs to a reasonable, user-friendly level, most automatic layouts should execute quickly enough. The programming overhead and in the case of a server-backed layout the additional communication overhead might not be worth the gains.

To prepare the migration, split up your current logic into two configuration steps:

One part is the creation of the ILayoutAlgorithm instance and its configuration. This is the part that will be moved to the worker thread. It should be mostly independent of your application state and the contents of your graph. Since the layout will be running in a different context, all the information that you need to dynamically construct and configure the algorithm instance needs to be serialized and sent to the worker.

If your layout setup is independent in a pure function, you can simply move that function declaration to the worker thread. All the parameters that you need to pass to that function need to be serialized and sent to the worker. For this, class LayoutExecutorAsync provides the layoutDescriptor property. This property basically consists of a name, and an arbitrary JSON block that can be sent to the worker context and passed to the function that will create and perform the layout (see Configuring the LayoutExecutorAsyncWorker class with a layout function).

If your code is using LayoutData to pass additional per-item information to the layout algorithm, you can use the layoutData property. It is able to serialize that information and works with most built-in types relevant for layout calculation.

Consider using GenericLayoutData if you want to transfer custom data per graph item to the worker.

Finally, set up the communication link as shown in Using Asynchronous Layout Calculation.

So the steps are:

  1. Determine whether you need asynchronous layout calculation
  2. Choose the type of remote implementation and come up with a communication channel (e.g. Web Worker, or layout server). Find ready-to-use example implementations in our demos:
  3. Refactor your setup code and split-off the creation of the ILayoutAlgorithm
  4. Replace the LayoutExecutor with LayoutExecutorAsync, removing the assignment of the layout algorithm and - if necessary - add your configuration variables to the layoutDescriptor.
  5. Move the creation code for the layout algorithm to the code in the remote location that hosts the LayoutExecutorAsyncWorker, using the same function code in the callback passed to the constructor. In the callback function, after the ILayoutAlgorithm has been created, call method applyLayout, passing the graph parameter that was also passed to the function.

Here’s an example that shows the steps described before:

Old code: Running the layout calculation on the main thread, two variants
await graphComponent.morphLayout({
  layout: new MinimumNodeSizeStage(
    new HierarchicLayout({ layoutOrientation: 'left-to-right' })
  ),
  morphDuration: '1s',
  easedAnimation: true
})

// or

await new LayoutExecutor({
  graphComponent,
  duration: '1s',
  easedAnimation: true,
  layout: new MinimumNodeSizeStage(
    new HierarchicLayout({ layoutOrientation: 'left-to-right' })
  )
}).start()

await graphComponent.morphLayout({
  layout: new MinimumNodeSizeStage(new HierarchicLayout({ layoutOrientation: 'left-to-right' })),
  morphDuration: '1s',
  easedAnimation: true
})

// or

await new LayoutExecutor({
  graphComponent,
  duration: '1s',
  easedAnimation: true,
  layout: new MinimumNodeSizeStage(new HierarchicLayout({ layoutOrientation: 'left-to-right' }))
}).start()

Old code refactored: Splitting up layout creation and configuration
/**
 * @typedef {Object} UserDefinedLayoutProperties
 * @property {LayoutOrientationStringValues} layoutOrientation
 */

function createLayout(options) {
  return new MinimumNodeSizeStage(
    new HierarchicLayout({ layoutOrientation: options.layoutOrientation })
  )
}

await new LayoutExecutor({
  graphComponent,
  duration: '1s',
  easedAnimation: true,
  layout: createLayout({ layoutOrientation: 'left-to-right' })
}).start()

type UserDefinedLayoutProperties = {
  layoutOrientation: LayoutOrientationStringValues
}

function createLayout(options: UserDefinedLayoutProperties): ILayoutAlgorithm {
  return new MinimumNodeSizeStage(new HierarchicLayout({ layoutOrientation: options.layoutOrientation }))
}

await new LayoutExecutor({
  graphComponent,
  duration: '1s',
  easedAnimation: true,
  layout: createLayout({ layoutOrientation: 'left-to-right' })
}).start()

New Code: Replacing the calling code on the main thread
new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent,
  layoutDescriptor: {
    name: 'UserDefined',
    properties: { layoutOrientation: 'left-to-right' }
  },
  duration: '1s',
  easedAnimation: true
})
new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent,
  layoutDescriptor: { name: 'UserDefined', properties: { layoutOrientation: 'left-to-right' } },
  duration: '1s',
  easedAnimation: true
})
New Code: Moving the layout creation and execution to the worker
function applyLayout(graph, layoutDescriptor) {
  // create and configure the layout algorithm
  if (layoutDescriptor.name === 'UserDefined') {
    const layout = createLayout(layoutDescriptor.properties)
    // run the layout
    layout.applyLayout(graph)
  }
}

const executor = new LayoutExecutorAsyncWorker(applyLayout)
const resultMessage = await executor.process(clientMessage)
function applyLayout(graph: LayoutGraph, layoutDescriptor: LayoutDescriptor): void {
  // create and configure the layout algorithm
  if (layoutDescriptor.name === 'UserDefined') {
    const layout = createLayout(layoutDescriptor.properties as UserDefinedLayoutProperties)
    // run the layout
    layout.applyLayout(graph)
  }
}

const executor = new LayoutExecutorAsyncWorker(applyLayout)
const resultMessage = await executor.process(clientMessage)

The following example shows how to transfer custom data to the worker. E.g., when using RecursiveGroupLayout, how to transfer the information about the inner layout algorithms of each group node.

Serialization of the custom data
// a mapping between group nodes and the layout algorithm that has to be applied
const mapper = new Mapper()
mapper.set(groupNode1, 'HierarchicLayout')
mapper.set(groupNode2, 'OrganicLayout')
mapper.set(groupNode3, 'RadialLayout')

// create the container for the data
const layoutData = new GenericLayoutData()
// and register the information in the data using a node mapping with a given key
layoutData.addNodeItemMapping('GroupNodeToLayoutDpkey').mapper = mapper

// create an asynchronous layout executor that calculates a layout on the worker
new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent,
  layoutDescriptor: {
    name: 'UserDefined',
    properties: { coreLayout: 'HierarchicLayout' }
  },
  layoutData,
  duration: '1s',
  animateViewport: true,
  easedAnimation: true
})
// a mapping between group nodes and the layout algorithm that has to be applied
const mapper = new Mapper<INode, string>()
mapper.set(groupNode1, 'HierarchicLayout')
mapper.set(groupNode2, 'OrganicLayout')
mapper.set(groupNode3, 'RadialLayout')

// create the container for the data
const layoutData = new GenericLayoutData()
// and register the information in the data using a node mapping with a given key
layoutData.addNodeItemMapping('GroupNodeToLayoutDpkey').mapper = mapper

// create an asynchronous layout executor that calculates a layout on the worker
new LayoutExecutorAsync({
  messageHandler: webWorkerMessageHandler,
  graphComponent,
  layoutDescriptor: { name: 'UserDefined', properties: { coreLayout: 'HierarchicLayout' } },
  layoutData,
  duration: '1s',
  animateViewport: true,
  easedAnimation: true
})
Deserialization of the custom data, layout creation and execution in the worker
function applyLayout(graph, layoutDescriptor) {
  if (layoutDescriptor.name === 'UserDefined') {
    // get the data-provider registered by the generic layout data
    const groupNodeLayoutsMap = graph.getDataProvider(
      'GroupNodeToLayoutDpkey'
    )
    // add the data-provider with RecursiveGroupLayout.GROUP_NODE_LAYOUT_DP_KEY (required by the RecursiveGroupLayout),
    // after translating the string information to an actual layout algorithm instance
    graph.addDataProvider(
      RecursiveGroupLayout.GROUP_NODE_LAYOUT_DP_KEY,
      new (class extends DataProviderBase {
        /**
         * @param {!Node} dataHolder
         * @returns {!ILayoutAlgorithm}
         */
        get(dataHolder) {
          const layoutName = groupNodeLayoutsMap.get(dataHolder)
          if (layoutName) {
            switch (layoutName) {
              case 'HierarchicLayout':
                return new HierarchicLayout()
              case 'RadialLayout':
                return new RadialLayout()
              case 'OrganicLayout':
                return new OrganicLayout({ minimumNodeDistance: 75 })
            }
          }
          return RecursiveGroupLayout.NULL_LAYOUT
        }
      })()
    )
    // create and configure the layout algorithm
    const layout = new RecursiveGroupLayout()
    if (layoutDescriptor.properties['coreLayout'] === 'HierarchicLayout') {
      layout.coreLayout = new HierarchicLayout()
    }
    // run the layout
    layout.applyLayout(graph)
  }
}
function applyLayout(graph: LayoutGraph, layoutDescriptor: LayoutDescriptor): void {
  if (layoutDescriptor.name === 'UserDefined') {
    // get the data-provider registered by the generic layout data
    const groupNodeLayoutsMap = graph.getDataProvider('GroupNodeToLayoutDpkey')!
    // add the data-provider with RecursiveGroupLayout.GROUP_NODE_LAYOUT_DP_KEY (required by the RecursiveGroupLayout),
    // after translating the string information to an actual layout algorithm instance
    graph.addDataProvider(
      RecursiveGroupLayout.GROUP_NODE_LAYOUT_DP_KEY,
      new (class extends DataProviderBase {
        get(dataHolder: Node): ILayoutAlgorithm {
          const layoutName = groupNodeLayoutsMap.get(dataHolder)
          if (layoutName) {
            switch (layoutName) {
              case 'HierarchicLayout':
                return new HierarchicLayout()
              case 'RadialLayout':
                return new RadialLayout()
              case 'OrganicLayout':
                return new OrganicLayout({ minimumNodeDistance: 75 })
            }
          }
          return RecursiveGroupLayout.NULL_LAYOUT
        }
      })()
    )
    // create and configure the layout algorithm
    const layout = new RecursiveGroupLayout()
    if (layoutDescriptor.properties!['coreLayout'] === 'HierarchicLayout') {
      layout.coreLayout = new HierarchicLayout()
    }
    // run the layout
    layout.applyLayout(graph)
  }
}

All that is left then is to connect the main thread with the worker thread. See Using Asynchronous Layout Calculation for examples as well as these demos:

Aborting Layout Calculations

All layout styles provided by yFiles support an abort mechanism with the aid of the AbortHandler class. Once an abort handler is attached to a graph, it can be used to trigger early termination (stop) of the current layout calculation.

stop(): void
Method to trigger termination of layout calculations.
stopDuration
cancelDuration
Properties to specify runtime thresholds (in milliseconds) before terminating a layout calculation. A value of 0 means no premature termination.

Stopping the layout calculation results in early but not immediate termination. The layout algorithm still delivers a consistent result. Canceling, in contrast, will end layout calculation instantly, throwing an AlgorithmAbortedException and all work done so far by the algorithm will be discarded.

When layout calculation is ended instantly, the state of the ILayoutAlgorithm instance may become corrupted. Hence, you have to create a new instance after each cancellation.

The AbortHandler can be used in two ways:

  1. Each LayoutData provides an AbortHandler instance that can be used to abort the layout calculation:

Aborting a layout calculation using layout data
/**
 * @param {*} graphComponent
 * @returns {!Promise}
 */
async startLayout(graphComponent) {
  const layout = new HierarchicLayout()
  // configure layout algorithm options ...

  const layoutData = new HierarchicLayoutData()
  // configure graph specific options ...

  // save AbortHandler for later use
  this.abortHandler = layoutData.abortHandler

  // start the layout calculation
  await graphComponent.morphLayout({ layout, layoutData })
}

stopLayout() {
  // abort the layout morphing
  if (this.abortHandler !== null) {
    this.abortHandler.stop()
  }
}
async startLayout(graphComponent: any): Promise<void> {
  const layout = new HierarchicLayout()
  // configure layout algorithm options ...

  const layoutData = new HierarchicLayoutData()
  // configure graph specific options ...

  // save AbortHandler for later use
  this.abortHandler = layoutData.abortHandler

  // start the layout calculation
  await graphComponent.morphLayout({ layout, layoutData })
}

stopLayout(): void {
  // abort the layout morphing
  if (this.abortHandler !== null) {
    this.abortHandler.stop()
  }
}

  1. If you do not use LayoutData, you can abort the layout calculation with the AbortHandler provided by the abortHandler property of the LayoutExecutor class:

Aborting a layout calculation using LayoutExecutor
/**
 * @param {!GraphComponent} graphComponent
 * @returns {!Promise}
 */
async startLayout(graphComponent) {
  const layout = new HierarchicLayout()
  // configure layout algorithm options ...

  const layoutData = new HierarchicLayoutData()
  // configure graph specific options ...

  const layoutExecutor = new LayoutExecutor({
    graphComponent,
    layout,
    layoutData
  })
  // configure the LayoutExecutor ...

  // save AbortHandler for later use
  this.abortHandler = layoutExecutor.abortHandler

  // start the layout calculation
  await layoutExecutor.start()
}

stopLayout() {
  // abort the layout morphing
  if (this.abortHandler !== null) {
    this.abortHandler.stop()
  }
}
async startLayout(graphComponent: GraphComponent): Promise<void> {
  const layout = new HierarchicLayout()
  // configure layout algorithm options ...

  const layoutData = new HierarchicLayoutData()
  // configure graph specific options ...

  const layoutExecutor = new LayoutExecutor({ graphComponent, layout, layoutData })
  // configure the LayoutExecutor ...

  // save AbortHandler for later use
  this.abortHandler = layoutExecutor.abortHandler

  // start the layout calculation
  await layoutExecutor.start()
}

stopLayout(): void {
  // abort the layout morphing
  if (this.abortHandler !== null) {
    this.abortHandler.stop()
  }
}

LayoutExecutorAsync doesn’t provide an AbortHandler property as it doesn’t support aborting the layout via message passing to the LayoutExecutorAsyncWorker. Instead it provides stopDuration and cancelDuration that are sent to and used by the LayoutExecutorAsyncWorker.Furthermore the layout calculation can be canceled which discards the layout result once it is sent back.

Edge-to-Edge Connections

By default, LayoutExecutor or morphLayout are configured to support edge-to-edge connections.

The layout algorithms of yFiles for HTML do not support edge-to-edge connections. Therefore, these kind of connections have either to be simulated or ignored. This is controlled by LayoutExecutor's property hideEdgesAtEdges. Setting this property to true will hide the edges from the layout algorithms.

Whether it is better to simulate edge-to-edge connections or to hide the edges from the algorithm is highly dependent on the type of algorithm and the purpose of the graph.

To mimic edge-to-edge connections, LayoutExecutor inserts temporary nodes for each source or target port which is owned by an edge and removes these after the layout algorithm has finished. These temporary nodes might disturb the layer assignment in algorithms like hierarchic layout or tree layout. Also, the temporary nodes will break straight edge paths produced by layout algorithms like Organic Layouts.