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.
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.
/**
* @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.
/**
* @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.
/**
* @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.
/**
* @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>.
/**
* @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.
/**
* @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.