JavaScript is required

Line Shape, Junctions, and Label Placement

This example demonstrates graph-wide control of edge shape, curved-line junction anchors, and label placement on a loaded avatar relationship map. It uses `getLines()` with `updateLine()` to restyle every rendered edge in place, while a floating helper window also exposes shared canvas settings and image export.

Runtime Line Shape, Junction, and Label Placement Controls

What This Example Builds

This example builds a full-screen relationship map where the same loaded graph can be restyled live. The canvas shows circular avatar nodes on a tiled background, color-coded relationship edges, and a floating control window that switches every line between straight and curved routing, changes how curved lines attach to nodes, and toggles labels between boxed text and text placed on the path.

The graph content itself stays fixed after load. The main highlight is that the user can compare several edge-rendering behaviors on one dense avatar graph without rebuilding the dataset or changing node positions.

How the Data Is Organized

The data comes from a local RGJsonData object in mock-data-api.ts. It declares rootId: "N13", twenty-one nodes, and a larger set of line records. Each node carries visible styling fields such as color, borderColor, and data.icon, while each line carries from, to, text, color, fontColor, and a typed data payload.

There is no real preprocessing before layout. fetchJsonData() only wraps the static object in a short Promise and initializeGraph() sends that result straight into setJsonData(). Some node pairs intentionally appear more than once, such as N1 -> N15 and N13 -> N8, which helps the example show how line shape and attachment choices behave on repeated connectors. In real projects, the same structure can represent people networks, investigation links, stakeholder maps, or service relationships with several connection types between the same entities.

How relation-graph Is Used

RGProvider wraps the page and RGHooks.useGraphInstance() is the main runtime control surface inside MyGraph. The graph options use the built-in force layout, keep debug mode off, set circular nodes, keep multiLineDistance at 20, cap the layout iteration count at 50, and provide fallback node color and border settings.

The important implementation detail is that the example does not rebuild RGJsonData when the selectors change. After the async load completes, initializeGraph() calls loading(), setJsonData(), updateMyGraphData(), clearLoading(), moveToCenter(), and zoomToFit(). Later, a second effect watches lineShape, lineJunctionPoint, and textOnPath, then walks through graphInstance.getLines() and rewrites each rendered line with updateLine(...).

That line update pass is where relation-graph features are combined. lineShape switches between RGLineShape.StandardStraight and RGLineShape.StandardCurve. When straight mode is active, both endpoints are forced back to RGJunctionPoint.border; when curved mode is active, the selected junction value is written into both fromJunctionPoint and toJunctionPoint. The same pass also toggles useTextOnPath, so label placement changes without reloading the graph.

The example also uses slots and shared helper components. RGSlotOnNode replaces the default node body with circular portrait avatars and labels below each node. DraggableWindow hosts the line controls and can open a shared CanvasSettingsPanel, where RGHooks.useGraphStore() and graphInstance.setOptions() switch wheel and drag behavior. The shared panel also exports the current graph image through prepareForImageGeneration(), domToImageByModernScreenshot(), and restoreAfterImageGeneration().

Local SCSS finishes the presentation by adding the tiled canvas background, white boxed line labels, and a blue halo for checked nodes.

Key Interactions

  • The Line Shape selector rewrites every loaded edge to either straight or curved routing.
  • The Line JunctionPoint selector appears only when curved routing is active and lets the user compare border, paired-side, and single-side anchor modes.
  • The Line Text On Path selector toggles labels between ordinary boxed labels and text rendered directly on the line path.
  • Clicking empty canvas space calls clearChecked(), which removes checked highlights from the graph.
  • The floating helper window can be dragged, minimized, switched into a canvas-settings overlay, and used to download the current graph as an image.

Key Code Fragments

This data fragment shows that the example starts from a fixed RGJsonData object with a root node, per-node styling, and custom avatar URLs.

const jsonData = {
    "rootId": "N13",
    "nodes": [
        {
            "id": "N1",
            "text": "Liangping.Hou",
            "color": "#ec6941",
            "borderColor": "#ff875e",
            "data": {

This options block proves that the graph uses force layout, circular nodes, and a fixed repeated-edge spacing before runtime line updates begin.

const graphOptions: RGOptions = {
    debug: false,
    defaultLineShape: RGLineShape.StandardStraight,
    defaultNodeShape: RGNodeShape.circle,
    multiLineDistance: 20,
    layout: {
        layoutName: 'force',
        maxLayoutTimes: 50
    },
    defaultNodeBorderWidth: 2,

This initialization flow shows that the graph loads once, applies the current line settings, and then centers and fits the viewport.

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 core technique: it rewrites every already rendered line instead of regenerating the dataset.

const updateMyGraphData = async () => {
    graphInstance.getLines().forEach((line) => {
        graphInstance.updateLine(line, {
            lineShape,
            fromJunctionPoint: lineShape === RGLineShape.StandardStraight ? RGJunctionPoint.border : lineJunctionPoint,
            toJunctionPoint: lineShape === RGLineShape.StandardStraight ? RGJunctionPoint.border : lineJunctionPoint,
            useTextOnPath: textOnPath
        });
    });
};

This control fragment proves that junction selection is conditional and only matters when the graph is showing curved lines.

{
    lineShape !== RGLineShape.StandardStraight &&
    <div>
        <div className="text-base py-2">Line JunctionPoint:</div>
        <SimpleUISelect
            data={[
                { value: RGJunctionPoint.border, text: 'Border' },
                { value: RGJunctionPoint.ltrb, text: 'Left/Top/Right/Bottom' },
                { value: RGJunctionPoint.lr, text: 'Left/Right' },
                { value: RGJunctionPoint.tb, text: 'Top/Bottom' },

This node slot shows that the example combines graph-wide line controls with custom avatar node rendering.

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

This shared settings fragment shows that the floating workspace can also export the current graph image without changing graph data.

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');
    }

What Makes This Example Distinct

The comparison records put this example closest to line-multi-lines-gap, custom-line-style, search-and-focus, and use-dagre-layout-2, but they also show a narrower lesson. Here the primary focus is graph-wide control of built-in edge behavior on an already loaded graph: shape switching, curved-line junction selection, and text-on-path switching.

Compared with line-multi-lines-gap, this example spends less effort on spacing repeated edges and more effort on how curved connectors attach to nodes. Compared with custom-line-style, the distinguishing value is not CSS skinning but runtime control of built-in line geometry and label placement. Compared with search-and-focus and use-dagre-layout-2, the graph is not mainly about navigation or relayout; node placement stays in the force layout while only edge rendering changes.

The comparison file also limits the claims that are safe to make. The floating helper window, canvas settings, image export, and avatar-style relationship scene are not unique to this example, and it is not a structural editor. What makes it stand out is the combination of a dense avatar relationship map, graph-wide getLines() plus updateLine() updates, a curved-only junction selector, and a runtime switch between boxed labels and text on the path.

Where Else This Pattern Applies

  • Relationship or social graphs where teams need to compare readable connector styles on a fixed dataset before choosing a production default.
  • Investigation, risk, or fraud maps where one pair of entities can carry several different relationship types and edge readability matters more than structure editing.
  • Service and dependency diagrams that need a compact control surface for connector routing, anchor policy, and label placement without rerunning layout logic.
  • Internal demo or QA workbenches where designers and engineers need to review line behavior on a realistic graph scene and export snapshots of the current state.