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
ogma.transformations.addNodeGrouping({
  groupIdFunction: node => node.getData('type'),
  nodeGenerator: (_, groupId) => ({
    attributes: {
      color: 'white',
      innerStroke: groupId
    },
    data: {
      type: groupId,
      open: groupId === 'red'
    }
  }),
  padding: 10,
  duration: 1000,
  easing: 'quadraticInOut',
  onGroupUpdate(group, nodes) {
    if (group.getData('type') === 'red') return computePositions(group, nodes);
  },
  showContents: node => node.getData('open')
});

ogma.transformations.onGroupsUpdated(() => {
  return {
    layout: 'force'
  };
});

/**
 * 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 Promise.resolve(subNodePositions);
}

// expand or collapse the group on double click
ogma.events.on('doubleclick', async ({ target }) => {
  if (!target || !target.isNode || !target.isVirtual()) return;
  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,
      onExpand: () => {
        target.setData('open', true);
      },
      computeSubNodesPosition: (group, subNodes) =>
        computePositions(group, subNodes)
    });
});
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'
    }
  });
};