<div id="sigma-container"></div>
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 SNAP_SIZE = GRID_SIZE / 4;
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}`;
// 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, {
itemSizesReference: "positions",
// 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);
// 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,
backdropColor: "#ffffff",
backdropShadowColor: "transparent",
backdropBorderColor: "black",
when: (_, state, graphState) =>
(!!graphState.selectionSize && !state.isSelected) ||
(!graphState.selectionSize && graphState.isDragging && !state.isDragged),
backdropVisibility: "visible",
// Dragged nodes appear on top
color: (_, _state, graphState) => (graphState.selectionSize || graphState.isDragging ? "lightgrey" : "grey"),
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
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 });