Refining a Style’s Behavior
There are other methods that you can override in NodeStyleBase<TVisual>. These methods connect the internal logic for the visualizations and are delegated to from several interfaces.
The original interfaces of these methods are:
IHitTestable
isHit(IInputModeContext, Point, INode), delegated to from IHitTestable:
Used to detect hits, for example, during mouse clicks, hover actions, or drag and drop operations. Also, methods that check for items at a specific location, such as GraphComponent.renderTree.hitElementsAt, rely on the implementation of this interface.


The ICanvasContext provides the hitTestRadius property (which can be set on GraphComponent). This is a distance that should be used for fuzzy hit testing to determine whether a shape has been hit. This 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 GeometryUtilities.ellipseContains.
isHit(context, p, node) {
return GeometryUtilities.ellipseContains(
node.layout.toRect(),
p,
context.hitTestRadius / context.zoom
)
}
isHit(context: IInputModeContext, p: Point, node: INode): boolean {
return GeometryUtilities.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.

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()
// check if node is inside the given box
if (box.contains(layout.topLeft) && box.contains(layout.bottomRight)) {
return true
}
// now check the outline
const outline = this.getOutline(node)
if (outline == null) {
// if there is no outline provided, return false
return false
}
// 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.pathIntersects(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()
// check if node is inside the given box
if (box.contains(layout.topLeft) && box.contains(layout.bottomRight)) {
return true
}
// now check the outline
const outline = this.getOutline(node)
if (outline == null) {
// if there is no outline provided, return false
return false
}
// 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.pathIntersects(box, eps)
}
ILassoTestable
isInPath(IInputModeContext, GeneralPath, INode), delegated to from ILassoTestable:
Is used to determine whether an item is hit by the lasso selection and should be selected. The style determines how lasso selection works and which parts of an item must 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.
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 visual representation of an item can extend beyond its logical bounds. This often occurs when an existing style is enhanced, such as by adding a drop shadow. IBoundsProvider implementations can be used to determine the bounding box that encompasses the complete visual representation of an item. This information is important for viewport-related functionalities, such as calculating the graph’s bounds when fitting content or displaying scrollbars. It also provides a useful indication of the required image size for image export.


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 determines whether 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.
It is acceptable to err on the side of caution by returning true
for items
that may be visible, rather than performing a more expensive, but more accurate check.
Note that if the visualization extends beyond the item’s usual bounds (as mentioned previously for IBoundsProvider), you must account for this in the visibility check. Otherwise, items may suddenly disappear even if part of them is still inside the viewport. It is common to implement IVisibilityTestable by delegating to IBoundsProvider and performing a bounding box check. The following code is the default implementation of isVisible in NodeStyleBase<TVisual>.
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 from the IShapeGeometry interface, and for edges from the IPathGeometry interface.
IShapeGeometry
The IShapeGeometry interface and its methods inform the framework about the geometry of a node’s visualization. This information is used to determine the endpoints of incoming and outgoing edges and to calculate 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.


isInside(node, point) {
return GeometryUtilities.ellipseContains(node.layout.toRect(), point, 0)
}
getIntersection(node, inner, outer) {
return GeometryUtilities.getEllipseLineIntersection(
node.layout.toRect(),
inner,
outer
)
}
getOutline(node) {
const outline = new GeneralPath()
outline.appendEllipse(node.layout.toRect(), false)
return outline
}
isInside(node: INode, point: Point): boolean {
return GeometryUtilities.ellipseContains(node.layout.toRect(), point, 0)
}
getIntersection(node: INode, inner: Point, outer: Point) {
return GeometryUtilities.getEllipseLineIntersection(
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 information is used to calculate suitable positions for edge labels in edge label models, to draw an edge’s selection, focus, and highlight decorations, and to snap labels to the edge path. The relevant methods are getTangent, getTangentForSegment, getPath, and getSegmentCount.
The abstract style classes implement all of the above-mentioned interfaces with reasonable defaults that work well in many cases. For nodes and edges, you typically do not need to override the defaults if your visualization has no complex outline and the nodes are roughly rectangular.
If your visualization is more complex or differs significantly from the default shape, consider overriding some of these methods to adjust them to your visualization. The user experience suffers if a node’s visual representation extends beyond the node bounds, and that part cannot be selected. Similarly, issues arise when the arrow vanishes beneath a node’s visualization or stops short of it.