JavaScript is required

Custom Mixed Subgraph Layouts

This example loads one static relation-graph dataset, partitions it by group metadata, and composes five subgraphs with different layout strategies on a single fixed canvas. It is a strong reference for metadata-driven mixed-layout orchestration, especially when per-group styling and one embedded force-driven region need to coexist in one viewer scene.

Compose Five Subgraph Layouts on One Fixed Canvas

What This Example Builds

This example builds a single relation-graph scene that looks like a collage of five separate subgraphs arranged around one canvas. The left, right, top, bottom, and bottom-right regions each use a different layout strategy, but they are still loaded into one graph instance and shown together in one viewport.

The user sees color-coded node groups, visibly different connector styles, and a floating white helper window above the graph. That window can be dragged, minimized, switched into a settings panel, and used to download an image of the current graph. The graph itself stays read-only: node and line clicks only log the selected item. The most notable point is the orchestration pattern, not any one layout in isolation. The example shows how one fixed base canvas can host several sublayout passes at the same time, including a top subgroup that keeps moving through a synchronized force layout.

How the Data Is Organized

The data is an inline RGJsonData object created inside initializeGraph(). It contains one static list of nodes and lines, but every node is tagged with data.myGroupId so the runtime can split the graph into left, right, top, bottom, and bottom-right partitions before layout.

There is only light preprocessing before setJsonData(...). A local createLine() helper assigns stable line-{n} ids to every edge, and the group tag on each node becomes the shared key for later decisions. The same metadata drives layout partitioning in MyMixLayoutManager, node rendering in RGSlotOnNode, and several line and node style overrides after load.

In a business graph, this same structure could represent service domains with different topology rules, dependency clusters around a central system, multi-team ownership areas, product families with different branching conventions, or hybrid knowledge maps where each region needs a different visual grammar.

How relation-graph Is Used

index.tsx wraps the example in RGProvider, which gives both MyGraph and the floating utility shell access to the active graph context. The main RelationGraph instance is configured with layoutName: 'fixed', rounded polyline corners, rectangular default nodes, and a thin node border. That fixed outer layout is important because the example does not want one global auto-layout to control the whole scene.

The runtime sequence is imperative. MyGraph gets the active instance through RGHooks.useGraphInstance(), shows a loading state, clears the graph, loads the inline JSON with setJsonData(...), runs applyMyLayout(), centers the viewport, fits the zoom, and clears the loading flag.

MyMixLayoutManager is the real lesson. It queries the loaded nodes from the live instance, filters them by node.data.myGroupId, pins a named root for each region, and then creates non-main layout clones with createLayout(...). The left group becomes a right-growing tree rooted at r, the right group becomes a left-to-right tree rooted at a, the top group becomes a force layout rooted at t, the bottom group becomes a center layout rooted at e, and the bottom-right group becomes a top-down tree rooted at br-root.

The example also uses relation-graph’s update APIs after data load. updateNodePosition() fixes the anchor point for each subgroup root, updateNode() changes shape and size for the circular groups, updateLine() changes routing and junction behavior per region, and getNodeOutgoingNodes() is used to move the right-side expand holder to the outgoing side of nodes that have children.

Custom rendering is handled through RGSlotOnNode. The slot reads the same group metadata and maps it to CSS classes such as .c-left-node and .c-right-node, while SCSS overrides adjust selected-line styling and group-specific node appearance. The floating DraggableWindow is shared helper scaffolding, but it still uses relation-graph hooks in a meaningful way: CanvasSettingsPanel reads option state through RGHooks.useGraphStore(), updates wheel and drag behavior with setOptions(), and uses prepareForImageGeneration() plus restoreAfterImageGeneration() when exporting a graph image.

Key Interactions

  • The helper window can be dragged by its title bar, so the controls can be moved without changing the graph layout.
  • The same window can be minimized or switched into a settings overlay without leaving the graph page.
  • The settings panel changes wheelEventAction live between scroll, zoom, and none.
  • The settings panel changes dragEventAction live between selection, move, and none.
  • Download Image captures the prepared graph canvas and saves it through the shared screenshot utility.
  • The top subgroup keeps moving after initialization because its root node is repositioned on a timer and synced back into the stored force-layout clone.

Key Code Fragments

This fragment shows that the dataset is assembled inline and that every line is normalized with an explicit id before the graph is loaded.

let lineIndex = 0;
const createLine = (line: any): JsonLine => {
    lineIndex++;
    return { id: `line-${lineIndex}`, ...line };
};

const myJsonData: RGJsonData = {
    nodes: [

This fragment shows how the same myGroupId field is embedded directly in the node data and later reused by the layout and rendering logic.

nodes: [
    { id: 'r', text: 'R', data: { myGroupId: 'left' } },
    { id: 'R-b', text: 'R-b', data: { myGroupId: 'left' } },
    { id: 'R-b-1', text: 'R-b-1', data: { myGroupId: 'left' } },
    // right group
    { id: 'a', text: 'a', data: { myGroupId: 'right' } },
    { id: 'c', text: 'c', data: { myGroupId: 'right' } },

This fragment shows the mount-time lifecycle that loads the graph, runs the custom layout manager, and fits the final scene to the viewport.

graphInstance.loading('Wait...');
graphInstance.clearGraph();
await graphInstance.setJsonData(myJsonData);
await myLayout.current.applyMyLayout();

graphInstance.moveToCenter();
graphInstance.zoomToFit();
graphInstance.clearLoading();

This fragment shows one of the non-main layout clones: the left group root is pinned first, then a dedicated tree layout is created just for that subgroup.

const groupRootNode = this.graphInstance.getNodeById('r');
if (groupRootNode) {
    const groupRootNodeXy = {
        x: -500,
        y: 0
    };
    this.graphInstance.updateNodePosition(groupRootNode, groupRootNodeXy.x, groupRootNodeXy.y);
    const currentLayoutClone = this.graphInstance.createLayout({
        layoutName: 'tree', from: 'right'
    });
    currentLayoutClone.isMainLayouer = false;
    currentLayoutClone.layoutOptions.fixedRootNode = true;

This fragment shows the force-driven top subgroup, including the stored layout clone that is later synchronized while the root moves on a timer.

const forceLayoutOptions: RGLayoutOptions = {
    layoutName: 'force',
    force_node_repulsion: 0.2,
    force_line_elastic: 1.5,
    maxLayoutTimes: Number.MAX_SAFE_INTEGER
};

const currentLayoutClone = this.graphInstance.createLayout(forceLayoutOptions);
currentLayoutClone.isMainLayouer = false;
currentLayoutClone.requireLinks = false;
currentLayoutClone.layoutOptions.fixedRootNode = true;
currentLayoutClone.placeNodes(eGroupNodes, groupRootNode);
this.myForceLayout = currentLayoutClone;

This fragment shows that the node slot uses group metadata to switch the rendered node class instead of relying on one default node body.

if (node.id === 'my-root') {
    nodeClass = 'my-root-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'left') {
    nodeClass = 'c-left-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'right') {
    nodeClass = 'c-right-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'top') {
    nodeClass = 'c-top-node c-valign-center';
} else if (node.data && node.data.myGroupId === 'bottom') {
    nodeClass = 'c-bottom-node c-valign-center';
}

This fragment shows the shared export flow that prepares the graph canvas, captures it, and restores the graph state after download.

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
/* ...downloadBlob(imageBlob, 'my-image-name')... */
await graphInstance.restoreAfterImageGeneration();

This fragment shows the SCSS-level differentiation between the left and right node families.

.c-left-node {
    background-color: #df7f03;
    width: 100px;
    height: 40px;
    color: white;
}

.c-right-node {
    width: 150px;
    height: 40px;
    background-color: #f0f0f0;
}

What Makes This Example Distinct

The comparison record makes the safe distinction clear: this is not merely a demo where two built-in layouts happen to share a canvas. Its strongest differentiator is the five-region composition recipe on top of one fixed graph instance. The example partitions one loaded dataset by data.myGroupId, pins a root for each region, and then combines tree, force, and center layout clones in the same scene.

Compared with multiple-layout-mixing-in-single-canvas, this example is more about orchestration scale and coordination. It uses five metadata-driven groups instead of two separately arranged subnetworks, and it ties layout choice to a broader set of post-load styling rules. Compared with mix-layout-8, it is narrower and easier to read as a reference because it is a predetermined viewer scene rather than an editor-like mixed-layout workspace with runtime graph authoring.

The comparison data also supports another restrained claim: the animated force segment is part of what makes this example unusual. The top cluster stores its force-layout clone, moves the root on a timer, and syncs that motion back into the sublayout while the other regions remain deterministic. That makes this example a strong starting point when one part of a graph needs ongoing motion inside an otherwise fixed mixed-layout composition.

The shared floating helper window is not the distinctive part by itself, since the comparison notes that this utility shell is reused elsewhere. What stands out here is the combination of that overlay with a fixed base canvas, five grouped sublayouts, per-group node and connector rules, and one timer-synchronized force island embedded among otherwise stable regions.

Where Else This Pattern Applies

This pattern transfers well to systems where one graph needs several layout grammars at once instead of one graph-wide algorithm. Examples include service landscapes with different cluster types, security maps where ingress, internal topology, and external dependencies need different shapes, or manufacturing views that mix process centers, branching trees, and monitoring islands in one canvas.

It is also useful when a single metadata key should control both placement and appearance. Teams could reuse the same approach for portfolio maps, hybrid org structures, product architecture overviews, or knowledge maps where each category needs its own connector routing, node silhouette, and root placement while still living inside one shared relation-graph instance.