C

NodeStyleBase<TVisual>
abstract

An abstract base class that makes it possible to easily implement a custom INodeStyle.
ImplementsInheritance Hierarchy

Remarks

The only method that needs to be implemented by subclasses is createVisual, however to improve rendering performance it is highly recommended to implement at least updateVisual, too.

Unlike a direct INodeStyle implementation, NodeStyleBase<TVisual> does not expose a separate INodeStyleRenderer. The base class uses a fixed renderer and simply forwards its calls back to the style instance.

Note that if your subclass has private fields (defined with JavaScript's # name syntax), the clone method needs to be overridden for the cloning to work properly.

Type Parameters

TVisual

The optional type of the created and updated by the and methods. This type argument can be omitted, but specifying a more concrete type helps conveniently implementing with TypeScript.

Examples

A very simple subclass of NodeStyleBase<TVisual> only needs a createVisual implementation:

/// <summary>
/// A custom node style that displays a node always as an exact circle inscribed in the node's
/// layout rectangle.
/// </summary>

class CircleNodeStyle extends NodeStyleBase {
  // The only method we actually *have* to implement

  createVisual(context: IRenderContext, node: INode): Visual | null {
    const size = Math.min(node.layout.width, node.layout.height)

    // we create a circular Element
    const circle = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'ellipse',
    )
    circle.setAttribute('rx', (size / 2).toString())
    circle.setAttribute('ry', (size / 2).toString())
    circle.setAttribute('stroke', 'black')
    circle.setAttribute('fill', 'red')

    // We now need to tell yFiles where to place the circle. This is done with
    SvgVisual.setTranslate(circle, node.layout.x, node.layout.y)
    return new SvgVisual(circle)
  }
}

This style would show nodes as follows (the light gray rectangle shows the actual node layout in which the circle is centered): From here, more features can easily be added as needed. Once the application grows beyond the prototype stage, it's usually a good idea to implement updateVisual as well:

updateVisual(
  context: IRenderContext,
  oldVisual: Visual,
  node: INode,
): Visual | null {
  // Our style does not have any options that can change, so the only thing left
  // to do here is to update location and size via SetCanvasArrangeRect
  const size = Math.min(node.layout.width, node.layout.height)

  const element = (oldVisual as SvgVisual).svgElement

  element.setAttribute('rx', (size / 2).toString())
  element.setAttribute('ry', (size / 2).toString())
  SvgVisual.setTranslate(element, node.layout.x, node.layout.y)

  return oldVisual
}

Another common customization, especially when the node style doesn't display a rectangular shape, is to change how hit-testing works on the node. This affects where clicking would select the node. In our example with a circle that is often quite a bit smaller than the node's layout rectangle, it's especially noticeable that clicking anywhere within that rectangle would select the node, and not just on the circle: But we can change that:

isHit(context: IInputModeContext, location: Point, node: INode): boolean {
  const size = Math.min(node.layout.width, node.layout.height)
  const circleBounds = new Rect(node.layout.center, new Size(size, size))
  // Also observe the hit-test radius from the context so that clicking just outside
  // the bounds of the circle will still register as a hit.
  // This is less important with node styles, but for edges that are usually just
  // displayed as a line, it's very hard to hit that line otherwise.
  return GeometryUtilities.ellipseContains(
    circleBounds,
    location,
    context.hitTestRadius,
  )

  // In our case, since we know our visualization is always a circle, we also could have done the math
  // ourselves:

  // The point only hits the circle when the distance to the circle's center is less than
  // its radius (plus the hit-test radius again):

  // return node.layout.center.distanceTo(location) <= size / 2 + context.hitTestRadius;
}

Now only points that lie within the circle would successfully hit the node:

See Also

Developer's Guide

Demos

Shows how to create custom styles for nodes, edges, labels, ports, and edge arrows.
Create a simple node style using SVG
Optimize rendering performance of an SVG node style
Customize which area of a node can be hovered and clicked

Members

Show:

Constructors

Initializes a new instance of the NodeStyleBase<TVisual> class.

Properties

Gets the renderer implementation for this instance.
The private implementation will delegate all API calls back to this instance.
readonlyfinal

Methods

Creates a new object that is a copy of the current instance.

Immutable subclasses should consider returning this for the best performance.

The base class implementation creates a simple shallow clone that copies all own properties to a new instance. This will not work for classes that use private fields or properties (defined with JavaScript's # name syntax) as no constructors will be run and implementations like this cannot be accessed from outside the classes that declare them.

Classes that use private members or require special initialization must override this method and manually create a new instance or call super.clone() and then explicitly copy the private fields and properties explicitly.

Return Value

Object
A new object that is a copy of this instance using a memberwise clone.
Creates the visual representation for node.
This method is called in response to a createVisual call to the instance that has been queried from the renderer.
protectedabstract

Parameters

context: IRenderContext
The render context.
node: INode
The node to which this style instance is assigned.

Return Value

TVisual
The visual as required by the createVisual interface.

See Also

Developer's Guide
API
updateVisual
Gets the bounds of the visual for the node in the given context.
This method is called in response to a getBounds call to the instance that has been queried from the renderer. This implementation simply yields the layout.
protected

Parameters

context: ICanvasContext
The canvas context.
node: INode
The node to which this style instance is assigned.

Return Value

Rect
The visual bounds of the visual representation.

Examples

Usually the bounds only have to be changed if the node visualization doesn't fit exactly into the node's layout rectangle. For example, if we know that our style exceeds the node's layout on the lower right by 3 pixels (e.g. due to a drop-shadow), this method's implementation could look like follows:
getBounds(context: ICanvasContext, node: INode): Rect {
  return node.layout.toRect().getEnlarged(new Insets(0, 0, 3, 3))
}

See Also

Developer's Guide
Gets the intersection of a line with the visual representation of the node.
This method is called in response to a getIntersection call to the instance that has been queried from the renderer. This implementation simply uses the outline to determine the intersection or the layout if the outline is null. If it is feasible to determine the intersection point for the current shape, this method should be implemented in addition to getOutline to improve performance.
protected

Parameters

node: INode
The node to which this style instance is assigned.
inner: Point
The coordinates of a point lying inside the shape.
outer: Point
The coordinates of a point lying outside the shape.

Return Value

Point
The intersection point if one has been found or null, otherwise.

See Also

Developer's Guide
API
isInside
Gets the outline of the visual style.
This implementation yields null to indicate that the layout depicts the outline. Implementing this method influences the behavior of isInside and getIntersection since the default implementations delegate to it.
protected

Parameters

node: INode
The node to which this style instance is assigned.

Return Value

GeneralPath
The outline of the visual representation or null.

See Also

Developer's Guide
Determines whether the visual representation of the node has been hit at the given location.
This method is called in response to a isHit call to the instance that has been queried from the renderer. This implementation uses the outline to determine whether the node has been hit.
protected

Parameters

context: IInputModeContext
The canvas context.
location: Point
The point to test.
node: INode
The node to which this style instance is assigned.

Return Value

boolean
true if the specified node representation is hit; false otherwise.

Examples

Hit-tests should follow the obvious shape of the node that users expect to be able to click to select a node. For example, let's take a cross-like node visualization like follows: Hit-testing would by default include the corners as well (red and green circles show where the hit-test succeeds or fails), since they lie within the node's layout rectangle. By customizing the hit-test to only allow hits within the node's shape, we get a much better result:

isHit(context: IInputModeContext, location: Point, node: INode): boolean {
  // size of the cross bars
  const size = 30
  // model the hit-test by building two overlapping rectangles for the cross
  const center = node.layout.center
  const rect1 = new Rect(center, new Size(node.layout.width, size))
  const rect2 = new Rect(center, new Size(size, node.layout.height))

  // if either of them contains the point, the hit-test succeeds
  return rect1.contains(location) || rect2.contains(location)
}

The corners of the node's layout rectangle are now excluded from the hit-test:

See Also

Developer's Guide
Determines whether the visualization for the specified node is included in the marquee selection.
This method is called in response to a isInBox call to the instance that has been queried from the renderer. This implementation simply tests whether the bounds intersect the marquee box.
protected

Parameters

context: IInputModeContext
The input mode context.
rectangle: Rect
The marquee selection box.
node: INode
The node to which this style instance is assigned.

Return Value

boolean
true if the specified node is visible and selected by the marquee rectangle; false otherwise.

Examples

The following example implementation shows how marquee selection can be customized to require the node to be completely inside the marquee rectangle instead of only intersecting it:
isInBox(
  context: IInputModeContext,
  rectangle: Rect,
  node: INode,
): boolean {
  const layout = node.layout
  // Require the top-left and bottom-right corners to be inside the rectangle.
  // That way the complete node must be inside.
  return (
    rectangle.contains(layout.topLeft) &&
    rectangle.contains(layout.bottomRight)
  )
}

See Also

Developer's Guide
Determines whether the visualization for the specified node is included in the lasso selection.
This method is called in response to a isInPath call to the instance that has been queried from the renderer. This implementation simply tests whether the bounds intersect the lasso path.
protected

Parameters

context: IInputModeContext
The input mode context.
path: GeneralPath
The lasso selection path.
node: INode
The node to which this style instance is assigned.

Return Value

boolean
true if the specified node is visible and selected by the lasso path, false otherwise.

See Also

Developer's Guide
Determines whether the provided point is geometrically inside the visual bounds of the node.
This method is called in response to a isInside call to the instance that has been queried from the renderer. This implementation simply uses the outline to determine whether the point is contained or the layout if the outline is null. If it is feasible to determine whether a given point lies inside the shape, this method should be implemented in addition to getOutline to improve performance.
protected

Parameters

node: INode
The node to which this style instance is assigned.
location: Point
The point to test.

Return Value

boolean
Whether the point is considered to lie inside the shape.

See Also

Developer's Guide
Determines whether the visualization for the specified node is visible in the context.
This method is called in response to a isVisible call to the instance that has been queried from the renderer. This implementation simply tests whether the bounds intersect the clip.
protected

Parameters

context: ICanvasContext
The canvas context.
rectangle: Rect
The clipping rectangle.
node: INode
The node to which this style instance is assigned.

Return Value

boolean
true if the specified node is visible in the clipping rectangle; false otherwise.

Examples

Visibility is important to consider when the node's visualization exceeds the node's layout rectangle, e.g. due to a drop shadow. For example, if we know that our style exceeds the node's layout on the lower right by 3 pixels, this method's implementation could look like follows:

isVisible(
  context: ICanvasContext,
  rectangle: Rect,
  node: INode,
): boolean {
  const enlargedNodeLayout = node.layout
    .toRect()
    .getEnlarged(new Insets(0, 0, 3, 3))
  // The return value is simply whether any part of enlargedNodeLayout is visible in rectangle
  return rectangle.intersects(enlargedNodeLayout)
}

In many cases, isVisible is closely aligned with getBounds, so isVisible may actually be implemented by delegating to getBounds as follows:

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

In fact, that's the default implementation for NodeStyleBase<TVisual>.

See Also

Developer's Guide
Performs the lookup operation for the getContext that has been queried from the renderer.

This implementation yields null for everything but:

For these interfaces an implementation will be returned that delegates to the methods in this instance.

protected

Parameters

node: INode
The node to use for the context lookup.
type: Constructor
The type to query.

Return Value

any
An implementation of the type or null.
Updates a visual representation for node previously created by createVisual.
This method is called in response to a updateVisual call to the instance that has been queried from the renderer. This implementation simply delegates to createVisual so subclasses should override to improve rendering performance.
protected

Parameters

context: IRenderContext
The render context.
oldVisual: TVisual
The visual that has been created in the call to createVisual.
node: INode
The node to which this style instance is assigned.

Return Value

TVisual
The visual as required by the createVisual interface.

See Also

Developer's Guide
API
createVisual