documentationfor yFiles for HTML 2.6

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. You can get it from the graph with the IGraph.decorator property.

GraphDecorator is organized in two levels. GraphDecorator itself provides a decorator for each graph element type, namely 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 make this more clear.

For example, the following decorator is responsible for the INodeSizeConstraintProvider interface:

const decorator = graph.decorator.nodeDecorator.sizeConstraintProviderDecorator

The read-only properties of these type-specific decorators actually are factory methods, so 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 care about the details of the lookup concept and its related types to decorate graph elements.

Explore the available decorators for each element type by simple code completion or looking into the API documentation.

Setting a Decoration

Once you got a specific decorator, you can decorate an item in the following ways:

  • Set a single implementation
  • Wrap the default implementation, i.e. the implementation that would be used without your decoration
  • Set 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 that are actually decorated.

All corresponding methods are specified by LookupDecorator<TDecoratedType,TInterface> and listed in the following.

setImplementation(implementation: TInterface): IContextLookupChainLink
setImplementation(predicate: Predicate<TDecoratedType>, implementation: TInterface): IContextLookupChainLink
Decorates all items (the selected items) with a single implementation.
setFactory(factory: Factory<TDecoratedType, TInterface>): IContextLookupChainLink
setFactory(predicate: Predicate<TDecoratedType>, factory: Func2<TDecoratedType, TInterface>): IContextLookupChainLink
Decorates all items (the selected items) with a factory method of the desired implementations. In this case, the only parameter of the factory method is the graph element.

For setImplementation and setFactory the nullIsFallback property determines how null values are handled that have been returned by the implementation or factory.

setImplementationWrapper(factory: WrapperFactory<TDecoratedType, TInterface>): IContextLookupChainLink
Decorates all items (the selected items) with a factory method of the desired implementations. In this case, the parameters of the factory method are the graph element and the default implementation, i.e. the implementation that would be used without this decoration.

For setImplementationWrapper the decorateNulls property determines if the implementation wrapper is called at all when the remaining lookup chain returns null for this interface. Per default the wrapper is not called when the wrapped implementation would be null.

hideImplementation(predicate: Predicate<TDecoratedType>): IContextLookupChainLink
Hides the default implementation, i.e. the implementation that would be used without this decoration.

Removing a Decoration

All of the above methods that set a decoration return an instance of IContextLookupChainLink. Store this instance if you intend to remove the decoration later, and simply ignore it otherwise.

To remove a decoration, get the graph’s ILookupDecorator and use its removeLookup method to remove the chain link instance. The following example shows how to remove a node size constraint provider.

// register the decoration
// store the link in a field
const chainLink = graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementation(
  new NodeSizeConstraintProvider(new Size(20, 20), new Size(200, 200))
)

// ... somewhere later ...

// unregister the decoration
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.decorator.removeLookup(INode.$class, chainLink)
// register the decoration
// store the link in a field
const chainLink = graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementation(
  new NodeSizeConstraintProvider(new Size(20, 20), new Size(200, 200))
)

// ... somewhere later ...

// unregister the decoration
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.decorator!.removeLookup(INode.$class, chainLink!)

Examples

This first example shows how to disable size constraints for nodes.

graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.hideImplementation()

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.nodeDecorator.sizeConstraintProviderDecorator.setImplementation(constraintProvider)

If the implementation needs to know the item it handles, set a factory method which is invoked for each item. The following example shows how to constrain a node’s size to half its current size at minimum and double its current size at maximum.

graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setFactory(
  (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. This can be done by decorating the default implementation in a wrapper implementation. The following example shows how to constrain a node’s size to half the values the default implementation does.

graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped) => {
  if (wrapped == null) {
    return null
  }
  return new (class extends BaseClass(INodeSizeConstraintProvider) {
    /**
     * @param {!INode} node
     * @returns {!Size}
     */
    getMinimumSize(node) {
      return wrapped.getMinimumSize(node).multiply(0.5)
    }

    /**
     * @param {!INode} node
     * @returns {!Size}
     */
    getMaximumSize(node) {
      return wrapped.getMaximumSize(node).multiply(0.5)
    }

    /**
     * @param {!INode} node
     */
    getMinimumEnclosedArea(node) {
      return wrapped.getMinimumEnclosedArea(node)
    }
  })()
})
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped) => {
  if (wrapped == null) {
    return null
  }
  return new (class extends BaseClass(INodeSizeConstraintProvider) {
    getMinimumSize(node: INode): Size {
      return wrapped.getMinimumSize(node).multiply(0.5)
    }

    getMaximumSize(node: INode): Size {
      return wrapped.getMaximumSize(node).multiply(0.5)
    }

    getMinimumEnclosedArea(node: INode) {
      return wrapped.getMinimumEnclosedArea(node)
    }
  })()
})

Note that per default this wrapper implementation is not called if wrapped is null. If null should be decorated as well, this has to be enabled and the code has to handle the possible null value:

const sizeConstraintProviderDecorator = graph.decorator.nodeDecorator.sizeConstraintProviderDecorator
// our implementation wrapper should also be called to wrap null values
sizeConstraintProviderDecorator.decorateNulls = true
sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped) => {
  const nonNullWrapped = wrapped || new NodeSizeConstraintProvider(node.layout, node.layout.toSize().multiply(2.0))
  return new NodeSizeConstraintProviderWrapper(nonNullWrapped)
})
const sizeConstraintProviderDecorator = graph.decorator.nodeDecorator.sizeConstraintProviderDecorator
// our implementation wrapper should also be called to wrap null values
sizeConstraintProviderDecorator.decorateNulls = true
sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped): INodeSizeConstraintProvider => {
  const nonNullWrapped = wrapped || new NodeSizeConstraintProvider(node!.layout, node!.layout.toSize().multiply(2.0))
  return new NodeSizeConstraintProviderWrapper(nonNullWrapped)
})

To change the decorateNulls value for a custom wrapper, the LookupDecorator<TDecoratedType,TInterface> instance has to be stored in a local variable as accessing the decorator properties twice results in two different instances. Therefore the following code would not work as expected:

graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.decorateNulls = true // this has no effect!!!
// the second SizeConstraintProviderDecorator access returns a new LookupDecorator instance
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped) => {
  // 'wrapped' is never null as this wrapper wouldn't be called in this case
  const nonNullWrapped = wrapped || new NodeSizeConstraintProvider(node.layout, node.layout.toSize().multiply(2.0))
  return new NodeSizeConstraintProviderWrapper(nonNullWrapped)
})
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.decorateNulls = true // this has no effect!!!
// the second SizeConstraintProviderDecorator access returns a new LookupDecorator instance
graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementationWrapper((node, wrapped) => {
  // 'wrapped' is never null as this wrapper wouldn't be called in this case
  const nonNullWrapped = wrapped || new NodeSizeConstraintProvider(node!.layout, node!.layout.toSize().multiply(2.0))
  return new NodeSizeConstraintProviderWrapper(nonNullWrapped)
})

As already mentioned, all methods have an overload which uses a given predicate to decorate only items that meet the predicate’s conditions. The following example shows how to constrain only nodes which are “normal” nodes, i.e., not group nodes:

graph.decorator.nodeDecorator.sizeConstraintProviderDecorator.setImplementation(
  (node) => node.tag === 'constrained',
  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 independent of each other. The following example first disables the reshape handles for all nodes by hiding the default implementation and after that adds a customized reshape behavior only for group nodes:

// first hide default reshape handle provider for all nodes
graph.decorator.nodeDecorator.reshapeHandleProviderDecorator.hideImplementation()
// only for group nodes a custom reshape handle provider is set that provides handles at the corners
graph.decorator.nodeDecorator.reshapeHandleProviderDecorator.setFactory(
  (node) => graph.isGroupNode(node),
  (node) => {
    const reshapeHandler = node.lookup(IReshapeHandler.$class)
    const constraintProvider = node.lookup(INodeSizeConstraintProvider.$class)
    const nodeReshapeHandleProvider = new NodeReshapeHandleProvider(node, reshapeHandler, HandlePositions.CORNERS)
    nodeReshapeHandleProvider.minimumSize = constraintProvider.getMinimumSize(node)
    nodeReshapeHandleProvider.maximumSize = constraintProvider.getMaximumSize(node)
    nodeReshapeHandleProvider.minimumEnclosedArea = constraintProvider.getMinimumEnclosedArea(node)

    return nodeReshapeHandleProvider
  }
)
// first hide default reshape handle provider for all nodes
graph.decorator.nodeDecorator.reshapeHandleProviderDecorator.hideImplementation()
// only for group nodes a custom reshape handle provider is set that provides handles at the corners
graph.decorator.nodeDecorator.reshapeHandleProviderDecorator.setFactory(
  (node) => graph.isGroupNode(node),
  (node) => {
    const reshapeHandler = node.lookup(IReshapeHandler.$class) as IReshapeHandler
    const constraintProvider = node.lookup(INodeSizeConstraintProvider.$class) as INodeSizeConstraintProvider
    const nodeReshapeHandleProvider = new NodeReshapeHandleProvider(node, reshapeHandler, HandlePositions.CORNERS)
    nodeReshapeHandleProvider.minimumSize = constraintProvider.getMinimumSize(node)
    nodeReshapeHandleProvider.maximumSize = constraintProvider.getMaximumSize(node)
    nodeReshapeHandleProvider.minimumEnclosedArea = constraintProvider.getMinimumEnclosedArea(node)

    return nodeReshapeHandleProvider
  }
)