Skip to content
  1. Examples
  2. Transformations

Visual grouping manual

This is an example of what one could achieve with the grouping feature.

Drag nodes on each other to group them.
Drag a group over a node to add it into the group.
Drag a group over another to merge them.

ts
import Ogma, { Color, NodeList, Node, NodeId } from '@linkurious/ogma';

const ogma = new Ogma({
  container: 'graph-container'
});

// Create an instance of Ogma and bind it to the graph-container.
let groupCount = 1;
const groups = new Map<NodeId, string>();
const colors = new Map<NodeId, Color>();

const getRandomColor = (): Color => {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
};

ogma.styles.addNodeRule(node => node.isVirtual(), {
  color: node => colors.get(node.getId()),
  opacity: 0.5
});

const graph = await ogma.generate.random({ nodes: 10, edges: 10 });
await ogma.setGraph(graph);
await ogma.layouts.force({ gpu: true, locate: true });

const randomNode = ogma
  .getNodes()
  .get(Math.floor(ogma.getNodes().size * Math.random()));
let distance = Infinity;
const closest = ogma.getNodes().reduce((acc, node) => {
  const pos = node.getPosition();
  const origin = randomNode.getPosition();
  const currDistance = Ogma.geometry.distance(origin.x, origin.y, pos.x, pos.y);
  if (currDistance < distance && node !== randomNode) {
    distance = currDistance;
    return node;
  }
  return acc;
}, randomNode);
console.log(randomNode.getId(), 'closest', closest.getId());
const initialGroupName = `group-${groupCount++}`;
groups.set(randomNode.getId(), initialGroupName);
groups.set(closest.getId(), initialGroupName);
colors.set(initialGroupName, getRandomColor());

const grouping = ogma.transformations.addNodeGrouping({
  selector: node => groups.has(node.getId()),
  groupIdFunction: node => groups.get(node.getId())!,
  nodeGenerator: (nodes, id) => ({ id }),
  showContents: true,
  duration: 300,
  onGroupUpdate: () => ({
    layout: 'force',
    params: {
      edgeStrength: 2,
      gravity: 0.1
    }
  })
});
ogma.transformations.onGroupsUpdated(() => {
  return {
    layout: 'force'
  };
});

ogma.events.on('dragEnd', ({ target }) => {
  if (!target || !target.isNode) return;
  const parent = target.getMetaNode();
  const { x, y } = (parent || target).getPositionOnScreen();
  const r =
    Number((parent || target).getAttribute('radius')) * ogma.view.getZoom();

  const overlapNodes = ogma.getNodes().filter(node => {
    const pos = node.getPositionOnScreen();
    const dist =
      Ogma.geometry.distance(x, y, pos.x, pos.y) -
      +node.getAttribute('radius') * ogma.view.getZoom();
    return dist < r;
  });

  if (overlapNodes.size < 2) return;

  const groupid =
    overlapNodes
      .map(node => (node.isVirtual() ? node.getId() : groups.get(node.getId())))
      .filter(e => e)[0] || `group-${groupCount++}`;
  if (!colors.has(groupid)) {
    colors.set(groupid, getRandomColor());
  }
  overlapNodes.forEach(node => {
    groups.set(node.getId(), groupid as string);
  });
  grouping.refresh();
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}