Appearance
Circular layout (plugin)
This example shows how to implement a circular layout.
ts
import Ogma from '@linkurious/ogma';
import { circularLayout } from './circular';
const ogma = new Ogma({
container: 'graph-container',
options: {
interactions: { drag: { enabled: false } }
}
});
const NodeTypes = [
'red',
'green',
'blue',
'yellow',
'orange',
'purple',
'pink',
'brown',
'grey',
'black'
];
// color the types
ogma.styles.addNodeRule({
color: node => node?.getData('type')
});
function degreesToRadians(degrees: number) {
return (degrees * Math.PI) / 180;
}
// @ts-ignore
const gui = new dat.GUI();
// initial options
const options = {
startAngle: 0,
clockwise: true,
sortBy: 'by size',
distanceRatio: 0.25,
duration: 500,
run: () => {
// get all nodes "as is"
let nodes = ogma.getNodes();
// figure out the sorting order
if (options.sortBy === 'by size') {
nodes = nodes.sort(
(a, b) => +a.getAttribute('radius') - +b.getAttribute('radius')
);
} else if (options.sortBy === 'by type') {
// group by types
const typeMap = new Map();
NodeTypes.forEach((type, i) => typeMap.set(type, i));
nodes = nodes.sort((a, b) => {
const aType = a.getData('type');
const bType = b.getData('type');
return typeMap.get(aType) - typeMap.get(bType);
});
}
// read start angle, duration and direction
const { startAngle, clockwise, duration, distanceRatio } = options;
// keep the layout around the current graph centroid
const { cx, cy } = nodes.getBoundingBox();
// get the radii of the nodes
const radii = nodes.getAttribute('radius');
// get target positions from the layout
const positions = circularLayout({
radii,
clockwise,
startAngle: degreesToRadians(startAngle - 90),
cx,
cy,
distanceRatio
});
// move camera to fit the target positions
ogma.view.locateRawGraph({
nodes: positions.map((pos, i) => {
return { id: i, attributes: { ...pos, radius: radii[i] } };
}),
edges: []
});
return nodes.setAttributes(positions, duration);
}
};
gui
.add(options, 'startAngle', 0, 360)
.step(1)
.name('start angle')
.onChange(options.run);
gui
.add(options, 'distanceRatio', 0, 3)
.step(0.05)
.name('distance')
.onChange(options.run);
gui
.add(options, 'sortBy', ['by size', 'by type', 'random'])
.onChange(options.run);
gui.add(options, 'clockwise').onChange(options.run);
gui.add(options, 'duration', 0, 10000).step(100).onChange(options.run);
gui.add(options, 'run').name('Run layout');
ogma.generate
.random({ nodes: 120, edges: 140 })
.then(graph => {
graph.nodes.forEach(node => {
node.attributes = node.attributes || {};
node.attributes.radius = 5 + Math.random() * 30;
node.data = {
type: NodeTypes[Math.floor(Math.random() * NodeTypes.length)]
};
});
return ogma.setGraph(graph);
})
.then(() => ogma.view.locateGraph())
.then(() => ogma.view.setZoom(0.05, { ignoreZoomLimits: true }))
.then(options.run);
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 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;
}
ts
import type { Point, PixelSize } from '@linkurious/ogma';
interface Options {
radii: PixelSize[] | number[];
cx?: number;
cy?: number;
startAngle?: number;
clockwise?: boolean;
getRadius?: (radius: PixelSize) => number;
distanceRatio?: number;
}
export function circularLayout({
radii,
clockwise = true,
cx = 0,
cy = 0,
startAngle = (3 / 2) * Math.PI,
getRadius = (radius: PixelSize) => Number(radius),
distanceRatio = 0.0
}: Options): Point[] {
const N = radii.length;
// dummy checks
if (N === 0) return [];
if (N === 1) return [{ x: cx, y: cy }];
// minDistance
const minDistance =
radii.map(getRadius).reduce((acc, r) => Math.max(acc, r), 0) *
(2 + distanceRatio);
const sweep = 2 * Math.PI - (2 * Math.PI) / N;
const deltaAngle = sweep / Math.max(1, N - 1);
const dcos = Math.cos(deltaAngle) - Math.cos(0);
const dsin = Math.sin(deltaAngle) - Math.sin(0);
const rMin = Math.sqrt(
(minDistance * minDistance) / (dcos * dcos + dsin * dsin)
);
const r = Math.max(rMin, 0);
return radii.map((_, i) => {
const angle = startAngle + i * deltaAngle * (clockwise ? 1 : -1);
const rx = r * Math.cos(angle);
const ry = r * Math.sin(angle);
return {
x: cx + rx,
y: cy + ry
};
});
}