Skip to content
  1. Examples

Grouping layout on a large graph new

This example shows the performance of the new Ogma grouping layout on large graphs.

ts
import Ogma, { Node, RawGraph } from '@linkurious/ogma';
import { labelPropagation } from './labelPropagation';
import chroma from 'chroma-js';

const graph = await Ogma.parse.jsonFromUrl('./graph.json');

console.log('Graph loaded:', graph);

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

await ogma.setGraph(graph as RawGraph);
const labels = await labelPropagation(ogma, 10);

// count unique labels
const uniqueLabels = new Set(labels.values());
console.log('Unique labels:', uniqueLabels.size);
console.log('Labels:', labels);
// Apply colors to labels
const colors = chroma.scale('OrRd').colors(uniqueLabels.size);
const colorMap = new Map(
  Array.from(uniqueLabels).map((label, index) => [label, colors[index]])
);

ogma.styles.addNodeRule({
  color: getColor,
  innerStroke: {
    width: 0.1,
    scalingMethod: 'scaled',
    color: n => {
      return chroma(getColor(n)).darken(2).hex();
    }
  },
  outerStroke: {
    width: 0
  }
});

function getColor(node: Node) {
  let label = labels.get(node.getId());
  if (!label) {
    label = labels.get(node.getSubNodes()?.get(0).getId()); // Fallback to first subnode ID if no label found
  }
  const color = colorMap.get(label);

  return node.isVirtual() ? chroma(color).tint(0.3).hex() : color;
}

console.log('Labels after label propagation:', labels);
//await ogma.layouts.force({ gpu: true, locate: true });
let start = Date.now();
await ogma.transformations
  .addNodeGrouping({
    groupIdFunction: node => {
      return labels.get(node.getId());
    },
    nodeGenerator: (_, id) => {
      return { data: { label: id } };
    },
    showContents: true,
    onGroupUpdate() {
      return {
        layout: 'force',
        params: { gpu: true }
      };
    }
  })
  .whenApplied();

await ogma.layouts.force({});
await ogma.view.moveToBounds(
  ogma
    .getNodes()
    .sort((a, b) => a.getAttribute('radius') - b.getAttribute('radius'))
    .slice(0, 1000)
    .getBoundingBox()
    .pad(100),
  { duration: 200 }
);

document.getElementById('info')!.innerHTML = `
  <div class="title"><span class="icon-timer"></span> Time: <span class="value">${Date.now() - start}ms</span></div>
  <sl-divider></sl-divider>
  <div class="title"><span class="icon-circle-small"></span> Nodes: <span class="value">${ogma.getNodes().size}</span></div>
  <sl-divider></sl-divider>
  <div class="title"><span class="icon-minus"></span> Edges: <span class="value">${ogma.getEdges().size}</span></div>
  <sl-divider></sl-divider>
  <div class="title"><span class="icon-bubbles"></span> Groups / layouts: <span class="value">${ogma.getNodes().filter(n => n.isVirtual()).size}</span></div>

  `;
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link type="text/css" rel="stylesheet" href="./styles.css" />
    <script src="https://cdn.jsdelivr.net/npm/@linkurious/ogma-ui-kit@0.0.9/dist/ogma-ui-kit.min.js"></script>
  </head>
  <body>
    <div id="graph-container"></div>
    <div class="panel" id="info"></div>
    <script src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.panel .row {
  text-align: right;
}

.panel .value {
  font-weight: bold;
  color: #333;
}
json
{
  "nodes": [
    {
      "id": 0
    },
    {
      "id": 1
    },
    {
      "id": 2
    },
    {
      "id": 4519
    },
    {
      "id": 23073
    },
    {
      "id": 33043
    },
    {
      "id": 33971
    },
    {
      "id": 75503
    },
    {
      "id": 101215
    },
    {
      "id": 120044
    },
    {
      "id": 123880
    },
    {
      "id": 124002
    },
    {
      "id": 206567
    },
    {
      "id": 274042
    },
    {
      "id": 369692
    },
    {
      "id": 411025
    },
    {
      "id": 413808
    },
    {
      "id": 5915
    },
    {
      "id": 7741
    },
    {
      "id": 7852
    },
    {
      "id": 7979
    },
    {
      "id": 8085
    },
    {
      "id": 8086
    },
    {
      "id": 9335
    },
    {
      "id": 10971
    },
    {
      "id": 12238
    },
    {
      "id": 13090
    },
    {
      "id": 13419
    },
    {
      "id": 13811
    },
    {
      "id": 14662
    },
    {
      "id": 15004
    },
    {
      "id": 15432
    },
    {
      "id": 16259
    },
    {
      "id": 16408
    },
    {
      "id": 16803
    },
    {
      "id": 17411
    },
    {
      "id": 18035
    },
    {
      "id": 21502
    },
    {
      "id": 22970
    },
    {
      "id": 27021
    },
    {
      "id": 27466
    },
    {
      "id": 28995
    },
    {
      "id": 29998
    },
    {
      "id": 31096
    },
    {
      "id": 33126
    },
    {
      "id": 33762
    },
    {
      "id": 35156
    },
    {
      "id": 36918
    },
    {
      "id": 37774
    },
    {
      "id": 40548
    },
    {
      "id": 44446
    },
    {
      "id": 44725
    },
    {
      "id": 46588
    },
    {
      "id": 47489
    },
    {
      "id": 49063
    },
    {
      "id": 49144
    },
    {
      "id": 56260
    },
    {
      "id": 58164
    },
    {
      "id": 58212
    },
    {
      "id": 63386
    },
    {
      "id": 66243
    },
    {
      "id": 66305
    },
    {
      "id": 66668
    },
    {
      "id": 67191
    },
    {
      "id": 73133
    },
    {
      "id": 74815
    },
    {
      "id": 75213
    },
    {
      "id": 77551
    },
    {
      "id": 78424
    },
    {
      "id": 81806
    },
    {
      "id": 83059
    },
    {
      "id": 84042
    },
    {
      "id": 84805
    },
    {
      "id": 85126
    },
    {
      "id": 86291
    },
    {
      "id": 90680
    },
    {
      "id": 92739
    },
    {
      "id": 92740
    },
    {
      "id": 93377
    },
    {
      "id": 95874
    },
    {
      "id": 97590
    },
    {
      "id": 97591
    },
    {
      "id": 97758
    },
    {
      "id": 103133
    },
    {
      "id": 110696
    },
    {
      "id": 113230
    },
    {
      "id": 115324
    },
    {
      "id": 122675
    },
    {
      "id": 159465
    },
    {
      "id": 162643
    },
    {
      "id": 164373
    },
    {
      "id": 166307
    },
    {
      "id": 173255
    },
    {
      "id": 173256
    },
    {
      "id": 179100
    },
    {
      "id": 193261
    },
    {
      "id": 194924
    },
    {
      "id": 198462
    },
    {
      "id": 223322
    },
    {
      "id": 229958
    },
    {
      "id": 248344
    },
    {
      "id": 253221
    },
    {
      "id": 255383
    },
    {
      "id": 268421
    },
    {
      "id": 321570
    },
    {
      "id": 321680
    },
    {
      "id": 6786
    },
    {
      "id": 10077
    },
    {
      "id": 23595
    },
    {
      "id": 37678
    },
    {
      "id": 44724
    },
    {
      "id": 54787
    },
    {
      "id": 65297
    },
    {
      "id": 75502
    },
    {
      "id": 79494
    },
    {
      "id": 84203
    },
    {
      "id": 93250
    },
    {
      "id": 108350
    },
    {
      "id": 108790
    },
    {
      "id": 127771
    },
    {
      "id": 139006
    },
    {
      "id": 157374
    },
    {
      "id": 189316
    },
    {
      "id": 3
    },
    {
      "id": 10816
    },
    {
      "id": 15821
    },
    {
      "id": 18373
    },
    {
      "id": 21946
    },
    {
      "id": 22531
    },
    {
      "id": 22955
    },
    {
      "id": 34140
    },
    {
      "id": 56565
    },
    {
      "id": 58183
    },
    {
      "id": 59273
    },
    {
      "id": 71407
    },
    {
      "id": 117331
    },
    {
      "id": 4
    },
    {
      "id": 11
    },
    {
      "id": 10
    },
    {
      "id": 53
    },
    {
      "id": 61
    },
    {
      "id": 368
    },
    {
      "id": 369
    },
    {
      "id": 3514
    },
    {
      "id": 3641
    },
    {
      "id": 35
    },
    {
      "id": 105
    },
    {
      "id": 21
    },
    {
      "id": 22378
    },
    {
      "id": 90421
    },
    {
      "id": 27
    },
    {
      "id": 138
    },
    {
      "id": 264
    },
    {
      "id": 29
    },
    {
      "id": 348
    },
    {
      "id": 364
    },
    {
      "id": 3890
    },
    {
      "id": 39
    },
    {
      "id": 274
    },
    {
      "id": 426
    },
    {
      "id": 2527
    },
    {
      "id": 25670
    },
    {
      "id": 33969
    },
    {
      "id": 45116
    },
    {
      "id": 48574
    },
    {
      "id": 55007
    }

...
ts
import Ogma, { NodeId } from '@linkurious/ogma';

export async function labelPropagation(ogma: Ogma, maxIterations = 20) {
  const nodes = ogma.getNodes().filter(node => !node.isVirtual());
  const labels = new Map();

  // Step 1: Initialize each node's label with its own ID
  nodes.forEach(node => labels.set(node.getId(), node.getId().toString()));

  // Step 2: Iterate label updates
  for (let i = 0; i < maxIterations; i++) {
    let changed = false;

    for (const node of nodes) {
      const neighbors = node.getAdjacentNodes();
      const neighborLabels = neighbors.map(n => labels.get(n.getId()));

      if (neighborLabels.length === 0) continue;

      // Count label frequencies
      const counts: Record<NodeId, number> = {};
      for (const label of neighborLabels) {
        counts[label] = (counts[label] || 0) + 1;
      }

      // Choose most frequent label
      const mostCommonLabel = Object.entries(counts).sort(
        (a, b) => b[1] - a[1]
      )[0][0];

      // Update label if it changed
      const nodeId = node.getId();
      if (labels.get(nodeId) !== mostCommonLabel) {
        labels.set(nodeId, mostCommonLabel);
        changed = true;
      }
    }

    if (!changed) break; // Stop if no label changed this round
  }

  return labels;
}