chevron_right Demos chevron_right Tutorial: Edge Style Implementation chevron_right 04 Render Performance
03 Create Parallel Polylines
05 Making the Style Configurable

Improving rendering performance

Until now, we have only implemented the createVisual method, which creates a new DOM element for each render frame. This is not an efficient approach and will result in performance issues for large graphs.

By overriding the updateVisual method, we can optimize the rendering performance in case no visualization-relevant data of the edge has changed. For example, if the location of a bend changes, we have to update the path. This approach will greatly improve the rendering performance for gestures such as panning and zooming the viewport.

Adjusting createVisual

To be able to update the visualization in updateVisual, we have to store the values with which the initial visualization was created. In this case, this is just the path of the edge. To get proper type-checking, we first declare the type of the data cache. This is where the yFiles' utility type TaggedSvgVisual comes in handy:

// the values we use to render the graphics
type Cache = { generalPath: GeneralPath }

// the type of visual we create and update in CustomEdgeStyle
type CustomEdgeStyleVisual = TaggedSvgVisual<SVGGElement, Cache>

With this type declaration, we can augment the class declaration for our edge style. EdgeStyleBase comes with an optional type argument which specifies the exact type for the visual returned by createVisual. This type argument ensures that updateVisual expects the type of visual that is created in createVisual. Although this is not strictly necessary, it helps with the TypeScript implementation:

export class CustomEdgeStyle extends EdgeStyleBase<CustomEdgeStyleVisual> {

To properly implement the interface and store the cache value with the visual, we adjust the createVisual method, first.

protected createVisual(
  context: IRenderContext,
  edge: IEdge
): CustomEdgeStyleVisual {
  const generalPath = super.getPath(edge)!
  const croppedGeneralPath = super.cropPath(
    edge,
    IArrow.NONE,
    IArrow.NONE,
    generalPath
  )!

  const widePath = croppedGeneralPath.createSvgPath()
  widePath.setAttribute('fill', 'none')
  widePath.setAttribute('stroke', 'black')
  widePath.setAttribute('stroke-width', '4')

  const thinPath = croppedGeneralPath.createSvgPath()
  thinPath.setAttribute('fill', 'none')
  thinPath.setAttribute('stroke', 'white')
  thinPath.setAttribute('stroke-width', '2')

  const group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
  group.append(widePath, thinPath)

  // we use the factory method to create a properly typed SvgVisual
  return SvgVisual.from(group, { generalPath })

Implementing updateVisual

Finally, we are ready to add the updateVisual implementation. Thanks to type parameter, we can let our IDE create the matching signature for the updateVisual method. In the method, we update the data of both SVG paths and cache the new path if the edge’s path has changed. Otherwise, we return the old visual without changes.

protected updateVisual(
  context: IRenderContext,
  oldVisual: CustomEdgeStyleVisual,
  edge: IEdge
): CustomEdgeStyleVisual {
  const cache = oldVisual.tag
  const oldGeneralPath = cache.generalPath
  const newGeneralPath = super.getPath(edge)!

  if (!newGeneralPath.hasSameValue(oldGeneralPath)) {
    const croppedGeneralPath = super.cropPath(
      edge,
      IArrow.NONE,
      IArrow.NONE,
      newGeneralPath
    )!
    const pathData = croppedGeneralPath.createSvgPathData()

    const group = oldVisual.svgElement
    const widePath = group.children[0]
    const thinPath = group.children[1]

    widePath.setAttribute('d', pathData)
    thinPath.setAttribute('d', pathData)

    cache.generalPath = newGeneralPath
  }
  return oldVisual
}
Note
Although implementing updateVisual is technically optional, it is highly recommended for larger graphs. Refraining from an efficient implementation may result in low frame-rates during animations and interactive gestures.