Skip to content

Commit

Permalink
fix(mermaid-js#5952): initial fix for architecture diagrams with extr…
Browse files Browse the repository at this point in the history
…eme heights
  • Loading branch information
NicolasNewman committed Nov 3, 2024
1 parent ece3b25 commit cb0a470
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 17 deletions.
23 changes: 22 additions & 1 deletion packages/mermaid/src/diagrams/architecture/architectureDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
setDiagramTitle,
} from '../common/commonDb.js';
import type {
ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair,
ArchitectureDirectionPairMap,
Expand All @@ -25,6 +26,7 @@ import type {
ArchitectureState,
} from './architectureTypes.js';
import {
getArchitectureDirectionAlignment,
getArchitectureDirectionPair,
isArchitectureDirection,
isArchitectureJunction,
Expand Down Expand Up @@ -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
*/
Expand All @@ -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<string, Exclude<ArchitectureAlignment, 'bend'>>
> = {};
const adjList = Object.entries(state.records.nodes).reduce<
Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => {
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) {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
Expand All @@ -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<string, number>
Expand Down Expand Up @@ -283,6 +303,7 @@ const getDataStructures = () => {
state.records.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
};
}
return state.records.dataStructures;
Expand Down
90 changes: 74 additions & 16 deletions packages/mermaid/src/diagrams/architecture/architectureRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 horizontalAlignments: Record<number, string[]> = {};
const verticalAlignments: Record<number, string[]> = {};
const horizontalAlignments: Record<number, Record<string, string[]>> = {};
const verticalAlignments: Record<number, Record<string, string[]>> = {};

// 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
),
};
});

Expand Down Expand Up @@ -244,7 +301,8 @@ function layoutArchitecture(
junctions: ArchitectureJunction[],
groups: ArchitectureGroup[],
edges: ArchitectureEdge[],
{ spatialMaps }: ArchitectureDataStructures
db: ArchitectureDB,
{ spatialMaps, groupAlignments }: ArchitectureDataStructures
): Promise<cytoscape.Core> {
return new Promise((resolve) => {
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions packages/mermaid/src/diagrams/architecture/architectureTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArchitectureDirection, 'L' | 'R'>;
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -249,9 +263,27 @@ export interface ArchitectureDB extends DiagramDB {

export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
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 {
adjList: ArchitectureAdjacencyList;
spatialMaps: ArchitectureSpatialMap[];
groupAlignments: ArchitectureGroupAlignments;
}

export interface ArchitectureState extends Record<string, unknown> {
Expand Down

0 comments on commit cb0a470

Please sign in to comment.