documentationfor yFiles for HTML 2.6

Customizing Arrows

Customizing the Standard Arrows

yFiles for HTML is shipped with a number of predefined arrows, see also Decorations: Arrows. All the standard arrows provided with class IArrow can be configured with a custom size and color by creating a new instance of class Arrow:

A thick orange edge with the default arrow (above) and a configured arrow (below)
styles arrow scale
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',
      scale: 4,
      type: 'default'
    })
  })
)

Creating a custom Arrow

For a completely new visualization one has 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.

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

The first step is to create a class which 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 later in detail. For now, we let them return 0.
getVisualCreator
Returns an implementation of IVisualCreator. We let our custom arrow class implement IVisualCreator, too, and simply return this.
getBoundsProvider
Returns an implementation of IBoundsProvider. We let our custom arrow class implement IBoundsProvider, too, and simply return this.

An empty IArrow implementation
class CustomArrow extends BaseClass(IArrow, IVisualCreator, IBoundsProvider) {
  /**
   * @type {number}
   */
  get length() {
    return 0
  }

  /**
   * @type {number}
   */
  get cropLength() {
    return 0
  }

  /**
   * @param {!IEdge} edge
   * @param {boolean} atSource
   * @param {!Point} anchor
   * @param {!Point} direction
   * @returns {!IBoundsProvider}
   */
  getBoundsProvider(edge, atSource, anchor, direction) {
    return this
  }

  /**
   * @param {!IEdge} edge
   * @param {boolean} atSource
   * @param {!Point} anchor
   * @param {!Point} direction
   * @returns {!IVisualCreator}
   */
  getVisualCreator(edge, atSource, anchor, direction) {
    return this
  }

  /**
   * @param {!IRenderContext} context
   * @returns {?Visual}
   */
  createVisual(context) {
    return null
  }

  /**
   * @param {!IRenderContext} context
   * @param {!Visual} oldVisual
   * @returns {?Visual}
   */
  updateVisual(context, oldVisual) {
    return this.createVisual(context)
  }

  /**
   * @param {!ICanvasContext} context
   * @returns {!Rect}
   */
  getBounds(context) {
    return Rect.EMPTY
  }
}class CustomArrow extends BaseClass(IArrow, IVisualCreator, IBoundsProvider) {
  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 in a way that the anchor point (the intersection between edge and node border) lies at 0,0 and the visualization extends into negative x coordinates. On this shape we apply a transform which 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
/**
 * @param {!IEdge} edge
 * @param {boolean} atSource
 * @param {!Point} anchor
 * @param {!Point} direction
 * @returns {!IVisualCreator}
 */
getVisualCreator(edge, atSource, anchor, direction) {
  // the anchor and direction are needed in createVisual - persist them
  this.anchor = anchor
  this.direction = direction
  return this
}

anchor = Point.ORIGIN
direction = Point.ORIGIN

/**
 * @param {!IRenderContext} context
 * @returns {!SvgVisual}
 */
createVisual(context) {
  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
}

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
}

The custom arrow visual
arrow custom nolength

This is almost what we want but some details are missing: the edge extends into the head and we also wanted a little space between arrow head and 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.

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

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
/**
 * @type {number}
 */
get length() {
  return 10
}

get length(): number {
  return 10
}

With correct length
arrow custom nocrop

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
/**
 * @type {number}
 */
get cropLength() {
  return 3
}

get cropLength(): number {
  return 3
}

With correct length and cropLength
arrow custom

Adjusting the Bounds and Optimizations

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

getBounds implementation
/**
 * @param {!ICanvasContext} context
 * @returns {!Rect}
 */
getBounds(context) {
  // apply a transform on the untransformed bounds
  return new Rect(-10, -5, 10, 10).getTransformed(
    new Matrix(this.direction.x, -this.direction.y, this.direction.y, this.direction.x, this.anchor.x, this.anchor.y)
  )
}

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

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

updateVisual implementation
/**
 * @param {!IRenderContext} context
 * @param {!SvgVisual} oldVisual
 * @returns {!SvgVisual}
 */
updateVisual(context, oldVisual) {
  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)
}

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)
}