mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Compare commits
20 Commits
knsv/mindm
...
e12036ed6c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e12036ed6c | ||
![]() |
4ea4a7b3fe | ||
![]() |
09c239fc25 | ||
![]() |
b01a0d7dcd | ||
![]() |
3e99158c04 | ||
![]() |
339927d7e5 | ||
![]() |
2785ca47a0 | ||
![]() |
b70abed096 | ||
![]() |
1a5a846d71 | ||
![]() |
20ddc12b42 | ||
![]() |
5e037ee315 | ||
![]() |
70eedd840d | ||
![]() |
bb0aa4009c | ||
![]() |
75f940bf9e | ||
![]() |
a46de01885 | ||
![]() |
a7e9f4f926 | ||
![]() |
99c8224dfa | ||
![]() |
13f8549f81 | ||
![]() |
cfd25ed33e | ||
![]() |
3a8952ebe0 |
5
.changeset/cute-crews-shake.md
Normal file
5
.changeset/cute-crews-shake.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Added IpSepCoLa algorithm for layout
|
@@ -12,6 +12,7 @@ gantt
|
|||||||
gitgraph
|
gitgraph
|
||||||
gzipped
|
gzipped
|
||||||
handDrawn
|
handDrawn
|
||||||
|
ipsepcola
|
||||||
kanban
|
kanban
|
||||||
marginx
|
marginx
|
||||||
marginy
|
marginy
|
||||||
|
153
cypress/integration/rendering/ipsepCola.spec.ts
Normal file
153
cypress/integration/rendering/ipsepCola.spec.ts
Normal 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
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
601
cypress/platform/ipsepcola_sample.html
Normal file
601
cypress/platform/ipsepcola_sample.html
Normal 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>
|
@@ -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:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L153)
|
||||||
|
|
||||||
## 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:156](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L156)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L155)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L154)
|
||||||
|
156
docs/layouts/development.md
Normal file
156
docs/layouts/development.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
> **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
|
||||||
|
|
||||||
|
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Implementing Your Custom Layout Algorithm
|
||||||
|
|
||||||
|
### 1. 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.
|
||||||
|
|
||||||
|
### 2. 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
|
||||||
|
|
||||||
|
### 3. 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
138
docs/layouts/introduction.md
Normal file
138
docs/layouts/introduction.md
Normal 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 |
|
||||||
|
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
---
|
46
docs/layouts/ipsepcola/implementation.md
Normal file
46
docs/layouts/ipsepcola/implementation.md
Normal 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.
|
186
docs/layouts/ipsepcola/overview.md
Normal file
186
docs/layouts/ipsepcola/overview.md
Normal 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.
|
@@ -141,6 +141,7 @@ function sidebarAll() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
...sidebarSyntax(),
|
...sidebarSyntax(),
|
||||||
|
...sidebarAlgorithms(),
|
||||||
...sidebarEcosystem(),
|
...sidebarEcosystem(),
|
||||||
...sidebarConfig(),
|
...sidebarConfig(),
|
||||||
...sidebarCommunity(),
|
...sidebarCommunity(),
|
||||||
@@ -223,6 +224,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 [
|
||||||
{
|
{
|
||||||
|
150
packages/mermaid/src/docs/layouts/development.md
Normal file
150
packages/mermaid/src/docs/layouts/development.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 🛠️ 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
|
||||||
|
|
||||||
|
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Implementing Your Custom Layout Algorithm
|
||||||
|
|
||||||
|
### 1. 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.
|
||||||
|
|
||||||
|
### 2. 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
|
||||||
|
|
||||||
|
### 3. 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
132
packages/mermaid/src/docs/layouts/introduction.md
Normal file
132
packages/mermaid/src/docs/layouts/introduction.md
Normal 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 |
|
||||||
|
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
---
|
@@ -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.
|
180
packages/mermaid/src/docs/layouts/ipsepcola/overview.md
Normal file
180
packages/mermaid/src/docs/layouts/ipsepcola/overview.md
Normal 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.
|
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal file
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of data4Layout.edges) {
|
||||||
|
insertEdge(
|
||||||
|
groups.edgePaths,
|
||||||
|
edge,
|
||||||
|
{},
|
||||||
|
data4Layout.type,
|
||||||
|
edge.start,
|
||||||
|
edge.end,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
@@ -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'),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -72,6 +72,12 @@ interface BaseNode {
|
|||||||
defaultWidth?: number;
|
defaultWidth?: number;
|
||||||
imageAspectRatio?: number;
|
imageAspectRatio?: number;
|
||||||
constraint?: 'on' | 'off';
|
constraint?: 'on' | 'off';
|
||||||
|
isEdgeLabel?: boolean;
|
||||||
|
edgeStart?: string;
|
||||||
|
edgeEnd?: string;
|
||||||
|
layer?: number;
|
||||||
|
order?: number;
|
||||||
|
isDummy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,6 +132,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 {
|
||||||
|
Reference in New Issue
Block a user