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',
|
||||
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 {
|
||||
shape?: ClusterShapeID;
|
||||
isGroup: true;
|
||||
isEdgeLabel?: boolean;
|
||||
edgeStart?: string;
|
||||
edgeEnd?: string;
|
||||
layer?: number;
|
||||
order?: number;
|
||||
isDummy?: boolean;
|
||||
}
|
||||
|
||||
export interface NonClusterNode extends BaseNode {
|
||||
shape?: ShapeID;
|
||||
isGroup: false;
|
||||
isEdgeLabel?: boolean;
|
||||
edgeStart?: string;
|
||||
edgeEnd?: string;
|
||||
layer?: number;
|
||||
order?: number;
|
||||
isDummy?: boolean;
|
||||
}
|
||||
|
||||
// Common properties for any node in the system
|
||||
@@ -126,6 +138,8 @@ export interface Edge {
|
||||
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
||||
look?: string;
|
||||
isUserDefinedId?: boolean;
|
||||
isLabelEdge?: boolean;
|
||||
points?: { x: number; y: number }[];
|
||||
}
|
||||
|
||||
export interface RectOptions {
|
||||
|
@@ -3,7 +3,9 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
"types": ["vitest/importMeta", "vitest/globals"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
|
Reference in New Issue
Block a user