The LayoutGraph API - Part 1: Structure
The yFiles graph structure implementation adheres to the idea that the graph should be the only instance in charge of all structural changes. (Structural changes most notably means node and edge creation and removal.) This paradigm has some important implications:
- There must exist a valid instance of type Graph to create any graph elements (“inside” this graph instance) at all.
- There is no way to create a node or an edge “outside” a graph.
- All created graph elements instantly belong to a distinct graph.
Additionally, there is a clear cascade of responsibilities for providing access to certain graph element information:
- A graph provides access to its nodes and edges.
- A node provides access to its edges; this can be all edges, or all incoming and outgoing edges, respectively.
- An edge provides access to its source and target node.
A graph can have an arbitrary number of nodes and edges, every node and edge, though, has exactly one graph. A node can have an arbitrary number of edges, but an edge always has two nodes (its source node and its target node).
Every instance of type Node and type Edge holds a reference to the graph instance it belongs to. This reference is cleared when the graph element has been removed or hidden from its graph. Furthermore, every graph element knows the position it has in the respective set of elements within its graph.
Class Graph offers access to its graph elements in several different ways. It returns enumerators and cursors to iterate over the respective sets of graph elements, or creates arrays containing references to all nodes or all edges from the graph. As an additional convenience, there are properties to get the first, respectively last element of any of the two sets.
While the returned cursors by definition have read-only behavior on the underlying container, the returned arrays actually are copies of the respective sets of elements from the graph at a certain point in time. In effect, this means that, for example, a returned node array can be modified in any way, i.e., nodes might be removed from the array or the sequence of nodes might be changed, without affecting the node set from the graph.
To change the sequence of any of the graph element sets class Graph has methods to move an element to the first or last position, respectively.
See also Collections and Iteration Mechanisms for more details.
Creating and Removing Graph Elements
When populating an existing graph with graph elements, it is important to observe certain preconditions:
- Only class Graph provides methods to create nodes and edges.
- To create an edge in a graph G it is necessary that both its source node and its target node already exist in G.
In addition to the most frequently used simple edge creation, it is also possible to specify the exact place where a newly created edge should be inserted into both the sequence of outgoing edges at its source node and the sequence of incoming edges at its target node. Furthermore, an already existing edge can be completely remodeled, i.e., both its end nodes as well as the insertion places at either end node can be changed. Reversing an edge, as another variant of changing an edge, is also supported.
Exact edge insertion shows the respective line of code for specifying the exact place of edge insertion.
The opposite operation to graph element creation is their removal. Class Graph offers methods to remove all graph elements at once, or to remove only a single node or a single edge. When removing either an instance of type Node or an instance of type Edge their reference to the containing graph is cleared.
When removing a single node from a graph, all its connecting edges are also removed. Technically, they are even removed before the node is, since an edge requires both its source node and target node to reside in the same graph.
An alternative to removing graph elements is to hide them. Hiding is a technique to only temporarily remove graph elements, i.e., taking them away from the graph structure to perform some task on the remaining graph, and thereafter putting them back into again, un-hiding them. Note that both removing and hiding a graph element only takes them away from the graph, the objects themselves still exist.
In order to be able to unhide previously hidden graph elements, it is necessary to retain them, for example, in a data structure. It is recommended to use the specialized classes for hiding of graph elements. These classes automatically keep track of all hidden elements. They are described in Hiding Graph Elements.
Hiding a graph element only differs from removing a graph element in that there is no graph event fired that signals the structural change. As a consequence, any listeners to such graph events won’t be notified when either node or edge is hidden, which can easily result in data structures getting out of sync with the actual structure of the graph.
In addition to the number of nodes and the number of edges an instance of type Graph also knows whether a single graph element belongs to it or if there is an edge connecting two nodes. The latter feature is especially useful if there is no reference of an edge at hand, for instance.
A graph element which has been hidden does not belong to any graph until it is unhidden again.
Class Node
Class Node is responsible for everything that is related to its connecting edges.
More specifically, an instance of type Node knows the overall number of connecting edges (the so-called degree of the node) as well as the division into the number of incoming edges (in-degree) and the number outgoing edges (out-degree).
A node also provides methods to easily test whether there is a connecting edge to another node.
Furthermore, it can give enumerators and cursors to iterate over all connecting edges, iterate over all incoming edges, and iterate over all outgoing edges. When iterating over all edges incoming edges appear first, outgoing edges last.
Since a self-loop is both an incoming and an outgoing edge at its node, it appears twice when iterating over all edges of the node. Also, a self-loop counts twice when asking for the degree of a node.
As a convenience, class Node furthermore provides enumerators and cursors to iterate over all its neighbors, to iterate over all its predecessors, or to iterate over all its successors. Predecessors and successors mean the nodes at the opposite of an incoming edge, and an outgoing edge, respectively.
Class Edge
The most important information an instance of type Edge provides is its source node and its target node. Closely related is the method to get the opposite when holding one of the edge’s end nodes.
Although a hidden edge holds no reference to a graph, it can still provide access to its source and target node, independent of their status.
As a convenience, class Edge's property selfLoop] additionally offers information whether the edge is a self-loop.
Ignoring the directedness of the yFiles algorithms graph structure implementation and instead interpreting it undirected would be done using the neighbors property, respectively method getNeighborCursor of class Node, or a combination of property edges, respectively method getEdgeCursor of class Node and method opposite of class Edge.
Group Nodes
As described in The Graph Model, yFiles for HTML provides an enhanced graph model with support for group nodes. In this model a node can be in another node, its parent group, and each group can contain an arbitrary number of nodes, its children.
A layout style that is suitable for group nodes typically places children of the same group near each other and have their parent enclosing them.
If an IGraph contains groups, applying a layout automatically creates a corresponding layout graph model with grouping information. Group node insets and minimum size constraints are automatically configured, too.
LayoutGraph and Graph have no intrinsic support for group nodes. Instead, a couple of dedicated data provider keys defined in class GroupingKeys must be registered with the graph to specify the information about the grouped graph’s hierarchy of nodes for automatic layout and graph analysis.
Layout preparation demonstrates how to set up a grouped graph by filling the corresponding data providers and registering them with the graph using the data provider look-up keys defined in class GroupingKeys.
The information for the node IDs and the parent node IDs is of symbolic nature that is used in the process of layout calculation to identify the proper parent for a given child, but also to find all children that belong to the same parent. Hence, it is important for the symbolic IDs to properly match between these two data providers, so that the grouped graph’s hierarchy of nodes is correctly expressed.
Encoding a grouped graph’s hierarchy of nodes in data providers demonstrates possible content for the data providers. Here, the nodes themselves are used to denote symbolic IDs for both “ordinary” nodes and group nodes. Carefully observe the usage of the indirection scheme in this example for setting up the parent-child relation.
A group node’s size is determined by the bounding box that encloses its children and additional insets that are added to each of the box’s side. To specify insets individually, a data provider can be used to hold an Insets object for each group node. This data provider is then registered with the graph using the look-up key GROUP_NODE_INSETS_DP_KEY defined in class GroupingKeys.
Defining a group node’s insets shows how to add individual Insets objects for group nodes to a node map, and how the node map is registered as a data provider with the graph.
Class FixGroupLayoutStage adds support for the fixed group node policy to both hierarchical layout and orthogonal layout. Setup for fixed group node content shows how the fixed group node policy is realized as a layout stage that is prepended to the actual layout algorithm’s invocation.
Hiding Graph Elements
This section describes features of the graph model of the layout part of yFiles. You’ll have to work with this model only if you implement your own ILayoutAlgorithm, ILayoutStage, or another type from the layout part. The section Applying an Automatic Layout describes how to apply an automatic layout to an IGraph or GraphComponent.
Support for hiding graph elements is further completed by classes LayoutGraphHider and GraphPartitionManager. In addition to the methods from class Graph, GraphHider offers various methods to hide arbitrary collections of graph elements or even all elements from the graph at once. Moreover, it automatically keeps track of all elements hidden as a result of such method calls.
To unhide graph elements, the LayoutGraphHider class provides a small set of methods that collectively employ all-at-once behavior when unhiding, i.e., all elements previously hidden are unhidden at once.
The GraphPartitionManager class extends the idea from LayoutGraphHider in a way so that it is possible to mark parts of the graph as so-called partitions and hide/unhide these.
When working with graph hierarchies, graph elements must not be hidden but instead should be removed.
Iteration Mechanisms
The yFiles cursor hierarchy with interface ICursor and its typed sub-interfaces INodeCursor and IEdgeCursor provides bidirectional iteration over a sequence of objects in a uniform fashion.
A cursor offers access to the object it currently points to, it tells whether the current position is valid or “out of range,” and finally a cursor gives the size of the sequence it iterates over. Upon creation, a cursor by default points to the first object of the underlying sequence. Moving the cursor position is possible both absolutely, i.e., to the first or last object of the sequence and relatively, i.e., to the next or the previous object. Note that the typing of the sub-interfaces takes place at the access methods, i.e., these return references of type Node and Edge, respectively.
A cursor cannot change the sequence it iterates over, particularly, it cannot remove elements from the underlying container. So effectively, a cursor constitutes a read-only view on the sequence it iterates over. To actually remove an element from the underlying container, the container itself must be accessible.
The most prominent yFiles types that provide cursors to iterate over object, node, or edge sequences are YList, NodeList, EdgeList, Graph, and Node.
Forward iteration with various cursor types and Backward iteration show the usage of cursors in conjunction with loops to iterate over a sequence of either typed or untyped objects. Note the additional cursor movement to the last position of the sequence with backward iteration.
Interface ICursor does not provide a method to remove an element from an underlying container. Instead, any elements must be removed from the container itself.
However, when an element that is pointed to by a cursor gets removed from the underlying container, the cursor does not change state, i.e., it still points to the same, now removed, element. Moving the cursor thereafter either forward or backward has the same effect as if the element was in the container, i.e., the cursor subsequently points to the next, respectively previous element from the foregoing sequence.
Once the cursor has been moved away from the removed element’s position, though, there is no way to return to that position again. In conclusion, a simple call sequence as in Call sequence to move a cursor does not necessarily return the cursor to the originating position in the sequence.
Additionally to iteration using cursors, there is another possibility to specifically iterate over the edges adjacent to a node. Non-cursor iteration over a node’s edges demonstrates this technique.
Collections
Classes NodeList and EdgeList, the typed descendants of class YList, are the most frequently encountered yFiles data structures to store graph elements.
Common to all lists is their support for the methods from interface ICollection<T> via the implementation in class YList. Beyond this implementation, class YList provides advanced list access and manipulation services which strongly rely on the possibilities offered by class ListCell.
High-level features
In addition to the methods from interface ICollection there are several further methods offered. For instance, it is possible to add elements explicitly to the front or the back of a list. Also, removing elements from the list is possible from both the front and the back. Note that the method names for removing elements from either end of the list resemble those of abstract data type stack.
Other methods offer list sorting, reversing a list, or the splicing of two lists into one.
To iterate over the elements of the list it provides an enumerator and an ICursor (respectively INodeCursor and IEdgeCursor with classes NodeList and EdgeList). See also the discussion in Iteration Mechanisms.
Efficiently removing elements from a list using a cursor shows how to efficiently remove an element from a list using a cursor. The cursor directly points to the element, there is no need to search the list.
Low-level features
Class ListCell is the building block of the doubly linked list structure. It knows both its predecessor and its successor, and provides read/write behavior for the actual data it is holding.
Using an instance of type ListCell, for example, it is possible to have fast access to the preceding, respectively succeeding cell from the containing list, or to insert new cells relatively to already contained ones. By way of the successors (predecessors) of a ListCell instance, direct iteration over the elements of a list is possible.
Binding Data to Graph Elements
This section describes features of the graph model of the layout part of yFiles. You’ll have to work with this model only if you implement your own ILayoutAlgorithm, ILayoutStage, or another type from the layout part. The section Applying an Automatic Layout describes how to apply an automatic layout to an IGraph or GraphComponent.
The concept of data accessors comprises two aspects which are commonly used in different scenarios. To bind supplemental data to graph elements that should be read-only, an implementation of interface IDataProvider suffices. We call these implementations data providers.
A data accessor with full read/write behavior, though, additionally implements interface IDataAcceptor. yFiles for HTML knows these implementations as maps, and has two dedicated interfaces already defined, INodeMap and IEdgeMap.
Both extend interface IDataMap which is a combination of interfaces IDataProvider and IDataAcceptor. Applying the two semantics it is, e.g., possible to restrict certain callees to “immutable” data, while others are allowed to make changes.
Observe that both interfaces INodeMap and IEdgeMap show identical signatures on their respective methods, using Object instead of either Node or Edge as the parameter type for their key. Actual implementations should nevertheless ensure that the keys provided have correct type.
The concept of data accessors gives a brief overview of the classes involved in the basic concepts of maps and data providers.
Maps and Data Providers
Common to all data accessor implementations which are offered by the yFiles library is that they cover all elements of a set, i.e., a node map provides values for all nodes from a graph (however, these may all be default values when there hasn’t been anything stored yet).
Storing and retrieving data associated with a node shows how node maps are used to store and retrieve arbitrary data.
const graph = getMyGraph()
const labelNodeMap = getMyNodeMap('labelNodeMap')
const counterNodeMap = getMyNodeMap('counterNodeMap')
// Bind a label to the first node of the node set.
// The bound data actually is of type string.
labelNodeMap.set(graph.firstNode, 'I am the first node!')
// Increase the value stored in 'counterNodeMap' for the last node.
// The bound data is an int.
counterNodeMap.setInt(graph.lastNode, counterNodeMap.getInt(graph.lastNode) + 1)
// Print out the label of the first node.
console.log('The name of the first node is: ')
console.log(labelNodeMap.get(graph.firstNode))
const graph: Graph = getMyGraph()
const labelNodeMap: INodeMap = getMyNodeMap('labelNodeMap')
const counterNodeMap: INodeMap = getMyNodeMap('counterNodeMap')
// Bind a label to the first node of the node set.
// The bound data actually is of type string.
labelNodeMap.set(graph.firstNode, 'I am the first node!')
// Increase the value stored in 'counterNodeMap' for the last node.
// The bound data is an int.
counterNodeMap.setInt(graph.lastNode, counterNodeMap.getInt(graph.lastNode) + 1)
// Print out the label of the first node.
console.log('The name of the first node is: ')
console.log(labelNodeMap.get(graph.firstNode))
Comparing map implementations lists the differences of some data accessor implementations.
Domain | Memory | Performance | Note | |
---|---|---|---|---|
Default Map Implementations
Although writing customized implementations for interfaces INodeMap or IEdgeMap is easy, the most frequently used way to get these is conveniently provided by class Graph. Creating default node maps shows one of the two methods that both return default implementations of these interfaces which can be used for most purposes and data types.
The maps returned by these methods hold exactly one value for a given key, i.e., no matter how many calls to any of the setter methods are issued for a given key, only the last value set will be held. Also, the type of the key given with a setter method is restricted to the respective type of graph elements, i.e., restricted to Node or Edge. The type of the value though, is not restricted to be same over the range of all nodes, for example. In fact, it would be perfectly legal to set a double value with one node, and boolean values with every other. This, however, is strongly discouraged, since it definitely leads to problems when the values will be retrieved.
Default map implementations can be created at any time, even when the graph is empty. From the moment of creation on, they will cover all graph elements from the respective set, as well as all respective elements created by the graph thereafter. However, these maps cannot cover elements that are hidden at the time of creation.
Note that these default implementations have to be properly disposed of after usage to release the allocated resources. To this end, class Graph has appropriate methods for either kind of default implementations.
const graph = getMyGraph()
// Obtain a new INodeMap default implementation from the graph.
const nodeMap = graph.createNodeMap()
// Set values for some of the nodes.
nodeMap.setNumber(graph.firstNode, 3.14)
nodeMap.setNumber(graph.lastNode, 42.0)
// Print the values stored in the node map.
graph.nodes.forEach((n) => {
console.log(`Node ${n}: ${nodeMap.getNumber(n)}`)
})
// Finally release the resources previously allocated by the createNodeMap()
// method.
graph.disposeNodeMap(nodeMap)
const graph: Graph = getMyGraph()
// Obtain a new INodeMap default implementation from the graph.
const nodeMap = graph.createNodeMap()
// Set values for some of the nodes.
nodeMap.setNumber(graph.firstNode, 3.14)
nodeMap.setNumber(graph.lastNode, 42.0)
// Print the values stored in the node map.
graph.nodes.forEach((n) => {
console.log(`Node ${n}: ${nodeMap.getNumber(n)}`)
})
// Finally release the resources previously allocated by the createNodeMap()
// method.
graph.disposeNodeMap(nodeMap)
Creating Customized Data Accessors
In addition to using the default map implementations provided by class Graph, there are further ways to create either maps or data providers. For example, data providers can be implemented so that the actual data is only implicitly defined, i.e., it is calculated on the fly when the value is asked for. This way, it is possible to “store” large amounts of data without having any memory be allocated. Using class DataProviderAdapter to create customized data providers demonstrates how to use class DataProviderAdapter to define a custom IDataProvider implementation.
// Define an IDataProvider implementation that for each node in the graph
// returns an int that is the square of the node's index.
class MyDataProvider extends DataProviderBase {
/**
* @param {*} dataHolder
* @returns {number}
*/
getInt(dataHolder) {
if (!(dataHolder instanceof YNode)) {
throw new Error('Key has wrong type.')
}
const node = dataHolder
return node.index * node.index
}
}
// Define an IDataProvider implementation that for each edge in the graph
// returns a distance value that is the difference of the values returned by
// the given IDataProvider for the source and target node.
class MyOtherDataProvider extends DataProviderBase {
/**
* @param {!IDataProvider} dp
*/
constructor(dp) {
super()
this.dp = dp
}
/**
* @param {*} dataHolder
* @returns {number}
*/
getInt(dataHolder) {
if (!(dataHolder instanceof Edge)) {
throw new Error('Key has wrong type.')
}
const edge = dataHolder
return this.dp.getInt(edge.target) - this.dp.getInt(edge.source)
}
}
/**
* @param {!Graph} graph
*/
function testDataProviders(graph) {
const edgeLengthProvider = new MyOtherDataProvider(new MyDataProvider())
// Display the values "stored" (i.e., calculated on the fly) for each edge.
graph.edges.forEach((e) => {
console.log(`Edge ${e}: ${edgeLengthProvider.getInt(e)}`)
})
}
// Define an IDataProvider implementation that for each node in the graph
// returns an int that is the square of the node's index.
class MyDataProvider extends DataProviderBase {
getInt(dataHolder: any | null): number {
if (!(dataHolder instanceof YNode)) {
throw new Error('Key has wrong type.')
}
const node = dataHolder
return node.index * node.index
}
}
// Define an IDataProvider implementation that for each edge in the graph
// returns a distance value that is the difference of the values returned by
// the given IDataProvider for the source and target node.
class MyOtherDataProvider extends DataProviderBase {
constructor(private readonly dp: IDataProvider) {
super()
}
getInt(dataHolder: any | null): number {
if (!(dataHolder instanceof Edge)) {
throw new Error('Key has wrong type.')
}
const edge = dataHolder
return this.dp.getInt(edge.target) - this.dp.getInt(edge.source)
}
}
function testDataProviders(graph: Graph): void {
const edgeLengthProvider = new MyOtherDataProvider(new MyDataProvider())
// Display the values "stored" (i.e., calculated on the fly) for each edge.
graph.edges.forEach((e) => {
console.log(`Edge ${e}: ${edgeLengthProvider.getInt(e)}`)
})
}
Class DataProviders offers a set of static methods to conveniently create several specialized data provider implementations for either nodes or edges. For instance, there are methods to create data providers from constant values, from given arrays, or from existing data providers.
Class Maps provides a set of static methods to conveniently create several specialized map implementations:
- The createNodeMap and createEdgeMap methods that take IMap<TKey,TValue> implementations and return either an INodeMap or an IEdgeMap view of the given instances. Basically, this allows for any valid IMap<TKey,TValue> implementation, e.g., IMap<TKey,TValue>, to be used in conjunction with the yFiles graph implementation.
- The various createIndexNodeMapForBoolean and createIndexEdgeMapForBoolean methods return map implementations that are fast and at the same time use little memory.
HashMap-backed map implementations
HashMap implementations are true multi-purpose data holders, there is no restriction to the type of the keys nor the type of the values. Particularly, the keys are not restricted to graph elements. Compared to the default implementations of interface INodeMap and IEdgeMap provided by class Graph they are generally a bit slower. However, their memory usage is proportional to the amount of the data that is actually associated with the entities.
Map implementations backed by an instance of type HashMap are especially suited for sparsely distributed data, i.e., only few entities of a domain have non-null data associated with them.
Index-based map implementations
Index-based map implementations are very fast and use only little memory, their drawback, however, is that once instantiated their values are restricted to the type given at creation time. More important though, all index-based containers require the respective set of graph elements to remain unaltered, i.e., there are no operations allowed that change the sequence of the graph elements in any way.
const graph = getMyGraph()
// Create an edge map that holds a boolean value for each edge from
// the edge set of the graph.
const map = Maps.createIndexEdgeMapForBoolean(new Array(graph.e))
// Store some data into the edge map.
// (For each edge the map will contain the boolean value indicating whether
// the edge points from a node with a lower index to a node with
// a higher index.)
graph.nodes.forEach((n) => {
n.edges.forEach((e) => {
map.setBoolean(e, e.source.index < e.target.index)
})
})
const graph: Graph = getMyGraph()
// Create an edge map that holds a boolean value for each edge from
// the edge set of the graph.
const map = Maps.createIndexEdgeMapForBoolean(new Array(graph.e))
// Store some data into the edge map.
// (For each edge the map will contain the boolean value indicating whether
// the edge points from a node with a lower index to a node with
// a higher index.)
graph.nodes.forEach((n) => {
n.edges.forEach((e) => {
map.setBoolean(e, e.source.index < e.target.index)
})
})
Notes on Data Accessors
Successfully using a data accessor requires a kind of “agreement” on the type the data accessor holds. More precisely, storing the value for a key and retrieving the value thereafter has to be done using setter and getter methods of matching type. The result when using setter/getter methods with non-matching types highly depends on the specific data accessor implementation. Successfully using data accessors demonstrates the proper usage of a data accessor.
This rule furthermore implies that a self-made data accessor has to provide the proper getter (and/or setter) methods when it is used as a parameter to an algorithm.
const graph = getMyGraph()
// get a default INodeMap implementation from the graph.
const nm = graph.createNodeMap()
// Store values for some chosen nodes.
nm.setBoolean(graph.firstNode, true)
nm.setBoolean(graph.lastNode, true)
// Retrieve the stored values.
// WRONG. WRONG. WRONG.
// Boolean values cannot be retrieved as ints.
const firstValue = nm.getInt(graph.firstNode)
const lastValue = nm.getInt(graph.lastNode)
// Retrieve the stored values.
// RIGHT.
const first = nm.getBoolean(graph.firstNode)
const last = nm.getBoolean(graph.lastNode)
const graph: Graph = getMyGraph()
// get a default INodeMap implementation from the graph.
const nm = graph.createNodeMap()
// Store values for some chosen nodes.
nm.setBoolean(graph.firstNode, true)
nm.setBoolean(graph.lastNode, true)
// Retrieve the stored values.
// WRONG. WRONG. WRONG.
// Boolean values cannot be retrieved as ints.
const firstValue = nm.getInt(graph.firstNode)
const lastValue = nm.getInt(graph.lastNode)
// Retrieve the stored values.
// RIGHT.
const first = nm.getBoolean(graph.firstNode)
const last = nm.getBoolean(graph.lastNode)
Binding Data to Elements of IGraph
Most layout algorithms can be configured with additional data. Internally, they are using data providers for this task. When applying a layout on an IGraph, there are different ways to provide the data.
The most recommended way is to use the corresponding LayoutData. For instance, HierarchicLayout can be configured using HierarchicLayoutData. Besides type safety and code completion, LayoutData has the advantage that it registers data providers on the internally used LayoutGraph while the original IGraph is not affected.
If one uses a custom layout stage, an instance of GenericLayoutData can be used. It provides the possibility of registering ItemCollection<TItem>s and ItemMapping<TItem,TValue>s with arbitrary data provider keys. Like the predefined LayoutData types, the GenericLayoutData leaves the original IGraph unaffected.
// an example data provider key for an ItemCollection:
// the corresponding provider returns true for each node which is contained in the collection
const IncludedNodesDpKey = new NodeDpKey(YBoolean.$class, ExampleStage.$class, 'IncludedNodesDpKey')
// an example data provider key for an ItemMapping:
// the corresponding provider returns a number for each edge
const EdgeWeightDpKey = new EdgeDpKey(YNumber.$class, ExampleStage.$class, 'EdgeWeightDpKey')
// generate generic data
const genericData = new GenericLayoutData()
// add an ItemCollection for IncludedNodesDpKey
const includedNodes = genericData.addNodeItemCollection(IncludedNodesDpKey)
includedNodes.delegate = (node) => 'IncludeMe' === node.tag
// add an ItemMapping for EdgeWeightDpKey
const edgeWeights = genericData.addEdgeItemMapping(EdgeWeightDpKey)
edgeWeights.mapper.set(edge1, 10)
edgeWeights.mapper.set(edge2, 20)
graphComponent.morphLayout(layout, '0.5s', genericData)
// an example data provider key for an ItemCollection:
// the corresponding provider returns true for each node which is contained in the collection
const IncludedNodesDpKey = new NodeDpKey(YBoolean.$class, ExampleStage.$class, 'IncludedNodesDpKey')
// an example data provider key for an ItemMapping:
// the corresponding provider returns a number for each edge
const EdgeWeightDpKey = new EdgeDpKey(YNumber.$class, ExampleStage.$class, 'EdgeWeightDpKey')
// generate generic data
const genericData = new GenericLayoutData()
// add an ItemCollection for IncludedNodesDpKey
const includedNodes = genericData.addNodeItemCollection(IncludedNodesDpKey)
includedNodes.delegate = (node: INode) => 'IncludeMe' === node.tag
// add an ItemMapping for EdgeWeightDpKey
const edgeWeights = genericData.addEdgeItemMapping(EdgeWeightDpKey)
edgeWeights.mapper.set(edge1, 10)
edgeWeights.mapper.set(edge2, 20)
graphComponent.morphLayout(layout, '0.5s', genericData)
Although not recommended, using IGraph's way to bind data using IMapper<K,V> and IMapperRegistry is another way to provide data for layout algorithms. LayoutExecutor or morphLayout create data providers for each IMapper<K,V> and transparently map from a Node to its original INode.
Time Complexity
Time complexities shows time complexities for several of the most frequent tasks when working with the yFiles graph structure implementation.
Creation of graph elements takes constant time as does the removal of an edge. Removing a node, though, implies the removal of all its connecting edges first, so this task takes linear time, i.e., time proportional to the number of connecting edges. Also, all iteration is done in linear time. Checking whether a given node or edge belong to a graph is done in constant time, while testing if there is an edge connecting two nodes takes linear time.
Task | Involved Type(s) | Complexity |
---|---|---|