OG
JS
Docs
Demos
Examples
Graph editing history
Add and remove a connected case, then travel backward or forward through the exact mutations.
Add case
Remove case
Remove edge
Undo
Redo
// Wrap related mutations in one `edit` transaction when they should undo together. // OGJS records exact node and edge changes, publishes history state after settlement, and // restores stable identities on undo or redo without application snapshots. import { Ogjs } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light' }); og.setGraph({ nodes: [ { id: 'case', x: 0, y: 0, size: 15, color: '#0f172a', label: 'Case' }, { id: 'account', x: -150, y: 100, size: 10, color: '#3b82f6', label: 'Account' }, { id: 'device', x: 150, y: 100, size: 10, color: '#8b5cf6', label: 'Device' }, { id: 'merchant', x: 0, y: -145, size: 10, color: '#f59e0b', label: 'Merchant' }, ], edges: [ { id: 'case-account', source: 'case', target: 'account' }, { id: 'case-device', source: 'case', target: 'device', width: 3, color: '#8b5cf6', curvature: 0.12, dash: [6, 3], arrow: true, data: { kind: 'risk' } }, { id: 'case-merchant', source: 'case', target: 'merchant' }, ], }); let serial = 0; let activeId = null; let activeEdgeId = null; const positions = [[-190, -90], [190, -90], [-220, 10], [220, 10]]; const controls = { add: document.querySelector('#add-node'), remove: document.querySelector('#remove-node'), edge: document.querySelector('#remove-edge'), undo: document.querySelector('#undo'), redo: document.querySelector('#redo'), }; function update(history = og.history.state) { // OGJS publishes history state after transactions settle. Controls can reflect undo and redo // availability without maintaining a duplicate command stack in application state. const stats = og.stats(); const state = { nodes: stats.nodes, edges: stats.edges, canUndo: history.canUndo, canRedo: history.canRedo, undoLabel: history.undoLabel, edgeStyle: og.getEdge('case-device') ?? null, activeEdge: activeEdgeId === null ? null : og.getEdge(activeEdgeId) ?? null, }; controls.undo.disabled = !state.canUndo; controls.redo.disabled = !state.canRedo; controls.remove.disabled = !activeId || og.getNode(activeId) === undefined; controls.edge.disabled = og.getEdge('case-device') === undefined; document.querySelector('#history-status').textContent = `${state.nodes} nodes · ${state.edges} edges · next undo: ${state.undoLabel ?? 'none'}`; og.camera.fit({ target: 'all', avoid: document.querySelector('.workflow-panel'), paddingPx: 24, maxOccupancy: .52, }); } controls.add.onclick = () => { const id = `signal-${++serial}`; const [x, y] = positions[(serial - 1) % positions.length]; activeId = id; const node = { id, x, y, size: 9, color: '#10b981', label: `Signal ${serial}` }; activeEdgeId = `signal-link-${serial}`; const edge = { id: activeEdgeId, source: 'case', target: id, color: '#64748b' }; // Add the node and its relationship in one named transaction. Undo cannot leave a dangling // edge or reverse only half of the user's completed action. og.edit('add connected case', (draft) => { draft.addNodes([node]); draft.addEdges([edge]); }); }; controls.remove.onclick = () => { if (activeId) og.removeNodes([activeId]); }; controls.edge.onclick = () => { og.removeEdgeById('case-device'); }; // The same public history methods reverse or replay both atomic transactions and individual // graph mutations; stable ids and edge attributes are restored exactly. controls.undo.onclick = () => { og.history.undo(); }; controls.redo.onclick = () => { og.history.redo(); }; og.history.onChange(update); update();