Custom Line Slot Rendering
This example fully replaces relation-graph's default edge renderer with a custom line slot that adds a moving dot, endpoint labels, and a separate Detail action. It keeps the standard line click pipeline intact, uses relation-graph's generated path and text helpers, and includes shared viewer utilities for interaction settings and image export.
Custom Line Slot Rendering with Endpoint Labels and Detail Actions
What This Example Builds
This example builds a left-to-right tree graph and replaces the default line renderer with a custom slot component. The visible result is a set of curved blue lines with a moving white dot, a label for each connection, and extra Start Text and End Text markers anchored directly to the computed path. The nodes themselves are kept visually light, with transparent boxes and a selection glow instead of heavy chrome.
Users can click lines, trigger a line-specific Detail action, open the floating settings window, change canvas interaction modes, and export the current graph as an image. The main point of the demo is that the entire line presentation is custom, but relation-graph still supplies the path geometry, label positioning, and normal line-click behavior.
How the Data Is Organized
The graph data is assembled inline as RGJsonData inside initializeGraph. It declares a single root node named base, several first-level incoming and outgoing links, and a few second-level child links under nodes 1 and 4. Each connection is still represented in the standard relation-graph form: from, to, and text.
There is only one preprocessing step before setJsonData: every line has showEndArrow set to false. That keeps the custom pipe-like styling and endpoint labels from competing with default arrowheads. After loading, the graph is centered and fitted to the viewport, but no additional data transformation is applied.
In a real application, the same structure could represent network routes, system dependencies, equipment connections, warehouse paths, or organization-level handoff relationships. The example keeps the payload simple so the documentation focus stays on line rendering rather than on domain-specific metadata.
How relation-graph Is Used
RGProvider wraps the demo so relation-graph hooks can resolve the active graph instance. Inside MyGraph, RelationGraph is configured with a tree layout that grows from the left, levelGaps of [500, 400], RGJunctionPoint.lr, and RGLineShape.StandardCurve. Those settings make the paths predictable enough to decorate consistently.
The main extension point is RGSlotOnLine. Instead of accepting relation-graph’s built-in edge output, the demo passes each line’s slot props into MyLineContent, along with a parent callback for the separate detail action. Inside that slot component, RGHooks.useGraphInstance() is used to call generateLinePath and generateLineTextStyle, so the custom renderer still uses relation-graph’s own geometry and text-position logic.
MyLineContent then keeps relation-graph primitives in the loop. RGLinePath renders the actual SVG edge and handles forwarded line clicks. The slot injects a moving <circle> inside the path renderer and adds start and end text groups that follow the same computed path through CSS offset-path. When the line is using the div-based text branch, RGLineText renders the HTML label container and appends a Detail link after the computed line text.
Outside the slot, the graph instance API is used for setup and viewer utilities. setJsonData, moveToCenter, and zoomToFit initialize the graph on mount. The floating DraggableWindow contains a settings panel that reads the current graph options through RGHooks.useGraphStore(), updates wheel and drag behavior with setOptions, and exports the canvas with prepareForImageGeneration and restoreAfterImageGeneration. The screenshot helper uses modern-screenshot to turn the prepared graph DOM into a downloadable image.
The visual customization is finalized in my-relation-graph.scss. That stylesheet removes the default node box styling, adds a checked-node glow, thickens the line background and foreground strokes, colors the label text, and animates the dot and endpoint labels along the SVG path by reading the custom --my-line-path variable from the slot.
Key Interactions
- Clicking the custom SVG line still enters relation-graph’s normal
onLineClickpipeline because the slot forwards the event throughgraphInstance.onLineClick(...). - The div-based label branch is also clickable and forwards the same line event, so replacing the line renderer does not remove standard line interaction.
- The appended
Detailaction calls back intoMyGraphand opens an alert for the current line text. - The floating
DraggableWindowcan be dragged, minimized, and switched into a settings panel. - The settings panel changes wheel behavior, changes drag behavior, and downloads an image of the current graph canvas.
Key Code Fragments
This block shows the layout and default line settings that the custom renderer is built on.
const graphOptions: RGOptions = {
defaultJunctionPoint: RGJunctionPoint.lr,
defaultLineShape: RGLineShape.StandardCurve,
layout: {
layoutName: 'tree',
from: 'left',
levelGaps: [500, 400]
}
};
This block shows the inline dataset and the preprocessing step that disables default end arrows before loading.
const myJsonData: RGJsonData = {
rootId: 'base',
nodes: [
{ id: 'base', text: '🏢 Base', },
// ...
],
lines: [
{ from: 'base', to: '1', text: 'Line X01' },
// ...
]
};
myJsonData.lines.forEach((line) => {
line.showEndArrow = false;
});
This block shows the graph instance loading the data and immediately framing it for presentation.
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
This block shows how the example replaces the default line renderer with a custom slot component.
<RelationGraph
options={graphOptions}
onLineClick={onLineClick}
>
<RGSlotOnLine>
{(lineSlotProps: RGLineSlotProps) => {
return (
<MyLineContent
{...lineSlotProps}
onMyLineDetailClick={onMyLineDetailClick}
/>
);
}}
</RGSlotOnLine>
</RelationGraph>
This block shows the custom slot deriving path geometry, deciding how to render text, and forwarding clicks back into relation-graph.
const linePathInfo = useMemo<RGLinePathInfo>(() => graphInstance.generateLinePath(lineConfig), [lineConfig]);
const onLineClick = (e: React.MouseEvent | React.TouchEvent) => {
graphInstance.onLineClick(lineConfig.line, e.nativeEvent);
};
const useTextOnPath = (lineConfig.line.useTextOnPath || defaultLineTextOnPath);
const useSvgTextPath = useTextOnPath && (lineConfig.line.lineShape !== RGLineShape.StandardStraight);
const useDivLineText = !useSvgTextPath && lineConfig.line.text;
const textStyle = graphInstance.generateLineTextStyle(lineConfig, linePathInfo);
This block shows the slot keeping RGLinePath but layering extra path-bound visuals on top of it.
<RGLinePath
lineConfig={lineConfig}
linePathInfo={linePathInfo}
useTextOnPath={useSvgTextPath}
checked={checked}
graphInstanceId={graphInstanceId}
onLineClick={onLineClick}
>
<circle className="my-dot" r="5"></circle>
<g className="my-text-start" style={{ transform: 'translate(0px, -3px)' }}>
<text fontSize={10} textAnchor={'start'}>Start Text</text>
</g>
<g className="my-text-end" style={{ transform: 'translate(0px, -3px)' }}>
<text fontSize={10} textAnchor={'end'}>End Text</text>
</g>
</RGLinePath>
This block shows the line label branch adding a separate detail affordance after the computed text.
<div
className={`rg-line-label ${useTextOnPath ? 'rg-line-label-on-path' : ''}`}
style={{
...textStyle.cssStyles
}}
onTouchStart={onLineClick}
onClick={onLineClick}
>
{textStyle.text}
<a
className="text-green-300 cursor-pointer hover:underline"
onClick={() => { onMyLineDetailClick(lineConfig.line); }}
>
Detail
</a>
</div>
This block shows the shared utility panel exporting the prepared graph canvas as an image.
const downloadImage = async () => {
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
if (imageBlob) {
downloadBlob(imageBlob, 'my-image-name');
}
await graphInstance.restoreAfterImageGeneration();
};
What Makes This Example Distinct
The comparison data positions this example as a focused reference for fully replacing relation-graph’s line renderer while still keeping relation-graph primitives and events in play. Its strongest distinguishing point is the combination of RGSlotOnLine, RGLinePath, RGLineText, generateLinePath, and generateLineTextStyle in one renderer, rather than stopping at stroke color changes or ribbon-like restyling. The animated dot plus explicit start and end text markers is also called out as a rare visual combination.
Compared with adv-line-slot-1, this example emphasizes endpoint annotation and simpler slot wiring rather than path-length-aware animation. Compared with adv-line-slot-2, it stays narrower in scope: it does not add route metrics or a custom node slot, and it keeps the focus on line rendering itself. Compared with div-on-line, it redraws every edge through RGSlotOnLine instead of leaving the native renderer intact and attaching a separate overlay to one selected line. Compared with adv-line-slot2 and customer-line1, it stays closer to relation-graph’s native line primitives instead of converting connections into filled ribbon areas or theme-driven bands.
That makes it a strong starting point when the requirement is not just to restyle a line, but to replace the line layer with a geometry-aware renderer that still behaves like a normal relation-graph line.
Where Else This Pattern Applies
This pattern can be reused in route monitoring, infrastructure maps, equipment wiring diagrams, dependency visualization, transport handoff views, or any directional network where the line needs more meaning than a plain stroke. It is especially useful when each connection needs animated flow cues, path-bound endpoint labels, a separate line-level action, and exportable viewer tooling without turning the graph into a full editor.