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/layeredLayout.js

227 lines
6.6 KiB

import {
GeomGraph,
GeomEdge,
GeomNode,
Point,
CurveFactory,
SugiyamaLayoutSettings,
LayerDirectionEnum,
layoutGeomGraph,
} from '@msagl/core';
import { parseDot } from '@msagl/parser';
/**
* 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) {
const { mappedEdges, DOTToIdMap } = createMappings(nodes, edges);
const dot = graphToDOT(mappedEdges, DOTToIdMap);
const graph = parseDot(dot);
const geomGraph = new GeomGraph(graph);
for (const e of graph.deepEdges) {
new GeomEdge(e);
}
for (const n of graph.nodesBreadthFirst) {
const gn = new GeomNode(n);
gn.boundaryCurve = CurveFactory.mkCircle(50, new Point(0, 0));
}
geomGraph.layoutSettings = new SugiyamaLayoutSettings();
geomGraph.layoutSettings.layerDirection = LayerDirectionEnum.LR;
geomGraph.layoutSettings.LayerSeparation = 60;
geomGraph.layoutSettings.commonSettings.NodeSeparation = 40;
layoutGeomGraph(geomGraph);
const nodesMap = {};
for (const node of geomGraph.nodesBreadthFirst) {
nodesMap[DOTToIdMap[node.id]] = {
obj: node,
};
}
for (const node of nodes) {
nodesMap[node.id] = {
...nodesMap[node.id],
datum: {
...node,
x: nodesMap[node.id].obj.center.x,
y: nodesMap[node.id].obj.center.y,
},
};
}
const edgesMapped = edges.map((e) => {
return {
...e,
source: nodesMap[e.source].datum,
target: nodesMap[e.target].datum,
};
});
// This section checks if there are separate disjointed subgraphs. If so it groups nodes for each and then aligns
// each subgraph, so it starts on a single vertical line. Otherwise, they are laid out randomly from left to right.
const subgraphs = [];
for (const e of edgesMapped) {
const sourceGraph = subgraphs.find((g) => g.nodes.has(e.source));
const targetGraph = subgraphs.find((g) => g.nodes.has(e.target));
if (sourceGraph && targetGraph) {
// if the node sets are not the same we merge them
if (sourceGraph !== targetGraph) {
targetGraph.nodes.forEach(sourceGraph.nodes.add, sourceGraph.nodes);
subgraphs.splice(subgraphs.indexOf(targetGraph), 1);
sourceGraph.top = Math.min(sourceGraph.top, targetGraph.top);
sourceGraph.bottom = Math.max(sourceGraph.bottom, targetGraph.bottom);
sourceGraph.left = Math.min(sourceGraph.left, targetGraph.left);
sourceGraph.right = Math.max(sourceGraph.right, targetGraph.right);
}
// if the sets are the same nothing to do.
} else if (sourceGraph) {
sourceGraph.nodes.add(e.target);
sourceGraph.top = Math.min(sourceGraph.top, e.target.y);
sourceGraph.bottom = Math.max(sourceGraph.bottom, e.target.y);
sourceGraph.left = Math.min(sourceGraph.left, e.target.x);
sourceGraph.right = Math.max(sourceGraph.right, e.target.x);
} else if (targetGraph) {
targetGraph.nodes.add(e.source);
targetGraph.top = Math.min(targetGraph.top, e.source.y);
targetGraph.bottom = Math.max(targetGraph.bottom, e.source.y);
targetGraph.left = Math.min(targetGraph.left, e.source.x);
targetGraph.right = Math.max(targetGraph.right, e.source.x);
} else {
// we don't have these nodes
subgraphs.push({
top: Math.min(e.source.y, e.target.y),
bottom: Math.max(e.source.y, e.target.y),
left: Math.min(e.source.x, e.target.x),
right: Math.max(e.source.x, e.target.x),
nodes: new Set([e.source, e.target]),
});
}
}
let top = 0;
let left = 0;
for (const g of subgraphs) {
if (top === 0) {
top = g.bottom + 200;
left = g.left;
} else {
const topDiff = top - g.top;
const leftDiff = left - g.left;
for (const n of g.nodes) {
n.x += leftDiff;
n.y += topDiff;
}
top += g.bottom - g.top + 200;
}
}
const finalNodes = Object.values(nodesMap).map((v) => v.datum);
centerNodes(finalNodes);
return [finalNodes, edgesMapped];
}
// We create mapping because the DOT language we use later to create the graph doesn't support arbitrary IDs. So we
// map our IDs to just an index of the node so the IDs are safe for the DOT parser and also create and inverse mapping
// for quick lookup.
function createMappings(nodes, edges) {
// Edges where the source and target IDs are the indexes we use for layout
const mappedEdges = [];
// Key is an ID of the node and value is new ID which is just iteration index
const idToDOTMap = {};
// Key is an iteration index and value is actual ID of the node
const DOTToIdMap = {};
let index = 0;
for (const node of nodes) {
idToDOTMap[node.id] = index.toString(10);
DOTToIdMap[index.toString(10)] = node.id;
index++;
}
for (const edge of edges) {
mappedEdges.push({ source: idToDOTMap[edge.source], target: idToDOTMap[edge.target] });
}
return {
mappedEdges,
DOTToIdMap,
idToDOTMap,
};
}
function graphToDOT(edges, nodeIDsMap) {
let dot = `
digraph G {
rankdir="LR"; TBbalance="min"
`;
for (const edge of edges) {
dot += edge.source + '->' + edge.target + ' ' + '[ minlen=3 ]\n';
}
dot += nodesDOT(nodeIDsMap);
dot += '}';
return dot;
}
function nodesDOT(nodeIdsMap) {
let dot = '';
for (const node of Object.keys(nodeIdsMap)) {
dot += node + ' [fixedsize=true, width=1.2, height=1.7] \n';
}
return dot;
}
/**
* Makes sure that the center of the graph based on its 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,
},
};
}