Skip to content
  1. Examples

Visual grouping expand/collapse new

This example shows how to load nodes asynchronously into visual groups. Double-click on the node to expand or collapse it. It will load its children asynchronously and run a recursive layout inside of the group to remove overlap.

ts
import Ogma from '@linkurious/ogma';
import { init, API } from './api';
import { ND, ED } from './types';
import { colors, greyColors } from './colors';

const ogma = new Ogma<ND, ED>({
  container: 'graph-container'
});

ogma.styles.addNodeRule({
  color: node => {
    const level = node.getData('level');
    return node.isVirtual() ? greyColors[level] : colors[level];
  }
});

const api: API = await init(ogma);
const subgraph = await api.getSubgraph(0, true);
await ogma.addGraph(subgraph);
await ogma.layouts.hierarchical({ locate: true });
const drilldown = ogma.transformations.addDrillDown({
  onGetSubgraph: node => {
    const position = node.getPosition();
    return api.getSubgraph(node.getId(), true).then(subgraph => {
      subgraph.nodes.forEach(
        n => (n.attributes = { ...(n.attributes || {}), ...position })
      );
      return subgraph;
    });
  },
  nodeGenerator: (nodes, id) => ({ id, data: { open: true } }),
  showContents: target => target.getData('open'),
  onGroupUpdate: (_, nodes) => ogma.layouts.hierarchical({ nodes }),
  duration: 250
});
ogma.transformations.onGroupsUpdated(() => ogma.layouts.hierarchical());
ogma.events.on('doubleclick', ({ target }) => {
  if (!target || !target.isNode || target.getId() === 0) return;
  // the group is already opened
  if (ogma.getNode(`ogma-group-${target.getId()}`)) return;
  if (target.isVirtual()) {
    target.setData('open', !target.getData('open'));
  } else {
    drilldown.drill(target);
  }
});
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;
}

button {
  position: absolute;
  top: 10px;
  left: 10px;
}

#custom-group-btn {
  top: 40px;
}
ts
import Ogma, { RawNode, RawEdge, RawGraph, NodeId } from '@linkurious/ogma';
import { Awaited } from './types';

let db: {
  nodes: RawNode[];
  edges: RawEdge[];
  nodesMap: Record<NodeId, RawNode>;
};

export const init = (ogma: Ogma, children = 3, levels = 6) =>
  ogma.generate
    .balancedTree({
      children,
      height: levels
    })
    .then(tree => {
      const nodesMap = tree.nodes.reduce(
        (acc, node) => {
          node.attributes = { ...node.attributes, x: 0, y: 0 };
          acc[node.id!] = node;
          return acc;
        },
        {} as Record<NodeId, RawNode>
      );
      tree.edges.forEach(edge => {
        const { source, target } = edge;
        const sourceNode = nodesMap[source];
        const targetNode = nodesMap[target];
        sourceNode.data = sourceNode.data || {};
        sourceNode.data.children = sourceNode.data.children || [];
        targetNode.data = targetNode.data || {};

        const sourceLevel = (sourceNode.data.level =
          sourceNode.data.level || 0);
        const targetLevel = sourceLevel + 1;
        sourceNode.data = {
          ...sourceNode.data,
          level: sourceLevel,
          children: [...(sourceNode.data.children as NodeId[]), target]
        };
        targetNode.data = {
          ...targetNode.data,
          level: targetLevel,
          parent: source
        };
      });
      db = { ...tree, nodesMap };

      return {
        getSubgraph(root: NodeId, includeRoot = false): Promise<RawGraph> {
          const edges = db.edges.filter(edge => edge.source === root);
          const nodes = edges.map(edge => db.nodesMap[edge.target]);
          if (includeRoot) nodes.push(db.nodesMap[root]);
          return Promise.resolve({ nodes, edges });
        },
        getGraph() {
          return Promise.resolve({ edges: db.edges, nodes: db.nodes });
        }
      };
    });

export type API = Awaited<ReturnType<typeof init>>;

// function createNode(id: NodeId, level: number, parent?: NodeId): RawNode {
//   const children = new Array(Math.max(2)).fill(0).map((_, i) => +id * 10 + i);
//   return {
//     id,
//     data: { level, children, parent }
//   };
// }

// function retrieveNodes(ids: NodeId[], parent?: NodeId): Promise<RawNode[]> {
//   if (ids.length === 0) return Promise.resolve([]);
//   const level = Math.floor(Math.log10(+ids[0]));
//   return new Promise(resolve => {
//     setTimeout(() => {
//       const nodes: RawNode[] = ids.map(id => createNode(id, level, parent));
//       resolve(nodes);
//     }, 10);
//   });
// }
ts
// 10 different colors for layers, shades of blue from darker to lighter
export const colors = [
  '#0d47a1',
  '#1565c0',
  '#1976d2',
  '#1e88e5',
  '#2196f3',
  '#42a5f5',
  '#64b5f6',
  '#90caf9',
  '#bbdefb',
  '#e3f2fd'
];

// 10 different shades of grey from #eee to #999
export const greyColors = [
  '#eee',
  '#e0e0e0',
  '#d1d1d1',
  '#c2c2c2',
  '#b3b3b3',
  '#a4a4a4',
  '#959595',
  '#868686',
  '#777777',
  '#686868'
];
ts
import { NodeId } from '@linkurious/ogma';

export interface ND {
  level: number;
  parent?: NodeId;
  children?: NodeId[];
}

export interface ED {
  isVirtual?: boolean;
}

export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;