JavaScript is required

Context-menu Node and Line Editing

This example turns relation-graph's right-click event into a compact editing workflow for adding nodes, starting new lines from existing nodes, and deleting targeted objects. It uses a view-layer menu overlay, coordinate conversion for canvas insertion, runtime graph mutation APIs, manual relayout, and shared canvas settings or image export utilities.

Context-menu Node and Line Editing in a Tree Graph

What This Example Builds

This example builds a small tree graph inside a full-screen white workspace and adds a custom right-click menu on top of it. Users can right-click empty canvas space to add a node, right-click an existing node to start a new connection, and right-click a node or line to delete that specific object. A floating helper window stays available for relayout, canvas settings, and image export.

The main point of the demo is not the starter dataset itself. The useful part is the editing flow: one transient menu handles object-aware actions without introducing a full toolbar-based editor shell or a larger persistence layer.

How the Data Is Organized

The starter graph is declared inline as staticJsonData. It uses standard relation-graph JSON with rootId, nodes, and lines, and the initial content is intentionally small: one root node, several surrounding nodes, and three labeled relationships.

Before setJsonData runs, the example performs one important preprocessing step. It maps over the starter lines and assigns an id to any line that does not already have one. That normalization matters because later menu actions remove lines by ID. After initialization, new nodes and lines are created directly on the live graph instance rather than by rebuilding the whole JSON payload.

In a real application, the same structure could represent organization links, ownership relationships, dependency maps, investigation boards, or lightweight asset-maintenance graphs. The example keeps the payload generic so the interaction pattern stays easy to reuse.

How relation-graph Is Used

RGProvider wraps the page so relation-graph hooks can resolve the active graph instance. RelationGraph is configured with a tree layout, RGJunctionPoint.border, defaultLineTextOnPath: true, and explicit horizontal and vertical gaps. Those settings keep the starter graph compact while leaving enough spacing for runtime additions and menu-driven relayout.

RGHooks.useGraphInstance() is the core integration point. The demo uses it to load data with setJsonData, frame the graph with moveToCenter() and zoomToFit(), convert browser events to graph-view coordinates with getViewXyByEvent(...), convert stored menu positions back to canvas coordinates with getCanvasXyByViewXy(...), create IDs with generateNewNodeId() and generateNewUUID(...), mutate the graph with addNodes, addLines, removeNodeById, and removeLineById, and rerun layout with updateOptions(...) plus doLayout().

The custom menu is rendered through RGSlotOnView, which makes it part of the graph’s overlay layer instead of a separate page-level popup. The onContextmenu handler stores the clicked target type and the menu position, then the slot branches between canvas, node, and line actions. This is why the same right-click entry point can create nodes, launch interactive line plotting, or delete the currently selected object.

The page also reuses a shared DraggableWindow subcomponent. In this example it provides instructions, an Organize Layout button, a settings panel backed by RGHooks.useGraphStore(), and image export through prepareForImageGeneration() and restoreAfterImageGeneration(). The local SCSS file adds a smaller node-label font and an orange highlight treatment for checked lines.

Key Interactions

  • Right-clicking the canvas opens a floating menu at the pointer position and inserts a new node at the matching canvas coordinate.
  • Right-clicking a node opens a node-specific menu action that starts startCreatingLinePlot(...), so the user can choose a target node for a new relationship.
  • Right-clicking a node or line exposes a targeted delete action that removes only that object from the live graph instance.
  • Clicking elsewhere dismisses the menu through a temporary global click listener.
  • Clicking Organize Layout reapplies the tree layout and runs doLayout() again after graph edits.
  • Opening the helper window settings lets the user change wheel and drag behavior and export the current graph as an image.

Key Code Fragments

This block shows the tree layout and default graph options that define the base workspace.

const graphOptions: RGOptions = {
    debug: false,
    defaultJunctionPoint: RGJunctionPoint.border,
    defaultExpandHolderPosition: 'right',
    defaultLineTextOnPath: true,
    layout: {
        layoutName: 'tree',
        treeNodeGapV: 20,
        treeNodeGapH: 150
    }
};

This block shows the initialization step that normalizes line IDs before the graph is loaded.

const myJsonData: RGJsonData = {
    ...staticJsonData,
    lines: staticJsonData.lines.map((line, index) => ({
        ...line,
        id: line.id || `line_auto_${index}`
    }))
};
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();

This block shows the right-click handler capturing both the clicked object type and the view-space position used to place the menu.

const onContextmenu = (e: RGUserEvent, objectType: string, object: any) => {
    const xyOnGraphView = graphInstance.getViewXyByEvent(e);
    setMenuPos(xyOnGraphView);
    setCurrentObj({ type: objectType, data: object });
    setShowNodeTipsPanel(true);

    const hideMenu = () => {
        setShowNodeTipsPanel(false);
        window.removeEventListener('click', hideMenu);
    };
    window.addEventListener('click', hideMenu);
};

This block shows how canvas insertion uses the stored menu position after converting it from view coordinates back into canvas coordinates.

{currentObj.type === 'canvas' && (
    <button
        className="flex items-center h-8 pl-2 hover:bg-green-100 text-sm text-gray-700 rounded transition-colors text-left"
        onClick={() => {
            const xyOnCanvas = graphInstance.getCanvasXyByViewXy(menuPos);
            addNodeOnCanvas(xyOnCanvas);
        }}
    >
        Add New Node
    </button>
)}

This block shows the node-started line-authoring flow, including runtime line ID generation when the target node is chosen.

const startAddLineFromNode = (e: React.MouseEvent | React.TouchEvent) => {
    const newLineTemplate: JsonLineLike = { lineWidth: 2, color: '#cebf88ff', fontColor: '#cebf88ff', text: 'New Line' };
    graphInstance.startCreatingLinePlot(e.nativeEvent, {
        template: newLineTemplate,
        fromNode: currentObj.data,
        onCreateLine: (from, to) => {
            if (to && 'id' in to) {
                const lineId = graphInstance.generateNewUUID(5);
                graphInstance.addLines([{ id: lineId, ...newLineTemplate, from: from.id, to: to.id, text: `New Line-${lineId}` }]);
            }
        }
    });
};

This block shows the same menu branching to targeted deletion for nodes and lines.

const handleDelete = () => {
    if (currentObj.type === 'node') {
        graphInstance.removeNodeById(currentObj.data.id);
    } else if (currentObj.type === 'line') {
        graphInstance.removeLineById(currentObj.data.id);
    }
};

This block shows the helper window reapplying layout after graph edits.

const layoutGraph = () => {
    graphInstance.updateOptions({
        layout: {
            layoutName: 'tree'
        }
    });
    graphInstance.doLayout();
};

This block shows the shared settings panel reading graph state and exporting the prepared canvas as an image.

const graphInstance = RGHooks.useGraphInstance();
const { options } = RGHooks.useGraphStore();
const dragMode = options.dragEventAction;
const wheelMode = options.wheelEventAction;

const downloadImage = async () => {
    const canvasDom = await graphInstance.prepareForImageGeneration();
    let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
    if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
        graphBackgroundColor = '#ffffff';
    }
    const imageBlob = await domToImageByModernScreenshot(canvasDom, {
        backgroundColor: graphBackgroundColor
    });
    if (imageBlob) {
        downloadBlob(imageBlob, 'my-image-name');
    }
    await graphInstance.restoreAfterImageGeneration();
};

What Makes This Example Distinct

The comparison data makes this a focused reference for right-click editing rather than a general-purpose editor demo. Its strongest distinguishing trait is that one onContextmenu entry point covers all three target types that matter in this workflow: canvas, node, and line. That means the same transient overlay can add a node, start a new connection, or delete the selected object without switching tools.

Compared with node-menu, this demo turns a context menu into real graph mutation instead of read-only feedback actions. Compared with my-graph-app, it is intentionally narrower: it keeps the same core right-click mechanics but drops persistence and a broader toolbar so the menu pattern is easier to extract. Compared with amount-summarizer, relayout-after-add-nodes, and line-vertex-on-node, the emphasis is not typed node editing, app-owned tree rebuilding, or advanced endpoint controls. The emphasis is a compact, target-sensitive menu that covers add, connect, delete, and relayout in one small tree workspace.

Another useful detail is the coordinate split. The menu is positioned in graph-view space, but new nodes are created in canvas space after conversion. Combined with line-ID normalization on initialization and the shared floating helper window, that gives this example a distinctive mix of lightweight editing behavior and practical maintenance utilities.

Where Else This Pattern Applies

This pattern transfers well to internal tools where users need a few direct authoring actions but not a full workflow editor. Examples include organization maintenance views, equipment or dependency cleanup tools, knowledge-map curation, incident investigation boards, and topology triage screens where people need to add a missing node, connect two existing objects, remove an invalid relation, and rerun layout quickly.

It also works as a bridge pattern when a team currently has a read-only relation graph and wants to introduce limited editing in stages. The same approach can later expand into validation, persistence, property forms, or role-based permissions, but this example keeps the reusable core small: context-sensitive right-click actions attached directly to the live graph instance.