Dagre Layout Integration and Line Label Positioning
A relation-graph example that renders custom node cards first, measures their live dimensions, then runs Dagre externally and writes the computed coordinates back into a fixed-layout scene. It also keeps orthogonal labeled connectors, a minimap overlay, and shared canvas utilities such as export and runtime interaction settings.
Integrate Dagre into a Fixed relation-graph Viewer
What This Example Builds
This example builds a full-height relation-graph viewer that loads a static directed graph, lets the graph render once so each node card has a measured size, then runs Dagre and writes the computed coordinates back into the live scene. The result is a graph with one larger root card, smaller rectangular child cards, orthogonal labeled connectors, a floating description and settings window, and a minimap overlay.
The user does not tune the layout interactively. The graph lays itself out on mount, then the user can pan, zoom, inspect the structure through the minimap, open canvas settings, drag or minimize the helper window, and export an image. The main teaching value is the external layout handoff pattern, not a built-in layout preset.
How the Data Is Organized
The graph data is declared inline as one RGJsonData object inside initializeGraph(). It uses a rootId, a flat nodes array, and a flat lines array. The nodes only carry ids and labels in the source data. Positions are intentionally absent because Dagre computes them later.
There is important preprocessing before the final layout appears. The code first assigns synthetic ids to any line that does not already have one. Then it calls setJsonData() so relation-graph can render the custom node slots and populate each node’s measured el_W and el_H. After that initial render, it reads links relative to the root node and adjusts each line label to start or end with a different Y offset before running Dagre. In a real application, the same shape could represent dependency graphs, approval flows, service topologies, organizational structures, or any other directed network where node content is custom-rendered and final coordinates should be computed by an external engine.
How relation-graph Is Used
RGProvider wraps the demo so hooks can resolve against the active graph instance. The graph options keep relation-graph in layoutName: 'fixed', which is the correct base mode when an external algorithm is responsible for final coordinates. The same options also define rectangular nodes, orthogonal connectors, top-bottom junction points, and neutral default colors so the Dagre output remains readable.
RGHooks.useGraphInstance() is the central integration point. The example uses it to load JSON data, inspect the rendered root node, traverse links, update line labels, read measured node sizes, add Dagre edges, write coordinates back with updateNodePosition(), and reset the viewport with moveToCenter() plus zoomToFit(). The node rendering itself is slot-based: RGSlotOnNode gives the root node a larger gray card while other nodes use smaller bordered rectangles. RGSlotOnView mounts RGMiniView, so overview navigation survives the external layout pass.
The floating utility window comes from shared helper code rather than example-specific logic, but it is still part of the delivered experience. Inside that helper, useGraphStore() reflects the current drag and wheel modes, and useGraphInstance() drives runtime option changes plus the prepare and restore lifecycle used for image export. The local SCSS does not redefine the whole graph theme. It mainly styles line labels as compact white chips with gray borders so the orthogonal connectors remain legible after relayout.
Key Interactions
The first important interaction is automatic rather than user-triggered: when the component mounts, the graph renders once, applies a label-placement pass, runs Dagre, and recenters itself. That startup flow is the main feature because it demonstrates how to combine relation-graph rendering with a third-party layout engine.
After layout, the user can navigate with normal pan and zoom behavior and keep orientation through the always-visible minimap. The floating helper window can be dragged, minimized, and switched into a canvas settings panel. That panel changes wheel behavior, changes canvas drag behavior, and downloads an image through the shared export helper. Node and line click handlers are wired only for console inspection, so they are not a meaningful part of the functional UX.
Key Code Fragments
This option block proves the graph stays in fixed-layout mode while still using relation-graph styling defaults for orthogonal links and rectangular nodes.
const graphOptions: RGOptions = {
debug: false,
layout: {
layoutName: 'fixed'
},
defaultNodeShape: RGNodeShape.rect,
defaultLineShape: RGLineShape.StandardOrthogonal,
defaultJunctionPoint: RGJunctionPoint.tb,
defaultNodeBorderWidth: 0,
defaultLineColor: '#666',
defaultNodeColor: '#fff'
};
This initialization fragment shows the two-phase flow: render first for measurement, then adjust line labels before calling the external layout routine.
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 Dagre setup proves the example uses rendered node dimensions from relation-graph instead of hard-coded sizes.
const g = new dagre.graphlib.Graph();
g.setGraph({ nodesep: 20, ranksep: 90, ranker: 'network-simplex' });
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);
});
This writeback block shows that Dagre only computes coordinates, while relation-graph remains responsible for the live scene and viewport management.
dagre.layout(g);
g.nodes().forEach((nodeId: string) => {
const dagreNode = g.node(nodeId);
graphInstance.updateNodePosition(nodeId, dagreNode.x, dagreNode.y);
});
graphInstance.moveToCenter();
graphInstance.zoomToFit();
This slot fragment proves the external layout does not prevent custom node cards or a minimap overlay from being used in the same graph.
<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>
);
}}
</RGSlotOnNode>
This helper fragment shows that the shared floating panel can still drive export lifecycle calls against the same graph instance.
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
// ...
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();
What Makes This Example Distinct
Comparison data shows that this example is not the only third-party layout integration demo, but it is one of the clearest baseline references for Dagre handoff. Relative to use-dagre-layout-2, it removes runtime Dagre tuning controls and keeps the lesson narrower: render the graph, measure the custom node slots, run Dagre once, and write positions back. That makes it easier to reuse as an implementation recipe rather than as a layout playground.
The comparison also highlights a rarer combination than nearby examples. Compared with use-sigma-layout, this example emphasizes directed structure, orthogonal connectors, and a pre-layout line-label heuristic instead of force-layout experimentation or dataset switching. Compared with use-d3-layout, it keeps node geometry stable and uses the external algorithm only for position updates, not for reshaping nodes into a different hierarchy visualization. The most distinctive package here is external Dagre integration plus render-measure-writeback flow, custom rectangular node cards, orthogonal label styling, minimap navigation, and shared viewer utilities in one compact viewer-oriented example.
Where Else This Pattern Applies
This pattern transfers well to systems that need a third-party layout engine but still want relation-graph slots, overlays, and viewport tools. Typical examples include dependency graphs, architecture diagrams, orchestration flows, policy trees, service call maps, and other directed structures where node content is richer than a plain text label.
It is also a useful starting point when the final node size depends on rendered HTML rather than a fixed schema. In that case, the render-measure-layout-writeback sequence can be reused with Dagre or replaced with another external engine while relation-graph continues to handle node slots, line styling, minimap display, export preparation, and runtime canvas controls.