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

175 lines
5.5 KiB

Monaco Editor: Load via ESM (#78261) * chore(monaco): bump monaco-editor to latest version * feat(codeeditor): use esm to load monaco editor * revert(monaco): put back previous version * feat(monaco): setup MonacoEnvironment when bootstrapping app * feat(monaco): load monaco languages from registry as workers * feat(webpack): clean up warnings, remove need to copy monaco into lib * fix(plugins): wip - remove amd loader workaround in systemjs hooks * chore(azure): clean up so QueryField passes typecheck * test(jest): update config to fix failing tests due to missing monaco-editor * test(jest): update config to work with monaco-editor and kusto * test(jest): prevent message eventlistener in nodeGraph/layout.worker tripping up monaco tests * test(plugins): wip - remove amd related tests from systemjs hooks * test(alerting): prefer clearAllMocks to prevent monaco editor failing due to missing matchMedia * test(parca): fix failing test due to undefined backendSrv * chore: move monacoEnv to app/core * test: increase testing-lib timeout to 2secs, fix parca test to assert dom element * feat(plugins): share kusto via systemjs * test(e2e): increase timeout for checking monaco editor in exemplars spec * test(e2e): assert monaco has loaded by checking the spinner is gone and window.monaco exists * test(e2e): check for monaco editor textarea * test(e2e): check monaco editor is loaded before assertions * test(e2e): add waitForMonacoToLoad util to reduce duplication * test(e2e): fix failing mysql spec * chore(jest): add comment to setupTests explaining need to incresae default timeout * chore(nodegraph): improve comment in layout.worker.utils to better explain the need for file
1 year ago
// This file is a workaround so the layout function can be imported in Jest mocks. If the jest mock imports the
// layout.worker.js file it will attach the eventlistener and then call the layout function with undefined data
// which causes tests to fail.
import { forceSimulation, forceLink, forceCollide, forceX } from 'd3-force';
/**
* 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 its 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 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,
},
};
}