Force Layout Node Weight And Line Elasticity
A full-screen force-layout playground that compares per-node weight and per-line elasticity on the same live graph. Users can randomize the value ranges, retune the running solver, and add child nodes while relation-graph recalculates the layout.
Force Layout Lab for Node Weight and Line Elasticity
What This Example Builds
This example builds a full-screen force-layout playground around a single root node and a ring of connected child nodes. The graph uses black circular nodes, straight links, a right-side built-in toolbar, and a floating white control window, so the canvas reads like a physics lab instead of a business diagram.
Users can switch the experiment target between nodes and lines, randomize the value range used for that target, retune the running force solver, and add two new child nodes to the selected node or the root. The main point is not just that the force layout runs, but that element-level force values are made visible on the canvas through node size, node text, line labels, and line width.
How the Data Is Organized
The initial dataset is assembled inline in initializeGraph(). It starts with one root node and then appends 30 child nodes plus 30 root-to-child lines before calling setJsonData(...).
After the data is mounted, the example immediately preprocesses the live graph instead of rebuilding JSON for each experiment. In node mode it clears previous overrides, assigns a random force_weight to each non-root node, scales the node size from that value, writes the sampled number into the node label, and then resets all child nodes into a comparable ring before restarting auto layout. In line mode it clears previous overrides and rewrites each rendered line with a random force_elastic, a numeric label, and a matching line width.
This structure maps cleanly to real data where one hub connects to many dependents. In a production system, the same pattern could represent a central service and its downstream consumers, a team lead and direct collaborators, a package and its dependencies, or any graph where node importance and link strength should visibly influence spacing.
How relation-graph Is Used
The demo is wrapped in RGProvider, and RelationGraph is mounted with relation-graph’s built-in force layout. Its options keep the graph minimal: debug mode is off, nodes are circular and black by default, lines are straight, the toolbar is vertical on the right side, and line junctions attach to node borders.
RGHooks.useGraphInstance() is the main control surface. The example uses it to load data with setJsonData(...), read the live graph with getNodes(), getLines(), getRootNode(), and getCheckedNode(), mutate rendered elements with updateNode(...) and updateLine(...), append structure with addNodes(...) and addLines(...), and manage layout motion with startAutoLayout(), stopAutoLayout(), enableNodeXYAnimation(), disableNodeXYAnimation(), moveToCenter(), zoomToFit(), enableCanvasAnimation(), and disableCanvasAnimation().
The force-layout controls are updated without remounting the graph. A React state object stores the slider values, and graphInstance.layoutor is cast to RGLayouts.ForceLayout so updateOptions(...) can push new coefficients into the running solver.
The floating control panel comes from the shared DraggableWindow component. That window adds dragging, minimizing, a shared canvas-settings overlay, and image export through prepareForImageGeneration(...) and restoreAfterImageGeneration(). Supporting controls are split into MyForceLayoutOptions for solver sliders, SimpleUISelect for mode switching, and SimpleUINumberRange for editable randomization ranges.
Styling is intentionally light. The local SCSS overrides white node text on dark nodes, keeps line labels on white chips by default, and inverts label and stroke colors for checked lines so selected elasticity samples remain readable.
Key Interactions
- The
Observation Objectselector switches the panel between the node-weight experiment and the line-elasticity experiment. - The numeric range inputs define the randomization interval used by the reset buttons, so users can widen or narrow the sampled force values before rerunning the experiment.
Randomly Reset Node Weightrewrites per-nodeforce_weight, size, text, and positions, then restarts the force layout from a comparable ring.Randomly Reset Line Elasticrewrites per-lineforce_elastic, labels, and widths on the mounted graph, then resumes auto layout.Add Two Child Nodes to Selected Nodesappends new nodes and lines to the checked node, or to the root when no node is selected, then relaunches the solver.- The floating window can be dragged or minimized, and its settings panel lets users switch wheel and drag behavior or export the current graph as an image.
Key Code Fragments
This fragment shows that the graph stays on relation-graph’s built-in force layout and configures the canvas around that choice.
const graphOptions: RGOptions = {
debug: false,
defaultNodeBorderWidth: 0,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeWidth: 70,
defaultNodeHeight: 70,
defaultNodeColor: '#000000',
defaultNodeShape: RGNodeShape.circle,
toolBarDirection: 'v',
toolBarPositionH: 'right',
toolBarPositionV: 'center',
layout: {
layoutName: 'force',
maxLayoutTimes: Number.MAX_SAFE_INTEGER
},
This fragment proves that node mode writes element-level force_weight values back into each child node’s size and label.
nodes.forEach(node => {
if (node.id === rootNode.id) return;
const nodeWeight = rangeForNode[0] + Math.random() * rangeForNode[1];
const size = 10 + 10 * Math.sqrt(nodeWeight);
graphInstance.updateNode(node, {
width: size,
height: size,
color: '#000000',
force_weight: nodeWeight,
text: nodeWeight.toFixed(1),
data: { ...node.data }
});
});
This fragment shows the staged reset that makes weight differences easier to compare before auto layout resumes.
nodes.forEach((node, nodeIndex) => {
if (node.id === rootNode.id) return;
const resetNodeXy = getOvalXy(rootNode.x, rootNode.y, 100, nodeIndex, nodes.length);
graphInstance.updateNode(node, {
x: resetNodeXy.x,
y: resetNodeXy.y
});
});
await graphInstance.sleep(500);
graphInstance.disableNodeXYAnimation();
graphInstance.startAutoLayout();
This fragment shows that line mode uses the live line set to encode force_elastic as both physics data and visible styling.
graphInstance.getLines().forEach(line => {
const lineElastic = rangeForLine[0] + Math.random() * rangeForLine[1];
graphInstance.updateLine(line, {
text: lineElastic.toFixed(1),
force_elastic: lineElastic,
lineWidth: 0.5 + lineElastic,
color: '#000000',
fontColor: '#000000',
data: { ...line.data }
});
});
This fragment shows the incremental graph mutation path used by the add-child action.
graphInstance.addNodes(newNodes);
graphInstance.addLines(newLines);
graphInstance.startAutoLayout();
This fragment shows how the slider panel updates the active ForceLayout instance without rebuilding the graph.
const updateMyOptions = async () => {
const forceLayout = graphInstance.layoutor as InstanceType<typeof RGLayouts.ForceLayout>;
if (forceLayout) {
forceLayout.updateOptions(myForceLayoutOptions);
}
};
What Makes This Example Distinct
Compared with the nearby layout-force-options example, this demo is much less about global solver coefficients alone. Its main lesson is that node mass and line pull strength can both be tested as per-element behaviors on the same mounted graph.
Compared with layout-force and performance-test-force-layout, this example is not mainly a baseline playground or a scale test. It adds a staged reset workflow, explicit value encoding, and light graph mutation so the user can compare force behavior visually instead of only moving sliders and watching the layout drift.
Compared with line-shape-and-label, the line updates here are not primarily about connector rendering. They exist to expose force_elastic inside the solver, so line labels and widths become measurement readouts for the force experiment.
The strongest rare combination in the comparison data is the mix of observation-mode switching, per-node force_weight overrides, per-line force_elastic overrides, live ForceLayout.updateOptions(...) tuning, and add-two-child-nodes mutation inside one draggable workspace. That makes this example a better starting point for comparative force-behavior studies than for general graph viewing.
Where Else This Pattern Applies
This pattern transfers well to internal tools that need to explain or debug layout behavior before real styling is added. A team can use the same approach to test how dependency strength, workload weight, influence score, or communication intensity should affect graph spacing.
It also fits teaching and tuning scenarios. For example, a workflow designer could compare strong and weak transitions between stages, an infrastructure team could visualize heavy services versus weak couplings, or an organization chart prototype could show how role weight changes clustering before moving on to richer node templates and domain data.