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'
});
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;
}