documentationfor yFiles for HTML 3.0.0.3

Customizing Arrows

Customizing the Standard Arrows

yFiles for HTML includes several predefined ArrowTypes (see also Decorations: Arrows). You can configure all standard arrow types with a custom size and color by creating a new instance of the Arrow class.

styles arrow scale
A thick orange edge with the default arrow (above) and a configured arrow (below)
Using an arrow with a custom color and size
const edge = graph.createEdge(
  node1,
  node2,
  new PolylineEdgeStyle({
    stroke: '8px solid red',
    targetArrow: new Arrow({
      fill: 'red',
      lengthScale: 4,
      widthScale: 4,
      type: 'stealth'
    })
  })
)

Creating a Custom Arrow

For a completely new visualization, you need to create a custom IArrow implementation. This section demonstrates step-by-step how to implement an arrow as a hollow circle and a line with a little distance to the node.

arrow custom
The custom arrow which will be created in this section

The first step is to create a class that implements IArrow. This interface exposes the following members:

length and cropLength

The length of the arrow and the distance to the node-edge intersection. We will discuss these in detail later. For now, let them return 0.

getVisualCreator

Returns an implementation of IVisualCreator. We let our custom arrow class implement IVisualCreator and simply return this.

getBoundsProvider

Returns an implementation of IBoundsProvider. We let our custom arrow class implement IBoundsProvider and simply return this.

An empty IArrow implementation
class CustomArrow extends BaseClass(IArrow, IVisualCreator, IBoundsProvider) {
  get cropAtPort(): boolean {
    return false
  }

  get length(): number {
    return 0
  }

  get cropLength(): number {
    return 0
  }

  getBoundsProvider(
    edge: IEdge,
    atSource: boolean,
    anchor: Point,
    direction: Point
  ): IBoundsProvider {
    return this
  }

  getVisualCreator(
    edge: IEdge,
    atSource: boolean,
    anchor: Point,
    direction: Point
  ): IVisualCreator {
    return this
  }

  createVisual(context: IRenderContext): Visual | null {
    return null
  }

  updateVisual(context: IRenderContext, oldVisual: Visual): Visual | null {
    return this.createVisual(context)
  }

  getBounds(context: ICanvasContext): Rect {
    return Rect.EMPTY
  }
}

Customizing the visualization

To get the visualization, we have to implement createVisual. First, we will create the ellipse and the line so that the anchor point (the intersection between the edge and the node border) lies at 0,0 and the visualization extends into negative x coordinates. On this shape, we apply a transform that places and rotates the drawing correctly. To do so, we need to persist the direction vector and the anchor point in getVisualCreator:

getVisualCreator and createVisual
getVisualCreator(
  edge: IEdge,
  atSource: boolean,
  anchor: Point,
  direction: Point
): IVisualCreator {
  // the anchor and direction are needed in createVisual - persist them
  this.anchor = anchor
  this.direction = direction
  return this
}

anchor = Point.ORIGIN
direction = Point.ORIGIN

createVisual(context: IRenderContext): SvgVisual {
  const ellipse = document.createElementNS(
    'http://www.w3.org/2000/svg',
    'ellipse'
  )
  ellipse.setAttribute('fill', 'none')
  ellipse.setAttribute('stroke', 'black')
  ellipse.cx.baseVal.value = -5
  ellipse.cy.baseVal.value = 0
  ellipse.rx.baseVal.value = 5
  ellipse.ry.baseVal.value = 5

  const line = document.createElementNS(
    'http://www.w3.org/2000/svg',
    'line'
  )
  line.setAttribute('stroke', 'black')
  line.x1.baseVal.value = 0
  line.y1.baseVal.value = -5
  line.x2.baseVal.value = 0
  line.y2.baseVal.value = 5

  const group = new SvgVisualGroup()
  group.add(new SvgVisual(ellipse))
  group.add(new SvgVisual(line))

  // Rotate the arrow and move it to correct position
  group.transform = new Matrix(
    this.direction.x,
    -this.direction.y,
    this.direction.y,
    this.direction.x,
    this.anchor.x,
    this.anchor.y
  )

  return group
}
arrow custom nolength
The custom arrow visual

This is almost what we want, but some details are missing: the edge extends into the head, and we also want a little space between the arrow head and the node.

Adjusting Length and CropLength

The distance between the node-edge intersection and the tip of the arrow (the anchor point) is defined by the property cropLength. The distance between the tip and the point where the actual edge rendering starts is defined by the property length.

arrow custom length
Definition of length, crop length, intersection, and anchor

First, we make the edge rendering stop at the arrow. Since our head’s size is 10,10, we change the length implementation to return 10, too.

length
get length(): number {
  return 10
}
arrow custom nocrop
With correct length

To introduce a distance between the node-edge intersection and the anchor point of the arrow, we implement cropLength to return a value > 0. We want the gap to be visible but not too large, so let us set it to 3:

cropLength
get cropLength(): number {
  return 3
}
arrow custom
With correct length and cropLength

Adjusting the Bounds and Optimizations

Like custom styles, arrows can provide bounds. Our custom style implementation makes use of Matrix's calculateTransformedBounds method to return a bounding box around the rotated and translated size rectangle.

getBounds implementation
getBounds(context: ICanvasContext): Rect {
  // apply a transform on the untransformed bounds
  const matrix = new Matrix(
    this.direction.x,
    -this.direction.y,
    this.direction.y,
    this.direction.x,
    this.anchor.x,
    this.anchor.y
  )
  return matrix.calculateTransformedBounds(new Rect(-10, -5, 10, 10))
}

Also, for an IVisualCreator implementation, it is good practice to implement updateVisual as shown in section Efficient Style Implementation. Our custom implementation is simple and only requires updating the transform:

updateVisual implementation
updateVisual(context: IRenderContext, oldVisual: SvgVisual): SvgVisual {
  if (oldVisual instanceof SvgVisualGroup) {
    // update the transform
    oldVisual.transform = new Matrix(
      this.direction.x,
      -this.direction.y,
      this.direction.y,
      this.direction.x,
      this.anchor.x,
      this.anchor.y
    )
    return oldVisual
  }
  return this.createVisual(context)
}