Investment and Shareholder Relationship Explorer
This example renders a focal-company relationship explorer with in-graph controls for investment, shareholder, and historical-investment branches. It combines typed lazy loading through appended RGJsonData fragments, post-layout expand-holder normalization, custom node slots, and a shared floating panel for canvas settings and image export.
Investment and Shareholder Relationship Explorer with Lazy Branch Expansion
What This Example Builds
This example builds a read-focused company relationship explorer around one focal company. The canvas starts with a root company card and three in-graph branch controls for Investment, Share Holder, and Historical Investment, so the user can open different relationship directions without leaving the graph itself.
Visually, the result is a left-to-right business tree with white company cards, blue control nodes, a blue root card, orthogonal connectors, and a patterned analysis-style background. Users can expand company or control nodes to load more branches, click empty canvas space to clear checked state, drag or minimize the floating helper window, switch wheel and drag behavior in the settings overlay, and export the current graph as an image.
The main point of interest is not just lazy loading. It is the combination of typed control-node navigation, incremental graph growth, and post-layout cleanup that makes upstream and downstream relationship branches read differently inside one tree.
How the Data Is Organized
The initial graph is seeded inline inside initializeGraph() as one RGJsonData object. That seed declares rootId: 'root', one focal company node, and three branch-control nodes whose data.myType values identify the branch category. Their line directions are intentionally different: shareholder and historical-investment controls are connected on the parent side of the root, while the outbound investment control is connected on the child side.
Before the first setJsonData(...) call, the seed data already prepares expansion behavior by setting expanded: false and childrenLoaded: false on the control nodes. After that first load, the code immediately runs a second preprocessing step in updateNodeStyles(): it reads computed node.lot.level values, moves expand holders to the left for negative levels, hides the root expand holder, moves positive levels to the right, and then shifts the root node left by 100 pixels to balance the final composition.
Additional graph data comes from local async helpers in mock-data-api.ts. Each helper returns another RGJsonData fragment rather than a full replacement dataset. Investment, historical-investment, and shareholder loaders return company nodes. Expanding a normal company node returns two new control nodes, Investment and Share Holder, so exploration can continue recursively through the same typed branch pattern.
In a real system, the same structure could represent company ids, shareholder entities, outbound investments, historical investment records, and relationship categories returned from different backend endpoints. The mock loaders would be replaced with actual API calls while keeping the same graph contract.
How relation-graph Is Used
The example is wrapped in RGProvider, and MyGraph obtains the live instance through RGHooks.useGraphInstance(). The graph options configure a tree layout with from: 'left', 100px horizontal spacing, 10px vertical spacing, rectangular nodes, orthogonal links, rounded polyline corners, left-right junction points, and automatic relayout when nodes expand or collapse.
The graph instance API drives both initialization and runtime updates. loading(), setJsonData(...), clearLoading(), moveToCenter(), and zoomToFit() prepare the first view. During expansion, addNodes(...), addLines(...), enableCanvasAnimation(), setCanvasCenter(...), sleep(...), disableCanvasAnimation(), and doLayout() turn each async fragment into a staged append-focus-relayout flow. getNodes(), getRootNode(), and updateNode(...) are then used to normalize expand-holder placement after layout. clearChecked() handles canvas deselection.
The example does not use default node rendering. RGSlotOnNode mounts NodeSlot, which renders three visual roles from node data: blue text buttons for branch controls, a blue filled root card for the focal company, and white rounded cards for ordinary companies. This is what lets the graph mix action-like navigation nodes with read-only business entities inside one canvas.
The floating helper window is implemented through the local DraggableWindow subcomponent, not through example-specific graph code. Inside that shared component, RGHooks.useGraphStore() reads the current interaction modes, setOptions(...) updates wheel and drag behavior at runtime, and the export flow uses prepareForImageGeneration(), getOptions(), and restoreAfterImageGeneration() together with modern-screenshot.
Styling is finalized with SCSS overrides. The stylesheet applies the tiled canvas background, recolors expand buttons and checked states, and defines the card treatments for root, control, ordinary, and unused more-btn node variants.
Key Interactions
The primary interaction is lazy branch exploration. Expanding a control node or company node triggers onNodeExpand, which first checks childrenLoaded, selects the correct loader from node.data.myType, appends the returned nodes and lines, centers the canvas around the expanded node, waits briefly for rendering to settle, reruns layout, and reapplies post-layout node adjustments.
This example also makes branch categories part of the interaction model. Users do not open a separate panel to choose between outbound investments and shareholders. They click blue in-graph control nodes, and the next fragment is chosen from that node type.
Canvas clicks matter because they reset checked state, which helps when the user has been exploring several branches and wants a clean view again. Node clicks exist, but they only log to the console, so they are not a meaningful user-facing feature here.
The floating helper window adds a secondary interaction layer. Users can drag the window by its title bar, minimize it, open the settings overlay, switch wheel behavior between scroll, zoom, and none, switch canvas dragging between selection, move, and none, and download the current graph as an image.
Key Code Fragments
This fragment establishes the graph as a left-to-right orthogonal tree and enables automatic relayout when branches open.
const graphOptions: RGOptions = {
debug: false,
defaultExpandHolderPosition: 'bottom',
defaultNodeShape: RGNodeShape.rect,
defaultNodeBorderWidth: 0,
defaultLineShape: RGLineShape.StandardOrthogonal,
defaultPolyLineRadius: 5,
defaultJunctionPoint: RGJunctionPoint.lr,
defaultNodeColor: '#ffffff',
reLayoutWhenExpandedOrCollapsed: true,
layout: {
layoutName: 'tree',
from: 'left',
This fragment shows that the initial dataset is not just a root company. It also seeds three typed branch controls before the graph is first rendered.
const myJsonData: RGJsonData = {
rootId: 'root',
nodes: [
{ id: 'root', text: 'Tian Technology Co., Ltd.', data: { myType: 'root' } },
{ id: 'root-invs', text: 'Investment', disablePointEvent: true, expandHolderPosition: 'left', expanded: false, data: { myType: 'investment-button', childrenLoaded: false } },
{ id: 'root-sh', text: 'Share Holder', disablePointEvent: true, expandHolderPosition: 'left', expanded: false, data: { myType: 'shareholder-button', childrenLoaded: false } },
{ id: 'root-history-invs', text: 'Historical Investment', expandHolderPosition: 'left', expanded: false, data: { myType: 'historical-investment-button', childrenLoaded: false } },
],
lines: [
{ from: 'root', to: 'root-invs', showEndArrow: false },
{ from: 'root-sh', to: 'root', showEndArrow: false },
{ from: 'root-history-invs', to: 'root', showEndArrow: false },
]
};
This fragment proves that expansion is routed by node type rather than by one generic loader.
const componyId = node.id;
const myType = node.data.myType;
let newNodeAndLines: RGJsonData;
if (myType === 'investment-button') {
newNodeAndLines = await fetchMockData4GetCompanyInverstment(componyId);
} else if (myType === 'historical-investment-button'){
newNodeAndLines = await fetchMockData4GetCompanyHistoricalInvestment(componyId);
} else if (myType === 'shareholder-button') {
newNodeAndLines = await fetchMockData4GetCompanyShareHolder(componyId);
} else {
newNodeAndLines = await fetchMockData4GetCompanyNodeChildren(componyId);
}
This fragment shows that new branches are appended into the live graph and then staged through focus and relayout instead of rebuilding the whole dataset.
newNodeAndLines.nodes.forEach((n: JsonNode) => {
n.x = node.x;
n.y = node.y;
});
graphInstance.addNodes(newNodeAndLines.nodes);
graphInstance.addLines(newNodeAndLines.lines);
graphInstance.clearLoading();
graphInstance.enableCanvasAnimation();
graphInstance.setCanvasCenter(node.x + node.el_W / 2, node.y);
await graphInstance.sleep(400);
graphInstance.disableCanvasAnimation();
await graphInstance.doLayout();
This fragment shows the post-layout cleanup that makes negative, root, and positive levels use different expand-holder placement.
graphInstance.getNodes().forEach((node) => {
if (!node.lot) return;
if (node.lot.level < 0) {
if (node.expandHolderPosition) {
graphInstance.updateNode(node.id, {
expandHolderPosition: 'left'
});
}
} else if (node.lot.level === 0) {
graphInstance.updateNode(node.id, {
expandHolderPosition: 'hide'
});
} else {
if (node.expandHolderPosition) {
graphInstance.updateNode(node.id, {
expandHolderPosition: 'right'
});
}
}
});
This fragment shows how RGSlotOnNode turns typed nodes into different visual roles inside the same graph.
const myType = node.data.myType;
const buttonTypes = ['investment-button', 'shareholder-button', 'historical-investment-button'];
return (
<>
{buttonTypes.includes(myType) && (
<div className="my-node my-button-node">
{node.text}
</div>
)}
{myType === 'root' && (
<div className="my-node my-root">
{node.text}
</div>
)}
{!myType && (
<div className="my-node">
{node.text}
</div>
)}
</>
);
This fragment shows the shared settings panel updating runtime canvas behavior without changing the relationship data itself.
<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
The comparison data places this example closest to investment, show-more-nodes-by-page, show-more-nodes-front, industry-chain, and expand-gradually, but its emphasis is different from each of them. Compared with investment, this example is less about a fixed equity structure and more about a focal-company investigation flow that separates shareholder and historical-investment exploration from outbound investment. Compared with the show-more-* examples, it is not mainly about pagination or revealing preloaded density; it uses branch-type-driven async loaders and lets ordinary company nodes hand off to new control nodes so exploration can continue recursively.
Its strongest distinctive point is the typed control-node workflow. Blue in-graph nodes are the navigation surface, and node.data.myType decides whether expansion should load investment, shareholder, historical-investment, or normal-company children. That is a rarer pattern than a generic expand button because the branch category is visible and actionable directly in the graph.
Another uncommon trait is the split-direction cleanup after layout. The graph stays in one left-to-right tree layout, but post-layout logic hides the root expand holder, keeps negative-level controls on the left, and moves positive-level controls to the right. That gives one focal company different relationship directions without switching layouts or rebuilding the viewer shell.
The comparison file also supports a narrower claim: this example is a better starting point for due-diligence-style company exploration than neighbors that focus on static taxonomies, simple reveal flows, or paged overflow handling. Its distinctive combination is typed control-node navigation, incremental addNodes(...) and addLines(...) growth, focus-center animation before relayout, and business-style node rendering on a patterned analysis canvas.
Where Else This Pattern Applies
This pattern transfers well to company due-diligence screens, shareholder tracing tools, investment portfolio explorers, and internal compliance views that need upstream and downstream relationship categories around one focal entity.
The same approach also fits other graph domains where the next query depends on the role of the expanded node. Examples include supplier and distributor investigations, account ownership tracing, legal-entity relationship reviews, and dependency explorers that mix entity cards with in-graph action nodes.
If a product later needs real backend data, the local mock loaders can be replaced with API calls that return the same RGJsonData fragments. The branch routing, append APIs, slot rendering, post-layout normalization, and export/settings shell can stay the same.