JavaScript is required

Lazy-Load Paginated Branch Nodes

This example shows how to lazy-load paginated investment and shareholder branches inside a left-to-right corporate relationship tree. The first page arrives on branch expand, later pages arrive through a synthetic `Load More(...)` node rendered inside the graph, and a floating helper window lets the user tune page size and access shared canvas settings and image export.

Lazy-Load Paginated Branches with an In-Graph Load More Node

What This Example Builds

This example builds a viewer-style corporate relationship tree around one focal company. The canvas shows a large blue root card, four typed branch nodes for investment and shareholder categories, and ordinary company nodes that appear only after the user expands one of those branches.

The main behavior is branch-scoped pagination inside the graph itself. The first page for a branch is fetched on first expand, later pages are requested through a synthetic Load More(...) node rendered as part of the diagram, and a floating helper window lets the user change page size while keeping the shared canvas settings and image export utilities available.

How the Data Is Organized

The initial graph is an inline RGJsonData seed with one root node and four branch nodes. Each branch stores data.myType to identify its data source and childrenLoaded to prevent the first page from being fetched more than once.

The paged data does not come from the initial graph JSON. mock-data-api.ts pre-generates four module-level company arrays and exposes fetchMockDataFromRemoteServer(...), which returns a delayed page slice plus the total count for one branch type. That keeps the example focused on incremental loading rather than on full-graph rebuilds.

Before each appended page is laid out, loadNextPageData(...) converts the fetched rows into a small RGJsonData fragment, tags each company node with ownerType, and attaches those nodes to the matching branch node with new lines. It also copies the current branch node’s x and y coordinates onto every appended node so the later relayout starts from the branch position rather than from unrelated coordinates.

In a real application, the same structure could represent paged investors, shareholders, subsidiaries, cases, tickets, or any other long sibling list where one branch should grow progressively instead of loading everything at once.

How relation-graph Is Used

RGProvider supplies the graph context, and RGHooks.useGraphInstance() drives both initialization and incremental updates. On mount, the example calls loading(), setJsonData(...), clearLoading(), moveToCenter(), and zoomToFit() to seed and frame the first view.

The graph uses a left-to-right tree layout with layoutName: 'tree', from: 'left', treeNodeGapH: 100, treeNodeGapV: 10, alignItemsX: 'start', and alignParentItemsX: 'end'. Combined with defaultLineShape: RGLineShape.StandardOrthogonal, defaultJunctionPoint: RGJunctionPoint.lr, and right-side expand holders, that produces a business-tree presentation where each branch grows horizontally from the root.

The loading flow combines relation-graph events and instance APIs. onNodeExpand checks childrenLoaded on the typed branch nodes, then loadNextPageData(...) uses getNodes() to find the correct branch node, addNodes(...) and addLines(...) to append the next page, sleep(400) to wait for measured node sizes, doLayout() to recompute positions, and removeNodeById(...) to replace the previous continuation node with the next one.

RGSlotOnNode is central to the presentation. It renders the root card, the four branch buttons, the blue Load More(...) continuation pill, and the ordinary white company cards as separate visual states even though they all live in the same graph node system. Local SCSS then overrides the canvas background, checked-state styling, expand button styling, and node skins.

The floating helper window and its settings panel come from shared subcomponents, not from this example alone. In this demo they provide the runtime page-size input, wheel and drag mode switches through setOptions(...), and image export through prepareForImageGeneration(), getOptions(), and restoreAfterImageGeneration().

Key Interactions

  • Expanding one typed branch for the first time fetches the first page of that branch’s company nodes.
  • Clicking the synthetic Load More(...) node appends the next page for the same branch without rebuilding the whole graph.
  • Changing the numeric page-size input affects later fetches because the current pageSize state is passed into the mock API call.
  • Clicking blank canvas space clears checked node and line state through clearChecked().
  • The floating helper window can be dragged, minimized, switched into a settings panel, and used to export the current canvas as an image.
  • Ordinary node clicks are logged, but they do not change graph state in this demo.

Key Code Fragments

This fragment shows that the graph starts with one company root and four typed branch nodes that are ready for expand-triggered loading.

const myJsonData: RGJsonData = {
    rootId: 'root',
    nodes: [
        { id: 'root', text: 'Tian Technology Co., Ltd.', expandHolderPosition: 'hide', data: { myType: 'root' } },
        { id: 'root-invs', text: 'Investment', disablePointEvent: true, expandHolderPosition: 'right', expanded: false, data: { myType: 'investment-button', childrenLoaded: false } },
        { id: 'root-history-invs', text: 'Historical Investment', expandHolderPosition: 'right', expanded: false, data: { myType: 'historical-investment-button', childrenLoaded: false } },
        { id: 'root-sh', text: 'Share Holder', disablePointEvent: true, expandHolderPosition: 'right', expanded: false, data: { myType: 'shareholder-button', childrenLoaded: false } },
        { id: 'root-history-shareholder', text: 'Historical Share Holder', expandHolderPosition: 'right', expanded: false, data: { myType: 'historical-shareholder-button', childrenLoaded: false } }
    ],

This fragment shows that the branch data source is a local mock API that returns a delayed page slice instead of all rows at once.

export const fetchMockDataFromRemoteServer = async (dataType: string, pageSize: number, fromIndex: number) => {
    return new Promise((resolve) => {
        setTimeout(function () {
            const allItems = mockRemoveDataBase[dataType];
            const total = allItems.length;
            const endIndex = Math.min((fromIndex + pageSize - 1), total);
            const currentPageItems = allItems.slice(fromIndex, endIndex);
            resolve({ currentPageItems, total });

This fragment shows that first expand only loads once for each typed branch, and the first request starts from index 0.

const onNodeExpand = async (node: RGNode, e: RGUserEvent) => {
    if (node.data.childrenLoaded) {
        return;
    }
    node.data.childrenLoaded = true;
    const myType = node.data.myType;
    const fromIndex = 0;
    await loadNextPageData(myType, fromIndex);
};

This fragment shows how one fetched page becomes an incremental graph append instead of a full data replacement.

for (const entItem of currentPageItems) {
    currentPageGraphJsonData.nodes.push({ id: entItem.companyId, text: entItem.companyName, data: { ownerType: myType } });
    currentPageGraphJsonData.lines.push({ from: typeNodeId, to: entItem.companyId });
}
updateLoadMoreButtonNode(typeNodeId, myType, fromIndex + currentPageItems.length, total, currentPageGraphJsonData);
currentPageGraphJsonData.nodes.forEach((n: JsonNode) => {
    n.x = typeNode.x;
    n.y = typeNode.y;
});
graphInstance.addNodes(currentPageGraphJsonData.nodes);
graphInstance.addLines(currentPageGraphJsonData.lines);

This fragment shows that the continuation control is itself a synthetic graph node that stores the next branch type and offset.

const loadNextPageButtonNodeId = `${myType}-next-button`;
graphInstance.removeNodeById(loadNextPageButtonNodeId);
const remainingItemCount = total - fromIndex + 1;
if (remainingItemCount > 0) {
    currentPageGraphJsonData.nodes.push({ id: loadNextPageButtonNodeId, text: `Load More(${remainingItemCount})`, data: { myType: 'more-btn', loadType: myType, fromIndex: (fromIndex + 1) } });
    currentPageGraphJsonData.lines.push({ from: typeNodeId, to: loadNextPageButtonNodeId });
}

This fragment shows that RGSlotOnNode renders the continuation node as a clickable in-graph action instead of plain node text.

{myType === 'more-btn' && (
  <div className="my-node more-btn px-2" onClick={() => {loadNextPage(node)}}>
    {node.text}
  </div>
)}
{!myType && (
  <div className="my-node">
    {node.text}
  </div>
)}

This fragment shows that the shared settings panel can export the graph canvas after relation-graph prepares its rendering state.

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

The comparison data positions this example as the branch-pagination variant of the company-tree demos rather than as a generic lazy-expansion example. Compared with show-more-nodes-front, it does not preload branch members and reveal hidden overflow. It fetches an initial page on expand and then keeps extending the same branch through repeated Load More(...) clicks rendered inside the graph.

Compared with investment-penetration, scene-org, and investment, the emphasis is also narrower and more repeatable. Those neighbors use append-based growth for business trees, but this example focuses on one fixed set of root branches that can keep loading additional pages under the same branch role, with runtime page-size control exposed in the floating helper window.

Compared with expand-gradually, the main lesson is not collapse and reveal of descendants that were already present in the initial data. The distinctive pattern here is graph-native continuation control: a typed branch button starts the first fetch, a synthetic continuation node preserves branch context, and incremental addNodes(...) plus addLines(...) plus doLayout() keeps the live graph growing in place.

The rarity data reinforces that combination. This example brings together typed root branch buttons, first-expand loading, a slot-rendered continuation pill, runtime page-size tuning, incremental append APIs, and a patterned blue-and-white business canvas. That makes it a strong starting point when dense sibling lists should arrive progressively without replacing the whole graph.

Where Else This Pattern Applies

This pattern applies to corporate ownership viewers, supplier or distributor maps, case investigation trees, and service dependency explorers where one category may contain many siblings and should load incrementally.

It also fits graph interfaces that need the control surface to stay inside the diagram instead of moving into a side panel or external pager. A branch-local continuation node is useful when the loading action should stay attached to one branch and remain visible near the affected nodes.

Another extension is analyst tooling where operators need a compact first view, adjustable batch size, and image export from the same screen. The current demo does not implement domain-specific review actions, but its branch-scoped paging pattern is directly reusable for those scenarios.