Custom GraphInstance Mixed Layout
This example replaces the default relation-graph core with a custom `RelationGraphCore` subclass that reads two inline datasets from `graphOptions.data` and composes them into one center-plus-tree scene. It is a focused reference for provider-level graph-core injection, custom `doLayout()` orchestration, and one-canvas mixed-layout composition.
Use a Custom GraphInstance to Load Data and Compose Two Layouts
What This Example Builds
This example builds a viewer-style relation-graph scene that renders two disconnected datasets inside one full-height canvas. The left side appears as a centered cluster of smaller circular nodes with straight links, while the right side appears as a left-to-right tree with wider rectangular nodes and curved labeled edges.
Users do not edit graph content directly. They inspect the prepared scene, use the built-in toolbar in the bottom-right corner, drag or minimize the floating helper window, switch wheel and drag behavior from its settings panel, and export the current canvas as an image. The key point is that the mixed scene is owned by a custom RelationGraphCore subclass rather than being assembled only from component-level button handlers.
How the Data Is Organized
The graph content is declared in MyGraph.tsx as two inline RGJsonData objects: myLeftJsonData with root a, and myRightJsonData with root 2. Instead of merging them into one payload and calling setJsonData(...), the example stores both objects under graphOptions.data and lets the custom graph instance consume them during layout.
There is a preprocessing step before placement. Inside MyGraphInstance.doLayout(), both datasets are restyled in memory before insertion: right-side nodes become semi-transparent with transparent fill, left-side nodes become 80x80 circles with smaller text, and left-side lines are changed from the graph defaults to straight border-attached connectors. Only after that mutation does the code call addNodes(...) and addLines(...) for each dataset.
In a real system, the same shape could represent one focal network plus one hierarchy that needs different layout rules: a service cluster plus downstream dependencies, a core device map plus a component tree, or a knowledge hub plus a classification branch. The demo labels are placeholders, but the data transport pattern maps directly to real entity IDs, labels, and relationship types.
How relation-graph Is Used
index.tsx wraps the example in RGProvider relationGraphCore={MyGraphInstance}. That is the architectural center of the demo: RelationGraph still renders the canvas, but the running graph core is replaced with a subclass that overrides doLayout(). In MyGraph.tsx, RGHooks.useGraphInstance() retrieves that live instance, and a mount-only useEffect(...) calls graphInstance.doLayout() once to populate and arrange the graph.
The graph options keep the outer canvas in layoutName: 'fixed', which leaves placement control to the custom instance. Those same options set 170x40 default nodes, curved left-right edge routing, black default edge color, on-path edge labels, a 10-character line-label cap, and a horizontal built-in toolbar placed at the bottom-right corner. The data field is used here as a custom transport channel for myLeftJsonData and myRightJsonData; it is not a general requirement of built-in layouts.
Inside MyGraphInstance.doLayout(), the code reads the two datasets from getOptions().data, injects them with addNodes(...) and addLines(...), and then performs a two-phase placement pass. createLayout({ layoutName: 'center' }) arranges the left network first. getNetworkNodesByNode(...) and getNodesRectBox(...) measure that placed region, and the measured maxX is used to offset the right root node. Then createLayout({ layoutName: 'tree', from: 'left', treeNodeGapH: 200, treeNodeGapV: 30 }) arranges the second network. The scene is finished with moveToCenter() and zoomToFit().
The example uses hooks and subcomponents, but not custom slots or custom event handlers. RGHooks.useGraphStore() is used inside the shared CanvasSettingsPanel to reflect the current wheel and drag modes, and graphInstance.setOptions(...) updates those modes at runtime. The imported my-relation-graph.scss file is only a selector scaffold with no declarations, so the visible styling comes mainly from graph options and the node and line mutations performed in the custom graph instance.
Key Interactions
- The floating helper window can be dragged by its title bar and minimized or restored.
- The helper window can switch into a settings overlay.
- The settings overlay changes
wheelEventActionlive betweenscroll,zoom, andnone. - The same overlay changes
dragEventActionlive betweenselection,move, andnone. - The built-in
relation-graphtoolbar stays available in the bottom-right corner of the canvas. Download Imageprepares the graph DOM for capture, exports the current view, and restores the graph state afterward.
Key Code Fragments
This fragment shows that the example replaces the default graph core at the provider level:
const Demo = () => {
return (
<RGProvider relationGraphCore={MyGraphInstance}>
<MyGraph />
</RGProvider>
);
};
This fragment shows that the host graph stays in fixed mode and carries both datasets through graphOptions.data:
layout: {
layoutName: 'fixed'
},
data: {
myLeftJsonData,
myRightJsonData
}
This fragment shows that initialization is a one-time call into the overridden graph-instance layout method:
const initializeGraph = async () => {
// The graphInstance here is a custom instance of relationGraphCore, it overrides the doLayout method, internally reads myLeftJsonData and myRightJsonData from graphOptions.data, and renders this data
graphInstance.doLayout();
};
useEffect(() => {
initializeGraph();
}, []);
This fragment shows that the custom graph core reads both datasets from options and injects them during layout:
async doLayout(customRootNode?: string | RGNode | undefined) {
const {myLeftJsonData, myRightJsonData} = this.getOptions().data;
myRightJsonData.nodes.forEach(node => {
// 为了方便关键线条端点与节点之间的间隙,让节点的视觉效果更不图虫
node.opacity = 0.5;
node.color = 'transparent';
});
this.addNodes(myRightJsonData.nodes);
this.addLines(myRightJsonData.lines);
This fragment shows the per-subgraph restyling that gives the left network a different visual grammar:
myLeftJsonData.nodes.forEach(node => {
// 为了方便关键线条端点与节点之间的间隙,让节点的视觉效果更不图虫
node.opacity = 0.5;
node.nodeShape = RGNodeShape.circle;
node.borderWidth = 1;
node.width = 80;
node.height = 80;
node.fontSize = 10;
node.color = 'transparent';
});
This fragment shows the layout handoff from the centered left network to the right-side tree:
const myLeftLayout = graphInstance.createLayout({
layoutName: 'center'
});
const leftNodes = graphInstance.getNetworkNodesByNode(leftRootNode);
myLeftLayout.placeNodes(leftNodes, leftRootNode);
const nodesRectInfo = graphInstance.getNodesRectBox(leftNodes);
rightRootNode.x = nodesRectInfo.maxX + 100; // 将右侧树的根节点放置在中心布局关系网的右侧 100位置
This fragment shows that the shared settings panel updates graph behavior on the live instance instead of rebuilding the data:
<SettingRow
label="Canvas Drag Event:"
options={[
{ label: 'Selection', value: 'selection' },
{ label: 'Move', value: 'move' },
{ label: 'none', value: 'none' },
]}
value={dragMode}
onChange={(newValue: string) => { graphInstance.setOptions({ dragEventAction: newValue }); }}
/>
What Makes This Example Distinct
The comparison data supports a specific distinction: this example is not notable because it is the only one that mixes a centered network with a right-side tree. Nearby examples such as multiple-layout-mixing-in-single-canvas, gap-of-line-and-node, and canvas-caliper already use closely related center-plus-tree compositions. What changes here is where the ownership lives.
Compared with multiple-layout-mixing-in-single-canvas, the visible composition is similar, but this example moves the orchestration into RGProvider relationGraphCore={MyGraphInstance} and an overridden doLayout(). The custom core reads myLeftJsonData and myRightJsonData from graphOptions.data, restyles them, injects them, measures the left subgraph, and places the right subgraph. That makes it a stronger starting point when a project needs the graph engine itself, not just the React component, to own loading and placement logic.
Compared with gap-of-line-and-node and canvas-caliper, the extra complexity here is infrastructural rather than presentational. Those examples use a similar mixed-layout scaffold for line tuning or coordinate overlays, while this one uses the scaffold to demonstrate provider-level graph-core replacement. The floating helper window, image export, and runtime wheel and drag controls are useful, but the comparison data does not support treating them as unique to this example.
Where Else This Pattern Applies
This pattern transfers well to projects where the graph instance needs to act like an orchestrator instead of a passive renderer. A team could use the same approach when different subgraphs need different layout rules, when data must be injected from custom option fields during layout, or when one subgraph must be measured before placing another beside it.
Possible extension scenarios include combining a service dependency cluster with an ownership tree, combining a device topology with a maintenance hierarchy, or combining one focal entity network with a secondary classification branch. Those are migration targets rather than implemented features, but the core technique here is reusable: keep the host canvas fixed, let a custom graph core ingest multiple datasets, and compose them through staged layout passes.