Appearance
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'
});
const preloader = ogma.layers.addOverlay({
element: generatePreloader('#0093ff'),
position: { x: 0, y: 0 },
scaled: false,
size: { width: '80px', height: '80px' }
});
await ogma.setGraph(graph as RawGraph, { batchSize: 500 });
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
}
});
ogma.styles.addEdgeRule({
color: '#999'
});
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 });
ogma.styles.setEdgesVisibility(false);
ogma.styles.setNodesVisibility(false);
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 }
};
},
padding: 20
})
.whenApplied();
await ogma.layouts.force({ gpu: true });
ogma.styles.setEdgesVisibility(true);
ogma.styles.setNodesVisibility(true);
preloader.destroy();
await ogma.view.moveToBounds(
ogma
.getNodes()
.sort(
(a, b) =>
Number(a.getAttribute('radius')) - Number(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>
`;
function generatePreloader(color: string) {
return `
<svg version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" xml:space="preserve">
<path fill="${color}" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite" />
</path>
</svg>
`;
}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@latest/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;
}