Timer-Driven Force Layout Parameter Cycling
This example builds a full-screen force-layout viewer that loads a large inline branching dataset, zooms out, and then alternates force coefficients every three seconds so the cluster keeps re-forming. It uses relation-graph's built-in force engine, `setJsonData(...)`, `setZoom(30)`, and `updateOptions(...)` rather than a control panel or custom node rendering.
Timer-Driven Force Layout Oscillation on a Full-Screen Graph
What This Example Builds
This example builds a self-running full-screen graph viewer. The canvas shows one large branching network as translucent white circles connected by straight translucent edges, all placed on a green gradient background.
The graph initializes itself on mount, immediately zooms out to 30, and then keeps changing shape because the force-layout coefficients are swapped every three seconds. There is no custom control panel or editing workflow. The main point is to demonstrate ambient motion on a live relation-graph instance without reloading data.
How the Data Is Organized
The dataset is declared inline as one RGJsonData object inside initializeGraph(). It uses rootId: 'a', contains 103 nodes and 102 lines, and expands from one root into four large branches (b, c, d, and e) with deeper child groups under each branch.
There is only one preprocessing step before setJsonData(...): every line is mapped to a generated sequential id with line_${index}. The example does not precompute coordinates, attach per-node styles, or fetch data from an external source. The structure stays close to the default relation-graph schema of nodes plus lines.
In a production setting, the same shape could represent service dependencies, organizational clusters, product module trees, or a background relationship map for a dashboard. The letter-based sample ids can be replaced by any business identifiers while keeping the same branching structure.
How relation-graph Is Used
index.tsx wraps the demo in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() as the control surface. That instance is responsible for loading the graph, setting the initial zoom level, and updating layout parameters later at runtime.
The RelationGraph component itself stays close to built-in behavior. It uses the built-in force layout, sets maxLayoutTimes to 30000, starts with force_node_repulsion: 1 and force_line_elastic: 1, renders circular nodes at 60 x 60, uses RGLineShape.StandardStraight, and places the built-in toolbar as a horizontal strip in the bottom-right corner.
No slots, custom node renderers, custom line renderers, or editing controllers are used here. The visual identity mostly comes from graph options plus one SCSS override: the outer container fills the viewport with height: 100vh, and .rg-map gets a left-to-right green gradient background.
The reusable relation-graph pattern is the runtime update path. After the first setJsonData(...), the example calls graphInstance.updateOptions(...) on a timer to modify force-layout coefficients on the live graph. That means the motion comes from the existing solver continuing with new parameters, not from rebuilding the dataset.
Key Interactions
- The graph boots automatically in
useEffect(...), so the user sees the prepared network without triggering any manual load step. - The main behavior is the three-second timer that alternates between
[0.2, 2]and[1, 0.2]forforce_node_repulsionandforce_line_elastic. That repeated swap makes the cluster tighten, loosen, and re-form over time. - A built-in toolbar is visible in the bottom-right, but the example does not add its own tuning controls, click handlers, editing actions, or staged relayout buttons. It stays in viewer mode.
Key Code Fragments
This hook-and-effect block proves that the graph is initialized automatically and then kept moving by a repeating timer.
const graphInstance = RGHooks.useGraphInstance();
const optIndex = useRef(0);
useEffect(() => {
initializeGraph();
const resizeTimer = setInterval(async () => {
await updateLayoutorOptions();
}, 3000);
return () => {
clearInterval(resizeTimer);
};
}, []);
This fragment shows that the dataset stays inline and that line ids are generated just before the data is loaded.
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'a' }, { id: 'b', text: 'b' },
// ...more branching nodes...
{ id: 'e2-9', text: 'e2-9' }
],
lines: [
{ from: 'a', to: 'b' }, { from: 'b', to: 'b1' },
// ...more branching links...
{ from: 'e2', to: 'e2-9' }
].map((line, index) => ({ ...line, id: `line_${index}` }))
};
These calls show the one-time bootstrapping sequence: load the data once, then force a wide initial camera view.
const initializeGraph = async () => {
// ...inline RGJsonData definition...
await graphInstance.setJsonData(myJsonData);
graphInstance.setZoom(30);
};
This update function is the reusable core pattern: change live force coefficients on the running graph instead of reloading the graph data.
const opts = [[0.2, 2], [1, 0.2]];
const opt = opts[optIndex.current++];
if (optIndex.current >= opts.length) {
optIndex.current = 0;
}
graphInstance.updateOptions({
layout: {
layoutName: 'force',
force_node_repulsion: opt[0],
force_line_elastic: opt[1]
}
});
This options block shows that the example relies on built-in relation-graph defaults for most of its appearance and layout behavior.
const graphOptions: RGOptions = {
defaultLineColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeBorderColor: 'rgba(255, 255, 255, 0.3)',
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 60,
defaultNodeHeight: 60,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultLineShape: RGLineShape.StandardStraight,
This stylesheet fragment shows the one decisive visual override that turns the graph into an ambient full-screen surface.
.my-graph {
.relation-graph {
.rg-map {
background: linear-gradient(to right, rgb(16, 185, 129), rgb(101, 163, 13));
}
}
}
What Makes This Example Distinct
The comparison data places this example near layout-force-options, toys-clock, and multi-group-3, but its emphasis is different from all three. Compared with layout-force-options, it removes the tuning panel and turns runtime force updates into a passive, self-running behavior. The user is not adjusting six coefficients or switching datasets. The graph simply keeps moving on its own.
Compared with toys-clock, the motion does not come from a marker node or a clock-specific playback routine. Compared with multi-group-3, it does not stage a one-time transition from center to force or call an explicit relayout sequence. Its motion stays inside the force solver itself through repeated coefficient updates on the same live graph.
The rare combination is what makes it useful: one large inline branching dataset, generated line ids, an aggressive setZoom(30) startup, full-viewport presentation, translucent circular defaults, and a timer that alternates force repulsion and elasticity without reloading data. That makes it a strong reference when the goal is unattended force-layout motion rather than graph editing, business semantics, or custom rendering.
Where Else This Pattern Applies
This pattern transfers well to kiosk displays, conference booth loops, landing-page background visuals, and dashboard screens that should stay visually active without operator input. The timer can keep a relationship surface alive while the underlying dataset remains fixed.
It also fits dependency maps or topology views where teams want the same graph to rebalance itself under changing runtime modes. In a production version, the timer could be replaced by status changes, mode switches, or scheduled intervals while keeping the same updateOptions(...) technique.
Finally, this is a practical starting point for branded motion surfaces. If a team wants layout-driven animation rather than bespoke SVG artwork, the combination of built-in force layout, runtime option updates, translucent defaults, and a lightweight canvas style override is easy to adapt.