JavaScript is required

Tree Layout Level and Node Spacing

This example demonstrates two runtime spacing strategies for the relation-graph tree layout on the same fixed hierarchy. Users can compare slider-driven `treeNodeGapH` and `treeNodeGapV` updates with a comma-separated `levelGaps` input, while each change reruns layout, recenters the graph, and fits the viewport.

Compare Tree Spacing with Sliders and Per-Level Gaps

What This Example Builds

This example builds a left-to-right tree-layout playground for spacing control. The canvas shows one fixed hierarchy with rectangular nodes and orthogonal connectors, while a floating utility window lets the user switch between two spacing strategies on the same graph.

In the first mode, the user changes horizontal level distance and vertical sibling distance with range inputs. In the second mode, the user enters a comma-separated levelGaps array to assign different distances to different depths. The most important point is not the sample data itself, but the side-by-side comparison between uniform spacing controls and per-level spacing controls on a live relation-graph instance.

How the Data Is Organized

The graph data is declared inline as one RGJsonData object with rootId, a flat nodes array, and a flat lines array. The hierarchy is therefore expressed explicitly through edge records rather than nested children objects.

There is no preprocessing before the initial setJsonData() call. The only later transformation happens in the second control mode, where the text input is split on commas, trimmed, and converted into numbers before being reused as layout.levelGaps.

In a real application, the same structure could represent reporting lines, dependency trees, category hierarchies, approval chains, or any directional tree where spacing at each depth needs to be tuned separately.

How relation-graph Is Used

The entry component wraps the demo in RGProvider, and MyGraph.tsx accesses the live graph through RGHooks.useGraphInstance(). The base options configure the built-in tree layout with from: 'left', treeNodeGapH: 400, and treeNodeGapV: 70, then keep the visuals intentionally plain with rectangular nodes, gray orthogonal lines, left-right junction points, right-side expand holders, and rounded polyline corners.

The instance API drives the actual lesson. On mount, the component calls setJsonData(), then moveToCenter() and zoomToFit(). When the active mode or spacing state changes, the component rewrites layout through setOptions({ layout }), runs doLayout(), and recenters plus fits the viewport again. In case 1 it updates treeNodeGapH and treeNodeGapV while clearing levelGaps. In case 2 it applies levelGaps and keeps treeNodeGapV for sibling spacing.

There are no custom node, line, canvas, or viewport slots, and there is no graph editing workflow. The only style override in the local SCSS is a black node-text color, which keeps the tree readable while leaving the overall appearance close to relation-graph defaults. The floating helper window comes from a shared DraggableWindow component, which also exposes a secondary canvas-settings panel for wheel mode, drag mode, and image export.

Key Interactions

The primary interaction is the mode switch. SimpleUISelect toggles between a slider-based panel and a text-input panel, and each tab change immediately triggers the corresponding relayout logic for the already-loaded tree.

Within the first mode, changing either range input reruns the tree layout immediately, so the user can compare wider or tighter level spacing against sibling spacing in real time. Within the second mode, editing the comma-separated value list updates levelGaps and reruns layout again, which makes uneven depth spacing visible without rebuilding the dataset.

The floating window itself can be dragged, minimized, and switched into a settings overlay. That shared overlay can change wheelEventAction, change dragEventAction, and export the current graph as an image by calling prepareForImageGeneration() and restoreAfterImageGeneration() around a screenshot capture step.

Key Code Fragments

This options block proves that the demo starts from a left-to-right tree with explicit spacing defaults and minimal styling.

const baseOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'tree',
        from: 'left',
        treeNodeGapH: 400,
        treeNodeGapV: 70,
    },
    defaultExpandHolderPosition: 'right',
    defaultNodeShape: RGNodeShape.rect,

This inline payload shows that the example uses explicit nodes and lines instead of nested tree data.

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'a' },
        { id: 'b', text: 'b' },
        { id: 'b1', text: 'b1' },
        { id: 'b1-1', text: 'b1-1' },
        // ...
    ],

This relayout flow is the first spacing strategy: update treeNodeGapH and treeNodeGapV, clear levelGaps, then rerun the current layout.

const layoutOptions: RGLayoutOptions = {
    layoutName: 'tree',
    from: 'left',
    treeNodeGapH: rangeHorizontal,
    treeNodeGapV: rangeVertical,
    levelGaps: []
};
graphInstance.setOptions({ layout: layoutOptions });
await graphInstance.doLayout();

This second panel shows how the example turns a text field into a per-level spacing array.

<input className="w-full border border-gray-900 p-1"
    value={levelGaps.join(',')}
    placeholder="Please enter content"
    onChange={(e) => {
        setLevelGaps(e.target.value.split(',').map(item => Number(item.trim())));
    }}
></input>

This shared settings code proves that the floating helper also controls canvas behavior and image export through graph-instance APIs.

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

Comparison data makes this example distinct because it packages two tree-spacing strategies into one compact demo on the same static hierarchy. Instead of only exposing one set of layout controls, it lets the user compare slider-driven treeNodeGapH and treeNodeGapV updates against parsed per-level levelGaps, with each change followed by setOptions({ layout }), doLayout(), recentering, and viewport fitting.

Compared with tree-data, this example is not about how tree data is loaded. Its stronger lesson is how an already-loaded tree can be re-spaced interactively without changing the dataset shape. Compared with ever-changing-tree and layout-folder2, it stays much narrower: it does not turn into a broad styling or direction-switching playground, and it keeps the visual treatment plain so spacing remains the main thing the user notices.

It is also close to center-layout-options, but it translates that two-mode spacing-comparison pattern into the directional tree layout. That makes the distinction between sibling spacing and per-level spacing more concrete, because the page exposes treeNodeGapH, treeNodeGapV, and levelGaps directly on the same left-to-right tree.

Where Else This Pattern Applies

  • Building an internal layout-tuning panel for organization trees, dependency trees, or approval graphs before adding domain-specific node templates.
  • Teaching teams when to use one global spacing rule versus depth-specific spacing rules in a tree-based product.
  • Creating technical demos that need immediate relayout from React state without rebuilding graph data on every control change.
  • Adding a lightweight graph workbench around a read-only hierarchy, where users adjust viewport behavior and export snapshots while testing layout parameters.