mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-12 11:59:39 +02:00
fix(#5952): initial fix for architecture diagrams with extreme heights
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
} from '../common/commonDb.js';
|
} from '../common/commonDb.js';
|
||||||
import type {
|
import type {
|
||||||
|
ArchitectureAlignment,
|
||||||
ArchitectureDB,
|
ArchitectureDB,
|
||||||
ArchitectureDirectionPair,
|
ArchitectureDirectionPair,
|
||||||
ArchitectureDirectionPairMap,
|
ArchitectureDirectionPairMap,
|
||||||
@@ -25,6 +26,7 @@ import type {
|
|||||||
ArchitectureState,
|
ArchitectureState,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
import {
|
import {
|
||||||
|
getArchitectureDirectionAlignment,
|
||||||
getArchitectureDirectionPair,
|
getArchitectureDirectionPair,
|
||||||
isArchitectureDirection,
|
isArchitectureDirection,
|
||||||
isArchitectureJunction,
|
isArchitectureJunction,
|
||||||
@@ -211,7 +213,7 @@ const addEdge = function ({
|
|||||||
const getEdges = (): ArchitectureEdge[] => state.records.edges;
|
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.
|
* If they have not been created, run the algorithms to generate them.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -220,10 +222,27 @@ const getDataStructures = () => {
|
|||||||
// Create an adjacency list of the diagram to perform BFS on
|
// Create an adjacency list of the diagram to perform BFS on
|
||||||
// Outer reduce applied on all services
|
// Outer reduce applied on all services
|
||||||
// Inner reduce applied on the edges for a service
|
// Inner reduce applied on the edges for a service
|
||||||
|
const groupAlignments: Record<
|
||||||
|
string,
|
||||||
|
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||||
|
> = {};
|
||||||
const adjList = Object.entries(state.records.nodes).reduce<
|
const adjList = Object.entries(state.records.nodes).reduce<
|
||||||
Record<string, ArchitectureDirectionPairMap>
|
Record<string, ArchitectureDirectionPairMap>
|
||||||
>((prevOuter, [id, service]) => {
|
>((prevOuter, [id, service]) => {
|
||||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((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) {
|
if (edge.lhsId === id) {
|
||||||
// source is LHS
|
// source is LHS
|
||||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||||
@@ -245,6 +264,7 @@ const getDataStructures = () => {
|
|||||||
// Configuration for the initial pass of BFS
|
// Configuration for the initial pass of BFS
|
||||||
const firstId = Object.keys(adjList)[0];
|
const firstId = Object.keys(adjList)[0];
|
||||||
const visited = { [firstId]: 1 };
|
const visited = { [firstId]: 1 };
|
||||||
|
// If a key is present in this object, it has not been visited
|
||||||
const notVisited = Object.keys(adjList).reduce(
|
const notVisited = Object.keys(adjList).reduce(
|
||||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||||
{} as Record<string, number>
|
{} as Record<string, number>
|
||||||
@@ -283,6 +303,7 @@ const getDataStructures = () => {
|
|||||||
state.records.dataStructures = {
|
state.records.dataStructures = {
|
||||||
adjList,
|
adjList,
|
||||||
spatialMaps,
|
spatialMaps,
|
||||||
|
groupAlignments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state.records.dataStructures;
|
return state.records.dataStructures;
|
||||||
|
@@ -12,7 +12,9 @@ import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
|||||||
import { getConfigField } from './architectureDb.js';
|
import { getConfigField } from './architectureDb.js';
|
||||||
import { architectureIcons } from './architectureIcons.js';
|
import { architectureIcons } from './architectureIcons.js';
|
||||||
import type {
|
import type {
|
||||||
|
ArchitectureAlignment,
|
||||||
ArchitectureDataStructures,
|
ArchitectureDataStructures,
|
||||||
|
ArchitectureGroupAlignments,
|
||||||
ArchitectureJunction,
|
ArchitectureJunction,
|
||||||
ArchitectureSpatialMap,
|
ArchitectureSpatialMap,
|
||||||
EdgeSingular,
|
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<number, Record<string, string[]>>,
|
||||||
|
alignmentDir: ArchitectureAlignment
|
||||||
|
): Record<string, string[]> => {
|
||||||
|
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<string, string[]>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const alignments = spatialMaps.map((spatialMap) => {
|
const alignments = spatialMaps.map((spatialMap) => {
|
||||||
const horizontalAlignments: Record<number, string[]> = {};
|
const horizontalAlignments: Record<number, Record<string, string[]>> = {};
|
||||||
const verticalAlignments: Record<number, string[]> = {};
|
const verticalAlignments: Record<number, Record<string, string[]>> = {};
|
||||||
|
|
||||||
// Group service ids in an object with their x and y coordinate as the key
|
// Group service ids in an object with their x and y coordinate as the key
|
||||||
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
||||||
if (!horizontalAlignments[y]) {
|
const nodeGroup = db.getNode(id)?.in ?? 'default';
|
||||||
horizontalAlignments[y] = [];
|
|
||||||
}
|
horizontalAlignments[y] ??= {};
|
||||||
if (!verticalAlignments[x]) {
|
horizontalAlignments[y][nodeGroup] ??= [];
|
||||||
verticalAlignments[x] = [];
|
horizontalAlignments[y][nodeGroup].push(id);
|
||||||
}
|
|
||||||
horizontalAlignments[y].push(id);
|
verticalAlignments[x] ??= {};
|
||||||
verticalAlignments[x].push(id);
|
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
|
// Merge the values of each object into a list if the inner list has at least 2 elements
|
||||||
return {
|
return {
|
||||||
horiz: Object.values(horizontalAlignments).filter((arr) => arr.length > 1),
|
horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(
|
||||||
vert: Object.values(verticalAlignments).filter((arr) => arr.length > 1),
|
(arr) => arr.length > 1
|
||||||
|
),
|
||||||
|
vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(
|
||||||
|
(arr) => arr.length > 1
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +301,8 @@ function layoutArchitecture(
|
|||||||
junctions: ArchitectureJunction[],
|
junctions: ArchitectureJunction[],
|
||||||
groups: ArchitectureGroup[],
|
groups: ArchitectureGroup[],
|
||||||
edges: ArchitectureEdge[],
|
edges: ArchitectureEdge[],
|
||||||
{ spatialMaps }: ArchitectureDataStructures
|
db: ArchitectureDB,
|
||||||
|
{ spatialMaps, groupAlignments }: 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');
|
||||||
@@ -320,7 +378,7 @@ function layoutArchitecture(
|
|||||||
addEdges(edges, cy);
|
addEdges(edges, cy);
|
||||||
|
|
||||||
// Use the spatial map to create alignment arrays for fcose
|
// 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
|
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
||||||
@@ -454,7 +512,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
|||||||
await drawServices(db, servicesElem, services);
|
await drawServices(db, servicesElem, services);
|
||||||
drawJunctions(db, servicesElem, junctions);
|
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 drawEdges(edgesElem, cy);
|
||||||
await drawGroups(groupElem, cy);
|
await drawGroups(groupElem, cy);
|
||||||
|
@@ -7,6 +7,8 @@ import type cytoscape from 'cytoscape';
|
|||||||
| Architecture Diagram Types |
|
| Architecture Diagram Types |
|
||||||
\*=======================================*/
|
\*=======================================*/
|
||||||
|
|
||||||
|
export type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend';
|
||||||
|
|
||||||
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
||||||
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
|
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
|
||||||
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
|
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
|
||||||
@@ -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 {
|
export interface ArchitectureStyleOptions {
|
||||||
archEdgeColor: string;
|
archEdgeColor: string;
|
||||||
archEdgeArrowColor: string;
|
archEdgeArrowColor: string;
|
||||||
@@ -249,9 +263,27 @@ export interface ArchitectureDB extends DiagramDB {
|
|||||||
|
|
||||||
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
|
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
|
||||||
export type ArchitectureSpatialMap = Record<string, number[]>;
|
export type ArchitectureSpatialMap = Record<string, number[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ArchitectureDataStructures {
|
export interface ArchitectureDataStructures {
|
||||||
adjList: ArchitectureAdjacencyList;
|
adjList: ArchitectureAdjacencyList;
|
||||||
spatialMaps: ArchitectureSpatialMap[];
|
spatialMaps: ArchitectureSpatialMap[];
|
||||||
|
groupAlignments: ArchitectureGroupAlignments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArchitectureState extends Record<string, unknown> {
|
export interface ArchitectureState extends Record<string, unknown> {
|
||||||
|
Reference in New Issue
Block a user