From 157a1513faea0ef062b283e61e679a9fb17f4c9b Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sun, 3 Nov 2024 15:53:09 -0600 Subject: [PATCH] fix(#5952): initial fix for architecture diagrams with extreme heights --- .../diagrams/architecture/architectureDb.ts | 23 ++++- .../architecture/architectureRenderer.ts | 90 +++++++++++++++---- .../architecture/architectureTypes.ts | 32 +++++++ 3 files changed, 128 insertions(+), 17 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index 93fa71ca3e..33d4ba309a 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -13,6 +13,7 @@ import { setDiagramTitle, } from '../common/commonDb.js'; import type { + ArchitectureAlignment, ArchitectureDB, ArchitectureDirectionPair, ArchitectureDirectionPairMap, @@ -25,6 +26,7 @@ import type { ArchitectureState, } from './architectureTypes.js'; import { + getArchitectureDirectionAlignment, getArchitectureDirectionPair, isArchitectureDirection, isArchitectureJunction, @@ -211,7 +213,7 @@ const addEdge = function ({ const getEdges = (): ArchitectureEdge[] => state.records.edges; /** - * Returns the current diagram's adjacency list & spatial map. + * Returns the current diagram's adjacency list, spatial map, & group alignments. * If they have not been created, run the algorithms to generate them. * @returns */ @@ -220,10 +222,27 @@ const getDataStructures = () => { // Create an adjacency list of the diagram to perform BFS on // Outer reduce applied on all services // Inner reduce applied on the edges for a service + const groupAlignments: Record< + string, + Record> + > = {}; const adjList = Object.entries(state.records.nodes).reduce< Record >((prevOuter, [id, service]) => { prevOuter[id] = service.edges.reduce((prevInner, edge) => { + // track the direction groups connect to one another + const lhsGroupId = getNode(edge.lhsId)?.in; + const rhsGroupId = getNode(edge.rhsId)?.in; + if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) { + const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir); + if (alignment !== 'bend') { + groupAlignments[lhsGroupId] ??= {}; + groupAlignments[lhsGroupId][rhsGroupId] = alignment; + groupAlignments[rhsGroupId] ??= {}; + groupAlignments[rhsGroupId][lhsGroupId] = alignment; + } + } + if (edge.lhsId === id) { // source is LHS const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir); @@ -245,6 +264,7 @@ const getDataStructures = () => { // Configuration for the initial pass of BFS const firstId = Object.keys(adjList)[0]; const visited = { [firstId]: 1 }; + // If a key is present in this object, it has not been visited const notVisited = Object.keys(adjList).reduce( (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }), {} as Record @@ -283,6 +303,7 @@ const getDataStructures = () => { state.records.dataStructures = { adjList, spatialMaps, + groupAlignments, }; } return state.records.dataStructures; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index af94295392..ad5fccb831 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -12,7 +12,9 @@ import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { getConfigField } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import type { + ArchitectureAlignment, ArchitectureDataStructures, + ArchitectureGroupAlignments, ArchitectureJunction, ArchitectureSpatialMap, EdgeSingular, @@ -149,25 +151,80 @@ function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) { }); } -function getAlignments(spatialMaps: ArchitectureSpatialMap[]): fcose.FcoseAlignmentConstraint { +function getAlignments( + db: ArchitectureDB, + spatialMaps: ArchitectureSpatialMap[], + groupAlignments: ArchitectureGroupAlignments +): fcose.FcoseAlignmentConstraint { + /** + * Flattens the alignment object so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment + * + * i.e., two groups which connect horizontally should not have vertical alignments with one another + * + * See: #5952 + * + * @param alignmentObj - alignment object with the outer key being the row/col and the inner key being the group name + * @param alignmentDir - alignment direction + * @returns flattened alignment object with an arbitrary key mapping to nodes in the same row/col + */ + const flattenAlignments = ( + alignmentObj: Record>, + alignmentDir: ArchitectureAlignment + ): Record => { + return Object.entries(alignmentObj).reduce( + (prev, [dir, alignments]) => { + let cnt = 0; + const arr = Object.entries(alignments); + if (arr.length === 1) { + prev[dir] = arr[0][1]; + return prev; + } + for (let i = 0; i < arr.length - 1; i += 1) { + const [aGroupId, aNodeIds] = arr[i]; + const [bGroupId, bNodeIds] = arr[i + 1]; + const alignment = groupAlignments[aGroupId][bGroupId]; + if (alignment === alignmentDir) { + prev[dir] ??= []; + prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; + } else { + const keyA = `${dir}-${cnt++}`; + prev[keyA] = aNodeIds; + const keyB = `${dir}-${cnt++}`; + prev[keyB] = bNodeIds; + } + } + + return prev; + }, + {} as Record + ); + }; + const alignments = spatialMaps.map((spatialMap) => { - const horizontalAlignments: Record = {}; - const verticalAlignments: Record = {}; + const horizontalAlignments: Record> = {}; + const verticalAlignments: Record> = {}; + // Group service ids in an object with their x and y coordinate as the key Object.entries(spatialMap).forEach(([id, [x, y]]) => { - if (!horizontalAlignments[y]) { - horizontalAlignments[y] = []; - } - if (!verticalAlignments[x]) { - verticalAlignments[x] = []; - } - horizontalAlignments[y].push(id); - verticalAlignments[x].push(id); + const nodeGroup = db.getNode(id)?.in ?? 'default'; + + horizontalAlignments[y] ??= {}; + horizontalAlignments[y][nodeGroup] ??= []; + horizontalAlignments[y][nodeGroup].push(id); + + verticalAlignments[x] ??= {}; + verticalAlignments[x][nodeGroup] ??= []; + verticalAlignments[x][nodeGroup].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), + horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter( + (arr) => arr.length > 1 + ), + vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter( + (arr) => arr.length > 1 + ), }; }); @@ -244,7 +301,8 @@ function layoutArchitecture( junctions: ArchitectureJunction[], groups: ArchitectureGroup[], edges: ArchitectureEdge[], - { spatialMaps }: ArchitectureDataStructures + db: ArchitectureDB, + { spatialMaps, groupAlignments }: ArchitectureDataStructures ): Promise { return new Promise((resolve) => { const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none'); @@ -320,7 +378,7 @@ function layoutArchitecture( addEdges(edges, cy); // Use the spatial map to create alignment arrays for fcose - const alignmentConstraint = getAlignments(spatialMaps); + const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments); // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it const relativePlacementConstraint = getRelativeConstraints(spatialMaps); @@ -454,7 +512,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) await drawServices(db, servicesElem, services); drawJunctions(db, servicesElem, junctions); - const cy = await layoutArchitecture(services, junctions, groups, edges, ds); + const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds); await drawEdges(edgesElem, cy); await drawGroups(groupElem, cy); diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index b3ef55ec69..cad2c5c369 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -7,6 +7,8 @@ import type cytoscape from 'cytoscape'; | Architecture Diagram Types | \*=======================================*/ +export type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend'; + export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B'; export type ArchitectureDirectionX = Extract; export type ArchitectureDirectionY = Extract; @@ -170,6 +172,18 @@ export const getArchitectureDirectionXYFactors = function ( } }; +export const getArchitectureDirectionAlignment = function ( + a: ArchitectureDirection, + b: ArchitectureDirection +): ArchitectureAlignment { + if (isArchitectureDirectionXY(a, b)) { + return 'bend'; + } else if (isArchitectureDirectionX(a)) { + return 'horizontal'; + } + return 'vertical'; +}; + export interface ArchitectureStyleOptions { archEdgeColor: string; archEdgeArrowColor: string; @@ -249,9 +263,27 @@ export interface ArchitectureDB extends DiagramDB { export type ArchitectureAdjacencyList = Record; export type ArchitectureSpatialMap = Record; + +/** + * Maps the direction that groups connect from. + * + * **Outer key**: ID of group A + * + * **Inner key**: ID of group B + * + * **Value**: 'vertical' or 'horizontal' + * + * Note: tmp[groupA][groupB] == tmp[groupB][groupA] + */ +export type ArchitectureGroupAlignments = Record< + string, + Record> +>; + export interface ArchitectureDataStructures { adjList: ArchitectureAdjacencyList; spatialMaps: ArchitectureSpatialMap[]; + groupAlignments: ArchitectureGroupAlignments; } export interface ArchitectureState extends Record {