Nodegraph: Fix issue with rendering single node (#84930)

Fix for single node graph case
This commit is contained in:
Andrej Ocenas 2024-03-26 13:43:43 +01:00 committed by GitHub
parent dd93d9958d
commit aba65747c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 38 additions and 41 deletions

View File

@ -18,6 +18,7 @@ const esModules = [
'@kusto/monaco-kusto', '@kusto/monaco-kusto',
'monaco-editor', 'monaco-editor',
'lodash-es', 'lodash-es',
'@msagl',
].join('|'); ].join('|');
module.exports = { module.exports = {

View File

@ -1,4 +1,4 @@
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker'; import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
export const createWorker = () => new Worker(new URL('./layout.worker.js', import.meta.url)); export const createWorker = () => new Worker(new URL('./layout.worker.js', import.meta.url));
export const createMsaglWorker = () => new Worker(new URL('./layoutMsagl.worker.js', import.meta.url)); export const createMsaglWorker = () => new Worker(new URL('./layoutLayered.worker.js', import.meta.url));

View File

@ -10,12 +10,6 @@ import {
} from '@msagl/core'; } from '@msagl/core';
import { parseDot } from '@msagl/parser'; import { parseDot } from '@msagl/parser';
addEventListener('message', async (event) => {
const { nodes, edges, config } = event.data;
const [newNodes, newEdges] = layout(nodes, edges, config);
postMessage({ nodes: newNodes, edges: newEdges });
});
/** /**
* Use d3 force layout to lay the nodes in a sensible way. This function modifies the nodes adding the x,y positions * 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. * and also fills in node references in edges instead of node ids.
@ -23,7 +17,7 @@ addEventListener('message', async (event) => {
export function layout(nodes, edges) { export function layout(nodes, edges) {
const { mappedEdges, DOTToIdMap } = createMappings(nodes, edges); const { mappedEdges, DOTToIdMap } = createMappings(nodes, edges);
const dot = edgesToDOT(mappedEdges); const dot = graphToDOT(mappedEdges, DOTToIdMap);
const graph = parseDot(dot); const graph = parseDot(dot);
const geomGraph = new GeomGraph(graph); const geomGraph = new GeomGraph(graph);
for (const e of graph.deepEdges) { for (const e of graph.deepEdges) {
@ -142,58 +136,41 @@ function createMappings(nodes, edges) {
// Key is an iteration index and value is actual ID of the node // Key is an iteration index and value is actual ID of the node
const DOTToIdMap = {}; const DOTToIdMap = {};
// Crate the maps both ways
let index = 0; let index = 0;
for (const edge of edges) { for (const node of nodes) {
if (!idToDOTMap[edge.source]) { idToDOTMap[node.id] = index.toString(10);
idToDOTMap[edge.source] = index.toString(10); DOTToIdMap[index.toString(10)] = node.id;
DOTToIdMap[index.toString(10)] = edge.source; index++;
index++; }
}
if (!idToDOTMap[edge.target]) { for (const edge of edges) {
idToDOTMap[edge.target] = index.toString(10);
DOTToIdMap[index.toString(10)] = edge.target;
index++;
}
mappedEdges.push({ source: idToDOTMap[edge.source], target: idToDOTMap[edge.target] }); mappedEdges.push({ source: idToDOTMap[edge.source], target: idToDOTMap[edge.target] });
} }
return { return {
mappedEdges, mappedEdges,
DOTToIdMap, DOTToIdMap,
idToDOTMap,
}; };
} }
function toDOT(edges, graphAttr = '', edgeAttr = '') { function graphToDOT(edges, nodeIDsMap) {
let dot = ` let dot = `
digraph G { digraph G {
${graphAttr} rankdir="LR"; TBbalance="min"
`; `;
for (const edge of edges) { for (const edge of edges) {
dot += edge.source + '->' + edge.target + ' ' + edgeAttr + '\n'; dot += edge.source + '->' + edge.target + ' ' + '[ minlen=3 ]\n';
} }
dot += nodesDOT(edges); dot += nodesDOT(nodeIDsMap);
dot += '}'; dot += '}';
return dot; return dot;
} }
function edgesToDOT(edges) { function nodesDOT(nodeIdsMap) {
return toDOT(edges, 'rankdir="LR"; TBbalance="min"', '[ minlen=3 ]');
}
function nodesDOT(edges) {
let dot = ''; let dot = '';
const visitedNodes = new Set(); for (const node of Object.keys(nodeIdsMap)) {
// TODO: height/width for default sizing but nodes can have variable size now dot += node + ' [fixedsize=true, width=1.2, height=1.7] \n';
const attr = '[fixedsize=true, width=1.2, height=1.7] \n';
for (const edge of edges) {
if (!visitedNodes.has(edge.source)) {
dot += edge.source + attr;
}
if (!visitedNodes.has(edge.target)) {
dot += edge.target + attr;
}
} }
return dot; return dot;
} }

View File

@ -0,0 +1,10 @@
import { layout } from './layeredLayout';
describe('layout', () => {
it('can render single node', () => {
const nodes = [{ id: 'A', incoming: 0 }];
const edges: unknown[] = [];
const graph = layout(nodes, edges);
expect(graph).toEqual([[{ id: 'A', incoming: 0, x: 0, y: 0 }], []]);
});
});

View File

@ -1,5 +1,6 @@
import { layout } from './layout.worker.utils'; import { layout } from './forceLayout';
// Separate from main implementation so it does not trip out tests
addEventListener('message', (event) => { addEventListener('message', (event) => {
const { nodes, edges, config } = event.data; const { nodes, edges, config } = event.data;
layout(nodes, edges, config); layout(nodes, edges, config);

View File

@ -0,0 +1,8 @@
import { layout } from './layeredLayout';
// Separate from main implementation so it does not trip out tests
addEventListener('message', async (event) => {
const { nodes, edges, config } = event.data;
const [newNodes, newEdges] = layout(nodes, edges, config);
postMessage({ nodes: newNodes, edges: newEdges });
});

View File

@ -1,7 +1,7 @@
import { Config } from 'app/plugins/panel/nodeGraph/layout'; import { Config } from 'app/plugins/panel/nodeGraph/layout';
import { EdgeDatum, NodeDatum } from 'app/plugins/panel/nodeGraph/types'; import { EdgeDatum, NodeDatum } from 'app/plugins/panel/nodeGraph/types';
const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.utils.js'); const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/forceLayout.js');
class LayoutMockWorker { class LayoutMockWorker {
timeout: number | undefined; timeout: number | undefined;