mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 10:54:15 +01:00 
			
		
		
		
	Compare commits
	
		
			26 Commits
		
	
	
		
			mermaid@11
			...
			halo-layou
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 39598baaa3 | ||
|   | 08fbed7c47 | ||
|   | df636c6d0a | ||
|   | 64554a6c60 | ||
|   | becadf0a7d | ||
|   | 54d485f173 | ||
|   | b4f5b8ddaf | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | cb5c1ae367 | ||
|   | b29081d4e8 | ||
|   | 654097c438 | ||
|   | 1e672868c4 | ||
|   | bff32827b5 | ||
|   | 65f9b29b86 | ||
|   | b4879d13b8 | ||
|   | 95964b5487 | ||
|   | 4e17da0a30 | ||
|   | 2a91849a38 | ||
|   | 082de76eef | ||
|   | 570ae78b15 | ||
|   | 885ac6f947 | ||
|   | 193fdb225e | ||
| ![autofix-ci[bot]](/assets/img/avatar_default.png)  | 7cbd80af33 | ||
|   | 16c448b89b | ||
|   | cb0a4703bd | ||
| ![autofix-ci[bot]](/assets/img/avatar_default.png)  | 8cb1c68166 | ||
|   | d752240efc | 
							
								
								
									
										5
									
								
								.changeset/angry-bags-brake.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changeset/angry-bags-brake.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| --- | ||||
| 'mermaid': patch | ||||
| --- | ||||
|  | ||||
| fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments | ||||
| @@ -171,6 +171,58 @@ describe.skip('architecture diagram', () => { | ||||
|             ` | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('should render an architecture diagram with a resonable height', () => { | ||||
|     imgSnapshotTest( | ||||
|       `architecture-beta | ||||
|               group federated(cloud)[Federated Environment] | ||||
|                   service server1(server)[System] in federated | ||||
|                   service edge(server)[Edge Device] in federated | ||||
|                   server1:R -- L:edge | ||||
|  | ||||
|               group on_prem(cloud)[Hub] | ||||
|                   service firewall(server)[Firewall Device] in on_prem | ||||
|                   service server(server)[Server] in on_prem | ||||
|                   firewall:R -- L:server | ||||
|  | ||||
|                   service db1(database)[db1] in on_prem | ||||
|                   service db2(database)[db2] in on_prem | ||||
|                   service db3(database)[db3] in on_prem | ||||
|                   service db4(database)[db4] in on_prem | ||||
|                   service db5(database)[db5] in on_prem | ||||
|                   service db6(database)[db6] in on_prem | ||||
|  | ||||
|                   junction mid in on_prem | ||||
|                   server:B -- T:mid | ||||
|  | ||||
|                   junction 1Leftofmid in on_prem | ||||
|                   1Leftofmid:R -- L:mid | ||||
|                   1Leftofmid:B -- T:db1 | ||||
|  | ||||
|                   junction 2Leftofmid in on_prem | ||||
|                   2Leftofmid:R -- L:1Leftofmid | ||||
|                   2Leftofmid:B -- T:db2 | ||||
|  | ||||
|                   junction 3Leftofmid in on_prem | ||||
|                   3Leftofmid:R -- L:2Leftofmid | ||||
|                   3Leftofmid:B -- T:db3 | ||||
|  | ||||
|                   junction 1RightOfMid in on_prem | ||||
|                   mid:R -- L:1RightOfMid | ||||
|                   1RightOfMid:B -- T:db4 | ||||
|                    | ||||
|                   junction 2RightOfMid in on_prem | ||||
|                   1RightOfMid:R -- L:2RightOfMid | ||||
|                   2RightOfMid:B -- T:db5         | ||||
|                    | ||||
|                   junction 3RightOfMid in on_prem | ||||
|                   2RightOfMid:R -- L:3RightOfMid | ||||
|                   3RightOfMid:B -- T:db6          | ||||
|  | ||||
|                   edge:R -- L:firewall | ||||
|       ` | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| // Skipped as the layout is not deterministic, and causes issues in E2E tests. | ||||
|   | ||||
| @@ -900,6 +900,153 @@ flowchart LR | ||||
|     n7@{ shape: rect} | ||||
|     n8@{ shape: rect} | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('6088-1: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
|       flowchart LR | ||||
|       subgraph S2 | ||||
|       subgraph s1["APA"] | ||||
|       D{"Use the editor"} | ||||
|       end | ||||
|  | ||||
|  | ||||
|       D -- Mermaid js --> I{"fa:fa-code Text"} | ||||
|             D --> I | ||||
|             D --> I | ||||
|  | ||||
|       end | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('6088-2: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
|       flowchart LR | ||||
|       a | ||||
|       subgraph s0["APA"] | ||||
|       subgraph s8["APA"] | ||||
|       subgraph s1["APA"] | ||||
|         D{"X"} | ||||
|         E[Q] | ||||
|       end | ||||
|       subgraph s3["BAPA"] | ||||
|         F[Q] | ||||
|         I | ||||
|       end | ||||
|             D --> I | ||||
|             D --> I | ||||
|             D --> I | ||||
|  | ||||
|       I{"X"} | ||||
|       end | ||||
|       end | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('6088-3: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
|       flowchart LR | ||||
|       a | ||||
|         D{"Use the editor"} | ||||
|  | ||||
|       D -- Mermaid js --> I{"fa:fa-code Text"} | ||||
|       D-->I | ||||
|       D-->I | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('6088-4: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
| flowchart LR | ||||
|  subgraph s1["Untitled subgraph"] | ||||
|         n1["Evaluate"] | ||||
|         n2["Option 1"] | ||||
|         n3["Option 2"] | ||||
|         n4["fa:fa-car Option 3"] | ||||
|   end | ||||
|  subgraph s2["Untitled subgraph"] | ||||
|         n5["Evaluate"] | ||||
|         n6["Option 1"] | ||||
|         n7["Option 2"] | ||||
|         n8["fa:fa-car Option 3"] | ||||
|   end | ||||
|     A["Start"] -- Some text --> B("Continue") | ||||
|     B --> C{"Evaluate"} | ||||
|     C -- One --> D["Option 1"] | ||||
|     C -- Two --> E["Option 2"] | ||||
|     C -- Three --> F["fa:fa-car Option 3"] | ||||
|     n1 -- One --> n2 | ||||
|     n1 -- Two --> n3 | ||||
|     n1 -- Three --> n4 | ||||
|     n5 -- One --> n6 | ||||
|     n5 -- Two --> n7 | ||||
|     n5 -- Three --> n8 | ||||
|     n1@{ shape: diam} | ||||
|     n2@{ shape: rect} | ||||
|     n3@{ shape: rect} | ||||
|     n4@{ shape: rect} | ||||
|     n5@{ shape: diam} | ||||
|     n6@{ shape: rect} | ||||
|     n7@{ shape: rect} | ||||
|     n8@{ shape: rect} | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('6088-5: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
| flowchart LR | ||||
|     A{A} --> B & C | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|       }); | ||||
|       it('6088-6: should handle diamond shape intersections', () => { | ||||
|         imgSnapshotTest( | ||||
|           `--- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
| flowchart LR | ||||
|     A{A} --> B & C | ||||
|     subgraph "subbe" | ||||
|       A | ||||
|     end | ||||
|  | ||||
| `, | ||||
|           { flowchart: { titleTopMargin: 0 } } | ||||
|         ); | ||||
|   | ||||
| @@ -88,33 +88,61 @@ | ||||
|   </head> | ||||
|  | ||||
|   <body> | ||||
|     <pre id="diagram4" class="mermaid2"> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
| flowchart LR | ||||
|  subgraph s1["Untitled subgraph"] | ||||
|         n1["Evaluate"] | ||||
|         n2["Option 1"] | ||||
|         n3["Option 2"] | ||||
|         n4["fa:fa-car Option 3"] | ||||
|       flowchart LR | ||||
|       subgraph S2 | ||||
|       subgraph s1["APA"] | ||||
|       D{"Use the editor"} | ||||
|       end | ||||
|     n1 -- One --> n2 | ||||
|     n1 -- Two --> n3 | ||||
|     n1 -- Three --> n4 | ||||
|     n5 | ||||
|     n1@{ shape: diam} | ||||
|     n2@{ shape: rect} | ||||
|     n3@{ shape: rect} | ||||
|     n4@{ shape: rect} | ||||
|     A["Start"] -- Some text --> B("Continue") | ||||
|     B --> C{"Evaluate"} | ||||
|     C -- One --> D["Option 1"] | ||||
|     C -- Two --> E["Option 2"] | ||||
|     C -- Three --> F["fa:fa-car Option 3"] | ||||
|  | ||||
|  | ||||
|       D -- Mermaid js --> I{"fa:fa-code Text"} | ||||
|             D --> I | ||||
|             D --> I | ||||
|  | ||||
|       end | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
|       flowchart LR | ||||
|       a | ||||
|       subgraph s0["APA"] | ||||
|       subgraph s8["APA"] | ||||
|       subgraph s1["APA"] | ||||
|         D{"X"} | ||||
|         E[Q] | ||||
|       end | ||||
|       subgraph s3["BAPA"] | ||||
|         F[Q] | ||||
|         I | ||||
|       end | ||||
|             D --> I | ||||
|             D --> I | ||||
|             D --> I | ||||
|  | ||||
|       I{"X"} | ||||
|       end | ||||
|       end | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
|       flowchart LR | ||||
|       a | ||||
|         D{"Use the editor"} | ||||
|  | ||||
|       D -- Mermaid js --> I{"fa:fa-code Text"} | ||||
|       D-->I | ||||
|       D-->I | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| @@ -155,7 +183,7 @@ flowchart LR | ||||
|     n8@{ shape: rect} | ||||
|  | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid2"> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| @@ -171,7 +199,7 @@ flowchart LR | ||||
|  | ||||
|  | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid2"> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| @@ -180,7 +208,19 @@ flowchart LR | ||||
|     A{A} --> B & C | ||||
| </pre | ||||
|     > | ||||
|     <pre id="diagram4" class="mermaid2"> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
| --- | ||||
| flowchart LR | ||||
|     A{A} --> B & C | ||||
|     subgraph "subbe" | ||||
|       A | ||||
|     end | ||||
| </pre | ||||
|     > | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
| --- | ||||
| config: | ||||
|   layout: elk | ||||
|   | ||||
| @@ -39,8 +39,8 @@ graph TB | ||||
|  | ||||
|     <script type="module"> | ||||
|       import mermaid from '/mermaid.esm.mjs'; | ||||
|       import flowchartELK from '/mermaid-flowchart-elk.esm.mjs'; | ||||
|       await mermaid.registerExternalDiagrams([flowchartELK]); | ||||
|       import layouts from '/mermaid-layout-elk.esm.mjs'; | ||||
|       mermaid.registerLayoutLoaders(layouts); | ||||
|       async function render(str) { | ||||
|         const { svg } = await mermaid.render('dynamic', str); | ||||
|         document.getElementById('dynamicDiagram').innerHTML = svg; | ||||
|   | ||||
| @@ -500,7 +500,7 @@ mermaid.ganttConfig = { | ||||
|   sectionFontSize: 24, // Font size for sections | ||||
|   numberSectionStyles: 1, // The number of alternating section styles | ||||
|   axisFormat: '%d/%m', // Date/time format of the axis | ||||
|   tickInterval: '1 week', // Axis ticks | ||||
|   tickInterval: '1week', // Axis ticks | ||||
|   topAxis: true, // When this flag is set, date labels will be added to the top of the chart | ||||
|   displayMode: 'compact', // Turns compact mode on | ||||
|   weekday: 'sunday', // On which day a week-based interval should start | ||||
|   | ||||
| @@ -86,7 +86,7 @@ todo[Todo] | ||||
|  | ||||
| ## Configuration Options | ||||
|  | ||||
| You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example: | ||||
| You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams `ticketBaseUrl`. This can be set as in the the following example: | ||||
|  | ||||
| ```yaml | ||||
| --- | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| # @mermaid-js/layout-elk | ||||
|  | ||||
| ## 0.1.7 | ||||
|  | ||||
| ### Patch Changes | ||||
|  | ||||
| - [#6090](https://github.com/mermaid-js/mermaid/pull/6090) [`654097c`](https://github.com/mermaid-js/mermaid/commit/654097c43801b2d606bc3d2bef8c6fbc3301e9e4) Thanks [@knsv](https://github.com/knsv)! - fix: Updated offset calculations for diamond shape when handling intersections | ||||
|  | ||||
| ## 0.1.6 | ||||
|  | ||||
| ### Patch Changes | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@mermaid-js/layout-elk", | ||||
|   "version": "0.1.6", | ||||
|   "version": "0.1.7", | ||||
|   "description": "ELK layout engine for mermaid", | ||||
|   "module": "dist/mermaid-layout-elk.core.mjs", | ||||
|   "types": "dist/layouts.d.ts", | ||||
|   | ||||
| @@ -705,14 +705,11 @@ export const render = async ( | ||||
|     bounds: { x: any; y: any; width: any; height: any; padding: any }, | ||||
|     isDiamond: boolean | ||||
|   ) => { | ||||
|     log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); | ||||
|     log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond); | ||||
|     const points: any[] = []; | ||||
|     let lastPointOutside = _points[0]; | ||||
|     let isInside = false; | ||||
|     _points.forEach((point: any) => { | ||||
|       // const node = clusterDb[edge.toCluster].node; | ||||
|       log.debug(' checking point', point, bounds); | ||||
|  | ||||
|       // check if point is inside the boundary rect | ||||
|       if (!outsideNode(bounds, point) && !isInside) { | ||||
|         // First point inside the rect found | ||||
| @@ -906,7 +903,7 @@ export const render = async ( | ||||
|  | ||||
|       const offset = calcOffset(sourceId, targetId, parentLookupDb); | ||||
|       log.debug( | ||||
|         'offset', | ||||
|         'APA18 offset', | ||||
|         offset, | ||||
|         sourceId, | ||||
|         ' ==> ', | ||||
| @@ -971,29 +968,22 @@ export const render = async ( | ||||
|         } | ||||
|         if (startNode.shape === 'diamond' || startNode.shape === 'diam') { | ||||
|           edge.points.unshift({ | ||||
|             x: startNode.x + startNode.width / 2 + offset.x, | ||||
|             y: startNode.y + startNode.height / 2 + offset.y, | ||||
|             x: startNode.offset.posX + startNode.width / 2, | ||||
|             y: startNode.offset.posY + startNode.height / 2, | ||||
|           }); | ||||
|         } | ||||
|         if (endNode.shape === 'diamond' || endNode.shape === 'diam') { | ||||
|           const x = endNode.x + endNode.width / 2 + offset.x; | ||||
|           // Add a point at the center of the diamond | ||||
|           if ( | ||||
|             Math.abs(edge.points[edge.points.length - 1].y - endNode.y - offset.y) > 0.01 || | ||||
|             Math.abs(edge.points[edge.points.length - 1].x - x) > 0.001 | ||||
|           ) { | ||||
|           edge.points.push({ | ||||
|               x: endNode.x + endNode.width / 2 + offset.x, | ||||
|               y: endNode.y + endNode.height / 2 + offset.y, | ||||
|             x: endNode.offset.posX + endNode.width / 2, | ||||
|             y: endNode.offset.posY + endNode.height / 2, | ||||
|           }); | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         edge.points = cutPathAtIntersect( | ||||
|           edge.points.reverse(), | ||||
|           { | ||||
|             x: startNode.x + startNode.width / 2 + offset.x, | ||||
|             y: startNode.y + startNode.height / 2 + offset.y, | ||||
|             x: startNode.offset.posX + startNode.width / 2, | ||||
|             y: startNode.offset.posY + startNode.height / 2, | ||||
|             width: sw, | ||||
|             height: startNode.height, | ||||
|             padding: startNode.padding, | ||||
| @@ -1004,8 +994,8 @@ export const render = async ( | ||||
|         edge.points = cutPathAtIntersect( | ||||
|           edge.points, | ||||
|           { | ||||
|             x: endNode.x + ew / 2 + endNode.offset.x, | ||||
|             y: endNode.y + endNode.height / 2 + endNode.offset.y, | ||||
|             x: endNode.offset.posX + endNode.width / 2, | ||||
|             y: endNode.offset.posY + endNode.height / 2, | ||||
|             width: ew, | ||||
|             height: endNode.height, | ||||
|             padding: endNode.padding, | ||||
|   | ||||
| @@ -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,12 +213,18 @@ 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 | ||||
|  */ | ||||
| const getDataStructures = () => { | ||||
|   if (state.records.dataStructures === undefined) { | ||||
|     // Tracks how groups are aligned with one another. Generated while creating the adj list | ||||
|     const groupAlignments: Record< | ||||
|       string, | ||||
|       Record<string, Exclude<ArchitectureAlignment, 'bend'>> | ||||
|     > = {}; | ||||
|  | ||||
|     // 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 | ||||
| @@ -224,6 +232,19 @@ const getDataStructures = () => { | ||||
|       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); | ||||
| @@ -245,6 +266,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> | ||||
| @@ -283,6 +305,7 @@ const getDataStructures = () => { | ||||
|     state.records.dataStructures = { | ||||
|       adjList, | ||||
|       spatialMaps, | ||||
|       groupAlignments, | ||||
|     }; | ||||
|   } | ||||
|   return state.records.dataStructures; | ||||
|   | ||||
| @@ -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,91 @@ 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 nodes with vertical alignments to one another | ||||
|    * | ||||
|    * See: #5952 | ||||
|    * | ||||
|    * @param alignmentObj - alignment object with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group | ||||
|    * @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]) => { | ||||
|         // prev is the mapping of x/y coordinate to an array of the nodes in that row/column | ||||
|         let cnt = 0; | ||||
|         const arr = Object.entries(alignments); // [group name, array of nodes within the group on axis dir] | ||||
|         if (arr.length === 1) { | ||||
|           // If only one group exists in the row/column, we don't need to do anything else | ||||
|           prev[dir] = arr[0][1]; | ||||
|           return prev; | ||||
|         } | ||||
|         for (let i = 0; i < arr.length - 1; i++) { | ||||
|           for (let j = i + 1; j < arr.length; j++) { | ||||
|             const [aGroupId, aNodeIds] = arr[i]; | ||||
|             const [bGroupId, bNodeIds] = arr[j]; | ||||
|             const alignment = groupAlignments[aGroupId]?.[bGroupId]; // Get how the two groups are intended to align (undefined if they aren't) | ||||
|  | ||||
|             if (alignment === alignmentDir) { | ||||
|               // If the intended alignment between the two groups is the same as the alignment we are parsing | ||||
|               prev[dir] ??= []; | ||||
|               prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev | ||||
|             } else if (aGroupId === 'default' || bGroupId === 'default') { | ||||
|               // If either of the groups are in the default space (not in a group), use the same behavior as above | ||||
|               prev[dir] ??= []; | ||||
|               prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; | ||||
|             } else { | ||||
|               // Otherwise, the nodes in the two groups are not intended to align | ||||
|               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 | ||||
|       ), | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
| @@ -244,7 +312,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'); | ||||
| @@ -318,9 +387,8 @@ function layoutArchitecture( | ||||
|     addServices(services, cy); | ||||
|     addJunctions(junctions, cy); | ||||
|     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 +522,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); | ||||
|   | ||||
| @@ -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'>; | ||||
| @@ -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<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> { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { getConfig } from '../../diagram-api/diagramAPI.js'; | ||||
| import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; | ||||
| import { log } from '../../logger.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/getDiagramElement.js'; | ||||
| import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; | ||||
| import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; | ||||
| import type { LayoutData } from '../../rendering-util/types.js'; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { select } from 'd3'; | ||||
| import { getConfig } from '../../diagram-api/diagramAPI.js'; | ||||
| import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; | ||||
| import { log } from '../../logger.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/getDiagramElement.js'; | ||||
| import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; | ||||
| import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; | ||||
| import type { LayoutData } from '../../rendering-util/types.js'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { getConfig } from '../../diagram-api/diagramAPI.js'; | ||||
| import type { DiagramStyleClassDef } from '../../diagram-api/types.js'; | ||||
| import { log } from '../../logger.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; | ||||
| import { getDiagramElement } from '../../rendering-util/getDiagramElement.js'; | ||||
| import { render } from '../../rendering-util/render.js'; | ||||
| import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; | ||||
| import type { LayoutData } from '../../rendering-util/types.js'; | ||||
|   | ||||
| @@ -390,7 +390,7 @@ mermaid.ganttConfig = { | ||||
|   sectionFontSize: 24, // Font size for sections | ||||
|   numberSectionStyles: 1, // The number of alternating section styles | ||||
|   axisFormat: '%d/%m', // Date/time format of the axis | ||||
|   tickInterval: '1 week', // Axis ticks | ||||
|   tickInterval: '1week', // Axis ticks | ||||
|   topAxis: true, // When this flag is set, date labels will be added to the top of the chart | ||||
|   displayMode: 'compact', // Turns compact mode on | ||||
|   weekday: 'sunday', // On which day a week-based interval should start | ||||
|   | ||||
| @@ -64,7 +64,7 @@ todo[Todo] | ||||
|  | ||||
| ## Configuration Options | ||||
|  | ||||
| You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example: | ||||
| You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams `ticketBaseUrl`. This can be set as in the the following example: | ||||
|  | ||||
| ```yaml | ||||
| --- | ||||
|   | ||||
							
								
								
									
										20
									
								
								packages/mermaid/src/rendering-util/insertElementsForSize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/mermaid/src/rendering-util/insertElementsForSize.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import type { SVG } from '../diagram-api/types.js'; | ||||
| import { getConfig } from '../diagram-api/diagramAPI.js'; | ||||
| import type { LayoutData } from './types.js'; | ||||
| import { insertNode } from './rendering-elements/nodes.js'; | ||||
|  | ||||
| export async function insertElementsForSize(el: SVG, data: LayoutData) { | ||||
|   const nodesElem = el.insert('g').attr('class', 'nodes'); | ||||
|   el.insert('g').attr('class', 'edges'); | ||||
|   for (const item of data.nodes) { | ||||
|     if (!item.isGroup) { | ||||
|       const node = item; | ||||
|       const config = getConfig(); | ||||
|       const newNode = await insertNode(nodesElem, node, { config, dir: node.dir }); | ||||
|       const boundingBox = newNode.node()!.getBBox(); | ||||
|       item.domId = newNode.attr('id'); | ||||
|       item.width = boundingBox.width; | ||||
|       item.height = boundingBox.height; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -3,6 +3,7 @@ import type { InternalHelpers } from '../internals.js'; | ||||
| import { internalHelpers } from '../internals.js'; | ||||
| import { log } from '../logger.js'; | ||||
| import type { LayoutData } from './types.js'; | ||||
| import { insertElementsForSize } from './insertElementsForSize.js'; | ||||
|  | ||||
| export interface RenderOptions { | ||||
|   algorithm?: string; | ||||
| @@ -51,6 +52,8 @@ export const render = async (data4Layout: LayoutData, svg: SVG) => { | ||||
|  | ||||
|   const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm]; | ||||
|   const layoutRenderer = await layoutDefinition.loader(); | ||||
|   // Add the bounding box to the layout so that the render can run | ||||
|   await insertElementsForSize(svg, data4Layout); | ||||
|   return layoutRenderer.render(data4Layout, svg, internalHelpers, { | ||||
|     algorithm: layoutDefinition.algorithm, | ||||
|   }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user