Appearance
Time layout
This example shows you the "Lombardi-style" time layout of movies and actors database. It's a way to present the graph as an event timeline.
js
import Ogma from '@linkurious/ogma';
const ogma = new Ogma({
container: 'graph-container',
options: {
edgesAlwaysCurvy: true
}
});
// actors node style
ogma.styles.addNodeRule(node => node.getData('type') === 'actor', {
text: {
content: node => node.getData('name'),
// hide if node is too small
minVisibleSize: 8,
margin: 0
}
});
// movies node style
ogma.styles.addNodeRule(node => node.getData('type') === 'movie', {
text: {
content: node => node.getData('title'),
style: 'italic',
minVisibleSize: 0
},
radius: 10,
color: node => node.getData('color')
});
// all edges are directed
ogma.styles.addEdgeRule(() => true, {
shape: {
head: 'arrow'
}
});
Promise.all([fetch('files/actors.json'), fetch('files/records.json')])
.then(responses => Promise.all(responses.map(r => r.text())))
.then(([actors, movies]) => {
let offsetId = 0;
const actorsMap = {};
const edges = [];
const data = JSON.parse(movies);
// .sort((a, b) => a.year - b.year)
// .filter(movie => movie.year < 1970);
window.data = data;
const movieNodes = new Array(100)
.fill(0)
.map((_, i, arr) => data[Math.floor((i / arr.length) * data.length)])
.map((movie, i, arr) => {
const moviesCount = arr.length;
movie.actors.forEach(actor => {
if (!actorsMap[actor]) {
actorsMap[actor] = {
id: ++offsetId + moviesCount,
data: {
name: actor,
type: 'actor'
}
};
}
edges.push({
source: actorsMap[actor].id,
target: i
});
});
return {
id: i,
data: {
type: 'movie',
title: movie.title,
year: movie.year,
color: movie.color
}
};
});
return Promise.all([
ogma.addNodes(movieNodes),
ogma.addNodes(Object.values(actorsMap)),
ogma.addEdges(edges)
]);
})
.then(() => {
const movies = ogma
.getNodes()
.filter(node => node.getData('type') === 'movie');
return timeLayout(movies)
.then(() => {
movies.setAttribute('layoutable', false);
return ogma.layouts.force({ duration: 0, locate: true });
})
.then(() => {
movies.setAttribute('layoutable', true);
});
});
/**
* @param {Ogma.Nodelist} movies
* @returns {Promise<void>}
*/
function timeLayout(movies) {
/** @type {Record<number, Ogma.NodeId[]>} */
const years = movies.reduce((years, movie) => {
const year = movie.getData('year');
years[year] = years[year] || [];
years[year].push(movie.getId());
return years;
}, {});
const colDist = 200;
const rowDist = 300;
/** @type {number[]} */
const xs = [];
return Promise.all(
Object.entries(years).reduce((promises, [year, nodeIds], i, arr) => {
const nodes = ogma.getNodes(nodeIds);
const x = (i - arr.length / 2) * colDist;
xs.push(x);
promises.push(nodes.setAttribute('x', x));
promises.push(
nodes.setAttribute(
'y',
nodeIds.map((_, j) => (j - nodeIds.length / 2) * rowDist)
)
);
return promises;
}, [])
).then(() => {
// add overlays
const { maxY } = ogma.view.getBounds();
// vertical year lines
const yearlines = ogma.layers.addCanvasLayer(ctx => {
const size = ogma.view.getSize();
const lb = ogma.view.screenToGraphCoordinates({ x: 0, y: 0 });
const rt = ogma.view.screenToGraphCoordinates({
x: size.width,
y: size.height
});
ctx.setLineDash([3, 5]);
ctx.strokeStyle = '#444';
ctx.beginPath();
xs.forEach(x => {
ctx.moveTo(x, lb.y);
ctx.lineTo(x, rt.y);
});
ctx.stroke();
});
yearlines.moveToBottom();
// year tooltips
const overlays = Object.keys(years).map((year, i) => {
const div = document.createElement('span');
div.classList.add('timeline');
const p = document.createElement('p');
p.innerText = year;
div.appendChild(p);
return ogma.layers.addOverlay({
element: div,
position: { x: xs[i], y: maxY - 50 },
scaled: false,
size: {
width: '',
height: ''
}
});
});
ogma.events.on(['viewChanged', 'pan'], () => {
requestAnimationFrame(() => {
const { minY } = ogma.view.getBounds();
overlays.forEach((o, i) => {
o.show();
o.setPosition({ x: xs[i], y: minY });
});
});
});
});
}
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!---->
<script src="https://cdn.jsdelivr.net/npm/d3-array@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-color@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-format@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-interpolate@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-time@3"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-time-format@4"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-scale@4"></script>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<script src="index.js"></script>
</body>
</html>
css
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
.timeline {
transition: all 0.5s ease;
}
.timeline p {
border-radius: 4px;
transform: translateX(-50%);
padding: 3px 8px;
background: white;
font-size: 12px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 3px 10px;
}
.context-menu {
background-color: blue;
}