mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-23 17:29:54 +02:00
Added support for ipsep-cola layout algorithm
This commit is contained in:
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: 'ipsecCola',
|
||||||
|
// 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>
|
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.js';
|
||||||
|
import type { D3Selection } from '../../../types.js';
|
||||||
|
import { insertCluster } from '../../rendering-elements/clusters.js';
|
||||||
|
import { insertEdge } from '../../rendering-elements/edges.js';
|
||||||
|
import { positionNode } from '../../rendering-elements/nodes.js';
|
||||||
|
|
||||||
|
export async function adjustLayout(
|
||||||
|
data4Layout: LayoutData,
|
||||||
|
groups: {
|
||||||
|
edgePaths: D3Selection<SVGGElement>;
|
||||||
|
rootGroups: D3Selection<SVGGElement>;
|
||||||
|
[key: string]: D3Selection<SVGGElement>;
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
for (const node of data4Layout.nodes) {
|
||||||
|
if (node.isGroup) {
|
||||||
|
await insertCluster(groups.clusters, node);
|
||||||
|
} else {
|
||||||
|
positionNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data4Layout.edges.forEach((edge) => {
|
||||||
|
insertEdge(
|
||||||
|
groups.edgePaths,
|
||||||
|
{ ...edge },
|
||||||
|
{},
|
||||||
|
data4Layout.type,
|
||||||
|
edge.start,
|
||||||
|
edge.end,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
import type { LayoutData, Node } from '../../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,83 @@
|
|||||||
|
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.js';
|
||||||
|
import { adjustLayout } from './adjustLayout.js';
|
||||||
|
import { layerAssignment } from './layerAssignment.js';
|
||||||
|
import { assignNodeOrder } from './nodeOrdering.js';
|
||||||
|
import { assignInitialPositions } from './assignInitialPositions.js';
|
||||||
|
import { applyCola } from './applyCola.js';
|
||||||
|
import { createGraphWithElements } from '../../createGraph.js';
|
||||||
|
import type { D3Selection } from '../../../types.js';
|
||||||
|
import type { SVG } from '../../../mermaid.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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
import type { Edge, LayoutData } from '../../types.js';
|
||||||
|
|
||||||
|
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,129 @@
|
|||||||
|
import type { Edge, LayoutData, Node } from '../../types.js';
|
||||||
|
|
||||||
|
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: 'ipsecCola',
|
||||||
|
loader: async () => await import('./layout-algorithms/ipsecCola/index.ts'),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -80,11 +80,23 @@ interface BaseNode {
|
|||||||
export interface ClusterNode extends BaseNode {
|
export interface ClusterNode extends BaseNode {
|
||||||
shape?: ClusterShapeID;
|
shape?: ClusterShapeID;
|
||||||
isGroup: true;
|
isGroup: true;
|
||||||
|
isEdgeLabel?: boolean;
|
||||||
|
edgeStart?: string;
|
||||||
|
edgeEnd?: string;
|
||||||
|
layer?: number;
|
||||||
|
order?: number;
|
||||||
|
isDummy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NonClusterNode extends BaseNode {
|
export interface NonClusterNode extends BaseNode {
|
||||||
shape?: ShapeID;
|
shape?: ShapeID;
|
||||||
isGroup: false;
|
isGroup: false;
|
||||||
|
isEdgeLabel?: boolean;
|
||||||
|
edgeStart?: string;
|
||||||
|
edgeEnd?: string;
|
||||||
|
layer?: number;
|
||||||
|
order?: number;
|
||||||
|
isDummy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common properties for any node in the system
|
// Common properties for any node in the system
|
||||||
@@ -126,6 +138,8 @@ export interface Edge {
|
|||||||
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
||||||
look?: string;
|
look?: string;
|
||||||
isUserDefinedId?: boolean;
|
isUserDefinedId?: boolean;
|
||||||
|
isLabelEdge?: boolean;
|
||||||
|
points?: { x: number; y: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RectOptions {
|
export interface RectOptions {
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"types": ["vitest/importMeta", "vitest/globals"]
|
"types": ["vitest/importMeta", "vitest/globals"],
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
|
Reference in New Issue
Block a user