Compare commits

...

20 Commits

Author SHA1 Message Date
shubham-mermaid
28f9d3b37b Use tokens from secrets. 2025-06-24 12:14:33 +05:30
shubham-mermaid
4e5a81e6e3 Revert "updated codecov token"
This reverts commit 42a6eeeef7.
2025-06-23 16:11:45 +05:30
shubham-mermaid
42a6eeeef7 updated codecov token 2025-06-23 15:56:26 +05:30
shubham-mermaid
61f51bddb4 Merge branch 'develop' into 6671-code-coverage 2025-06-23 15:09:20 +05:30
shubham-mermaid
afded8d957 testing vi coverage 2025-06-23 15:07:56 +05:30
shubham-mermaid
339927d7e5 Trigger Build 2025-06-16 13:35:40 +05:30
shubham-mermaid
2785ca47a0 Added cypress testcase for ipsepCola layout 2025-06-11 14:36:05 +05:30
shubham-mermaid
b70abed096 Updated nodeOrdering test case 2025-06-10 20:50:39 +05:30
autofix-ci[bot]
1a5a846d71 [autofix.ci] apply automated fixes 2025-06-10 15:17:07 +00:00
shubham-mermaid
20ddc12b42 Removed duplicate files 2025-06-10 20:42:00 +05:30
shubham-mermaid
5e037ee315 Added documentations for implemetation of layout algorithms 2025-06-10 20:19:18 +05:30
shubham-mermaid
70eedd840d Added changeset for layout algorithm 2025-06-10 19:25:59 +05:30
shubham-mermaid
bb0aa4009c Updated test cases for ipsepcola layout algorithm 2025-06-10 14:29:23 +05:30
autofix-ci[bot]
75f940bf9e [autofix.ci] apply automated fixes 2025-06-10 08:04:36 +00:00
shubham-mermaid
a46de01885 Updated ts import to js 2025-06-10 13:29:27 +05:30
shubham-mermaid
a7e9f4f926 Updated all ts imports to js 2025-06-10 13:24:39 +05:30
shubham-mermaid
99c8224dfa Updated folder name 2025-06-10 13:16:42 +05:30
shubham-mermaid
13f8549f81 Added test cases for layer assignments, node ordering and assigning initial positions for ipsepcola layout 2025-06-10 10:12:06 +05:30
shubham-mermaid
cfd25ed33e Updated calculateIterations for ipsepcola layout 2025-06-10 10:07:36 +05:30
shubham-mermaid
3a8952ebe0 Added support for ipsep-cola layout algorithm 2025-06-09 20:38:52 +05:30
30 changed files with 4551 additions and 10 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Added IpSepCoLa algorithm for layout

View File

@@ -12,6 +12,7 @@ gantt
gitgraph gitgraph
gzipped gzipped
handDrawn handDrawn
ipsepcola
kanban kanban
marginx marginx
marginy marginy

View File

@@ -148,4 +148,4 @@ jobs:
name: mermaid-codecov name: mermaid-codecov
fail_ci_if_error: false fail_ci_if_error: false
verbose: true verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766 token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -52,4 +52,4 @@ jobs:
name: mermaid-codecov name: mermaid-codecov
fail_ci_if_error: false fail_ci_if_error: false
verbose: true verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766 token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -0,0 +1,153 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
describe('Flowchart IPSepCoLa', () => {
it('1-ipsepCola: should render a simple flowchart', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`
);
});
it('2-ipsepCola: handle bidirectional edges', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart TD
subgraph D
A --> B
A --> B
B --> A
B --> A
end
`
);
});
it('3-ipsepCola: handle multiple self loops', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart
a --> a
a --> a
a --> a
a --> a
`
);
});
it('4-ipsepCola: handle state diagram example', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
`
);
});
it('5-ipsepCola: handle multiple subgraphs with edges between them', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart LR
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
one --> two
three --> two
two --> c2
`
);
});
it('6-ipsepCola: handle class diagram example', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
classDiagram
class AuthService {
+login(username: string, password: string): boolean
+logout(): void
+register(): void
}
class User {
-username: string
-password: string
-role: Role
+changePassword(): void
}
class Role {
-name: string
-permissions: string[]
+hasPermission(): boolean
}
AuthService --> User
User --> Role
`
);
});
it('7-ipsepCola: should render a decision flowchart', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart TD
Start([Start]) --> Prep[Preparation Step]
Prep --> Split{Ready to Process?}
Split -->|Yes| T1[Task A]
Split -->|Yes| T2[Task B]
T1 --> Merge
T2 --> Merge
Merge((Join Results)) --> Finalize[Finalize Process]
Finalize --> End([End])
`
);
});
it('8-ipsepCola: handle nested subgraphs', () => {
imgSnapshotTest(
`---
config:
layout: ipsepCola
---
flowchart LR
subgraph main
subgraph subcontainer
subcontainer-child
end
subcontainer-child--> subcontainer-sibling
end
`
);
});
});

View File

@@ -0,0 +1,601 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<style>
body {
/* background: #333; */
font-family: 'Arial';
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
svg {
border: 3px solid #300;
margin: 10px;
}
pre {
width: 100%;
}
</style>
</head>
<body>
<pre id="diagram4" class="mermaid">
flowchart
A --> B
subgraph hello
C --> D
end
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
A --> B
A --> B
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
A[hello] --> A
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
subgraph C
c
end
A --> C
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
subgraph C
c
end
A --> c
</pre
>
<pre id="diagram4" class="mermaid">
flowchart TD
subgraph D
A --> B
A --> B
B --> A
B --> A
end
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
subgraph B2
A --> B --> C
B --> D
end
B2 --> X
</pre
>
<pre id="diagram4" class="mermaid">
stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
</pre
>
<pre id="diagram4" class="mermaid">
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
</pre
>
<pre id="diagram4" class="mermaid">
flowchart TD
P1
P1 -->P1.5
subgraph P1.5
P2
P2.5(( A ))
P3
end
P2 --> P4
P3 --> P6
P1.5 --> P5
</pre>
<pre id="diagram4" class="mermaid">
%% Length of edges
flowchart TD
L1 --- L2
L2 --- C
M1 ---> C
R1 .-> R2
R2 <.-> C
C -->|Label 1| E1
C <-- Label 2 ---> E2
C ----> E3
C <-...-> E4
C ======> E5
</pre>
<pre id="diagram4" class="mermaid">
%% Stadium shape
flowchart TD
A([stadium shape test])
A -->|Get money| B([Go shopping])
B --> C([Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?])
C -->|One| D([Laptop])
C -->|Two| E([iPhone])
C -->|Three| F([Car<br/>wroom wroom])
click A "index.html#link-clicked" "link test"
click B testClick "click test"
classDef someclass fill:#f96;
class A someclass;
class C someclass;
</pre>
<pre id="diagram4" class="mermaid">
%% should render escaped without html labels
flowchart TD
a["<strong>Haiya</strong>"]---->b
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs in reverse order
flowchart LR
a -->b
subgraph A
B
end
subgraph B
b
end
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs in several levels
flowchart LR
b-->B
a-->c
subgraph O
A
end
subgraph B
c
end
subgraph A
a
b
B
end
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with edges in and out
flowchart LR
internet
nat
routeur
lb1
lb2
compute1
compute2
subgraph project
routeur
nat
subgraph subnet1
compute1
lb1
end
subgraph subnet2
compute2
lb2
end
end
internet --> routeur
routeur --> subnet1 & subnet2
subnet1 & subnet2 --> nat --> internet
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links
flowchart LR
subgraph main
subgraph subcontainer
subcontainer-child
end
subcontainer-child--> subcontainer-sibling
end
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with ingoing links
flowchart LR
subgraph one[One]
subgraph sub_one[Sub One]
_sub_one
end
subgraph sub_two[Sub Two]
_sub_two
end
_one
end
%% here, either the first or the second one
sub_one --> sub_two
_one --> b
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links 3
flowchart LR
subgraph container_Beta
process_C-->Process_D
end
subgraph container_Alpha
process_A-->process_B
process_A-->|messages|process_C
end
process_B-->|via_AWSBatch|container_Beta
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links 4
flowchart LR
subgraph A
a -->b
end
subgraph B
b
end
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links 2
flowchart LR
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
one --> two
three --> two
two --> c2
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links 5
flowchart LR
subgraph container_Beta
process_C-->Process_D
end
subgraph container_Alpha
process_A-->process_B
process_B-->|via_AWSBatch|container_Beta
process_A-->|messages|process_C
end
</pre>
<pre id="diagram4" class="mermaid">
%% More subgraphs
flowchart LR
subgraph two
b1
end
subgraph three
c2
end
three --> two
two --> c2
note[There are two links in this diagram]
</pre>
<pre id="diagram4" class="mermaid">
%% nested subgraphs with outgoing links 5
flowchart LR
A[red text] -->|default style| B(blue text)
C([red text]) -->|default style| D[[blue text]]
E[(red text)] -->|default style| F((blue text))
G>red text] -->|default style| H{blue text}
I{{red text}} -->|default style| J[/blue text/]
K[\\ red text\\] -->|default style| L[/blue text\\]
M[\\ red text/] -->|default style| N[blue text];
O(((red text))) -->|default style| P(((blue text)));
linkStyle default color:Sienna;
style A stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style B stroke:#0000ff,fill:#ccccff,color:#0000ff;
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style D stroke:#0000ff,fill:#ccccff,color:#0000ff;
style E stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style F stroke:#0000ff,fill:#ccccff,color:#0000ff;
style G stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style H stroke:#0000ff,fill:#ccccff,color:#0000ff;
style I stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style J stroke:#0000ff,fill:#ccccff,color:#0000ff;
style K stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style L stroke:#0000ff,fill:#ccccff,color:#0000ff;
style M stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style N stroke:#0000ff,fill:#ccccff,color:#0000ff;
style O stroke:#ff0000,fill:#ffcccc,color:#ff0000;
style P stroke:#0000ff,fill:#ccccff,color:#0000ff;
</pre>
<pre id="diagram4" class="mermaid">
%% labels on edges in a cluster
flowchart RL
subgraph one
a1 -- I am a long label --> a2
a1 -- Another long label --> a2
end
</pre>
<pre id="diagram4" class="mermaid">
%% labels on edges in a cluster
flowchart RL
subgraph one
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
a1 -- Another long label --> a2
a3 --> a1 & a2 & a3 & a4
a1 --> a4
end
</pre>
<pre id="diagram4" class="mermaid">
%% labels on edges in a cluster
flowchart RL
subgraph one
a1[Iam a node with a super long label]
a2[I am another node with a mega long label]
a3[I am a node with a super long label]
a4[I am another node with a mega long label]
a1 -- Another long label --> a2
a3 --> a1 & a2 & a3 & a4
a1 --> a4
end
</pre>
<pre id="diagram4" class="mermaid">
%% labels on edges in a cluster
flowchart RL
a1[I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. ]
a2[I am another node with a mega long label]
a3[I am a node with a super long label]
a4[I am another node with a mega long label]
a1 & a2 & a3 & a4 --> a5 & a6 & a7 & a8 & a9 & a10
</pre>
<pre id="diagram4" class="mermaid">
flowchart
subgraph Z
subgraph X
a --> b
end
subgraph Y
c --> d
end
end
Y --> X
X --> P
P --> Y
</pre
>
<pre id="diagram4" class="mermaid">
flowchart
a --> b
b --> c
b --> d
c --> a
</pre
>
<pre id="diagram3" class="mermaid">
flowchart TD
Start([Start]) --> Prep[Preparation Step]
Prep --> Split{Ready to Process?}
Split -->|Yes| T1[Task A]
Split -->|Yes| T2[Task B]
T1 --> Merge
T2 --> Merge
Merge((Join Results)) --> Finalize[Finalize Process]
Finalize --> End([End])
</pre
>
<pre id="diagram4" class="mermaid">
flowchart TD
A[Start Build] --> B[Compile Source]
B --> C[Test Suite]
C --> D{Tests Passed?}
D -->|No| E[Notify Developer]
E --> A
D -->|Yes| F[Build Docker Image]
subgraph Deploy Pipeline
F --> G[Deploy to Staging]
G --> H[Run Integration Tests]
H --> I{Tests Passed?}
I -->|No| J[Rollback & Alert]
I -->|Yes| K[Deploy to Production]
end
K --> L([Success])
</pre
>
<pre class="mermaid">
classDiagram
class Controller {
+handleRequest(): void
}
class View {
+render(): void
}
class Model {
+getData(): any
+setData(data: any): void
}
Controller --> Model
Controller --> View
Model --> View : notifyChange()
</pre
>
<pre class="mermaid">
classDiagram
class AuthService {
+login(username: string, password: string): boolean
+logout(): void
+register(): void
}
class User {
-username: string
-password: string
-role: Role
+changePassword(): void
}
class Role {
-name: string
-permissions: string[]
+hasPermission(): boolean
}
AuthService --> User
User --> Role
</pre
>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
const staticBellIconPack = {
prefix: 'fa6-regular',
icons: {
bell: {
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
width: 448,
},
},
width: 512,
height: 512,
};
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
},
{
name: 'fa',
loader: () => staticBellIconPack,
},
]);
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
window.callback = function () {
alert('A callback was triggered');
};
function callback() {
alert('It worked');
}
await mermaid.initialize({
theme: 'redux-dark',
// theme: 'default',
// theme: 'forest',
handDrawnSeed: 12,
look: 'classic ',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// layout: 'dagre',
layout: 'ipsepCola',
// layout: 'elk',
// layout: 'sugiyama',
// htmlLabels: false,
flowchart: { titleTopMargin: 10 },
// fontFamily: 'Caveat',
// fontFamily: 'Kalam',
// fontFamily: 'courier',
fontFamily: 'arial',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
kanban: {
htmlLabels: false,
},
fontSize: 12,
logLevel: 0,
securityLevel: 'loose',
callback,
});
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -10,7 +10,7 @@
# Interface: LayoutData # Interface: LayoutData
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145) Defined in: [packages/mermaid/src/rendering-util/types.ts:159](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L159)
## Indexable ## Indexable
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
> **config**: [`MermaidConfig`](MermaidConfig.md) > **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148) Defined in: [packages/mermaid/src/rendering-util/types.ts:162](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L162)
--- ---
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
> **edges**: `Edge`\[] > **edges**: `Edge`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147) Defined in: [packages/mermaid/src/rendering-util/types.ts:161](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L161)
--- ---
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
> **nodes**: `Node`\[] > **nodes**: `Node`\[]
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146) Defined in: [packages/mermaid/src/rendering-util/types.ts:160](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L160)

177
docs/layouts/development.md Normal file
View File

@@ -0,0 +1,177 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/development.md](../../packages/mermaid/src/docs/layouts/development.md).
# 🛠️ How to Create a New Layout Algorithm in Mermaid
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
---
## 📦 Prerequisites
Before starting, ensure the following:
- You have [Node.js](https://nodejs.org/) installed.
- You have [pnpm](https://pnpm.io/) installed globally:
```bash
npm install -g pnpm
```
---
## 🔄 Step-by-Step Integration
### 1. Clone the Mermaid Repository
```bash
git clone https://github.com/mermaid-js/mermaid.git
cd mermaid
```
### 2. Install Dependencies
Mermaid uses `pnpm` for dependency management:
```bash
pnpm i
```
### 3. Start the Development Server
This will spin up a local dev environment with hot reload:
```bash
pnpm dev
```
---
## 🧠 Implementing Your Custom Layout Algorithm
### 4. Create Your Layout Folder
Navigate to the relevant source directory and create a folder for your new algorithm:
```bash
cd packages/mermaid/src/layout
mkdir myCustomLayout
touch myCustomLayout/index.ts
```
> 📁 You can organize supporting files, utils, and types inside this folder.
### 5. Register the Layout Algorithm
Open the file:
```
packages/mermaid/src/rendering-util/render.ts
```
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
```ts
registerDefaultLayoutLoaders([
...,
{
id: 'myCustomLayout',
loader: () => import('../layout/myCustomLayout'),
},
]);
```
This tells Mermaid how to load your layout dynamically by name (`id`).
---
## 🧪 Testing Your Algorithm
### 6. Create a Test File
To visually test your layout implementation, create a test HTML file in:
```
cypress/platform/
```
Example:
```bash
touch cypress/platform/myCustomLayoutTest.html
```
Inside the file, load your diagram like this:
```html
<!DOCTYPE html>
<html>
<head>
<script type="module">
import mermaid from '/dist/mermaid.esm.mjs';
mermaid.initialize({
startOnLoad: true,
theme: 'default',
layout: 'myCustomLayout', // Use your layout here
});
</script>
</head>
<body>
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
</body>
</html>
```
### 7. Open in Browser
After running `pnpm dev`, open your test in the browser:
```
http://localhost:9000/myCustomLayoutTest.html
```
You should see your diagram rendered using your new layout engine.
---
## 📝 Tips
- Keep your layout algorithm modular and readable.
- Use TypeScript types and helper functions for better structure.
- Add comments and constraints where necessary.
- If applicable, create a unit test and add a visual test for Cypress.
---
## 📚 Example File Structure
```
packages/
└── mermaid/
└── src/
└── layout/
└── myCustomLayout/
├── index.ts
├── utils.ts
└── types.ts
```
---
## ✅ Final Checklist
- [ ] All dependencies installed via `pnpm i`
- [ ] Layout folder and files created under `src/layout/`
- [ ] Entry registered in `registerDefaultLayoutLoaders`
- [ ] HTML test file added under `cypress/platform/`
- [ ] Diagram renders as expected at `localhost:9000`
- [ ] Code is linted and documented
---
> 💡 Youre now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!

View File

@@ -0,0 +1,138 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/introduction.md](../../packages/mermaid/src/docs/layouts/introduction.md).
# 📊 Layout Algorithms in Mermaid
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
---
## 🔹 Dagre Layout
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
### Key Features:
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
- **Edge routing**: Attempts to reduce edge crossings and bends.
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
### Technical Overview:
- Works in four stages:
1. **Cycle Removal**
2. **Layer Assignment**
3. **Node Ordering**
4. **Coordinate Assignment**
- Outputs crisp layouts where edge direction is clear and logical.
### Limitations:
- No native support for **grouped or nested structures**.
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
- Limited edge label placement capabilities.
---
## 🔸 ELK (Eclipse Layout Kernel)
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
### Key Features:
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
### Technical Overview:
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
- Different engines are plugged in depending on the chosen layout strategy.
- Works well with large, complex, and deeply nested graphs.
### Limitations:
- Requires verbose configuration for best results.
- Can be slower than Dagre for small or simple diagrams.
- More complex to integrate and control dynamically.
---
## 🆕 IPSep-CoLa
### 🌐 Introduction
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
It is particularly useful for diagrams where:
- **Group integrity** is important (e.g., modules, clusters).
- **Edge labels** need smart placement.
- **Overlaps** must be prevented even under tight space constraints.
---
### ⚙️ How IPSep-CoLa Works
#### 1. **Constraint-Based Force Simulation**:
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
#### 2. **Incremental Refinement**:
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
- Initial layout is produced using a base force simulation.
- The layout is iteratively adjusted using **constraint solvers**.
- Additional forces (spring, collision avoidance, containment) are incrementally added.
#### 3. **Edge Label Handling**:
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
---
### 📌 Use Cases
IPSep-CoLa is ideal for:
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
- **Clustered views** (e.g., social network groupings).
- **Diagrams with heavy labeling** where label placement affects readability.
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
---
## 🔍 Comparison Table
| Feature | Dagre | ELK | IPSep-CoLa (Upcoming) |
| ------------------------- | ----------- | ------------------- | ------------------------------ |
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
---
## 🧾 Summary
Each layout engine in Mermaid serves a different purpose:
- **Dagre** is best for fast, simple, and readable DAGs.
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
---

View File

@@ -0,0 +1,46 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/implementation.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/implementation.md).
## IPSEPCOLA Documentation :
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
## How IPSep-CoLa built :
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
1. Layer Assignment :
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
2. Node ordering:
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
3. AssignInitial positions to node :
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 \_ nodeSpacing, 2 \_ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
4. Force-Directed Simulation with Constraints :
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
- Group Constraints: Ensures child nodes stay near their parent groups.
- Cooling Factor: Gradually reduces movement to stabilize the layout.
5. Incremental Refinement :
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
6. Adjusting the Final Layout :
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.

View File

@@ -0,0 +1,186 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/overview.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/overview.md).
## IPSEPCOLA Documentation :
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
## Introduction :
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
## How IPSep-CoLa Works :
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
1. Graph Preprocessing :
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
2. Force-Directed Simulation with Constraints :
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
Repulsion Forces: Pushes nodes apart to prevent overlaps.
Group Constraints: Ensures child nodes stay near their parent groups.
Cooling Factor: Gradually reduces movement to stabilize the layout.
3. Incremental Refinement :
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
## Key Features :
1. Group-Aware Layout: Maintains separation between nested structures.
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
## Use Cases :
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
2. Network Visualization (dependency graphs, data pipelines)
3. Interactive Graph Editors (real-time layout adjustments)
4. Clustered Data Visualization (UML diagrams, biological networks)
## **Examples**
### **Example 1**
```
---
config:
layout: ipsepCola
---
flowchart TD
CEO --> MKT["Marketing Head"]
CEO --> ENG["Engineering Head"]
ENG --> DEV["Developer"]
ENG --> QA["QA Tester"]
```
### **Example 2**
```
---
config:
layout: ipsepCola
---
flowchart TD
Start["Start"] --> Red{"Is it red?"}
Red -- Yes --> Round{"Is it round?"}
Red -- No --> NotApple["❌ Not an Apple"]
Round -- Yes --> Apple["✅ It's an Apple"]
Round -- No --> NotApple2["❌ Not an Apple"]
```
### **Example 3**
```
---
config:
layout: ipsepCola
---
flowchart TD
A[Module A] --> B[Module B]
A --> C[Module C]
B --> D[Module D]
C --> D
D --> E[Module E]
```
### **Example 4**
```
---
config:
layout: ipsepCola
---
flowchart TD
Source1["📦 Raw Data (CSV)"]
Source2["🌐 API Data"]
Source1 --> Clean["🧹 Clean & Format"]
Source2 --> Clean
Clean --> Transform["🔄 Transform Data"]
Transform --> Load["📥 Load into Data Warehouse"]
Load --> BI["📊 BI Dashboard"]
```
### **Example 5**
```
---
config:
layout: ipsepCola
---
classDiagram
class Person {
-String name
-int age
+greet(): void
}
class Employee {
-int employeeId
+calculateSalary(): float
}
class Manager {
-String department
+assignTask(): void
}
Person <|-- Employee
Employee <|-- Manager
```
### **Example 6**
```
---
config:
layout: ipsepCola
---
flowchart TD
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
Leaf --> Glucose["🍬 Glucose"]
Leaf --> Oxygen["💨 Oxygen"]
```
### **Example 7**
```
---
config:
layout: ipsepCola
---
flowchart TD
Internet["🌐 Internet"] --> Router["📡 Router"]
Router --> Server1["🖥️ Server A"]
Router --> Server2["🖥️ Server B"]
Router --> Laptop["💻 Laptop"]
%% New device joins
Router --> Mobile["📱 Mobile"]
```
## Limitations :
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
2. Parameter Tuning: Requires adjustments for different graph types.
3. Non-Determinism: Small variations may occur between runs due to force simulation.
## Conclusion :
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.

View File

@@ -37,7 +37,7 @@
"e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress", "e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress",
"coverage:cypress:clean": "rimraf .nyc_output coverage/cypress", "coverage:cypress:clean": "rimraf .nyc_output coverage/cypress",
"coverage:merge": "tsx scripts/coverage.ts", "coverage:merge": "tsx scripts/coverage.ts",
"coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge", "coverage": "pnpm test:coverage --run && pnpm coverage:merge",
"ci": "vitest run", "ci": "vitest run",
"test": "pnpm lint && vitest run", "test": "pnpm lint && vitest run",
"test:watch": "vitest --watch", "test:watch": "vitest --watch",

View File

@@ -141,6 +141,7 @@ function sidebarAll() {
], ],
}, },
...sidebarSyntax(), ...sidebarSyntax(),
...sidebarAlgorithms(),
...sidebarEcosystem(), ...sidebarEcosystem(),
...sidebarConfig(), ...sidebarConfig(),
...sidebarCommunity(), ...sidebarCommunity(),
@@ -222,6 +223,27 @@ function sidebarEcosystem() {
]; ];
} }
function sidebarAlgorithms() {
return [
{
text: '🧠 Diagram Algorithms',
collapsed: false,
items: [
{ text: 'Introduction', link: '/layouts/introduction' },
{ text: 'Layout Algorithm Development', link: '/layouts/development' },
{
text: 'IPSep-Cola',
collapsed: false,
items: [
{ text: 'Overview', link: '/layouts/ipsepcola/overview' },
{ text: 'Implementation', link: '/layouts/ipsepcola/implementation' },
],
},
],
},
];
}
function sidebarCommunity() { function sidebarCommunity() {
return [ return [
{ {

View File

@@ -0,0 +1,171 @@
# 🛠️ How to Create a New Layout Algorithm in Mermaid
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
---
## 📦 Prerequisites
Before starting, ensure the following:
- You have [Node.js](https://nodejs.org/) installed.
- You have [pnpm](https://pnpm.io/) installed globally:
```bash
npm install -g pnpm
```
---
## 🔄 Step-by-Step Integration
### 1. Clone the Mermaid Repository
```bash
git clone https://github.com/mermaid-js/mermaid.git
cd mermaid
```
### 2. Install Dependencies
Mermaid uses `pnpm` for dependency management:
```bash
pnpm i
```
### 3. Start the Development Server
This will spin up a local dev environment with hot reload:
```bash
pnpm dev
```
---
## 🧠 Implementing Your Custom Layout Algorithm
### 4. Create Your Layout Folder
Navigate to the relevant source directory and create a folder for your new algorithm:
```bash
cd packages/mermaid/src/layout
mkdir myCustomLayout
touch myCustomLayout/index.ts
```
> 📁 You can organize supporting files, utils, and types inside this folder.
### 5. Register the Layout Algorithm
Open the file:
```
packages/mermaid/src/rendering-util/render.ts
```
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
```ts
registerDefaultLayoutLoaders([
...,
{
id: 'myCustomLayout',
loader: () => import('../layout/myCustomLayout'),
},
]);
```
This tells Mermaid how to load your layout dynamically by name (`id`).
---
## 🧪 Testing Your Algorithm
### 6. Create a Test File
To visually test your layout implementation, create a test HTML file in:
```
cypress/platform/
```
Example:
```bash
touch cypress/platform/myCustomLayoutTest.html
```
Inside the file, load your diagram like this:
```html
<!DOCTYPE html>
<html>
<head>
<script type="module">
import mermaid from '/dist/mermaid.esm.mjs';
mermaid.initialize({
startOnLoad: true,
theme: 'default',
layout: 'myCustomLayout', // Use your layout here
});
</script>
</head>
<body>
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
</body>
</html>
```
### 7. Open in Browser
After running `pnpm dev`, open your test in the browser:
```
http://localhost:9000/myCustomLayoutTest.html
```
You should see your diagram rendered using your new layout engine.
---
## 📝 Tips
- Keep your layout algorithm modular and readable.
- Use TypeScript types and helper functions for better structure.
- Add comments and constraints where necessary.
- If applicable, create a unit test and add a visual test for Cypress.
---
## 📚 Example File Structure
```
packages/
└── mermaid/
└── src/
└── layout/
└── myCustomLayout/
├── index.ts
├── utils.ts
└── types.ts
```
---
## ✅ Final Checklist
- [ ] All dependencies installed via `pnpm i`
- [ ] Layout folder and files created under `src/layout/`
- [ ] Entry registered in `registerDefaultLayoutLoaders`
- [ ] HTML test file added under `cypress/platform/`
- [ ] Diagram renders as expected at `localhost:9000`
- [ ] Code is linted and documented
---
> 💡 Youre now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!

View File

@@ -0,0 +1,132 @@
# 📊 Layout Algorithms in Mermaid
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
---
## 🔹 Dagre Layout
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
### Key Features:
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
- **Edge routing**: Attempts to reduce edge crossings and bends.
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
### Technical Overview:
- Works in four stages:
1. **Cycle Removal**
2. **Layer Assignment**
3. **Node Ordering**
4. **Coordinate Assignment**
- Outputs crisp layouts where edge direction is clear and logical.
### Limitations:
- No native support for **grouped or nested structures**.
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
- Limited edge label placement capabilities.
---
## 🔸 ELK (Eclipse Layout Kernel)
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
### Key Features:
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
### Technical Overview:
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
- Different engines are plugged in depending on the chosen layout strategy.
- Works well with large, complex, and deeply nested graphs.
### Limitations:
- Requires verbose configuration for best results.
- Can be slower than Dagre for small or simple diagrams.
- More complex to integrate and control dynamically.
---
## 🆕 IPSep-CoLa
### 🌐 Introduction
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
It is particularly useful for diagrams where:
- **Group integrity** is important (e.g., modules, clusters).
- **Edge labels** need smart placement.
- **Overlaps** must be prevented even under tight space constraints.
---
### ⚙️ How IPSep-CoLa Works
#### 1. **Constraint-Based Force Simulation**:
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
#### 2. **Incremental Refinement**:
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
- Initial layout is produced using a base force simulation.
- The layout is iteratively adjusted using **constraint solvers**.
- Additional forces (spring, collision avoidance, containment) are incrementally added.
#### 3. **Edge Label Handling**:
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
---
### 📌 Use Cases
IPSep-CoLa is ideal for:
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
- **Clustered views** (e.g., social network groupings).
- **Diagrams with heavy labeling** where label placement affects readability.
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
---
## 🔍 Comparison Table
| Feature | Dagre | ELK | IPSep-CoLa (Upcoming) |
| ------------------------- | ----------- | ------------------- | ------------------------------ |
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
---
## 🧾 Summary
Each layout engine in Mermaid serves a different purpose:
- **Dagre** is best for fast, simple, and readable DAGs.
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
---

View File

@@ -0,0 +1,40 @@
## IPSEPCOLA Documentation :
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
## How IPSep-CoLa built :
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
1. Layer Assignment :
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
2. Node ordering:
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
3. AssignInitial positions to node :
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 _ nodeSpacing, 2 _ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
4. Force-Directed Simulation with Constraints :
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
- Group Constraints: Ensures child nodes stay near their parent groups.
- Cooling Factor: Gradually reduces movement to stabilize the layout.
5. Incremental Refinement :
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
6. Adjusting the Final Layout :
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.

View File

@@ -0,0 +1,180 @@
## IPSEPCOLA Documentation :
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
## Introduction :
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
## How IPSep-CoLa Works :
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
1. Graph Preprocessing :
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
2. Force-Directed Simulation with Constraints :
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
Repulsion Forces: Pushes nodes apart to prevent overlaps.
Group Constraints: Ensures child nodes stay near their parent groups.
Cooling Factor: Gradually reduces movement to stabilize the layout.
3. Incremental Refinement :
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
## Key Features :
1. Group-Aware Layout: Maintains separation between nested structures.
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
## Use Cases :
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
2. Network Visualization (dependency graphs, data pipelines)
3. Interactive Graph Editors (real-time layout adjustments)
4. Clustered Data Visualization (UML diagrams, biological networks)
## **Examples**
### **Example 1**
```
---
config:
layout: ipsepCola
---
flowchart TD
CEO --> MKT["Marketing Head"]
CEO --> ENG["Engineering Head"]
ENG --> DEV["Developer"]
ENG --> QA["QA Tester"]
```
### **Example 2**
```
---
config:
layout: ipsepCola
---
flowchart TD
Start["Start"] --> Red{"Is it red?"}
Red -- Yes --> Round{"Is it round?"}
Red -- No --> NotApple["❌ Not an Apple"]
Round -- Yes --> Apple["✅ It's an Apple"]
Round -- No --> NotApple2["❌ Not an Apple"]
```
### **Example 3**
```
---
config:
layout: ipsepCola
---
flowchart TD
A[Module A] --> B[Module B]
A --> C[Module C]
B --> D[Module D]
C --> D
D --> E[Module E]
```
### **Example 4**
```
---
config:
layout: ipsepCola
---
flowchart TD
Source1["📦 Raw Data (CSV)"]
Source2["🌐 API Data"]
Source1 --> Clean["🧹 Clean & Format"]
Source2 --> Clean
Clean --> Transform["🔄 Transform Data"]
Transform --> Load["📥 Load into Data Warehouse"]
Load --> BI["📊 BI Dashboard"]
```
### **Example 5**
```
---
config:
layout: ipsepCola
---
classDiagram
class Person {
-String name
-int age
+greet(): void
}
class Employee {
-int employeeId
+calculateSalary(): float
}
class Manager {
-String department
+assignTask(): void
}
Person <|-- Employee
Employee <|-- Manager
```
### **Example 6**
```
---
config:
layout: ipsepCola
---
flowchart TD
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
Leaf --> Glucose["🍬 Glucose"]
Leaf --> Oxygen["💨 Oxygen"]
```
### **Example 7**
```
---
config:
layout: ipsepCola
---
flowchart TD
Internet["🌐 Internet"] --> Router["📡 Router"]
Router --> Server1["🖥️ Server A"]
Router --> Server2["🖥️ Server B"]
Router --> Laptop["💻 Laptop"]
%% New device joins
Router --> Mobile["📱 Mobile"]
```
## Limitations :
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
2. Parameter Tuning: Requires adjustments for different graph types.
3. Non-Determinism: Small variations may occur between runs due to force simulation.
## Conclusion :
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.

View File

@@ -0,0 +1,148 @@
import type { Selection } from 'd3';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
import type { LayoutData, NonClusterNode } from './types.js';
import { getConfig } from '../diagram-api/diagramAPI.js';
import { insertNode } from './rendering-elements/nodes.js';
type D3Selection<T extends SVGElement = SVGElement> = Selection<
T,
unknown,
Element | null,
unknown
>;
/**
* Creates a graph by merging the graph construction and DOM element insertion.
*
* This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes)
* into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions
* are computed using each node's bounding box.
*
* @param element - The D3 selection in which the SVG groups are inserted.
* @param data4Layout - The layout data containing nodes and edges.
* @returns A promise resolving to an object containing the graph and the inserted groups.
*/
export async function createGraphWithElements(
element: D3Selection,
data4Layout: LayoutData
): Promise<{
graph: graphlib.Graph;
groups: {
clusters: D3Selection<SVGGElement>;
edgePaths: D3Selection<SVGGElement>;
edgeLabels: D3Selection<SVGGElement>;
nodes: D3Selection<SVGGElement>;
rootGroups: D3Selection<SVGGElement>;
};
nodeElements: Map<string, D3Selection<SVGElement | SVGGElement>>;
}> {
const graph = new graphlib.Graph({
multigraph: true,
compound: true,
});
const edgesToProcess = [...data4Layout.edges];
const config = getConfig();
// Create groups for clusters, edge paths, edge labels, and nodes.
const rootGroups = element.insert('g').attr('class', 'root');
const clusters = rootGroups.insert('g').attr('class', 'clusters');
const edgePaths = rootGroups.insert('g').attr('class', 'edges edgePath');
const edgeLabels = rootGroups.insert('g').attr('class', 'edgeLabels');
const nodesGroup = rootGroups.insert('g').attr('class', 'nodes');
const nodeElements = new Map<string, D3Selection<SVGElement | SVGGElement>>();
// Insert nodes into the DOM and add them to the graph.
for (const node of data4Layout.nodes) {
if (node.isGroup) {
graph.setNode(node.id, { ...node });
} else {
const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
nodeElements.set(node.id, childNodeEl as D3Selection<SVGElement | SVGGElement>);
node.width = boundingBox.width;
node.height = boundingBox.height;
graph.setNode(node.id, { ...node });
}
}
// Add edges to the graph.
for (const edge of edgesToProcess) {
if (edge.label && edge.label?.length > 0) {
// Create a label node for the edge
const startNode = data4Layout.nodes.find((n) => n.id == edge.start);
const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`;
const labelNode: NonClusterNode = {
id: labelNodeId,
label: edge.label,
edgeStart: edge.start ?? '',
edgeEnd: edge.end ?? '',
shape: 'labelRect',
width: 0,
height: 0,
isEdgeLabel: true,
isDummy: true,
parentId: undefined,
isGroup: false,
layer: 0,
order: 0,
...(startNode?.dir ? { dir: startNode.dir } : {}),
};
// Insert the label node into the DOM
const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: startNode?.dir });
const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
// Update node dimensions
labelNode.width = boundingBox.width;
labelNode.height = boundingBox.height;
// Add to graph and tracking maps
graph.setNode(labelNodeId, { ...labelNode });
nodeElements.set(labelNodeId, labelNodeEl as D3Selection<SVGElement | SVGGElement>);
data4Layout.nodes.push(labelNode);
// Create two edges to replace the original one
const edgeToLabel = {
...edge,
id: `${edge.id}-to-label`,
end: labelNodeId,
label: undefined,
isLabelEdge: true,
arrowTypeEnd: 'none',
arrowTypeStart: 'none',
};
const edgeFromLabel = {
...edge,
id: `${edge.id}-from-label`,
start: labelNodeId,
end: edge.end,
label: undefined,
isLabelEdge: true,
arrowTypeStart: 'none',
arrowTypeEnd: 'arrow_point',
};
graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel });
graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel });
data4Layout.edges.push(edgeToLabel, edgeFromLabel);
const edgeIdToRemove = edge.id;
data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove);
const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id);
if (indexInOriginal !== -1) {
data4Layout.edges.splice(indexInOriginal, 1);
}
} else {
// Regular edge without label
graph.setEdge(edge.id, edge.start, edge.end, { ...edge });
const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id);
if (!edgeExists) {
data4Layout.edges.push(edge);
}
}
}
return {
graph,
groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups },
nodeElements,
};
}

View File

@@ -0,0 +1,34 @@
import type { LayoutData } from '../../types.ts';
import type { D3Selection } from '../../../types.ts';
import { insertCluster } from '../../rendering-elements/clusters.js';
import { insertEdge } from '../../rendering-elements/edges.js';
import { positionNode } from '../../rendering-elements/nodes.js';
export async function adjustLayout(
data4Layout: LayoutData,
groups: {
edgePaths: D3Selection<SVGGElement>;
rootGroups: D3Selection<SVGGElement>;
[key: string]: D3Selection<SVGGElement>;
}
): Promise<void> {
for (const node of data4Layout.nodes) {
if (node.isGroup) {
await insertCluster(groups.clusters, node);
} else {
positionNode(node);
}
}
data4Layout.edges.forEach((edge) => {
insertEdge(
groups.edgePaths,
{ ...edge },
{},
data4Layout.type,
edge.start,
edge.end,
data4Layout.diagramId
);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
import type { Node } from '../../types.ts';
import { assignInitialPositions } from './assignInitialPositions.js';
import { layerAssignment } from './layerAssignment.js';
import { assignNodeOrder } from './nodeOrdering.js';
describe('assignInitialPositioning', () => {
beforeEach(function () {
flow.parser.yy = new FlowDB();
flow.parser.yy.clear();
});
it('should correctly assign initial positioning to node', async () => {
const flowchart = `
flowchart LR
A --> B --> C
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(50, layoutData);
const firstNode = layoutData.nodes.find((node: Node) => node.id === 'A');
const secondNode = layoutData.nodes.find((node: Node) => node.id === 'B');
const thirdNode = layoutData.nodes.find((node: Node) => node.id === 'C');
// Call Initial Position Assignment for the graph
assignInitialPositions(100, 130, layoutData);
const node1 = layoutData.nodes.find((node: Node) => node.id === 'A');
const node2 = layoutData.nodes.find((node: Node) => node.id === 'B');
const node3 = layoutData.nodes.find((node: Node) => node.id === 'C');
expect(node1.x).toEqual(firstNode.order * 100);
expect(node1.y).toEqual(firstNode.layer * 130);
expect(node2.x).toEqual(secondNode.order * 100);
expect(node2.y).toEqual(secondNode.layer * 130);
expect(node3.x).toEqual(thirdNode.order * 100);
expect(node3.y).toEqual(thirdNode.layer * 130);
});
it('should correctly assign initial positioning to nodes in subgraphs', async () => {
const flowchart = `
flowchart LR
subgraph two
b1
end
subgraph three
c2
end
three --> two
two --> c2
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(50, layoutData);
const twoNode = layoutData.nodes.find((node: Node) => node.id === 'two');
const b1Node = layoutData.nodes.find((node: Node) => node.id === 'b1');
const threeNode = layoutData.nodes.find((node: Node) => node.id === 'three');
const c2Node = layoutData.nodes.find((node: Node) => node.id === 'c2');
// Call Initial Position Assignment for the graph
assignInitialPositions(100, 130, layoutData);
expect(twoNode.x).toEqual(twoNode.order * 100);
expect(twoNode.y).toEqual(twoNode.layer * 130);
expect(b1Node.x).toEqual(b1Node.order * 100);
expect(b1Node.y).toEqual(b1Node.layer * 130);
expect(threeNode.x).toEqual(threeNode.order * 100);
expect(threeNode.y).toEqual(threeNode.layer * 130);
expect(c2Node.x).toEqual(c2Node.order * 100);
expect(c2Node.y).toEqual(c2Node.layer * 130);
});
it('should correctly assign initial positioning to nodes in complex subgraph', async () => {
const flowchart = `
flowchart LR
subgraph one
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
a1 -- Another long label --> a2
a3 --> a1 & a2 & a3 & a4
a1 --> a4
end
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(50, layoutData);
const oneNode = layoutData.nodes.find((node: Node) => node.id === 'one');
const a1Node = layoutData.nodes.find((node: Node) => node.id === 'a1');
const a2Node = layoutData.nodes.find((node: Node) => node.id === 'a2');
const a3Node = layoutData.nodes.find((node: Node) => node.id === 'a3');
const a4Node = layoutData.nodes.find((node: Node) => node.id === 'a4');
// Call Initial Position Assignment for the graph
assignInitialPositions(100, 130, layoutData);
expect(oneNode.x).toEqual(oneNode.order * 100);
expect(oneNode.y).toEqual(oneNode.layer * 130);
expect(a1Node.x).toEqual(a1Node.order * 100);
expect(a1Node.y).toEqual(a1Node.layer * 130);
expect(a2Node.x).toEqual(a2Node.order * 100);
expect(a2Node.y).toEqual(a2Node.layer * 130);
expect(a3Node.x).toEqual(a3Node.order * 100);
expect(a3Node.y).toEqual(a3Node.layer * 130);
expect(a4Node.x).toEqual(a4Node.order * 100);
expect(a4Node.y).toEqual(a4Node.layer * 130);
});
it('should correctly assign initial positioning to nodes in TD subgraphs', async () => {
const flowchart = `
flowchart TD
P1
P1 -->P1.5
subgraph P1.5
P2
P2.5(( A ))
P3
end
P2 --> P4
P3 --> P6
P1.5 --> P5
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(50, layoutData);
const p1Node = layoutData.nodes.find((node: Node) => node.id === 'P1');
const p15Node = layoutData.nodes.find((node: Node) => node.id === 'P1.5');
const p2Node = layoutData.nodes.find((node: Node) => node.id === 'P2');
const p25Node = layoutData.nodes.find((node: Node) => node.id === 'P2.5');
const p3Node = layoutData.nodes.find((node: Node) => node.id === 'P3');
const p4Node = layoutData.nodes.find((node: Node) => node.id === 'P4');
const p5Node = layoutData.nodes.find((node: Node) => node.id === 'P5');
const p6Node = layoutData.nodes.find((node: Node) => node.id === 'P6');
// Call Initial Position Assignment for the graph
assignInitialPositions(100, 130, layoutData);
expect(p1Node.x).toEqual(p1Node.order * 100);
expect(p1Node.y).toEqual(p1Node.layer * 130);
expect(p15Node.x).toEqual(p15Node.order * 100);
expect(p15Node.y).toEqual(p15Node.layer * 130);
expect(p2Node.x).toEqual(p2Node.order * 100);
expect(p2Node.y).toEqual(p2Node.layer * 130);
expect(p25Node.x).toEqual(p25Node.order * 100);
expect(p25Node.y).toEqual(p25Node.layer * 130);
expect(p3Node.x).toEqual(p3Node.order * 100);
expect(p3Node.y).toEqual(p3Node.layer * 130);
expect(p4Node.x).toEqual(p4Node.order * 100);
expect(p4Node.y).toEqual(p4Node.layer * 130);
expect(p5Node.x).toEqual(p5Node.order * 100);
expect(p5Node.y).toEqual(p5Node.layer * 130);
expect(p6Node.x).toEqual(p6Node.order * 100);
expect(p6Node.y).toEqual(p6Node.layer * 130);
});
it('should correctly assign initial positioning to nodes in TD subgraphs', async () => {
const flowchart = `
flowchart
subgraph Z
subgraph X
a --> b
end
subgraph Y
c --> d
end
end
Y --> X
X --> P
P --> Y
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(50, layoutData);
const zNode = layoutData.nodes.find((node: Node) => node.id === 'Z');
const yNode = layoutData.nodes.find((node: Node) => node.id === 'Y');
const xNode = layoutData.nodes.find((node: Node) => node.id === 'X');
const aNode = layoutData.nodes.find((node: Node) => node.id === 'a');
const bNode = layoutData.nodes.find((node: Node) => node.id === 'b');
const cNode = layoutData.nodes.find((node: Node) => node.id === 'c');
const dNode = layoutData.nodes.find((node: Node) => node.id === 'd');
const pNode = layoutData.nodes.find((node: Node) => node.id === 'P');
// Call Initial Position Assignment for the graph
assignInitialPositions(100, 130, layoutData);
expect(zNode.x).toEqual(zNode.order * 100);
expect(zNode.y).toEqual(zNode.layer * 130);
expect(yNode.x).toEqual(yNode.order * 100);
expect(yNode.y).toEqual(yNode.layer * 130);
expect(xNode.x).toEqual(xNode.order * 100);
expect(xNode.y).toEqual(xNode.layer * 130);
expect(aNode.x).toEqual(aNode.order * 100);
expect(aNode.y).toEqual(aNode.layer * 130);
expect(bNode.x).toEqual(bNode.order * 100);
expect(bNode.y).toEqual(bNode.layer * 130);
expect(cNode.x).toEqual(cNode.order * 100);
expect(cNode.y).toEqual(cNode.layer * 130);
expect(dNode.x).toEqual(dNode.order * 100);
expect(dNode.y).toEqual(dNode.layer * 130);
expect(pNode.x).toEqual(pNode.order * 100);
expect(pNode.y).toEqual(pNode.layer * 130);
});
});

View File

@@ -0,0 +1,27 @@
import type { LayoutData, Node } from '../../types.ts';
/**
* Assigns initial x and y positions to each node
* based on its rank and order.
*
* @param nodeSpacing - Horizontal spacing between nodes
* @param layerHeight - Vertical spacing between layers
* @param data4Layout - Layout data used to update node positions
*/
export function assignInitialPositions(
nodeSpacing: number,
layerHeight: number,
data4Layout: LayoutData
): void {
data4Layout.nodes.forEach((node: Node) => {
const layer = node.layer ?? 0;
const order = node.order ?? 0;
const x = order * nodeSpacing;
const y = layer * layerHeight;
node.x = x;
node.y = y;
});
}

View File

@@ -0,0 +1,89 @@
import insertMarkers from '../../rendering-elements/markers.js';
import { clear as clearGraphlib } from '../dagre/mermaid-graphlib.js';
import { clear as clearNodes } from '../../rendering-elements/nodes.js';
import { clear as clearClusters } from '../../rendering-elements/clusters.js';
import { clear as clearEdges } from '../../rendering-elements/edges.js';
import type { LayoutData, Node } from '../../types.ts';
import type { D3Selection } from '../../../types.ts';
import type { SVG } from '../../../mermaid.ts';
import { adjustLayout } from './adjustLayout.js';
import { createGraphWithElements } from '../../createGraph.js';
import { layerAssignment } from './layerAssignment.js';
import { assignNodeOrder } from './nodeOrdering.js';
import { assignInitialPositions } from './assignInitialPositions.js';
import { applyCola } from './applyCola.js';
export async function render(data4Layout: LayoutData, svg: SVG): Promise<void> {
const element = svg.select('g') as unknown as D3Selection<SVGElement>;
// Insert markers and clear previous elements
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
clearNodes();
clearEdges();
clearClusters();
clearGraphlib();
// Create the graph and insert the SVG groups and nodes
const { groups } = await createGraphWithElements(element, data4Layout);
// layer assignment
layerAssignment(data4Layout);
// assign node order using barycenter heuristic method
assignNodeOrder(1, data4Layout);
// assign initial coordinates
assignInitialPositions(100, 130, data4Layout);
const iteration = calculateIterations(data4Layout);
applyCola(
{
iterations: iteration * 4,
springLength: 80,
springStrength: 0.1,
repulsionStrength: 70000,
},
data4Layout
);
data4Layout.nodes = sortGroupNodesToEnd(data4Layout.nodes);
await adjustLayout(data4Layout, groups);
}
function sortGroupNodesToEnd(nodes: Node[]): Node[] {
const nonGroupNodes = nodes.filter((n) => !n.isGroup);
const groupNodes = nodes
.filter((n) => n.isGroup)
.map((n) => {
const width = typeof n.width === 'number' ? n.width : 0;
const height = typeof n.height === 'number' ? n.height : 0;
return {
...n,
_area: width * height,
};
})
.sort((a, b) => b._area - a._area)
.map((n, idx) => {
const { _area, ...cleanNode } = n;
cleanNode.order = nonGroupNodes.length + idx;
return cleanNode;
});
return [...nonGroupNodes, ...groupNodes];
}
function calculateIterations(data4Layout: LayoutData) {
const nodesCount = data4Layout.nodes.length;
const edgesCount = data4Layout.edges.length;
const groupNodes = data4Layout.nodes.filter((node) => {
if (node.isGroup) {
return node;
}
});
let iteration = nodesCount + edgesCount;
if (groupNodes.length > 0) {
iteration = iteration * 5;
}
return iteration;
}

View File

@@ -0,0 +1,161 @@
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
import type { Node } from '../../types.ts';
import { layerAssignment } from './layerAssignment.js';
describe('layerAssignment', () => {
beforeEach(function () {
flow.parser.yy = new FlowDB();
flow.parser.yy.clear();
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart LR
A --> B --> C
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'A').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'B').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'C').layer).toEqual(3);
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart LR
subgraph two
b1
end
subgraph three
c2
end
three --> two
two --> c2
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'two').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'b1').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'three').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'c2').layer).toEqual(3);
// expect(graph.getNodeAttributes('B').layer).toEqual(2);
// expect(graph.getNodeAttributes('C').layer).toEqual(3);
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart LR
subgraph one
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
a1 -- Another long label --> a2
a3 --> a1 & a2 & a3 & a4
a1 --> a4
end
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'one').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'a1').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'a2').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'a3').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'a4').layer).toEqual(2);
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart TD
P1
P1 -->P1.5
subgraph P1.5
P2
P2.5(( A ))
P3
end
P2 --> P4
P3 --> P6
P1.5 --> P5
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'P1').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'P1.5').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P2').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P2.5').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P3').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P4').layer).toEqual(3);
expect(layoutData.nodes.find((node: Node) => node.id === 'P5').layer).toEqual(3);
expect(layoutData.nodes.find((node: Node) => node.id === 'P6').layer).toEqual(3);
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart
subgraph Z
subgraph X
a --> b
end
subgraph Y
c --> d
end
end
Y --> X
X --> P
P --> Y
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'Z').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'Y').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'X').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'a').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'b').layer).toEqual(3);
expect(layoutData.nodes.find((node: Node) => node.id === 'c').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'd').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P').layer).toEqual(3);
});
it('should correctly assign the layers to node', async () => {
const flowchart = `
flowchart LR
A --|Test Label|--> B --> C
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'A').layer).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'B').layer).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'C').layer).toEqual(3);
});
});

View File

@@ -0,0 +1,90 @@
import type { Edge, LayoutData } from '../../types.ts';
export function layerAssignment(data4Layout: LayoutData): void {
const removedEdges: Edge[] = [];
const visited = new Set<string>();
const visiting = new Set<string>();
function dfs(nodeId: string): void {
visited.add(nodeId);
visiting.add(nodeId);
const outbound = data4Layout.edges.filter((e) => e.start === nodeId);
for (const edge of outbound) {
const neighbor = edge.end!;
if (!visited.has(neighbor)) {
dfs(neighbor);
} else if (visiting.has(neighbor)) {
// Cycle detected: temporarily remove this edge
removedEdges.push(edge);
}
}
visiting.delete(nodeId);
}
// Remove cycles using DFS
for (const node of data4Layout.nodes) {
if (!visited.has(node.id)) {
dfs(node.id);
}
}
// Filter out removed edges temporarily
const workingEdges = data4Layout.edges.filter((e) => !removedEdges.includes(e));
// Build in-degree map
const inDegree: Record<string, number> = {};
for (const node of data4Layout.nodes) {
inDegree[node.id] = 0;
}
for (const edge of workingEdges) {
if (edge.end) {
inDegree[edge.end]++;
}
}
// Queue of nodes with in-degree 0
const queue: string[] = [];
for (const nodeId in inDegree) {
if (inDegree[nodeId] === 0) {
queue.push(nodeId);
}
}
// Map to store calculated ranks/layers
const ranks: Record<string, number> = {};
while (queue.length > 0) {
const nodeId = queue.shift()!;
const parents = workingEdges.filter((e) => e.end === nodeId).map((e) => e.start!);
const layoutNode = data4Layout.nodes.find((n) => n.id === nodeId);
if (layoutNode?.parentId && parents.length == 0) {
const parentNode = data4Layout.nodes.find((n) => n.id === layoutNode.parentId);
if (!parentNode?.layer) {
parents.push(parentNode?.id ?? '');
}
}
const parentRanks = parents.map((p) => ranks[p] ?? 0);
const rank = parentRanks.length ? Math.min(...parentRanks) + 1 : 0;
ranks[nodeId] = rank;
// Update layer in data4Layout.nodes
if (layoutNode) {
layoutNode.layer = rank + 1;
}
// Decrement in-degree of children
for (const edge of workingEdges) {
if (edge.start === nodeId && edge.end) {
inDegree[edge.end]--;
if (inDegree[edge.end] === 0) {
queue.push(edge.end);
}
}
}
}
}

View File

@@ -0,0 +1,155 @@
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
import type { Node } from '../../types.ts';
import { layerAssignment } from './layerAssignment.js';
import { assignNodeOrder } from './nodeOrdering.js';
describe('nodeOrdering', () => {
beforeEach(function () {
flow.parser.yy = new FlowDB();
flow.parser.yy.clear();
});
it('should correctly assign the orders to node', async () => {
const flowchart = `
flowchart LR
A --> B --> C
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(1, layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'A').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'B').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'C').order).toEqual(0);
});
it('should correctly assign the orders to node', async () => {
const flowchart = `
flowchart LR
subgraph two
b1
end
subgraph three
c2
end
three --> two
two --> c2
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(1, layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'two').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'b1').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'three').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'c2').order).toEqual(0);
});
it('should correctly assign the orders to node', async () => {
const flowchart = `
flowchart LR
subgraph one
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
a1 -- Another long label --> a2
a3 --> a1 & a2 & a3 & a4
a1 --> a4
end
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(1, layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'one').order).toEqual(0.75);
expect(layoutData.nodes.find((node: Node) => node.id === 'a1').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'a2').order).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'a3').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'a4').order).toEqual(2);
});
it('should correctly assign the orders to node', async () => {
const flowchart = `
flowchart TD
P1
P1 -->P1.5
subgraph P1.5
P2
P2.5(( A ))
P3
end
P2 --> P4
P3 --> P6
P1.5 --> P5
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(1, layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'P1').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'P1.5').order).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'P2').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'P2.5').order).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P3').order).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'P4').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'P5').order).toEqual(2);
expect(layoutData.nodes.find((node: Node) => node.id === 'P6').order).toEqual(1);
});
it('should correctly assign the orders to node', async () => {
const flowchart = `
flowchart
subgraph Z
subgraph X
a --> b
end
subgraph Y
c --> d
end
end
Y --> X
X --> P
P --> Y
`;
// Get layout data from flowDb
await flow.parse(flowchart);
const layoutData = flow.parser.yy.getData();
// Call Layer Assignment for the graph
layerAssignment(layoutData);
// Call Node order Assignment for the graph
assignNodeOrder(1, layoutData);
expect(layoutData.nodes.find((node: Node) => node.id === 'Z').order).toEqual(8);
expect(layoutData.nodes.find((node: Node) => node.id === 'Y').order).toEqual(0.5);
expect(layoutData.nodes.find((node: Node) => node.id === 'X').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'a').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'b').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'c').order).toEqual(0);
expect(layoutData.nodes.find((node: Node) => node.id === 'd').order).toEqual(1);
expect(layoutData.nodes.find((node: Node) => node.id === 'P').order).toEqual(1);
});
});

View File

@@ -0,0 +1,129 @@
import type { Edge, LayoutData, Node } from '../../types.ts';
type LayerMap = Record<number, Node[]>;
function groupNodesByLayer(nodes: Node[]): LayerMap {
const layers: LayerMap = {};
nodes.forEach((node: Node) => {
if (node.isGroup) {
return;
}
const layer = node.layer ?? 0;
if (!layers[layer]) {
layers[layer] = [];
}
layers[layer].push(node);
});
return layers;
}
/**
* Assign horizontal ordering to nodes, excluding group nodes from ordering.
* Groups are assigned `order` after real nodes are sorted.
*/
export function assignNodeOrder(iterations: number, data4Layout: LayoutData): void {
const nodes = data4Layout.nodes;
const edges = data4Layout.edges;
const nodeMap = new Map<string, Node>(nodes.map((n) => [n.id, n]));
const isLayered = nodes.some((n) => n.layer !== undefined);
if (isLayered) {
const layers = groupNodesByLayer(nodes);
const sortedLayers = Object.keys(layers)
.map(Number)
.sort((a, b) => a - b);
// Initial order
for (const layer of sortedLayers) {
layers[layer].forEach((node, index) => {
node.order = index;
});
}
// Barycenter iterations
for (let i = 0; i < iterations; i++) {
for (let l = 1; l < sortedLayers.length; l++) {
sortLayerByBarycenter(layers[sortedLayers[l]], 'inbound', edges, nodeMap);
}
for (let l = sortedLayers.length - 2; l >= 0; l--) {
sortLayerByBarycenter(layers[sortedLayers[l]], 'outbound', edges, nodeMap);
}
}
// Assign order to group nodes at the end
for (const node of nodes) {
if (node.isGroup) {
const childOrders = nodes
.filter((n) => n.parentId === node.id)
.map((n) => nodeMap.get(n.id)?.order)
.filter((o): o is number => typeof o === 'number');
node.order = childOrders.length
? childOrders.reduce((a, b) => a + b, 0) / childOrders.length
: nodes.length;
}
}
}
}
function sortLayerByBarycenter(
layerNodes: Node[],
direction: 'inbound' | 'outbound' | 'both',
edges: Edge[],
nodeMap: Map<string, Node>
): void {
const edgeMap = new Map<string, Set<string>>();
edges.forEach((e: Edge) => {
if (e.start && e.end) {
if (!edgeMap.has(e.start)) {
edgeMap.set(e.start, new Set());
}
edgeMap.get(e.start)?.add(e.end);
}
});
const baryCenters = layerNodes.map((node, originalIndex) => {
const neighborOrders: number[] = [];
edges.forEach((edge) => {
if (direction === 'inbound' && edge.end === node.id) {
const source = nodeMap.get(edge.start ?? '');
if (source?.order !== undefined) {
neighborOrders.push(source.order);
}
} else if (direction === 'outbound' && edge.start === node.id) {
const target = nodeMap.get(edge.end ?? '');
if (target?.order !== undefined) {
neighborOrders.push(target.order);
}
} else if (direction === 'both' && (edge.start === node.id || edge.end === node.id)) {
const neighborId = edge.start === node.id ? edge.end : edge.start;
const neighbor = nodeMap.get(neighborId ?? '');
if (neighbor?.order !== undefined) {
neighborOrders.push(neighbor.order);
}
}
});
const barycenter =
neighborOrders.length === 0
? Infinity // Push unconnected nodes to the end
: neighborOrders.reduce((sum, o) => sum + o, 0) / neighborOrders.length;
return { node, barycenter, originalIndex };
});
baryCenters.sort((a, b) => {
if (a.barycenter !== b.barycenter) {
return a.barycenter - b.barycenter;
}
// Stable tie-breaker based on original index
return a.originalIndex - b.originalIndex;
});
baryCenters.forEach((entry, index) => {
entry.node.order = index;
});
}

View File

@@ -39,6 +39,10 @@ const registerDefaultLayoutLoaders = () => {
name: 'dagre', name: 'dagre',
loader: async () => await import('./layout-algorithms/dagre/index.js'), loader: async () => await import('./layout-algorithms/dagre/index.js'),
}, },
{
name: 'ipsepCola',
loader: async () => await import('./layout-algorithms/ipsepCola/index.js'),
},
]); ]);
}; };

View File

@@ -80,11 +80,23 @@ interface BaseNode {
export interface ClusterNode extends BaseNode { export interface ClusterNode extends BaseNode {
shape?: ClusterShapeID; shape?: ClusterShapeID;
isGroup: true; isGroup: true;
isEdgeLabel?: boolean;
edgeStart?: string;
edgeEnd?: string;
layer?: number;
order?: number;
isDummy?: boolean;
} }
export interface NonClusterNode extends BaseNode { export interface NonClusterNode extends BaseNode {
shape?: ShapeID; shape?: ShapeID;
isGroup: false; isGroup: false;
isEdgeLabel?: boolean;
edgeStart?: string;
edgeEnd?: string;
layer?: number;
order?: number;
isDummy?: boolean;
} }
// Common properties for any node in the system // Common properties for any node in the system
@@ -126,6 +138,8 @@ export interface Edge {
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted'; thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
look?: string; look?: string;
isUserDefinedId?: boolean; isUserDefinedId?: boolean;
isLabelEdge?: boolean;
points?: { x: number; y: number }[];
} }
export interface RectOptions { export interface RectOptions {

View File

@@ -3,9 +3,7 @@ import { cp } from 'fs/promises';
const main = async () => { const main = async () => {
const coverageDir = 'coverage'; const coverageDir = 'coverage';
const coverageFiles = ['vitest', 'cypress'].map( const coverageFiles = ['vitest'].map((dir) => `${coverageDir}/${dir}/coverage-final.json`);
(dir) => `${coverageDir}/${dir}/coverage-final.json`
);
//copy coverage files from vitest and cypress to coverage folder //copy coverage files from vitest and cypress to coverage folder
await Promise.all( await Promise.all(