feat(arch): disconnected graph handling for positioning

This commit is contained in:
NicolasNewman 2024-04-03 15:32:43 -05:00
parent 36f52be3bf
commit 92d819ede5
3 changed files with 92 additions and 72 deletions

View File

@ -7,6 +7,7 @@ import type {
ArchitectureLine, ArchitectureLine,
ArchitectureDirectionPairMap, ArchitectureDirectionPairMap,
ArchitectureDirectionPair, ArchitectureDirectionPair,
ArchitectureSpatialMap,
} from './architectureTypes.js'; } from './architectureTypes.js';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import { getArchitectureDirectionPair, isArchitectureDirection, shiftPositionByArchitectureDirectionPair } from './architectureTypes.js'; import { getArchitectureDirectionPair, isArchitectureDirection, shiftPositionByArchitectureDirectionPair } from './architectureTypes.js';
@ -135,29 +136,44 @@ const getDataStructures = () => {
return prev return prev
}, {}); }, {});
// Configuration for the initial pass of BFS
const [firstId, _] = Object.entries(adjList)[0]; const [firstId, _] = Object.entries(adjList)[0];
const spatialMap = {[firstId]: [0,0]};
const visited = {[firstId]: 1}; const visited = {[firstId]: 1};
const queue = [firstId]; const notVisited = Object.keys(adjList).reduce((prev, id) => (
id === firstId ? prev : {...prev, [id]: 1}
), {} as Record<string, number>);
// Perform BFS on adjacency list // Perform BFS on adjacency list
while(queue.length > 0) { const BFS = (startingId: string): ArchitectureSpatialMap => {
const id = queue.shift(); const spatialMap = {[startingId]: [0,0]};
if (id) { const queue = [startingId];
visited[id] = 1 while(queue.length > 0) {
const adj = adjList[id]; const id = queue.shift();
const [posX, posY] = spatialMap[id]; if (id) {
Object.entries(adj).forEach(([dir, rhsId]) => { visited[id] = 1
if (!visited[rhsId]) { delete notVisited[id]
console.log(`${id} -- ${rhsId}`); const adj = adjList[id];
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair([posX, posY], dir as ArchitectureDirectionPair) const [posX, posY] = spatialMap[id];
queue.push(rhsId); Object.entries(adj).forEach(([dir, rhsId]) => {
} if (!visited[rhsId]) {
}) console.log(`${id} -- ${rhsId}`);
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair([posX, posY], dir as ArchitectureDirectionPair)
queue.push(rhsId);
}
})
}
} }
return spatialMap;
}
const spatialMaps = [BFS(firstId)];
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
while (Object.keys(notVisited).length > 0) {
spatialMaps.push(BFS(Object.keys(notVisited)[0]))
} }
datastructures = { datastructures = {
adjList, adjList,
spatialMap spatialMaps
} }
console.log(datastructures) console.log(datastructures)
} }

View File

@ -7,16 +7,12 @@ import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { import {
isArchitectureDirectionX,
type ArchitectureDB, type ArchitectureDB,
type ArchitectureDirection, type ArchitectureDirection,
type ArchitectureGroup, type ArchitectureGroup,
type ArchitectureLine, type ArchitectureLine,
type ArchitectureService, type ArchitectureService,
isArchitectureDirectionY,
ArchitectureDataStructures, ArchitectureDataStructures,
ArchitectureDirectionPair,
isArchitectureDirectionXY,
ArchitectureDirectionName, ArchitectureDirectionName,
getOppositeArchitectureDirection, getOppositeArchitectureDirection,
} from './architectureTypes.js'; } from './architectureTypes.js';
@ -25,7 +21,6 @@ import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { D3Element } from '../../mermaidAPI.js'; import type { D3Element } from '../../mermaidAPI.js';
import { drawEdges, drawGroups, drawService } from './svgDraw.js'; import { drawEdges, drawGroups, drawService } from './svgDraw.js';
import { getConfigField } from './architectureDb.js'; import { getConfigField } from './architectureDb.js';
import { X } from 'vitest/dist/reporters-5f784f42.js';
cytoscape.use(fcose); cytoscape.use(fcose);
@ -104,7 +99,7 @@ function layoutArchitecture(
services: ArchitectureService[], services: ArchitectureService[],
groups: ArchitectureGroup[], groups: ArchitectureGroup[],
lines: ArchitectureLine[], lines: ArchitectureLine[],
{adjList, spatialMap}: ArchitectureDataStructures {adjList, spatialMaps}: ArchitectureDataStructures
): Promise<cytoscape.Core> { ): Promise<cytoscape.Core> {
return new Promise((resolve) => { return new Promise((resolve) => {
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
@ -160,21 +155,28 @@ function layoutArchitecture(
// Use the spatial map to create alignment arrays for fcose // Use the spatial map to create alignment arrays for fcose
const [horizontalAlignments, verticalAlignments] = (() => { const [horizontalAlignments, verticalAlignments] = (() => {
const _horizontalAlignments: Record<number, string[]> = {} const alignments = spatialMaps.map(spatialMap => {
const _verticalAlignments: Record<number, string[]> = {} const _horizontalAlignments: Record<number, string[]> = {}
// Group service ids in an object with their x and y coordinate as the key const _verticalAlignments: Record<number, string[]> = {}
Object.entries(spatialMap).forEach(([id, [x, y]]) => { // Group service ids in an object with their x and y coordinate as the key
if (!_horizontalAlignments[y]) _horizontalAlignments[y] = []; Object.entries(spatialMap).forEach(([id, [x, y]]) => {
if (!_verticalAlignments[x]) _verticalAlignments[x] = []; if (!_horizontalAlignments[y]) _horizontalAlignments[y] = [];
_horizontalAlignments[y].push(id); if (!_verticalAlignments[x]) _verticalAlignments[x] = [];
_verticalAlignments[x].push(id); _horizontalAlignments[y].push(id);
_verticalAlignments[x].push(id);
})
// Merge the values of each object into a list if the inner list has at least 2 elements
return {
horiz: Object.values(_horizontalAlignments).filter(arr => arr.length > 1),
vert: Object.values(_verticalAlignments).filter(arr => arr.length > 1)
}
}) })
// Merge the values of each object into a list if the inner list has at least 2 elements // Merge the alginment lists for each spatial map into one 2d array per axis
return [ return alignments.reduce(([prevHoriz, prevVert], {horiz, vert}) => {
Object.values(_horizontalAlignments).filter(arr => arr.length > 1), return [[...prevHoriz, ...horiz], [...prevVert, ...vert]]
Object.values(_verticalAlignments).filter(arr => arr.length > 1) }, [[] as string[][], [] as string[][]])
]
})(); })();
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
@ -182,44 +184,46 @@ function layoutArchitecture(
const _relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [] const _relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}` const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`
const strToPos = (pos: string) => pos.split(',').map(p => parseInt(p)); const strToPos = (pos: string) => pos.split(',').map(p => parseInt(p));
const invSpatialMap = Object.fromEntries(Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]))
console.log('===== invSpatialMap =====')
console.log(invSpatialMap);
// perform BFS spatialMaps.forEach(spatialMap => {
const queue = [posToStr([0,0])]; const invSpatialMap = Object.fromEntries(Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]))
const visited: Record<string, number> = {}; console.log('===== invSpatialMap =====')
const directions: Record<ArchitectureDirection, number[]> = { console.log(invSpatialMap);
"L": [-1, 0],
"R": [1, 0], // perform BFS
"T": [0, 1], const queue = [posToStr([0,0])];
"B": [0, -1] const visited: Record<string, number> = {};
} const directions: Record<ArchitectureDirection, number[]> = {
while (queue.length > 0) { "L": [-1, 0],
const curr = queue.shift(); "R": [1, 0],
if (curr) { "T": [0, 1],
visited[curr] = 1 "B": [0, -1]
const currId = invSpatialMap[curr];
if (currId) {
const currPos = strToPos(curr);
Object.entries(directions).forEach(([dir, shift]) => {
const newPos = posToStr([(currPos[0]+shift[0]), (currPos[1]+shift[1])]);
const newId = invSpatialMap[newPos];
// If there is an adjacent service to the current one and it has not yet been visited
if (newId && !visited[newPos]) {
queue.push(newPos);
// @ts-ignore cannot determine if left/right or top/bottom are paired together
_relativeConstraints.push({
[ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,
[ArchitectureDirectionName[getOppositeArchitectureDirection(dir as ArchitectureDirection)]]: currId,
gap: 100
})
}
})
}
} }
} while (queue.length > 0) {
const curr = queue.shift();
if (curr) {
visited[curr] = 1
const currId = invSpatialMap[curr];
if (currId) {
const currPos = strToPos(curr);
Object.entries(directions).forEach(([dir, shift]) => {
const newPos = posToStr([(currPos[0]+shift[0]), (currPos[1]+shift[1])]);
const newId = invSpatialMap[newPos];
// If there is an adjacent service to the current one and it has not yet been visited
if (newId && !visited[newPos]) {
queue.push(newPos);
// @ts-ignore cannot determine if left/right or top/bottom are paired together
_relativeConstraints.push({
[ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,
[ArchitectureDirectionName[getOppositeArchitectureDirection(dir as ArchitectureDirection)]]: currId,
gap: 100
})
}
})
}
}
}
})
return _relativeConstraints; return _relativeConstraints;
})(); })();
console.log(`Horizontal Alignments:`) console.log(`Horizontal Alignments:`)

View File

@ -148,7 +148,7 @@ export type ArchitectureAdjacencyList = {[id: string]: ArchitectureDirectionPair
export type ArchitectureSpatialMap = Record<string, number[]> export type ArchitectureSpatialMap = Record<string, number[]>
export type ArchitectureDataStructures = { export type ArchitectureDataStructures = {
adjList: ArchitectureAdjacencyList; adjList: ArchitectureAdjacencyList;
spatialMap: ArchitectureSpatialMap; spatialMaps: ArchitectureSpatialMap[];
} }
export interface ArchitectureFields { export interface ArchitectureFields {