Custom Force Layout on Concentric Rings
This example mounts a custom `RGLayouts.ForceLayout` subclass that keeps relationship nodes moving under force rules while constraining them to configurable concentric rings. A floating control panel lets users retune repulsion, line elasticity, ring diameters, canvas behavior, and image export on the live graph.
Custom Force Layout with Concentric Ring Constraints
What This Example Builds
This example builds a full-screen relationship map where a custom force layout keeps nodes moving, but only along configurable concentric rings. The canvas shows a portrait root node in the center, two anchored first-ring branch roots, explicit labeled relationship lines, and a layered circular backdrop that matches the solver’s orbit rules.
Users can retune node repulsion and line elasticity while the graph is running, drag bars to change ring diameters, use the built-in toolbar to control the live layout, and open a floating settings panel for canvas interaction changes and image export. The main point is not the kinship-flavored sample labels. It is a concrete reference for keeping force motion readable inside an ordered radial composition.
How the Data Is Organized
The graph data is declared inline as one RGJsonData object with rootId, nodes, and lines. Each node carries presentation and layout metadata inside data, especially myColor, myLevel, and limitCircular, while some nodes also use force_weight, fixed, disableDrag, and disablePointEvent to influence solver behavior and interaction.
Before anything is inserted into relation-graph, the code preprocesses that dataset. It centers the root at the graph origin, places the two first-ring anchor nodes with getOvalPoint(...), marks them as fixed and non-draggable, colors their expand controls, and rewrites every line into a dashed no-arrow style. The circularSet state stores ring diameters in pixels, then updateLayoutCircleSet() converts them into radii with an offset so the custom layout constrains node centers rather than outer edges.
In a real application, the same structure could describe tiered stakeholder maps, layered dependency graphs, influence rings around a central entity, or any dataset where each node belongs to a radial band and still needs force-based spacing within that band.
How relation-graph Is Used
index.tsx wraps the page in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() as the control surface. The base graph options keep the host canvas in layoutName: 'fixed', set the default node shape to circles, enable line text on path, and place the toolbar horizontally at the bottom-right. The example does not call setJsonData(). Instead, it preprocesses the inline dataset and loads it with addNodes() and addLines().
The core relation-graph customization is MyForceLayout extends RGLayouts.ForceLayout. In resetCalcNodes() it copies each visible node into a calculation record and carries data.limitCircular into the solver state. In calcNodesPosition() it keeps the usual repulsion and parent-child elasticity, then, after the early iterations, projects every non-root non-fixed node back onto the configured radius for its assigned ring. The page mounts this layouter with setLayoutor(myLayout, true, true), runs placeNodes(...) once, then uses stopAutoLayout() and startAutoLayout() to apply live force-parameter changes.
The example also uses RGSlotOnNode to replace the default node body with two custom renderings: a circular portrait for the root and circular text badges for the other nodes. The concentric ring backdrop is rendered as absolute-positioned layered DOM inside RelationGraph, so the visual guide zones stay aligned with the graph coordinate origin. Styling in my-relation-graph.scss overrides the canvas background, selected-line highlight, node color variants, and the circular node skin. Shared helper components add a draggable floating panel, a drag-based ring-size editor, runtime canvas-mode switching through graphInstance.setOptions(...), and image export through prepareForImageGeneration() and restoreAfterImageGeneration().
Key Interactions
- Moving the
Node Repulsion Coefficientslider updates the mounted custom layouter and restarts auto layout so the spacing force changes immediately. - Moving the
Line Elastic Coefficientslider changes how strongly parent-child links pull related nodes while the solver is running. - Dragging the
SimpleUINumberArraybars changescircularSet, which updates the ring radii used byMyForceLayoutwithout rebuilding the graph data. - The floating helper window can be dragged, minimized, and switched into a settings overlay.
- The settings overlay changes wheel behavior, changes canvas drag behavior, and exports the current scene as an image.
- The root and the two first-ring anchor nodes stay fixed, so the interaction model is about retuning the layout rather than editing graph structure.
Key Code Fragments
This snippet shows that the host graph stays in fixed mode, uses circular nodes, and keeps the toolbar available while the custom layouter runs on top.
const graphOptions: RGOptions = {
debug: true,
defaultLineColor: '#aaaaaa',
defaultNodeColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeBorderWidth: 0,
defaultNodeShape: RGNodeShape.circle,
defaultNodeWidth: 0,
defaultNodeHeight: 0,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
layout: {
layoutName: 'fixed'
}
};
This fragment proves that ring membership is part of the node data model and that the graph carries style metadata separately from visible text.
{ id: 'a', text: '', color: '#cccccc', width: 80, height: 80, force_weight: 10000, disablePointEvent: true, disableDrag: true, data: { myColor: 'root-color', myLevel: 'my-root', limitCircular: 0, img: 'https://img1.baidu.com/it/u=1728803666,3540199393&fm=253&fmt=auto&app=138&f=JPEG?w=200&h=200' } },
{ id: 'l1-1', text: '同一单位', force_weight: 100, data: { myColor: 'my-node-yellow', myLevel: 'my-level1', limitCircular: 1 } },
{ id: 'l2-1', text: '刘宁', data: { myColor: 'my-node-yellow', myLevel: 'my-level2', limitCircular: 2 } },
{ id: 'l3-1', text: '刘六六', data: { myColor: 'my-node-yellow', myLevel: 'my-level3', limitCircular: 3 } },
{ id: 'l1-2', text: '同一系统', force_weight: 100, data: { myColor: 'my-node-green', myLevel: 'my-level1', limitCircular: 1 } },
{ id: 'l4-1', text: '刘明明', data: { myColor: 'my-node-green', myLevel: 'my-level4', limitCircular: 4 } }
This block shows the preprocessing step that fixes the first-ring anchors and rewrites every relationship line into a dashed no-arrow style before loading the graph.
const leve1NodeForSystem = graphJsonData.nodes.find(nodeJson => nodeJson.id === 'l1-2');
leve1NodeForSystem.color = '#40c989';
leve1NodeForSystem.fixed = true;
let nodePoint = getOvalPoint(rootNodeJson.x, rootNodeJson.y, circularSet[0] / 2, 90);
leve1NodeForSystem.x = nodePoint.x;
leve1NodeForSystem.y = nodePoint.y;
leve1NodeForSystem.disableDrag = true;
graphJsonData.lines.forEach(line => {
line.dashType = 2;
line.showEndArrow = false;
});
This is the key custom-layout rule: after the force step runs, each node is projected back onto the radius for its assigned ring.
if (__node1.limitCircular > 0) {
const rgNode = __node1.rgNode;
const nodeCenterX = __node1.x + rgNode.el_W / 2;
const nodeCenterY = __node1.y + rgNode.el_H / 2;
const nodeNewX = nodeCenterX + __node1.Fx;
const nodeNewY = nodeCenterY + __node1.Fy;
const distanceToCenter = isPointInCircleAndIntersection(
0, 0, this.levelCircleSet[__node1.limitCircular], nodeNewX, nodeNewY
);
__node1.Fx = distanceToCenter.intersection.x - nodeCenterX;
__node1.Fy = distanceToCenter.intersection.y - nodeCenterY;
}
This handler shows how live physics tuning is applied to the mounted layouter instead of rebuilding the graph.
const updateLayoutOptions = async () => {
graphInstance.stopAutoLayout();
const forceLayout = graphInstance.layoutor as MyForceLayout;
if (forceLayout) {
forceLayout.updateOptions({
force_node_repulsion,
force_line_elastic
});
forceLayout.start();
}
graphInstance.startAutoLayout();
};
This fragment shows that ring geometry is also editable at runtime through React state and a dedicated UI control.
const updateLayoutCircleSet = async (myLayout: MyForceLayout) => {
myLayout.setLevelCircleSet(circularSet.map(v => v / 2 - 60));
};
useEffect(() => {
const myLayout = graphInstance.layoutor as MyForceLayout;
if (myLayout) {
updateLayoutCircleSet(myLayout);
}
}, [circularSet]);
What Makes This Example Distinct
The prepared comparison data makes the main distinction clear: this example is not just another custom force demo. Compared with customer-layout-force, it keeps explicit relationship lines, assigns each node to a limitCircular tier, fixes the two first-ring branch roots, and continuously projects moving nodes back onto orbit radii. The emphasis is ordered radial motion, not free-form clustering in an edge-free field.
The comparison data also distinguishes it from workspace-style neighbors such as multiple-layout-mixing-in-single-canvas and canvas-caliper. Those examples add structure around the scene, but here the circles are part of the solver rule itself because circularSet is passed into MyForceLayout.setLevelCircleSet(...). The backdrop, the anchor placement, and the constraint logic all reinforce the same layered reading.
Its rare feature combination is also unusually focused: a custom RGLayouts.ForceLayout subclass, live force-parameter tuning, a drag-based ring-diameter editor, fixed anchor nodes, a dark canvas, dashed no-arrow relationship lines, and circular avatar-and-badge node slots. Shared helpers like the floating settings panel and image export are not unique by themselves, but in this example they support a constrained layout-engineering workflow rather than a generic viewer.
Where Else This Pattern Applies
This pattern transfers directly to graphs that need both hierarchy bands and local force-based spacing. Examples include stakeholder maps organized by influence distance, layered risk networks, supplier ecosystems grouped by proximity to a focal company, or dependency maps split into core, adjacent, and peripheral rings.
It is also useful when teams want end users to tune layout geometry without editing graph structure. The draggable ring-size editor can become a control for policy zones, ownership circles, impact bands, or service tiers, while the same custom layouter pattern keeps nodes readable inside whichever radial model the application needs.