JavaScript is required

Sequential Tree Group Layout

This example takes one local hierarchy, splits its first-level branches into separate relation-graph groups, and lays each group out with its own left-to-right folder layout inside a fixed canvas. It then recenters a shared root above the combined bounds, adds display-only orthogonal connectors, and reruns the composition when branches expand or collapse.

Composing Sequential Tree Groups Under a Shared Root

What This Example Builds

This example builds a read-only hierarchy viewer with one large All Departments root above several first-level subtree groups placed in a horizontal row. Each group comes from the same source tree, but the canvas presents them as one graph by drawing separate display-only orthogonal connectors from the shared root to each group root.

Users can expand or collapse branches, navigate with the built-in toolbar and minimap, drag or minimize the floating description window, switch canvas wheel and drag modes in the settings drawer, and export the graph as an image. The main lesson is the layout composition: despite the helper text mentioning several tree directions, the reviewed code uses the same left-origin folder layout for every group and focuses on arranging those groups together inside one fixed graph.

How the Data Is Organized

The source data is a local nested tree object with id, text, and children, returned asynchronously from getMyTreeJsonData(). The overall root is not rendered by sending the full hierarchy to setJsonData(...). Instead, analysisGroups(...) takes the root’s first-level children and turns each subtree into a separate MyTreeGroup.

Inside flatNodeData(...), each nested subtree is converted into flat nodes and lines arrays. During that preprocessing, every node gets data.hasChildren and data.deep, and buildMyTreeGroup(...) adds data.myGroupId so the layout code can query all nodes that belong to one group. Parent nodes also move their expand handle to the right side. In a real application, this structure could represent product categories, organization divisions, capability maps, or any taxonomy where first-level branches need to stay visually distinct while still sharing one top root.

How relation-graph Is Used

index.tsx wraps the example in RGProvider, and MyGraph.tsx renders RelationGraph with layoutName: 'fixed'. That fixed outer layout is deliberate: the example does not ask relation-graph to place the whole hierarchy automatically. Instead, it uses RGHooks.useGraphInstance() to load data imperatively, animate node movement, wait for initial rendering, run a custom layout manager, add manual root connectors, then center and fit the viewport.

The custom logic lives in MyMixTreeLayout. It calls addNodes(...) and addLines(...) to load the injected root plus each flattened group. For each group, it creates a local folder layout with from: 'left', fixedRootNode = true, treeNodeGapV = 20, and treeNodeGapH = -100. Later groups are positioned from the previous group’s bounding box via getNodesRectBox(...), which creates the side-by-side composition. After every group has been placed, the code restyles the first-level group roots, computes the combined bounds, and repositions the shared root above that row.

The visible top-level connectors are also manual. connectRootToChildrenTreeRoot() adds thick orthogonal lines marked forDisplayOnly, so those links are drawn for presentation instead of being derived directly from the flattened subtree data. Interaction hooks stay minimal: onNodeExpand and onNodeCollapse rerun the full group-layout pass, while node and line clicks only log information. The example remains viewer-oriented; it does not expose graph editing controls even though the layout helper contains an unused selection helper.

relation-graph slots handle the presentation layer. RGSlotOnNode replaces the default node content with a centered text card, and RGSlotOnView mounts RGMiniView as a minimap overlay. The shared DraggableWindow component adds a floating description panel, a settings drawer that updates live canvas options with setOptions(...), and screenshot export through prepareForImageGeneration() and restoreAfterImageGeneration(). The SCSS file then overrides internal node text color and the selected-line label and stroke styles.

Key Interactions

  • Expanding or collapsing any branch triggers applyAllGroupLayout(), so one visibility change can reposition sibling groups and recenter the shared root.
  • The built-in toolbar and RGMiniView make it practical to navigate the wide composed layout without writing custom viewport code.
  • The floating helper window can be dragged, minimized, and reopened into a settings drawer.
  • The settings drawer changes wheel behavior and canvas drag behavior at runtime, then uses the graph instance’s image-generation lifecycle to export a screenshot.

Key Code Fragments

This initialization sequence shows that the example loads the raw tree, runs the custom composer, adds manual root connectors, and only then fits the viewport.

const initializeGraph = async () => {
    graphInstance.enableNodeXYAnimation();
    const myTreeJsonData = await getMyTreeJsonData();
    await myMixTreeLayout.current.loadData(myTreeJsonData);
    await graphInstance.sleep(200);
    myMixTreeLayout.current.applyAllGroupLayout();
    myMixTreeLayout.current.connectRootToChildrenTreeRoot();
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};

This preprocessing step assigns a stable group id to every flattened node and moves expand handles to the right, which is what makes per-group layout queries possible later.

const myGroupId = 'G-' + treeData.id;
const rootNodeId = treeData.id;
const { nodes, lines } = flatNodeData([treeData], null);
nodes.forEach(node => {
    if (!node.data) node.data = {};
    node.data.myGroupId = myGroupId;
    if (node.data.hasChildren) {
        node.expandHolderPosition = 'right';
    }
});

This placement rule is what turns several separate tree layouts into one sequential horizontal composition.

if (group.refs.length > 0) {
    const refedMyGroupId = group.refs[0];
    const refedGroup = this.getGroupById(refedMyGroupId);
    if (!group.layouted) {
        this.layoutGroup(refedGroup);
    }
    const refedGroupView = this.getGroupViewInfo(refedMyGroupId);
    groupRootNodeXy.x = refedGroupView.maxX + 50;
}

This local layout call shows that each subtree reuses the same relation-graph folder layouter instead of switching among several visible layout engines.

const myGroupLayout = this.graphInstance.createLayout(layoutOptions as RGLayoutOptions);
myGroupLayout.isMainLayouer = false;
myGroupLayout.layoutOptions.fixedRootNode = true;
myGroupLayout.placeNodes(groupNodes, groupRootNode);

This fragment shows that the top-level root connectors are added explicitly as presentation lines rather than coming straight from the original tree data.

const line: JsonLine = {
    id: 'root-to-' + group.rootNodeId,
    from: this.rootId,
    to: group.rootNodeId,
    lineShape: RGLineShape.SimpleOrthogonal,
    fromJunctionPoint: RGJunctionPoint.bottom,
    toJunctionPoint: RGJunctionPoint.top,
    lineWidth: 4,
    forDisplayOnly: true,
    color: 'rgba(0,0,0,0.2)',
    showEndArrow: false
};

This slot configuration makes the graph use custom text cards and adds a minimap overlay without changing the underlying node data.

<RGSlotOnNode>
    {({ node }) => {
        return (
            <div className="px-2 min-w-[140px] flex items-center justify-center w-full h-full text-sm text-slate-800 font-medium select-none">
                {node.text}
            </div>
        );
    }}
</RGSlotOnNode>
<RGSlotOnView>
    <RGMiniView />
</RGSlotOnView>

What Makes This Example Distinct

This example is unusual because it combines a fixed outer graph, per-group folder layouts, sequential placement from previous group bounds, a separately injected shared root, and manual display-only root connectors. That combination makes it a strong reference for grouped hierarchy composition, not just for rendering one tree.

Compared with organizational-chart, it uses a very similar grouped-tree architecture but keeps the node presentation simple, so the layout pattern is easier to study without role-card business styling. Compared with mix-layout-2, it keeps the full structure inside graph nodes and lines rather than anchoring groups to HTML dashboard rows or hidden canvas targets. Compared with layout-tree and use-dagre-layout-2, its value is neither preset orientation switching nor external layout-engine tuning; the distinctive lesson is how several local tree passes can coexist in one fixed graph and be recomputed together after expand-collapse changes.

Where Else This Pattern Applies

  • A product taxonomy viewer where each top-level catalog branch should keep its own local tree while still rolling up to one portfolio root.
  • A capability map or service map where first-level domains need separate spacing rules but must stay visually connected for executive review.
  • A grouped organization browser where divisions reflow together when departments are expanded or collapsed.
  • A standards, component, or knowledge classification browser that needs a read-only minimap and export workflow around a wide multi-branch hierarchy.