Appearance
Distance annotation
This example shows how to use the canvas layer API to enhance the distance from the central node of the radial layout to all the other nodes.
ts
import Ogma, { NodeId, Node } from '@linkurious/ogma';
import { renderLayoutOutlines } from './render';
const ogma = new Ogma({
container: 'graph-container',
// make it transparent if you don't want the ogma layer to hide the previous ones
options: { backgroundColor: 'transparent' }
});
// Layout
let centralNode: Node;
const run = (
centralNodeId: NodeId,
animate: boolean,
overlap: boolean,
randomize: boolean
) => {
console.time('radial stress');
return ogma.layouts
.radial({
centralNode: centralNodeId,
renderSteps: animate,
randomize: randomize,
allowOverlap: overlap,
maxIterations: 200
})
.then(() => {
console.timeEnd('radial stress');
ogma.view.locateGraph({
easing: 'linear',
duration: 200
});
});
};
const runLayout = () => {
const form = document.querySelector('#focus-select') as HTMLFormElement;
const focusInput = Array.prototype.filter.call(
form['focus'],
input => input.checked
)[0];
const focusNode = focusInput.value;
const animate = (form['animate'] as unknown as HTMLInputElement).checked;
const overlap = (form['overlap'] as HTMLInputElement).checked;
const randomize = (form['randomize'] as HTMLInputElement).checked;
centralNode = ogma.getNode(focusNode)!;
return run(focusNode, animate, overlap, randomize);
};
// Draw on the draw layer
function draw(ctx: CanvasRenderingContext2D) {
renderLayoutOutlines(ctx, centralNode, ogma.getNodes());
}
Ogma.parse
.gexfFromUrl('karate.gexf')
.then(g => {
ogma.setGraph(g);
ogma.view.locateGraph();
document
.querySelector<HTMLButtonElement>('#layout')!
.addEventListener('click', evt => {
evt.preventDefault();
runLayout();
});
return runLayout();
})
.then(() => {
// add a draw layer
const drawLayer = ogma.layers.addCanvasLayer(draw);
drawLayer.moveDown();
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<form id="focus-select" class="control-bar">
<div class="content">
<label
><input type="radio" name="focus" checked value="1.0" />
Instructor</label
>
<label
><input type="radio" name="focus" value="34.0" /> Administrator</label
>
<label
><input type="checkbox" name="overlap" /> allow nodes overlap</label
>
<label><input type="checkbox" name="animate" /> animate</label>
<label><input type="checkbox" name="randomize" /> randomize</label>
</div>
<div class="controls">
<button id="layout">Run</button>
</div>
</form>
<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;
}
#focus-select {
position: absolute;
top: 20px;
right: 20px;
padding: 10px;
background: white;
z-index: 400;
}
#focus-select label {
display: block;
}
#focus-select .controls {
text-align: center;
margin-top: 10px;
}
#focus-select .content {
line-height: 1.5em;
}
.control-bar {
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
}
ts
import Ogma, { Node, NodeId, NodeList } from '@linkurious/ogma';
// distances have to be sorted in ascending order
const cleanDistances = (distances: number[], tolerance = 8, maxCount = 20) => {
let distance,
last = 0;
const cleanedDistances = [];
for (let i = 0; i < distances.length; i++) {
distance = distances[i];
if (distance - last > tolerance) {
cleanedDistances.push(distance);
last = distance;
}
if (cleanedDistances.length >= maxCount) return cleanedDistances;
}
return cleanedDistances;
};
// distances have to be sorted in ascending order
const renderOutlines = (
ctx: CanvasRenderingContext2D,
distances: number[],
fontSize = 8
) => {
ctx.lineWidth = 1;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
distances = cleanDistances(distances);
// concentric circles
let i, len, distance, radius;
const maxDist = distances[distances.length - 1],
minDist = distances[0];
for (i = 0, len = distances.length; i < len; i++) {
distance = distances[i];
radius = distance;
ctx.beginPath();
ctx.strokeStyle = `rgba(0, 0, 0, ${Math.min(
0.5,
(maxDist - distance) / (maxDist - minDist)
)})`;
ctx.moveTo(radius, 0);
ctx.arc(0, 0, radius, 0, 2 * Math.PI, false);
ctx.moveTo(radius, 0);
ctx.stroke();
}
// label backgrounds
ctx.beginPath();
ctx.lineWidth = 0;
ctx.fillStyle = '#ffffff';
for (i = 0, len = distances.length; i < len; i++) {
distance = distances[i];
radius = distance;
ctx.arc(radius, 0, fontSize, 0, 2 * Math.PI, false);
}
ctx.fill();
// label texts
ctx.fillStyle = 'black';
ctx.font = fontSize + 'px sans-serif';
for (i = 0, len = distances.length; i < len; i++) {
distance = distances[i];
radius = distance;
ctx.fillText(String(i + 1), radius, 0);
}
};
// calculates distances of elements from center and stores them to be rendered
// in `renderLayoutOutlines`
const collectRadii = (centralNode: Node, nodes: NodeList) => {
const layoutData = {};
const positions = nodes.getPosition();
const ids = nodes.getId();
const center = centralNode.getPosition();
const layers: Record<number, NodeId[]> = {};
for (let i = 0, len = nodes.size; i < len; i++) {
const pos = positions[i];
const dist = Math.round(
Ogma.geometry.distance(center.x, center.y, pos.x, pos.y)
);
layers[dist] = layers[dist] || [];
layers[dist].push(ids[i]);
}
return {
layers,
center,
positions,
distances: Object.keys(layers).map(key => parseInt(key))
};
};
export const renderLayoutOutlines = (
ctx: CanvasRenderingContext2D,
centralNode: Node,
nodes: NodeList
) => {
const { distances } = collectRadii(centralNode, nodes);
const { x, y } = centralNode.getPosition();
ctx.save();
ctx.translate(x, y);
renderOutlines(ctx, distances);
ctx.restore();
};