Root-Side Branch Visibility Toggles
This example renders a root-centered bidirectional tree where the root node exposes separate left and right toggle buttons. Each button hides or reveals one side of the hierarchy by filtering related nodes with `lot.level`, while shared demo utilities provide canvas settings and image export.
Root-Centered Tree with Independent Left and Right Branch Toggles
What This Example Builds
This example builds a bidirectional tree centered on one root node. Users see incoming branches on the left, outgoing branches on the right, and two pink action buttons placed outside the root so each side can be shown or hidden independently.
The main point is not generic expand or collapse behavior. It is a custom root control surface built with RGSlotOnNode, where one button targets the left side of the hierarchy and the other targets the right side.
How the Data Is Organized
The graph data is declared inline as one RGJsonData object with rootId: 'a', 13 nodes, and 12 lines. The component does not preprocess the dataset before layout. Instead, it adds the nodes and lines arrays to the graph instance, runs doLayout(), and then writes two runtime flags, leftExpanded and rightExpanded, onto the root node.
That structure maps well to real data where one focal entity needs two directional neighborhoods, such as upstream and downstream lineage, parent and child relationships, supplier and customer trees, or cause and effect branches around one incident.
How relation-graph Is Used
The example is wrapped in RGProvider, and RGHooks.useGraphInstance() drives initialization and later updates. The graph uses a tree layout with treeNodeGapH: 150, RGLineShape.StandardCurve, RGJunctionPoint.lr, a gray default line color, and path-following line labels. The built-in toolbar is kept enabled and positioned horizontally at the bottom-right corner.
The most important customization is RGSlotOnNode. It replaces the default node body only when node.lot.level === 0, which makes the root a special interaction target while non-root nodes stay simple text blocks. The example then relies on instance APIs such as addNodes, addLines, doLayout, getRootNode, getNodeRelatedNodes, updateNodeData, updateNode, moveToCenter, and zoomToFit.
The floating description panel and settings overlay come from the shared DraggableWindow helper rather than example-specific graph logic. That helper uses RGHooks.useGraphStore() and setOptions(...) to switch wheel and drag behavior, and it supports image export through prepareForImageGeneration() and restoreAfterImageGeneration(). The local SCSS file is effectively a placeholder, so the visible styling is mainly delivered through the custom node slot markup and shared window component.
Key Interactions
- Clicking the left root button toggles visibility for related nodes whose computed
lot.levelis negative. - Clicking the right root button toggles visibility for related nodes whose computed
lot.levelis positive. - The root buttons switch between plus and minus icons based on the root node’s stored
leftExpandedandrightExpandedflags. - Only the root node gets the extra controls; every other node remains read-only text.
- The floating helper window can be dragged, minimized, reopened, and switched into a settings overlay.
- The settings overlay changes wheel behavior, changes canvas drag behavior, and downloads the current graph as an image.
Key Code Fragments
This fragment shows that the example starts from static inline tree data rather than loading or rebuilding data on demand.
const treeJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'Root Node a', width: 120, height: 80 },
{ id: 'R-b', text: 'R-b' },
{ id: 'R-c', text: 'R-c' },
{ id: 'R-c-1', text: 'R-c-1' },
{ id: 'R-c-2', text: 'R-c-2' },
{ id: 'R-d', text: 'R-d' },
{ id: 'b', text: 'b' },
This fragment shows the initialization flow: load nodes and lines, run layout, add root metadata, then fit the viewport.
const initializeGraph = async () => {
graphInstance.addNodes(treeJsonData.nodes);
graphInstance.addLines(treeJsonData.lines);
await graphInstance.doLayout();
const rootNode = graphInstance.getRootNode();
graphInstance.updateNodeData(rootNode, {
leftExpanded: true,
rightExpanded: true,
});
graphInstance.moveToCenter();
graphInstance.zoomToFit();
};
This fragment proves that left-side visibility is not controlled by a built-in expand holder. It is computed from related nodes and the layout-side value in lot.level.
const toggleRootNodeLeft = async () => {
const rootNode = graphInstance.getRootNode();
if (rootNode) {
graphInstance.updateNodeData(rootNode, {
leftExpanded: !rootNode.data.leftExpanded
});
const relatedNodes = graphInstance.getNodeRelatedNodes(rootNode);
const leftNodes = relatedNodes.filter(n => n.lot.level < 0);
leftNodes.forEach(node => {
graphInstance.updateNode(node, {
hidden: !rootNode.data.leftExpanded
});
});
}
};
This fragment shows how the root node gets a custom interaction surface while all other nodes fall back to plain text rendering.
<RGSlotOnNode>
{({ node }) => {
return node.lot.level === 0 ? (
<div className="px-6 py-1 w-full h-full flex place-items-center justify-center text-xs">
<div className="px-3 py-0.5 bg-gray-100 bg-opacity-30 rounded text-black text-sm">
{node.text}
</div>
{/* left and right buttons omitted */}
</div>
) : (
<div className="px-6 py-1 w-full h-full flex place-items-center justify-center text-xs">
{node.text}
</div>
);
}}
</RGSlotOnNode>
This fragment shows that canvas settings and export are shared demo utilities, not the main example-specific graph behavior.
<SettingRow
label="Wheel Event:"
options={[
{ label: 'Scroll', value: 'scroll' },
{ label: 'Zoom', value: 'zoom' },
{ label: 'None', value: 'none' },
]}
value={wheelMode}
onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
What Makes This Example Distinct
According to the comparison data, this example is most distinct when read against other expand-related demos. Compared with expand-animation and expand-gradually, it does not focus on relation-graph’s built-in expand holders, initial collapse states, or relayout policy after expansion. Its main lesson is a custom root node UI that splits opposite sides of the same hierarchy into two independent actions.
Compared with open-all-close-all, the control scope is much narrower and more targeted. That example orchestrates recursive whole-graph expansion and collapse, while this one keeps the dataset static and applies immediate visibility changes only to the nodes related to the root.
Compared with other RGSlotOnNode examples such as element-connect-to-node, the custom slot is used less as a general visual composition technique and more as a root-only behavior surface. The combination that stands out is a centered bidirectional tree, root metadata flags, side-aware filtering through getNodeRelatedNodes(...) plus lot.level, and direct hidden updates on existing nodes.
Where Else This Pattern Applies
This pattern transfers well to viewers where one central entity needs separate controls for opposite relationship directions. Examples include upstream versus downstream data lineage, manager versus report branches in an org view, supplier versus customer dependencies, and cause versus consequence maps in incident analysis.
It is also useful when a product team wants custom controls that look and behave differently from relation-graph’s default expand affordances. The same approach can be extended to root-scoped filters, directional emphasis, or one-click branch summaries without changing the underlying graph dataset format.