JavaScript is required

Graph Undo/Redo History

This example shows a compact editable relation-graph canvas with snapshot-based undo, redo, and direct history jumping. It captures graph state after supported edits, exposes both keyboard shortcuts and floating history controls, and restores older versions by rebuilding nodes and lines from serialized graph JSON.

Undo, Redo, and Snapshot History in a Compact Graph Editor

What This Example Builds

This example builds a small editable graph canvas with document-style history controls. Users see a fixed-position relation graph, a floating toolbar for undo, redo, and history visibility, and a floating history panel that lists saved snapshots in reverse chronological order.

The graph itself stays visually simple: rectangular nodes, standard links, and full-viewport canvas space. The important behavior is that ordinary edits such as moving nodes, resizing nodes, creating links, and removing a node are captured as snapshots that can be undone, redone, or revisited directly by clicking a history entry.

Its most useful point is not visual customization. It is the compact combination of graph editing overlays, keyboard shortcuts, branch truncation after rewinding, and rebuild-based history restoration in a much smaller example than a full editor workbench.

How the Data Is Organized

The initial graph is declared inline as RGJsonData, with each node carrying an id, text, and authored x and y coordinates, and each line carrying an id, from, to, and label text. Because the layout is fixed, those coordinates are the actual rendered positions instead of inputs to an automatic layout pass.

Before setJsonData(...), there is no transformation pipeline. The example loads the seed dataset directly, centers the viewport, and immediately stores an initial snapshot. After that, the history manager serializes the live graph state returned by getGraphJsonData() into a HistoryGraphData record that contains a generated snapshot id, local timestamp, serialized JSON string, operation description, and approximate payload size in kilobytes.

That structure maps cleanly to real application data such as workflow drafts, topology revisions, org-chart edits, or relationship reviews. In those cases, the node and line payloads can hold business identifiers and metadata, while the snapshot wrapper acts as lightweight revision history for the graph state itself.

How relation-graph Is Used

The demo uses RGProvider at the entry point and renders a RelationGraph configured for a fixed layout. The graph options keep defaults close to the library baseline, but make the editor predictable by setting rectangular nodes, explicit default node dimensions, a 2-pixel default line width, dragEventAction: 'selection', and disableDragNode: false.

RGHooks.useGraphInstance() is the main integration point. The component uses it to load the seed graph with setJsonData(...), center the canvas with moveToCenter(), capture snapshots with getGraphJsonData(), remove nodes with removeNode(...), create new ids with generateNewUUID(...), start interactive connection creation with startCreatingLinePlot(...), and restore saved states through clearGraph(), addNodes(...), and addLines(...).

The editing UI is assembled inside RGSlotOnView. RGEditingNodeController enables edit overlays around the active node, RGEditingResize adds resize handles, RGEditingConnectController supports connection targeting, and the custom MyNodeToolbar adds directional connect buttons plus a delete button around the selected node. RGHooks.useEditingNodes() keeps that toolbar visible only when a single node is currently being edited.

Customization is intentionally restrained. The graph keeps default node rendering, while the history toolbar and history panel use inline positioning and simple white overlay styling. That makes the state-management pattern easier to reuse without inheriting a large visual shell.

Key Interactions

Clicking a node sets it as the current editing node, which activates the resize controller and the custom around-node toolbar. Clicking blank canvas space clears editing nodes, clears any editing line state, and clears checked selections so the editor returns to an idle state.

Dragging a node and finishing a resize both create snapshots. Removing the selected node also creates a new snapshot after the deletion. For connection creation, the selected-node toolbar launches startCreatingLinePlot(...); when the user completes a valid connection, the code adds the new line through the graph instance and stores another history entry.

Undo and redo work from both the floating toolbar and keyboard shortcuts. The component listens for Ctrl or Cmd plus Z, Y, or Shift+Z, prevents the browser default, and routes the action to the history manager. The history panel then exposes the same timeline in a visible form, where any saved version card can be clicked to jump straight to that snapshot.

Key Code Fragments

This fragment shows that the editor is built on a fixed layout with simple defaults, so the authored node coordinates are the graph state that history needs to preserve.

const graphOptions: RGOptions = {
    debug: false,
    defaultNodeShape: RGNodeShape.rect,
    defaultNodeWidth: 100,
    defaultNodeHeight: 40,
    defaultLineWidth: 2,
    layout: {
        layoutName: 'fixed',
    },
    dragEventAction: 'selection',
    disableDragNode: false,
};

This effect proves that keyboard shortcuts are a first-class part of the workflow, not just an extra button binding.

if (isCtrlOrCmd) {
    if (e.key === 'z' && !e.shiftKey) {
        e.preventDefault();
        if (currentIndex > 0) {
            myHistoryManager.current.undo();
        }
    } else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) {
        e.preventDefault();
        if (currentIndex < history.length - 1) {
            myHistoryManager.current.redo();
        }
    }
}

This fragment is the core history policy: rewinding and then editing removes the forward branch, and the stored snapshot list is capped to a maximum size.

let newHistory = [...this.historyList];
if (this.currentIndex < newHistory.length - 1) {
    newHistory = newHistory.slice(0, this.currentIndex + 1);
}

newHistory.push(snapshot);

if (newHistory.length > this.maxHistorySize) {
    newHistory = newHistory.slice(-this.maxHistorySize);
}

This restore path shows that undo, redo, and direct version jumps are implemented by rebuilding the graph from serialized nodes and lines.

loadHistory(snapshot: HistoryGraphData) {
    const data = JSON.parse(snapshot.jsonString);
    this.graphInstance.clearChecked();
    this.graphInstance.clearGraph();
    this.graphInstance.addNodes(data.nodes);
    this.graphInstance.addLines(data.lines);
}

This selected-node toolbar fragment demonstrates how line creation and node removal are surfaced as contextual in-graph actions instead of a separate side panel.

const editingNodes = RGHooks.useEditingNodes();
const onlyNode = editingNodes.nodes[0];
return (
    editingNodes.nodes.length === 1 ? <div className="w-full h-full">
            <div className="pointer-events-auto absolute top-[50%] left-[-40px] transform translate-y-[-50%]">
                <button
                    className="cursor-pointer h-6 w-6 bg-blue-500 hover:bg-blue-700 text-white rounded shadow flex place-items-center justify-center"
                    onClick={(e) => {
                        startCreateLineFromCurrentNode(onlyNode, RGJunctionPoint.right, e)
                    }}
                >

What Makes This Example Distinct

According to the prepared comparison data, this example is closest to compact editing demos such as create-line-from-node, line-vertex-on-node, and editor-button-on-line, because it uses the same selected-node editing shell and in-graph connection workflow. The difference is where the teaching emphasis sits. Here, contextual line creation is only one source of mutations feeding a reusable history system.

That makes the example distinct in two ways. First, it combines several comparatively rare elements in one small screen: fixed-layout editing, selected-node action chips, resize overlays, snapshot capture after supported edits, keyboard undo and redo, branch truncation after rewinding, and clickable version restoration. Second, compared with broader editor examples, it deliberately avoids palette creation, custom line slots, and heavier editor chrome so the undo/redo mechanism stays easy to study in isolation.

A conservative reading of the comparison file is that this is a strong starting point when the requirement is graph-state history on top of ordinary editor gestures, but not a full workbench. It should not be treated as a complete editor reference for every possible edit action or canvas-level state.

Where Else This Pattern Applies

This pattern transfers well to graph tools that need revision-safe editing without adopting a large editor shell. Examples include workflow designers where users often rearrange and reconnect steps, infrastructure topology tools where operators need to try layout or connection changes and roll them back, and knowledge-graph curation screens where analysts want visible checkpoints while refining a small subgraph.

It also fits review-oriented scenarios where direct version jumping is more useful than a single-step undo button. A moderation console, collaborative draft review surface, or teaching tool for graph editing can all reuse the same snapshot manager and history panel pattern while swapping in domain-specific node data and custom styling.