Diagram Editor Workbench
This example turns relation-graph into a full diagram editor workbench with creation tools, contextual node and line editing, grouping, container nodes, layout switching, and local history. It is a strong reference for teams that need one integrated editing shell rather than a narrow single-feature demo.
Building a Diagram Editor Workbench with relation-graph
What This Example Builds
This example builds a full-screen diagram editor rather than a read-only graph viewer. The canvas is wrapped by a top toolbar, a bottom creation palette, floating node and line toolbars, right-click menus, a layout dialog, optional rulers, a custom background layer, and a minimap toggle.
Users can create new nodes and lines from templates, resize and restyle nodes, edit line geometry and markers, group selected nodes, move nodes into container nodes, switch layout strategies, and save or reopen local versions. The most important point is not any single widget, but the way the example turns relation-graph into a document-style editing surface with history, mixed connection targets, and multiple custom node renderers working together.
How the Data Is Organized
The live document model is explicit. MyGraphDocJson stores four top-level collections: nodes, lines, fakeLines, and myGroups. That means the editor does not treat groups or mixed-target connections as temporary UI state. They are part of the same saved document as ordinary nodes and links.
Container membership is stored on nodes through data.ownerContainerId, and container nodes keep data.containerNodesIds. Group membership is stored separately in myGroups, where each group records style metadata plus the member node ids in the serialized form. The bottom toolbar also defines reusable node and line templates before anything is added to the graph, so creation starts from structured presets instead of ad hoc mutations.
Before a snapshot is recorded, the editor clears and reloads the graph through initialDocData() and loadDocVersion(). That reload path reconstructs nodes, normal lines, fake lines, and groups together. The first history entry is intentionally delayed for a short time so restored node sizes are measured before the snapshot is captured. In a real product, the same document shape could represent workflow steps, service topologies, AI pipelines, manufacturing stations, or internal approval diagrams.
How relation-graph Is Used
The example uses RGProvider and useRelationGraph() to get both the graph instance and the RelationGraph component inside the editor shell. The graph starts in a fixed layout with editor-oriented defaults such as hidden built-in toolbar, selection dragging, scroll-wheel panning, and a custom RelationGraphPlusCore subclass.
relation-graph slots carry most of the UI composition. RGSlotOnView hosts fixed editor chrome such as toolbars, dialogs, context menus, the minimap button, and optional calipers. RGSlotOnNode swaps node renderers so the same graph can display normal nodes, container nodes, CPU-style pin nodes, AI-model nodes with input/output ports, and a custom BoBo node. RGSlotOnCanvas renders dynamic group overlays that move with the canvas instead of staying fixed to the viewport.
The editor also leans heavily on built-in editing hooks and controllers. RGEditingLineController, RGEditingReferenceLine, RGEditingConnectController, RGMiniToolBar, and RGMiniView are all mounted directly in the scene. Creation flows come from startCreatingNodePlot() and startCreatingLinePlot(), and the instance API is used for createLayout(), moveToCenter(), zoomToFit(), updateNode(), updateLine(), addNodes(), addLines(), addFakeLines(), _clearGraph(), _addNodes(), _addLines(), and _addFakeLines().
Layout support is broader than a normal demo. The picker exposes tree, center, simple-tree, force, folder, random, grid, flow, column-grid, and force-directed modes. flow delegates placement to a Dagre-based helper, force-directed delegates to a Sigma-based helper, and simple-tree is translated into a standard relation-graph tree layout with simpleTree = true.
Styling is also customized at the framework boundary. The example injects generated CSS variables into document.head for reusable node and line style classes, defines five SVG line markers, and drives the editor background from graph zoom and canvas offsets through CSS variables.
Key Interactions
Node clicks and line clicks switch editing focus, while canvas clicks clear the active selection. Rectangle selection feeds back into the graph by marking nodes as selected and then promoting that selection into the editing controller.
The bottom toolbar starts drag-create flows for both nodes and lines. New nodes can be dropped directly into container nodes during creation, and existing free nodes can later be dragged over containers or groups to show drop feedback before membership is committed.
Right-click behavior is target-specific. Nodes, lines, groups, and the blank canvas each open their own context menu panel. That keeps destructive or structural actions close to the current target instead of burying them in one global inspector.
Line authoring supports mixed targets. When both endpoints are ordinary nodes, the new connection is stored as a normal line. When the target is a non-node connect target such as a group or a custom endpoint, the editor downgrades the connection into a fake line and preserves target metadata inside the saved document.
Undo, redo, copy, paste, delete, and local save make the example feel like a lightweight document editor instead of a transient interaction demo. Snapshot history is available in the UI, and saved versions are kept in browser local storage.
Key Code Fragments
This fragment shows the document shape that the editor serializes and restores.
export type MyGraphDocJson = {
nodes: JsonNode[];
lines: JsonLine[];
fakeLines: JsonLine[];
myGroups: MyNodesGroupJson[]
}
This fragment shows that graph startup is tied to custom fake-line target resolution and local-history recovery.
const onReady = async(graphInstance: RelationGraphInstance) => {
if (graphInstance) {
myGraphActions.current.setGraphInstance(graphInstance);
graphInstance.setFakeLineTargetRender(myGraphActions.current.getFakeLineTarget.bind(myGraphActions.current));
await myGraphActions.current.onReady();
}
myGraphActions.current.setReactiveData(editorState);
myGraphActions.current.addListeners();
await loadLocalHistory();
};
This fragment shows that history snapshots serialize fake lines and groups together with ordinary graph data.
const {nodes, lines} = this.getGraphJsonData();
const fakeLines: JsonLine[] = [];
for (const elLine of graphInstance.getFakeLines()) {
const jsonLine = graphInstance.transRGLineToJsonObject(elLine);
fakeLines.push(jsonLine);
}
const myGroups = this.editorState.myGroups.map(group => {
return {
...group,
groupNodes: group.groupNodes.map(node => node.id)
};
});
This fragment shows how the creation palette uses relation-graph’s plotting API instead of manually placing DOM overlays.
graphInstance.startCreatingNodePlot(e, {
templateText: tempNode.text,
templateNode: newNodeTemplate,
onCreateNode: (x, y, nodeTemplate) => {
onMyNodeCreateFinish(x, y, nodeTemplate);
}
});
This fragment shows the key branch that keeps node-to-node links as normal lines and downgrades mixed-target connections into fake lines.
newLineJson.fromType = fromNode.targetType;
newLineJson.toType = toNode.targetType;
if (fromNode.targetType === RGInnerConnectTargetType.Node && toNode.targetType === RGInnerConnectTargetType.Node) {
if (newLineJson.isFakeLine) {
this.getGraphInstance().addFakeLines([newLineJson]);
} else {
this.getGraphInstance().addLines([newLineJson]);
}
} else {
This fragment shows how one custom node renderer exposes relation-graph connect targets as real editing ports.
<RGConnectTarget
junctionPoint={RGJunctionPoint.left}
targetId={node.id + '-input-' + item.name}
lineTemplate={{color: '#f8b817', lineWidth: 2}}
disableDrag={disableEdit}
>
<div className="bg-white w-4 h-4 rounded-full border border-gray-200 hover:bg-amber-300">
</div>
</RGConnectTarget>
What Makes This Example Distinct
Its distinct value is breadth. Compared with undo-redo-example, this is not a history-focused sample with a small graph. History is only one subsystem inside a larger editor that also handles groups, containers, fake lines, multiple node types, layout switching, local saved versions, and target-specific context menus.
Compared with change-line-path, change-line-text, and customize-line-toolbar, this example does not isolate one line-editing lesson. It embeds line geometry, text, markers, animation, connect points, and display-only mode inside a complete authoring workspace that also creates and edits nodes, groups, and layouts.
Compared with element-line-edit and element-connect-to-node, it uses fake-line and connect-target mechanics as part of ongoing authoring rather than as a prepared showcase. Users can start a new connection from the editor palette, and the resulting document persists both ordinary lines and mixed-target fake lines in the same snapshot model.
The comparison data also supports one broader conclusion: this example is unusual because it combines palette-based creation, contextual toolbars, batch selection, grouping, container membership logic, layout reflow, minimap utilities, custom node ports, custom markers, and local-first document history in one canvas shell. It is better treated as a reusable editor reference than as a “small process editing tool.”
Where Else This Pattern Applies
This pattern can be adapted to workflow builders where users need to create and revise steps, decisions, and branch connections inside one canvas. It also fits internal architecture editors, AI pipeline designers, and diagramming tools where some endpoints are whole nodes while others are typed ports or grouped regions.
The same structure is also useful for domain editors that need local drafts before backend persistence exists. A team can start with local snapshot history, template-driven creation, and custom node slots, then later replace the saved document source with server-backed storage without changing the editor’s basic graph model.