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