Force Layout Live Parameter Tuning
This example builds a force-directed network from one static branching dataset and exposes live controls for node repulsion, line elasticity, and relayout duration. It is a compact reference for updating the active ForceLayout instance and rerunning the built-in solver without rebuilding the graph data.
Force Layout Live Parameter Tuning
What This Example Builds
This example builds a full-screen force-directed graph playground around one static branching dataset. The canvas shows uniform circular nodes and straight teal links, while a floating white control window lets the user tune node repulsion, line elasticity, and the number of layout iterations.
The main point is not custom rendering or graph editing. The useful part is that the graph is loaded once, then the running force solver is adjusted in place. The same floating window also exposes shared workspace utilities such as wheel-mode switching, drag-mode switching, and image export.
How the Data Is Organized
The graph data is assembled inline inside MyGraph.tsx. It uses one RGJsonData object with rootId: 'a', a rawNodes array, and a rawLines array. Before calling setJsonData, the example maps every raw line to a new object with a generated id, so the final dataset passed into relation-graph has explicit line identifiers.
The sample data forms a rooted branching structure: one root node a, four top-level branches (b, c, d, e), and deeper child groups below each branch. In total, the file defines 103 nodes and 102 links. In a production app, the same structure could represent an organization tree, dependency expansion, category taxonomy, incident propagation tree, or any hierarchy-like network that still benefits from force spacing instead of a strict tree layout.
How relation-graph Is Used
The page is wrapped in RGProvider, and MyGraph obtains the provider-scoped graph instance through RGHooks.useGraphInstance(). The RelationGraph component receives a focused set of graph options: circular nodes, 60x60 default node size, straight border-attached links, cyan and teal default colors, and the built-in force layout.
The graph instance API drives the runtime behavior. initializeGraph() loads the prepared RGJsonData, centers the graph, and sets a fixed zoom level. updateMyOptions() reaches into graphInstance.layoutor as RGLayouts.ForceLayout and pushes new physics values without rebuilding the dataset. A separate restartForceLayout() path rewrites layout.maxLayoutTimes, calls doLayout(), then recenters and re-zooms the canvas so the user can compare continuous layout with a fixed iteration budget.
There are no node slots, line slots, or editing APIs in this example. Styling is done through graph options plus a local SCSS file that forces white node text and defines checked-line overrides. The draggable helper window and its settings panel come from shared local components and use relation-graph hooks again for canvas options and image generation.
Key Interactions
- The
Node Repulsionslider changesforce_node_repulsionon the active force layouter. - The
Line Elasticslider changesforce_line_elasticon the active force layouter. - A segmented selector switches between
Layout ForeverandLayout Fixed Times(...). - When fixed mode is selected, a range input controls
maxLayoutTimesand triggers a freshdoLayout()run. - The floating helper window can be dragged and minimized, so the user can inspect the graph while keeping controls nearby.
- The helper window’s settings view changes wheel behavior, changes canvas drag behavior, and downloads an image snapshot.
Node and line click handlers are present, but in the reviewed source they only log the clicked objects to the console. They do not drive layout, selection, or editing behavior here.
Key Code Fragments
This fragment shows that the example uses relation-graph’s built-in force layout together with a minimal visual style instead of custom node rendering.
const graphOptions: RGOptions = {
debug: true,
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 60,
defaultNodeHeight: 60,
defaultLineColor: 'rgba(0, 186, 189, 1)',
defaultNodeColor: 'rgba(0, 206, 209, 1)',
defaultLineShape: RGLineShape.StandardStraight,
layout: { layoutName: 'force', maxLayoutTimes: Number.MAX_SAFE_INTEGER },
defaultJunctionPoint: RGJunctionPoint.border
};
This fragment shows that the graph data is prepared once by adding explicit line IDs before loading it into the graph instance.
const linesWithIds = rawLines.map((line, index) => ({
...line,
id: `l${index + 1}`
}));
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: rawNodes,
lines: linesWithIds
};
This fragment shows the one-time data load and the explicit viewport reset that follows it.
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.setZoom(30);
This fragment shows the first runtime control path: update the active ForceLayout instance directly when the sliders change.
const forceLayout = graphInstance.layoutor as InstanceType<typeof RGLayouts.ForceLayout>;
if (forceLayout) {
forceLayout.updateOptions(myForceLayoutOptions);
}
This fragment shows the second runtime control path: switch the relayout mode by rewriting layout options and rerunning the solver.
graphInstance.updateOptions({
layout: {
layoutName: 'force',
maxLayoutTimes: layoutForever ? Number.MAX_SAFE_INTEGER : maxLayoutTimes
}
});
await graphInstance.doLayout();
graphInstance.moveToCenter();
graphInstance.setZoom(30);
This fragment shows how the floating UI separates continuous layout from slider-capped layout runs.
<SimpleUISelect currentValue={layoutForever} data={[
{ value: true, text: 'Layout Forever' },
{ value: false, text: `Layout Fixed Times(${maxLayoutTimes})` }
]} onChange={(newValue: boolean) => { setLayoutForever(newValue); }} />
{!layoutForever && <input
type="range"
min="10"
max="1000"
step="20"
value={maxLayoutTimes}
What Makes This Example Distinct
This example is distinct because it treats the built-in force layout as a minimal live baseline rather than as a large control surface. The graph stays on one inline dataset, the visible force controls stay limited to repulsion and line elasticity, and iteration control is kept in a separate continuous-versus-fixed relayout switch.
Compared with layout-force-options, this version is easier to read as a starting point because it does not add dataset presets or a broader panel of global force coefficients. Compared with performance-test-force-layout, it avoids performance scaffolding such as synthetic scale presets, minimap navigation, and custom node rendering. Compared with layout-force-options-pro, it stays at global solver tuning instead of per-node or per-line force overrides and runtime graph mutation.
Another distinct aspect is the visual restraint. The page keeps borderless 60px circles, straight teal links, and one draggable white helper window, so the force behavior itself remains the main thing being demonstrated.
Where Else This Pattern Applies
This pattern transfers well to internal tools where teams need to tune spacing behavior before committing to a final graph design. For example, the same approach can be used for dependency graphs, service topology maps, organization structures, or taxonomy explorers where the data stays stable but the desired force behavior is still being calibrated.
It is also a practical pattern for operator-facing workbenches. A product can load one graph once, let users adjust solver intensity and rerun duration, then preserve the same dataset while they compare readability, overlap reduction, and export quality under different layout settings.