Force Layout Runtime Parameter Controls
This example demonstrates a full-screen relation-graph force-layout playground with two preset datasets, six runtime force sliders, and a switch between continuous and fixed-iteration layout runs. It uses the live graph instance to reload data, mutate the active ForceLayout, and expose shared canvas settings and image export from a floating utility window.
Runtime Force Layout Tuning with Dataset Presets
What This Example Builds
This example builds a full-screen force-layout playground around relation-graph’s built-in force layout. The page shows a blue circular-node network on the main canvas and a floating control window above it. Users can switch between a compact weighted sample and a larger branching sample, adjust six global force coefficients, choose whether layout runs continuously or stops after a fixed number of iterations, and open shared canvas utilities such as drag-mode settings, wheel-mode settings, and image export.
The main point is not custom rendering. It is a runtime control pattern for the built-in layouter: keep the graph mounted, mutate the active force solver, and rerun layout only when the iteration policy changes.
How the Data Is Organized
The example keeps two local RGJsonData presets in example-data.ts. exampleDataSmall is a compact tree-like graph with one oversized root node that sets force_weight: 10000, while exampleDataBig is a denser branching graph with many more descendants. Before loading either preset, initializeGraph() maps over every line and adds a generated id such as l1, l2, and l3, then rebuilds the payload with rootId: 'a'.
In a production graph, the same structure could represent two saved scenarios of the same domain data: a focused subgraph versus a broader network, a low-density customer relationship view versus a full account neighborhood, or a small test case versus a realistic dataset for tuning layout parameters.
How relation-graph Is Used
The entry component wraps the page in RGProvider, then renders RelationGraph inside MyGraph, so all runtime graph APIs are available through RGHooks.useGraphInstance() rather than through prop drilling. The graph stays on the built-in force layout, with circular nodes, straight lines, border junction points, and matching blue defaults for nodes and links. The stylesheet in my-relation-graph.scss only overrides checked-state text and line colors; it does not replace node or line templates.
The runtime control pattern is split into two layers. First, the live solver is mutated directly by reading graphInstance.layoutor, casting it to RGLayouts.ForceLayout, and calling updateOptions(myForceLayoutOptions). That is what makes the six sliders act like live tuning controls instead of a full reload path. Second, the example uses graphInstance.updateOptions({ layout }) followed by doLayout() when the user changes continuous mode versus fixed iteration mode. This separation keeps coefficient tuning lightweight while still allowing the page to demonstrate a capped rerun policy.
Dataset changes use a more explicit reload path. The example picks small or big, rebuilds the line array with generated ids, then calls stopAutoLayout(), clearGraph(), sleep(500), and setJsonData(...). After loading, it recenters the graph with moveToCenter() and resets the zoom to 30. No custom node, line, canvas, or viewport slots are defined in this example; it relies on built-in rendering and shared helper components instead.
The floating tool window comes from the shared DraggableWindow.tsx component. That wrapper adds dragging, minimize/restore behavior, a canvas-settings overlay, and screenshot export. The segmented selectors come from SimpleUISelect.tsx, and image export ultimately uses domToImageByModernScreenshot.ts to convert the prepared graph DOM into a downloadable blob.
Key Interactions
- Switching the dataset preset reloads the graph from either
exampleDataSmallorexampleDataBig, then recenters and zooms the viewport again. - Moving any of the six range inputs updates the active
ForceLayoutinstance in place, so users can watch the current graph respond without rebuilding the component. - Toggling between
Layout ForeverandLayout Fixed Times(...)changes how long the force solver runs. When fixed mode is selected, an additional range input appears so the iteration cap can be adjusted. - The floating control surface itself is interactive: it can be dragged, minimized, expanded, and switched into a shared settings panel.
- The shared settings panel changes wheel behavior, changes canvas drag behavior, and exports the current graph as an image. Those tools are available because of the shared helper window, not because this example implements a dedicated export workflow of its own.
Key Code Fragments
This block proves the example is configured around relation-graph’s built-in force layout and built-in default rendering rather than custom slots.
const graphOptions: RGOptions = {
debug: true,
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 80,
defaultNodeHeight: 80,
defaultLineColor: 'rgb(0,139,189)',
defaultNodeColor: 'rgb(0,139,189)',
defaultLineShape: RGLineShape.StandardStraight,
layout: {
layoutName: 'force',
maxLayoutTimes: Number.MAX_SAFE_INTEGER
}
};
This block shows the explicit dataset reload path, including generated line ids and the graph-instance APIs used to clear, reload, recenter, and reset zoom.
const linesWithIds = exampleData.lines.map((line, index) => ({
...line,
id: `l${index + 1}`
}));
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: exampleData.nodes,
lines: linesWithIds
};
graphInstance.stopAutoLayout();
graphInstance.clearGraph();
await graphInstance.sleep(500);
await graphInstance.setJsonData(myJsonData);
This block is the core runtime-tuning technique: the page reads the current layouter and pushes new global force coefficients into the live ForceLayout.
const updateMyOptions = async () => {
const forceLayout = graphInstance.layoutor as InstanceType<typeof RGLayouts.ForceLayout>;
if (forceLayout) {
forceLayout.updateOptions(myForceLayoutOptions);
}
};
This block shows that continuous-versus-fixed layout mode is handled separately from the live coefficient updates.
const restartForceLayout = async () => {
graphInstance.updateOptions({
layout: {
layoutName: 'force',
maxLayoutTimes: layoutForever ? Number.MAX_SAFE_INTEGER : maxLayoutTimes
}
});
await graphInstance.doLayout();
graphInstance.moveToCenter();
graphInstance.setZoom(30);
};
This block proves the force-control panel exposes six independent global coefficients rather than only one or two simple sliders.
<div className="py-2">Node Repulsion: {myOptions.force_node_repulsion}</div>
<input type="range" min="0.2" max="3" step="0.1" value={myOptions.force_node_repulsion} />
<div className="py-2">Line Elastic: {myOptions.force_line_elastic}</div>
<input type="range" min="0.2" max="3" step="0.1" value={myOptions.force_line_elastic} />
<div className="py-2">maxTractionLength: {myOptions.maxTractionLength}</div>
<input type="range" min="100" max="1000" step="50" value={myOptions.maxTractionLength} />
This block shows that the inherited floating window also exposes canvas settings and export through graph-instance APIs.
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
await graphInstance.restoreAfterImageGeneration();
What Makes This Example Distinct
This example is distinct because it combines three patterns that are often split apart in other demos. First, it exposes six global force coefficients through a dedicated panel and applies them to the live layouter with RGLayouts.ForceLayout.updateOptions(...). Second, it compares those same controls across two preset datasets instead of holding the user on one static graph. Third, it separates live coefficient mutation from iteration-policy reruns, which makes the example more useful for runtime tuning than a simpler force demo.
Compared with layout-force, this page is less of a minimal built-in-force baseline and more of a comparison lab for global force settings across preset graphs. Compared with performance-test-force-layout, it stays on ordinary built-in rendering and curated local datasets instead of moving into performanceMode, very large synthetic graphs, a minimap, or custom node-slot stress testing. Compared with layout-switching demos such as io-tree-layout and layout-tree, the main lesson is solver behavior on a running force instance, not orientation changes or hierarchy-specific routing rules.
The smaller preset also includes an oversized weighted root, which makes the effect of the force solver easier to observe visually. That detail is part of the sample data, not a separate UI control.
Where Else This Pattern Applies
This pattern applies when a product needs a tuning surface for a built-in force layout without building a custom layouter from scratch. Good examples include internal knowledge-graph tools where analysts need to stabilize spacing, investigation views where dense neighborhoods must be compared against smaller focused subgraphs, and pre-sales demos where teams want to show how one graph engine behaves under different densities and solver durations.
It also transfers well to admin or diagnostic tools. A support console could expose the same kind of floating panel for drag behavior, wheel behavior, screenshot export, and limited layout controls while keeping the graph itself read-only. The important idea is to separate data reload, live solver mutation, and rerun policy so each interaction stays predictable.