Skip to content
This is the alpha v4 version website. Looking for the v3 documentation?

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 dragged
const 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 (isSelected on nodes, selectionSize on 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 nodes
for (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-select
renderer.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 selection
renderer.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 node
renderer.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:

EventDescription
nodeDragStartFired when drag begins. Call preventSigmaDefault() to cancel.
nodeDragFired on every pointer move during drag.
nodeDragEndFired when the pointer is released.
  • node is the node under the pointer (the one the user grabbed).
  • allDraggedNodes is the full set of nodes being moved (from getDraggedNodes).