Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-architecture-fcose-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': minor
---

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
23 changes: 23 additions & 0 deletions cypress/integration/rendering/architecture/architecture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,29 @@ describe('architecture diagram', () => {
});
});

describe('architecture - fcose layout knobs', () => {
// A linear chain demonstrates `idealEdgeLengthMultiplier` cleanly: bumping the multiplier
// visibly stretches the gap between successive nodes. The 3-DB β†’ MCP repro for #6120 is
// not used here because that case is rooted in the BFS spatial-map collapsing siblings to
// the same coordinate before fcose runs, which the knobs in this PR cannot escape; the
// declarative `align row|column` directive (separate PR) is the actual fix for that.
const chain = `architecture-beta
service a(server)[A]
service b(server)[B]
service c(server)[C]
a:R --> L:b
b:R --> L:c
`;

it('should render with default fcose knobs', () => {
imgSnapshotTest(chain);
});

it('should render with an increased idealEdgeLengthMultiplier', () => {
imgSnapshotTest(chain, { architecture: { idealEdgeLengthMultiplier: 3 } });
});
});

describe('architecture - external', () => {
it('should allow adding external icons', () => {
urlSnapshotTest('/architecture-external.html');
Expand Down
25 changes: 25 additions & 0 deletions docs/syntax/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,31 @@ mermaid.initialize({
| ----------- | ------- | ------- | ---------------------------------------------------------------------- |
| `randomize` | boolean | `false` | Whether to randomize initial node positions before running the layout. |

### Layout tuning (v\<MERMAID_RELEASE_VERSION>+)

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:

| Option | Type | Default | Description |
| --------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nodeSeparation` | number | `75` | Minimum separation, in pixels, between sibling nodes in the same group. Pass-through to fcose. |
| `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. |
| `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. |
| `numIter` | number | `2500` | Maximum fcose iterations. Increase for higher-quality layouts on large diagrams at the cost of render time. |

Example β€” bumping `idealEdgeLengthMultiplier` stretches the spacing between connected nodes in a chain:

```
%%{init: {"architecture": {"idealEdgeLengthMultiplier": 3}}}%%
architecture-beta
service a(server)[A]
service b(server)[B]
service c(server)[C]
a:R --> L:b
b:R --> L:c
```

> **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.

## Icons

By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.
Expand Down
27 changes: 27 additions & 0 deletions packages/mermaid/src/config.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,33 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
*
*/
randomize?: boolean;
/**
* Minimum separation (in pixels) between sibling nodes in the same group, passed through to the
* underlying fcose layout. Increase to spread overlapping siblings apart when many edges share the
* same port direction.
*
*/
nodeSeparation?: number;
/**
* Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the
* same group. Increase to add breathing room; decrease to pack the diagram tighter. Edges crossing
* group boundaries are unaffected and use a fixed shorter length.
*
*/
idealEdgeLengthMultiplier?: number;
/**
* Spring elasticity (0–1) applied to edges between nodes within the same group, passed through to
* fcose. Higher values pull connected nodes closer together; lower values let the layout spread them
* out. Edges crossing group boundaries are unaffected.
*
*/
edgeElasticity?: number;
/**
* Maximum number of iterations the fcose layout algorithm runs before stopping. Increase for higher
* quality on large or densely-connected diagrams at the cost of render time.
*
*/
numIter?: number;
}
/**
* The object containing configurations specific for mindmap diagrams
Expand Down
39 changes: 38 additions & 1 deletion packages/mermaid/src/diagrams/architecture/architecture.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { it, describe, expect, vi } from 'vitest';
import { it, describe, expect, vi, beforeEach, afterEach } from 'vitest';
import cytoscape from 'cytoscape';
import { parser } from './architectureParser.js';
import { ArchitectureDB } from './architectureDb.js';
import { setConfig, reset as resetConfig } from '../../config.js';
describe('architecture diagrams', () => {
let db: ArchitectureDB;
beforeEach(() => {
Expand Down Expand Up @@ -158,6 +159,42 @@ describe('architecture diagrams', () => {
});
});

describe('fcose layout config', () => {
afterEach(() => {
resetConfig();
});

it('should default the fcose knobs to documented values', () => {
expect(db.getConfigField('nodeSeparation')).toBe(75);
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(1.5);
expect(db.getConfigField('edgeElasticity')).toBe(0.45);
expect(db.getConfigField('numIter')).toBe(2500);
});

it('should round-trip user-supplied fcose knobs', () => {
setConfig({
architecture: {
nodeSeparation: 120,
idealEdgeLengthMultiplier: 2,
edgeElasticity: 0.6,
numIter: 5000,
},
});
expect(db.getConfigField('nodeSeparation')).toBe(120);
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(2);
expect(db.getConfigField('edgeElasticity')).toBe(0.6);
expect(db.getConfigField('numIter')).toBe(5000);
});

it('should leave defaults intact when only one knob is set', () => {
setConfig({ architecture: { nodeSeparation: 200 } });
expect(db.getConfigField('nodeSeparation')).toBe(200);
expect(db.getConfigField('idealEdgeLengthMultiplier')).toBe(1.5);
expect(db.getConfigField('edgeElasticity')).toBe(0.45);
expect(db.getConfigField('numIter')).toBe(2500);
});
});

describe('addJunction validation', () => {
it('should throw if junction id is already in use by a service', () => {
db.addGroup({ id: 'g1', title: 'Group' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,17 @@ function layoutArchitecture(
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db);

const iconSize = db.getConfigField('iconSize');
const sameGroupIdealLength = db.getConfigField('idealEdgeLengthMultiplier') * iconSize;
const crossGroupIdealLength = 0.5 * iconSize;
const sameGroupElasticity = db.getConfigField('edgeElasticity');

const layout = cy.layout({
name: 'fcose',
quality: 'proof',
randomize: db.getConfigField('randomize'),
nodeSeparation: db.getConfigField('nodeSeparation'),
numIter: db.getConfigField('numIter'),
styleEnabled: false,
animate: false,
nodeDimensionsIncludeLabels: false,
Expand All @@ -419,18 +426,13 @@ function layoutArchitecture(
const [nodeA, nodeB] = edge.connectedNodes();
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity =
parentA === parentB
? 1.5 * db.getConfigField('iconSize')
: 0.5 * db.getConfigField('iconSize');
return elasticity;
return parentA === parentB ? sameGroupIdealLength : crossGroupIdealLength;
},
edgeElasticity(edge: EdgeSingular) {
const [nodeA, nodeB] = edge.connectedNodes();
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity = parentA === parentB ? 0.45 : 0.001;
return elasticity;
return parentA === parentB ? sameGroupElasticity : 0.001;
},
alignmentConstraint,
relativePlacementConstraint,
Expand Down
25 changes: 25 additions & 0 deletions packages/mermaid/src/docs/syntax/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,31 @@ mermaid.initialize({
| ----------- | ------- | ------- | ---------------------------------------------------------------------- |
| `randomize` | boolean | `false` | Whether to randomize initial node positions before running the layout. |

### Layout tuning (v<MERMAID_RELEASE_VERSION>+)

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:

| Option | Type | Default | Description |
| --------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nodeSeparation` | number | `75` | Minimum separation, in pixels, between sibling nodes in the same group. Pass-through to fcose. |
| `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. |
| `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. |
| `numIter` | number | `2500` | Maximum fcose iterations. Increase for higher-quality layouts on large diagrams at the cost of render time. |

Example β€” bumping `idealEdgeLengthMultiplier` stretches the spacing between connected nodes in a chain:

```
%%{init: {"architecture": {"idealEdgeLengthMultiplier": 3}}}%%
architecture-beta
service a(server)[A]
service b(server)[B]
service c(server)[C]
a:R --> L:b
b:R --> L:c
```

> **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.

## Icons

By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.
Expand Down
31 changes: 31 additions & 0 deletions packages/mermaid/src/schemas/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
- iconSize
- fontSize
- randomize
- nodeSeparation
- idealEdgeLengthMultiplier
- edgeElasticity
- numIter
properties:
padding:
type: number
Expand All @@ -999,6 +1003,33 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
When true, nodes start at random positions, which may produce varied but potentially better-spaced layouts.
type: boolean
default: false
nodeSeparation:
description: |
Minimum separation (in pixels) between sibling nodes in the same group, passed through to the
underlying fcose layout. Increase to spread overlapping siblings apart when many edges share the
same port direction.
type: number
default: 75
idealEdgeLengthMultiplier:
description: |
Multiplier applied to `iconSize` to compute the ideal length of edges between nodes within the
same group. Increase to add breathing room; decrease to pack the diagram tighter. Edges crossing
group boundaries are unaffected and use a fixed shorter length.
type: number
default: 1.5
edgeElasticity:
description: |
Spring elasticity (0–1) applied to edges between nodes within the same group, passed through to
fcose. Higher values pull connected nodes closer together; lower values let the layout spread them
out. Edges crossing group boundaries are unaffected.
type: number
default: 0.45
numIter:
description: |
Maximum number of iterations the fcose layout algorithm runs before stopping. Increase for higher
quality on large or densely-connected diagrams at the cost of render time.
type: number
default: 2500

MindmapDiagramConfig:
title: Mindmap Diagram Config
Expand Down
Loading