Bidirectional Tree Layout
This example builds a root-centered bidirectional tree and lets the user switch the same dataset between horizontal and vertical tree presets. After layout, it uses `node.lot.level` to recolor the incoming branch and optionally reverse that branch's arrow direction without reloading the data.
Root-Centered Bidirectional Tree with Reversible Parent Arrows
What This Example Builds
This example builds a two-sided tree centered on one root node. One branch flows into the root, another branch expands away from it, and the whole graph can be switched between a horizontal tree and a vertical tree without changing the dataset itself.
What users see is a compact hierarchy viewer with rectangular nodes, green child-side branches, and a differently styled incoming branch. The most important detail is that the incoming side is not just recolored after layout: its lines can also switch to reversed arrowheads, which makes directionality explicit when the graph is used for upstream versus downstream relationships.
The demo also includes a floating utility window. It holds the orientation selector, the parent-arrow selector, and a shared settings panel for canvas drag mode, wheel behavior, and image export.
How the Data Is Organized
The graph data is defined inline as one RGJsonData object with a single rootId, a flat nodes array, and a flat lines array. The structure does not precompute explicit “left side” or “right side” styling metadata. Instead, it relies on relation-graph’s tree layout result to determine which branch ended up on the negative side of the root.
The incoming branch is represented by edges that point into the root, such as R-b -> a, while the outgoing branch uses normal root-to-child edges such as a -> b. That same pattern can represent parent and child relations, upstream and downstream dependencies, lineage around a focal asset, or any split hierarchy where one side should be read as “toward the center” and the other as “away from the center.”
Before setJsonData() runs, the only preprocessing is orientation-specific option assembly. The code chooses a tree preset, node gaps, junction points, and default line shapes based on the current selector value. The branch-specific coloring and arrow logic happens later, after layout metadata is available on each node.
How relation-graph Is Used
The example is wrapped in RGProvider, then MyGraph retrieves the active graph instance with RGHooks.useGraphInstance(). The rendered RelationGraph starts with an empty options prop, and the real configuration is applied at runtime through graphInstance.setOptions() followed by graphInstance.setJsonData().
For layout, the demo stays on the built-in tree layout and swaps between two presets. Horizontal mode uses from: 'left', treeNodeGapH: 150, treeNodeGapV: 20, RGJunctionPoint.lr, and RGLineShape.Curve2. Vertical mode uses from: 'top', treeNodeGapH: 20, treeNodeGapV: 150, RGJunctionPoint.tb, and RGLineShape.StandardCurve. Both modes keep rectangular nodes with a fixed 130 x 40 default size and zero border width.
The main runtime technique is post-layout restyling. After the dataset is loaded, the code reads graphInstance.getNodes() and checks node.lot.level. Nodes with negative levels are treated as the incoming branch. Those nodes are recolored, their IDs are collected, and then graphInstance.getLines() plus graphInstance.updateLine() are used to restyle all connected lines. When the reverse-arrow option is enabled, those lines switch to RGLineShape.StandardOrthogonal, red color, showStartArrow: true, and showEndArrow: false. Otherwise the same branch keeps orthogonal routing with its default arrow direction and amber color.
No node, line, canvas, or viewport slots are customized here. The example relies on the default graph renderers and uses SCSS overrides instead: node text is forced to white, and checked lines receive a stronger orange stroke and label treatment.
The shared DraggableWindow helper adds secondary graph-instance usage. Its CanvasSettingsPanel reads graph store options through RGHooks.useGraphStore(), changes wheelEventAction and dragEventAction with graphInstance.setOptions(), and exports the current canvas through prepareForImageGeneration() and restoreAfterImageGeneration().
This is a viewer-style example rather than an editor. Node and line click handlers are wired into RelationGraph, but they only log the clicked objects and do not modify the graph.
Key Interactions
The primary interaction is orientation switching. Choosing Horizontal Tree or Vertical Tree rebuilds the graph with a different tree preset, different junction defaults, and different baseline line shapes, then recenters and refits the result.
The second core interaction is the parent-arrow toggle. It does not reload data or recompute the graph structure. Instead, it reruns the post-layout styling pass and flips arrowheads only on the incoming branch connected through negative node.lot.level nodes.
The floating utility window adds two supporting interactions. Users can switch wheel behavior between scroll, zoom, and none, and they can switch canvas dragging between selection, move, and none. The same panel can export the current graph view as an image by asking relation-graph for an export-ready canvas DOM first.
The node and line click handlers are intentionally minor in this demo. They are present for inspection and debugging, but they are not the entry point for any visible feature.
Key Code Fragments
This fragment shows that the data is a single inline tree with one focal root and an incoming branch that points into that root.
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'Root Node a' },
{ id: 'R-b', text: 'R-b' }, { id: 'R-b-1', text: 'R-b-1' }, { id: 'R-b-2', text: 'R-b-2' }, { id: 'R-b-3', text: 'R-b-3' },
{ id: 'R-c', text: 'R-c' }, { id: 'R-c-1', text: 'R-c-1' }, { id: 'R-c-2', text: 'R-c-2' },
This fragment shows the horizontal preset: the demo stays on layoutName: 'tree' but swaps direction, spacing, junction points, and default line shape as one preset.
if (activeTabName === 'h') {
layoutOptions = {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 150,
treeNodeGapV: 20
};
defaultJunctionPoint = RGJunctionPoint.lr;
defaultLineShape = RGLineShape.Curve2;
}
This fragment shows that orientation changes rebuild the configured graph, then center and fit it in the viewport.
const graphOptions: RGOptions = {
debug: false,
layout: layoutOptions,
defaultNodeShape: RGNodeShape.rect,
defaultNodeWidth: 130,
defaultNodeHeight: 40,
defaultLineShape,
defaultJunctionPoint: defaultJunctionPoint,
defaultNodeBorderWidth: 0
};
graphInstance.setOptions(graphOptions);
await graphInstance.setJsonData(myJsonData);
This fragment shows the post-layout classification step that derives branch identity from node.lot.level instead of from extra input flags.
for (const node of graphInstance.getNodes()) {
if (node.lot && node.lot.level !== undefined && node.lot.level < 0) {
graphInstance.updateNode(node, { color: '#ca8a04' });
leftNodes.push(node);
} else {
graphInstance.updateNode(node, { color: '#3f9802' });
}
}
const leftNodeIds: string[] = leftNodes.map(n => n.id);
This fragment shows the branch-specific arrow reversal. The toggle changes existing line styles without reloading the dataset.
if (leftNodeIds.includes(line.from) || leftNodeIds.includes(line.to)) {
if (reverseParentArrows) {
graphInstance.updateLine(line, {
lineShape: RGLineShape.StandardOrthogonal,
showStartArrow: true,
showEndArrow: false,
color: '#ff0000'
});
}
This fragment shows how the shared utility panel uses relation-graph’s export lifecycle before creating the image blob.
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
What Makes This Example Distinct
According to the comparison data, this example is not just another tree-orientation switcher. Its distinctive combination is a root-centered bidirectional tree, post-layout branch classification through node.lot.level, polarity-based recoloring, and optional arrow reversal on the incoming side, all inside a minimal viewer shell.
Compared with bothway-tree2, this version spends its control budget on parent-side arrow semantics and orthogonal branch restyling rather than on branch-specific line-label placement. Compared with layout-tree, the focus is not generic one-direction tree relayout, but a centered two-sided hierarchy where one branch keeps its own color and arrow policy. Compared with io-tree-layout, it stays on the built-in tree layout rather than switching to io-tree routing and junction-anchor rewriting.
The rarity metadata also supports a narrower claim: the orientation selector, parent-arrow selector, orientation-specific junction defaults, and runtime restyling of existing lines are all rare features in the example set. That makes this demo a strong starting point when the problem is not “draw any tree,” but “draw one focal node with a meaningfully different branch on the reverse side.”
Where Else This Pattern Applies
This pattern transfers well to upstream and downstream dependency inspection, such as package dependency viewers, data lineage around one table, or service maps where callers and callees should sit on opposite sides of a focal service.
It also fits product structures that need parent-side versus child-side emphasis, including bill-of-material trees, inheritance viewers, category ancestry explorers, and organizational contexts where supervisors and reports should be visually separated without moving to two different diagrams.
The post-layout classification technique is reusable beyond trees. Any graph where layout metadata can identify a branch, depth range, or side of the focal node can use the same getNodes() and updateLine() pattern to apply branch-specific arrows, colors, or routing rules after the layout engine has already done the positioning work.