Appearance
Radial layout on a transport network
This example shows the use of the radial layout algorithm on the Paris metro map.
Click on any station to position other stations according to the shortest path from the selected one. Use 'Push factor' control to adjust the density of the nodes on the layers, thus making them easier to read.
ts
import Ogma, { NodeId, Node, Point } from '@linkurious/ogma';
const ogma = new Ogma({
container: 'graph-container'
});
const overlapInput = document.querySelector('#overlap') as HTMLInputElement;
const repulsionInput = document.querySelector(
'#push-ratio'
) as HTMLInputElement;
const randomizeInput = document.querySelector('#randomize') as HTMLInputElement;
const run = (centralNodeId: NodeId) => {
const overlap = overlapInput.checked;
const repulsion = parseFloat(repulsionInput.value);
const randomize = randomizeInput.checked;
// disable UI while layout is running
overlapInput.disabled =
repulsionInput.disabled =
randomizeInput.disabled =
true;
console.time('radial stress');
ogma.layouts
.radial({
locate: true, // locate nodes while calculating the layout
centralNode: centralNodeId,
randomize: randomize,
allowOverlap: overlap,
repulsion: repulsion,
radiusDelta: 130,
duration: 300
})
.then(() => {
console.timeEnd('radial stress');
// enable UI
overlapInput.removeAttribute('disabled');
repulsionInput.removeAttribute('disabled');
randomizeInput.removeAttribute('disabled');
});
};
Ogma.parse
.jsonFromUrl('paris-metro.json')
.then(graph => ogma.setGraph(graph))
.then(() => ogma.view.locateGraph())
.then(() => {
centralNode = ogma.getNode('ChâteletPARIS-01ER')!;
centralNode.setSelected(true);
ogma.events.on('click', evt => {
if (evt.target && evt.target.isNode) {
centralNode = evt.target;
run(evt.target.getId());
}
});
ogma.events.on('layoutEnd', evt => {
// store initial positions
if (!initialPositions) {
initialPositions = evt.positions.before;
nodeIds = evt.ids;
}
});
setTimeout(() => {
run(centralNode.getId());
}, 1000);
});
// to store the positions before layout
let initialPositions: Point[], nodeIds: NodeId[];
let centralNode: Node;
// reset to initial positions
const reset = () => {
ogma.clearSelection();
centralNode = ogma.getNode('ChâteletPARIS-01ER')!;
centralNode.setSelected(true);
if (nodeIds) ogma.getNodes().setAttributes(initialPositions);
};
document.querySelector('#options')!.addEventListener('change', () => {
if (centralNode) {
requestAnimationFrame(() => {
run(centralNode.getId());
});
}
});
document.querySelector('#reset')!.addEventListener('click', evt => {
evt.preventDefault();
reset();
});
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>
<div class="attribution">
Paris Metro | Graph data ©
<a
target="_blank"
href="https://github.com/totetmatt/gexf/tree/master/metro/Paris"
>Matthieu Totet</a
>
CC-BY-SA
</div>
<form id="options" class="control-bar">
<div class="content">
<label
><input
type="range"
id="push-ratio"
value="1"
min="0.1"
max="2"
step="0.1"
/>
Push factor</label
>
<label
><input type="checkbox" id="overlap" /> allow nodes overlap</label
>
<label><input type="checkbox" id="randomize" /> randomize nodes</label>
</div>
<div class="controls">
<button id="reset">Reset</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;
}
#options {
position: absolute;
top: 20px;
right: 20px;
padding: 10px;
background: white;
z-index: 400;
}
#options label {
display: block;
}
#options .controls {
text-align: center;
margin-top: 10px;
}
#options .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;
}
.attribution {
position: absolute;
right: 0px;
bottom: 0px;
padding: 0px;
z-index: 1000;
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 11px;
padding: 1px 5px;
background: rgba(255, 255, 255, 0.7);
}
json
{
"nodes": [
{
"degree": 5,
"id": "Gare de LyonPARIS-12EME",
"inDegree": 3,
"outDegree": 2,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.844757,
"longitude": 2.3740723,
"pinned": false,
"size": 42.42640687119285,
"text": "Gare de Lyon",
"x": 525.1153564453125,
"y": -281,
"isNode": true,
"data": {
"latin_name": "Gare de Lyon",
"Eccentricity": 23,
"Betweenness Centrality": 66284.82695915208,
"Closeness Centrality": 8.378731343283581,
"local_name": "Gare de Lyon",
"location": "PARIS-12EME",
"Eigenvector Centrality": 1,
"lines": "Line14-Line1-RERA-RERD",
"latitude": 48.844757,
"longitude": 2.3740723
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#67328E",
"#F2C931"
],
"text": "Gare de Lyon"
}
},
{
"degree": 3,
"id": "Gare du NordPARIS-10EME",
"inDegree": 2,
"outDegree": 1,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.880035,
"longitude": 2.3545492,
"pinned": false,
"size": 42.42640687119285,
"text": "Gare du Nord",
"x": 496.4261779785156,
"y": -1209,
"isNode": true,
"data": {
"latin_name": "Gare du Nord",
"Eccentricity": 25,
"Betweenness Centrality": 36901.69440836936,
"Closeness Centrality": 8.89179104477612,
"local_name": "Gare du Nord",
"location": "PARIS-10EME",
"Eigenvector Centrality": 0.5366748142943254,
"lines": "Line4-Line5-RERB-RERD",
"latitude": 48.880035,
"longitude": 2.3545492
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#BB4D98",
"#DE8B53"
],
"text": "Gare du Nord"
}
},
{
"degree": 7,
"id": "NationPARIS-12EME",
"inDegree": 3,
"outDegree": 4,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.848465,
"longitude": 2.3959057,
"pinned": false,
"size": 60,
"text": "Nation",
"x": 989.2464599609375,
"y": -235,
"isNode": true,
"data": {
"latin_name": "Nation",
"Eccentricity": 24,
"Betweenness Centrality": 31660.579437229462,
"Closeness Centrality": 9.078358208955224,
"local_name": "Nation",
"location": "PARIS-12EME",
"Eigenvector Centrality": 0.7770134775054275,
"lines": "Line1-Line2-Line6-Line9-RERA",
"latitude": 48.848465,
"longitude": 2.3959057
},
"attributes": {
"radius": 60,
"color": [
"#F2C931",
"#216EB4",
"#75c695",
"#CDC83F"
],
"text": "Nation"
}
},
{
"degree": 6,
"id": "Charles de Gaulle - EtoilePARIS-08EME",
"inDegree": 1,
"outDegree": 5,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.87441,
"longitude": 2.2957628,
"pinned": false,
"size": 51.96152422706631,
"text": "Charles de Gaulle - Etoile",
"x": -973.6716918945312,
"y": -1286,
"isNode": true,
"data": {
"latin_name": "Charles de Gaulle - Etoile",
"Eccentricity": 26,
"Betweenness Centrality": 30386.45905483406,
"Closeness Centrality": 9.42910447761194,
"local_name": "Charles de Gaulle - Etoile",
"location": "PARIS-08EME",
"Eigenvector Centrality": 0.35464528135158707,
"lines": "Line1-Line2-Line6-RERA",
"latitude": 48.87441,
"longitude": 2.2957628
},
"attributes": {
"radius": 51.96152422706631,
"color": [
"#F2C931",
"#216EB4",
"#75c695"
],
"text": "Charles de Gaulle - Etoile"
}
},
{
"degree": 4,
"id": "InvalidesPARIS-07EME",
"inDegree": 2,
"outDegree": 2,
"active": false,
"halo": false,
"hidden": false,
"latitude": 48.862553,
"longitude": 2.313989,
"pinned": false,
"size": 42.42640687119285,
"text": "Invalides",
"x": -689.1814575195312,
"y": -520,
"isNode": true,
"data": {
"latin_name": "Invalides",
"Eccentricity": 25,
"Betweenness Centrality": 22916.702586302596,
"Closeness Centrality": 9.598880597014926,
"local_name": "Invalides",
"location": "PARIS-07EME",
"Eigenvector Centrality": 0.44784291910876195,
"lines": "Line13-Line8-RERC",
"latitude": 48.862553,
"longitude": 2.313989
},
"attributes": {
"radius": 42.42640687119285,
"color": [
"#89C7D6",
"#C5A3CA"
],
"text": "Invalides"
}
},
{
"degree": 8,
"id": "ChâteletPARIS-01ER",
"inDegree": 0,
"outDegree": 8,
...