ssi-patterns / ssi-pattern-connections-visualization / examples / mouse-manipulations / index.ts
index.ts
Raw
import Graph from "graphology";
import Sigma from "sigma";
import chroma from "chroma-js";
import { v4 as uuid } from "uuid";
import ForceSupervisor from "graphology-layout-force/worker";

// Retrieve the html document for sigma container
const container = document.getElementById("sigma-container") as HTMLElement;

// Create a sample graph
const graph = new Graph();
graph.addNode("n1", { x: 0, y: 0, size: 10, color: chroma.random().hex() });
graph.addNode("n2", { x: -5, y: 5, size: 10, color: chroma.random().hex() });
graph.addNode("n3", { x: 5, y: 5, size: 10, color: chroma.random().hex() });
graph.addNode("n4", { x: 0, y: 10, size: 10, color: chroma.random().hex() });
graph.addEdge("n1", "n2");
graph.addEdge("n2", "n4");
graph.addEdge("n4", "n3");
graph.addEdge("n3", "n1");

// Create the spring layout and start it
const layout = new ForceSupervisor(graph, { isNodeFixed: (_, attr) => attr.highlighted });
layout.start();

// Create the sigma
const renderer = new Sigma(graph, container);

//
// Drag'n'drop feature
// ~~~~~~~~~~~~~~~~~~~
//

// State for drag'n'drop
let draggedNode: string | null = null;
let isDragging = false;

// On mouse down on a node
//  - we enable the drag mode
//  - save in the dragged node in the state
//  - highlight the node
//  - disable the camera so its state is not updated
renderer.on("downNode", (e) => {
  isDragging = true;
  draggedNode = e.node;
  graph.setNodeAttribute(draggedNode, "highlighted", true);
});

// On mouse move, if the drag mode is enabled, we change the position of the draggedNode
renderer.getMouseCaptor().on("mousemovebody", (e) => {
  if (!isDragging || !draggedNode) return;

  // Get new position of node
  const pos = renderer.viewportToGraph(e);

  graph.setNodeAttribute(draggedNode, "x", pos.x);
  graph.setNodeAttribute(draggedNode, "y", pos.y);

  // Prevent sigma to move camera:
  e.preventSigmaDefault();
  e.original.preventDefault();
  e.original.stopPropagation();
});

// On mouse up, we reset the autoscale and the dragging mode
renderer.getMouseCaptor().on("mouseup", () => {
  if (draggedNode) {
    graph.removeNodeAttribute(draggedNode, "highlighted");
  }
  isDragging = false;
  draggedNode = null;
});

// Disable the autoscale at the first down interaction
renderer.getMouseCaptor().on("mousedown", () => {
  if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox());
});

//
// Create node (and edge) by click
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//

// When clicking on the stage, we add a new node and connect it to the closest node
renderer.on("clickStage", ({ event }: { event: { x: number; y: number } }) => {
  // Sigma (ie. graph) and screen (viewport) coordinates are not the same.
  // So we need to translate the screen x & y coordinates to the graph one by calling the sigma helper `viewportToGraph`
  const coordForGraph = renderer.viewportToGraph({ x: event.x, y: event.y });

  // We create a new node
  const node = {
    ...coordForGraph,
    size: 10,
    color: chroma.random().hex(),
  };

  // Searching the two closest nodes to auto-create an edge to it
  const closestNodes = graph
    .nodes()
    .map((nodeId) => {
      const attrs = graph.getNodeAttributes(nodeId);
      const distance = Math.pow(node.x - attrs.x, 2) + Math.pow(node.y - attrs.y, 2);
      return { nodeId, distance };
    })
    .sort((a, b) => a.distance - b.distance)
    .slice(0, 2);

  // We register the new node into graphology instance
  const id = uuid();
  graph.addNode(id, node);

  // We create the edges
  closestNodes.forEach((e) => graph.addEdge(id, e.nodeId));
});