Appearance
Force layout with custom node masses and edge weights new
This example shows how you can control individual node mass
es and edge weight
s in a force-directed layout in order to express different properties of your dataset. It allows you to control the strength of the forces that act on the nodes and edges in the layout, which can be useful for emphasizing certain aspects of the data.
ts
import Ogma, { NodeId, RawGraph } from '@linkurious/ogma';
interface NodeData {
importance: number;
}
interface EdgeData {
weight: number;
}
const ogma = new Ogma<NodeData, EdgeData>({
container: 'graph-container',
options: {
backgroundColor: '#505050'
}
});
(async () => {
let gravity: number | undefined = 0;
let theta: number | undefined = 0.64;
let steps: number | undefined = 300;
async function runLayout() {
await ogma.layouts.force({
steps,
locate: true,
gravity,
gpu: true,
nodeMass: n => n.getData('importance'),
edgeWeight: e => e.getData('weight')
});
}
//await runLayout();
const examples = {
async arch() {
gravity = 0;
theta = undefined;
steps = 600;
ogma.styles.addRule({
nodeAttributes: {
radius: n => n.getData('importance') * 5,
text: {
content: n => Math.round(n.getData('importance')),
color: '#bbb',
backgroundColor: '#333',
minVisibleSize: 5,
tip: false
}
},
edgeAttributes: {
width: e => e.getData('weight') * 5,
color: 'white',
text: {
color: 'white',
content: edge => {
const weight = edge.getData('weight');
return weight === 1 ? '1' : weight.toFixed(2);
},
minVisibleSize: 0
}
}
});
await ogma.setGraph({
nodes: Array.from({ length: 10 }, (_, i) => ({
id: i,
data: { importance: i < 5 ? i + 1 : 10 - i }
})),
edges: [
...Array.from({ length: 9 }, (_, i) => ({
source: i,
target: i + 1,
data: { weight: 1 }
})),
{ source: 9, target: 0, data: { weight: 0.05 } }
]
});
await runLayout();
},
async grid() {
gravity = 0;
theta = undefined;
const graph = await ogma.generate.grid({ rows: 5, columns: 5 });
await ogma.setGraph(graph);
await ogma.view.locateGraph({ padding: 150 });
ogma.getNodes().forEach((node, i) => {
const id = node.getId();
// cornder nodes are the most important
const importance =
id === 0 || id === 4 || id === 20 || id === 24 ? 10 : 0.5;
node.setData('importance', importance);
});
ogma.getEdges().forEach(edge => {
edge.setData(
'weight',
10 /
Math.max(
edge.getSource().getData('importance') +
edge.getTarget().getData('importance')
)
);
});
ogma.styles.addRule({
nodeAttributes: {
radius: n => 5 + n.getData('importance'),
text: {
content: n => n.getData('importance').toFixed(2),
minVisibleSize: 50,
font: 'Georgia',
color: 'white'
}
},
edgeAttributes: {
width: e => e.getData('weight') / 5,
text: {
color: 'white',
minVisibleSize: 0,
font: 'Georgia',
content: edge => {
const weight = edge.getData('weight');
if (weight < 1) return weight.toFixed(2);
return undefined;
}
}
}
});
},
async groups() {
gravity = 0.1;
theta = undefined;
const input = await Ogma.parse.gexfFromUrl('files/arctic.gexf');
const nodesByColor = new Map<NodeId, string>();
input.nodes.forEach(node => {
nodesByColor.set(node.id!, node.attributes!.color as string);
(node.data as NodeData).importance = 1;
});
input.edges.forEach(edge => {
// if edges belong to the same group, increase weight
const sameGroup =
nodesByColor.get(edge.source) === nodesByColor.get(edge.target);
(edge.data as EdgeData) = { weight: sameGroup ? 20 : 0.1 };
});
ogma.styles.addRule({
nodeAttributes: {},
edgeAttributes: {
width: 1,
color: e => {
const source = e.getSource().getId();
const target = e.getTarget().getId();
if (nodesByColor.get(source) === nodesByColor.get(target))
return nodesByColor.get(source);
return 'rgba(0, 0, 0, 0.05)';
}
}
});
await ogma.setGraph(input as RawGraph<NodeData, EdgeData>);
await ogma.view.locateGraph({ padding: 150 });
await ogma.removeNodes(ogma.getNodes().filter(n => n.getDegree() === 0));
},
async layers() {
gravity = 0;
theta = 0.34;
steps = 600;
const graph = await ogma.generate.random({ nodes: 500, edges: 0 });
await ogma.setGraph(graph);
await ogma.view.locateGraph();
// 5 contrast pastel colors
const colors = ['#FFC0CB', '#FFD700', '#98FB98', '#87CEEB', '#FFA07A'];
ogma.styles.addRule({
nodeAttributes: {
radius: n => n.getData('importance') * 5,
color: n => colors[n.getData('importance') - 1]
}
});
await ogma.addEdges(
ogma.getNodes().map((n, i) => ({ source: i, target: 0 }))
);
await ogma.getNodes().forEach((node, i) => {
node.setData('importance', Math.floor(i / 100) + 1);
});
await ogma.view.locateGraph({ padding: 150 });
await runLayout();
},
async centrality() {
gravity = undefined;
theta = 0.64;
const graph = await ogma.generate.barabasiAlbert({
nodes: 256
});
await ogma.setGraph(graph);
ogma.getNodes().setData(
'importance',
ogma.algorithms
.betweenness({
directed: false,
normalized: true
})
.map(v => 1 + 20 * v)
);
ogma.getEdges().setData('weight', edge => {
const source = edge.getSource();
const target = edge.getTarget();
if (
source.getData('importance') > 1 &&
target.getData('importance') > 1
)
return 10;
return 1;
});
await ogma.styles
.addRule({
nodeAttributes: {
radius: n => n.getData('importance') * 10
},
edgeAttributes: {
width: e => (e.getData('weight') <= 1 ? 0.1 : 10)
}
})
.whenApplied();
await ogma.view.locateGraph({ padding: 150 });
await runLayout();
}
};
const state = {
runLayout,
example: 'layers' as keyof typeof examples,
examples
};
// @ts-expect-error dat.GUI
const gui = new dat.GUI();
gui
.add(state, 'example', ['layers', 'grid', 'groups', 'arch', 'centrality'])
.name('Example')
.onChange(async () => {
ogma.styles.getRuleList().forEach(rule => rule.destroy());
steps = 300;
await ogma.clearGraph();
await state.examples[state.example]();
//await runLayout();
});
gui.add(state, 'runLayout').name('Run layout');
await examples.layers();
})();
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/dat.gui@0.7.9/build/dat.gui.min.js"></script>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></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;
}