Node Neighborhood Group Background
This example loads a force-directed relation graph, seeds three saved neighborhoods, and lets users click any node to create another neighborhood highlight. Each highlight is drawn as a canvas-layer SVG region derived from live node bounds, using a rectangle for one-node groups and a convex-hull footprint for multi-node groups.
Drawing Live Neighborhood Background Areas on a Force-Directed Graph
What This Example Builds
This example builds a full-height relation graph viewer with force-directed motion and persistent neighborhood highlights. The canvas starts with three saved groups already visible, each rendered as a translucent green background behind a related set of nodes.
Users can click any node to create another saved highlight region for that node and its related nodes. The most important idea is that the grouping effect does not restyle nodes or lines directly. Instead, it adds a separate canvas-layer overlay that follows live node positions.
How the Data Is Organized
The graph data is declared inline as RGJsonData inside initializeGraph(). Each node only stores id and text, and each line starts as a simple { from, to } pair before a final .map(...) pass generates the required line ids.
There is almost no preprocessing before setJsonData(...). The only data transformation is automatic line-id generation, and the grouping state is not embedded in the dataset at all. Group membership is derived at runtime from getNodeById(...) and getNodeRelatedNodes(...), then stored in local React state as an array of { groupId, groupNodes }.
In a production graph, the same structure could represent dependency neighborhoods, related customer records, risk clusters, or local subgraphs that an analyst wants to compare temporarily without changing the underlying graph data.
How relation-graph Is Used
The example is wrapped in RGProvider, and MyGraph uses RGHooks.useGraphInstance() as the main control surface. The graph is configured as a force layout with layoutName: 'force' and maxLayoutTimes: Number.MAX_SAFE_INTEGER, so layout motion can continue until cleanup stops it. Default styling is set through graph options rather than custom node templates: borderless circular nodes, 60 x 60 sizing, straight lines, border junction points, and a purple fill color.
The graph instance API handles the full lifecycle. setJsonData(...) loads the inline dataset, setZoom(30) establishes the initial viewport, getNodeById(...) and getNodeRelatedNodes(...) build each saved neighborhood, and stopAutoLayout() runs on unmount. The graph surface itself remains mostly default.
The main customization happens through RGSlotOnCanvas. Each saved group is rendered by GroupAreaBackground, which uses RGHooks.useGraphStore() to watch shouldRenderNodes and recalculate SVG geometry from current node coordinates and measured node sizes. That component generates a simple rectangle for one-node groups and a convex-hull polygon for multi-node groups. The local SCSS adds almost no additional styling, so the example stays focused on graph behavior rather than presentation chrome.
Key Interactions
- The graph seeds three visible neighborhoods immediately after data load by calling
createNodeGroup('d1'),createNodeGroup('b4'), andcreateNodeGroup('c1'). - Clicking a node runs
onNodeClick, which uses the clicked node as the group root and expands the saved group withgetNodeRelatedNodes(...). - Clicking a node whose group already exists does nothing, so the overlay list stays deduplicated.
- The saved-group list is capped at three entries. Adding a fourth distinct group drops the oldest one.
- Overlay geometry follows the current graph state because the background component recomputes its path when node rendering updates occur.
Key Code Fragments
This options block shows that the example is a force-layout viewer with circular nodes and straight edges.
const graphOptions: RGOptions = {
debug: false,
defaultNodeBorderWidth: 0,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeColor: 'rgb(130,102,246)',
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 60,
defaultNodeHeight: 60,
layout: {
layoutName: 'force',
maxLayoutTimes: Number.MAX_SAFE_INTEGER
},
defaultJunctionPoint: RGJunctionPoint.border
};
This line-processing step keeps the source dataset simple and adds ids only at load time.
lines: [
{ from: 'a', to: 'b' }, { from: 'b', to: 'b1' },
// ...
{ from: 'e2', to: 'e2-8' }, { from: 'e2', to: 'e2-9' }
].map((line, idx) => ({ ...line, id: `line-${idx}` }))
This initialization sequence loads the graph, sets the zoom level, and prepopulates three saved overlays.
await graphInstance.setJsonData(myJsonData);
graphInstance.setZoom(30);
createNodeGroup('d1');
createNodeGroup('b4');
createNodeGroup('c1');
This handler is the core grouping logic: it resolves the clicked node, expands it with related nodes, and keeps only the newest three unique groups.
const createNodeGroup = (nodeId: string) => {
const groupCoreNode = graphInstance.getNodeById(nodeId);
if (!groupCoreNode) return;
const relatedNodes = graphInstance.getNodeRelatedNodes(groupCoreNode);
const groupNodes = [groupCoreNode, ...relatedNodes];
const groupId = 'my-group-' + groupCoreNode.id;
setMyNodeGroups(prev => {
if (prev.some(g => g.groupId === groupId)) return prev;
const nextGroups = [...prev, { groupId, groupNodes }];
return nextGroups.length > 3 ? nextGroups.slice(1) : nextGroups;
});
};
This slot wiring keeps the background layer separate from the main graph renderer.
<RelationGraph
options={graphOptions}
onNodeClick={onNodeClick}
>
<RGSlotOnCanvas>
{myNodeGroups.map(thisGroup => (
<GroupAreaBackground key={thisGroup.groupId} group={thisGroup} />
))}
</RGSlotOnCanvas>
</RelationGraph>
This path generator switches between a rectangle for one node and a convex-hull footprint for multi-node groups.
if (nodes.length === 1) {
const { x, y, width, height } = nodes[0];
return `M${x},${y} h${width} v${height} h${-width} Z`;
}
const hullPoints = convexHull(points);
if (hullPoints.length < 3) return '';
return `M${hullPoints[0].x},${hullPoints[0].y} ${hullPoints.slice(1).map(p => `L${p.x},${p.y}`).join(' ')} Z`;
This hook makes the overlay recalculate from current node bounds when relation-graph reports node rendering updates.
const {shouldRenderNodes} = RGHooks.useGraphStore();
const groupArea = useMemo(() => {
return generateSVGPath(group.groupNodes.map((node: RGNode) => ({
x: node.x,
y: node.y,
width: node.el_W || 60,
height: node.el_H || 40
})));
}, [shouldRenderNodes]);
What Makes This Example Distinct
Compared with nodes-group-rect-background, this example focuses on footprint geometry rather than one shared bounding box. The comparison data shows that its main distinction is turning grouped nodes into a filled region derived from node corner points, with a single-node fallback rectangle, instead of drawing one padded rounded rectangle around the whole neighborhood.
Compared with layout-force-options, the force solver is supporting infrastructure rather than the subject of the demo. The distinctive lesson here is how a moving force-layout graph can host graph-derived region overlays built from related-node neighborhoods.
Compared with built-in-slots, this is not a general layer comparison. It uses RGSlotOnCanvas for one very specific job: rendering SVG geometry that tracks current node positions. The rare combination is the important part: seeded groups, click-to-group related-node lookup, deduplicated three-group retention, and live convex-hull overlays on top of a force-directed graph.
Where Else This Pattern Applies
This pattern transfers well to graph views where users need temporary neighborhood emphasis without changing node or line styles. Examples include service dependency inspection, fraud-ring exploration, attack-path review, and relationship-audit workflows.
It is also a practical pattern for knowledge-graph or network-analysis tools that need cluster-shaped callouts instead of per-node highlighting. A saved overlay can represent “records related to this entity,” “systems touched by this incident,” or “nodes participating in this dependency chain” while the graph keeps moving underneath.
Finally, the same approach is useful when teams need graph annotations that are computed from runtime geometry rather than stored in the dataset. The reusable idea is to keep grouping state outside the graph data and derive the visible region from current node bounds on the canvas layer.