Bidirectional Tree Line Label Tuning
This example builds a root-centered bidirectional tree with orthogonal links and runtime switching between horizontal and vertical tree presets. Its main focus is branch-specific line-label tuning, using post-layout branch detection to recolor each side and apply different label geometry controls to parent-side and child-side links.
Bidirectional Tree Line Label Tuning
What This Example Builds
This example builds a root-centered bidirectional tree where one branch grows toward the root and the other grows away from it. The graph can switch between horizontal and vertical tree presets, while keeping the same dataset and the same two-sided structure.
Users see rectangular nodes connected by orthogonal lines, with parent-side branches colored yellow and child-side branches colored green. The most important behavior is that each side of the tree can tune its line-label geometry independently, so label placement stays readable even when the orientation changes.
How the Data Is Organized
The data is a static inline RGJsonData object with a single rootId, a flat nodes array, and a flat lines array. Some lines point into the root and some point away from it, which gives the layout enough directionality to produce a bidirectional tree around the center node.
There is no preprocessing before setJsonData. The meaningful transformation happens after layout: the component reads node.lot.level, treats negative levels as the reverse-side branch, and then applies side-specific colors and line-label settings at runtime.
In a real application, the same structure could represent upstream and downstream dependencies, managers and reports, ownership inflow and outflow, or any hierarchy that needs one focal node in the middle instead of a single top-level root.
How relation-graph Is Used
The example uses RGProvider to provide hook context and renders a RelationGraph surface with runtime configuration instead of relying on a large declarative options prop. RGHooks.useGraphInstance() is the main control point: the component calls setOptions(), setJsonData(), moveToCenter(), and zoomToFit() during initialization, then uses getNodes(), getLines(), updateNode(), and updateLine() for post-layout restyling.
The layout always uses layoutName: 'tree', but swaps between from: 'left' and from: 'top'. That orientation switch also changes spacing and the default junction point, using RGJunctionPoint.lr for horizontal mode and RGJunctionPoint.tb for vertical mode. The visual baseline stays stable across both presets: rectangular nodes, no node border, and RGLineShape.SimpleOrthogonal for the connectors.
The graph does not use custom node slots or custom line slots. Instead, it leans on built-in rendering and customizes the result in two ways: runtime line updates for geometry and SCSS overrides for label appearance. The .rg-line-label rule turns built-in labels into compact white chips whose border and text color inherit the current line color, which keeps the two branch groups visually aligned with their connectors.
The floating control window is a local subcomponent, not a graph feature by itself, but it matters to how the example is assembled. It hosts the orientation selector, two MyLinesOptions panels, and a shared canvas-settings overlay that can change wheel and drag behavior and export the current graph as an image.
Key Interactions
The orientation selector switches between horizontal and vertical tree presets. That change rebuilds the graph options, reloads the same JSON data, reapplies branch styling, and recenters the viewport.
The two MyLinesOptions panels update the current graph in place. Each side can change textAnchor, placeText, textOffsetX, textOffsetY, and polyLineStartDistance without rebuilding the dataset, so the demo behaves like a focused label-tuning playground.
The floating window itself can be dragged and minimized. Its settings overlay can switch wheel behavior between scroll and zoom, switch canvas drag behavior between selection and move, and export the graph canvas through the shared screenshot helper.
Node click and line click handlers only log objects to the console, so they do not materially change the example’s behavior.
Key Code Fragments
This fragment shows that orientation switching changes layout direction, spacing, junction defaults, and the graph’s baseline rendering options before loading the same JSON data.
if (activeTabName === 'h') {
layoutOptions = {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 150,
treeNodeGapV: 20
};
defaultJunctionPoint = RGJunctionPoint.lr;
} else {
layoutOptions = {
layoutName: 'tree',
from: 'top',
treeNodeGapH: 20,
treeNodeGapV: 150
};
defaultJunctionPoint = RGJunctionPoint.tb;
}
This fragment proves that branch grouping is derived from layout metadata, then used to recolor nodes and apply different label geometry to each side.
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);
for (const line of graphInstance.getLines()) {
if (leftNodeIds.includes(line.from) || leftNodeIds.includes(line.to)) {
graphInstance.updateLine(line, {
color: '#ca8a04',
textAnchor: parentsLineOptions.textAnchor ? parentsLineOptions.textAnchor : (activeTabName === 'v' ? 'center' : 'start'),
placeText: parentsLineOptions.placeText
});
}
}
This fragment shows that line-label controls are exposed as reusable UI state rather than hardcoded per-edge values.
<SimpleUISelect
data={[
{ value: '', text: 'Auto' },
{ value: 'start', text: 'start' },
{ value: 'middle', text: 'middle' },
{ value: 'end', text: 'end' }
]}
currentValue={lineOptions.textAnchor}
onChange={(newValue: string) => {
lineOptionsUpdater({
...lineOptions,
textAnchor: newValue
});
}}
/>
This fragment shows how the built-in line labels are restyled into bordered chips instead of plain SVG text.
.rg-line-peel {
.rg-line-label {
background-color: #fff;
color: var(--rg-line-color);
border: 1px solid var(--rg-line-color);
font-size: 10px;
}
}
What Makes This Example Distinct
Compared with the closely related bothway-tree example, this variant is much more focused on line-label geometry than on arrow direction or branch-shape variation. The comparison data consistently points to its strongest differentiator: two separate MyLinesOptions panels that restyle parent-side and child-side branches independently after layout.
Compared with layout-tree and io-tree-layout, this example is less about general relayout mechanics and more about keeping a two-sided tree readable when built-in labels must remain visible. The combination of a bidirectional tree, orthogonal connectors, runtime orientation switching, post-layout branch detection through node.lot.level, and branch-specific label controls is the reusable core.
It is also a narrower reference than the line example. Instead of cataloging many edge styles up front, it demonstrates how to keep standard relation-graph lines and mutate them in place after layout, which is useful when the branch grouping is only known after the layout engine has run.
Where Else This Pattern Applies
This pattern transfers well to upstream and downstream dependency viewers, supply-chain trees, ownership structures, or approval flows where one focal entity needs inbound and outbound branches in the same canvas.
It is also useful when line labels carry operational meaning, such as percentages, roles, stages, or relationship types, and those labels need different offsets or anchors on opposite sides of the same hierarchy.
The post-layout grouping technique is especially practical when upstream versus downstream status should be inferred from layout results instead of being duplicated as explicit flags in the input dataset.