documentationfor yFiles for HTML 2.6

Basic Style Implementation

The easiest and quickest way to create a custom visualization is to use one of yFiles for HTML’s convenience base classes. Each style interface has one of those convenience classes. For example, for nodes and their INodeStyle interface, there is the abstract class NodeStyleBase<TVisual>, which only has one abstract method: createVisual.

Of course there are also similar abstract classes for edge, label, and port styles (namely EdgeStyleBase<TVisual>, LabelStyleBase<TVisual>, and PortStyleBase<TVisual>), but for simplicity’s sake we will use NodeStyleBase<TVisual> as the example for this chapter. Keep in mind that each subsection also applies to the other implementations as well.

The createVisual method has to be implemented to return an object of type Visual. Since the framework expects this method to create a new object, it is called only when really necessary, e.g. when an item is displayed for the first time. The section Predefined Visuals teaches you how to use one of the predefined Visual implementations and the section Implementing the IVisualCreator Interface explains how to create your own to return it from this method.

By extending the abstract class NodeStyleBase<TVisual> and implementing createVisual you already get a working style implementation, ready to use. It is not necessarily fast, however. Of course, there are techniques and common practices that vastly enhance the performance, which we will talk about in the subsequent section Efficient Style Implementation.

Predefined Visuals

Visual is the interface for the low-level entities in yFiles for HTML that actually visualize data by drawing on HTML5’s graphics context. The createVisual method is always called for one item at a time, which means that there is a 1:1 relationship between items and Visuals.

There are some predefined Visual implementations provided by yFiles for HTML that you can directly use.

SvgVisual
Wraps an SVGElement as Visual to create visualizations based on SVG elements.
SvgVisualGroup
Groups multiple Visuals together in one Visual. Visuals can be added or removed. Additionally, it has a transform property that is applied to the group and therefore to all of its children.
HtmlVisual
Wraps an HTMLElement as Visual to create visualizations based on HTML markup.
HtmlCanvasVisual
Base class for Visuals that use HTML5’s canvas for the visualization. The visualization is implemented by method paint. In contrast to the SvgVisual this kind of visual can not be used in a SvgVisualGroup. See also section Canvas Rendering.
WebGLVisual
Base class for Visuals that draw to an HTML5 canvas using WebGL for the visualization. The visualization is implemented by method render. In contrast to the SvgVisual this kind of visual can not be used in a SvgVisualGroup. See also section WebGL Rendering.

Implementing the IVisualCreator Interface

The SvgVisual merely serves as a container for the SVG element. The actual drawing can be implemented in the createVisual method where the Visual is created. Contextual information needed for drawing (like the bounds to draw in, or the item to visualize) is available in this method.

Efficient Style Implementation

In most cases, the performance of your custom visualization can be vastly improved with just a few easy steps.

It is not necessary to create a new Visual each time the rendering process decides to update the visible area. In fact, yFiles for HTML’s rendering engine does not throw away the Visual that was used for the previous rendering step, but asks the style implementation for an update of the Visual, given the new contextual information (e.g. changed location or size of the item to visualize). By that it gives the chance to reuse the Visual object and makes it a container for the logic to draw the information on the canvas.

For this, the updateVisual method is called when an update is queried for the canvas. This method is passed the context of the rendering process as well as the old Visual that was previously used to visualize the given item.

With overriding updateVisual and updating the old Visual with the new information (such as location and size of the item to visualize), you avoid unnecessarily re-creating objects on every drawn frame. It is therefore highly recommended to implement the updateVisual method when implementing custom styles.

Simple updateVisual override
/**
 * @param {!IRenderContext} context
 * @param {!Visual} oldVisual
 * @param {!INode} node
 * @returns {?Visual}
 */
updateVisual(context, oldVisual, node) {
  // if the old visual cannot be updated: create a new visual from scratch
  if (this.needsToBeRebuilt(oldVisual) || !(oldVisual instanceof SvgVisual)) {
    return this.createVisual(context, node)
  }
  // update the visual by updating the layout
  oldVisual.svgElement.setAttribute('transform', `translate(${node.layout.x} ${node.layout.y})`)
  // return the original visual
  return oldVisual
}
updateVisual(context: IRenderContext, oldVisual: Visual, node: INode): Visual | null {
  // if the old visual cannot be updated: create a new visual from scratch
  if (this.needsToBeRebuilt(oldVisual) || !(oldVisual instanceof SvgVisual)) {
    return this.createVisual(context, node)
  }
  // update the visual by updating the layout
  oldVisual.svgElement.setAttribute('transform', `translate(${node.layout.x} ${node.layout.y})`)
  // return the original visual
  return oldVisual
}

Additionally, you can share information that is needed to visualize items with the same style between multiple instances of Visual. This can be done by storing the information in the style instance itself and updating the Visuals in each update pass accordingly. This way you can bypass costly loading of resources or allocation of objects in your createVisual or updateVisual methods that are potentially called very often in an application’s life cycle.

Decorating Styles

It is possible to decorate existing styles with another style. This way, you can extend an existing visualization without having to subclass the other class.

The important thing to know is that in your owncreateVisual method you can easily call the wrapped style’screateVisual method to obtain the visualization of the inner style for that element.

For this, you need to obtain the renderer from the wrapped style (using its renderer property), and retrieve the IVisualCreator from the renderer via getVisualCreator.

Since the call is rather complicated, here is a demonstration of how to obtain the wrapped style’s Visual:

How to obtain the visualization of a decorated style
const visual = wrappedStyle.renderer.getVisualCreator(item, wrappedStyle).createVisual(context)

Accordingly, in your implementation of updateVisual, you should let the IVisualCreator of the wrapped style update the visualization that was also created from it:

How to update the visualization of a decorated style
const updatedVisual = wrappedStyle.renderer.getVisualCreator(item, wrappedStyle).updateVisual(context, oldVisual)

The decorated style’s updateVisual method might return a different instance. This means that if you stored the old visual somewhere (cf. example below), you may have to replace that stored instance after calling updateVisual on the wrapped style.

It is common that decorators combine style visualizations by using a SvgVisualGroup. In these cases it is important for the updateVisual method to pass Visuals to the correct style’s updateVisual method. Also, if the updateVisual methods of the decorated styles return new instances, they have to be exchanged with the old ones on the original group:

/**
 * @param {!IRenderContext} context
 * @param {!INode} node
 * @returns {!SvgVisualGroup}
 */
createVisual(context, node) {
  const group = new SvgVisualGroup()
  // add the element to decorate first to the group
  group.add(this.wrappedStyle.renderer.getVisualCreator(node, this.wrappedStyle).createVisual(context))

  // create a decoration and add it as second visual
  const decorationElement = this.createDecorationElement(node)
  group.add(new SvgVisual(decorationElement))
  return group
}

/**
 * @param {!IRenderContext} context
 * @param {!Visual} oldVisual
 * @param {!INode} node
 */
updateVisual(context, oldVisual, node) {
  const group = oldVisual
  if (group.children.size !== 2) {
    // something unexpected: rebuild from scratch
    return this.createVisual(context, node)
  }
  // the decorated style's visual is the first child
  const oldDecoratedVisual = group.children.get(0)
  // call the update visual method for the decorated style
  const newDecoratedVisual = this.wrappedStyle.renderer
    .getVisualCreator(node, this.wrappedStyle)
    .updateVisual(context, oldDecoratedVisual)
  if (newDecoratedVisual !== oldDecoratedVisual) {
    // note that the returned visual might be a new instance:
    // exchange the visual in this case
    group.children.set(0, newDecoratedVisual)
  }

  // update the decoration
  this.updateDecorationElement(node, group.children.get(1).svgElement)
  return oldVisual
}

createVisual(context: IRenderContext, node: INode): SvgVisualGroup {
  const group = new SvgVisualGroup()
  // add the element to decorate first to the group
  group.add(this.wrappedStyle.renderer.getVisualCreator(node, this.wrappedStyle).createVisual(context))

  // create a decoration and add it as second visual
  const decorationElement = this.createDecorationElement(node)
  group.add(new SvgVisual(decorationElement))
  return group
}

updateVisual(context: IRenderContext, oldVisual: Visual, node: INode) {
  const group = oldVisual as SvgVisualGroup
  if (group.children.size !== 2) {
    // something unexpected: rebuild from scratch
    return this.createVisual(context, node)
  }
  // the decorated style's visual is the first child
  const oldDecoratedVisual = group.children.get(0)
  // call the update visual method for the decorated style
  const newDecoratedVisual = this.wrappedStyle.renderer
    .getVisualCreator(node, this.wrappedStyle)
    .updateVisual(context, oldDecoratedVisual)
  if (newDecoratedVisual !== oldDecoratedVisual) {
    // note that the returned visual might be a new instance:
    // exchange the visual in this case
    group.children.set(0, newDecoratedVisual)
  }

  // update the decoration
  this.updateDecorationElement(node, group.children.get(1).svgElement)
  return oldVisual
}