documentationfor yFiles for HTML 2.6

Refining a Style’s Behavior

There are other methods that you can override in NodeStyleBase<TVisual>. These methods wire up the internal logic for the visualizations and are actually delegated to from several interfaces.

The original interfaces of these methods are:

IHitTestable

isHit(IInputModeContext, Point, INode), delegated to from IHitTestable:

Used for detection of hits, for example for mouse clicks, hover, or drag and drop. Also, methods that try to check for items at a certain location, such as GraphComponent.hitElementsAt, rely on the implementation of this interface.

Customizing the hit tests
Default: the hand cursor wrongly indicates the node as selectable anywhere within its bounding box
Overridden: the node is not considered as hit outside its elliptical shape

The ICanvasContext provides the hitTestRadius property (which can be set on GraphComponent). This is a distance that should be used for fuzzy testing of whether a shape has been hit. It is especially important for things like edge paths or points where it can be hard to exactly hit a certain pixel with the mouse. The hit test radius is given in view coordinates to ensure it does not depend on the zoom level. The example below takes the hit test radius into account by passing it as a parameter to GeomUtilities.ellipseContains.

Considering an elliptical shape in isHit
/**
 * @param {!IInputModeContext} context
 * @param {!Point} p
 * @param {!INode} node
 * @returns {boolean}
 */
isHit(context, p, node) {
  return GeomUtilities.ellipseContains(node.layout.toRect(), p, context.hitTestRadius / context.zoom)
}

isHit(context: IInputModeContext, p: Point, node: INode): boolean {
  return GeomUtilities.ellipseContains(node.layout.toRect(), p, context.hitTestRadius / context.zoom)
}

IMarqueeTestable

isInBox(IInputModeContext, Rect, INode), delegated to from IMarqueeTestable:

Is used to test whether an item is hit by the marquee selection and should therefore be selected. It is at the discretion of the style whether intersection with the item’s visualization is enough to select it or whether the marquee selection box must fully encompass the item. All styles included with yFiles for HTML only consider intersection.

Customizing the marquee test
The marquee selection box should have to intersect with the elliptical shape
Considering an elliptical shape in the isInBox method
/**
 * @param {!IInputModeContext} context
 * @param {!Rect} box
 * @param {!INode} node
 * @returns {boolean}
 */
isInBox(context, box, node) {
  // early exit if not even the bounds are contained in the box
  if (!super.isInBox(context, box, node)) {
    return false
  }

  const eps = context.hitTestRadius / context.zoom
  const layout = node.layout.toRect()
  const outline = this.getOutline(node)

  if (outline == null) {
    return box.contains(layout.topLeft) && box.contains(layout.bottomRight)
  }

  // check if node is inside the given box
  if (box.contains(layout.topLeft) && box.contains(layout.bottomRight)) {
    return true
  }

  // check if the box is inside the node outline
  if (outline.pathContains(box.topLeft, eps) && outline.pathContains(box.bottomRight, eps)) {
    return true
  }

  // check if the node's outline and the box intersect
  return outline.intersects(box, eps)
}

isInBox(context: IInputModeContext, box: Rect, node: INode): boolean {
  // early exit if not even the bounds are contained in the box
  if (!super.isInBox(context, box, node)) {
    return false
  }

  const eps = context.hitTestRadius / context.zoom
  const layout = node.layout.toRect()
  const outline = this.getOutline(node)

  if (outline == null) {
    return box.contains(layout.topLeft) && box.contains(layout.bottomRight)
  }

  // check if node is inside the given box
  if (box.contains(layout.topLeft) && box.contains(layout.bottomRight)) {
    return true
  }

  // check if the box is inside the node outline
  if (outline.pathContains(box.topLeft, eps) && outline.pathContains(box.bottomRight, eps)) {
    return true
  }

  // check if the node's outline and the box intersect
  return outline.intersects(box, eps)
}

ILassoTestable

isInPath(IInputModeContext, GeneralPath, INode), delegated to from ILassoTestable:

Is used to test whether an item is hit by the lasso selection and should therefore be selected. It is at the discretion of the style how lasso selection works exactly and which parts of an item have to be contained within the lasso path. All styles included with yFiles for HTML consider an item selected when the lasso path and the item’s shape intersect.

Changing lasso selection to consider a node selected as soon as its center is contained in the lasso path
/**
 * @param {!IInputModeContext} context
 * @param {!GeneralPath} path
 * @param {!INode} node
 * @returns {boolean}
 */
isInPath(context, path, node) {
  // Check whether the center of the node is contained in the lasso path
  return path.areaContains(node.layout.center, context.hitTestRadius)
}

isInPath(context: IInputModeContext, path: GeneralPath, node: INode): boolean {
  // Check whether the center of the node is contained in the lasso path
  return path.areaContains(node.layout.center, context.hitTestRadius)
}

IBoundsProvider

getBounds(ICanvasContext, INode), delegated to from IBoundsProvider:

In some cases the visualization of an item can exceed the logical bounds of the item. Often this happens when an existing style is decorated in some way, for example when a drop shadow is added. IBoundsProvider implementations can be queried for the bounding box of the complete visualization of an item. This information is important for functionality related to the viewport, like calculating the bounds of a graph when fitting the content, or for showing the scrollbars. It is also a good hint about the required image size when exporting images.

Customizing the bounds provider
Default: the node shadow is ignored and we cannot scroll to see it
Overridden: the drop shadow is included
Including a drop shadow in getBounds
/**
 * @param {!ICanvasContext} context
 * @param {!INode} node
 * @returns {!Rect}
 */
getBounds(context, node) {
  const bounds = node.layout.toRect()
  // expand bounds to include drop shadow
  return bounds.getEnlarged(new Insets(0, 0, 3, 3))
}

getBounds(context: ICanvasContext, node: INode): Rect {
  const bounds = node.layout.toRect()
  // expand bounds to include drop shadow
  return bounds.getEnlarged(new Insets(0, 0, 3, 3))
}

IVisibilityTestable

isVisible(ICanvasContext, Rect, INode), delegated to from IVisibilityTestable:

This interface is used to determine if an item is currently visible in the canvas. The framework does not render objects that are considered invisible — neither createVisual nor updateVisual are called for the item in that case.

Consequently the visibility check should be faster than calling updateVisual and it is not a big deal to err on the side of caution by returning true for items that may be visible instead of having a more expensive check that is more accurate.

Note that if the visualization extends beyond the item’s usual bounds — like mentioned above for IBoundsProvider already — you must take those into account for visibility as well. Otherwise items may suddenly disappear although there is still some part of them inside the viewport. It is not unusual to implement IVisibilityTestable by delegating to IBoundsProvider and a bounding box check. The following code is actually the default implementation of isVisible in NodeStyleBase<TVisual>.

Delegating the implementation of isVisible to IBoundsProvider
/**
 * @param {!ICanvasContext} context
 * @param {*} clip
 * @param {!INode} node
 * @returns {boolean}
 */
isVisible(context, clip, node) {
  return this.getBounds(context, node).intersects(clip)
}

isVisible(context: ICanvasContext, clip: any, node: INode): boolean {
  return this.getBounds(context, node).intersects(clip)
}

The methods above are defined in all abstract style classes (mentioned in the section Basic Style Implementation). But there are also special methods for nodes and edges that can be overridden in NodeStyleBase<TVisual> and EdgeStyleBase<TVisual>. For nodes, the methods are delegated to from the IShapeGeometry interface and for edges from the IPathGeometry interface.

IShapeGeometry

The IShapeGeometry interface and its methods tell the framework about the geometry of a node’s visualization. This is used to determine the end points of incoming and outgoing edges and helps calculating the correct placement for arrows so that they point exactly to the border of a node’s visualization. This involves the methods getIntersection, isInside, and getOutline.

Customizing the shape geometry
Default: the edges end at the node’s bounding box
Overridden: the edges end at the node’s actual shape
Considering an elliptical node shape in the IShapeGeometry-related methods
/**
 * @param {!INode} node
 * @param {!Point} point
 * @returns {boolean}
 */
isInside(node, point) {
  return GeomUtilities.ellipseContains(node.layout.toRect(), point, 0)
}

/**
 * @param {!INode} node
 * @param {!Point} inner
 * @param {!Point} outer
 */
getIntersection(node, inner, outer) {
  return GeomUtilities.findEllipseLineIntersection(node.layout.toRect(), inner, outer)
}

/**
 * @param {!INode} node
 * @returns {!GeneralPath}
 */
getOutline(node) {
  const outline = new GeneralPath()
  outline.appendEllipse(node.layout.toRect(), false)
  return outline
}

isInside(node: INode, point: Point): boolean {
  return GeomUtilities.ellipseContains(node.layout.toRect(), point, 0)
}

getIntersection(node: INode, inner: Point, outer: Point) {
  return GeomUtilities.findEllipseLineIntersection(node.layout.toRect(), inner, outer)
}

getOutline(node: INode): GeneralPath {
  const outline = new GeneralPath()
  outline.appendEllipse(node.layout.toRect(), false)
  return outline
}

IPathGeometry

The IPathGeometry interface and its methods describe the geometry of an edge. This is used for correctly calculating the possible positions for edge labels in edge label models, for drawing an edge’s selection, focus, and highlight decorations, as well as for snapping labels to the edge path. This involves the methods getTangent, getTangentForSegment, getPath, and getSegmentCount.

All of the above mentioned interfaces are implemented with reasonable defaults on the abstract style classes that work fine in many cases. For nodes and edges, you will not need to override the defaults in cases where your visualization has no complex outline and in the case where nodes are more or less of rectangular shape.

As soon as your visualization becomes more complex and differs from the default shape, you may want to consider overriding some of these methods to adjust them to your visualization. After all, the user experience suffers if a node’s visualization extends beyond the node bounds and that part cannot be used to select the node, or when the arrow vanishes beneath a node’s visualization or stops just short of it.