Compare commits

..

62 Commits

Author SHA1 Message Date
renovate[bot]
863e498964 chore(deps): update eslint 2025-09-15 09:31:08 +00:00
Shubham P
181af8167b Merge pull request #6934 from fulldecent/patch-2
Specify score range for task syntax
2025-09-15 09:18:39 +00:00
Shubham P
799d2ed547 Merge branch 'develop' into patch-2 2025-09-15 14:38:57 +05:30
Shubham P
08160a74b4 Merge pull request #6931 from mermaid-js/renovate/npm-vite-vulnerability
chore(deps): update dependency vite to v7.0.7 [security]
2025-09-15 08:22:27 +00:00
Knut Sveidqvist
6d221fb3ca Merge pull request #6944 from mermaid-js/docs/fix-mindmap-rendering
6932:Fix mindmap rendering in docs and apply tidytree layout
2025-09-15 08:06:41 +00:00
Shubham P
8b20907141 Merge branch 'develop' into renovate/npm-vite-vulnerability 2025-09-15 13:13:40 +05:30
darshanr0107
a459c436c9 docs: fix rendering and ensure tidytree layout is applied
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-15 12:00:46 +05:30
Sidharth Vinod
1c2a0020bd Merge pull request #6939 from aloisklink/build/fix-netlify-mermaid-live-preview
build: change mermaid live preview to use pnpm
2025-09-12 21:18:29 +05:30
Alois Klink
141c6b3808 build: change mermaid live preview to use pnpm
Right now, the netlify build seems to be failing since we're ignoring
the `mermaid-live-editor` lockfile.

This is causing errors with broken dependencies.

Switching to `pnpm`, which the `mermaid-live-editor` uses, fixes these
issues!
2025-09-13 00:35:29 +09:00
renovate[bot]
8d4ffdf808 chore(deps): update dependency vite to v7.0.7 [security] 2025-09-12 20:14:12 +09:00
Alois Klink
32106e259c build(docs): set build.target = 'modules'
Explicility set the `build.target` to `modules`, as Vite v7 changes this
and drops support for older browsers.

We probably should do this eventually too, but maybe as part of a
Mermaid v12 release.
2025-09-12 20:14:12 +09:00
Shubham P
b36edd557e Merge pull request #6921 from quilicicf/feat/6627_add_ids_in_architecture_diagrams
feat(architecture): Add ids in generated SVG
2025-09-11 05:10:13 +00:00
Shubham P
5e3b5e8f36 Merge branch 'develop' into feat/6627_add_ids_in_architecture_diagrams 2025-09-11 10:22:18 +05:30
Shubham P
764b315dc1 Updated changeset 2025-09-11 10:22:04 +05:30
autofix-ci[bot]
47c0d2d040 [autofix.ci] apply automated fixes 2025-09-10 18:00:00 +00:00
William Entriken
ac3b777bf6 Specify score range for task syntax 2025-09-10 13:54:31 -04:00
Ashish Jain
166782cd38 Merge pull request #6854 from mermaid-js/mindmaps-and-elk-updates
Update elk layout to handle start/stop of edges properly for all shapes
2025-09-10 15:26:22 +00:00
darshanr0107
b37eb6d0d1 fix: arrow head color not matching arrow color
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-10 20:30:31 +05:30
Knut Sveidqvist
f759f5dcf7 Merge branch 'develop' into mindmaps-and-elk-updates 2025-09-10 15:59:19 +02:00
Knut Sveidqvist
80bcefe321 Merge branch 'mindmaps-and-elk-updates' of github.com:mermaid-js/mermaid into mindmaps-and-elk-updates 2025-09-10 15:59:08 +02:00
Knut Sveidqvist
70cbbe69d8 Handing edges for edges leaving subgraphs 2025-09-10 15:58:20 +02:00
Knut Sveidqvist
baf4093e8d Merge pull request #6826 from mermaid-js/6784-edge-label-color-mismatch
6784: Fix edge ID styling mismatch with linkStyle color
2025-09-10 13:41:44 +00:00
darshanr0107
fd185f7694 chore: fix failing test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-10 18:41:23 +05:30
Shubham P
027d7b6368 Merge pull request #6926 from saurabhg772244/Added-missing-types-in-diagramDB
chore: Added missing types in diagramDB
2025-09-10 12:59:42 +00:00
Knut Sveidqvist
7986b66a88 Fix for edge calculation to subgraphs 2025-09-10 14:39:08 +02:00
darshanr0107
edb0edc451 chore: fix failing tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-10 17:21:58 +05:30
Knut Sveidqvist
b511a2e9be Merge branch 'develop' into mindmaps-and-elk-updates 2025-09-10 10:42:24 +02:00
autofix-ci[bot]
b80ea26a2b [autofix.ci] apply automated fixes 2025-09-08 08:35:29 +00:00
saurabhg772244
f88986a87d Updated DiagramDB interface to use DiagramOrientation for setDirection method 2025-09-08 13:59:52 +05:30
saurabhg772244
e16f0848ab Added missing types in diagramDB 2025-09-08 12:33:37 +05:30
quilicicf
2812a0d12a feat(architecture): Add ids in generated SVG 2025-09-06 14:27:28 +02:00
Knut Sveidqvist
25fa26d915 fix(layout-elk): prevent NaN paths from duplicate points 2025-09-05 16:24:32 +02:00
Knut Sveidqvist
62915183b1 Merge branch 'mindmaps-and-elk-updates' of github.com:mermaid-js/mermaid into mindmaps-and-elk-updates 2025-09-05 15:51:40 +02:00
Knut Sveidqvist
6874ab3fb6 Adjusted elk-config 2025-09-05 15:50:58 +02:00
darshanr0107
040af4f545 fix: failing unit test
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-05 19:16:43 +05:30
Knut Sveidqvist
65ca3eabfd Some cleanup 2025-09-05 15:21:45 +02:00
Knut Sveidqvist
8b9bbad842 Fix for render issue to and from subgraphs 2025-09-05 14:48:15 +02:00
darshanr0107
d2773db7dc fix: review comments and unit tests
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-05 16:43:29 +05:30
Shubham P
3840451fda Merge pull request #6918 from mermaid-js/chore/revert-marked-dependency-to-v16
revert: upgrade marked package from ^15.0.7 to ^16.0.0
2025-09-05 10:31:51 +00:00
shubhamparikh2704
cfe9238882 revert: upgrade marked package from ^15.0.7 to ^16.0.0
- Revert marked package version to ^16.0.0 for better compatibility
- Update pnpm-lock.yaml to reflect the version change
- Add changeset to document the dependency update
- All tests pass with the reverted version
- Build completes successfully
2025-09-05 15:44:00 +05:30
darshanr0107
0dd46a3543 fix: resolve TypeScript errors in mermaid-layout-elk
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-04 14:57:24 +05:30
darshanr0107
f81e63663c fix: pnpm lock issue
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-04 14:25:18 +05:30
darshanr0107
7109e3a17f fix: pnpm lock fil issue
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-04 14:19:18 +05:30
darshanr0107
e0bd51941e Revert "fix: revert pnpm-lock file"
This reverts commit 38f4e67ca7.
2025-09-04 14:14:01 +05:30
darshanr0107
38f4e67ca7 fix: revert pnpm-lock file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-04 14:04:42 +05:30
darshanr0107
681d829227 fix: pnpm lock issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-04 14:00:38 +05:30
darshanr0107
164e44c3d9 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into mindmaps-and-elk-updates 2025-09-04 13:46:33 +05:30
darshanr0107
2e1d156d66 Merge branch 'develop' into 6784-edge-label-color-mismatch 2025-09-02 17:21:02 +05:30
darshanr0107
e863ad1547 chore: revert unintended pnpm-lock.yaml changes
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-02 17:09:13 +05:30
darshanr0107
7091792694 fix: build issues
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-21 14:15:50 +05:30
darshanr0107
efd94b705d fix: build issues
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-21 14:11:48 +05:30
darshanr0107
9ec989e633 Merge branch 'develop' of https://github.com/mermaid-js/mermaid into 6784-edge-label-color-mismatch 2025-08-21 12:06:04 +05:30
Knut Sveidqvist
a716a525c3 Merge remote-tracking branch 'origin/develop' into mindmaps-and-elk-updates 2025-08-14 13:53:33 +02:00
Knut Sveidqvist
11abfc9ae5 Refactor code structure for improved readability and maintainability 2025-08-12 16:08:19 +02:00
darshanr0107
227cef05b3 fix: the breaking argos
Some optional description over here if you need to add more info

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-12 18:55:11 +05:30
Knut Sveidqvist
81b0ffb92a Merge branch '6088-fix-for-diamond-intersections' into mindmaps-and-elk-updates 2025-08-12 11:11:51 +02:00
darshanr0107
1d3681053b added changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 12:34:41 +05:30
darshanr0107
93df13898f fix: ensure class def color is applied to edge label
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-08-06 12:21:34 +05:30
Knut Sveidqvist
8314554eb5 Merge branch 'test-merge' into 6088-fix-for-diamond-intersections 2025-06-25 13:00:03 +02:00
Knut Sveidqvist
b7c03dc27e Some cleanup 2025-06-25 12:58:54 +02:00
Knut Sveidqvist
c7f2f609a9 Intersections ok 2025-06-24 20:30:50 +02:00
Knut Sveidqvist
4c3de3a1ec Merge remote-tracking branch 'origin/develop' into test-merge 2025-06-24 10:58:28 +02:00
49 changed files with 2973 additions and 1797 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Render newlines as spaces in class diagrams

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Handle arrows correctly when auto number is enabled

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
Add IDs in architecture diagrams

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Ensure edge label color is applied when using classDef with edge IDs

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.

View File

@@ -0,0 +1,7 @@
---
'mermaid': minor
'@mermaid-js/layout-tidy-tree': minor
'@mermaid-js/layout-elk': minor
---
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
feat: Add IDs in architecture diagrams

View File

@@ -0,0 +1,9 @@
---
'mermaid': patch
---
chore: revert marked dependency from ^15.0.7 to ^16.0.0
- Reverted marked package version to ^16.0.0 for better compatibility
- This is a dependency update that maintains API compatibility
- All tests pass with the updated version

View File

@@ -8,6 +8,7 @@ compositTitleSize
cose
curv
doublecircle
elem
elems
gantt
gitgraph

View File

@@ -98,12 +98,12 @@ describe('Configuration', () => {
it('should handle arrowMarkerAbsolute set to true', () => {
renderGraph(
`flowchart TD
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]
`,
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]
`,
{
arrowMarkerAbsolute: true,
}
@@ -113,8 +113,7 @@ describe('Configuration', () => {
cy.get('path')
.first()
.should('have.attr', 'marker-end')
.should('exist')
.and('include', 'url(http\\:\\/\\/localhost');
.and('include', 'url(http://localhost');
});
});
it('should not taint the initial configuration when using multiple directives', () => {

View File

@@ -109,7 +109,7 @@ describe('Flowchart ELK', () => {
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
verifyNumber(maxWidthValue, 380);
verifyNumber(maxWidthValue, 380, 15);
});
});
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
@@ -128,7 +128,7 @@ describe('Flowchart ELK', () => {
const width = parseFloat(svg.attr('width'));
// use within because the absolute value can be slightly different depending on the environment ±5%
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
verifyNumber(width, 380);
verifyNumber(width, 380, 15);
expect(svg).to.not.have.attr('style');
});
});

View File

@@ -1186,4 +1186,17 @@ end
imgSnapshotTest(graph, { htmlLabels: false });
});
});
it('V2 - 17: should apply class def colour to edge label', () => {
imgSnapshotTest(
` graph LR
id1(Start) link@-- "Label" -->id2(Stop)
style id1 fill:#f9f,stroke:#333,stroke-width:4px
class id2 myClass
classDef myClass fill:#bbf,stroke:#f66,stroke-width:2px,color:white,stroke-dasharray: 5 5
class link myClass
`
);
});
});

View File

@@ -32,26 +32,8 @@
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&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=Recursive:wght@300..1000&display=swap"
rel="stylesheet"
/>
<style>
.recursive-mermaid {
font-family: 'Recursive', sans-serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings:
'slnt' 0,
'CASL' 0,
'CRSV' 0.5,
'MONO' 0;
}
body {
/* background: rgb(221, 208, 208); */
/* background: #333; */
@@ -63,9 +45,7 @@
h1 {
color: grey;
}
.mermaid {
border: 1px solid red;
}
.mermaid2 {
display: none;
}
@@ -103,11 +83,6 @@
width: 100%;
}
.class2 {
fill: red;
fill-opacity: 1;
}
/* tspan {
font-size: 6px !important;
} */
@@ -130,76 +105,194 @@
</head>
<body>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
layout: elk
---
mindmap
root((mindmap is a long thing))
A
B
C
D
</pre
flowchart-elk TB
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">
---
config:
layout: tidy-tree
layout: elk
---
mindmap
root((mindmap))
A
B
</pre
flowchart TB
process_C
subgraph container_Alpha
subgraph process_B
pppB
end
subgraph process_A
pppA
end
process_B-->|via_AWSBatch|container_Beta
process_A-->|messages|container_Beta
end
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
layout: elk
---
mindmap
root((mindmap))
A
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
c
d
B
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
D
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
flowchart TB
subgraph container_Beta
process_C
end
subgraph container_Alpha
subgraph process_B
pppB
end
subgraph process_A
pppA
end
process_B-->|via_AWSBatch|container_Beta
process_A-->|messages|container_Beta
end
</pre>
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
flowchart TB
subgraph container_Beta
process_C
end
process_B-->|via_AWSBatch|container_Beta
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
classDiagram
note "I love this diagram!\nDo you love it?"
Class01 <|-- AveryLongClass : Cool
&lt;&lt;interface&gt;&gt; Class01
Class03 "1" *-- "*" Class04
Class05 "1" o-- "many" Class06
Class07 "1" .. "*" Class08
Class09 "1" --> "*" C2 : Where am i?
Class09 "*" --* "*" C3
Class09 "1" --|> "1" Class07
Class12 <|.. Class08
Class11 ..>Class12
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class01 : -int privateChimp
Class01 : +int publicGorilla
Class01 : #int protectedMarmoset
Class08 <--> C2: Cool label
class Class10 {
&lt;&lt;service&gt;&gt;
int id
test()
}
note for Class10 "Cool class\nI said it's very cool class!"
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
requirementDiagram
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
flowchart-elk TB
internet
nat
router
compute1
subgraph project
router
nat
subgraph subnet1
compute1
end
end
%% router --> subnet1
subnet1 --> nat
%% nat --> internet
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
flowchart-elk TB
internet
nat
router
lb1
lb2
compute1
compute2
subgraph project
router
nat
subgraph subnet1
compute1
lb1
end
subgraph subnet2
compute2
lb2
end
end
internet --> router
router --> subnet1 & subnet2
subnet1 & subnet2 --> nat --> internet
</pre
>
<pre id="diagram4" class="mermaid">
---
config:
@@ -227,91 +320,35 @@ treemap
"Leaf 2.2": 25
"Leaf 2.3": 12
classDef class1 fill:red,color:blue,stroke:#FFD600;
</pre>
<pre id="diagram5" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
---
flowchart LR
I["fa:fa-code Text"] -- Mermaid js --> D["Use<br/>the<br/>editor!"]
I --> D & D
D@{ shape: question}
I@{ shape: question}
</pre
>
<pre id="diagram4" class="mermaid2">
---
config:
treemap:
valueFormat: '$0,0'
---
treemap
"Budget"
"Operations"
"Salaries": 7000
"Equipment": 2000
"Supplies": 1000
"Marketing"
"Advertising": 4000
"Events": 1000
</pre
>
</pre>
<pre id="diagram4" class="mermaid">
treemap
title Accessible Treemap Title
"Category A"
"Item A1": 10
"Item A2": 20
"Category B"
"Item B1": 15
"Item B2": 25
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
a
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
b
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: tidy-tree
---
flowchart TB
A --> n0["1"]
A --> n1["2"]
A --> n2["3"]
A --> n3["4"] --> Q & R & S & T
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart TB
A --> n0["1"]
A --> n1["2"]
A --> n2["3"]
A --> n3["4"] --> Q & R & S & T
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: dagre
---
mindmap
root((mindmap is a long thing))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
@@ -320,128 +357,112 @@ treemap
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
flowchart:
curve: linear
---
flowchart LR
root{mindmap} --- Origins --- Europe
Origins --> Asia
root --- Background --- Rich
Background --- Poor
subgraph apa
Background
Poor
end
A[A] --> B[B]
A[A] --- B([C])
A@{ shape: diamond}
%%B@{ shape: diamond}
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: linear
---
flowchart LR
root{mindmap} --- Origins --- Europe
Origins --> Asia
root --- Background --- Rich
Background --- Poor
A[A] -- Mermaid js --> B[B]
A[A] -- Mermaid js --- B[B]
A@{ shape: diamond}
B@{ shape: diamond}
</pre>
<pre id="diagram4" class="mermaid2">
flowchart
D(("for D"))
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
---
flowchart LR
A e1@==> B
e1@{ animate: true}
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
I --> D & D
D@{ shape: question}
I@{ shape: question}
</pre>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
class e1 animate
</pre>
<h2>infinite</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
</pre>
<h2>Mermaid - edge-animation-slow</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
e1@{ animation: fast}
</pre>
<h2>Mermaid - edge-animation-fast</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
class e1 edge-animation-fast
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
curve: rounded
elk:
nodePlacementStrategy: NETWORK_SIMPLEX
---
flowchart LR
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
D --> I & I
a["a"]
D@{ shape: trap-b}
I@{ shape: lean-l}
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
<pre id="diagram4" class="mermaid2">
---
flowchart LR
%% subgraph s1["Untitled subgraph"]
C["Evaluate"]
%% end
info </pre
>
<pre id="diagram4" class="mermaid2">
B --> C
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
flowchart:
//curve: linear
---
flowchart LR
%% A ==> B
%% A2 --> B2
A{A} --> B((Bo boo)) & B & B & B
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
theme: default
look: classic
---
flowchart LR
subgraph s1["APA"]
D{"Use the editor"}
end
subgraph S2["S2"]
s1
I>"fa:fa-code Text"]
E["E"]
end
D -- Mermaid js --> I
D --> I & E
E --> I
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -466,7 +487,7 @@ config:
end
end
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -479,45 +500,7 @@ config:
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart LR
a
subgraph s0["APA"]
subgraph s8["APA"]
subgraph s1["APA"]
D{"X"}
E[Q]
end
subgraph s3["BAPA"]
F[Q]
I
end
D --> I
D --> I
D --> I
I{"X"}
end
end
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
flowchart LR
a
D{"Use the editor"}
D -- Mermaid js --> I{"fa:fa-code Text"}
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -556,7 +539,7 @@ flowchart LR
n8@{ shape: rect}
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -572,7 +555,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -581,7 +564,7 @@ flowchart LR
A{A} --> B & C
</pre
>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -593,7 +576,7 @@ flowchart LR
end
</pre
>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
@@ -611,7 +594,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
kanban:
@@ -630,81 +613,81 @@ kanban
task3[💻 Develop login feature]@{ ticket: 103 }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
flowchart LR
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
kanban
id2[In progress]
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
</pre>
<pre id="diagram4" class="mermaid2">
<pre id="diagram4" class="mermaid">
---
config:
kanban:
@@ -768,18 +751,22 @@ kanban
alert('It worked');
}
await mermaid.initialize({
// theme: 'forest',
// theme: 'base',
// theme: 'default',
// theme: 'forest',
// handDrawnSeed: 12,
// look: 'handDrawn',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// layout: 'dagre',
// layout: 'elk',
layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
flowchart: { titleTopMargin: 10 },
fontFamily: "'Recursive', sans-serif",
// fontFamily: 'Caveat',
// fontFamily: 'Kalam',
// fontFamily: 'courier',
fontFamily: 'arial',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',

View File

@@ -10,7 +10,7 @@
# Interface: ExternalDiagramDefinition
Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L94)
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/me
> **detector**: `DiagramDetector`
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L98)
---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/me
> **id**: `string`
Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L95)
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
---
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/me
> **loader**: `DiagramLoader`
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99)

View File

@@ -10,7 +10,7 @@
# Interface: LayoutLoaderDefinition
Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L21)
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.co
> `optional` **algorithm**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L27)
---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.co
> **loader**: `LayoutLoader`
Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23)
Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L26)
---
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.co
> **name**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22)
Defined in: [packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25)

View File

@@ -10,7 +10,7 @@
# Interface: RenderOptions
Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
## Properties
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com
> `optional` **algorithm**: `string`
Defined in: [packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
Defined in: [packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)

View File

@@ -12,4 +12,4 @@
> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)

View File

@@ -12,4 +12,4 @@
> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
Defined in: [packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130)

View File

@@ -6,18 +6,6 @@
# Blog
## [The Essential Guide to Mermaid Chart Plugin for VS Code \[08/2025\]](https://docs.mermaidchart.com/blog/posts/the-essential-guide-to-mermaid-chart-plugin-for-vs-code-08-2025)
9/9/2025 • 5 mins
Creating diagrams in VS Code has never been easier—the Mermaid VS Code plugin transforms text-based syntax into clear, professional visuals right inside your editor. With live previews, smart editing, and AI-powered diagram generation, it removes the friction from visualization so you can focus on coding.
## [How to Create Perfect Flowcharts Using AI in 2025 Step-by-Step Guide](https://docs.mermaidchart.com/blog/posts/how-to-create-perfect-flowcharts-using-ai-in-2025-step-by-step-guide)
7/22/2025 • 8 mins
In 2025, AI tools make creating flowcharts faster and easier than ever. With Mermaids AI flowchart generator, you can turn plain text into clear, accurate diagrams in seconds. This guide walks through how it works and how to get the most out of it for technical and business workflows alike.
## [Mermaid introduces the Visual Editor for Entity Relationship diagrams](https://docs.mermaidchart.com/blog/posts/mermaid-introduces-the-visual-editor-for-entity-relationship-diagrams)
7/15/2025 • 7 mins

View File

@@ -38,3 +38,5 @@ Each user journey is split into sections, these describe the part of the task
the user is trying to complete.
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
Score is a number between 1 and 5, inclusive.

View File

@@ -69,7 +69,7 @@
"@changesets/cli": "^2.27.12",
"@cspell/eslint-plugin": "^8.19.4",
"@cypress/code-coverage": "^3.12.49",
"@eslint/js": "^9.26.0",
"@eslint/js": "^9.35.0",
"@rollup/plugin-typescript": "^12.1.2",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
@@ -93,7 +93,7 @@
"cypress-image-snapshot": "^4.0.1",
"cypress-split": "^1.24.14",
"esbuild": "^0.25.0",
"eslint": "^9.26.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-cypress": "^4.3.0",
"eslint-plugin-html": "^8.1.3",
@@ -126,7 +126,7 @@
"tslib": "^2.8.1",
"tsx": "^4.7.3",
"typescript": "~5.7.3",
"typescript-eslint": "^8.38.0",
"typescript-eslint": "^8.43.0",
"vite": "^7.0.3",
"vite-plugin-istanbul": "^7.0.0",
"vitest": "^3.0.6"

View File

@@ -1,16 +1,5 @@
# @mermaid-js/layout-elk
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0
## 0.1.9
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-elk",
"version": "0.2.0",
"version": "0.1.9",
"description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import {
intersection,
ensureTrulyOutside,
makeInsidePoint,
tryNodeIntersect,
replaceEndpoint,
type RectLike,
type P,
} from '../geometry.js';
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
describe('geometry helpers', () => {
it('intersection: vertical approach hits bottom border', () => {
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
const h = rect.height / 2; // 25
const outside: P = { x: 0, y: 100 };
const inside: P = { x: 0, y: 0 };
const res = intersection(rect, outside, inside);
expect(approx(res.x, 0)).toBe(true);
expect(approx(res.y, h)).toBe(true);
});
it('ensureTrulyOutside nudges near-boundary point outward', () => {
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
// near bottom boundary (y ~ h)
const near: P = { x: 0, y: rect.height / 2 - 0.2 };
const out = ensureTrulyOutside(rect, near, 10);
expect(out.y).toBeGreaterThan(rect.height / 2);
});
it('makeInsidePoint keeps x for vertical and y from center', () => {
const rect: RectLike = { x: 10, y: 5, width: 100, height: 50 };
const outside: P = { x: 10, y: 40 };
const center: P = { x: 99, y: -123 }; // center y should be used
const inside = makeInsidePoint(rect, outside, center);
expect(inside.x).toBe(outside.x);
expect(inside.y).toBe(center.y);
});
it('tryNodeIntersect returns null for wrong-side intersections', () => {
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
const outside: P = { x: -50, y: 0 };
const node = { intersect: () => ({ x: 10, y: 0 }) } as any; // right side of center
const res = tryNodeIntersect(node, rect, outside);
expect(res).toBeNull();
});
it('replaceEndpoint dedup removes end/start appropriately', () => {
const pts: P[] = [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
];
// remove duplicate end
replaceEndpoint(pts, 'end', { x: 1, y: 1 });
expect(pts.length).toBe(1);
const pts2: P[] = [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
];
// remove duplicate start
replaceEndpoint(pts2, 'start', { x: 0, y: 0 });
expect(pts2.length).toBe(1);
});
});

View File

@@ -0,0 +1,209 @@
/* Geometry utilities extracted from render.ts for reuse and testing */
export interface P {
x: number;
y: number;
}
export interface RectLike {
x: number; // center x
y: number; // center y
width: number;
height: number;
padding?: number;
}
export interface NodeLike {
intersect?: (p: P) => P | null;
}
export const EPS = 1;
export const PUSH_OUT = 10;
export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
const halfW = bounds.width / 2;
const halfH = bounds.height / 2;
const left = bounds.x - halfW;
const right = bounds.x + halfW;
const top = bounds.y - halfH;
const bottom = bounds.y + halfH;
const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
return onLeft || onRight || onTop || onBottom;
};
/**
* Compute intersection between a rectangle (center x/y, width/height) and the line
* segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
*
* This version avoids snapping to outsidePoint when certain variables evaluate to 0
* (previously caused vertical top/bottom cases to miss the border). It only enforces
* axis-constant behavior for purely vertical/horizontal approaches.
*/
export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
const x = node.x;
const y = node.y;
const dx = Math.abs(x - insidePoint.x);
const w = node.width / 2;
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
const h = node.height / 2;
const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x);
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
// Intersection is top or bottom of rect.
const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
r = (R * q) / Q;
const res = {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
};
// Keep axis-constant special-cases only
if (R === 0) {
res.x = outsidePoint.x;
}
if (Q === 0) {
res.y = outsidePoint.y;
}
return res;
} else {
// Intersection on sides of rect
if (insidePoint.x < outsidePoint.x) {
r = outsidePoint.x - w - x;
} else {
r = x - w - outsidePoint.x;
}
const q = (Q * r) / R;
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
// Only handle axis-constant cases
if (R === 0) {
_x = outsidePoint.x;
}
if (Q === 0) {
_y = outsidePoint.y;
}
return { x: _x, y: _y };
}
};
export const outsideNode = (node: RectLike, point: P): boolean => {
const x = node.x;
const y = node.y;
const dx = Math.abs(point.x - x);
const dy = Math.abs(point.y - y);
const w = node.width / 2;
const h = node.height / 2;
return dx >= w || dy >= h;
};
export const ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
const dx = Math.abs(p.x - bounds.x);
const dy = Math.abs(p.y - bounds.y);
const w = bounds.width / 2;
const h = bounds.height / 2;
if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
const dirX = p.x - bounds.x;
const dirY = p.y - bounds.y;
const len = Math.sqrt(dirX * dirX + dirY * dirY);
if (len > 0) {
return {
x: bounds.x + (dirX / len) * (len + push),
y: bounds.y + (dirY / len) * (len + push),
};
}
}
return p;
};
export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
const isVertical = Math.abs(outside.x - bounds.x) < EPS;
const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
return {
x: isVertical
? outside.x
: outside.x < bounds.x
? bounds.x - bounds.width / 4
: bounds.x + bounds.width / 4,
y: isHorizontal ? outside.y : center.y,
};
};
export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
if (!node?.intersect) {
return null;
}
const res = node.intersect(outside);
if (!res) {
return null;
}
const wrongSide =
(outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
if (wrongSide) {
return null;
}
const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
if (dist <= EPS) {
return null;
}
return res;
};
export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
const inside = makeInsidePoint(bounds, outside, center);
return intersection(bounds, outside, inside);
};
export const computeNodeIntersection = (
node: NodeLike,
bounds: RectLike,
outside: P,
center: P
): P => {
const outside2 = ensureTrulyOutside(bounds, outside);
return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
};
export const replaceEndpoint = (
points: P[],
which: 'start' | 'end',
value: P | null | undefined,
tol = 0.1
) => {
if (!value || points.length === 0) {
return;
}
if (which === 'start') {
if (
points.length > 0 &&
Math.abs(points[0].x - value.x) < tol &&
Math.abs(points[0].y - value.y) < tol
) {
// duplicate start remove it
points.shift();
} else {
points[0] = value;
}
} else {
const last = points.length - 1;
if (
points.length > 0 &&
Math.abs(points[last].x - value.x) < tol &&
Math.abs(points[last].y - value.y) < tol
) {
// duplicate end remove it
points.pop();
} else {
points[last] = value;
}
}
};

View File

@@ -1,11 +1,26 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
// @ts-ignore TODO: Investigate D3 issue
import { curveLinear } from 'd3';
import ELK from 'elkjs/lib/elk.bundled.js';
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
import {
type P,
type RectLike,
outsideNode,
computeNodeIntersection,
replaceEndpoint,
onBorder,
} from './geometry.js';
type Node = LayoutData['nodes'][number];
// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers
const epsilon = 0.0001;
// Minimal structural type to avoid depending on d3 Selection typings
interface D3Selection<T extends Element> {
node(): T | null;
attr(name: string, value: string): D3Selection<T>;
}
interface LabelData {
width: number;
height: number;
@@ -16,18 +31,9 @@ interface LabelData {
interface NodeWithVertex extends Omit<Node, 'domId'> {
children?: LayoutData['nodes'];
labelData?: LabelData;
domId?: Node['domId'] | SVGGroup | d3.Selection<SVGAElement, unknown, Element | null, unknown>;
}
interface Point {
x: number;
y: number;
}
function distance(p1?: Point, p2?: Point): number {
if (!p1 || !p2) {
return 0;
}
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
domId?: D3Selection<SVGAElement | SVGGElement>;
}
export const render = async (
data4Layout: LayoutData,
svg: SVG,
@@ -61,39 +67,26 @@ export const render = async (
// Add the element to the DOM
if (!node.isGroup) {
// Create a clean node object for ELK with only the properties it expects
const child: NodeWithVertex = {
id: node.id,
width: node.width,
height: node.height,
// Store the original node data for later use
label: node.label,
isGroup: node.isGroup,
shape: node.shape,
padding: node.padding,
cssClasses: node.cssClasses,
cssStyles: node.cssStyles,
look: node.look,
// Include parentId for subgraph processing
parentId: node.parentId,
};
const child = node as NodeWithVertex;
graph.children.push(child);
nodeDb[node.id] = child;
nodeDb[node.id] = node;
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
const boundingBox = childNodeEl.node()!.getBBox();
// Store the domId separately for rendering, not in the ELK graph
child.domId = childNodeEl;
child.calcIntersect = node.calcIntersect;
child.width = boundingBox.width;
child.height = boundingBox.height;
} else {
// A subgraph
const child: NodeWithVertex & { children: NodeWithVertex[] } = {
...node,
domId: undefined,
children: [],
};
// Let elk render with the copy
graph.children.push(child);
// Save the original containing the intersection function
nodeDb[node.id] = child;
await addVertices(nodeEl, nodeArr, child, node.id);
@@ -168,7 +161,7 @@ export const render = async (
height: node.height,
};
if (node.isGroup) {
log.debug('id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
// TODO use faster way of cloning
const clusterNode = JSON.parse(JSON.stringify(node));
@@ -177,10 +170,10 @@ export const render = async (
clusterNode.width = Math.max(clusterNode.width, node.labelData.width);
await insertCluster(subgraphEl, clusterNode);
log.debug('id (UIO)= ', node.id, node.width, node.shape, node.labels);
log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels);
} else {
log.info(
'id NODE = ',
'Id NODE = ',
node.id,
node.x,
node.y,
@@ -222,25 +215,19 @@ export const render = async (
});
});
subgraphs.forEach(function (subgraph: { id: string | number }) {
const data: any = { id: subgraph.id };
if (parentLookupDb.parentById[subgraph.id] !== undefined) {
data.parent = parentLookupDb.parentById[subgraph.id];
}
});
return parentLookupDb;
};
const getEdgeStartEndPoint = (edge: any) => {
const source: any = edge.start;
const target: any = edge.end;
// edge.start and edge.end are IDs (string/number) in our layout data
const sourceId: string | number = edge.start;
const targetId: string | number = edge.end;
// Save the original source and target
const sourceId = source;
const targetId = target;
const source = sourceId;
const target = targetId;
const startNode = nodeDb[edge.start.id];
const endNode = nodeDb[edge.end.id];
const startNode = nodeDb[sourceId];
const endNode = nodeDb[targetId];
if (!startNode || !endNode) {
return { source, target };
@@ -263,6 +250,112 @@ export const render = async (
/**
* Add edges to graph based on parsed graph definition
*/
// Edge helper maps and utilities (de-duplicated)
const ARROW_MAP: Record<string, [string, string]> = {
arrow_open: ['arrow_open', 'arrow_open'],
arrow_cross: ['arrow_open', 'arrow_cross'],
double_arrow_cross: ['arrow_cross', 'arrow_cross'],
arrow_point: ['arrow_open', 'arrow_point'],
double_arrow_point: ['arrow_point', 'arrow_point'],
arrow_circle: ['arrow_open', 'arrow_circle'],
double_arrow_circle: ['arrow_circle', 'arrow_circle'],
};
const computeStroke = (
stroke: string | undefined,
defaultStyle?: string,
defaultLabelStyle?: string
) => {
// Defaults correspond to 'normal'
let thickness = 'normal';
let pattern = 'solid';
let style = '';
let labelStyle = '';
if (stroke === 'dotted') {
pattern = 'dotted';
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
} else if (stroke === 'thick') {
thickness = 'thick';
style = 'stroke-width: 3.5px;fill:none;';
} else {
// normal
style = defaultStyle ?? 'fill:none;';
if (defaultLabelStyle !== undefined) {
labelStyle = defaultLabelStyle;
}
}
return { thickness, pattern, style, labelStyle };
};
const getCurve = (edgeInterpolate: any, edgesDefaultInterpolate: any, confCurve: any) => {
if (edgeInterpolate !== undefined) {
return interpolateToCurve(edgeInterpolate, curveLinear);
}
if (edgesDefaultInterpolate !== undefined) {
return interpolateToCurve(edgesDefaultInterpolate, curveLinear);
}
// @ts-ignore TODO: fix this
return interpolateToCurve(confCurve, curveLinear);
};
const buildEdgeData = (
edge: any,
defaults: {
defaultStyle?: string;
defaultLabelStyle?: string;
defaultInterpolate?: any;
confCurve: any;
},
common: any
) => {
const edgeData: any = { style: '', labelStyle: '' };
edgeData.minlen = edge.length || 1;
// maintain legacy behavior
edge.text = edge.label;
// Arrowhead fill vs none
edgeData.arrowhead = edge.type === 'arrow_open' ? 'none' : 'normal';
// Arrow types
const arrowMap = ARROW_MAP[edge.type] ?? ARROW_MAP.arrow_open;
edgeData.arrowTypeStart = arrowMap[0];
edgeData.arrowTypeEnd = arrowMap[1];
// Optional edge label positioning flags
edgeData.startLabelRight = edge.startLabelRight;
edgeData.endLabelLeft = edge.endLabelLeft;
// Stroke
const strokeRes = computeStroke(edge.stroke, defaults.defaultStyle, defaults.defaultLabelStyle);
edgeData.thickness = strokeRes.thickness;
edgeData.pattern = strokeRes.pattern;
edgeData.style = (edgeData.style || '') + (strokeRes.style || '');
edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || '');
// Curve
// @ts-ignore - defaults.confCurve is present at runtime but missing in type
edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
// Arrowhead style + labelpos when we have label text
const hasText = (edge?.text ?? '') !== '';
if (hasText) {
edgeData.arrowheadStyle = 'fill: #333';
edgeData.labelpos = 'c';
} else if (edge.style !== undefined) {
edgeData.arrowheadStyle = 'fill: #333';
}
edgeData.labelType = edge.labelType;
edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
return edgeData;
};
const addEdges = async function (
dataForLayout: { edges: any; direction?: string },
graph: {
@@ -284,7 +377,6 @@ export const render = async (
const edges = dataForLayout.edges;
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
const linkIdCnt: any = {};
const dir = dataForLayout.direction || 'DOWN';
let defaultStyle: string | undefined;
let defaultLabelStyle: string | undefined;
@@ -314,105 +406,24 @@ export const render = async (
linkIdCnt[linkIdBase]++;
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
}
const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase];
const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase];
edge.id = linkId;
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
const linkNameStart = 'LS_' + edge.start;
const linkNameEnd = 'LE_' + edge.end;
const edgeData: any = { style: '', labelStyle: '' };
edgeData.minlen = edge.length || 1;
edge.text = edge.label;
// Set link type for rendering
if (edge.type === 'arrow_open') {
edgeData.arrowhead = 'none';
} else {
edgeData.arrowhead = 'normal';
}
// Check of arrow types, placed here in order not to break old rendering
edgeData.arrowTypeStart = 'arrow_open';
edgeData.arrowTypeEnd = 'arrow_open';
/* eslint-disable no-fallthrough */
switch (edge.type) {
case 'double_arrow_cross':
edgeData.arrowTypeStart = 'arrow_cross';
case 'arrow_cross':
edgeData.arrowTypeEnd = 'arrow_cross';
break;
case 'double_arrow_point':
edgeData.arrowTypeStart = 'arrow_point';
case 'arrow_point':
edgeData.arrowTypeEnd = 'arrow_point';
break;
case 'double_arrow_circle':
edgeData.arrowTypeStart = 'arrow_circle';
case 'arrow_circle':
edgeData.arrowTypeEnd = 'arrow_circle';
break;
}
let style = '';
let labelStyle = '';
edgeData.startLabelRight = edge.startLabelRight;
edgeData.endLabelLeft = edge.endLabelLeft;
switch (edge.stroke) {
case 'normal':
style = 'fill:none;';
if (defaultStyle !== undefined) {
style = defaultStyle;
}
if (defaultLabelStyle !== undefined) {
labelStyle = defaultLabelStyle;
}
edgeData.thickness = 'normal';
edgeData.pattern = 'solid';
break;
case 'dotted':
edgeData.thickness = 'normal';
edgeData.pattern = 'dotted';
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
break;
case 'thick':
edgeData.thickness = 'thick';
edgeData.pattern = 'solid';
edgeData.style = 'stroke-width: 3.5px;fill:none;';
break;
}
edgeData.style = edgeData.style += style;
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
const conf = getConfig();
if (edge.interpolate !== undefined) {
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
} else if (edges.defaultInterpolate !== undefined) {
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
} else {
// @ts-ignore TODO: fix this
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
}
if (edge.text === undefined) {
if (edge.style !== undefined) {
edgeData.arrowheadStyle = 'fill: #333';
}
} else {
edgeData.arrowheadStyle = 'fill: #333';
edgeData.labelpos = 'c';
}
edgeData.labelType = edge.labelType;
edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
const edgeData = buildEdgeData(
edge,
{
defaultStyle,
defaultLabelStyle,
defaultInterpolate: edges.defaultInterpolate,
// @ts-ignore - conf.curve exists at runtime but is missing from typing
confCurve: conf.curve,
},
common
);
edgeData.id = linkId;
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
@@ -421,13 +432,11 @@ export const render = async (
// calculate start and end points of the edge, note that the source and target
// can be modified for shapes that have ports
// @ts-ignore TODO: fix this
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir);
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge);
log.debug('abc78 source and target', source, target);
// Add the edge to the graph
graph.edges.push({
// @ts-ignore TODO: fix this
id: 'e' + edge.start + edge.end,
...edge,
sources: [source],
targets: [target],
@@ -461,6 +470,7 @@ export const render = async (
case 'RL':
return 'LEFT';
case 'TB':
case 'TD': // TD is an alias for TB in Mermaid
return 'DOWN';
case 'BT':
return 'UP';
@@ -484,6 +494,203 @@ export const render = async (
}
}
// Node bounds helpers (global)
const getEffectiveGroupWidth = (node: any): number => {
const labelW = node?.labels?.[0]?.width ?? 0;
const padding = node?.padding ?? 0;
return Math.max(node.width ?? 0, labelW + padding);
};
const boundsFor = (node: any): RectLike => {
const width = node?.isGroup ? getEffectiveGroupWidth(node) : node.width;
return {
x: node.offset.posX + node.width / 2,
y: node.offset.posY + node.height / 2,
width,
height: node.height,
padding: node.padding,
};
};
// Helper utilities for endpoint handling around cutter2
type Side = 'start' | 'end';
const approxEq = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
const isCenterApprox = (pt: P, node: { x: number; y: number }) =>
approxEq(pt.x, node.x) && approxEq(pt.y, node.y);
const getCandidateBorderPoint = (
points: P[],
node: any,
side: Side
): { candidate: P; centerApprox: boolean } => {
if (!points?.length) {
return { candidate: { x: node.x, y: node.y } as P, centerApprox: true };
}
if (side === 'start') {
const first = points[0];
const centerApprox = isCenterApprox(first, node);
const candidate = centerApprox && points.length > 1 ? points[1] : first;
return { candidate, centerApprox };
} else {
const last = points[points.length - 1];
const centerApprox = isCenterApprox(last, node);
const candidate = centerApprox && points.length > 1 ? points[points.length - 2] : last;
return { candidate, centerApprox };
}
};
const dropAutoCenterPoint = (points: P[], side: Side, doDrop: boolean) => {
if (!doDrop) {
return;
}
if (side === 'start') {
if (points.length > 0) {
points.shift();
}
} else {
if (points.length > 0) {
points.pop();
}
}
};
const applyStartIntersectionIfNeeded = (points: P[], startNode: any, startBounds: RectLike) => {
let firstOutsideStartIndex = -1;
for (const [i, p] of points.entries()) {
if (outsideNode(startBounds, p)) {
firstOutsideStartIndex = i;
break;
}
}
if (firstOutsideStartIndex !== -1) {
const outsidePointForStart = points[firstOutsideStartIndex];
const startCenter = points[0];
const startIntersection = computeNodeIntersection(
startNode,
startBounds,
outsidePointForStart,
startCenter
);
replaceEndpoint(points, 'start', startIntersection);
log.debug('UIO cutter2: start-only intersection applied', { startIntersection });
}
};
const applyEndIntersectionIfNeeded = (points: P[], endNode: any, endBounds: RectLike) => {
let outsideIndexForEnd = -1;
for (let i = points.length - 1; i >= 0; i--) {
if (outsideNode(endBounds, points[i])) {
outsideIndexForEnd = i;
break;
}
}
if (outsideIndexForEnd !== -1) {
const outsidePointForEnd = points[outsideIndexForEnd];
const endCenter = points[points.length - 1];
const endIntersection = computeNodeIntersection(
endNode,
endBounds,
outsidePointForEnd,
endCenter
);
replaceEndpoint(points, 'end', endIntersection);
log.debug('UIO cutter2: end-only intersection applied', { endIntersection });
}
};
const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
const startBounds = boundsFor(startNode);
const endBounds = boundsFor(endNode);
if (_points.length === 0) {
return [];
}
// Copy the original points array
const points: P[] = [..._points] as P[];
// The first point is the center of sNode, the last point is the center of eNode
const startCenter = points[0];
const endCenter = points[points.length - 1];
// Minimal, structured logging for diagnostics
log.debug('PPP cutter2: bounds', { startBounds, endBounds });
log.debug('PPP cutter2: original points', _points);
let firstOutsideStartIndex = -1;
// Single iteration through the array
for (const [i, point] of points.entries()) {
if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
firstOutsideStartIndex = i;
}
if (outsideNode(endBounds, point)) {
// keep scanning; we'll also scan from the end for the last outside point
}
}
// Calculate intersection with start node if we found a point outside it
if (firstOutsideStartIndex !== -1) {
const outsidePointForStart = points[firstOutsideStartIndex];
const startIntersection = computeNodeIntersection(
startNode,
startBounds,
outsidePointForStart,
startCenter
);
log.debug('UIO cutter2: start intersection', startIntersection);
replaceEndpoint(points, 'start', startIntersection);
}
// Calculate intersection with end node
let outsidePointForEnd = null;
let outsideIndexForEnd = -1;
for (let i = points.length - 1; i >= 0; i--) {
if (outsideNode(endBounds, points[i])) {
outsidePointForEnd = points[i];
outsideIndexForEnd = i;
break;
}
}
if (!outsidePointForEnd && points.length > 1) {
outsidePointForEnd = points[points.length - 2];
outsideIndexForEnd = points.length - 2;
}
if (outsidePointForEnd) {
const endIntersection = computeNodeIntersection(
endNode,
endBounds,
outsidePointForEnd,
endCenter
);
log.debug('UIO cutter2: end intersection', { endIntersection, outsideIndexForEnd });
replaceEndpoint(points, 'end', endIntersection);
}
// Final cleanup: Check if the last point is too close to the previous point
if (points.length > 1) {
const lastPoint = points[points.length - 1];
const secondLastPoint = points[points.length - 2];
const distance = Math.sqrt(
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
);
if (distance < 2) {
log.debug('UIO cutter2: trimming tail point (too close)', {
distance,
lastPoint,
secondLastPoint,
});
points.pop();
}
}
log.debug('UIO cutter2: final points', points);
return points;
};
// @ts-ignore - ELK is not typed
const elk = new ELK();
const element = svg.select('g');
@@ -495,17 +702,19 @@ export const render = async (
id: 'root',
layoutOptions: {
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'elk.layered.crossingMinimization.forceNodeModelOrder':
data4Layout.config.elk?.forceNodeModelOrder,
'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder,
'elk.algorithm': algorithm,
'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
'elk.direction': 'DOWN',
'spacing.baseValue': 35,
'spacing.baseValue': 40,
'elk.layered.crossingMinimization.forceNodeModelOrder':
data4Layout.config.elk?.forceNodeModelOrder,
'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder,
'elk.layered.unnecessaryBendpoints': true,
'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy,
// 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER',
// 'elk.layered.cycleBreaking.strategy': 'MODEL_ORDER',
// 'spacing.nodeNode': 20,
// 'spacing.nodeNodeBetweenLayers': 25,
// 'spacing.edgeNode': 20,
@@ -513,22 +722,28 @@ export const render = async (
// 'spacing.edgeEdge': 10,
// 'spacing.edgeEdgeBetweenLayers': 20,
// 'spacing.nodeSelfLoop': 20,
// Tweaking options
// 'nodePlacement.favorStraightEdges': true,
// 'elk.layered.nodePlacement.favorStraightEdges': true,
// 'nodePlacement.feedbackEdges': true,
// 'elk.layered.wrapping.multiEdge.improveCuts': true,
// 'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
'elk.layered.wrapping.multiEdge.improveCuts': true,
'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
// 'elk.layered.wrapping.strategy': 'MULTI_EDGE',
// 'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
// 'elk.layered.mergeHierarchyEdges': true,
// 'elk.layered.wrapping.strategy': 'SINGLE_EDGE',
'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
'elk.layered.mergeHierarchyEdges': true,
// 'elk.layered.feedbackEdges': true,
// 'elk.layered.crossingMinimization.semiInteractive': true,
// 'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1,
// 'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0,
// 'elk.layered.wrapping.validify.strategy': 'LOOK_BACK',
// 'elk.insideSelfLoops.activate': true,
// 'elk.separateConnectedComponents': true,
// 'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE',
// 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', // NODES_AND_EDGES
// 'elk.layered.considerModelOrder.strategy': 'EDGES', // NODES_AND_EDGES
// 'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES
},
children: [],
@@ -538,7 +753,7 @@ export const render = async (
log.info('Drawing flowchart using v4 renderer', elk);
// Set the direction of the graph based on the parsed information
const dir = data4Layout.direction || 'DOWN';
const dir = data4Layout.direction ?? 'DOWN';
elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
// Create the lookup db for the subgraphs and their children to used when creating
@@ -569,15 +784,16 @@ export const render = async (
// Subgraph
if (parentLookupDb.childrenById[node.id] !== undefined) {
// Set label and adjust node width separately (avoid side effects in labels array)
node.labels = [
{
text: node.label,
width: node?.labelData?.width || 50,
height: node?.labelData?.height || 50,
width: node?.labelData?.width ?? 50,
height: node?.labelData?.height ?? 50,
},
(node.width = node.width + 2 * node.padding),
log.debug('UIO node label', node?.labelData?.width, node.padding),
];
node.width = node.width + 2 * node.padding;
log.debug('UIO node label', node?.labelData?.width, node.padding);
node.layoutOptions = {
'spacing.baseValue': 30,
'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]',
@@ -641,7 +857,7 @@ export const render = async (
try {
g = await elk.layout(elkGraph);
log.debug('APA01 after - success');
log.debug('APA01 layout result:', JSON.stringify(g, null, 2));
log.info('APA01 layout result:', JSON.stringify(g, null, 2));
} catch (error) {
log.error('APA01 ELK layout error:', error);
throw error;
@@ -702,10 +918,10 @@ export const render = async (
// sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width);
sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding);
// sw = startNode.width;
log.debug(
log.info(
'UIO width',
startNode.id,
startNode.with,
startNode.width,
'bbox.width=',
bbox.width,
'lw=',
@@ -725,7 +941,7 @@ export const render = async (
log.debug(
'UIO width',
startNode.id,
startNode.with,
startNode.width,
bbox.width,
'EW = ',
ew,
@@ -733,38 +949,109 @@ export const render = async (
startNode.innerHTML
);
}
startNode.x = startNode.offset.posX + startNode.width / 2;
startNode.y = startNode.offset.posY + startNode.height / 2;
endNode.x = endNode.offset.posX + endNode.width / 2;
endNode.y = endNode.offset.posY + endNode.height / 2;
if (startNode.calcIntersect) {
const intersection = startNode.calcIntersect(
{
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: startNode.width,
height: startNode.height,
},
edge.points[0]
);
// Only add center points for non-subgraph nodes or when the edge path doesn't already end near the target
const shouldAddStartCenter = startNode.shape !== 'rect33';
const shouldAddEndCenter = endNode.shape !== 'rect33';
if (distance(intersection, edge.points[0]) > epsilon) {
edge.points.unshift(intersection);
}
}
if (endNode.calcIntersect) {
const intersection = endNode.calcIntersect(
{
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: endNode.width,
height: endNode.height,
},
edge.points[edge.points.length - 1]
);
if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
edge.points.push(intersection);
}
if (shouldAddStartCenter) {
edge.points.unshift({
x: startNode.x,
y: startNode.y,
});
}
if (shouldAddEndCenter) {
edge.points.push({
x: endNode.x,
y: endNode.y,
});
}
// Debug and sanitize points around cutter2
const prevPoints = Array.isArray(edge.points) ? [...edge.points] : [];
const endBounds = boundsFor(endNode);
log.debug(
'PPP cutter2: Points before cutter2:',
JSON.stringify(edge.points),
'endBounds:',
endBounds,
onBorder(endBounds, edge.points[edge.points.length - 1])
);
// Block for reducing variable scope and guardrails for the cutter function
{
const startBounds = boundsFor(startNode);
const endBounds = boundsFor(endNode);
const startIsGroup = !!startNode?.isGroup;
const endIsGroup = !!endNode?.isGroup;
const { candidate: startCandidate, centerApprox: startCenterApprox } =
getCandidateBorderPoint(prevPoints as P[], startNode, 'start');
const { candidate: endCandidate, centerApprox: endCenterApprox } =
getCandidateBorderPoint(prevPoints as P[], endNode, 'end');
const skipStart = startIsGroup && onBorder(startBounds, startCandidate);
const skipEnd = endIsGroup && onBorder(endBounds, endCandidate);
dropAutoCenterPoint(prevPoints as P[], 'start', skipStart && startCenterApprox);
dropAutoCenterPoint(prevPoints as P[], 'end', skipEnd && endCenterApprox);
if (skipStart || skipEnd) {
if (!skipStart) {
applyStartIntersectionIfNeeded(prevPoints as P[], startNode, startBounds);
}
if (!skipEnd) {
applyEndIntersectionIfNeeded(prevPoints as P[], endNode, endBounds);
}
log.debug('PPP cutter2: skipping cutter2 due to on-border group endpoint(s)', {
skipStart,
skipEnd,
startCenterApprox,
endCenterApprox,
startCandidate,
endCandidate,
});
edge.points = prevPoints;
} else {
edge.points = cutter2(startNode, endNode, prevPoints);
}
}
log.debug('PPP cutter2: Points after cutter2:', JSON.stringify(edge.points));
const hasNaN = (pts: { x: number; y: number }[]) =>
pts?.some((p) => !Number.isFinite(p?.x) || !Number.isFinite(p?.y));
if (!Array.isArray(edge.points) || edge.points.length < 2 || hasNaN(edge.points)) {
log.warn(
'POI cutter2: Invalid points from cutter2, falling back to prevPoints',
edge.points
);
// Fallback to previous points and strip any invalid ones just in case
const cleaned = prevPoints.filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y));
edge.points = cleaned.length >= 2 ? cleaned : prevPoints;
}
log.debug('UIO cutter2: Points after cutter2 (sanitized):', edge.points);
// Remove consecutive duplicate points to avoid zero-length segments in path builders
const deduped = edge.points.filter(
(p: { x: number; y: number }, i: number, arr: { x: number; y: number }[]) => {
if (i === 0) {
return true;
}
const prev = arr[i - 1];
return Math.abs(p.x - prev.x) > 1e-6 || Math.abs(p.y - prev.y) > 1e-6;
}
);
if (deduped.length !== edge.points.length) {
log.debug('UIO cutter2: removed consecutive duplicate points', {
before: edge.points,
after: deduped,
});
}
edge.points = deduped;
const paths = insertEdge(
edgesEl,
edge,
@@ -772,8 +1059,10 @@ export const render = async (
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
data4Layout.diagramId,
true
);
log.info('APA12 edge points after insert', JSON.stringify(edge.points));
edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;

View File

@@ -1,12 +0,0 @@
# @mermaid-js/layout-tidy-tree
## 0.2.0
### Minor Changes
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- Updated dependencies [[`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800), [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20), [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05), [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8)]:
- mermaid@11.11.0

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-tidy-tree",
"version": "0.2.0",
"version": "0.1.0",
"description": "Tidy-tree layout engine for mermaid",
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -1,19 +1,5 @@
# mermaid
## 11.11.0
### Minor Changes
- [#6704](https://github.com/mermaid-js/mermaid/pull/6704) [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05) Thanks [@omkarht](https://github.com/omkarht)! - feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- [#6905](https://github.com/mermaid-js/mermaid/pull/6905) [`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Render newlines as spaces in class diagrams
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
## 11.10.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.11.0",
"version": "11.10.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -82,7 +82,7 @@
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.7",
"marked": "^16.0.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",

View File

@@ -3,6 +3,7 @@ import type * as d3 from 'd3';
import type { SetOptional, SetRequired } from 'type-fest';
import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type { DiagramOrientation } from '../diagrams/git/gitGraphTypes.js';
export interface DiagramMetadata {
title?: string;
@@ -35,7 +36,8 @@ export interface DiagramDB {
getAccTitle?: () => string;
setAccDescription?: (description: string) => void;
getAccDescription?: () => string;
getDirection?: () => string | undefined;
setDirection?: (dir: DiagramOrientation) => void;
setDisplayMode?: (title: string) => void;
bindFunctions?: (element: Element) => void;
}

View File

@@ -0,0 +1,48 @@
import { describe } from 'vitest';
import { draw } from './architectureRenderer.js';
import { Diagram } from '../../Diagram.js';
import { addDetector } from '../../diagram-api/detectType.js';
import architectureDetector from './architectureDetector.js';
import { ensureNodeFromSelector, jsdomIt } from '../../tests/util.js';
const { id, detector, loader } = architectureDetector;
addDetector(id, detector, loader); // Add architecture schemas to Mermaid
describe('architecture diagram SVGs', () => {
jsdomIt('should add ids', async () => {
const svgNode = await drawDiagram(`
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
`);
const nodesForGroup = svgNode.querySelectorAll(`#group-api`);
expect(nodesForGroup.length).toBe(1);
const serviceIds = [...svgNode.querySelectorAll(`[id^=service-]`)].map(({ id }) => id).sort();
expect(serviceIds).toStrictEqual([
'service-db',
'service-disk1',
'service-disk2',
'service-server',
]);
const edgeIds = [...svgNode.querySelectorAll(`.edge[id^=L_]`)].map(({ id }) => id).sort();
expect(edgeIds).toStrictEqual(['L_db_server_0', 'L_disk1_server_0', 'L_disk2_db_0']);
});
});
async function drawDiagram(diagramText: string): Promise<Element> {
const diagram = await Diagram.fromText(diagramText, {});
await draw('NOT_USED', 'svg', '1.0.0', diagram);
return ensureNodeFromSelector('#svg');
}

View File

@@ -20,6 +20,7 @@ import {
type ArchitectureJunction,
type ArchitectureService,
} from './architectureTypes.js';
import { getEdgeId } from '../../utils.js';
export const drawEdges = async function (
edgesEl: D3Element,
@@ -91,7 +92,8 @@ export const drawEdges = async function (
g.insert('path')
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
.attr('class', 'edge');
.attr('class', 'edge')
.attr('id', getEdgeId(source, target, { prefix: 'L' }));
if (sourceArrow) {
const xShift = isArchitectureDirectionX(sourceDir)
@@ -206,8 +208,9 @@ export const drawGroups = async function (
if (data.type === 'group') {
const { h, w, x1, y1 } = node.boundingBox();
groupsEl
.append('rect')
const groupsNode = groupsEl.append('rect');
groupsNode
.attr('id', `group-${data.id}`)
.attr('x', x1 + halfIconSize)
.attr('y', y1 + halfIconSize)
.attr('width', w)
@@ -262,6 +265,7 @@ export const drawGroups = async function (
')'
);
}
db.setElementForId(data.id, groupsNode);
}
})
);
@@ -342,9 +346,9 @@ export const drawServices = async function (
);
}
serviceElem.attr('class', 'architecture-service');
serviceElem.attr('id', `service-${service.id}`).attr('class', 'architecture-service');
const { width, height } = serviceElem._groups[0][0].getBBox();
const { width, height } = serviceElem.node().getBBox();
service.width = width;
service.height = height;
db.setElementForId(service.id, serviceElem);

View File

@@ -37,6 +37,7 @@ export class MindmapDB {
private nodes: MindmapNode[] = [];
private count = 0;
private elements: Record<number, D3Element> = {};
private baseLevel?: number;
public readonly nodeType: typeof nodeType;
constructor() {
@@ -54,6 +55,7 @@ export class MindmapDB {
this.nodes = [];
this.count = 0;
this.elements = {};
this.baseLevel = undefined;
}
public getParent(level: number): MindmapNode | null {
@@ -72,6 +74,17 @@ export class MindmapDB {
public addNode(level: number, id: string, descr: string, type: number): void {
log.info('addNode', level, id, descr, type);
let isRoot = false;
if (this.nodes.length === 0) {
this.baseLevel = level;
level = 0;
isRoot = true;
} else if (this.baseLevel !== undefined) {
level = level - this.baseLevel;
isRoot = false;
}
const conf = getConfig();
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
@@ -92,6 +105,7 @@ export class MindmapDB {
children: [],
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
isRoot,
};
const parent = this.getParent(level);
@@ -99,7 +113,7 @@ export class MindmapDB {
parent.children.push(node);
this.nodes.push(node);
} else {
if (this.nodes.length === 0) {
if (isRoot) {
this.nodes.push(node);
} else {
throw new Error(
@@ -204,8 +218,7 @@ export class MindmapDB {
// Build CSS classes for the node
const cssClasses = ['mindmap-node'];
// Add section-specific classes
if (node.level === 0) {
if (node.isRoot === true) {
// Root node gets special classes
cssClasses.push('section-root', 'section--1');
} else if (node.section !== undefined) {

View File

@@ -15,6 +15,7 @@ export interface MindmapNode {
icon?: string;
x?: number;
y?: number;
isRoot?: boolean;
}
export type FilledMindMapNode = RequiredDeep<MindmapNode>;

View File

@@ -40,26 +40,105 @@ const openSourceFeatures: Feature[] = [
{ iconUrl: '/icons/version-history.svg', featureName: 'Version history' },
];
const editorColumns: EditorColumn[] = [
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=2_editor_selection&utm_campaign=start_pro&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
features: openSourceFeatures,
},
const playgroundFeatures: Feature[] = [
{ iconUrl: '/icons/public.svg', featureName: 'Diagram stored in URL' },
{ iconUrl: '/icons/terminal.svg', featureName: 'Code editor' },
{ iconUrl: '/icons/whiteboard.svg', featureName: 'Whiteboard' },
];
const editorColumnVariants: EditorColumn[][] = [
[
{
title: 'Playground',
description: 'Basic features, no login',
redirectUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=3_editor_selection_A&utm_campaign=start_playground',
buttonText: 'Start free',
features: playgroundFeatures,
},
{
title: 'Free or Pro',
description: 'Advanced features, Free or Pro account',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=3_editor_selection_A&utm_campaign=start_free',
proTrialButtonText: 'Start free',
highlighted: true,
redBarText: 'Best for collaboration',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
redirectUrl: 'https://mermaid.live/edit',
buttonText: 'Start free',
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
redirectUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_B&utm_campaign=start_free',
buttonText: 'Start Free',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start Pro trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_B&utm_campaign=start_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
isButtonMargined: true,
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free trial',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_C&utm_campaign=start_trial&redirect=%2Fapp%2Fuser%2Fbilling%2Fcheckout%3FisFromMermaid%3Dtrue',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
buttonText: 'Start free',
redirectUrl: 'https://mermaid.live/edit',
features: openSourceFeatures,
},
],
[
{
title: 'Mermaid Pro',
description: 'Unlock AI and real-time collaboration',
highlighted: true,
redBarText: 'Recommended',
proTrialButtonText: 'Start free',
proTrialUrl:
'https://www.mermaidchart.com/app/sign-up?utm_source=mermaid_js&utm_medium=editor_selection_D&utm_campaign=start_free',
features: mermaidChartFeatures,
},
{
title: 'Open Source',
description: 'Code only, no login',
redirectUrl: 'https://mermaid.live/edit',
buttonText: 'Start free',
features: openSourceFeatures,
},
],
];
const editorColumns = editorColumnVariants[Math.floor(Math.random() * editorColumnVariants.length)];
const isVisible = ref(false);
const handleMouseDown = (e: MouseEvent) => {

View File

@@ -1,7 +1,13 @@
import mermaid, { type MermaidConfig } from 'mermaid';
import zenuml from '../../../../../mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
import tidyTreeLayout from '../../../../../mermaid-layout-tidy-tree/dist/mermaid-layout-tidy-tree.core.mjs';
import layouts from '../../../../../mermaid-layout-elk/dist/mermaid-layout-elk.core.mjs';
const init = mermaid.registerExternalDiagrams([zenuml]);
const init = Promise.all([
mermaid.registerExternalDiagrams([zenuml]),
mermaid.registerLayoutLoaders(layouts),
mermaid.registerLayoutLoaders(tidyTreeLayout),
]);
mermaid.registerIconPacks([
{
name: 'logos',

View File

@@ -1,17 +1,5 @@
# Blog
## [The Essential Guide to Mermaid Chart Plugin for VS Code [08/2025]](https://docs.mermaidchart.com/blog/posts/the-essential-guide-to-mermaid-chart-plugin-for-vs-code-08-2025)
9/9/2025 • 5 mins
Creating diagrams in VS Code has never been easier—the Mermaid VS Code plugin transforms text-based syntax into clear, professional visuals right inside your editor. With live previews, smart editing, and AI-powered diagram generation, it removes the friction from visualization so you can focus on coding.
## [How to Create Perfect Flowcharts Using AI in 2025 Step-by-Step Guide](https://docs.mermaidchart.com/blog/posts/how-to-create-perfect-flowcharts-using-ai-in-2025-step-by-step-guide)
7/22/2025 • 8 mins
In 2025, AI tools make creating flowcharts faster and easier than ever. With Mermaids AI flowchart generator, you can turn plain text into clear, accurate diagrams in seconds. This guide walks through how it works and how to get the most out of it for technical and business workflows alike.
## [Mermaid introduces the Visual Editor for Entity Relationship diagrams](https://docs.mermaidchart.com/blog/posts/mermaid-introduces-the-visual-editor-for-entity-relationship-diagrams)
7/15/2025 • 7 mins

View File

@@ -33,7 +33,7 @@
"pathe": "^2.0.3",
"unocss": "^66.4.2",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.1.1",
"vite": "^7.0.0",
"vite-plugin-pwa": "^1.0.0",
"vitepress": "1.6.3",
"workbox-window": "^7.3.0"

View File

@@ -20,3 +20,5 @@ Each user journey is split into sections, these describe the part of the task
the user is trying to complete.
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
Score is a number between 1 and 5, inclusive.

View File

@@ -13,6 +13,10 @@ const virtualModuleId = 'virtual:mermaid-config';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
export default defineConfig({
build: {
// Vite v7 changes the default target and drops old browser support
target: 'modules',
},
optimizeDeps: {
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
// This needs to be excluded from optimization

View File

@@ -4,6 +4,9 @@ import { internalHelpers } from '../internals.js';
import { log } from '../logger.js';
import type { LayoutData } from './types.js';
// console.log('MUST be removed, this only for keeping dev server working');
// import tmp from './layout-algorithms/dagre/index.js';
export interface RenderOptions {
algorithm?: string;
}

View File

@@ -1,9 +1,13 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { evaluate, getUrl } from '../../diagrams/common/common.js';
import { evaluate } from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import { createText } from '../createText.js';
import utils from '../../utils.js';
import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js';
import {
getLineFunctionsWithOffset,
markerOffsets,
markerOffsets2,
} from '../../utils/lineWithOffset.js';
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
import {
@@ -25,10 +29,10 @@ import {
import rough from 'roughjs';
import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
import { isLabelStyle, styles2String } from './shapes/handDrawnShapeStyles.js';
const edgeLabels = new Map();
const terminalLabels = new Map();
export const edgeLabels = new Map();
export const terminalLabels = new Map();
export const clear = () => {
edgeLabels.clear();
@@ -43,8 +47,10 @@ export const getLabelStyles = (styleArray) => {
export const insertEdgeLabel = async (elem, edge) => {
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
const { labelStyles } = styles2String(edge);
edge.labelStyle = labelStyles;
const labelElement = await createText(elem, edge.label, {
style: getLabelStyles(edge.labelStyle),
style: edge.labelStyle,
useHtmlLabels,
addSvgBackground: true,
isNode: false,
@@ -55,7 +61,7 @@ export const insertEdgeLabel = async (elem, edge) => {
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
// Create inner g, label, this will be positioned now for centering the text
const label = edgeLabel.insert('g').attr('class', 'label');
const label = edgeLabel.insert('g').attr('class', 'label').attr('data-id', edge.id);
label.node().appendChild(labelElement);
// Center the label
@@ -438,7 +444,33 @@ const fixCorners = function (lineData) {
}
return newLineData;
};
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
const generateDashArray = (len, oValueS, oValueE) => {
const middleLength = len - oValueS - oValueE;
const dashLength = 2; // Length of each dash
const gapLength = 2; // Length of each gap
const dashGapPairLength = dashLength + gapLength;
// Calculate number of complete dash-gap pairs that can fit
const numberOfPairs = Math.floor(middleLength / dashGapPairLength);
// Generate the middle pattern array
const middlePattern = Array(numberOfPairs).fill(`${dashLength} ${gapLength}`).join(' ');
// Combine all parts
const dashArray = `0 ${oValueS} ${middlePattern} ${oValueE}`;
return dashArray;
};
export const insertEdge = function (
elem,
edge,
clusterDb,
diagramType,
startNode,
endNode,
id,
skipIntersect = false
) {
const { handDrawnSeed } = getConfig();
let points = edge.points;
let pointsHasChanged = false;
@@ -452,11 +484,12 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
edgeClassStyles.push(edge.cssCompiledStyles[key]);
}
if (head.intersect && tail.intersect) {
log.debug('UIO intersect check', edge.points, head.x, tail.x);
if (head.intersect && tail.intersect && !skipIntersect) {
points = points.slice(1, edge.points.length - 1);
points.unshift(tail.intersect(points[0]));
log.debug(
'Last point APA12',
'Last point UIO',
edge.start,
'-->',
edge.end,
@@ -466,6 +499,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
);
points.push(head.intersect(points[points.length - 1]));
}
const pointsStr = btoa(JSON.stringify(points));
if (edge.toCluster) {
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
@@ -529,6 +563,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
curve = curveBasis;
}
// if (edge.curve) {
// curve = edge.curve;
// }
const { x, y } = getLineFunctionsWithOffset(edge);
const lineFunction = line().x(x).y(y).curve(curve);
@@ -560,10 +598,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
strokeClasses += ' edge-pattern-solid';
}
let svgPath;
let linePath = lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : [];
let linePath =
edge.curve === 'rounded'
? generateRoundedPath(applyMarkerOffsetsToPoints(lineData, edge), 5)
: lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
let animatedEdge = false;
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
Object.assign([], lineData);
@@ -594,7 +636,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles;
const pathStyle =
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
';' +
(edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
svgPath = elem
.append('path')
.attr('d', linePath)
@@ -604,11 +649,39 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
)
.attr('style', pathStyle);
//eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
// Possible fix to remove eslint-disable-next-line
//strokeColor = /stroke:([^;]+)/.exec(pathStyle)?.[1];
animatedEdge =
edge.animate === true || !!edge.animation || stylesFromClasses.includes('animation');
const pathNode = svgPath.node();
const len = typeof pathNode.getTotalLength === 'function' ? pathNode.getTotalLength() : 0;
const oValueS = markerOffsets2[edge.arrowTypeStart] || 0;
const oValueE = markerOffsets2[edge.arrowTypeEnd] || 0;
if (edge.look === 'neo' && !animatedEdge) {
const dashArray =
edge.pattern === 'dotted' || edge.pattern === 'dashed'
? generateDashArray(len, oValueS, oValueE)
: `0 ${oValueS} ${len - oValueS - oValueE} ${oValueE}`;
// No offset needed because we already start with a zero-length dash that effectively sets us up for a gap at the start.
const mOffset = `stroke-dasharray: ${dashArray}; stroke-dashoffset: 0;`;
svgPath.attr('style', mOffset + svgPath.attr('style'));
}
}
// DEBUG code, DO NOT REMOVE
// adds a red circle at each edge coordinate
// MC Special
svgPath.attr('data-edge', true);
svgPath.attr('data-et', 'edge');
svgPath.attr('data-id', edge.id);
svgPath.attr('data-points', pointsStr);
// DEBUG code, adds a red circle at each edge coordinate
// cornerPoints.forEach((point) => {
// elem
// .append('circle')
@@ -618,19 +691,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
// lineData.forEach((point) => {
// elem
// .append('circle')
// .style('stroke', 'red')
// .style('fill', 'red')
// .attr('r', 1)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
if (edge.showPoints) {
lineData.forEach((point) => {
elem
.append('circle')
.style('stroke', 'red')
.style('fill', 'red')
.attr('r', 1)
.attr('cx', point.x)
.attr('cy', point.y);
});
}
let url = '';
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
url = getUrl(true);
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
log.info('arrowTypeStart', edge.arrowTypeStart);
log.info('arrowTypeEnd', edge.arrowTypeEnd);
@@ -649,3 +730,134 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
paths.originalPath = edge.points;
return paths;
};
/**
* Generates SVG path data with rounded corners from an array of points.
* @param {Array} points - Array of points in the format [{x: Number, y: Number}, ...]
* @param {Number} radius - The radius of the rounded corners
* @returns {String} - SVG path data string
*/
function generateRoundedPath(points, radius) {
if (points.length < 2) {
return '';
}
let path = '';
const size = points.length;
const epsilon = 1e-5;
for (let i = 0; i < size; i++) {
const currPoint = points[i];
const prevPoint = points[i - 1];
const nextPoint = points[i + 1];
if (i === 0) {
// Move to the first point
path += `M${currPoint.x},${currPoint.y}`;
} else if (i === size - 1) {
// Last point, draw a straight line to the final point
path += `L${currPoint.x},${currPoint.y}`;
} else {
// Calculate vectors for incoming and outgoing segments
const dx1 = currPoint.x - prevPoint.x;
const dy1 = currPoint.y - prevPoint.y;
const dx2 = nextPoint.x - currPoint.x;
const dy2 = nextPoint.y - currPoint.y;
const len1 = Math.hypot(dx1, dy1);
const len2 = Math.hypot(dx2, dy2);
// Prevent division by zero
if (len1 < epsilon || len2 < epsilon) {
path += `L${currPoint.x},${currPoint.y}`;
continue;
}
// Normalize the vectors
const nx1 = dx1 / len1;
const ny1 = dy1 / len1;
const nx2 = dx2 / len2;
const ny2 = dy2 / len2;
// Calculate the angle between the vectors
const dot = nx1 * nx2 + ny1 * ny2;
// Clamp the dot product to avoid numerical issues with acos
const clampedDot = Math.max(-1, Math.min(1, dot));
const angle = Math.acos(clampedDot);
// Skip rounding if the angle is too small or too close to 180 degrees
if (angle < epsilon || Math.abs(Math.PI - angle) < epsilon) {
path += `L${currPoint.x},${currPoint.y}`;
continue;
}
// Calculate the distance to offset the control point
const cutLen = Math.min(radius / Math.sin(angle / 2), len1 / 2, len2 / 2);
// Calculate the start and end points of the curve
const startX = currPoint.x - nx1 * cutLen;
const startY = currPoint.y - ny1 * cutLen;
const endX = currPoint.x + nx2 * cutLen;
const endY = currPoint.y + ny2 * cutLen;
// Draw the line to the start of the curve
path += `L${startX},${startY}`;
// Draw the quadratic Bezier curve
path += `Q${currPoint.x},${currPoint.y} ${endX},${endY}`;
}
}
return path;
}
// Helper function to calculate delta and angle between two points
function calculateDeltaAndAngle(point1, point2) {
if (!point1 || !point2) {
return { angle: 0, deltaX: 0, deltaY: 0 };
}
const deltaX = point2.x - point1.x;
const deltaY = point2.y - point1.y;
const angle = Math.atan2(deltaY, deltaX);
return { angle, deltaX, deltaY };
}
// Function to adjust the first and last points of the points array
function applyMarkerOffsetsToPoints(points, edge) {
// Copy the points array to avoid mutating the original data
const newPoints = points.map((point) => ({ ...point }));
// Handle the first point (start of the edge)
if (points.length >= 2 && markerOffsets[edge.arrowTypeStart]) {
const offsetValue = markerOffsets[edge.arrowTypeStart];
const point1 = points[0];
const point2 = points[1];
const { angle } = calculateDeltaAndAngle(point1, point2);
const offsetX = offsetValue * Math.cos(angle);
const offsetY = offsetValue * Math.sin(angle);
newPoints[0].x = point1.x + offsetX;
newPoints[0].y = point1.y + offsetY;
}
// Handle the last point (end of the edge)
const n = points.length;
if (n >= 2 && markerOffsets[edge.arrowTypeEnd]) {
const offsetValue = markerOffsets[edge.arrowTypeEnd];
const point1 = points[n - 1];
const point2 = points[n - 2];
const { angle } = calculateDeltaAndAngle(point2, point1);
const offsetX = offsetValue * Math.cos(angle);
const offsetY = offsetValue * Math.sin(angle);
newPoints[n - 1].x = point1.x - offsetX;
newPoints[n - 1].y = point1.y - offsetY;
}
return newPoints;
}

View File

@@ -20,7 +20,11 @@ export const compileStyles = (node: Node) => {
// the array is the styles of node from the classes it is using
// node.cssStyles is an array of styles directly set on the node
// concat the arrays and remove duplicates such that the values from node.cssStyles are used if there are duplicates
const stylesMap = styles2Map([...(node.cssCompiledStyles || []), ...(node.cssStyles || [])]);
const stylesMap = styles2Map([
...(node.cssCompiledStyles || []),
...(node.cssStyles || []),
...(node.labelStyle || []),
]);
return { stylesMap, stylesArray: [...stylesMap] };
};

View File

@@ -4,12 +4,22 @@ import type { EdgeData, Point } from '../types.js';
// under any transparent markers.
// The offsets are calculated from the markers' dimensions.
export const markerOffsets = {
aggregation: 18,
extension: 18,
composition: 18,
aggregation: 17.25,
extension: 17.25,
composition: 17.25,
dependency: 6,
lollipop: 13.5,
arrow_point: 4,
//arrow_cross: 24,
} as const;
// We need to draw the lines a bit shorter to avoid drawing
// under any transparent markers.
// The offsets are calculated from the markers' dimensions.
export const markerOffsets2 = {
arrow_point: 9,
arrow_cross: 12.5,
arrow_circle: 12.5,
} as const;
/**
@@ -104,6 +114,7 @@ export const getLineFunctionsWithOffset = (
adjustment *= DIRECTION === 'right' ? -1 : 1;
offset += adjustment;
}
return pointTransformer(d).x + offset;
},
y: function (

View File

@@ -1,19 +1,5 @@
# mermaid
## 11.11.0
### Minor Changes
- [#6704](https://github.com/mermaid-js/mermaid/pull/6704) [`012530e`](https://github.com/mermaid-js/mermaid/commit/012530e98e9b8b80962ab270b6bb3b6d9f6ada05) Thanks [@omkarht](https://github.com/omkarht)! - feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
- [#6802](https://github.com/mermaid-js/mermaid/pull/6802) [`c8e5027`](https://github.com/mermaid-js/mermaid/commit/c8e50276e877c4de7593a09ec458c99353e65af8) Thanks [@darshanr0107](https://github.com/darshanr0107)! - feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
### Patch Changes
- [#6905](https://github.com/mermaid-js/mermaid/pull/6905) [`33bc4a0`](https://github.com/mermaid-js/mermaid/commit/33bc4a0b4e2ca6d937bb0a8c4e2081b1362b2800) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Render newlines as spaces in class diagrams
- [#6886](https://github.com/mermaid-js/mermaid/pull/6886) [`e0b45c2`](https://github.com/mermaid-js/mermaid/commit/e0b45c2d2b41c2a9038bf87646fa3ccd7560eb20) Thanks [@darshanr0107](https://github.com/darshanr0107)! - fix: Handle arrows correctly when auto number is enabled
## 11.10.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/tiny",
"version": "11.11.0",
"version": "11.10.0",
"description": "Tiny version of mermaid",
"type": "commonjs",
"main": "./dist/mermaid.tiny.js",

2487
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ pushd packages/mermaid
# Append commit hash to version
jq ".version = .version + \"+${COMMIT_REF:0:7}\"" package.json > package.tmp.json
mv package.tmp.json package.json
yarn link
popd
pnpm run -r clean
@@ -26,13 +25,14 @@ cd mermaid-live-editor
git clean -xdf
rm -rf docs/
# We have to use npm instead of yarn because it causes trouble in netlify
# Tells PNPM that mermaid-live-editor is not part of this workspace
touch pnpm-workspace.yaml
# Install dependencies
yarn install
pnpm install --frozen-lockfile
# Link local mermaid to live editor
yarn link mermaid
pnpm link ../packages/mermaid
# Force Build the site
yarn run build
pnpm run build