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',
'monaco-editor',
'lodash-es',
'@msagl',
].join('|');
module.exports = {

View File

@ -1,4 +1,4 @@
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
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';
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
* 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) {
const { mappedEdges, DOTToIdMap } = createMappings(nodes, edges);
const dot = edgesToDOT(mappedEdges);
const dot = graphToDOT(mappedEdges, DOTToIdMap);
const graph = parseDot(dot);
const geomGraph = new GeomGraph(graph);
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
const DOTToIdMap = {};
// Crate the maps both ways
let index = 0;
for (const edge of edges) {
if (!idToDOTMap[edge.source]) {
idToDOTMap[edge.source] = index.toString(10);
DOTToIdMap[index.toString(10)] = edge.source;
index++;
}
for (const node of nodes) {
idToDOTMap[node.id] = index.toString(10);
DOTToIdMap[index.toString(10)] = node.id;
index++;
}
if (!idToDOTMap[edge.target]) {
idToDOTMap[edge.target] = index.toString(10);
DOTToIdMap[index.toString(10)] = edge.target;
index++;
}
for (const edge of edges) {
mappedEdges.push({ source: idToDOTMap[edge.source], target: idToDOTMap[edge.target] });
}
return {
mappedEdges,
DOTToIdMap,
idToDOTMap,
};
}
function toDOT(edges, graphAttr = '', edgeAttr = '') {
function graphToDOT(edges, nodeIDsMap) {
let dot = `
digraph G {
${graphAttr}
rankdir="LR"; TBbalance="min"
`;
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 += '}';
return dot;
}
function edgesToDOT(edges) {
return toDOT(edges, 'rankdir="LR"; TBbalance="min"', '[ minlen=3 ]');
}
function nodesDOT(edges) {
function nodesDOT(nodeIdsMap) {
let dot = '';
const visitedNodes = new Set();
// TODO: height/width for default sizing but nodes can have variable size now
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;
}
for (const node of Object.keys(nodeIdsMap)) {
dot += node + ' [fixedsize=true, width=1.2, height=1.7] \n';
}
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) => {
const { nodes, edges, config } = event.data;
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 { 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 {
timeout: number | undefined;