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 these convenience classes. For example, for nodes and their INodeStyle interface, there is the abstract class NodeStyleBase<TVisual>, which has only 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, 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 must 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, for example, when an item is displayed for the first time. Predefined Visuals teaches you how to use one of the predefined Visual implementations, and 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. However, it is not necessarily fast. Of course, there are techniques and common practices that vastly enhance the performance, which we will talk about in Efficient Style Implementation.
Predefined Visuals
Visual is the interface for the low-level entities in yFiles for HTML that visualize data by drawing on HTML’s graphics context. The createVisual method is called for one item at a time, establishing a 1:1 relationship between items and Visuals.
yFiles for HTML provides some predefined Visual implementations that you can use directly.
- SvgVisual
- Wraps an SVGElement as a Visual to create visualizations based on SVG elements.
- SvgVisualGroup
- Groups multiple Visuals together into 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 HTML’s canvas for the visualization. The visualization is implemented by the render method. In contrast to the SvgVisual, this kind of visual cannot be used in a SvgVisualGroup. See also section Canvas Rendering.
- WebGLVisual
- Base class for Visuals that draw to an HTML canvas using WebGL for the visualization. The visualization is implemented by the render method. In contrast to the SvgVisual, this kind of visual cannot be used in a SvgVisualGroup.
Implementing the IVisualCreator Interface
The SvgVisual acts as a container for the SVG element. The actual drawing is implemented in the createVisual method where the Visual is created. This method provides contextual information needed for drawing, such as the bounds in which to draw, or the item to visualize.
Efficient Style Implementation
In most cases, the performance of your custom visualization can be greatly 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 discard the Visual that was used for the previous rendering step. Instead, it asks the style implementation to update the Visual, given the new contextual information (e.g., changed location or size of the item to visualize). This provides an opportunity to reuse the Visual object and makes it a container for the logic to draw the information on the canvas.
The updateVisual method is called when an update is requested for the canvas. This method receives the context of the rendering process and the old Visual that was previously used to visualize the given item.
By 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. Therefore, it is highly recommended to implement the updateVisual method when implementing custom styles.
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.
Delegating Styles
If one of the predefined styles is almost what you need, but you want to reconfigure its properties dynamically, or you want to use a customized or domain-specific API, then delegating styles are a good solution.
There are delegating styles for nodes, edges, labels, and ports. To use one, you only need to implement the
corresponding getStyle
method. This method takes the item and returns a configured style instance.
In the following example, a RectangleNodeStyle is used to delegate to, and the node’s tag is used to configure its background color.
class MyDelegatingNodeStyle extends DelegatingNodeStyle {
delegatingStyle = new RectangleNodeStyle()
getStyle(node) {
// configure delegatingStyle depending on the tag of the given node
if (node.tag instanceof Fill) {
this.delegatingStyle.fill = node.tag
} else {
this.delegatingStyle.fill = 'darkgray'
}
return this.delegatingStyle
}
}
class MyDelegatingNodeStyle extends DelegatingNodeStyle {
private readonly delegatingStyle: RectangleNodeStyle =
new RectangleNodeStyle()
protected getStyle(node: INode): INodeStyle {
// configure delegatingStyle depending on the tag of the given node
if (node.tag instanceof Fill) {
this.delegatingStyle.fill = node.tag
} else {
this.delegatingStyle.fill = 'darkgray'
}
return this.delegatingStyle
}
}
Composing Styles
It is possible to combine multiple existing styles into one composite style.
There are composite styles for nodes, edges, labels, and ports. All of them simply take the styles to combine in their constructor.
Most aspects, such as visualization or hit testing, will be delegated to all style instances. However, for other aspects, such as looking up the style context, a 'main' style is defined.
The following example code defines an edge style composed of a thicker 'outer' style in brown and a thinner 'inner' style in orange.
const outerEdgeStyle = new PolylineEdgeStyle({
stroke: new Stroke({
fill: 'saddleBrown',
thickness: 4,
lineCap: LineCap.SQUARE
}),
sourceArrow: new Arrow(
ArrowType.ELLIPSE,
'saddleBrown',
null,
1.61,
1.61,
0.6
),
targetArrow: new Arrow(ArrowType.TRIANGLE, 'saddleBrown', null, 3, 3)
})
const innerEdgeStyle = new PolylineEdgeStyle({
stroke: new Stroke({
fill: 'darkOrange',
thickness: 2,
lineCap: LineCap.SQUARE
}),
sourceArrow: new Arrow(
ArrowType.ELLIPSE,
'darkOrange',
null,
1.25,
1.25,
1.7
),
targetArrow: new Arrow(
ArrowType.TRIANGLE,
'darkOrange',
null,
2.5,
2.5,
3
)
})
const composite = new CompositeEdgeStyle(outerEdgeStyle, innerEdgeStyle)
const outerEdgeStyle = new PolylineEdgeStyle({
stroke: new Stroke({
fill: 'saddleBrown',
thickness: 4,
lineCap: LineCap.SQUARE
}),
sourceArrow: new Arrow(
ArrowType.ELLIPSE,
'saddleBrown',
null,
1.61,
1.61,
0.6
),
targetArrow: new Arrow(ArrowType.TRIANGLE, 'saddleBrown', null, 3, 3)
})
const innerEdgeStyle = new PolylineEdgeStyle({
stroke: new Stroke({
fill: 'darkOrange',
thickness: 2,
lineCap: LineCap.SQUARE
}),
sourceArrow: new Arrow(
ArrowType.ELLIPSE,
'darkOrange',
null,
1.25,
1.25,
1.7
),
targetArrow: new Arrow(
ArrowType.TRIANGLE,
'darkOrange',
null,
2.5,
2.5,
3
)
})
const composite: CompositeEdgeStyle = new CompositeEdgeStyle(
outerEdgeStyle,
innerEdgeStyle
)
This results in an edge style that appears to have a brown border around the orange path with source and target arrows.
