JavaScript is required

IO Tree Layout Orientation and Link Routing

This example builds a static IO-tree viewer that can switch between horizontal and vertical presets at runtime. Its main lesson is a post-layout routing pass that compares computed node levels and rewrites each line's junction anchors, offsets, and geometry so inbound and outbound branches remain readable.

IO Tree Layout Orientation and Link Routing

What This Example Builds

This example builds a read-only IO tree viewer around one root node and a directed set of inbound and outbound branches. The same graph can switch between top-down and left-to-right io-tree presets while keeping compact rectangular nodes and orthogonal connectors.

Users can change the layout direction, toggle whether node size may change during layout, inspect the graph through an embedded mini view, and open the shared canvas settings overlay. The main teaching point is the post-layout routing pass that rewrites line junction anchors after relation-graph has already calculated node levels.

How the Data Is Organized

The data is declared inline as a static RGJsonData object with rootId: 'root', a flat nodes array, and a flat lines array. The line directions are significant: some branches point into the root-side path and others point away from it, which gives the io-tree layout enough structure to represent inbound and outbound relationships on the same canvas.

There is no preprocessing before setJsonData. The main transformation happens immediately after the initial load and after every relayout, when the component walks getLinks() and compares link.fromNode.lot?.level with link.toNode.lot?.level to decide how each line should attach to its endpoints.

In a real application, the same structure could represent service inputs and outputs, upstream and downstream dependencies, supply routing, approval inflow and outflow, or any directed relationship view where edge direction changes how branches should be read.

How relation-graph Is Used

The example is mounted inside RGProvider, then MyGraph uses RGHooks.useGraphInstance() as the main runtime control surface. The initial options only establish the visual baseline: rectangular nodes, no node border, and shared amber colors for nodes and lines. The real layout behavior is built at runtime through two RGOptions presets, both using layoutName: 'io-tree' but swapping from, defaultJunctionPoint, and the changeNodeSizeDuringLayout policy.

When the selector or checkbox changes, the component calls updateOptions() first, then normalizes already-mounted nodes and lines with getNodes(), updateNode(), getLines(), and updateLine(). After a short sleep(100), it runs doLayout(), then reprocesses every link through getLinks() so the code can use both endpoint nodes and the underlying line record in one pass. That second pass is where line text is cleared, connector radius and width are set, and junction points or offsets are reassigned from computed layout levels.

The canvas layer stays mostly built-in. There are no custom node or line templates; instead, the example uses RGSlotOnView to mount RGMiniView, and uses SCSS to override the built-in node text color to white. A local DraggableWindow subcomponent supplies the floating control shell, while its embedded CanvasSettingsPanel uses relation-graph hooks to switch wheel and drag behavior and export the canvas as an image.

Key Interactions

The layout selector switches between horizontal and vertical io-tree presets. That change does more than flip orientation: it swaps the default junction convention, reruns the layout, recenters the graph, and zooms it to fit the viewport.

The Change Node Size During Layout checkbox also triggers a full relayout. Its visible effect is subtle in this dataset, but it materially changes the routing logic because the code switches between offset-based anchors and horizontalLine or verticalLine junction targets when node sizes are allowed to vary.

The floating utility window can be dragged, minimized, and opened into a settings overlay. That overlay can change wheel mode, change canvas drag behavior, and download an image generated from prepareForImageGeneration() and restoreAfterImageGeneration().

Node and line click handlers exist, but they only log objects to the console, so they are not a functional part of the demo.

Key Code Fragments

This fragment shows that the horizontal preset is explicitly built around the built-in io-tree layout, compact spacing, orthogonal lines, and left-right junction defaults.

// inside graphOptionsH
layout: {
    layoutName: 'io-tree',
    treeNodeGapH: 10,
    treeNodeGapV: 10,
    from: 'left',
    changeNodeSizeDuringLayout
},
defaultNodeWidth: 120,
defaultNodeHeight: 30,
defaultLineShape: RGLineShape.StandardOrthogonal,
defaultJunctionPoint: RGJunctionPoint.lr

This fragment proves that relayout starts by updating options and normalizing existing node and line state before doLayout() runs again.

const targetOptions = layoutFrom === 'left' ? graphOptionsH : graphOptionsV;
graphInstance.updateOptions(targetOptions);
graphInstance.getNodes().forEach((node) => {
    graphInstance.updateNode(node, {
        width: targetOptions.defaultNodeWidth,
        height: targetOptions.defaultNodeHeight
    });
});
graphInstance.getLines().forEach((line) => {
    graphInstance.updateLine(line, {
        lineShape: targetOptions.defaultLineShape
    });
});

This fragment shows how the routing pass uses computed node levels plus the changeNodeSizeDuringLayout flag to choose different junction behavior for reverse-direction branches.

if (link.fromNode.lot?.level > link.toNode.lot?.level) {
    if (treeFrom === 'top') {
        lineProps.fromJunctionPoint = RGJunctionPoint.right;
        let toJunctionPoint = RGJunctionPoint.bottom;
        if (changeNodeSizeDuringLayout) {
            toJunctionPoint = 'horizontalLine';
        } else {
            lineProps.toJunctionPointOffsetX = -5;
        }
        lineProps.toJunctionPoint = toJunctionPoint;
    }
}

This fragment shows that the UI exposes both orientation switching and the node-size policy as live controls inside the floating helper window.

<SimpleUISelect
    data={[
        { value: 'left', text: 'Horizontal Tree' },
        { value: 'top', text: 'Vertical Tree' }
    ]}
    currentValue={layoutFrom}
    onChange={(newValue: string) => { setLayoutFrom(newValue); }}
/>
<SimpleUIBoolean currentValue={changeNodeSizeDuringLayout} onChange={setChangeNodeSizeDuringLayout} label="Change Node Size During Layout" />

This fragment shows the only local style override in the example: built-in node labels are forced to white so they remain legible on the amber cards.

.rg-node-peel {
    .rg-node {
        .rg-node-text {
            color: #ffffff;
        }
    }
}

What Makes This Example Distinct

The comparison data places layout-tree, bothway-tree2, bothway-tree, and layout-folder2 nearest to this demo, but io-tree-layout emphasizes a different lesson than any of them. Compared with layout-tree, it is less about generic tree preset swapping and more about the built-in io-tree layout plus connector attachment decisions that are recalculated after layout.

Compared with bothway-tree2, the focus shifts away from visible line labels and toward per-link junction rewriting. Compared with bothway-tree and layout-folder2, the main value is not branch coloring or business-card presentation, but keeping a static IO-style relationship graph readable by comparing computed start and end node levels and then adjusting each line’s anchors or offsets accordingly.

That combination is also rare inside the shared full-screen viewer shell used by several examples. The comparison and rarity data both point to the same distinguishing mix: runtime orientation switching for io-tree, node-size-sensitive relayout control, amber rectangular nodes, orthogonal elbow connectors, and a getLinks() pass that rewrites route geometry after the layout engine finishes.

Where Else This Pattern Applies

This pattern transfers well to system dependency maps, upstream and downstream data pipelines, network ingress and egress views, and approval or event flows where a single focal node has both inbound and outbound relationships.

It is also useful when teams want to keep relation-graph’s built-in tree layout but still need custom connector attachment rules after layout. The same approach can be extended to choose anchors from edge direction, branch type, status, or congestion rules without abandoning built-in rendering.