Drag and drop
Sigma includes built-in node drag support via the enableNodeDrag setting. Combined with autoRescale: "once", it
handles coordinate conversion, camera panning suppression, and state management automatically.
Live example
In this example, you can drag any node to reposition it. The force layout pins dragged nodes, and resumes when released.
import Graph from "graphology";import ForceSupervisor from "graphology-layout-force/worker";import Sigma from "sigma";
import data from "../_data/data.json";import { DEFAULT_STYLES } from "sigma/types";
const container = document.getElementById("sigma-container") as HTMLElement;
const graph = new Graph();graph.import(data);
const renderer = new Sigma(graph, container, { settings: { autoRescale: "once", itemSizesReference: "positions", enableNodeDrag: true, getDraggedNodes: (node) => (graph.getNodeAttribute(node, "label").includes("b") ? [] : [node]), }, styles: { nodes: [ DEFAULT_STYLES.nodes, { size: (attributes) => attributes.size / 2, cursor: "grab", }, { whenState: "isDragged", then: { labelVisibility: "visible", backdropVisibility: "visible", cursor: "grabbing", }, }, ], stage: [ { whenState: "isDragging", then: { cursor: "grabbing" }, }, ], },});
// Force layout: pin nodes while they are being draggedconst layout = new ForceSupervisor(graph, { settings: { repulsion: 0.2, attraction: 0.0002, }, isNodeFixed: (node) => renderer.getNodeState(node).isDragged,});layout.start();Basic setup
Enable drag with two settings:
const renderer = new Sigma(graph, container, { settings: { autoRescale: "once", enableNodeDrag: true, },});autoRescale: "once" freezes the coordinate system after the first render, preventing the viewport from rescaling as
nodes are dragged outside the original bounding box.
Styling dragged nodes
Sigma exposes isDragged as a node state flag and isDragging as a graph state flag, both usable in the styles API:
const renderer = new Sigma(graph, container, { settings: { autoRescale: "once", enableNodeDrag: true, }, styles: { nodes: { // Dragged nodes appear on top with their label visible depth: { whenState: "isDragged", then: "topNodes", else: "nodes", }, labelVisibility: { whenState: "isDragged", then: "visible", else: "auto", }, }, },});Integration with force layouts
Since sigma tracks the isDragged state on each node, the force layout can read it directly via getNodeState(). No
event handlers or extra attributes is needed:
import ForceSupervisor from "graphology-layout-force/worker";
const renderer = new Sigma(graph, container, { settings: { autoRescale: "once", enableNodeDrag: true },});
const layout = new ForceSupervisor(graph, { isNodeFixed: (node) => renderer.getNodeState(node).isDragged,});layout.start();Cancelling drag for specific nodes
The getDraggedNodes setting allows telling Sigma which nodes should be dragged. This can be used to drag multiple
nodes at once, or in the contrary, prevent some nodes to be dragged:
const renderer = new Sigma(graph, container, { settings: { // ... getDraggedNodes: (node) => (graph.getNodeAttribute(node, "locked") ? [] : [node]), // ... },});Alternatively, you can use preventSigmaDefault() on the nodeDragStart event to make nodes non-draggable:
renderer.on("nodeDragStart", ({ node, preventSigmaDefault }) => { if (graph.getNodeAttribute(node, "locked")) { preventSigmaDefault(); }});Multi-node drag with selection and grid snapping
The following example combines several drag features on a grid graph:
- Click a node to select it, shift+click for multi-select, click the stage to clear
- Drag a selected node to move the entire selection
- Positions snap to a sub-grid via
dragPositionToAttributes - Selection state is managed through custom sigma state types (
isSelectedon nodes,selectionSizeon the graph) - Non-selected nodes and edges are dimmed when a selection exists
import Graph from "graphology";import Sigma from "sigma";import type { BaseEdgeState, BaseGraphState, BaseNodeState } from "sigma/types";import { DEFAULT_STYLES } from "sigma/types";import type { Attributes } from "graphology-types";
const GRID_SIZE = 8;const SNAP_SIZE = GRID_SIZE / 4;const GRID_COLS = 6;const GRID_ROWS = 6;
const container = document.getElementById("sigma-container") as HTMLElement;const graph = new Graph();
// Create a grid of nodesfor (let row = 0; row < GRID_ROWS; row++) { for (let col = 0; col < GRID_COLS; col++) { const id = `${row}-${col}`; graph.addNode(id, { x: col * GRID_SIZE, y: row * GRID_SIZE, }); }}
// Connect neighbors (right and down)for (let row = 0; row < GRID_ROWS; row++) { for (let col = 0; col < GRID_COLS; col++) { const id = `${row}-${col}`; if (col < GRID_COLS - 1) graph.addEdge(id, `${row}-${col + 1}`); if (row < GRID_ROWS - 1) graph.addEdge(id, `${row + 1}-${col}`); }}
const renderer = new Sigma(graph, container, { customNodeState: { isSelected: false, }, customGraphState: { selectionSize: 0, }, settings: { autoRescale: "once", itemSizesReference: "positions", enableNodeDrag: true, // Multi-node drag: if the dragged node is selected, drag the whole selection getDraggedNodes: (node) => { if (renderer.getNodeState(node).isSelected) { return graph.filterNodes((n: string) => renderer.getNodeState(n).isSelected); } return [node]; }, // Snap positions to sub-grid dragPositionToAttributes: (position) => ({ x: Math.round(position.x / SNAP_SIZE) * SNAP_SIZE, y: Math.round(position.y / SNAP_SIZE) * SNAP_SIZE, }), }, styles: { nodes: [ DEFAULT_STYLES.nodes, { color: "grey", size: 2, backdropColor: "#ffffff", backdropShadowColor: "transparent", backdropBorderColor: "black", backdropBorderWidth: 2, backdropPadding: 4, cursor: "grab", }, { when: (_, state, graphState) => (!!graphState.selectionSize && !state.isSelected) || (!graphState.selectionSize && graphState.isDragging && !state.isDragged), then: { color: "lightgrey", }, }, // Selected nodes { whenState: "isSelected", then: { backdropVisibility: "visible", }, }, // Dragged nodes appear on top { whenState: "isDragged", then: { depth: "topNodes", cursor: "grabbing", }, }, ], edges: { size: 1, color: (_, _state, graphState) => (graphState.selectionSize || graphState.isDragging ? "lightgrey" : "grey"), }, stage: { whenState: "isDragging", then: { cursor: "grabbing" }, }, },});
// Click to toggle selection, shift+click for multi-selectrenderer.on("clickNode", ({ node, event }) => { const isSelected = renderer.getNodeState(node).isSelected; let selectionSize = renderer.getGraphState().selectionSize ?? 0;
if (!event.original.shiftKey) { // Clear previous selection selectionSize = 0; graph.forEachNode((n) => { if (renderer.getNodeState(n).isSelected) { renderer.setNodeState(n, { isSelected: false }); } }); }
selectionSize += !isSelected ? 1 : -1; renderer.setNodeState(node, { isSelected: !isSelected }); renderer.setGraphState({ selectionSize: Math.max(selectionSize, 0) });});
// Click on stage clears selectionrenderer.on("clickStage", () => { renderer.setGraphState({ selectionSize: 0 }); graph.forEachNode((n) => { if (renderer.getNodeState(n).isSelected) { renderer.setNodeState(n, { isSelected: false }); } });});
// Reset selection state on start dragging an unselected noderenderer.on("nodeDragStart", ({ node }) => { if (!renderer.getNodeState(node).isSelected) { graph.forEachNode((n) => { renderer.setNodeState(n, { isSelected: false }); }); renderer.setGraphState({ selectionSize: 0 }); }});Custom state types
The example extends sigma’s built-in state with application-specific flags:
const renderer = new Sigma(graph, container, { // ... customNodeState: { isSelected: false, }, customGraphState: { selectionSize: 0, }, // ...});These custom flags can then be used in style predicates ("isSelected") and read/written via getNodeState() / setNodeState() / setGraphState().
The getDraggedNodes setting
This callback connects your selection state to drag. When the dragged node is selected, all selected nodes move together:
const renderer = new Sigma(graph, container, { settings: { // ... getDraggedNodes: (node) => { if (renderer.getNodeState(node).isSelected) { return graph.filterNodes((n: string) => renderer.getNodeState(n).isSelected); } return [node]; }, // ... },});Position constraints
dragPositionToAttributes transforms positions before they are written to the graph. It receives the computed
graph-space position and returns the attributes to set:
const SNAP_SIZE = 0.5;const renderer = new Sigma(graph, container, { settings: { // ... dragPositionToAttributes: (position) => ({ x: Math.round(position.x / SNAP_SIZE) * SNAP_SIZE, y: Math.round(position.y / SNAP_SIZE) * SNAP_SIZE, }), // ... },});When position attributes differ from x/y (e.g. via styles mapping x: { attribute: "lng" }), sigma infers the
correct attribute names automatically. Use dragPositionToAttributes when you need to transform values (snapping,
scaling) or write to different attributes than the ones used for reading.
Events
Three events are available for observing the drag lifecycle. All payloads include node, allDraggedNodes, and the
wrapped MouseCoords event:
| Event | Description |
|---|---|
nodeDragStart | Fired when drag begins. Call preventSigmaDefault() to cancel. |
nodeDrag | Fired on every pointer move during drag. |
nodeDragEnd | Fired when the pointer is released. |
nodeis the node under the pointer (the one the user grabbed).allDraggedNodesis the full set of nodes being moved (fromgetDraggedNodes).