Force Clustering with Controlled Anchor Motion
This example uses hidden zero-opacity links to keep three groups clustered in a live force layout, then drives the group anchors around the root with a timer. It combines slot-based node rendering, a canvas backdrop, runtime line-visibility inspection, theme switching, `RGMiniView`, and a shared floating settings and export panel. It is most useful as a reference for controlled motion on top of relation-graph's built-in force solver.
Animating Hidden-Link Force Clusters in a Galaxy-Style Graph
What This Example Builds
This example builds a full-screen force-layout viewer with one central root, three first-level group nodes, and twelve child labels under each group. The visible result is a galaxy-style scene: the root appears as an electric-border sphere, the first ring behaves like oversized planets, and the child nodes stay clustered around those moving anchors.
Users can start or stop the motion, reveal or hide the normally invisible links, switch between a plain wrapper and the galaxy skin, drag the floating control window, open a canvas settings panel, and export the graph as an image. The graph itself stays mounted while those controls change how the scene behaves and looks.
The main point is not decorative styling alone. The important lesson is that relation-graph’s force layout can stay active while the code continuously moves selected anchor nodes and optionally exposes the hidden scaffold that keeps each cluster together.
How the Data Is Organized
The data lives in one inline RGJsonData object named myJsonData. It declares rootId: 'root', a flat nodes array, and a flat lines array. The dataset contains 40 nodes in total: one fixed root, three top-level group nodes (g1, g2, g3), and thirty-six child labels split evenly across those groups.
The graph does one small preprocessing step before loading. During initialization, it walks the lines array and assigns synthetic ids such as l1, l2, and l3 to any line that does not already have one, then sends the data to graphInstance.setJsonData(...). The line records all start with opacity: 0, so they act as force-layout scaffolding before they act as visible edges.
The node records also carry some presentation metadata. node.data.myClassName is consumed by the custom node slot to add color variants for the green and blue groups. The source also includes imgOffset fields, but the reviewed files do not read them during rendering.
In a real product, this same structure could represent grouped products around category hubs, services around platform domains, tags around topic clusters, or knowledge-graph entities around high-level concepts. The hidden lines are especially transferable to cases where layout relationships matter even when those relationships should not stay visible in the final scene.
How relation-graph Is Used
index.tsx wraps the page in RGProvider, which lets both the graph component and the shared floating utility window resolve the same relation-graph context. Inside MyGraph.tsx, the example renders one RelationGraph with the built-in force layout. The options keep maxLayoutTimes effectively unlimited and tune the solver with force_node_repulsion: 0.1 and force_line_elastic: 2, so the layout keeps reacting while the anchors move.
The graph options also hand most visible styling to custom markup and SCSS. defaultNodeShape is set to RGNodeShape.circle, defaultNodeWidth and defaultNodeHeight are both 0, defaultNodeBorderWidth is 0, and defaultLineShape is RGLineShape.StandardCurve. That combination means relation-graph handles geometry and force behavior, while the example’s slots and stylesheet decide how the scene looks.
RGHooks.useGraphInstance() drives nearly all runtime behavior. It loads the JSON, reads nodes and options, updates anchor positions with updateNodePosition(...), recenters and fits the viewport with moveToCenter() and zoomToFit(), rewrites line opacity with getLines() plus updateLine(...), and restarts layout after dragging with startAutoLayout(). In the shared CanvasSettingsPanel, RGHooks.useGraphStore() exposes the current wheel and drag modes, and setOptions(...) applies new ones live. The same panel also uses prepareForImageGeneration() and restoreAfterImageGeneration() around screenshot capture.
Three relation-graph slots shape the page. RGSlotOnCanvas injects a large circular backdrop behind the graph. RGSlotOnNode replaces the root with ElectricBorderCard and renders every other node as a circular label whose class can be extended from node.data.myClassName. RGSlotOnView mounts RGMiniView as an always-visible overview. The stylesheet then switches between the base .my-graph rules and the .my-graph-style-galaxy overrides without rebuilding the graph.
This remains a viewer-oriented example, not an editor. The runtime APIs are used for motion, inspection, viewport management, live option changes, and export rather than node or line authoring.
Key Interactions
The Move selector is the core interaction. It turns a 100 millisecond timer on or off, and that timer continuously repositions the three first-level anchors around the root.
The Line Visible selector exposes the structural scaffold. When switched on, the code rewrites every loaded line from opacity: 0 to opacity: 1, so the user can inspect the same hidden edges that are shaping the force clusters.
The Graph Style selector swaps the wrapper class between my-graph-style-galaxy and the base .my-graph appearance. The graph data and layout instance stay the same; only the wrapper-level styling changes.
Dragging a node affects the live layout loop in two ways. The orbit logic skips whichever node is currently being dragged, and onNodeDragEnd restarts auto layout if it is no longer running.
The floating utility window is also interactive. Users can drag it by the title bar, minimize it, open the settings overlay, change wheel and canvas-drag behavior, and download an image. Clicking a node label triggers a simple alert with that node’s text, but that click is secondary to the layout and inspection behavior.
Key Code Fragments
This options block proves that the example relies on relation-graph’s built-in force solver and keeps the layout running while custom markup handles the visual style.
const graphOptions: RGOptions = {
debug: false,
defaultLineColor: '#aaaaaa',
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 0,
defaultNodeHeight: 0,
defaultLineShape: RGLineShape.StandardCurve,
defaultLineTextOnPath: true,
layout: {
layoutName: 'force',
maxLayoutTimes: Number.MAX_SAFE_INTEGER,
force_node_repulsion: 0.1,
force_line_elastic: 2
}
};
This initialization step shows the only preprocessing pass before the data is loaded into the graph instance.
myJsonData.lines.forEach((line: JsonLine, index: number) => {
if (!line.id) {
line.id = `l${index + 1}`;
}
});
This movement loop shows that the orbit effect is not left to the solver alone. The code computes positions for the three first-level anchors and writes them back into the live graph.
level1Nodes.forEach((thisNode, index) => {
if (thisNode.id === graphOptions.draggingNodeId) {
return;
}
const deg = level1DegSet[index] + rotateCountRef.current * level1SpeedSet[index];
const nodePoint = getOvalPoint(rootNode.x, rootNode.y, level1CircleSet[index], deg);
const newX = nodePoint.x - level1NodeR;
const newY = nodePoint.y - level1NodeR;
graphInstance.updateNodePosition(thisNode.id, newX, newY);
});
This helper proves that the hidden scaffold is still part of the graph model and can be revealed on demand by rewriting line opacity.
const updateMyData = () => {
graphInstance.getLines().forEach(line => {
graphInstance.updateLine(line, {
opacity: lineOpacity
});
});
};
This slot fragment shows how the example adds a background layer, custom node rendering, and an always-on minimap without changing the underlying graph data.
<RelationGraph
options={graphOptions}
onNodeDragEnd={onNodeDragEnd}
>
<RGSlotOnCanvas>{/* ... */}</RGSlotOnCanvas>
<RGSlotOnNode>{/* ... */}</RGSlotOnNode>
<RGSlotOnView>
<RGMiniView />
</RGSlotOnView>
</RelationGraph>
This node-slot branch is what turns the root into the demo’s central sphere instead of a normal circular node.
node.id === 'root' ? (
<ElectricBorderCard width={'200px'} height={'200px'} borderRadius="50%">
<div className="p-3 h-full w-full">
<div className="my-root h-full w-full"></div>
</div>
</ElectricBorderCard>
) : (
This export helper shows how the shared settings panel asks relation-graph to prepare the canvas for capture before the screenshot utility runs.
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
// ...
await graphInstance.restoreAfterImageGeneration();
This SCSS fragment shows that the galaxy skin is a wrapper-level override, not a separate graph configuration.
.my-graph.my-graph-style-galaxy {
.relation-graph {
.rg-map {
background: radial-gradient(circle at 20% 20%,
#003963 0%,
#002247 40%,
#001223 65%,
#161616 100%);
}
What Makes This Example Distinct
The comparison data describes this example as unusually focused on choreographed force behavior rather than static layout. Its clearest distinguishing point is that the graph stays live while a 100 millisecond timer continually repositions the three first-ring anchors. That makes the scene read as an orbiting system instead of a force snapshot that simply settles once.
It also stands out because the same hidden links that shape the clusters can be revealed at runtime. The force scaffold is not a separate debug dataset or a one-time preprocessing trick. It stays in the loaded graph model, and the Line Visible control exposes it by rewriting opacity. The comparison notes treat that inspectable hidden scaffold as one of the strongest differences from nearby viewer examples.
Compared with css-theme, this example uses wrapper-class theme switching to support a moving force scene instead of a broader theme gallery. Compared with zodiac-partner, it puts more emphasis on live clustering behavior, scaffold inspection, and orbiting anchors than on a choice-driven workflow. Compared with gee-thumbnail-diagram, the minimap is supporting chrome rather than the main lesson. Across those neighbors, the distinctive combination is hidden-link force clustering, programmatic anchor motion, on-demand scaffold reveal, slot-based galaxy styling, an electric-border root, and an always-visible RGMiniView.
Where Else This Pattern Applies
This pattern can be reused for grouped product catalogs, service portfolios, topic maps, or knowledge-graph summaries where clusters should stay visually close to a hub but the clustering edges should remain hidden most of the time. It is a practical fit when structural edges exist mainly to guide layout rather than to communicate explicit relationships to end users.
It also applies to operational or educational views where anchor nodes need controlled motion or controlled coordinates while the surrounding cluster keeps reacting naturally. Examples include animated platform maps, narrative walkthroughs, rotating category showcases, and guided demos that want more motion than a static force layout provides.
A third transfer scenario is debugging and explanation. Teams can present a polished scene by default, then expose the scaffold on demand when they need to explain why nodes group the way they do, verify layout structure, or capture the graph as documentation.