JavaScript is required

Dagre Position Layout Runtime Controls

This example builds a fixed-layout relation-graph scene that lets relation-graph render and measure the nodes, then hands position calculation to Dagre. Its main lesson is how to keep Dagre's ranker and spacing parameters live at runtime while preserving custom node cards, orthogonal labeled connectors, a minimap, and shared canvas utilities.

Runtime Dagre Position Tuning in a Fixed relation-graph Scene

What This Example Builds

This example builds a full-screen, read-only relation-graph viewer that loads one static network, lets relation-graph render and measure the nodes, and then uses Dagre only to compute node positions. The final view keeps custom node cards, orthogonal labeled connectors, a floating helper window, and a minimap instead of switching to a Dagre-specific renderer.

Users can retune Dagre’s ranker, nodesep, and ranksep while the same graph instance stays mounted. They can also drag or minimize the helper window, open shared canvas settings, export an image, and inspect the relaid graph through the embedded mini view. The main point of the example is the runtime relayout loop, not just the initial Dagre handoff.

How the Data Is Organized

The dataset is declared inline as one RGJsonData object with rootId: 'root', a flat nodes array, and a flat lines array. The node texts are intentionally generic, so the example stays focused on layout behavior instead of domain semantics.

There is a small preprocessing step before the custom layout runs. The code assigns an id to every line if one is missing, calls setJsonData(...) so relation-graph can render and measure each node, then inspects the live links relative to the root node and rewrites each line’s label placement with placeText and textOffsetY.

That shape maps cleanly to dependency graphs, workflow stages, service topology, org branches, or any other network where the application wants to keep graph data simple and delegate coordinate calculation to an external layout engine.

How relation-graph Is Used

The entry component wraps the page in RGProvider, and MyGraph uses RGHooks.useGraphInstance() as the main runtime API. The graph itself stays in layoutName: 'fixed', which is the key setup for this pattern: relation-graph renders the scene and exposes measured node dimensions, while Dagre handles coordinate calculation outside the built-in layout system.

The RGOptions object sets rectangular nodes, orthogonal connectors, top-bottom junction points, no default node border, gray line color, and white node fill. After setJsonData(...), the code uses getNodeById(...), getLinks(), and updateLine(...) to adjust line-label placement before the first Dagre pass. The custom doMyLayout() function then builds a Dagre graph from getNodes() plus getLines(), uses measured el_W and el_H values for accurate sizing, runs dagre.layout(g), and writes the resulting positions back through updateNodePosition(...). Each layout pass ends with moveToCenter() and zoomToFit().

Runtime tuning is driven by React state. dagreRanker, dagreGapH, and dagreGapV live in component state, and a useEffect reruns doMyLayout() whenever any of them changes. RGSlotOnNode customizes node rendering so the root becomes a larger gray card and the remaining nodes become smaller bordered labels. RGSlotOnView mounts RGMiniView as a viewport overlay.

The floating DraggableWindow is shared helper UI, but it matters to the example’s working pattern. It holds the Dagre controls, supports dragging and minimizing, and opens CanvasSettingsPanel, which uses relation-graph hooks to change wheel behavior, change canvas drag behavior, and export the graph via prepareForImageGeneration() and restoreAfterImageGeneration(). Styling is split between slot markup and SCSS: node cards are defined in JSX, while .rg-line-label is restyled into a small white badge with a gray border.

Key Interactions

The most important interaction is live Dagre tuning. Changing the ranker selector or either spacing slider immediately reruns the external Dagre pass on the already loaded graph, so users can compare layout variants without rebuilding data.

The helper window is also part of the experience. It can be dragged out of the way, minimized, or switched into a settings overlay that changes wheel behavior, changes canvas drag behavior, and downloads an image of the graph.

Navigation remains practical even after repeated relayouts because the example keeps RGMiniView mounted in the graph view. Node and line click handlers exist, but they only log objects to the console and are not part of the main feature set.

Key Code Fragments

This fragment shows that relation-graph is kept in fixed mode so Dagre can provide positions without replacing the rest of the graph renderer.

const graphOptions: RGOptions = {
    debug: false,
    layout: {
        layoutName: 'fixed' // 使用自定义布局时,建议设为 fixed
    },
    defaultNodeShape: RGNodeShape.rect,
    defaultLineShape: RGLineShape.StandardOrthogonal,
    defaultJunctionPoint: RGJunctionPoint.tb,
    defaultNodeBorderWidth: 0,
    defaultLineColor: '#666',
    defaultNodeColor: '#fff'
};

This fragment proves that the example first mounts the graph, then rewrites line-label placement heuristics on the live links before running Dagre.

await graphInstance.setJsonData(myJsonData);
const rootNode = graphInstance.getNodeById(myJsonData.rootId)!;
graphInstance.getLinks().forEach(link => {
    if (link.fromNode.y < rootNode.y) {
        graphInstance.updateLine(link.line.id, { placeText: 'start', textOffsetY: 20 });
    } else {
        graphInstance.updateLine(link.line.id, { placeText: 'end', textOffsetY: -20 });
    }
});
await doMyLayout();

This fragment shows the render-measure-layout step where Dagre consumes relation-graph’s measured node dimensions and existing edges.

const g = new dagre.graphlib.Graph();
g.setGraph({ nodesep: dagreGapH, ranksep: dagreGapV, ranker: dagreRanker });

graphInstance.getNodes().forEach(node => {
    g.setNode(node.id, { width: node.el_W || 100, height: node.el_H || 40 });
});

graphInstance.getLines().forEach(line => {
    g.setEdge(line.from, line.to, line);
});
dagre.layout(g);

This fragment shows the writeback step that moves the existing relation-graph nodes to Dagre’s computed coordinates and then refits the viewport.

g.nodes().forEach((nodeId: string) => {
    const dagreNode = g.node(nodeId);
    graphInstance.updateNodePosition(nodeId, dagreNode.x, dagreNode.y);
});

graphInstance.moveToCenter();
graphInstance.zoomToFit();

This fragment shows that Dagre tuning is exposed as normal React state, not as a one-shot initialization setting.

<SimpleUISelect
    data={[
        { value: 'network-simplex', text: 'network-simplex' },
        { value: 'longest-path', text: 'longest-path' },
        { value: 'tight-tree', text: 'tight-tree' }
    ]}
    currentValue={dagreRanker}
    onChange={(newValue: string) => { setDagreRanker(newValue); }}
/>

This fragment proves that relayout is triggered by state changes after the graph is already mounted.

useEffect(() => {
    doMyLayout();
}, [dagreRanker, dagreGapH, dagreGapV]);

This fragment shows the custom node slot that makes the root visually heavier than the remaining nodes.

<RGSlotOnNode>
    {({ node }) => {
        return (
            node.id === 'root' ? (
                <div className="px-2 min-w-[200px] min-h-[50px] rounded border border-gray-500 bg-gray-100 flex items-center justify-center w-full h-full text-sm text-slate-800 font-bold select-none">
                    {node.text}
                </div>
            ) : (
                <div className="px-2 min-w-[100px] rounded border border-gray-500 flex items-center justify-center w-full h-full text-sm text-slate-800 font-medium select-none">
                    {node.text}
                </div>

This fragment shows the SCSS override that turns connector labels into small white chips instead of leaving the default line-label styling.

.rg-line-peel {
    .rg-line-label {
        background-color: #fff;
        border: #666 solid 1px;
        font-size: 10px;;
    }
}

What Makes This Example Distinct

The comparison data shows that the closest neighbor is use-dagre-layout, but this example goes further into runtime experimentation. Both examples use the same general render-measure-Dagre-writeback pattern, yet this one keeps ranker, nodesep, and ranksep live after the initial load, which makes it a better starting point when a team needs to compare position-only Dagre variants on one mounted graph.

Compared with use-d3-layout, the distinctive emphasis is stability rather than geometry transformation. This example keeps node cards and connector style stable and only rewrites positions, while the D3 example changes layout families and rewrites more of the node geometry itself.

Compared with io-tree-layout, the center of gravity is external layout integration, not built-in preset switching. The graph remains in layoutName: 'fixed', the algorithm stays outside relation-graph, and the article’s main lesson is how to keep that external layout controllable at runtime.

The rarity records also highlight the feature combination rather than any single isolated trick: live Dagre parameter tuning, line-label placement heuristics, root-emphasized slot cards, orthogonal labeled connectors, and minimap-assisted navigation all appear together in one compact layout workbench.

Where Else This Pattern Applies

This pattern transfers well to internal layout-evaluation tools, dependency explorers, service maps, org networks, and workflow viewers where the graph should remain mounted while users compare spacing or ranking variants of an external algorithm.

It is also useful when relation-graph is the main viewer shell but layout coordinates come from somewhere else. Dagre can be replaced with another external layouter or even a backend service, while the same relation-graph-side structure still handles slots, connector styling, viewport fitting, canvas settings, and export.