Multi-Group Graph Layout and Style Editor
Builds a fixed-canvas graph workbench where users can insert randomly generated tree groups, assign each group its own layout and visual style, and then edit or delete the group as a unit. The example combines per-group sublayout execution, floating configuration panels, selection overlays, minimap support, and shared canvas export and interaction settings.
Editing Multiple Graph Groups on a Fixed Canvas
What This Example Builds
This example builds a lightweight graph workbench rather than a single preloaded chart. The page shows a full-screen RelationGraph canvas with a dark grid background, a floating white control window, a minimap, and selection overlays for the currently edited group.
Users can insert new graph groups at runtime. Each inserted group is generated as a random tree, placed around a chosen root coordinate, styled with its own node and line settings, and laid out with its own algorithm. After insertion, clicking any node in that group promotes the whole group into an editable selection, which exposes batch relayout, restyling, and deletion actions.
The main point of the example is not just “mixed layout” in the abstract. It shows how to keep the outer canvas fixed while treating each subgraph as an independently styled and independently laid out module.
How the Data Is Organized
The source data starts as a tree-shaped JsonNode structure produced by generateRandomTreeData(...). Its configuration is small but expressive: depth, childCount, and hasChildrenProbability define how large and how branchy each generated group can be.
Before the data reaches relation-graph, flattenTreeData(...) converts that nested tree into flat nodes and lines arrays. During that flattening step, each node keeps an isLeaf flag inside data, which gives downstream code a place to attach view-specific metadata without changing the visible node fields.
The group manager then adds one more layer of preprocessing. appendTreeNodeData(...) writes the same myGroupId into every node in the new group and assigns a shared junction-point style to every line. That myGroupId becomes the key that drives selection, relayout, style mutation, and deletion later.
The example also keeps a separate runtime map from myGroupId to { groupRootNodeId, layoutOptions }. That map is what makes the editor stateful: once a group is on the canvas, the code can retrieve that group’s root node and rerun only that group’s chosen layout algorithm.
In a real product, the random tree can be replaced with any grouped relationship data: business capability islands, service clusters, workflow fragments, organizational units, or multiple dependency subgraphs that need to coexist in one workspace.
How relation-graph Is Used
The outer RelationGraph runs in fixed layout mode, so relation-graph does not try to arrange the entire canvas at once. Instead, the example uses RGHooks.useGraphInstance() as the control surface for runtime graph operations such as addNodes, addLines, updateNode, updateLine, createLayout, setEditingNodes, removeNodes, removeLines, moveToCenter, and zoomToFit.
Per-group layouting is delegated to MyMixLayoutManager. When a group is inserted or edited, the manager finds only the nodes with the selected myGroupId, moves that group’s root node to the stored coordinate, creates a non-main layout instance with fixedRootNode = true, and runs the chosen algorithm against just that subset. The layout editor exposes center, force, tree, folder, circle, and fixed, plus algorithm-specific parameters and general alignment settings.
Slots and editing helpers carry a lot of the interaction design. RGSlotOnView hosts RGMiniView, RGEditingNodeController, and RGEditingReferenceLine, so the selected group gets viewport-level action buttons, reference-line assistance, and a live minimap without changing the main graph markup.
The floating DraggableWindow is more than a wrapper. In creation mode it contains the random-tree generator, root-position controls, and default group styles. In editing mode it switches to the tabbed group editor. Its shared settings panel uses the graph instance and graph store to change wheel behavior, change drag behavior, and export the current canvas image through prepareForImageGeneration() and restoreAfterImageGeneration().
Styling is partly handled through relation-graph properties and partly through CSS overrides. The SCSS file gives the canvas a dark gridded workspace look and forces line labels onto white backgrounds, which keeps labels readable even when a group uses custom colors or animated lines.
Key Interactions
The primary interaction is group insertion. The user sets tree-generation ranges, decides whether the root position is random or manually entered, chooses default node, line, and layout settings, and then inserts a new group into the fixed canvas.
Selection is group-based rather than node-based. Clicking one node calls selectGroupNodes(...), which resolves the full myGroupId cohort and passes that set into setEditingNodes(...). That makes the viewport overlay and the edit panel operate on the group as a unit.
The selected group can then be relaid out or restyled in batch. The layout tab changes the stored layout options for that group, while the node and line tabs mutate the appearance of every node or every connecting line inside the same group. Applying changes reruns layout for that subset only.
The viewport overlay adds two direct actions: open the group editor and delete the selected group. Clicking blank canvas exits the current edit context by clearing checked state and emptying the editing-node set.
There are also workspace-level interactions. The settings overlay can switch mouse wheel behavior between scroll, zoom, and none, switch drag behavior between selection, move, and none, and export the current canvas as an image. Line clicks are not part of the editing workflow here; the current handler only logs the clicked line.
Key Code Fragments
This fragment shows that the main canvas is intentionally fixed, so mixed layout behavior is pushed into per-group runtime logic instead of one global auto-layout.
const graphOptions: RGOptions = {
debug: false,
layout: {
layoutName: 'fixed'
}
};
This fragment shows how generated tree data is turned into one identifiable group before it is added to relation-graph.
const treeJsonData: RGJsonData = flattenTreeData(treeRootNode);
treeJsonData.nodes.forEach((node: JsonNode) => {
node.data = { myGroupId: groupItemsDefaultOptions.myGroupId };
});
treeJsonData.lines.forEach((line: JsonLine) => {
line.fromJunctionPoint = groupItemsDefaultOptions.junctionPoint;
line.toJunctionPoint = groupItemsDefaultOptions.junctionPoint;
});
this.graphInstance.addNodes(treeJsonData.nodes);
this.graphInstance.addLines(treeJsonData.lines);
This fragment shows the runtime insertion flow: generate a UUID, build tree data, append it, then lay out only the new group.
const newGroupId = graphInstance.generateNewUUID(8);
const treeRootNode: JsonNode = generateRandomTreeData(generateRandomTreeDataConfig, newGroupId);
const leftGroupInfo = myLayout.current.appendTreeNodeData(
treeRootNode,
{
myGroupId: newGroupId,
junctionPoint: RGJunctionPoint.lr
}
);
await graphInstance.sleep(500);
await myLayout.current.layoutGroupNodes({
This fragment shows how a selected group is relaid out around its own root while remaining separate from the main canvas layout.
const groupRootNode = this.graphInstance.getNodeById(groupRootNodeId);
if (groupRootNode) {
this.graphInstance.updateNodePosition(groupRootNode, rootNodeXy.x, rootNodeXy.y);
const myGroupLayout = this.graphInstance.createLayout<InstanceType<typeof RGLayouts.ForceLayout>>(layoutOptions);
myGroupLayout.isMainLayouer = false;
myGroupLayout.layoutOptions.fixedRootNode = true;
myGroupLayout.placeNodes(groupNodes, groupRootNode);
}
This fragment shows that node clicks and blank-canvas clicks control entry into and exit from the group editing state.
const onNodeClick = (node: RGNode, $event: RGUserEvent) => {
console.log('Node clicked:', node.id, node);
myLayout.current.selectGroupNodes(node.data?.myGroupId);
if (editingGroupStyles.myGroupId !== node.data?.myGroupId) {
openEditGroupStylesPanel();
}
};
const onCanvasClick = () => {
graphInstance.clearChecked();
graphInstance.setEditingNodes([]);
This fragment shows the viewport-level tooling layered onto the graph: minimap, group actions, and reference-line assistance.
<RelationGraph
options={graphOptions}
onNodeClick={onNodeClick}
onLineClick={onLineClick}
onCanvasClick={onCanvasClick}
>
<RGSlotOnView>
<RGMiniView />
<RGEditingNodeController>
This fragment shows the shared canvas settings path for export and live interaction-mode changes.
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();
What Makes This Example Distinct
According to the prepared comparison data, this example is most distinct when read as a multi-group authoring pattern, not as a generic layout demo. Its uncommon trait is the combination of a fixed outer canvas with runtime insertion of multiple random-tree groups, each carrying its own saved root coordinate, layout options, node style, and line style.
Compared with batch-operations-on-nodes, the selection model is more opinionated. That example batch-edits arbitrary selected nodes, while this one upgrades a node click into a whole-group selection via myGroupId and then treats the selected subgraph as a designed module.
Compared with gee-node-alignment-guides, reference lines are supporting infrastructure rather than the main lesson. The stronger lesson here is the full workflow of group insertion, group relayout, group restyling, and group deletion inside one compact editor surface.
Compared with layout-center, this example is less about demonstrating one layout across one dataset and more about composing several graph islands incrementally. Compared with undo-redo-example, it is also narrower than a full free-form editor: it does not focus on arbitrary edge authoring or history management, but on repeated insertion and batch editing of grouped tree networks.
That makes it a particularly strong starting point when a team needs a semi-structured graph workspace: more dynamic than a static demo, but lighter than a full diagram editor.
Where Else This Pattern Applies
This pattern can be reused for architecture workbenches where teams drop several service clusters onto one board and tune each cluster separately. It also fits planning tools where each inserted group represents a department, a project stream, or a capability map that needs its own local layout.
Another extension is a visual composition surface for reusable graph modules. A product could let users save styled subgraphs as templates, insert them into a larger canvas, and then keep editing each inserted module through the same group-level selection and relayout flow shown here.
It also transfers well to educational or simulation tooling. Instead of random trees, the generator could produce scenario packs, dependency bundles, or lesson-specific graph fragments, while the fixed outer canvas still acts as the common workspace for arranging, comparing, and exporting the resulting groups.