Decorating Graph Elements
Many aspects of the user interaction and of the visualization of selection, focus, and custom highlight can be customized by implementing a small, dedicated interface. To use such a customization, you can decorate the whole graph or any of its elements with your implementation.
IGraph provides an instance of GraphDecorator for this purpose, which you can get from the graph using the IGraph.decorator property.
GraphDecorator is organized in two levels. GraphDecorator itself provides a decorator for each graph element type: NodeDecorator, EdgeDecorator, LabelDecorator, PortDecorator, and BendDecorator. Then, each of these type-specific decorators provides properties to define a certain aspect of user interaction or visualization for its element type. The following example will clarify this structure.
For example, the following decorator is responsible for the INodeSizeConstraintProvider interface:
const decorator = graph.decorator.nodes.sizeConstraintProvider
The read-only properties of these type-specific decorators are actually factory methods. This means that new LookupDecorator<TDecoratedType,TInterface> instances are returned each time the properties are accessed. The Examples show the advantages and pitfalls of this behavior.
Aside from the information provided in this section, you typically don’t need to be concerned with the details of the lookup concept and its related types to decorate graph elements.
Explore the available decorators for each element type by using code completion or looking at the API documentation.
Setting a Decoration
Once you have a specific decorator, you can decorate an item in the following ways:
- Add a constant implementation.
- Add a factory that wraps the default implementation (the implementation that would be used without your decoration).
- Add a factory that creates item-specific implementations.
- Hide the default implementation.
For each of these options, you can either decorate all elements of that type or specify a predicate that selects the items to be decorated.
All corresponding methods are specified by LookupDecorator<TDecoratedType,TInterface> and listed below:
- addConstant(implementation: TInterface): IContextLookupChainLink
- addConstant(predicate: Predicate<TDecoratedType>, implementation: TInterface): IContextLookupChainLink
- Decorates all items (or the selected items) with a single constant implementation.
- addFactory(factory: Factory<TDecoratedType, TInterface>): IContextLookupChainLink
- addFactory(predicate: Predicate<TDecoratedType>, factory: Factory<TDecoratedType, TInterface>): IContextLookupChainLink
- Decorates all items (or the selected items) with a factory method that provides the desired implementations. In this case, the only parameter of the factory method is the graph element.
- addWrapperFactory(factory: WrapperFactory<TDecoratedType, TInterface>): IContextLookupChainLink
- addWrapperFactory(predicate: Predicate<TDecoratedType>, factory: WrapperFactory<TDecoratedType, TInterface>): IContextLookupChainLink
- Decorates all items (or the selected items) with a factory method that provides the desired implementations. In this case, the parameters of the factory method are the graph element and the default implementation (the implementation that would be used without this decoration).
- hide(predicate: Predicate<TDecoratedType>): IContextLookupChainLink
- Hides the default implementation (the implementation that would be used without this decoration).
Removing a Decoration
The methods described above for setting a decoration return an instance of IContextLookupChainLink. Store this instance if you plan to remove the decoration later; otherwise, you can ignore it.
To remove a decoration, use the same decorator you used to create it. For example, to remove a decoration for nodes, call the NodeDecorator's remove method. The process is similar for other types of items. The following example demonstrates how to remove a node size constraint provider.
// register the decoration
// store the link in a field
const chainLink = graph.decorator.nodes.sizeConstraintProvider.addConstant(
new NodeSizeConstraintProvider(new Size(20, 20), new Size(200, 200))
)
// ... somewhere later ...
// unregister the decoration
graph.decorator.nodes.remove(chainLink)
Examples
The first example demonstrates how to disable size constraints for nodes.
graph.decorator.nodes.sizeConstraintProvider.hide()
The next example shows how to set the same size constraint provider for all nodes.
const constraintProvider = new NodeSizeConstraintProvider(
new Size(20, 20),
new Size(200, 200)
)
graph.decorator.nodes.sizeConstraintProvider.addConstant(constraintProvider)
If the implementation needs to know the item it handles, set a factory method that is invoked for each item. The following example demonstrates how to constrain a node’s size to a minimum of half its current size and a maximum of double its current size.
graph.decorator.nodes.sizeConstraintProvider.addFactory(
(node) =>
new NodeSizeConstraintProvider(
new Size(node.layout.width / 2, node.layout.height / 2),
new Size(node.layout.width * 2, node.layout.height * 2)
)
)
Another common use case is to modify the default implementation. You can achieve this by wrapping the default implementation in a decorator implementation. The following example demonstrates how to constrain a node’s size to half the values of the default implementation.
graph.decorator.nodes.sizeConstraintProvider.addWrapperFactory(
(_node, originalProvider) =>
originalProvider != null
? INodeSizeConstraintProvider.create({
getMinimumSize() {
return originalProvider.getMinimumSize().multiply(0.5)
},
getMaximumSize() {
return originalProvider.getMaximumSize().multiply(0.5)
},
getMinimumEnclosedArea() {
return originalProvider.getMinimumEnclosedArea()
}
})
: null
)
graph.decorator.nodes.sizeConstraintProvider.addWrapperFactory(
(_node, originalProvider) =>
originalProvider != null
? INodeSizeConstraintProvider.create({
getMinimumSize(): Size {
return originalProvider.getMinimumSize().multiply(0.5)
},
getMaximumSize(): Size {
return originalProvider.getMaximumSize().multiply(0.5)
},
getMinimumEnclosedArea() {
return originalProvider.getMinimumEnclosedArea()
}
})
: null
)
As mentioned earlier, all methods have an overload that uses a given predicate to decorate only items that meet the predicate’s conditions. The following example shows how to constrain only nodes that are “normal” nodes, i.e., not group nodes:
graph.decorator.nodes.sizeConstraintProvider.addConstant(
(node) => !graph.isGroupNode(node),
new NodeSizeConstraintProvider(new Size(20, 20), new Size(200, 200))
)
Because each decorator property access creates a new LookupDecorator<TDecoratedType,TInterface>, it is easy to configure different behavior for different graph elements independently. The following example first disables the reshape handles for all nodes by hiding the default implementation, and then adds a customized reshape behavior only for group nodes:
// first hide default reshape handle provider for all nodes
graph.decorator.nodes.reshapeHandleProvider.hide()
// only for group nodes a custom reshape handle provider is set that provides handles at the corners
graph.decorator.nodes.reshapeHandleProvider.addFactory(
(node) => graph.isGroupNode(node),
(node) => {
const reshapeHandler = node.lookup(IReshapeHandler)
const constraintProvider = node.lookup(INodeSizeConstraintProvider)
const nodeReshapeHandleProvider = new NodeReshapeHandleProvider(
node,
reshapeHandler,
HandlePositions.CORNERS
)
nodeReshapeHandleProvider.minimumSize =
constraintProvider.getMinimumSize()
nodeReshapeHandleProvider.maximumSize =
constraintProvider.getMaximumSize()
nodeReshapeHandleProvider.minimumEnclosedArea =
constraintProvider.getMinimumEnclosedArea()
return nodeReshapeHandleProvider
}
)
// first hide default reshape handle provider for all nodes
graph.decorator.nodes.reshapeHandleProvider.hide()
// only for group nodes a custom reshape handle provider is set that provides handles at the corners
graph.decorator.nodes.reshapeHandleProvider.addFactory(
(node) => graph.isGroupNode(node),
(node) => {
const reshapeHandler = node.lookup(IReshapeHandler) as IReshapeHandler
const constraintProvider = node.lookup(
INodeSizeConstraintProvider
) as INodeSizeConstraintProvider
const nodeReshapeHandleProvider = new NodeReshapeHandleProvider(
node,
reshapeHandler,
HandlePositions.CORNERS
)
nodeReshapeHandleProvider.minimumSize =
constraintProvider.getMinimumSize()
nodeReshapeHandleProvider.maximumSize =
constraintProvider.getMaximumSize()
nodeReshapeHandleProvider.minimumEnclosedArea =
constraintProvider.getMinimumEnclosedArea()
return nodeReshapeHandleProvider
}
)