Skip to content
  1. Examples

Expand/collapse in place new

Double-click group nodes to expand or collapse them.

ts
import Ogma, { Node, NodeList } from '@linkurious/ogma';
import { addStyles } from './styles';

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

const NodeTypes = [
  'red',
  'green',
  'blue',
  'orange',
  'purple',
  'pink',
  'brown',
  'grey',
  'black'
];

addStyles(ogma);

// generate the graph
const graph = await ogma.generate.random({ nodes: 100, edges: 140 });
graph.nodes.forEach(node => {
  node.attributes = node.attributes || {};
  node.attributes.radius = 10;
  node.data = {
    type: NodeTypes[Math.floor(Math.random() * NodeTypes.length)]
  };
});
await ogma.setGraph(graph);
await ogma.view.locateGraph();

// add the node grouping
await ogma.transformations
  .addNodeGrouping({
    groupIdFunction: node => node.getData('type'),
    nodeGenerator: (_, groupId) => ({
      attributes: {
        color: 'white',
        innerStroke: groupId
      },
      data: {
        type: groupId,
        open: groupId === 'red'
      }
    }),
    padding: 10,
    onGroupUpdate(group, nodes) {
      if (group.getData('type') === 'red')
        return computePositions(group, nodes);
    },
    showContents: node => node.getData('open')
  })
  .whenApplied();
await ogma.layouts.force({ locate: true });

/**
 * This is how we compute the positions of the subnodes within the group.
 * @param target virtual node
 * @param subNodes subnodes of the group
 * @returns subnodes positions
 */
async function computePositions(target: Node, subNodes: NodeList) {
  const targetPosition = target.getPosition();
  const origin = target.getPosition();

  // circle pack the subnodes
  const subNodePositions = await ogma.algorithms.circlePack({
    origin,
    nodes: subNodes,
    margin: 10,
    dryRun: true
  });

  // we want to keep the barycenter of the subnodes at the target position
  const centroid = Ogma.geometry.computeCentroid(subNodePositions);
  const offsetX = targetPosition.x - centroid.x;
  const offsetY = targetPosition.y - centroid.y;
  subNodePositions.forEach(p => {
    p.x += offsetX;
    p.y += offsetY;
  });

  return subNodePositions;
}

// expand or collapse the group on double click
ogma.events.on('doubleclick', async ({ target }) => {
  if (target && target.isNode && target.isVirtual()) {
    const isOpen = target.getData('open');

    // if the group is open, collapse it, otherwise expand it
    if (isOpen)
      ogma.transformations.collapseGroup({
        node: target,
        onCollapse: () => {
          target.setData('open', false);
        }
      });
    else
      ogma.transformations.expandGroup({
        node: target,
        computeSubNodesPosition: (target, subNodes) =>
          computePositions(target, subNodes),
        onExpand: () => {
          target.setData('open', true);
        }
      });
  }
});
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;
}
ts
import Ogma from '@linkurious/ogma';

export const addStyles = (ogma: Ogma) => {
  ogma.styles.setSelectedNodeAttributes({
    outline: false
  });

  // color the types
  ogma.styles.addRule({
    nodeAttributes: {
      color: n => (n.isVirtual() ? 'white' : n.getData('type')),
      innerStroke: {
        minVisibleSize: 0,
        width: 2
      },
      badges: {
        topRight: n => {
          if (!n.isVirtual() || n.getData('open')) return;
          const color = n.getData('type');
          return {
            text: {
              content: n.getSubNodes()?.size,
              color: 'white'
            },
            color,
            stroke: {
              width: 0
            }
          };
        }
      }
    },
    edgeAttributes: {
      width: 1,
      detectable: false,
      color: '#ccc'
    }
  });
};