Skip to content
  1. Examples

Incremental ungrouping

This example shows how to use the incremental parametter in the force layout. It allows to easilly expand and collapse the nodes in the graph.

Double-click green nodes to incrementally expand the grouped nodes.

Double-click expanded nodes to collapse their leafs.

ts
import Ogma, { Node, RawGraph } from '@linkurious/ogma';

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

const duration = 200;

const isRoot = (n: Node) => n.getDegree() > 3;

// Generates graph with high-degree nodes
const generateDenseTree = (
  N: number,
  maxR = 5,
  minR = 5
): Promise<RawGraph> => {
  return new Promise(resolve => {
    const nodes = new Array(N).fill(0).map((_, i) => {
      return {
        id: i,
        attributes: {
          text: 'Node #' + i,
          radius: minR + Math.random() * (maxR - minR),
          x: -50 + Math.random() * 100,
          y: -50 + Math.random() * 100
        }
      };
    });
    const edges = new Array(N - 1).fill(0).map((_, i) => {
      return {
        id: i,
        source: Math.floor(Math.sqrt(i) / 2),
        target: i + 1
      };
    });

    resolve({ nodes: nodes, edges: edges });
  });
};

// collapses node leafs
const collapseNode = (node: Node, duration: number = 0) => {
  const group = getTransformationById(+node.getId());
  if (group) {
    group.enable();
    return group;
  }
  const leafs = node
    .getAdjacentNodes()
    .filter(node => !node.isVirtual() && !node.getData('isCore'));
  const nodesToGroup = new Set(leafs.getId());
  nodesToGroup.add(node.getId());

  // add collapse grouping rule
  return ogma.transformations.addNodeGrouping({
    restorePositions: false,
    selector: node => nodesToGroup.has(node.getId()),
    nodeGenerator: (nodes, id, transformation) => ({
      id: 'p' + node.getId(),
      attributes: {
        text: 'Parent ' + node.getId(),
        color: '#008800'
      },
      data: {
        groupId: transformation.getId(),
        centralNode: node.getId(), // link to central node,
        isMetaNode: true
      }
    }),
    duration
  });
};

const getTransformationById = (id: number) => {
  const transformations = ogma.transformations.getList();
  for (let i = 0; i < transformations.length; i++) {
    if (transformations[i].getId() === id) {
      return transformations[i];
    }
  }
  return null;
};

// Expands high-degree node
const expandNode = (node: Node, duration = 0) => {
  // retrieve grouping data
  const nodes = node.getSubNodes()!;
  const groupId = node.getData('groupId');
  // remove group and use incremental positioning layout
  return getTransformationById(groupId)!
    .disable()
    .then(() => {
      return ogma.layouts.force({
        nodes,
        incremental: true,
        duration
      });
    });
};

// Utility function to group "flower"-nodes for this demo
const collapseFlowerNodes = () => {
  const coreNodes = ogma.getNodes().filter(isRoot);
  coreNodes.setData('isCore', new Array(coreNodes.size).fill(true));
  coreNodes.setAttributes({ color: 'green' });

  // collapse flower-nodes
  return Promise.all(
    coreNodes.map(n => {
      return collapseNode(n).whenApplied();
    })
  );
};

// generate graph and collapse the nodes
const graph = await generateDenseTree(300);
await ogma.setGraph(graph);
await collapseFlowerNodes();
await ogma.layouts.force({ locate: true });
await ogma.view.setZoom(ogma.view.getZoom() * 0.25);
await new Promise(resolve => setTimeout(resolve, 2000));
expandNode(ogma.getNode('p8')!, duration);

// expand or collapse on double click
ogma.events.on('doubleclick', evt => {
  if (evt.target && evt.target.isNode) {
    const node = evt.target;
    if (node.getData('isMetaNode')) {
      // expand grouped node
      expandNode(node, duration).then(() => {
        ogma.view.locateGraph({ duration });
      });
    } else if (node.getData('isCore')) {
      // collapse high-degree node
      collapseNode(node, duration);
    }
  }
});
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;
}