JavaScript is required

Multi-Line Gap Control

This example demonstrates live control of spacing between repeated links by binding `multiLineDistance` to React state and updating the loaded graph in place. It combines that gap control with graph-wide line-shape and label-placement toggles on a small repeated-edge tree, while reusing a floating settings panel for canvas modes and image export.

Runtime Multi-Line Gap Control for Repeated Connectors

What This Example Builds

This example builds a full-screen graph workbench for repeated-edge spacing. The canvas shows three avatar nodes connected by several duplicate relationships, so the same node pair can display multiple parallel-looking links at once. A floating panel lets the user switch all lines between straight and curved shapes, toggle whether labels follow the path, and widen or tighten the spacing between repeated connectors.

The graph content is intentionally small. The main value is that one loaded graph stays on screen while edge geometry changes live, which makes the effect of multiLineDistance, line shape, and text placement easy to compare.

How the Data Is Organized

The data comes from a local RGJsonData object in mock-data-api.ts. It defines three nodes, each with a color and an avatar URL in node.data.icon, plus ten line records. Several lines repeat the same from and to pairs, which gives the example a reliable surface for testing spacing between concurrent links.

There is almost no preprocessing before layout runs. fetchJsonData() only wraps the static dataset in a 200 millisecond Promise, and initializeGraph() passes the result directly into setJsonData(). After that, the structure stays fixed and only visual line properties are changed at runtime. In a real application, the same shape can represent people with several relationship types, services with multiple dependency channels, or systems with several flows between the same endpoints.

How relation-graph Is Used

RGProvider wraps the example, and RGHooks.useGraphInstance() is the main control surface inside MyGraph. The graph options use a tree layout with treeNodeGapH and treeNodeGapV both set to 200, so the three-node scene has enough room to show separated duplicate links clearly.

The important line options are defaultJunctionPoint: RGJunctionPoint.border and multiLineDistance: multiLineGap. The border junction setting is what makes repeated links between the same nodes appear as separated parallel routes instead of collapsing into one visual path. The example also sets defaultLineShape to straight in the options, but immediately rewrites loaded lines from React state so the initial render becomes curved with a 50 pixel gap.

The runtime update path is the core technique. One effect loads the JSON and centers the graph with loading(), setJsonData(), clearLoading(), moveToCenter(), and zoomToFit(). A second effect watches lineShape, textOnPath, and multiLineGap, then applies them through updateOptions({ multiLineDistance }) and a graph-wide getLines().forEach(updateLine(...)) pass. That means the graph data does not have to be rebuilt when users change presentation settings.

The node appearance is customized with RGSlotOnNode. Instead of the default node renderer, each node becomes a circular avatar with the label placed below it in the node color. Local SCSS adds the tiled canvas background, styles the line labels, and shows a blue halo when a node enters the checked state.

The example also reuses shared helper components. DraggableWindow provides the floating panel shell, and its CanvasSettingsPanel uses RGHooks.useGraphStore() plus graphInstance.setOptions() to switch wheel and drag behavior at runtime. The same panel can export the current graph as an image through prepareForImageGeneration(), domToImageByModernScreenshot(), and restoreAfterImageGeneration(). These utilities support the workbench, but they are not the main lesson of this example.

Key Interactions

  • The line shape selector switches every loaded line between RGLineShape.StandardStraight and RGLineShape.StandardCurve.
  • The line text selector toggles useTextOnPath for all current lines, so labels can move from normal label blocks onto the SVG path.
  • The range slider changes multiLineGap, which immediately updates multiLineDistance on the live graph and changes the spacing between repeated connectors.
  • Clicking empty canvas space calls clearChecked(), which removes any checked highlight from nodes or lines.
  • The floating panel can be dragged, minimized, and switched into a settings overlay that changes wheel mode, drag mode, and supports image export.

Key Code Fragments

This options block shows that repeated-line spacing is a first-class graph option and that border junction points are required for the visual comparison.

const graphOptions: RGOptions = {
    debug: false,
    defaultLineShape: RGLineShape.StandardStraight,
    defaultNodeShape: RGNodeShape.circle,
    defaultJunctionPoint: RGJunctionPoint.border,
    multiLineDistance: multiLineGap,
    layout: {
        layoutName: 'tree',
        treeNodeGapH: 200,
        treeNodeGapV: 200
    },

This initialization flow proves that the example loads fixed JSON once and then prepares the viewport instead of recalculating data for each UI change.

const initializeGraph = async () => {
    const myJsonData: RGJsonData = await fetchJsonData();

    graphInstance.loading();
    await graphInstance.setJsonData(myJsonData);
    await updateMyGraphData();
    graphInstance.clearLoading();
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};

This update function is the central implementation detail: it changes graph-wide spacing through options and then rewrites every current line’s shape and label mode.

const updateMyGraphData = async () => {
    graphInstance.updateOptions({
        multiLineDistance: multiLineGap
    });
    graphInstance.getLines().forEach((line) => {
        graphInstance.updateLine(line, {
            lineShape,
            useTextOnPath: textOnPath
        });
    });
};

This control fragment shows that the line presentation UI is state-driven rather than hard-coded in the JSON data.

<SimpleUISelect
    data={[
        { value: RGLineShape.StandardStraight, text: 'Straight' },
        { value: RGLineShape.StandardCurve, text: 'Curve' }
    ]}
    onChange={(newValue: number) => setLineShape(newValue)}
    currentValue={lineShape}
/>

This node slot proves that the example is not using the default node body and instead renders avatar portraits with labels below.

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div className="w-20 h-20 flex place-items-center justify-center">
            <div className="my-node-avatar" style={{ backgroundImage: `url(${node.data?.icon})` }} />
            <div className="absolute transform translate-y-[55px]" style={{ color: node.color }}>{node.text}</div>
        </div>
    )}
</RGSlotOnNode>

This shared settings fragment shows how the workbench can also change canvas interaction mode without touching the graph data.

<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 records place this example closest to line-shape-and-label, gap-of-line-and-node, and custom-line-style, but they also show a clear difference in emphasis. Here the main lesson is live multiLineDistance control on a graph that already contains repeated links between the same node pairs. The shape switch and text-on-path switch matter because they help users judge the spacing under different line presentations on the same loaded graph.

Compared with line-shape-and-label, this example focuses more on spacing among repeated edges than on junction-anchor comparison. Compared with gap-of-line-and-node, the spacing lesson is between concurrent links on one node pair, not between a line endpoint and the node border it touches. Compared with custom-line-style, line-style1, and line-style2, the distinctive value is built-in geometric tuning rather than CSS-driven skinning or checked-line emphasis.

The comparison file also limits what should be claimed. This is not the only example that batch-updates rendered lines with getLines() and updateLine(...), and the draggable settings window, canvas interaction controls, and image export flow are shared utilities used elsewhere. The distinctive combination here is a repeated-edge tree, border junction points, avatar nodes, a tiled showcase background, and a live gap slider that keeps the same graph state visible while connector geometry changes.

Where Else This Pattern Applies

  • People or organization graphs where two entities can have several simultaneous relationship types and those lines must remain readable.
  • Service dependency maps where one system can connect to another through multiple channels such as API traffic, events, ownership, and alerts.
  • Knowledge graph viewers that need a compact style-tuning board before a final edge presentation rule is chosen for a larger product surface.
  • Demo or QA workbenches where teams need to compare line spacing, routing, and label placement on a fixed graph state without rebuilding the dataset.