JavaScript is required

Managed Tree Branch Growth with Reapply

This example shows how to keep a nested tree outside relation-graph, grow branches through inline plus-button placeholders, flatten the updated tree, and reapply it after each edit. It is a focused reference for guided branch creation on app-managed data, with automatic relayout, viewport fitting, and shared canvas settings and export helpers.

Grow a Managed Tree and Reapply It After Each Branch Edit

What This Example Builds

This example builds a left-to-right tree where regular nodes appear as purple pill labels and each branch ends with a circular plus button. Clicking one of those inline controls creates one to three new child nodes, reruns layout, and refits the viewport so the updated branch is immediately visible.

The main point is not generic node insertion by itself. The component keeps its own nested tree as the source of truth, mutates that tree directly, then replays the flattened result back into relation-graph after each edit. A floating helper window adds description text, canvas settings, and image export, but the central lesson is the managed-data reapply cycle.

How the Data Is Organized

The source data starts as an inline nested JsonNode tree named myTreeJsonData. Real content nodes such as a, b, b1, and c3 sit beside placeholder children whose data.myType is set to 'my-add-button'. Those placeholder nodes are not visual decorations only; they are part of the interaction model, because they mark the insertion points for new branches.

The component stores that nested structure in myTreeDataRef, so the application-owned tree remains authoritative instead of the live graph instance objects. Before each render pass, flattenTreeData() traverses the tree and converts it into flat nodes and lines arrays for relation-graph. During that step, each flattened node keeps its id, text, and data, and the helper also marks whether the source node is a leaf.

There is one extra preprocessing step before the flattened data is reapplied: every new node gets its initial x and y from newNodeInitialXy, which is updated from the clicked plus button. That means fresh branches initially emerge from the interaction point before doLayout() settles them into the left-to-right tree.

In a real product, the same pattern can represent an organization tree, a category taxonomy, a guided workflow builder, a decision tree, or any hierarchy where the domain model should stay outside the graph renderer.

How relation-graph Is Used

index.tsx wraps the demo in RGProvider, and MyGraph.tsx uses RGHooks.useGraphInstance() as the main integration point. The graph options configure a horizontal tree with layoutName: 'tree', from: 'left', and treeNodeGapH: 100. Node chrome is hidden with transparent fill and border settings, while defaultNodeShape: RGNodeShape.rect, defaultLineShape: RGLineShape.StandardOrthogonal, defaultJunctionPoint: RGJunctionPoint.lr, defaultPolyLineRadius: 10, and the light-purple line defaults define the final geometry and connector style. The built-in toolbar stays available in the bottom-right corner.

The instance API drives the whole update cycle. The component flattens its managed tree, applies the result with addNodes(...) and addLines(...), waits briefly, calls doLayout(), and then runs zoomToFit(). When a branch grows, it also uses generateNewNodeId() so newly inserted nodes do not collide with existing ids.

Custom rendering happens through RGSlotOnNode. The slot checks node.data.myType: placeholder nodes render as clickable plus buttons, while normal nodes render as purple rounded labels. There are no custom line, canvas, or viewport slots here. Styling adjustments live in my-relation-graph.scss, which adds a pale-purple checked halo for nodes and matching checked-state overrides for lines and labels.

The floating helper shell comes from the shared DraggableWindow component. That helper uses RGHooks.useGraphStore() to read the current drag and wheel modes, setOptions() to change them at runtime, and prepareForImageGeneration() plus restoreAfterImageGeneration() to export the graph as an image. Those controls are useful, but they are shared demo infrastructure rather than the distinctive technique of this example.

Key Interactions

  • Clicking a circular plus button grows the parent branch by one to three generated child nodes.
  • Each new child node receives its own trailing add-button placeholder, so the branch can keep growing through the same in-canvas affordance.
  • After every mutation, the component reapplies the flattened tree, reruns layout, and fits the viewport to the updated structure.
  • The helper window can be dragged, minimized, opened into a settings overlay, and used to switch wheel and drag behavior at runtime.
  • The same settings panel can export the current graph view as an image.

Key Code Fragments

This fragment shows that the source of truth is a nested JsonNode tree that mixes real nodes with placeholder add controls.

const myTreeJsonData: JsonNode = {
    'id': 'a', 'text': 'a', children: [
        {
            'id': 'b', 'text': 'b', children: [
                {
                    'id': 'b1', 'text': 'b1', children: [
                        { 'id': 'b1-add-button', 'text': 'add node', data: { myType: 'my-add-button' } },

This fragment proves that the component flattens the managed tree, seeds insertion coordinates, reapplies the full result, and then relayouts the graph.

const flatJsonData = flattenTreeData(myTreeDataRef.current);
flatJsonData.nodes.forEach(newNode => {
    newNode.x = newNodeInitialXy.current.x;
    newNode.y = newNodeInitialXy.current.y;
});
graphInstance.addNodes(flatJsonData.nodes);
graphInstance.addLines(flatJsonData.lines);
await graphInstance.sleep(350);
await graphInstance.doLayout();
graphInstance.zoomToFit();

This fragment shows how the helper converts the nested tree into the flat node and line arrays expected by relation-graph.

export function flattenTreeData(root: JsonNode) {
    const nodes: any[] = [];
    const lines: any[] = [];

    const traverse = (node: JsonNode, parentId: string | null) => {
        nodes.push({
            id: node.id,
            text: node.text,
            data: { isLeaf: !node.children || node.children.length === 0, ...(node.data || {}) }
        });

This fragment shows the constrained editing flow: locate the clicked placeholder in the managed tree, generate new ids, append new children, keep the add button last, and trigger a reapply.

const treeNodeInfo = findNodeInMyTreeData(buttonNode.id);
if (!treeNodeInfo) return;

const nodeInTree = treeNodeInfo.parentNode;
const randomNewNodesNum = Math.ceil(Math.random() * 3);
for (let i = 0; i < randomNewNodesNum; i++) {
    const randomId = graphInstance.generateNewNodeId();
    const newNodeId = 'N-' + randomId;
    nodeInTree.children.push({
        id: newNodeId,
        text: newNodeId,
        children: [{ id: newNodeId + '-add-button', text: 'add node', data: { myType: 'my-add-button' } }]
    });
}

This fragment shows that RGSlotOnNode is part of the editing workflow, not just a cosmetic skin.

<RGSlotOnNode>
    {({ node }) => {
        return node.data && node.data.myType === 'my-add-button'
            ? (
                <div
                    className="cursor-pointer h-8 w-8 text-purple-800 hover:bg-purple-700 hover:text-white rounded-full flex place-items-center justify-center"
                    onClick={() => addNodesForParent(node)}
                >
                    <CirclePlus size={20} />
                </div>

This fragment shows that the shared floating window can change canvas behavior and trigger image export through graph instance APIs.

const graphInstance = RGHooks.useGraphInstance();
const { options } = RGHooks.useGraphStore();
const dragMode = options.dragEventAction;
const wheelMode = options.wheelEventAction;

const canvasDom = await graphInstance.prepareForImageGeneration();
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

What Makes This Example Distinct

According to the comparison data, this example is distinct because it keeps an external nested tree as the authoritative model, mutates that tree, flattens it again, and reapplies the result after each structural change. That is different from examples such as expand-gradually and open-by-level, which primarily reveal branches that already exist in the loaded dataset rather than creating brand-new children.

Compared with create-object-from-menu, this example is much more constrained. It still adds nodes at runtime, but it does so through inline placeholder children rendered in RGSlotOnNode, not through a general-purpose right-click CRUD workflow with broader create, connect, and delete behavior. Compared with node-style4 and node-style2, the custom node rendering is functional first: the slot is used to drive branch growth, not only to reskin a static tree.

One especially unusual detail is that the component seeds every flattened node with the clicked add-button coordinates before relayout. Combined with generated ids, recursive tree lookup, the preserved trailing placeholder, and automatic fit-to-screen after each edit, that makes the demo a focused reference for guided branch growth on app-managed data rather than a static viewer or a full graph editor.

Where Else This Pattern Applies

This pattern fits products where users can extend a hierarchy, but only in controlled ways. Examples include organization planners that allow new team branches, bill-of-material editors that append components under approved parents, taxonomy managers that add categories in place, and guided decision-tree builders where each step must keep the domain model authoritative outside the canvas.

It also works well when the graph view is only one projection of a richer application state. If the same tree must also feed forms, validation rules, persistence logic, or audit history, this managed-tree approach gives the application a stable source of truth while still using relation-graph for layout, node rendering, viewport handling, and export.