C

HierarchicalLayoutData<TNode, TEdge, TNodeEdge, TNodeLabel, TEdgeLabel>

Specifies custom data for the HierarchicalLayout.
Inheritance Hierarchy

Members

Show:

Constructors

Parameters

Properties

Gets or sets a mapping from edges to alternative paths for edges connecting to groups, group content, or folder nodes.

When running in incremental layout mode, the alternative edge paths are considered during the routing of fixed (i.e., non-incremental) edges.

The alternative paths should be used in conjunction with alternative group bounds to achieve more stable layout results when collapsing/expanding a group node as follows:

  1. Collapsing: edges adjacent to the group itself and edges where one of the endpoints (source/target) lies inside the group should get the path before collapsing the group as an alternative path. If both endpoints are either inside or outside the group, no alternative path is required.
  2. Expanding: edges adjacent to the expanded folder node (which is now a group) should get the path before expanding as an alternative path.
An edge path consists of the location of the source port, followed by the locations of the bends, followed by the location of the target port as returned by pathPoints.
conversionfinal

See Also

Developer's Guide
API
ALTERNATIVE_EDGE_PATH_DATA_KEY
Gets or sets a mapping from group/folder nodes to alternative bounds for these nodes.
When running in incremental layout mode, the alternative bounds of the collapsed/expanded group will be used during the layering and sequencing phase of the algorithm.
The alternative bounds help to achieve more stable layout results when collapsing/expanding groups in incremental layout mode. More precisely, the alternative bounds specify the original size of the expanded/collapsed group (i.e., before expanding/collapsing).
To further improve the results you should also specify alternativeEdgePaths for edges adjacent to such groups. In addition, a recursive groupLayeringPolicy should be selected and recursive edge style should be set to DIRECTED. Edges that end at a group itself or a folder node should not have port candidates at this end (because this may destroy the recursive directed edge style required to obtain stable results).
The algorithm assumes that there is at most one node with associated alternative bounds.
conversionfinal

See Also

Developer's Guide
API
ALTERNATIVE_GROUP_BOUNDS_DATA_KEY
Gets or sets the collection of core nodes used by the BfsLayerAssigner.

The core nodes will be placed in the first layer. If there are no core nodes, then nodes with in-degree 0 are considered to be the core nodes.

The BfsLayerAssigner is used when fromScratchLayeringStrategy is set to BFS.

conversionfinal

See Also

Developer's Guide
API
CORE_NODES_DATA_KEY
Gets or sets a mapping from edges to their priority to be a 'critical' edge.

The layout tries to vertically align each node pair that is connected by a critical edge. Critical edges are marked by having a priority greater than 0; edges with a zero or negative priority are not considered critical.

Critical edges highlight different edge paths that are relevant for a user. The layout algorithm tries to vertically align each node pair that is connected by a critical edge. Conflicts between different critical edges are always resolved in favor of the higher priority.

Critical edges are always connected to the center port. Hence, the edge distribution is no longer uniform.
Critical edges by default also get crossing costs assigned based on their priorities (see property reduceCriticalEdgeCrossings). Thus, they are favored during the edge crossing minimization phase.
conversionfinal

Examples

Critical edges are often determined by some external data source and are often part of a larger path. The following example shows how two different such paths can be made 'critical' with different priorities:

Configuring critical edges via a mapper
// Set two different critical edge paths with different priorities
for (const edge of path1) {
  layoutData.criticalEdgePriorities.mapper.set(edge, 5)
}
// Lower priority for the second path, which then may deviate from a straight line
for (const edge of path2) {
  layoutData.criticalEdgePriorities.mapper.set(edge, 2)
}

Alternatively, a delegate may be used as well, if determining edge criticality is easier when looking at the edge itself:

Configuring critical edges via a delegate
// Assume that the edge's tag holds an instance that has an 'IsCritical' property
layoutData.criticalEdgePriorities = (edge: IEdge): 0 | 1 =>
  edge.tag.isCritical ? 1 : 0

Sample Graphs

See Also

Developer's Guide
API
CRITICAL_EDGE_PRIORITY_DATA_KEY
Gets or sets the mapping from edges to their crossing cost.
The crossing costs of the edges are considered during the crossing minimization phase. The total crossing costs are minimized using a heuristic where the cost of a crossing between two edges is defined as the product of the edges' crossing costs. A crossing with an edge that has a cost of zero is cost-free and such a crossing will therefore not be avoided. The default crossing cost of an edge is 1, which is used for edges which do not have an individual crossing cost.
Edges that are very important can get larger crossing costs than other edges in order to improve their readability. For edges with criticalEdgePriorities, by default the specified priority automatically affects the crossing cost (i.e. higher priority means higher crossing cost). This default behavior can be configured using reduceCriticalEdgeCrossings.
The implemented crossing minimization is a heuristic. Therefore, there is no guarantee that crossing a certain edge is actually avoided, even though it has high crossing costs.
Avoid using very large cost values. Due to the fact that costs need to be multiplied and summed up, this might lead to overflows and undesired results.
conversionfinal

Examples

When there are only a few edges to customize crossing costs for, the easiest way is usually to use the mapper:

Defining edge crossing costs via a mapper
const layoutData = new HierarchicalLayoutData()
// Try harder to prevent crossings with edge1
layoutData.edgeCrossingCosts.mapper.set(edge1, 1.5)
// Crossings with edge2 are not as bad
layoutData.edgeCrossingCosts.mapper.set(edge2, 0.2)

If the crossing cost can readily be computed from the edge itself, the mapperFunction is often the more convenient option:

Defining edge crossing costs via a delegate
// The more labels an edge has, the more important it is to
// prevent crossing with it
layoutData.edgeCrossingCosts = (edge: IEdge): number =>
  1 + edge.labels.size

See Also

Developer's Guide
API
EDGE_CROSSING_COST_DATA_KEY, reduceCriticalEdgeCrossings
Gets or sets the mapping of edges to their HierarchicalLayoutEdgeDescriptor.
If an edge is mapped to null, the default descriptor is used.
conversionfinal

Examples

This property allows changing how edges are routed for each edge individually. To just change a few edges that deviate from the default style, using the mapper is probably the best option:

Using a mapper to set different EdgeLayoutDescriptors for different edges
// Ensure larger lengths for the first and last segments of edge1
layoutData.edgeDescriptors.mapper.set(
  edge1,
  new HierarchicalLayoutEdgeDescriptor({
    minimumFirstSegmentLength: 30,
    minimumLastSegmentLength: 50,
  }),
)
// Change the routing style to octilinear for edge2
layoutData.edgeDescriptors.mapper.set(
  edge2,
  new HierarchicalLayoutEdgeDescriptor({
    routingStyleDescriptor: new RoutingStyleDescriptor(
      HierarchicalLayoutRoutingStyle.OCTILINEAR,
    ),
  }),
)
// All other edges not set in the mapper use the default EdgeLayoutDescriptor
// set on HierarchicalLayout.

If a HierarchicalLayoutEdgeDescriptor is easier to create from every edge itself, using a delegate is often easier:

Using a delegate to set a different EdgeLayoutDescriptor for each edge
// Use a property from a custom business object in the edge's tag as the minimum length
layoutData.edgeDescriptors = (edge) => {
  const customData = edge.tag as CustomData
  return new HierarchicalLayoutEdgeDescriptor({
    minimumLength: customData.minimumLength,
    routingStyleDescriptor: new RoutingStyleDescriptor(
      customData.shouldUseOctilinearRouting
        ? HierarchicalLayoutRoutingStyle.OCTILINEAR
        : HierarchicalLayoutRoutingStyle.POLYLINE,
    ),
  })
}

See Also

Developer's Guide
API
defaultEdgeDescriptor, EDGE_DESCRIPTOR_DATA_KEY
Gets or sets the mapping from edges to their directedness.
Generally, the hierarchical layout algorithm assigns nodes to layers such that most of the edges point in the main layout direction. The directedness of an edge specifies whether it should comply with this strategy. More precisely, a value of 1 means that the edge should fully comply, a value of -1 means that it should comply inversely (the edge should point against the main layout direction), and a value of 0 means that the direction doesn't matter at all and the endpoints of the edges may be placed at the same layer. If there are conflicting preferences, edges with higher absolute values are more likely to point in the desired direction.
If left unspecified, the algorithm assumes that all edges have directedness 1.
Considering the edge directedness requires a special layering algorithm. Hence, HierarchicalLayout ignores the specified fromScratchLayerAssigner and automatically switches to the ConstraintIncrementalLayerAssigner in such cases.
conversionfinal

Examples

The easiest option is to define all edges with the same directedness:

Treating all edges as directed
layoutData.edgeDirectedness = 1

Handling only certain edges differently can be done easily by using the mapper property:

Using a mapper to set directedness for certain edges
// edge1 should be considered directed
layoutData.edgeDirectedness.mapper.set(edge1, 1)
// edge2 should be considered directed against the flow
layoutData.edgeDirectedness.mapper.set(edge2, -1)
// All other edges not set in the mapper are treated as undirected

In cases where the directedness for each edge can be determined by looking at the edge itself it's often easier to just set a delegate instead of preparing a mapper:

Using a delegate to determine edge directedness
// Treat edges as directed or undirected based on their style's arrowhead
layoutData.edgeDirectedness = (edge: IEdge): 0 | 1 | -1 => {
  const style = edge.style as PolylineEdgeStyle
  // edges with either no arrows on both ends or an arrow on both ends should be considered undirected
  if (
    (style.sourceArrow === null && style.targetArrow === null) ||
    (style.sourceArrow !== null && style.targetArrow !== null)
  ) {
    return 0
  }
  // edges with only a target arrow are directed from source to target
  if (style.targetArrow !== null) {
    return 1
  }
  // edges with only a source arrow are directed from target to source
  if (style.sourceArrow !== null) {
    return -1
  }
  return 0
}

Sample Graphs

ShownSetting: Layout result when dashed edges are undirected, i.e., have directedness 0

See Also

Developer's Guide
API
EDGE_DIRECTEDNESS_DATA_KEY
Gets or sets the mapping that provides an EdgeLabelPreferredPlacement instance for edge ILabels.
conversionfinal

Examples

Depending on how much customization is needed, some ways of setting EdgeLabelPreferredPlacements are more convenient than others. For example, to set the same descriptor for all labels, you can just use the constant property:

Setting the same EdgeLabelPreferredPlacement for all labels
layoutData.edgeLabelPreferredPlacements = new EdgeLabelPreferredPlacement(
  {
    // Place labels along the edge
    angleReference: LabelAngleReferences.RELATIVE_TO_EDGE_FLOW,
    angle: 0,
    // ... on either side
    edgeSide: LabelEdgeSides.LEFT_OF_EDGE | LabelEdgeSides.RIGHT_OF_EDGE,
    // ... with a bit of distance to the edge
    distanceToEdge: 5,
  },
)

If some labels should use custom placement or this has to be configured ahead of time, you can use the mapper instead:

Customizing the placement of certain labels individually via a mapper
// Place label1 orthogonal to the edge anywhere on it
layoutData.edgeLabelPreferredPlacements.mapper.set(
  label1,
  new EdgeLabelPreferredPlacement({
    placementAlongEdge: LabelAlongEdgePlacements.ANYWHERE,
    angleReference: LabelAngleReferences.RELATIVE_TO_EDGE_FLOW,
    angle: Math.PI / 2,
  }),
)
// Place label2 near the edge's source on either side of it, and make it parallel to the edge
layoutData.edgeLabelPreferredPlacements.mapper.set(
  label2,
  new EdgeLabelPreferredPlacement({
    placementAlongEdge: LabelAlongEdgePlacements.AT_SOURCE,
    edgeSide: LabelEdgeSides.RIGHT_OF_EDGE | LabelEdgeSides.LEFT_OF_EDGE,
    angleReference: LabelAngleReferences.RELATIVE_TO_EDGE_FLOW,
    angle: 0,
  }),
)

When the preferred placement can be inferred from the label itself, a delegate is usually the easiest choice:

Configuring preferred placement for all labels with a delegate
layoutData.edgeLabelPreferredPlacements = (
  label: ILabel,
): EdgeLabelPreferredPlacement => {
  const customData = label.tag as CustomData
  return new EdgeLabelPreferredPlacement({
    angle: 0,
    angleReference: LabelAngleReferences.RELATIVE_TO_EDGE_FLOW,
    // If the tag says to place the label in the center, put it in the center parallel to the edge's path
    // All other labels can be placed anywhere, but on the side of the edge.
    placementAlongEdge: customData.placeInCenter
      ? LabelAlongEdgePlacements.AT_CENTER
      : LabelAlongEdgePlacements.ANYWHERE,
    edgeSide: customData.placeInCenter
      ? LabelEdgeSides.ON_EDGE
      : LabelEdgeSides.LEFT_OF_EDGE | LabelEdgeSides.RIGHT_OF_EDGE,
  })
}

Note that the preferred placement can also be inferred from an arbitrary ILabelModelParameter:

Configuring preferred placement from a label model parameter
layoutData.edgeLabelPreferredPlacements =
  EdgeLabelPreferredPlacement.fromParameter(
    NinePositionsEdgeLabelModel.CENTER_CENTERED,
  )

See Also

Developer's Guide
API
EdgeLabelPreferredPlacement, EDGE_LABEL_PREFERRED_PLACEMENT_DATA_KEY
Gets or sets the mapping from edges to their thickness.
The specified non-negative thickness is considered when calculating minimum distances so that there are no overlaps between edges and other graph elements. By default, each edge has thickness 0.
The port assignments ON_GRID and on sub-grid ignore the thickness of edges.
conversionfinal

Examples

The easiest option is to define the same thickness for all edges:

Specifying a uniform thickness for all edges
layoutData.edgeThickness = 3

Handling only certain edges differently can be done easily by using the mapper property:

Using a mapper to set a custom thickness for certain edges
layoutData.edgeThickness.mapper.set(edge1, 2)
layoutData.edgeThickness.mapper.set(edge2, 5)
// All other edges not set in the mapper have no defined thickness

In cases where the thickness for each edge can be determined by looking at the edge itself it's often easier to just set a delegate instead of preparing a mapper:

Using a delegate to determine edge thickness
// Use the style's pen thickness as edge thickness
layoutData.edgeThickness = (edge: IEdge): number => {
  const style = edge.style
  if (style instanceof PolylineEdgeStyle) {
    return style.stroke!.thickness
  }
  return 0
}

Sample Graphs

ShownSetting: Edge thickness 0

See Also

Developer's Guide
API
EDGE_THICKNESS_DATA_KEY
Gets or sets the collection of folder nodes used for recursive edge styles in incremental layout mode.
When using recursive edges (recursiveEdgePolicy) in incremental layout mode, edges will also start at the bottom and end at the top of marked folder nodes. This will keep the edge routes more stable since the connection sides won't change.
conversionfinal

Examples

Using the folderNodes property is probably easiest with a delegate that checks for each node whether it's a folder node or not:

Dynamically determine whether a node is a folder node
layoutData.folderNodes = (node) =>
  graph.foldingView!.isInFoldingState(node)

However, the other options are also still there, such as using an IEnumerable<T> via source:

Using a static collection of folder nodes
layoutData.folderNodes = graph.nodes.filter((node) =>
  graph.foldingView!.isInFoldingState(node),
)

or explicitly using the items collection to add or remove items, perhaps based on the user collapsing/expanding group nodes:

Adding and removing nodes from the FolderNodes backing collection
// When collapsing a node, mark it as a folder node for layout
graphEditorInputMode.navigationInputMode.addEventListener(
  'group-collapsed',
  (evt) => layoutData.folderNodes.items.add(evt.item),
)
// When expanding the node again, we unmark it again
graphEditorInputMode.navigationInputMode.addEventListener(
  'group-expanded',
  (evt) => layoutData.folderNodes.items.remove(evt.item),
)

See Also

Developer's Guide
API
FOLDER_NODES_DATA_KEY
Gets or sets the mapping from nodes to their layer index when using the GivenLayersAssigner.

If the GivenLayersAssigner is used, the nodes are assigned to layers in the following way:

  • Nodes with the same index are placed in the same layer.
  • A node with a smaller index is placed in a layer above that of a node with a larger layer index ("above" with respect to the given LayoutOrientation, i.e., on a smaller y-coordinate for orientation top-to-bottom).
  • The given layer indices are normalized such that there are no empty layers and the smallest layer has value 0.

The GivenLayersAssigner is used when fromScratchLayeringStrategy is set to USER_DEFINED.

conversionfinal

See Also

Developer's Guide
API
LAYER_INDEX_DATA_KEY
Gets or sets the mapping from nodes to their layer index relative to the root node of the grid component they belong to.
An offset value i specified for a grid node is interpreted relative to the root node as follows, assuming that the root node is in layer x.
  • If the grid component is placed in the layers after the root, the grid node will be placed in layer x + i
  • If the grid component is placed in the layers before the root, the grid node will be placed in layer x - i
This setting is only considered for nodes that belong to grid components defined by property gridComponents. If givenLayersIndices are defined for all nodes, the index for nodes of a grid component are also interpreted in a relative way as described here.
The specified number of nodes before the bus and number of nodes after the bus might be violated if the layer indices provided here require that a layer contains too many nodes to fulfill the node count constraints.
conversionfinal

Examples

When the layer offsets can be readily inferred from the nodes, then the easiest option is usually to use the mapperFunction:

layoutData.gridComponentRootOffsets = (node: INode): number =>
  node.tag.offset

In case only some nodes need to be mapped to their respective layer offset, it's sometimes easier to use the mapper:

layoutData.gridComponentRootOffsets.mapper.set(node1, 0)
layoutData.gridComponentRootOffsets.mapper.set(node2, 1)

See Also

Developer's Guide
API
gridComponents, ROOT_OFFSET_DATA_KEY
Gets or sets the mapping from edges to their GridComponentDescriptor which defines the grid component they belong to.

All edges with the same GridComponentDescriptor adjacent to a specific node (the root) and having the same direction are arranged on a grid with a central bus structure. The edgeDirectedness is also considered here. The direction defines the placement of the nodes in the following way:

  • Grid nodes connected by out-edges of the root are placed in layers starting with the layer immediately after the layer that contains the root (e.g. below the root for top-to-bottom layout orientation).
  • Grid nodes connected by in-edges of the root are placed in the layers ending with the layer immediately before the layer that contains the root (e.g. above the root for top-to-bottom layout orientation).

The grid components are arranged using a style that yields more compact layout results. Edges to the child nodes, the so-called grid nodes, are routed using a shared segment that connects to the common root node. The grid nodes are arranged using several layers above or below the root node such that the whole substructure has a compact area. By default, each grid layer contains equally many grid nodes, grid nodes of adjacent layers are center-aligned and the shared bus segment is routed in the middle of the nodes. This produces compact, symmetric arrangements of the grid components.

The arrangement can be customized for each grid component individually, by using the settings offered by GridComponentDescriptor. It allows to configure the number of grid nodes before and after the common bus segment.

Some restrictions apply for grid components:

  • Only nodes that are in the same LayoutGridCellDescriptor as the root node are included in the grid component.
  • The grid nodes must all belong to the same group node. Also, a group node can not be part of a grid component; neither as grid element nor as the root.
  • Grid structures can not be nested. That means that if a node is a grid node it can not be a root node of another grid component at the same time.
  • For bus edges, the integrated edge labeling feature does not place port labels as close to the actual port as it does for other edges (see AT_SOURCE_PORT and AT_TARGET_PORT).
  • Edge grouping constraints at the side of the grid root node are ignored for bus edges; the grouping induced by the grid component is considered to be stronger.
  • If in incremental layout mode, the bus edges are always treated in an incremental way such that the bus-style routing remains consistent (e.g. if a fixed bus edge contains bends that suggest another route, they are ignored). The reason is that the bus-style layout of the grid components has a higher priority. Furthermore, the location of the bus segment relative to the elements is not derived from the sketch but depends on the settings maximumNodesBeforeBus and maximumNodesAfterBus as well as the before/after constraints defined by nodesBeforeBus. This means that it can happen that a fixed grid node which was placed before the bus in the input drawing may be placed after the bus if the bus was enlarged due to the insertion of incremental grid nodes.
  • It is not recommended to define grid components inside a group node which has an incremental group hint. Fixed grid nodes inside such a group are not necessarily kept fixed but are treated like incrementally inserted nodes.
The bus-like arrangement is especially suitable for star structures (i.e. a node with a high in- or out-degree) which can otherwise consume a lot of area and make the layout less compact. Ideally, the grid nodes have no other edges aside from the bus edge.
When two parallel edges have the same GridComponentDescriptor, only one edge will be treated as a bus edge.
If layering constraints are specified, they are not considered for grid nodes because the grid component is layered independently.
If grid nodes have edges to other grid nodes (from the same or from another bus), then these edges are not considered in the same way during the layering as other edges; in consequence, it can happen that both grid nodes end up in the same final layer and thus the edge becomes a same-layer edge. The same can happen if a group has an edge to a grid node where the grid node is outside said group but the root of the grid component is a child of it.
final

Examples

Grid components can be configured by first adding a new GridComponentDescriptor and saving the resulting ItemCollection<TItem>. That one can then be used to define the structure of the grid:

// Retrieve an ItemCollection<IEdge> which defines all edges
// of a grid component. This component will be arranged according to the GridComponentDescriptor
const grid = layoutData.gridComponents.add(
  new GridComponentDescriptor(),
)
// You can use this ItemCollection<IEdge> as usual, using either one of
// the Source, Items, Mapper, or Delegate properties.
// Here, we simply use the selected edges:
grid.source = graphComponent.selection.edges

Since adding a new GridComponentDescriptor for a grid returns the ItemCollection<TItem>, configuring grid components can also be chained conveniently:

//  component1Edges, component2Edges, component3Edges are of type IEnumerable.<IEdge>
layoutData.gridComponents.add(new GridComponentDescriptor()).source =
  component1Edges
layoutData.gridComponents.add(new GridComponentDescriptor()).source =
  component2Edges
layoutData.gridComponents.add(new GridComponentDescriptor()).source =
  component3Edges

Sample Graphs

ShownSetting: A graph with no grid components

See Also

Developer's Guide
API
GridComponentDescriptor, GRID_COMPONENT_DESCRIPTOR_DATA_KEY
Gets or sets the mapping from group nodes to the costs for a crossing with the group node border.
Like the edgeCrossingCosts, these costs are considered during the crossing minimization phase. The total costs are minimized using a heuristic where the cost of a crossing between a group node border and an edge is defined as the product of the respective group border crossing cost and the edge crossing cost. If no individual crossing costs are defined, the cost for crossing a group node border is 5. This means that by default, a crossing between an edge and a group node border is more expensive than a crossing between two edges. Also note that a crossing with a group node border that has a cost of zero is cost-free and such a crossing will therefore not be avoided.
In case of a vertical layout orientation (default), these costs only apply to vertical group borders. If the layout orientation is horizontal (e.g. left-to-right), the costs only apply to horizontal group borders.
Avoid using very large cost values. Due to the fact that costs need to be multiplied and summed up, this might lead to overflows and undesired results.
conversionfinal

Examples

When there are only a few group nodes to customize crossing costs for, the easiest way is usually to use the mapper:

Defining group nodes crossing costs via a mapper
// Try harder to prevent crossings with group node1
layoutData.groupBorderCrossingCosts.mapper.set(node1, 7)
// Crossings with group node2 are not as bad
layoutData.groupBorderCrossingCosts.mapper.set(node2, 3)

If the crossing cost can readily be computed from the group node itself, the mapperFunction is often the more convenient option:

Defining group nodes crossing costs via a delegate
// The more labels a group node has, the more important it is to
// prevent crossing with it
layoutData.groupBorderCrossingCosts = (node: INode): number =>
  5 + node.labels.size

See Also

Developer's Guide
API
EDGE_CROSSING_COST_DATA_KEY, GROUP_BORDER_CROSSING_COST_DATA_KEY
Gets or sets a collection of edges that should be sequenced incrementally.
Edges included in the collection are assigned a hint specifying they should be inserted incrementally.
conversionfinal

Examples

Elements which should be placed incrementally can be provided with a delegate:

Creating incremental hints for nodes and edges
// create a HierarchicalLayout which rearranges
// only the incremental graph elements
const hl = new HierarchicalLayout({ fromSketchMode: true })

// provide additional data to configure the HierarchicalLayout
const hlData = new HierarchicalLayoutData()
// specify the nodes to rearrange
hlData.incrementalNodes = (node: INode): boolean => isIncremental(node)
// specify the nodes and edges to rearrange
hlData.incrementalNodes = (item): boolean => isIncremental(item)
hlData.incrementalEdges = (item): boolean => isIncremental(item)

graph.applyLayout(hl, hlData)

More specific hints can be created using IncrementalNodeHint:

Creating individual incremental hints for nodes and edges
// create a HierarchicalLayout which rearranges
// only the incremental graph elements
const hl = new HierarchicalLayout({ fromSketchMode: true })

// provide additional data to configure the HierarchicalLayout for incremental edges
const hlData = new HierarchicalLayoutData({
  // sequence hint for incremental edges
  incrementalEdges: incrementalEdges,
})

// provide additional GenericLayoutData to configure incremental nodes
const glData = new GenericLayoutData<INode, IEdge, ILabel, ILabel>()
const nodeIncrementalHintsMapping = (glData.addItemMapping(
  HierarchicalLayout.INCREMENTAL_NODE_HINTS_DATA_KEY,
).mapperFunction = (node) => {
  if (incrementalNodes.includes(node)) {
    // create a hint for incremental nodes
    if (graph.isGroupNode(node)) {
      // special hint for groups
      return IncrementalNodeHint.INCREMENTAL_GROUP
    }
    if (fixedNodes.includes(node)) {
      // exact layer for the fixedNodes
      return IncrementalNodeHint.USE_EXACT_LAYER_COORDINATES
    }
    // simple layer hint for all other nodes
    return IncrementalNodeHint.LAYER_INCREMENTALLY
  }
  return IncrementalNodeHint.NONE
})

// combine both layout data instances
const layoutData = hlData.combineWith(glData)

graph.applyLayout(hl, layoutData)

See Also

Developer's Guide
API
incrementalNodes, INCREMENTAL_EDGE_HINTS_DATA_KEY
Gets or sets a collection of nodes that should be layered incrementally.
Nodes included in the collection are assigned a hint specifying that they should be inserted incrementally.
conversionfinal

Examples

Elements which should be placed incrementally can be provided with a delegate:

Creating incremental hints for nodes and edges
// create a HierarchicalLayout which rearranges
// only the incremental graph elements
const hl = new HierarchicalLayout({ fromSketchMode: true })

// provide additional data to configure the HierarchicalLayout
const hlData = new HierarchicalLayoutData()
// specify the nodes to rearrange
hlData.incrementalNodes = (node: INode): boolean => isIncremental(node)
// specify the nodes and edges to rearrange
hlData.incrementalNodes = (item): boolean => isIncremental(item)
hlData.incrementalEdges = (item): boolean => isIncremental(item)

graph.applyLayout(hl, hlData)

More specific hints can be created using IncrementalNodeHint:

Creating individual incremental hints for nodes and edges
// create a HierarchicalLayout which rearranges
// only the incremental graph elements
const hl = new HierarchicalLayout({ fromSketchMode: true })

// provide additional data to configure the HierarchicalLayout for incremental edges
const hlData = new HierarchicalLayoutData({
  // sequence hint for incremental edges
  incrementalEdges: incrementalEdges,
})

// provide additional GenericLayoutData to configure incremental nodes
const glData = new GenericLayoutData<INode, IEdge, ILabel, ILabel>()
const nodeIncrementalHintsMapping = (glData.addItemMapping(
  HierarchicalLayout.INCREMENTAL_NODE_HINTS_DATA_KEY,
).mapperFunction = (node) => {
  if (incrementalNodes.includes(node)) {
    // create a hint for incremental nodes
    if (graph.isGroupNode(node)) {
      // special hint for groups
      return IncrementalNodeHint.INCREMENTAL_GROUP
    }
    if (fixedNodes.includes(node)) {
      // exact layer for the fixedNodes
      return IncrementalNodeHint.USE_EXACT_LAYER_COORDINATES
    }
    // simple layer hint for all other nodes
    return IncrementalNodeHint.LAYER_INCREMENTALLY
  }
  return IncrementalNodeHint.NONE
})

// combine both layout data instances
const layoutData = hlData.combineWith(glData)

graph.applyLayout(hl, layoutData)

See Also

Developer's Guide
API
incrementalEdges, INCREMENTAL_NODE_HINTS_DATA_KEY
Gets or sets the layout data to specify layer constraints.

Layer constraints affect the assignment of nodes to layers. A layer constraint can be specified either explicitly for a given node or by mapping some or all nodes to an IComparable, e.g., a number, with the nodeComparables property.

In the latter case, the comparables are used to create placeInOrder constraints between one node and another, if the one precedes the other. These constraints work just like explicitly created constraints. Note that equal values do not result in placeInSameLayer constraints, and no constraints are added for nodes mapped to null.

If there is a fixed layer assignment, for example because the layers are defined by business data, the givenLayersIndices should be used instead.
final

Examples

One way to map each node to an IComparable is to use the mapperFunction:

Using a delegate to map nodes to objects that can be ordered to define relative layer placements
// Map each node to an IComparable, in this case taken from a custom object stored in the tag
layoutData.layerConstraints.nodeComparables = (node) => {
  const data = node.tag
  if (data !== null) {
    return data.layerOrder
  }
  // Don't constrain layer placement if the tag doesn't contain our custom object.
  return null
}

If only some nodes should be constrained, the mapper can be used instead:

Using a mapper to map certain nodes to objects that can be ordered to define relative layer placements
// Define layer constraints for a few nodes via a mapper. Nodes that are not
// explicitly mapped don't have constraints on their layer placement.
const mapper = layoutData.layerConstraints.nodeComparables
  .mapper as IMapper<INode, any>
// Place both node1 and node2 in the same layer by assigning the same value
mapper.set(node1, 5)
mapper.set(node2, 5)
// Place node3 somewhere above node1 and node2 by assigning a smaller value
mapper.set(node3, 2)
// Place node4 somewhere below node1 and node2 by assigning a larger value
mapper.set(node4, 8)
// The exact values don't matter much they just define an ordering. However, all
// values must have the same type you cannot mix int and string or double, for example.

If those mappings have been prepared beforehand, e.g. in a HashMap<TKey, TValue> or IMapper<K, V>, that property on the ItemMapping<TItem, TValue> can also be set:

Setting a prepared mapper or dictionary for relative layer assignment
layoutData.layerConstraints.nodeComparables = layerOrderMapper
// Or create the mapping from a Map, instead:
layoutData.layerConstraints.nodeComparables = layerOrderMap

IComparable is only the most natural option if such an object can be readily produced from a node, or there already exists such a mapping from nodes to something that can be compared. In many cases it can be easier to construct constraints manually with the respective methods on LayerConstraintData<TNode>:

Defining custom layer constraints
// Place all nodes in sameLayerNodes into the same layer
const firstSameLayerNode = sameLayerNodes.first()!
for (const node of sameLayerNodes) {
  layoutData.layerConstraints.placeInSameLayer(firstSameLayerNode, node)
}
// Ensure that node2 is placed two layers below node1
layoutData.layerConstraints.placeInOrder(node1, node2, 2)
// Also place another node in the top layer
layoutData.layerConstraints.placeAtTop(nodeAtTop)

Sample Graphs

ShownSetting: A graph laid out with HierarchicalLayout with default settings. The labels indicate the desired placement of certain nodes relative to a reference node.

See Also

Developer's Guide
Gets a mapper from nodes to the index of their layer.
This property can be used after the layout to retrieve the layer assignment for each node. If only the layer assignment is needed and not the whole layout, consider using layerAssignment instead of applying the HierarchicalLayout.
readonlyfinal

Examples

Retrieving layering information after a layout
const layoutData = new HierarchicalLayoutData()
graph.applyLayout(new HierarchicalLayout(), layoutData)

for (const node of graph.nodes) {
  console.log(
    `Node ${node} has been placed in layer ${layoutData.layerIndicesResult.get(node)}`,
  )
}

See Also

API
LAYER_INDEX_RESULT_DATA_KEY
Gets or sets the layoutGridData.
The HierarchicalLayout doesn't support multiple grids, i.e., all nodes have to be mapped to cells of the same layoutGrid.
final

Examples

The following sample shows how to assign nodes to layout grid cells simply via cell indices:

Assigning nodes to layout grid cells via indices
// Create four nodes and place them in grid cells in the following way
// +---+---+---+
// | 1 | 2 | 3 |
// +---+---+---+
//     | 4 |
//     +---+

const gridData = layoutData.layoutGridData

const node1 = graph.createNode()
const node2 = graph.createNode()
const node3 = graph.createNode()
const node4 = graph.createNode()

// Assign the nodes to their rows and columns.
// Note that you don't have to create or use LayoutGrid directly in this case.
// Setting the indices is enough.
gridData.rowIndices.mapper.set(node1, 0)
gridData.rowIndices.mapper.set(node2, 0)
gridData.rowIndices.mapper.set(node3, 0)
gridData.rowIndices.mapper.set(node4, 1)
gridData.columnIndices.mapper.set(node1, 0)
gridData.columnIndices.mapper.set(node2, 1)
gridData.columnIndices.mapper.set(node3, 2)
gridData.columnIndices.mapper.set(node4, 1)

graph.applyLayout(new HierarchicalLayout(), layoutData)

If used this way there is no need to create a LayoutGrid instance or work with it directly. For more flexibility, e.g., to use cells that span multiple columns or rows, the LayoutGrid can be used as well:

Assigning nodes to layout grid cells via factory methods on the grid
// Create three nodes and place them in grid cells in the following way
// +---+---+
// | 1 | 2 |
// +---+---+
// |   3   |
// +---+---+

const gridData = layoutData.layoutGridData

const node1 = graph.createNode(new Rect(0, 0, 50, 50))
const node2 = graph.createNode(new Rect(0, 0, 50, 50))
const node3 = graph.createNode(new Rect(0, 0, 125, 50))
// Create a new LayoutGrid with two rows and two columns
const grid = new LayoutGrid(2, 2)
// Assign the nodes to their cells
const gridCells = new Mapper<INode, LayoutGridCellDescriptor>()
gridCells.set(node1, grid.createCellDescriptor(0, 0))
gridCells.set(node2, grid.createCellDescriptor(0, 1))
gridCells.set(node3, grid.createRowSpanDescriptor(1))
gridData.layoutGridCellDescriptors = gridCells

graph.applyLayout(new HierarchicalLayout(), layoutData)

See Also

Developer's Guide
Gets or sets the mapping of nodes to their HierarchicalLayoutNodeDescriptor
If a node is mapped to null, the default descriptor is used.
conversionfinal

Examples

This property allows changing how nodes are laid out within their layer for each node individually. To just change a few nodes that deviate from the default handling, using the mapper is probably the best option:

Using a mapper to set different NodeLayoutDescriptors for different nodes
// Set a custom minimum distance of node1 to other element
layoutData.nodeDescriptors.mapper.set(
  node1,
  new HierarchicalLayoutNodeDescriptor({ minimumDistance: 50 }),
)
// Align node2 at the bottom of its layer
layoutData.nodeDescriptors.mapper.set(
  node2,
  new HierarchicalLayoutNodeDescriptor({ layerAlignment: 1 }),
)
// All other nodes not set in the mapper use the default NodeLayoutDescriptor
// set on HierarchicalLayout.

If a HierarchicalLayoutNodeDescriptor is easier to create from every node itself, using a delegate is often easier:

Using a delegate to set a different NodeLayoutDescriptor for each node
// Use a property from a custom business object in the node's tag as the minimum length
layoutData.nodeDescriptors = (node) => {
  const customData = node.tag as CustomData
  const descriptor = new HierarchicalLayoutNodeDescriptor({
    minimumDistance: 5,
  })
  switch (customData.alignment) {
    case 'top-placement':
      descriptor.layerAlignment = 0
      break
    case 'center-placement':
      descriptor.layerAlignment = 0.5
      break
    case 'bottom-placement':
      descriptor.layerAlignment = 1
      break
  }
  return descriptor
}

See Also

Developer's Guide
API
defaultNodeDescriptor, NODE_DESCRIPTOR_DATA_KEY
Gets or sets the mapping from nodes to their margins.
Node margins allow to reserve space around nodes.
conversionfinal

Examples

The easiest option is to reserve the same space around all nodes, by setting a constant value:

Using constant space around all nodes
layoutData.nodeMargins = new Insets(20)

Handling only certain nodes differently can be done easily by using the mapper property:

Using a mapper to set margins for certain nodes
// node1 only reserves space above and below
layoutData.nodeMargins.mapper.set(node1, new Insets(20, 10, 0, 0))
// node2 has space all around
layoutData.nodeMargins.mapper.set(node2, new Insets(25))
// all other nodes don't get extra space

In cases where the nodeMargins for each node can be determined by looking at the node itself it's often easier to just set a mapperFunction instead of preparing a mapper:

Using a delegate to determine margins for all nodes
// Retrieve the space around the node from its tag property
layoutData.nodeMargins = (node: INode): Insets =>
  new Insets(parseFloat(node.tag))

See Also

Developer's Guide
API
NODE_MARGIN_DATA_KEY
Gets or sets the collection of grid component nodes that should be placed before the common bus segment.
If this collection is not specified, the nodes will be placed in an alternating way.
The side assignment induced by this setting is ignored if there are any sequenceConstraints between the grid nodes. Furthermore, it can happen that the specified maximum number of nodes before the bus and after the bus is exceeded due to the side placement constraints.
conversionfinal

Examples

When the placement can be readily inferred from the nodes, then the easiest option is usually to use the predicate:

layoutData.nodesBeforeBus = (node) =>
  graphComponent.selection.nodes.includes(node)

Alternatively, an existing IEnumerable<T> containing the nodes can be passed via source:

layoutData.folderNodes = graphComponent.selection.nodes

or items can be added or removed one-by-one using the items collection:

layoutData.nodesBeforeBus.items.add(node1)
layoutData.nodesBeforeBus.items.add(node2)

See Also

Developer's Guide
API
gridComponents, PLACE_BEFORE_BUS_DATA_KEY
Gets or sets the mapping from nodes to an object defining the node type, which influences the ordering of nodes during the sequencing such that nodes of the same type are preferably placed next to each other.
The node types are a subordinate criterion during the sequencing of nodes within their layer. More precisely, the sequencing algorithm prefers to place nodes of the same type next to each other if this does not induce additional crossings or conflicts with other constraints (like node groups, LayoutGrid, or sequenceConstraints. The algorithm uses an additional local optimization heuristic to improve the placement with respect to node types and, thus, does not guarantee optimal results. This additional step may significantly increase the required runtime.
Node types do not affect the layer assignment.
conversionfinal

Sample Graphs

ShownSetting: Without node types

See Also

Developer's Guide
API
NODE_TYPE_DATA_KEY
Gets or sets the sub-data that provides a way of influencing the placement of the ports.

The port placement can be influenced by specifying EdgePortCandidates for the source and target of an edge, as well as by specifying NodePortCandidates at the nodes.

If an edge that has EdgePortCandidates connects to a node with NodePortCandidates, the HierarchicalLayout tries to match both collections to find an appropriate port. An edge port candidate matches a node port candidate if

  • Their matchingIds are equal or one type is null,
  • They belong to a common side or at least one side is ANY, and
  • If both candidates are fixed, they describe the same positions.

The position of a port candidate is defined by offset or the actual offset of the edge endpoint for fixed-from-sketch candidates. In case there is no matching port, a port candidate specified for the edge is preferred.

The HierarchicalLayout does not support LayoutPortCandidates with multiple sides (e.g. RIGHT or LEFT). To model that an edge should connect at one of several sides, define multiple candidates instead, where each candidate has a single side.

In addition, it is possible to specify that ports should be grouped at the source or target. In addition, it is possible to specify that ports should be aligned at the source or target.

final
Gets or sets whether or not critical edges automatically get crossing costs assigned based on their critical edge priorities.
If enabled, an edge gets the critical edge priority plus one as its crossing cost - unless, a higher crossing cost was manually specified. Edges with a priority that is negative or zero get 1 as the crossing cost (which is the default for it). If disabled, the critical edge priorities do not automatically affect the crossing minimization. The user can still provide higher crossing costs manually, of course.
final

Default Value

The default value is: true
Critical edges automatically get crossing costs based on their priorities

See Also

API
edgeCrossingCosts, criticalEdgePriorities
Gets or sets the layout data to specify sequence constraints.
final

Examples

One way to set sequence constraints is by mapping nodes or edges to an IComparable (e.g. a number) which define the order in which those nodes are placed within the same layer. This can be done with the itemComparables property. One way to map each item to such an IComparable is to use the mapperFunction:

Using a delegate to map items to objects that can be ordered to define relative placements within a layer
// Map each item to an IComparable, in this case taken from a custom object stored in the tag
layoutData.sequenceConstraints.itemComparables = (item) => {
  const data = item.tag
  if (data !== null) {
    return data.sequenceOrder
  }
  // Don't constrain sequencing if the tag doesn't contain our custom object.
  return null
}

If only some nodes should be constrained, the mapper can be used instead:

Using a mapper to map certain items to objects that can be ordered to define relative placements within a layer
// Define sequence constraints for a few nodes via a mapper. Nodes that are not
// explicitly mapped don't have constraints on their placement within the layer.
const mapper = layoutData.sequenceConstraints.itemComparables
  .mapper as IMapper<INode, any>
// Place node1 before node2
mapper.set(node1, 1)
mapper.set(node2, 3)
// Place node3 between both of them
mapper.set(node3, 2)
// Place node4 after the others by assigning a larger value
mapper.set(node4, 8)
// The exact values don't matter much they just define an ordering. However, all
// values must have the same type you cannot mix int and string or double, for example.

If those mappings have been prepared beforehand, e.g. in a HashMap<TKey, TValue> or IMapper<K, V>, that property on the ItemMapping<TItem, TValue> can also be set:

Setting a prepared mapper or Map for relative placement within a layer
layoutData.sequenceConstraints.itemComparables = sequenceMapper
// Or create the mapping from a Map, instead:
layoutData.sequenceConstraints.itemComparables = sequenceMap

IComparable is only the most natural option if such an object can be readily produced from a node, or there already exists such a mapping from nodes to something that can be compared. In many cases it can be easier to construct constraints manually with the respective methods on SequenceConstraintData<TNode, TEdge, TItem>:

Defining custom sequence constraints
// Ensure that node1 is placed before node2
layoutData.sequenceConstraints.placeNodeBeforeNode(node1, node2)
// Also place another node at the very start
layoutData.sequenceConstraints.placeNodeAtHead(firstNodeInLayer)

Sample Graphs

ShownSetting: A graph laid out with HierarchicalLayout with default settings. The labels indicate the desired placement of certain nodes relative to a reference node.

See Also

Developer's Guide
Gets a mapper from nodes to the sequence index in their layer.
This property can be used after the layout to retrieve the sequencing for each node. If only the sequencing (and layering) information is needed and not the whole layout, HierarchicalLayout can be run with stopAfterSequencing set to true. This will skip the remaining stages of the layout and therefore reduce the runtime.
readonlyfinal

Examples

Retrieving layering and sequencing information after a layout
const layoutData = new HierarchicalLayoutData()

graph.applyLayout(new HierarchicalLayout(), layoutData)

for (const node of graph.nodes) {
  console.log(
    `Node ${node} has been placed in layer ${layoutData.layerIndicesResult.get(
      node,
    )} at position ${layoutData.sequenceIndicesResult.get(node)}`,
  )
}

See Also

API
SEQUENCE_INDEX_RESULT_DATA_KEY
Gets or sets a mapping from edges to an object representing their source edge group.

Edges sharing a source group identifier will share a common bus near the source or at a common source node if possible.

The HierarchicalLayout is able to group incoming and outgoing edges. Therefore, the source group identifier of the outgoing edges of a node has to match with the target group identifier of the incoming edges.

conversionfinal

Examples

One simple way to use source groups is to use the edge's source node as group ID which effectively groups all edges with the same source together:

Grouping all edges on the source side
layoutData.sourceGroupIds = (edge: IEdge) => edge.sourceNode

Another useful way to use a delegate here would be grouping edges by some commonality, such as the same color:

Grouping edges by color
layoutData.sourceGroupIds = (edge: IEdge) => {
  const style = edge.style
  if (style instanceof PolylineEdgeStyle) {
    return style.stroke!.fill
  }
  return null
}

If only certain edges should be grouped it may sometimes be easier to use the mapper to set the group IDs:

Grouping certain edges with a mapper
for (const group of edgeGroups) {
  for (const edge of group) {
    // Use the collection as group ID, since it's common to all edges in it
    layoutData.sourceGroupIds.mapper.set(edge, group)
  }
}

See Also

Developer's Guide
API
sourcePortGroupIds, targetGroupIds, SOURCE_EDGE_GROUP_ID_DATA_KEY
Gets or sets the mapping of nodes to a subcomponent which is arranged by the layout algorithm defined by property layoutAlgorithm.

The nodes forming a subcomponent are arranged by another layout algorithm and that sub-layout is finally embedded into the global hierarchical layout. This feature allows, for example, to highlight special sub-structures by arranging them in a different fashion. Note that subcomponents may not be nested, that is, a subcomponent is not allowed to contain nodes that belong to yet another subcomponent. Nesting of subcomponents results in an exception.

To conveniently define a subcomponent as well as the layout algorithm responsible for it use the add method of this property. It takes the HierarchicalLayoutSubcomponentDescriptor as parameter and allows to define the nodes associated to it using the returned ItemCollection<TItem>.

A subcomponent can be connected to nodes of another subcomponent as well as to nodes that are not part of any subcomponent. Edges that model these connections are so-called inter-edges. For inter-edges there apply some smaller restrictions with respect to the supported features (see the documentation of SUBCOMPONENT_DESCRIPTOR_DATA_KEY for details).

If all inter-edges of a subcomponent connect to the same external node, that single external so-called connector node is included in the calculation of the component layout when using placement policy AUTOMATIC or ALWAYS_INTEGRATED. In case the result is feasible, this results in a better and more compact integration of the component in the main layout. In that case the sub-layout is fully responsible for the routing of inter-edges (meaning the restrictions below may not apply anymore).

Subcomponents can be defined on grouped graphs with the following restrictions:

  • All subcomponent nodes must be on the same hierarchy level if the group node itself is not part of the subcomponent.
  • If a group is assigned to a subcomponent, all its descendants (including other group nodes) must be in that component, too.

When running in incremental mode, it is still up to the sub-layout algorithm to decide whether to treat a subcomponent element as incremental or fixed. If the given layout of the subcomponent should be considered, then the from-sketch mode of the used layout algorithm needs to be enabled (if such a mode is supported).

It is possible to define another HierarchicalLayout instance as layout algorithm responsible for a sub-component. This way, different hierarchical layout styles can be conveniently combined into one layout (e.g. layouts with different orientations). The best layout results are achieved if a sub-component is connected to other parts only by very few inter-edges.
For the layout of a subcomponent, the specified subcomponent layout algorithm is fully responsible. Thus, the supported features and layout style are defined solely by that algorithm.
When using a LayoutGrid, all nodes of a subcomponent need to be assigned to the same LayoutGridCellDescriptor or, alternatively, not be assigned to a cell at all. Otherwise, an exception will the thrown during layout. Furthermore, it is not possible that the layout of a subcomponent uses another layout grid instance at the same time - in other words, nested layout grids are not supported.
final

Examples

Subcomponents can be configured by first adding a new HierarchicalLayoutSubcomponentDescriptor and saving the resulting ItemCollection<TItem>. That one can then be used to define the subset of nodes which the layout should process:

// Retrieve an ItemCollection<INode> which defines all nodes
// of a subcomponent. This subcomponent will be arranged by an OrganicLayout
const organicSubset = layoutData.subcomponents.add(
  new HierarchicalLayoutSubcomponentDescriptor(new OrganicLayout()),
)
// You can use this ItemCollection<INode> as usual, using either one of
// the Source, Items, Mapper, or Delegate properties.
// Here, we simply use the selected nodes:
organicSubset.source = graphComponent.selection.nodes

Since adding a new subcomponent returns the ItemCollection<TItem>, configuring subcomponents can also be conveniently chained:

layoutData.subcomponents.add(
  new HierarchicalLayoutSubcomponentDescriptor(
    new OrganicLayout(),
    HierarchicalLayoutSubcomponentPlacementPolicy.ISOLATED,
  ),
).source = organicNodes
layoutData.subcomponents.add(
  new HierarchicalLayoutSubcomponentDescriptor(new TreeLayout()),
).source = treeNodes
layoutData.subcomponents.add(
  new HierarchicalLayoutSubcomponentDescriptor(new OrthogonalLayout()),
).source = orthogonalNodes

See Also

Developer's Guide
API
SUBCOMPONENT_DESCRIPTOR_DATA_KEY
Gets or sets the mapping from tabular group nodes to comparison functions used to sort the child nodes of the group.
The comparison assigned to a tabular group allows to define a specific order of the child nodes of that tabular group. If there are groups within a tabular group, each of them has to be associated with a comparison if a specific ordering is desired (i.e., the inner groups do not use the comparison associated with the parent group). Otherwise, the algorithm determines a suitable order of the elements within such groups.
The ordering of the children defined here is enforced even for fixed (non-incremental) elements when running the algorithm in from-sketch mode. Avoid defining an order, if tabular child nodes should be fixed so that their sketch is preserved.
The order defined here is considered more important than sequence constraints between child nodes. Head or tail constraints can only be specified for the tabular group itself and are ignored if assigned to child nodes.
conversionfinal

Examples

Ordering children by label text for all tabular groups
layoutData.tabularGroupChildComparators = (
  child1: INode,
  child2: INode,
) => child1.labels.get(0).text.localeCompare(child2.labels.get(0).text)
Ordering children of a single tabular group by using custom data
// for the comparison, we assume that child nodes have a custom data tag which holds a column property
layoutData.tabularGroupChildComparators.mapper.set(
  tabularGroupNode1,
  (child1: INode, child2: INode) =>
    child1.tag.column.CompareTo(child2.tag.column),
)

See Also

Developer's Guide
API
TABULAR_GROUP_CHILD_COMPARATOR_DATA_KEY
Gets or sets the collection of tabular group nodes whose children are arranged in a tabular fashion.

Children of tabular groups are laid out in a compact tabular fashion as shown in the figure below. Groups contained in a tabular group behave like tabular groups as well.

Child nodes are placed next to each other within the same layer with a distance defined by tabularGroupChildDistance, which is zero by default.

There are some setups for which a tabular group may not be tight (i.e., maximally compact):

  • If edges directly connect to a tabular group (not to its content), then it is not guaranteed that the group becomes maximally compact. Children may be placed a little further apart to fulfill the defined edgeDistance.
  • The same is true if there are children with self-loops. Such self-loops are always drawn within the tabular group.
  • In addition, labels of edges connecting two children of the same tabular group may intersect with other edge segments.

Besides, the table groups may not be tight if there are user specified constraints like node margins, node labels that exceed the size of the associated child node, and port candidates connecting to a side orthogonal to the layout orientation.

To define a specific order of child nodes in a tabular group it is possible to provide a custom comparison via property tabularGroupChildComparators.
Children of tabular groups are never drawn in a stacked style.
If tabular groups are defined, the bendReduction optimization cannot be applied because reduction of bends would cause tabular groups to become less compact.
conversionfinal

Examples

Using a delegate to mark all group nodes of the graph as tabular groups
layoutData.tabularGroups.predicate = (node) => graph.isGroupNode(node)
Using a delegate to marks specific groups by looking at custom data tag
// assume that the tag of a node contains a flag indicating whether a group is a tabular group
layoutData.tabularGroups.predicate = (node) => node.tag.isTabularGroup
Marking two specific nodes as tabular groups
layoutData.tabularGroups.mapper = new Mapper()
layoutData.tabularGroups.mapper.set(tabularGroupNode1, true)
layoutData.tabularGroups.mapper.set(tabularGroupNode2, true)
Using a collection to define the tabular group items
// directly assign a collection of tabular group nodes (e.g. of type List<INode>)
layoutData.tabularGroups.items = tabularGroupNodes

Sample Graphs

See Also

Developer's Guide
API
TABULAR_GROUPS_DATA_KEY
Gets or sets a mapping from edges to an object representing their target edge group.

Edges sharing a target group identifier will share a common bus near the target or at a common target node if possible.

The HierarchicalLayout is able to group incoming and outgoing edges. Therefore, the target group identifier of the incoming edges of a node has to match with the source group identifier of the outgoing edges.

conversionfinal

Examples

One simple way to use source groups is to use the edge's target node as group ID which effectively groups all edges with the same target together:

Grouping all edges on the target side
layoutData.targetGroupIds = (edge: IEdge) => edge.targetNode

Another useful way to use a delegate here would be grouping edges by some commonality, such as the same color:

Grouping edges by color
layoutData.targetGroupIds = (edge: IEdge) => {
  const style = edge.style
  if (style instanceof PolylineEdgeStyle) {
    return style.stroke!.fill
  }
  return null
}

If only certain edges should be grouped it may sometimes be easier to use the mapper to set the group IDs:

Grouping certain edges with a mapper
for (const group of edgeGroups) {
  for (const edge of group) {
    // Use the collection as group ID, since it's common to all edges in it
    layoutData.targetGroupIds.mapper.set(edge, group)
  }
}

See Also

Developer's Guide
API
sourceGroupIds, targetPortGroupIds, TARGET_EDGE_GROUP_ID_DATA_KEY
Gets or sets the collection of group nodes for which the ports of the adjacent edges are uniformly distributed.

By default, there is no guarantee that the ports at group nodes are uniformly distributed. This setting only affects the two sides of a group node that are in flow and against the flow of the layout orientation. For example, for orientation TOP_TO_BOTTOM only the edges incident to the top and bottom group node side are considered but not the edges at the left and right side.

The uniform group port assignment considers the edgeDistance such that two ports keep the specified distance to each other. Group nodes may in consequence be enlarged to satisfy that constraints, potentially generating less compact layouts. The distance from the group node border to the first/last port on each group side may be influenced by using group node padding.

Importantly, it is not guaranteed that the group node ports of a specific side can be uniformly distributed. In the following cases ports of a specific side are not uniformly distributed:

  1. There are edges that cross the group node border because they connect to a child node of the group.
  2. There is a self-loop at the group node which has its source and target port at the same group node side.
Forcing group nodes to have uniform ports can have several side-effects and also affect other parts of the layout. In general, results can become less compact and there might be more bends, because bends may be introduced where they would not be necessary when ports are not uniformly distributed.
Enabling uniform group ports for at least one group node may significantly increase the runtime of the algorithm. Also, the uniform distribution may be skipped in case that the stopDuration is restricted and there is no time left.
conversionfinal

Examples

Dynamically determine whether a node is a group node
layoutData.uniformPortAssignmentGroups = (node: INode): boolean =>
  graph.isGroupNode(node)
Using a static collection of group nodes
layoutData.uniformPortAssignmentGroups = graph.nodes.filter((node) =>
  graph.isGroupNode(node),
)

Sample Graphs

ShownSetting: Default

See Also

API
UNIFORM_PORT_ASSIGNMENT_GROUPS_DATA_KEY

Methods

Combines this instance with the given layout data.
This keeps the current instance unmodified and instead returns a new instance that dynamically combines the contents of all involved instances.
final

Parameters

data: LayoutData<TNode, TEdge, TNodeLabel, TEdgeLabel>
The LayoutData<TNode, TEdge, TNodeLabel, TEdgeLabel> to combine this instance with.

Return Value

LayoutData<TNode, TEdge, TNodeLabel, TEdgeLabel>
The combined layout data.

See Also

Developer's Guide
API
CompositeLayoutData, GenericLayoutData