NodeStyleBase<TVisualextends Visual>
An abstract base class that makes it possible to easily implement a custom INodeStyle.
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.
This implementation differs from the straightforward INodeStyle implementation in that there is no visible separation between the style and its INodeStyleRenderer. Instead the renderer used by the base class is fixed and delegates all calls back to the style instance.
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
/**
* @param {!IRenderContext} context
* @param {!INode} node
* @returns {?Visual}
*/
createVisual(context, node) {
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)
}
}
/// <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):
/**
* @param {!IRenderContext} context
* @param {!Visual} oldVisual
* @param {!INode} node
* @returns {?Visual}
*/
updateVisual(context, oldVisual, node) {
// 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.svgElement
element.setAttribute('rx', (size / 2).toString())
element.setAttribute('ry', (size / 2).toString())
SvgVisual.setTranslate(element, node.layout.x, node.layout.y)
return oldVisual
}
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:
/**
* @param {!IInputModeContext} context
* @param {!Point} location
* @param {!INode} node
* @returns {boolean}
*/
isHit(context, location, node) {
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 GeomUtilities.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;
}
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 GeomUtilities.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:
Related Programming Samples
- Custom Styles
- Shows how to create custom styles for nodes, edges, labels, ports, and edge arrows.
- 01 Create A Rectangle
- Create a simple node style using SVG
- 03 Render Performance
- Optimize rendering performance of an SVG node style
- 07 Hit-Testing
- Customize which area of a node can be hovered and clicked
Type Parameters
- TVisual: Visual
- The optional type of the
created and updated by the and methods. This type argument can be omit, but specifying a more concrete type helps conveniently implementing with TypeScript.
Type Details
- yfiles module
- view-component
- yfiles-umd modules
- All view modules
- Legacy UMD name
- yfiles.styles.NodeStyleBase
See Also
Constructors
Initializes a new instance of the NodeStyleBase<TVisual> class.
Properties
Gets the renderer implementation for this instance.
Remarks
See Also
Implements
Methods
Creates a new object that is a copy of the current instance.
Remarks
this
.Returns
- ↪Object
- A new object that is a copy of this instance using memberwiseClone.
Implements
Creates the visual representation for node
.
Remarks
Parameters
A map of options to pass to the method.
- context - IRenderContext
- The render context.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪TVisual
- The visual as required by the createVisual interface.
See Also
Gets the bounds of the visual for the node in the given context.
Remarks
Parameters
A map of options to pass to the method.
- context - ICanvasContext
- The canvas context.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪Rect
- The visual bounds of the visual representation.
Examples
/**
* @param {!ICanvasContext} context
* @param {!INode} node
* @returns {!Rect}
*/
getBounds(context, node) {
return node.layout.toRect().getEnlarged(new Insets(0, 0, 3, 3))
}
getBounds(context: ICanvasContext, node: INode): Rect {
return node.layout.toRect().getEnlarged(new Insets(0, 0, 3, 3))
}
See Also
Gets the intersection of a line with the visual representation of the node.
Remarks
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.Parameters
A map of options to pass to the method.
- 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.
Returns
- ↪Point
- The intersection point if one has been found or
null
, otherwise.
See Also
Gets the outline of the visual style.
Remarks
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.Parameters
A map of options to pass to the method.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪GeneralPath
- The outline of the visual representation or
null
.
See Also
Determines whether the visual representation of the node has been hit at the given location.
Remarks
Parameters
A map of options to pass to the method.
- context - IInputModeContext
- The canvas context.
- location - Point
- The point to test.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪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:
/**
* @param {!IInputModeContext} context
* @param {!Point} location
* @param {!INode} node
* @returns {boolean}
*/
isHit(context, location, node) {
// 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)
}
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
Determines whether the visualization for the specified node is included in the marquee selection.
Remarks
Parameters
A map of options to pass to the method.
- context - IInputModeContext
- The input mode context.
- rectangle - Rect
- The marquee selection box.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪boolean
true
if the specified node is visible is selected by the marquee rectangle;false
otherwise.
Examples
/**
* @param {!IInputModeContext} context
* @param {!Rect} rectangle
* @param {!INode} node
* @returns {boolean}
*/
isInBox(context, rectangle, node) {
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)
)
}
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
Determines whether the visualization for the specified node is included in the lasso selection.
Remarks
Parameters
A map of options to pass to the method.
- context - IInputModeContext
- The input mode context.
- path - GeneralPath
- The lasso selection path.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪boolean
true
if the specified node is visible is selected by the lasso pathfalse
otherwise.
See Also
Determines whether the provided point is geometrically inside the visual bounds of the node.
Remarks
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.Parameters
A map of options to pass to the method.
Returns
- ↪boolean
- Whether the point is considered to lie inside the shape.
See Also
Determines whether the visualization for the specified node is visible in the context.
Remarks
Parameters
A map of options to pass to the method.
- context - ICanvasContext
- The canvas context.
- rectangle - Rect
- The clipping rectangle.
- node - INode
- The node to which this style instance is assigned.
Returns
- ↪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:
/**
* @param {!ICanvasContext} context
* @param {!Rect} rectangle
* @param {!INode} node
* @returns {boolean}
*/
isVisible(context, rectangle, node) {
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)
}
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:
/**
* @param {!ICanvasContext} context
* @param {!Rect} rectangle
* @param {!INode} node
* @returns {boolean}
*/
isVisible(context, rectangle, node) {
return this.getBounds(context, node).intersects(rectangle)
}
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
Performs the lookup operation for the getContext that has been queried from the renderer.
Remarks
This implementation yields null
for everything but:
For these interfaces an implementation will be returned that delegates to the methods in this instance.
Parameters
A map of options to pass to the method.
Returns
- ↪any
- An implementation of the
type
ornull
.
Updates a visual representation for node
previously created by createVisual.
Remarks
Parameters
A map of options to pass to the method.
- 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.
Returns
- ↪TVisual
- The visual as required by the createVisual interface.