Skip to content
  1. Examples

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,
  onGroupUpdate: (_, nodes) =>
    ogma.layouts.force({
      nodes,
      edgeStrength: 2,
      gravity: 0.1
    })
});

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;
}