/usr/share/grafana/public/app/plugins/panel/nodeGraph
// 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, }, }; }
.
Edit
..
Edit
Edge.tsx
Edit
EdgeArrowMarker.tsx
Edit
EdgeLabel.tsx
Edit
Legend.test.tsx
Edit
Legend.tsx
Edit
Marker.tsx
Edit
Node.test.tsx
Edit
Node.tsx
Edit
NodeGraph.test.tsx
Edit
NodeGraph.tsx
Edit
NodeGraphPanel.tsx
Edit
README.md
Edit
ViewControls.tsx
Edit
createLayoutWorker.ts
Edit
editor
Edit
forceLayout.js
Edit
img
Edit
layeredLayout.js
Edit
layeredLayout.test.ts
Edit
layeredLayout.worker.js
Edit
layout.test.ts
Edit
layout.ts
Edit
layout.worker.js
Edit
module.tsx
Edit
panelcfg.cue
Edit
panelcfg.gen.ts
Edit
plugin.json
Edit
suggestions.ts
Edit
types.ts
Edit
useCategorizeFrames.ts
Edit
useContextMenu.tsx
Edit
useFocusPositionOnLayout.ts
Edit
useHighlight.ts
Edit
useNodeLimit.ts
Edit
usePanning.ts
Edit
useZoom.ts
Edit
utils.test.ts
Edit
utils.ts
Edit