Skip to content

Commit 91b1ccf

Browse files
committed
Merge PR #7707 into release/11.15.0
The current `develop` branch has some issues, but this PR looks good to go into the v11.15.0 release.
2 parents 84abc58 + 9592079 commit 91b1ccf

8 files changed

Lines changed: 183 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'mermaid': minor
3+
---
4+
5+
feat: expose four fcose layout knobs for architecture-beta diagrams (`nodeSeparation`, `idealEdgeLengthMultiplier`, `edgeElasticity`, `numIter`) so authors can tune layout density and spread overlapping siblings without changing diagram source

cypress/integration/rendering/architecture/architecture.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,29 @@ describe('architecture diagram', () => {
338338
});
339339
});
340340

341+
describe('architecture - fcose layout knobs', () => {
342+
// A linear chain demonstrates `idealEdgeLengthMultiplier` cleanly: bumping the multiplier
343+
// visibly stretches the gap between successive nodes. The 3-DB → MCP repro for #6120 is
344+
// not used here because that case is rooted in the BFS spatial-map collapsing siblings to
345+
// the same coordinate before fcose runs, which the knobs in this PR cannot escape; the
346+
// declarative `align row|column` directive (separate PR) is the actual fix for that.
347+
const chain = `architecture-beta
348+
service a(server)[A]
349+
service b(server)[B]
350+
service c(server)[C]
351+
a:R --> L:b
352+
b:R --> L:c
353+
`;
354+
355+
it('should render with default fcose knobs', () => {
356+
imgSnapshotTest(chain);
357+
});
358+
359+
it('should render with an increased idealEdgeLengthMultiplier', () => {
360+
imgSnapshotTest(chain, { architecture: { idealEdgeLengthMultiplier: 3 } });
361+
});
362+
});
363+
341364
describe('architecture - external', () => {
342365
it('should allow adding external icons', () => {
343366
urlSnapshotTest('/architecture-external.html');

docs/syntax/architecture.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,31 @@ mermaid.initialize({
224224
| ----------- | ------- | ------- | ---------------------------------------------------------------------- |
225225
| `randomize` | boolean | `false` | Whether to randomize initial node positions before running the layout. |
226226

227+
### Layout tuning (v\<MERMAID_RELEASE_VERSION>+)
228+
229+
The following options pass through to the underlying [fcose](https://github.com/iVis-at-Bilkent/cytoscape.js-fcose) layout so you can adjust spacing and density without changing your diagram source:
230+
231+
| Option | Type | Default | Description |
232+
| --------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
233+
| `nodeSeparation` | number | `75` | Minimum separation, in pixels, between sibling nodes in the same group. Pass-through to fcose. |
234+
| `idealEdgeLengthMultiplier` | number | `1.5` | Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the same group. Increase for breathing room, decrease to pack tighter. Cross-group edges are not affected. |
235+
| `edgeElasticity` | number | `0.45` | Spring elasticity (0–1) on same-group edges. Higher pulls connected nodes closer; lower lets them spread out. Cross-group edges are not affected. |
236+
| `numIter` | number | `2500` | Maximum fcose iterations. Increase for higher-quality layouts on large diagrams at the cost of render time. |
237+
238+
Example — bumping `idealEdgeLengthMultiplier` stretches the spacing between connected nodes in a chain:
239+
240+
```
241+
%%{init: {"architecture": {"idealEdgeLengthMultiplier": 3}}}%%
242+
architecture-beta
243+
service a(server)[A]
244+
service b(server)[B]
245+
service c(server)[C]
246+
a:R --> L:b
247+
b:R --> L:c
248+
```
249+
250+
> **Note:** these knobs tune fcose's force-directed layout; they do not change which nodes the layout heuristic considers adjacent. If two siblings render on top of each other because they share the same logical position in the spatial map (a known limitation tracked in [#6120](https://github.com/mermaid-js/mermaid/issues/6120)), no combination of these knobs will move them apart — see the upcoming `align row|column` directive instead.
251+
227252
## Icons
228253

229254
By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.

packages/mermaid/src/config.type.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,33 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
11231123
*
11241124
*/
11251125
randomize?: boolean;
1126+
/**
1127+
* Minimum separation (in pixels) between sibling nodes in the same group, passed through to the
1128+
* underlying fcose layout. Increase to spread overlapping siblings apart when many edges share the
1129+
* same port direction.
1130+
*
1131+
*/
1132+
nodeSeparation?: number;
1133+
/**
1134+
* Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the
1135+
* same group. Increase to add breathing room; decrease to pack the diagram tighter. Edges crossing
1136+
* group boundaries are unaffected and use a fixed shorter length.
1137+
*
1138+
*/
1139+
idealEdgeLengthMultiplier?: number;
1140+
/**
1141+
* Spring elasticity (0–1) applied to edges between nodes within the same group, passed through to
1142+
* fcose. Higher values pull connected nodes closer together; lower values let the layout spread them
1143+
* out. Edges crossing group boundaries are unaffected.
1144+
*
1145+
*/
1146+
edgeElasticity?: number;
1147+
/**
1148+
* Maximum number of iterations the fcose layout algorithm runs before stopping. Increase for higher
1149+
* quality on large or densely-connected diagrams at the cost of render time.
1150+
*
1151+
*/
1152+
numIter?: number;
11261153
}
11271154
/**
11281155
* The object containing configurations specific for mindmap diagrams

packages/mermaid/src/diagrams/architecture/architecture.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { it, describe, expect, vi } from 'vitest';
1+
import { it, describe, expect, vi, beforeEach, afterEach } from 'vitest';
22
import cytoscape from 'cytoscape';
33
import { parser } from './architectureParser.js';
44
import { ArchitectureDB } from './architectureDb.js';
5+
import { setConfig, reset as resetConfig } from '../../config.js';
56
describe('architecture diagrams', () => {
67
let db: ArchitectureDB;
78
beforeEach(() => {
@@ -158,6 +159,42 @@ describe('architecture diagrams', () => {
158159
});
159160
});
160161

162+
describe('fcose layout config', () => {
163+
afterEach(() => {
164+
resetConfig();
165+
});
166+
167+
it('should default the fcose knobs to documented values', () => {
168+
expect(db.getConfigField('nodeSeparation')).toBe(75);
169+
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(1.5);
170+
expect(db.getConfigField('edgeElasticity')).toBe(0.45);
171+
expect(db.getConfigField('numIter')).toBe(2500);
172+
});
173+
174+
it('should round-trip user-supplied fcose knobs', () => {
175+
setConfig({
176+
architecture: {
177+
nodeSeparation: 120,
178+
idealEdgeLengthMultiplier: 2,
179+
edgeElasticity: 0.6,
180+
numIter: 5000,
181+
},
182+
});
183+
expect(db.getConfigField('nodeSeparation')).toBe(120);
184+
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(2);
185+
expect(db.getConfigField('edgeElasticity')).toBe(0.6);
186+
expect(db.getConfigField('numIter')).toBe(5000);
187+
});
188+
189+
it('should leave defaults intact when only one knob is set', () => {
190+
setConfig({ architecture: { nodeSeparation: 200 } });
191+
expect(db.getConfigField('nodeSeparation')).toBe(200);
192+
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(1.5);
193+
expect(db.getConfigField('edgeElasticity')).toBe(0.45);
194+
expect(db.getConfigField('numIter')).toBe(2500);
195+
});
196+
});
197+
161198
describe('addJunction validation', () => {
162199
it('should throw if junction id is already in use by a service', () => {
163200
db.addGroup({ id: 'g1', title: 'Group' });

packages/mermaid/src/diagrams/architecture/architectureRenderer.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,17 @@ function layoutArchitecture(
406406
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
407407
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);
408408

409+
const iconSize = db.getConfigField('iconSize');
410+
const sameGroupIdealLength = db.getConfigField('idealEdgeLengthMultiplier') * iconSize;
411+
const crossGroupIdealLength = 0.5 * iconSize;
412+
const sameGroupElasticity = db.getConfigField('edgeElasticity');
413+
409414
const layout = cy.layout({
410415
name: 'fcose',
411416
quality: 'proof',
412417
randomize: db.getConfigField('randomize'),
418+
nodeSeparation: db.getConfigField('nodeSeparation'),
419+
numIter: db.getConfigField('numIter'),
413420
styleEnabled: false,
414421
animate: false,
415422
nodeDimensionsIncludeLabels: false,
@@ -419,18 +426,13 @@ function layoutArchitecture(
419426
const [nodeA, nodeB] = edge.connectedNodes();
420427
const { parent: parentA } = nodeData(nodeA);
421428
const { parent: parentB } = nodeData(nodeB);
422-
const elasticity =
423-
parentA === parentB
424-
? 1.5 * db.getConfigField('iconSize')
425-
: 0.5 * db.getConfigField('iconSize');
426-
return elasticity;
429+
return parentA === parentB ? sameGroupIdealLength : crossGroupIdealLength;
427430
},
428431
edgeElasticity(edge: EdgeSingular) {
429432
const [nodeA, nodeB] = edge.connectedNodes();
430433
const { parent: parentA } = nodeData(nodeA);
431434
const { parent: parentB } = nodeData(nodeB);
432-
const elasticity = parentA === parentB ? 0.45 : 0.001;
433-
return elasticity;
435+
return parentA === parentB ? sameGroupElasticity : 0.001;
434436
},
435437
alignmentConstraint,
436438
relativePlacementConstraint,

packages/mermaid/src/docs/syntax/architecture.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,31 @@ mermaid.initialize({
186186
| ----------- | ------- | ------- | ---------------------------------------------------------------------- |
187187
| `randomize` | boolean | `false` | Whether to randomize initial node positions before running the layout. |
188188

189+
### Layout tuning (v<MERMAID_RELEASE_VERSION>+)
190+
191+
The following options pass through to the underlying [fcose](https://github.com/iVis-at-Bilkent/cytoscape.js-fcose) layout so you can adjust spacing and density without changing your diagram source:
192+
193+
| Option | Type | Default | Description |
194+
| --------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
195+
| `nodeSeparation` | number | `75` | Minimum separation, in pixels, between sibling nodes in the same group. Pass-through to fcose. |
196+
| `idealEdgeLengthMultiplier` | number | `1.5` | Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the same group. Increase for breathing room, decrease to pack tighter. Cross-group edges are not affected. |
197+
| `edgeElasticity` | number | `0.45` | Spring elasticity (0–1) on same-group edges. Higher pulls connected nodes closer; lower lets them spread out. Cross-group edges are not affected. |
198+
| `numIter` | number | `2500` | Maximum fcose iterations. Increase for higher-quality layouts on large diagrams at the cost of render time. |
199+
200+
Example — bumping `idealEdgeLengthMultiplier` stretches the spacing between connected nodes in a chain:
201+
202+
```
203+
%%{init: {"architecture": {"idealEdgeLengthMultiplier": 3}}}%%
204+
architecture-beta
205+
service a(server)[A]
206+
service b(server)[B]
207+
service c(server)[C]
208+
a:R --> L:b
209+
b:R --> L:c
210+
```
211+
212+
> **Note:** these knobs tune fcose's force-directed layout; they do not change which nodes the layout heuristic considers adjacent. If two siblings render on top of each other because they share the same logical position in the spatial map (a known limitation tracked in [#6120](https://github.com/mermaid-js/mermaid/issues/6120)), no combination of these knobs will move them apart — see the upcoming `align row|column` directive instead.
213+
189214
## Icons
190215

191216
By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.

packages/mermaid/src/schemas/config.schema.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
982982
- iconSize
983983
- fontSize
984984
- randomize
985+
- nodeSeparation
986+
- idealEdgeLengthMultiplier
987+
- edgeElasticity
988+
- numIter
985989
properties:
986990
padding:
987991
type: number
@@ -999,6 +1003,33 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
9991003
When true, nodes start at random positions, which may produce varied but potentially better-spaced layouts.
10001004
type: boolean
10011005
default: false
1006+
nodeSeparation:
1007+
description: |
1008+
Minimum separation (in pixels) between sibling nodes in the same group, passed through to the
1009+
underlying fcose layout. Increase to spread overlapping siblings apart when many edges share the
1010+
same port direction.
1011+
type: number
1012+
default: 75
1013+
idealEdgeLengthMultiplier:
1014+
description: |
1015+
Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the
1016+
same group. Increase to add breathing room; decrease to pack the diagram tighter. Edges crossing
1017+
group boundaries are unaffected and use a fixed shorter length.
1018+
type: number
1019+
default: 1.5
1020+
edgeElasticity:
1021+
description: |
1022+
Spring elasticity (0–1) applied to edges between nodes within the same group, passed through to
1023+
fcose. Higher values pull connected nodes closer together; lower values let the layout spread them
1024+
out. Edges crossing group boundaries are unaffected.
1025+
type: number
1026+
default: 0.45
1027+
numIter:
1028+
description: |
1029+
Maximum number of iterations the fcose layout algorithm runs before stopping. Increase for higher
1030+
quality on large or densely-connected diagrams at the cost of render time.
1031+
type: number
1032+
default: 2500
10021033

10031034
MindmapDiagramConfig:
10041035
title: Mindmap Diagram Config

0 commit comments

Comments
 (0)