Google Income Statement Ribbon Graph Tutorial
This example is a six-stage tutorial that turns a nested Google income statement into a left-to-right relation-graph infographic. It starts with a plain tree, then adds custom node slots, amount-scaled node heights, ribbon-style line slots, and final brand annotations while keeping the graph interactive.
Building a Step-by-Step Google Income Statement Ribbon Graph
What This Example Builds
This example builds a guided viewer for a single income-statement dataset rather than a single fixed graph. A floating helper window lets the user switch through six stages: a plain tree, a node-slot version, an amount-scaled node version, a filled-ribbon line version, an offset-aware ribbon version, and a final branded infographic.
The canvas shows revenue branches feeding into a synthetic root and expense or profit branches flowing away from it, so the finished scene reads like a financial breakdown instead of a generic hierarchy. The main value is not only the final appearance, but the fact that the intermediate steps remain visible inside one example.
How the Data Is Organized
The source data in my-data.ts is a nested object with two top-level branches: revenues and expenses. getMyJsonData() creates one synthetic root node, then findNodesFromOrignData() recursively flattens nested details arrays into nodes and lines for RGJsonData.
The preprocessing also stores rendering metadata on each line. Every generated edge carries fromOffsetYPercent, fromHeightPercent, toOffsetYPercent, and toHeightPercent inside line.data, which later controls ribbon height and attachment position in the custom line renderer.
The direction of the generated links is deliberate. Revenue children connect toward the root, while the expense and profit side connects away from the root. In a real project, the same pattern can represent revenue composition, cost breakdowns, margin bridges, budget allocation trees, or any nested metric structure where edge thickness should encode contribution.
How relation-graph Is Used
Each stage is mounted inside its own RGProvider, and every step component uses RGHooks.useGraphInstance() plus an initialization routine in useEffect. The base RelationGraph options stay stable across versions: tree layout, left-to-right direction, treeNodeGapH: 300, treeNodeGapV: 100, rectangular nodes, standard curved lines, 3px line width, and left-right junction points.
The example then adds customization layer by layer. Step 2 introduces RGSlotOnNode to replace the default node body with custom HTML content. Step 3 keeps the slot approach but makes node height proportional to node.data.amount and adds business metrics above the anchor box. Step 4 adds RGSlotOnLine and replaces thin strokes with a filled SVG area created by createAreaLinePath(). Step 5 upgrades that to createAreaLinePathWithOffset(), which reads the percentages generated during preprocessing so each ribbon occupies only its proportional vertical slice.
The later custom-line stages recover live geometry with graphInstance.getLinkByLine(lineConfig.line) and manually forward clicks back into relation-graph through graphInstance.onLineClick(...), so the custom SVG path still participates in graph interaction. The final step keeps both slots, adds product-specific SVG components for selected nodes, binds onLineClick and onCanvasClick, and performs an extra zoom(-10) after zoomToFit() to open up the infographic composition.
Presentation details are handled with local styling rather than layout changes. Step4Version.scss removes the visible node border, adds the multiradial background, defines ribbon opacity and hover behavior, and adds the halo styling used for checked nodes and branded callout blocks.
Key Interactions
- The floating helper window switches
stepNamebetween six complete graph implementations and updates the explanation text at the same time. - The helper window can be dragged by its title bar and minimized, which lets the user inspect the graph without losing the step controls.
- In Step 4 and later, the visible line body is a filled SVG ribbon with
pointer-events: fill, so custom-rendered edges stay clickable after the default stroke is visually replaced. - In the final stage, ribbon clicks are still forwarded into relation-graph, and a canvas click clears the checked state through
graphInstance.clearChecked().
Key Code Fragments
This fragment shows that the wrapper swaps complete graph variants, which is why the example works as a tutorial instead of a single finished view.
{stepName === 'v1' && <RGProvider><Step1Version /></RGProvider>}
{stepName === 'v2' && <RGProvider><Step2Version /></RGProvider>}
{stepName === 'v3' && <RGProvider><Step3Version /></RGProvider>}
{stepName === 'v4' && <RGProvider><Step4Version /></RGProvider>}
{stepName === 'v5' && <RGProvider><Step5Version /></RGProvider>}
{stepName === 'final' && <RGProvider><StepFinalVersion /></RGProvider>}
This fragment shows how the flattened graph keeps percentage metadata on each line for later ribbon geometry.
allJsonLines.push({
from: parentJsonNode.id,
to: cJsonNode.id,
color: cJsonNode.color,
data: {
fromOffsetYPercent: sumAmount / parentJsonNode.data.amount,
fromHeightPercent: cJsonNode.data.amount / parentJsonNode.data.amount,
toOffsetYPercent: 0,
toHeightPercent: 1,
}
});
This fragment shows that node height is driven directly by the business amount, not by a fixed visual preset.
<div style={{width: '40px', height: `${(node.data?.amount / 100) * 200}px`, position: 'relative'}}>
<div style={{
position: 'absolute',
top: '0px',
left: '0px',
color: node.color,
width: '200px',
}}>
This fragment shows how the custom line slot uses live link geometry and then forwards clicks back to relation-graph.
const link = graphInstance.getLinkByLine(lineConfig.line)!;
const path = createAreaLinePathWithOffset(
link.fromNode,
link.toNode,
lineConfig.line,
1
);
const onClick = (e) => {
graphInstance.onLineClick(lineConfig.line, e);
};
This fragment shows that the final stage adds brand-specific callouts inside the node slot instead of changing the underlying graph data shape.
{node.text === 'Google Cloud' && (
<div className="c-node-icon-desc">
<IconForGoogleCloud/>
<div style={{
fontSize: '20px',
color: '#666666',
width: '250px',
}}>{node.data.desc}</div>
</div>
)}
What Makes This Example Distinct
According to the prepared comparison data, the closest neighbor is demo-for-google-income-statement-final, but the intent is different. The final-only neighbor is a concise reference for the finished infographic, while this example preserves the whole build-up path from plain tree rendering to branded ribbon graph.
The rare part is the combination, not any single API call by itself. This example combines recursive preprocessing around a synthetic root, opposite branch directions for revenue and expense flows, amount-scaled node slots, clickable ribbon-style line slots, and a draggable tutorial shell that switches among six implementations. That makes it a strong starting point when the goal is to teach or study progressive graph customization.
The comparison data also separates it from other custom-line viewers. Against adv-line-slot-2, the custom line logic here is used to express financial composition rather than route status or movement. Against examples like node-line-tips-contentmenu and customize-fullscreen-action, the surrounding window is mainly an orchestration layer for staged visual transformation, not a tooltip/menu showcase or a fullscreen wrapper lesson.
Where Else This Pattern Applies
This pattern transfers well to investor-relations visuals, CFO dashboards, and internal business reviews where one parent metric needs to be decomposed into branded subcategories with visible proportional weight.
It also fits budget allocation views, gross-to-net bridges, cloud-cost breakdowns, and product-portfolio composition charts where a tree layout is still useful, but thin edges are too weak to communicate contribution.
The tutorial-shell approach is also reusable outside finance. Teams can use the same multi-stage wrapper to demonstrate how a plain relation-graph example evolves into a more customized deliverable without forcing readers to compare several separate demos by hand.