The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/nodeGraph/layout.worker.js

176 lines
5.4 KiB

import { forceSimulation, forceLink, forceCollide, forceX } from 'd3-force';
addEventListener('message', (event) => {
const { nodes, edges, config } = event.data;
layout(nodes, edges, config);
postMessage({ nodes, edges });
});
/**
* Use d3 force layout to lay the nodes in a sensible way. This function modifies the nodes adding the x,y positions
* and also fills in node references in edges instead of node ids.
*/
export function layout(nodes, edges, config) {
// Start with some hardcoded positions so it starts laid out from left to right
let { roots, secondLevelRoots } = initializePositions(nodes, edges);
// There always seems to be one or more root nodes each with single edge and we want to have them static on the
// left neatly in something like grid layout
[...roots, ...secondLevelRoots].forEach((n, index) => {
n.fx = n.x;
});
const simulation = forceSimulation(nodes)
.force(
'link',
forceLink(edges)
.id((d) => d.id)
.distance(config.linkDistance)
.strength(config.linkStrength)
)
// to keep the left to right layout we add force that pulls all nodes to right but because roots are fixed it will
// apply only to non root nodes
.force('x', forceX(config.forceX).strength(config.forceXStrength))
// Make sure nodes don't overlap
.force('collide', forceCollide(config.forceCollide));
// 300 ticks for the simulation are recommended but less would probably work too, most movement is done in first
// few iterations and then all the forces gets smaller https://github.com/d3/d3-force#simulation_alphaDecay
simulation.tick(config.tick);
simulation.stop();
// We do centering here instead of using centering force to keep this more stable
centerNodes(nodes);
}
/**
* This initializes positions of the graph by going from the root to it's children and laying it out in a grid from left
* to right. This works only so, so because service map graphs can have cycles and children levels are not ordered in a
* way to minimize the edge lengths. Nevertheless this seems to make the graph easier to nudge with the forces later on
* than with the d3 default initial positioning. Also we can fix the root positions later on for a bit more neat
* organisation.
*
* This function directly modifies the nodes given and only returns references to root nodes so they do not have to be
* found again later on.
*
* How the spacing could look like approximately:
* 0 - 0 - 0 - 0
* \- 0 - 0 |
* \- 0 -/
* 0 - 0 -/
*/
function initializePositions(nodes, edges) {
// To prevent going in cycles
const alreadyPositioned = {};
const nodesMap = nodes.reduce((acc, node) => {
acc[node.id] = node;
return acc;
}, {});
const edgesMap = edges.reduce((acc, edge) => {
const sourceId = edge.source;
acc[sourceId] = [...(acc[sourceId] || []), edge];
return acc;
}, {});
let roots = nodes.filter((n) => n.incoming === 0);
// For things like service maps we assume there is some root (client) node but if there is none then selecting
// any node as a starting point should work the same.
if (!roots.length) {
roots = [nodes[0]];
}
let secondLevelRoots = roots.reduce((acc, r) => {
acc.push(...(edgesMap[r.id] ? edgesMap[r.id].map((e) => nodesMap[e.target]) : []));
return acc;
}, []);
const rootYSpacing = 300;
const nodeYSpacing = 200;
const nodeXSpacing = 200;
let rootY = 0;
for (const root of roots) {
let graphLevel = [root];
let x = 0;
while (graphLevel.length > 0) {
const nextGraphLevel = [];
let y = rootY;
for (const node of graphLevel) {
if (alreadyPositioned[node.id]) {
continue;
}
// Initialize positions based on the spacing in the grid
node.x = x;
node.y = y;
alreadyPositioned[node.id] = true;
// Move to next Y position for next node
y += nodeYSpacing;
if (edgesMap[node.id]) {
nextGraphLevel.push(...edgesMap[node.id].map((edge) => nodesMap[edge.target]));
}
}
graphLevel = nextGraphLevel;
// Move to next X position for next level
x += nodeXSpacing;
// Reset Y back to baseline for this root
y = rootY;
}
rootY += rootYSpacing;
}
return { roots, secondLevelRoots };
}
/**
* Makes sure that the center of the graph based on it's bound is in 0, 0 coordinates.
* Modifies the nodes directly.
*/
function centerNodes(nodes) {
const bounds = graphBounds(nodes);
for (let node of nodes) {
node.x = node.x - bounds.center.x;
node.y = node.y - bounds.center.y;
}
}
/**
* Get bounds of the graph meaning the extent of the nodes in all directions.
*/
function graphBounds(nodes) {
if (nodes.length === 0) {
return { top: 0, right: 0, bottom: 0, left: 0, center: { x: 0, y: 0 } };
}
const bounds = nodes.reduce(
(acc, node) => {
if (node.x > acc.right) {
acc.right = node.x;
}
if (node.x < acc.left) {
acc.left = node.x;
}
if (node.y > acc.bottom) {
acc.bottom = node.y;
}
if (node.y < acc.top) {
acc.top = node.y;
}
return acc;
},
{ top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity }
);
const y = bounds.top + (bounds.bottom - bounds.top) / 2;
const x = bounds.left + (bounds.right - bounds.left) / 2;
return {
...bounds,
center: {
x,
y,
},
};
}