mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
feat(arch): disconnected graph handling for positioning
This commit is contained in:
parent
36f52be3bf
commit
92d819ede5
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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:`)
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user