mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-19 20:24:16 +01:00
Merge branch 'develop' into feat/usecase-diagram-implementation
This commit is contained in:
@@ -33,6 +33,11 @@ export const packageOptions = {
|
|||||||
packageName: 'mermaid-layout-elk',
|
packageName: 'mermaid-layout-elk',
|
||||||
file: 'layouts.ts',
|
file: 'layouts.ts',
|
||||||
},
|
},
|
||||||
|
'mermaid-layout-tidy-tree': {
|
||||||
|
name: 'mermaid-layout-tidy-tree',
|
||||||
|
packageName: 'mermaid-layout-tidy-tree',
|
||||||
|
file: 'index.ts',
|
||||||
|
},
|
||||||
examples: {
|
examples: {
|
||||||
name: 'mermaid-examples',
|
name: 'mermaid-examples',
|
||||||
packageName: 'examples',
|
packageName: 'examples',
|
||||||
|
|||||||
5
.changeset/deep-times-make.md
Normal file
5
.changeset/deep-times-make.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add IDs in architecture diagrams
|
||||||
5
.changeset/four-eyes-wish.md
Normal file
5
.changeset/four-eyes-wish.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: Ensure edge label color is applied when using classDef with edge IDs
|
||||||
7
.changeset/hungry-guests-drive.md
Normal file
7
.changeset/hungry-guests-drive.md
Normal 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
|
||||||
5
.changeset/proud-colts-smell.md
Normal file
5
.changeset/proud-colts-smell.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Add IDs in architecture diagrams
|
||||||
9
.changeset/revert-marked-dependency.md
Normal file
9
.changeset/revert-marked-dependency.md
Normal 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
|
||||||
@@ -5,8 +5,10 @@ bmatrix
|
|||||||
braintree
|
braintree
|
||||||
catmull
|
catmull
|
||||||
compositTitleSize
|
compositTitleSize
|
||||||
|
cose
|
||||||
curv
|
curv
|
||||||
doublecircle
|
doublecircle
|
||||||
|
elem
|
||||||
elems
|
elems
|
||||||
gantt
|
gantt
|
||||||
gitgraph
|
gitgraph
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
BRANDES
|
BRANDES
|
||||||
|
Buzan
|
||||||
circo
|
circo
|
||||||
handDrawn
|
handDrawn
|
||||||
KOEPF
|
KOEPF
|
||||||
|
|||||||
2
.github/workflows/validate-lockfile.yml
vendored
2
.github/workflows/validate-lockfile.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
# 2) No unwanted vitepress paths
|
# 2) No unwanted vitepress paths
|
||||||
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
||||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
|
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3) Lockfile only changes when package.json changes
|
# 3) Lockfile only changes when package.json changes
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
|||||||
coverage/
|
coverage/
|
||||||
.idea/
|
.idea/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
.instructions/
|
||||||
|
|
||||||
dist
|
dist
|
||||||
v8-compile-cache-0
|
v8-compile-cache-0
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ describe('Configuration', () => {
|
|||||||
it('should handle arrowMarkerAbsolute set to true', () => {
|
it('should handle arrowMarkerAbsolute set to true', () => {
|
||||||
renderGraph(
|
renderGraph(
|
||||||
`flowchart TD
|
`flowchart TD
|
||||||
A[Christmas] -->|Get money| B(Go shopping)
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
C -->|Two| E[iPhone]
|
||||||
C -->|Three| F[fa:fa-car Car]
|
C -->|Three| F[fa:fa-car Car]
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
arrowMarkerAbsolute: true,
|
arrowMarkerAbsolute: true,
|
||||||
}
|
}
|
||||||
@@ -113,8 +113,7 @@ describe('Configuration', () => {
|
|||||||
cy.get('path')
|
cy.get('path')
|
||||||
.first()
|
.first()
|
||||||
.should('have.attr', 'marker-end')
|
.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', () => {
|
it('should not taint the initial configuration when using multiple directives', () => {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ describe('Flowchart ELK', () => {
|
|||||||
const style = svg.attr('style');
|
const style = svg.attr('style');
|
||||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
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', () => {
|
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
||||||
@@ -128,7 +128,7 @@ describe('Flowchart ELK', () => {
|
|||||||
const width = parseFloat(svg.attr('width'));
|
const width = parseFloat(svg.attr('width'));
|
||||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
// 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);
|
// 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');
|
expect(svg).to.not.have.attr('style');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1186,4 +1186,17 @@ end
|
|||||||
imgSnapshotTest(graph, { htmlLabels: false });
|
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
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
79
cypress/integration/rendering/mindmap-tidy-tree.spec.js
Normal file
79
cypress/integration/rendering/mindmap-tidy-tree.spec.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||||
|
|
||||||
|
describe('Mindmap Tidy Tree', () => {
|
||||||
|
it('1-tidy-tree: should render a simple mindmap without children', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
` ---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('2-tidy-tree: should render a simple mindmap', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
` ---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('3-tidy-tree: should render a mindmap with different shapes', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
` ---
|
||||||
|
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
|
||||||
|
id)I am a cloud(
|
||||||
|
id))I am a bang((
|
||||||
|
Tools
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('4-tidy-tree: should render a mindmap with children', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
` ---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
((This is a mindmap))
|
||||||
|
child1
|
||||||
|
grandchild 1
|
||||||
|
grandchild 2
|
||||||
|
child2
|
||||||
|
grandchild 3
|
||||||
|
grandchild 4
|
||||||
|
child3
|
||||||
|
grandchild 5
|
||||||
|
grandchild 6
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -159,12 +159,10 @@ root
|
|||||||
});
|
});
|
||||||
it('square shape', () => {
|
it('square shape', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
|
||||||
root[
|
root[
|
||||||
The root
|
The root
|
||||||
]
|
]`,
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -172,12 +170,10 @@ mindmap
|
|||||||
});
|
});
|
||||||
it('rounded rect shape', () => {
|
it('rounded rect shape', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
|
||||||
root((
|
root((
|
||||||
The root
|
The root
|
||||||
))
|
))`,
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -185,12 +181,10 @@ mindmap
|
|||||||
});
|
});
|
||||||
it('circle shape', () => {
|
it('circle shape', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
|
||||||
root(
|
root(
|
||||||
The root
|
The root
|
||||||
)
|
)`,
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -198,10 +192,8 @@ mindmap
|
|||||||
});
|
});
|
||||||
it('default shape', () => {
|
it('default shape', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
The root`,
|
||||||
The root
|
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -209,12 +201,10 @@ mindmap
|
|||||||
});
|
});
|
||||||
it('adding children', () => {
|
it('adding children', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
|
||||||
The root
|
The root
|
||||||
child1
|
child1
|
||||||
child2
|
child2`,
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -222,13 +212,11 @@ mindmap
|
|||||||
});
|
});
|
||||||
it('adding grand children', () => {
|
it('adding grand children', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`mindmap
|
||||||
mindmap
|
|
||||||
The root
|
The root
|
||||||
child1
|
child1
|
||||||
child2
|
child2
|
||||||
child3
|
child3`,
|
||||||
`,
|
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
shouldHaveRoot
|
shouldHaveRoot
|
||||||
@@ -240,25 +228,21 @@ mindmap
|
|||||||
`mindmap
|
`mindmap
|
||||||
id1[\`**Start** with
|
id1[\`**Start** with
|
||||||
a second line 😎\`]
|
a second line 😎\`]
|
||||||
id2[\`The dog in **the** hog... a *very long text* about it
|
id2[\`The dog in **the** hog... a *very long text* about it Word!\`]`
|
||||||
Word!\`]
|
|
||||||
`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||||
it('has a label with char sequence "graph"', () => {
|
it('has a label with char sequence "graph"', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
` mindmap
|
||||||
mindmap
|
|
||||||
root
|
root
|
||||||
Photograph
|
Photograph
|
||||||
Waterfall
|
Waterfall
|
||||||
Landscape
|
Landscape
|
||||||
Geography
|
Geography
|
||||||
Mountains
|
Mountains
|
||||||
Rocks
|
Rocks`,
|
||||||
`,
|
|
||||||
{ flowchart: { defaultRenderer: 'elk' } }
|
{ flowchart: { defaultRenderer: 'elk' } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,26 +32,8 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||||
rel="stylesheet"
|
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>
|
<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 {
|
body {
|
||||||
/* background: rgb(221, 208, 208); */
|
/* background: rgb(221, 208, 208); */
|
||||||
/* background: #333; */
|
/* background: #333; */
|
||||||
@@ -63,9 +45,7 @@
|
|||||||
h1 {
|
h1 {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
.mermaid {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
.mermaid2 {
|
.mermaid2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -103,11 +83,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.class2 {
|
|
||||||
fill: red;
|
|
||||||
fill-opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tspan {
|
/* tspan {
|
||||||
font-size: 6px !important;
|
font-size: 6px !important;
|
||||||
} */
|
} */
|
||||||
@@ -131,6 +106,194 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
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: elk
|
||||||
|
---
|
||||||
|
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: elk
|
||||||
|
---
|
||||||
|
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 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
|
||||||
|
<<interface>> 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 {
|
||||||
|
<<service>>
|
||||||
|
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:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -157,84 +320,149 @@ treemap
|
|||||||
"Leaf 2.2": 25
|
"Leaf 2.2": 25
|
||||||
"Leaf 2.3": 12
|
"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="mermaid">
|
||||||
|
---
|
||||||
|
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>
|
||||||
>
|
<pre id="diagram4" class="mermaid">
|
||||||
<pre id="diagram4" class="mermaid2">
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
flowchart:
|
||||||
|
curve: linear
|
||||||
|
---
|
||||||
|
flowchart LR
|
||||||
|
A[A] --> B[B]
|
||||||
|
A[A] --- B([C])
|
||||||
|
A@{ shape: diamond}
|
||||||
|
%%B@{ shape: diamond}
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
flowchart:
|
||||||
|
curve: linear
|
||||||
|
---
|
||||||
|
flowchart LR
|
||||||
|
A[A] -- Mermaid js --> B[B]
|
||||||
|
A[A] -- Mermaid js --- B[B]
|
||||||
|
A@{ shape: diamond}
|
||||||
|
B@{ shape: diamond}
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
flowchart:
|
||||||
|
curve: rounded
|
||||||
|
---
|
||||||
|
flowchart LR
|
||||||
|
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="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:
|
config:
|
||||||
treemap:
|
layout: elk
|
||||||
valueFormat: '$0,0'
|
|
||||||
---
|
---
|
||||||
treemap
|
flowchart LR
|
||||||
"Budget"
|
%% subgraph s1["Untitled subgraph"]
|
||||||
"Operations"
|
C["Evaluate"]
|
||||||
"Salaries": 7000
|
%% end
|
||||||
"Equipment": 2000
|
|
||||||
"Supplies": 1000
|
|
||||||
"Marketing"
|
|
||||||
"Advertising": 4000
|
|
||||||
"Events": 1000
|
|
||||||
|
|
||||||
</pre
|
B --> C
|
||||||
>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
treemap
|
---
|
||||||
title Accessible Treemap Title
|
config:
|
||||||
"Category A"
|
layout: elk
|
||||||
"Item A1": 10
|
flowchart:
|
||||||
"Item A2": 20
|
//curve: linear
|
||||||
"Category B"
|
---
|
||||||
"Item B1": 15
|
|
||||||
"Item B2": 25
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
|
||||||
flowchart LR
|
|
||||||
AB["apa@apa@"] --> B(("`apa@apa`"))
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
|
||||||
flowchart
|
|
||||||
D(("for D"))
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
|
||||||
flowchart LR
|
|
||||||
A e1@==> B
|
|
||||||
e1@{ animate: true}
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A e1@--> B
|
%% A ==> B
|
||||||
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
|
%% A2 --> B2
|
||||||
class e1 animate
|
A{A} --> B((Bo boo)) & B & B & B
|
||||||
</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>
|
|
||||||
|
|
||||||
<pre id="diagram4" class="mermaid2">
|
</pre>
|
||||||
|
<pre id="diagram4" class="mermaid">
|
||||||
info </pre
|
---
|
||||||
>
|
config:
|
||||||
<pre id="diagram4" class="mermaid2">
|
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:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -259,7 +487,7 @@ config:
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -272,7 +500,7 @@ config:
|
|||||||
D-->I
|
D-->I
|
||||||
D-->I
|
D-->I
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -311,7 +539,7 @@ flowchart LR
|
|||||||
n8@{ shape: rect}
|
n8@{ shape: rect}
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -327,7 +555,7 @@ flowchart LR
|
|||||||
|
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -336,7 +564,7 @@ flowchart LR
|
|||||||
A{A} --> B & C
|
A{A} --> B & C
|
||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -348,7 +576,7 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: elk
|
||||||
@@ -366,7 +594,7 @@ flowchart LR
|
|||||||
|
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
kanban:
|
kanban:
|
||||||
@@ -385,81 +613,81 @@ kanban
|
|||||||
task3[💻 Develop login feature]@{ ticket: 103 }
|
task3[💻 Develop login feature]@{ ticket: 103 }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
|
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||||
A:::AClass
|
A:::AClass
|
||||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
flowchart LR
|
flowchart LR
|
||||||
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
kanban
|
kanban
|
||||||
id2[In progress]
|
id2[In progress]
|
||||||
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
||||||
</pre>
|
</pre>
|
||||||
<pre id="diagram4" class="mermaid2">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
kanban:
|
kanban:
|
||||||
@@ -523,18 +751,22 @@ kanban
|
|||||||
alert('It worked');
|
alert('It worked');
|
||||||
}
|
}
|
||||||
await mermaid.initialize({
|
await mermaid.initialize({
|
||||||
// theme: 'forest',
|
// theme: 'base',
|
||||||
// theme: 'default',
|
// theme: 'default',
|
||||||
// theme: 'forest',
|
// theme: 'forest',
|
||||||
// handDrawnSeed: 12,
|
// handDrawnSeed: 12,
|
||||||
// look: 'handDrawn',
|
// look: 'handDrawn',
|
||||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||||
// layout: 'dagre',
|
// layout: 'dagre',
|
||||||
// layout: 'elk',
|
layout: 'elk',
|
||||||
// layout: 'fixed',
|
// layout: 'fixed',
|
||||||
// htmlLabels: false,
|
// htmlLabels: false,
|
||||||
flowchart: { titleTopMargin: 10 },
|
flowchart: { titleTopMargin: 10 },
|
||||||
fontFamily: "'Recursive', sans-serif",
|
|
||||||
|
// fontFamily: 'Caveat',
|
||||||
|
// fontFamily: 'Kalam',
|
||||||
|
// fontFamily: 'courier',
|
||||||
|
fontFamily: 'arial',
|
||||||
sequence: {
|
sequence: {
|
||||||
actorFontFamily: 'courier',
|
actorFontFamily: 'courier',
|
||||||
noteFontFamily: 'courier',
|
noteFontFamily: 'courier',
|
||||||
|
|||||||
376
cypress/platform/mindmap-layouts.html
Normal file
376
cypress/platform/mindmap-layouts.html
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>Mermaid Quick Test Page</title>
|
||||||
|
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
|
||||||
|
<style>
|
||||||
|
div.mermaid {
|
||||||
|
font-family: 'Courier New', Courier, monospace !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: dagre
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: cose-bilkent
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: dagre
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: cose-bilkent
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
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
|
||||||
|
id)I am a cloud(
|
||||||
|
id))I am a bang((
|
||||||
|
Tools
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: dagre
|
||||||
|
---
|
||||||
|
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
|
||||||
|
id)I am a cloud(
|
||||||
|
id))I am a bang((
|
||||||
|
Tools
|
||||||
|
</pre>
|
||||||
|
<pre 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<br/>and features
|
||||||
|
On Automatic creation
|
||||||
|
Uses
|
||||||
|
Creative techniques
|
||||||
|
Strategic planning
|
||||||
|
Argument mapping
|
||||||
|
Tools
|
||||||
|
id)I am a cloud(
|
||||||
|
id))I am a bang((
|
||||||
|
Tools
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: cose-bilkent
|
||||||
|
---
|
||||||
|
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
|
||||||
|
id)I am a cloud(
|
||||||
|
id))I am a bang((
|
||||||
|
Tools
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
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]
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: dagre
|
||||||
|
---
|
||||||
|
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]
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
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]
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: cose-bilkent
|
||||||
|
---
|
||||||
|
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]
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
((This is a mindmap))
|
||||||
|
child1
|
||||||
|
grandchild 1
|
||||||
|
grandchild 2
|
||||||
|
child2
|
||||||
|
grandchild 3
|
||||||
|
grandchild 4
|
||||||
|
child3
|
||||||
|
grandchild 5
|
||||||
|
grandchild 6
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: dagre
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
((This is a mindmap))
|
||||||
|
child1
|
||||||
|
grandchild 1
|
||||||
|
grandchild 2
|
||||||
|
child2
|
||||||
|
grandchild 3
|
||||||
|
grandchild 4
|
||||||
|
child3
|
||||||
|
grandchild 5
|
||||||
|
grandchild 6
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
((This is a mindmap))
|
||||||
|
child1
|
||||||
|
grandchild 1
|
||||||
|
grandchild 2
|
||||||
|
child2
|
||||||
|
grandchild 3
|
||||||
|
grandchild 4
|
||||||
|
child3
|
||||||
|
grandchild 5
|
||||||
|
grandchild 6
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: cose-bilkent
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
((This is a mindmap))
|
||||||
|
child1
|
||||||
|
grandchild 1
|
||||||
|
grandchild 2
|
||||||
|
child2
|
||||||
|
grandchild 3
|
||||||
|
grandchild 4
|
||||||
|
child3
|
||||||
|
grandchild 5
|
||||||
|
grandchild 6
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from '/mermaid.esm.mjs';
|
||||||
|
import tidytree from '/mermaid-layout-tidy-tree.esm.mjs';
|
||||||
|
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||||
|
mermaid.registerLayoutLoaders(layouts);
|
||||||
|
mermaid.registerLayoutLoaders(tidytree);
|
||||||
|
mermaid.initialize({
|
||||||
|
theme: 'default',
|
||||||
|
logLevel: 3,
|
||||||
|
securityLevel: 'loose',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import externalExample from './mermaid-example-diagram.esm.mjs';
|
import externalExample from './mermaid-example-diagram.esm.mjs';
|
||||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||||
|
import tidyTree from './mermaid-layout-tidy-tree.esm.mjs';
|
||||||
import zenUml from './mermaid-zenuml.esm.mjs';
|
import zenUml from './mermaid-zenuml.esm.mjs';
|
||||||
import mermaid from './mermaid.esm.mjs';
|
import mermaid from './mermaid.esm.mjs';
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ const contentLoaded = async function () {
|
|||||||
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
|
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
|
||||||
|
|
||||||
mermaid.registerLayoutLoaders(layouts);
|
mermaid.registerLayoutLoaders(layouts);
|
||||||
|
mermaid.registerLayoutLoaders(tidyTree);
|
||||||
mermaid.initialize(graphObj.mermaid);
|
mermaid.initialize(graphObj.mermaid);
|
||||||
/**
|
/**
|
||||||
* CC-BY-4.0
|
* CC-BY-4.0
|
||||||
|
|||||||
@@ -2,223 +2,227 @@
|
|||||||
"durations": [
|
"durations": [
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/configuration.spec.js",
|
"spec": "cypress/integration/other/configuration.spec.js",
|
||||||
"duration": 6162
|
"duration": 5841
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||||
"duration": 2148
|
"duration": 2138
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||||
"duration": 3585
|
"duration": 3370
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/iife.spec.js",
|
"spec": "cypress/integration/other/iife.spec.js",
|
||||||
"duration": 2099
|
"duration": 2052
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/interaction.spec.js",
|
"spec": "cypress/integration/other/interaction.spec.js",
|
||||||
"duration": 12119
|
"duration": 12243
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/rerender.spec.js",
|
"spec": "cypress/integration/other/rerender.spec.js",
|
||||||
"duration": 2063
|
"duration": 2065
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/other/xss.spec.js",
|
"spec": "cypress/integration/other/xss.spec.js",
|
||||||
"duration": 31921
|
"duration": 31288
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||||
"duration": 3385
|
"duration": 3421
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||||
"duration": 108
|
"duration": 97
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/block.spec.js",
|
"spec": "cypress/integration/rendering/block.spec.js",
|
||||||
"duration": 18063
|
"duration": 18500
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||||
"duration": 5519
|
"duration": 5793
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||||
"duration": 40040
|
"duration": 40966
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||||
"duration": 38665
|
"duration": 39176
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||||
"duration": 22836
|
"duration": 23468
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||||
"duration": 37096
|
"duration": 38291
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||||
"duration": 16452
|
"duration": 16949
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||||
"duration": 10387
|
"duration": 9480
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/current.spec.js",
|
"spec": "cypress/integration/rendering/current.spec.js",
|
||||||
"duration": 2803
|
"duration": 2753
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||||
"duration": 86891
|
"duration": 88028
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||||
"duration": 15206
|
"duration": 15615
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||||
"duration": 3540
|
"duration": 3706
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||||
"duration": 41975
|
"duration": 43905
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||||
"duration": 30909
|
"duration": 31217
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
||||||
"duration": 7881
|
"duration": 7531
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||||
"duration": 24294
|
"duration": 25423
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||||
"duration": 47652
|
"duration": 49664
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||||
"duration": 32049
|
"duration": 32525
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||||
"duration": 20248
|
"duration": 20915
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||||
"duration": 51202
|
"duration": 53556
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||||
"duration": 283546
|
"duration": 283038
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||||
"duration": 57257
|
"duration": 59434
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||||
"duration": 3352
|
"duration": 3101
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||||
"duration": 7423
|
"duration": 7099
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||||
"duration": 7804
|
"duration": 7567
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||||
"duration": 3847
|
"duration": 3817
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||||
"duration": 2637
|
"duration": 2624
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
|
||||||
|
"duration": 4246
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||||
"duration": 11658
|
"duration": 11967
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||||
"duration": 149500
|
"duration": 151914
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||||
"duration": 115427
|
"duration": 116698
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||||
"duration": 4801
|
"duration": 4967
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||||
"duration": 6786
|
"duration": 6700
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||||
"duration": 9422
|
"duration": 8963
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||||
"duration": 5652
|
"duration": 5540
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||||
"duration": 2787
|
"duration": 2782
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||||
"duration": 53631
|
"duration": 54797
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||||
"duration": 7075
|
"duration": 6914
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
||||||
"duration": 20446
|
"duration": 20481
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||||
"duration": 37326
|
"duration": 38490
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||||
"duration": 29208
|
"duration": 30766
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||||
"duration": 16328
|
"duration": 16705
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||||
"duration": 30541
|
"duration": 30928
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||||
"duration": 8611
|
"duration": 8424
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
||||||
"duration": 11878
|
"duration": 12533
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||||
"duration": 20400
|
"duration": 21197
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||||
"duration": 3528
|
"duration": 3455
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
40
docs/config/layouts.md
Normal file
40
docs/config/layouts.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||||
|
>
|
||||||
|
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/layouts.md](../../packages/mermaid/src/docs/config/layouts.md).
|
||||||
|
|
||||||
|
# Layouts
|
||||||
|
|
||||||
|
This page lists the available layout algorithms supported in Mermaid diagrams.
|
||||||
|
|
||||||
|
## Supported Layouts
|
||||||
|
|
||||||
|
- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/)
|
||||||
|
- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree)
|
||||||
|
- **cose-bilkent**: Cose Bilkent layout for force-directed graphs
|
||||||
|
- **dagre**: Dagre layout for layered graphs
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
You can specify the layout in your diagram's YAML config or initialization options. For example:
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
B-->C;
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
B-->C;
|
||||||
|
```
|
||||||
@@ -10,10 +10,6 @@
|
|||||||
|
|
||||||
# mermaid
|
# mermaid
|
||||||
|
|
||||||
## Classes
|
|
||||||
|
|
||||||
- [UnknownDiagramError](classes/UnknownDiagramError.md)
|
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
|
|
||||||
- [DetailedError](interfaces/DetailedError.md)
|
- [DetailedError](interfaces/DetailedError.md)
|
||||||
@@ -27,6 +23,7 @@
|
|||||||
- [RenderOptions](interfaces/RenderOptions.md)
|
- [RenderOptions](interfaces/RenderOptions.md)
|
||||||
- [RenderResult](interfaces/RenderResult.md)
|
- [RenderResult](interfaces/RenderResult.md)
|
||||||
- [RunOptions](interfaces/RunOptions.md)
|
- [RunOptions](interfaces/RunOptions.md)
|
||||||
|
- [UnknownDiagramError](interfaces/UnknownDiagramError.md)
|
||||||
|
|
||||||
## Type Aliases
|
## Type Aliases
|
||||||
|
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
> **Warning**
|
|
||||||
>
|
|
||||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
|
||||||
>
|
|
||||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md).
|
|
||||||
|
|
||||||
[**mermaid**](../../README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Class: UnknownDiagramError
|
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
|
||||||
|
|
||||||
## Extends
|
|
||||||
|
|
||||||
- `Error`
|
|
||||||
|
|
||||||
## Constructors
|
|
||||||
|
|
||||||
### new UnknownDiagramError()
|
|
||||||
|
|
||||||
> **new UnknownDiagramError**(`message`): [`UnknownDiagramError`](UnknownDiagramError.md)
|
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/errors.ts:2](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L2)
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
##### message
|
|
||||||
|
|
||||||
`string`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`UnknownDiagramError`](UnknownDiagramError.md)
|
|
||||||
|
|
||||||
#### Overrides
|
|
||||||
|
|
||||||
`Error.constructor`
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
### cause?
|
|
||||||
|
|
||||||
> `optional` **cause**: `unknown`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.cause`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### message
|
|
||||||
|
|
||||||
> **message**: `string`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.message`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### name
|
|
||||||
|
|
||||||
> **name**: `string`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.name`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### stack?
|
|
||||||
|
|
||||||
> `optional` **stack**: `string`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.stack`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### prepareStackTrace()?
|
|
||||||
|
|
||||||
> `static` `optional` **prepareStackTrace**: (`err`, `stackTraces`) => `any`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:143
|
|
||||||
|
|
||||||
Optional override for formatting stack traces
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
##### err
|
|
||||||
|
|
||||||
`Error`
|
|
||||||
|
|
||||||
##### stackTraces
|
|
||||||
|
|
||||||
`CallSite`\[]
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
#### See
|
|
||||||
|
|
||||||
<https://v8.dev/docs/stack-trace-api#customizing-stack-traces>
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.prepareStackTrace`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### stackTraceLimit
|
|
||||||
|
|
||||||
> `static` **stackTraceLimit**: `number`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:145
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.stackTraceLimit`
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
### captureStackTrace()
|
|
||||||
|
|
||||||
> `static` **captureStackTrace**(`targetObject`, `constructorOpt`?): `void`
|
|
||||||
|
|
||||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:136
|
|
||||||
|
|
||||||
Create .stack property on a target object
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
##### targetObject
|
|
||||||
|
|
||||||
`object`
|
|
||||||
|
|
||||||
##### constructorOpt?
|
|
||||||
|
|
||||||
`Function`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`void`
|
|
||||||
|
|
||||||
#### Inherited from
|
|
||||||
|
|
||||||
`Error.captureStackTrace`
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ExternalDiagramDefinition
|
# 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
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/me
|
|||||||
|
|
||||||
> **detector**: `DiagramDetector`
|
> **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`
|
> **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`
|
> **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)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: LayoutData
|
# Interface: LayoutData
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
|
Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168)
|
||||||
|
|
||||||
## Indexable
|
## Indexable
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.co
|
|||||||
|
|
||||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
|
Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.co
|
|||||||
|
|
||||||
> **edges**: `Edge`\[]
|
> **edges**: `Edge`\[]
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
|
Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.co
|
|||||||
|
|
||||||
> **nodes**: `Node`\[]
|
> **nodes**: `Node`\[]
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
|
Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: LayoutLoaderDefinition
|
# 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
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.co
|
|||||||
|
|
||||||
> `optional` **algorithm**: `string`
|
> `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`
|
> **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`
|
> **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)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ page.
|
|||||||
|
|
||||||
### detectType()
|
### detectType()
|
||||||
|
|
||||||
> **detectType**: (`text`, `config`?) => `string`
|
> **detectType**: (`text`, `config?`) => `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
|
Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ An array of objects with the id of the diagram.
|
|||||||
|
|
||||||
### ~~init()~~
|
### ~~init()~~
|
||||||
|
|
||||||
> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`>
|
> **init**: (`config?`, `nodes?`, `callback?`) => `Promise`<`void`>
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
|
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/
|
|||||||
|
|
||||||
[`MermaidConfig`](MermaidConfig.md)
|
[`MermaidConfig`](MermaidConfig.md)
|
||||||
|
|
||||||
**Deprecated**, please set configuration in [initialize](Mermaid.md#initialize).
|
**Deprecated**, please set configuration in [initialize](#initialize).
|
||||||
|
|
||||||
##### nodes?
|
##### nodes?
|
||||||
|
|
||||||
@@ -141,13 +141,13 @@ Called once for each rendered diagram's id.
|
|||||||
|
|
||||||
#### Deprecated
|
#### Deprecated
|
||||||
|
|
||||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
Use [initialize](#initialize) and [run](#run) instead.
|
||||||
|
|
||||||
Renders the mermaid diagrams
|
Renders the mermaid diagrams
|
||||||
|
|
||||||
#### Deprecated
|
#### Deprecated
|
||||||
|
|
||||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
Use [initialize](#initialize) and [run](#run) instead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ Configuration object for mermaid.
|
|||||||
|
|
||||||
### ~~mermaidAPI~~
|
### ~~mermaidAPI~~
|
||||||
|
|
||||||
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }; `render`: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
|
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
|
||||||
|
|
||||||
@@ -184,73 +184,81 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/
|
|||||||
|
|
||||||
#### Deprecated
|
#### Deprecated
|
||||||
|
|
||||||
Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
Use [parse](#parse) and [render](#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### parse()
|
### parse()
|
||||||
|
|
||||||
> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>
|
> **parse**: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
|
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
|
||||||
|
|
||||||
|
#### Call Signature
|
||||||
|
|
||||||
|
> (`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||||
|
|
||||||
Parse the text and validate the syntax.
|
Parse the text and validate the syntax.
|
||||||
|
|
||||||
#### Parameters
|
##### Parameters
|
||||||
|
|
||||||
##### text
|
###### text
|
||||||
|
|
||||||
`string`
|
`string`
|
||||||
|
|
||||||
The mermaid diagram definition.
|
The mermaid diagram definition.
|
||||||
|
|
||||||
##### parseOptions
|
###### parseOptions
|
||||||
|
|
||||||
[`ParseOptions`](ParseOptions.md) & `object`
|
[`ParseOptions`](ParseOptions.md) & `object`
|
||||||
|
|
||||||
Options for parsing.
|
Options for parsing.
|
||||||
|
|
||||||
#### Returns
|
##### Returns
|
||||||
|
|
||||||
`Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
`Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||||
|
|
||||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||||
|
|
||||||
#### See
|
##### See
|
||||||
|
|
||||||
[ParseOptions](ParseOptions.md)
|
[ParseOptions](ParseOptions.md)
|
||||||
|
|
||||||
#### Throws
|
##### Throws
|
||||||
|
|
||||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||||
|
|
||||||
|
#### Call Signature
|
||||||
|
|
||||||
|
> (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>
|
||||||
|
|
||||||
Parse the text and validate the syntax.
|
Parse the text and validate the syntax.
|
||||||
|
|
||||||
#### Parameters
|
##### Parameters
|
||||||
|
|
||||||
##### text
|
###### text
|
||||||
|
|
||||||
`string`
|
`string`
|
||||||
|
|
||||||
The mermaid diagram definition.
|
The mermaid diagram definition.
|
||||||
|
|
||||||
##### parseOptions?
|
###### parseOptions?
|
||||||
|
|
||||||
[`ParseOptions`](ParseOptions.md)
|
[`ParseOptions`](ParseOptions.md)
|
||||||
|
|
||||||
Options for parsing.
|
Options for parsing.
|
||||||
|
|
||||||
#### Returns
|
##### Returns
|
||||||
|
|
||||||
`Promise`<[`ParseResult`](ParseResult.md)>
|
`Promise`<[`ParseResult`](ParseResult.md)>
|
||||||
|
|
||||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||||
|
|
||||||
#### See
|
##### See
|
||||||
|
|
||||||
[ParseOptions](ParseOptions.md)
|
[ParseOptions](ParseOptions.md)
|
||||||
|
|
||||||
#### Throws
|
##### Throws
|
||||||
|
|
||||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||||
|
|
||||||
@@ -332,7 +340,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/
|
|||||||
|
|
||||||
### render()
|
### render()
|
||||||
|
|
||||||
> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>
|
> **render**: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
|
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ParseOptions
|
# Interface: ParseOptions
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mer
|
|||||||
|
|
||||||
> `optional` **suppressErrors**: `boolean`
|
> `optional` **suppressErrors**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||||
|
|
||||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||||
The `parseError` function will not be called.
|
The `parseError` function will not be called.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ParseResult
|
# Interface: ParseResult
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
|
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mer
|
|||||||
|
|
||||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
|
||||||
|
|
||||||
The config passed as YAML frontmatter or directives
|
The config passed as YAML frontmatter or directives
|
||||||
|
|
||||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
|||||||
|
|
||||||
> **diagramType**: `string`
|
> **diagramType**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||||
|
|
||||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: RenderOptions
|
# 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
|
## Properties
|
||||||
|
|
||||||
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:7](https://github.com
|
|||||||
|
|
||||||
> `optional` **algorithm**: `string`
|
> `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)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: RenderResult
|
# Interface: RenderResult
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
|
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/me
|
|||||||
|
|
||||||
> `optional` **bindFunctions**: (`element`) => `void`
|
> `optional` **bindFunctions**: (`element`) => `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
|
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
|
||||||
|
|
||||||
Bind function to be called after the svg has been inserted into the DOM.
|
Bind function to be called after the svg has been inserted into the DOM.
|
||||||
This is necessary for adding event listeners to the elements in the svg.
|
This is necessary for adding event listeners to the elements in the svg.
|
||||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
|||||||
|
|
||||||
> **diagramType**: `string`
|
> **diagramType**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
|
||||||
|
|
||||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||||
|
|
||||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
|||||||
|
|
||||||
> **svg**: `string`
|
> **svg**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||||
|
|
||||||
The svg code for the rendered graph.
|
The svg code for the rendered graph.
|
||||||
|
|||||||
65
docs/config/setup/mermaid/interfaces/UnknownDiagramError.md
Normal file
65
docs/config/setup/mermaid/interfaces/UnknownDiagramError.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||||
|
>
|
||||||
|
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md).
|
||||||
|
|
||||||
|
[**mermaid**](../../README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Interface: UnknownDiagramError
|
||||||
|
|
||||||
|
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
||||||
|
|
||||||
|
## Extends
|
||||||
|
|
||||||
|
- `Error`
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### cause?
|
||||||
|
|
||||||
|
> `optional` **cause**: `unknown`
|
||||||
|
|
||||||
|
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
`Error.cause`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### message
|
||||||
|
|
||||||
|
> **message**: `string`
|
||||||
|
|
||||||
|
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
`Error.message`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
> **name**: `string`
|
||||||
|
|
||||||
|
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
`Error.name`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### stack?
|
||||||
|
|
||||||
|
> `optional` **stack**: `string`
|
||||||
|
|
||||||
|
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
||||||
|
|
||||||
|
#### Inherited from
|
||||||
|
|
||||||
|
`Error.stack`
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
# Type Alias: InternalHelpers
|
# Type Alias: InternalHelpers
|
||||||
|
|
||||||
> **InternalHelpers**: _typeof_ `internalHelpers`
|
> **InternalHelpers** = _typeof_ `internalHelpers`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33)
|
Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Type Alias: ParseErrorFunction()
|
# Type Alias: ParseErrorFunction()
|
||||||
|
|
||||||
> **ParseErrorFunction**: (`err`, `hash`?) => `void`
|
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
|
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
# Type Alias: SVG
|
# Type Alias: SVG
|
||||||
|
|
||||||
> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
> **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)
|
||||||
|
|||||||
@@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
# Type Alias: SVGGroup
|
# Type Alias: SVGGroup
|
||||||
|
|
||||||
> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
> **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)
|
||||||
|
|||||||
89
docs/config/tidy-tree.md
Normal file
89
docs/config/tidy-tree.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||||
|
>
|
||||||
|
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/tidy-tree.md](../../packages/mermaid/src/docs/config/tidy-tree.md).
|
||||||
|
|
||||||
|
# Tidy-tree Layout
|
||||||
|
|
||||||
|
The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Organizes nodes in a tidy, non-overlapping tree
|
||||||
|
- Ideal for mindmaps and hierarchical data
|
||||||
|
- Automatically adjusts spacing for readability
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- Currently, tidy-tree is primarily supported for mindmap diagrams.
|
||||||
@@ -326,7 +326,9 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
|||||||
|
|
||||||
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||||
|
| Bang | Bang | `bang` | Bang | `bang` |
|
||||||
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
|
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
|
||||||
|
| Cloud | Cloud | `cloud` | cloud | `cloud` |
|
||||||
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
|
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
|
||||||
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
|
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
|
||||||
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
|
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
|
||||||
|
|||||||
@@ -314,3 +314,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
|
|||||||
cspell:locale en,en-gb
|
cspell:locale en,en-gb
|
||||||
cspell:ignore Buzan
|
cspell:ignore Buzan
|
||||||
--->
|
--->
|
||||||
|
|
||||||
|
## Layouts
|
||||||
|
|
||||||
|
Mermaid also supports a Tidy Tree layout for mindmaps.
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ Each user journey is split into sections, these describe the part of the task
|
|||||||
the user is trying to complete.
|
the user is trying to complete.
|
||||||
|
|
||||||
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
||||||
|
|
||||||
|
Score is a number between 1 and 5, inclusive.
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ xychart
|
|||||||
|
|
||||||
## Chart Theme Variables
|
## Chart Theme Variables
|
||||||
|
|
||||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
@@ -163,6 +163,52 @@ config:
|
|||||||
| yAxisLineColor | Color of the y-axis line |
|
| yAxisLineColor | Color of the y-axis line |
|
||||||
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
||||||
|
|
||||||
|
### Setting Colors for Lines and Bars
|
||||||
|
|
||||||
|
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
themeVariables:
|
||||||
|
xyChart:
|
||||||
|
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||||
|
---
|
||||||
|
xychart
|
||||||
|
title "Different Colors in xyChart"
|
||||||
|
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||||
|
y-axis "valuesY" 0 --> 50
|
||||||
|
%% Black line
|
||||||
|
line [10,20,30,40]
|
||||||
|
%% Blue bar
|
||||||
|
bar [20,30,25,35]
|
||||||
|
%% Green bar
|
||||||
|
bar [15,25,20,30]
|
||||||
|
%% Red line
|
||||||
|
line [5,15,25,35]
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
themeVariables:
|
||||||
|
xyChart:
|
||||||
|
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||||
|
---
|
||||||
|
xychart
|
||||||
|
title "Different Colors in xyChart"
|
||||||
|
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||||
|
y-axis "valuesY" 0 --> 50
|
||||||
|
%% Black line
|
||||||
|
line [10,20,30,40]
|
||||||
|
%% Blue bar
|
||||||
|
bar [20,30,25,35]
|
||||||
|
%% Green bar
|
||||||
|
bar [15,25,20,30]
|
||||||
|
%% Red line
|
||||||
|
line [5,15,25,35]
|
||||||
|
```
|
||||||
|
|
||||||
## Example on config and theme
|
## Example on config and theme
|
||||||
|
|
||||||
```mermaid-example
|
```mermaid-example
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default tseslint.config(
|
|||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
|
'**/*.d.ts',
|
||||||
'**/dist/',
|
'**/dist/',
|
||||||
'**/node_modules/',
|
'**/node_modules/',
|
||||||
'.git/',
|
'.git/',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
|
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The ELK Layout engine will not be available in all providers that support mermaid by default.
|
> The ELK Layout engine will not be available in all providers that support mermaid by default.
|
||||||
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
|
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
|
||||||
|
|
||||||
@@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts);
|
|||||||
- `elk.mrtree`: Multi-root tree layout
|
- `elk.mrtree`: Multi-root tree layout
|
||||||
- `elk.sporeOverlap`: Spore overlap layout
|
- `elk.sporeOverlap`: Spore overlap layout
|
||||||
|
|
||||||
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->
|
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively. -->
|
||||||
|
|||||||
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal file
67
packages/mermaid-layout-elk/src/__tests__/geometry.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
9
packages/mermaid-layout-elk/src/find-common-ancestor.d.ts
vendored
Normal file
9
packages/mermaid-layout-elk/src/find-common-ancestor.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface TreeData {
|
||||||
|
parentById: Record<string, string>;
|
||||||
|
childrenById: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
export declare const findCommonAncestor: (
|
||||||
|
id1: string,
|
||||||
|
id2: string,
|
||||||
|
{ parentById }: TreeData
|
||||||
|
) => string;
|
||||||
209
packages/mermaid-layout-elk/src/geometry.ts
Normal file
209
packages/mermaid-layout-elk/src/geometry.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,6 @@
|
|||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"types": ["vitest/importMeta", "vitest/globals"]
|
"types": ["vitest/importMeta", "vitest/globals"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts"],
|
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||||
"typeRoots": ["./src/types"]
|
"typeRoots": ["./src/types"]
|
||||||
}
|
}
|
||||||
|
|||||||
59
packages/mermaid-layout-tidy-tree/README.md
Normal file
59
packages/mermaid-layout-tidy-tree/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# @mermaid-js/layout-tidy-tree
|
||||||
|
|
||||||
|
This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default.
|
||||||
|
> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
```
|
||||||
|
|
||||||
|
### With bundlers
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install @mermaid-js/layout-tidy-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
|
||||||
|
|
||||||
|
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||||
|
```
|
||||||
|
|
||||||
|
### With CDN
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
|
||||||
|
|
||||||
|
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tidy Tree Layout Overview
|
||||||
|
|
||||||
|
tidy-tree: The bidirectional tidy tree layout
|
||||||
|
|
||||||
|
The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node:
|
||||||
|
Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||||
|
Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||||
|
|
||||||
|
This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams.
|
||||||
|
|
||||||
|
Layout Structure:
|
||||||
|
[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
[GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
||||||
46
packages/mermaid-layout-tidy-tree/package.json
Normal file
46
packages/mermaid-layout-tidy-tree/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@mermaid-js/layout-tidy-tree",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Tidy-tree layout engine for mermaid",
|
||||||
|
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
|
||||||
|
"types": "dist/layouts.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
|
||||||
|
"types": "./dist/layouts.d.ts"
|
||||||
|
},
|
||||||
|
"./": "./"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"diagram",
|
||||||
|
"markdown",
|
||||||
|
"tidy-tree",
|
||||||
|
"mermaid",
|
||||||
|
"layout"
|
||||||
|
],
|
||||||
|
"scripts": {},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mermaid-js/mermaid"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
"Knut Sveidqvist",
|
||||||
|
"Sidharth Vinod"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d3": "^7.9.0",
|
||||||
|
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
|
"mermaid": "workspace:^"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"mermaid": "^11.0.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
50
packages/mermaid-layout-tidy-tree/src/index.ts
Normal file
50
packages/mermaid-layout-tidy-tree/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams
|
||||||
|
*
|
||||||
|
* This module provides a layout algorithm implementation using the
|
||||||
|
* non-layered-tidy-tree-layout algorithm for positioning nodes and edges
|
||||||
|
* in tree structures with a bidirectional approach.
|
||||||
|
*
|
||||||
|
* The algorithm creates two separate trees that grow horizontally in opposite
|
||||||
|
* directions from a central root node:
|
||||||
|
* - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||||
|
* - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||||
|
*
|
||||||
|
* This creates a balanced, symmetric layout that is ideal for mindmaps,
|
||||||
|
* organizational charts, and other tree-based diagrams.
|
||||||
|
*
|
||||||
|
* The algorithm follows the unified rendering pattern and can be used
|
||||||
|
* by any diagram type that provides compatible LayoutData.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render function for the bidirectional tidy-tree layout algorithm
|
||||||
|
*
|
||||||
|
* This function follows the unified rendering pattern used by all layout algorithms.
|
||||||
|
* It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm,
|
||||||
|
* and renders the positioned elements to the SVG.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Alternates root children between left and right trees
|
||||||
|
* - Left tree grows horizontally to the left (rotated 90° counterclockwise)
|
||||||
|
* - Right tree grows horizontally to the right (rotated 90° clockwise)
|
||||||
|
* - Uses tidy-tree algorithm for optimal spacing within each tree
|
||||||
|
* - Creates symmetric, balanced layouts
|
||||||
|
* - Maintains proper edge connections between all nodes
|
||||||
|
*
|
||||||
|
* Layout Structure:
|
||||||
|
* ```
|
||||||
|
* [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||||
|
* ↓ ↓ ↓ ↓
|
||||||
|
* [GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param layoutData - Layout data containing nodes, edges, and configuration
|
||||||
|
* @param svg - SVG element to render to
|
||||||
|
* @param helpers - Internal helper functions for rendering
|
||||||
|
* @param options - Rendering options
|
||||||
|
*/
|
||||||
|
export { default } from './layouts.js';
|
||||||
|
export * from './types.js';
|
||||||
|
export * from './layout.js';
|
||||||
|
export { render } from './render.js';
|
||||||
409
packages/mermaid-layout-tidy-tree/src/layout.test.ts
Normal file
409
packages/mermaid-layout-tidy-tree/src/layout.test.ts
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { executeTidyTreeLayout, validateLayoutData } from './layout.js';
|
||||||
|
import type { LayoutResult } from './types.js';
|
||||||
|
import type { LayoutData, MermaidConfig } from 'mermaid';
|
||||||
|
|
||||||
|
// Mock non-layered-tidy-tree-layout
|
||||||
|
vi.mock('non-layered-tidy-tree-layout', () => ({
|
||||||
|
BoundingBox: vi.fn().mockImplementation(() => ({})),
|
||||||
|
Layout: vi.fn().mockImplementation(() => ({
|
||||||
|
layout: vi.fn().mockImplementation((treeData) => {
|
||||||
|
const result = { ...treeData };
|
||||||
|
|
||||||
|
if (result.id?.toString().startsWith('virtual-root')) {
|
||||||
|
result.x = 0;
|
||||||
|
result.y = 0;
|
||||||
|
} else {
|
||||||
|
result.x = 100;
|
||||||
|
result.y = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.children) {
|
||||||
|
result.children.forEach((child: any, index: number) => {
|
||||||
|
child.x = 50 + index * 100;
|
||||||
|
child.y = 100;
|
||||||
|
|
||||||
|
if (child.children) {
|
||||||
|
child.children.forEach((grandchild: any, gIndex: number) => {
|
||||||
|
grandchild.x = 25 + gIndex * 50;
|
||||||
|
grandchild.y = 200;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
boundingBox: {
|
||||||
|
left: 0,
|
||||||
|
right: 200,
|
||||||
|
top: 0,
|
||||||
|
bottom: 250,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tidy-Tree Layout Algorithm', () => {
|
||||||
|
let mockConfig: MermaidConfig;
|
||||||
|
let mockLayoutData: LayoutData;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockConfig = {
|
||||||
|
theme: 'default',
|
||||||
|
} as MermaidConfig;
|
||||||
|
|
||||||
|
mockLayoutData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'root',
|
||||||
|
label: 'Root',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child1',
|
||||||
|
label: 'Child 1',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child2',
|
||||||
|
label: 'Child 2',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child3',
|
||||||
|
label: 'Child 3',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child4',
|
||||||
|
label: 'Child 4',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'root_child1',
|
||||||
|
start: 'root',
|
||||||
|
end: 'child1',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'root_child2',
|
||||||
|
start: 'root',
|
||||||
|
end: 'child2',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'root_child3',
|
||||||
|
start: 'root',
|
||||||
|
end: 'child3',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'root_child4',
|
||||||
|
start: 'root',
|
||||||
|
end: 'child4',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: mockConfig,
|
||||||
|
direction: 'TB',
|
||||||
|
type: 'test',
|
||||||
|
diagramId: 'test-diagram',
|
||||||
|
markers: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateLayoutData', () => {
|
||||||
|
it('should validate correct layout data', () => {
|
||||||
|
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing data', () => {
|
||||||
|
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing config', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, config: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid nodes array', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, nodes: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid edges array', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, edges: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('executeTidyTreeLayout function', () => {
|
||||||
|
it('should execute layout algorithm successfully', async () => {
|
||||||
|
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toBeDefined();
|
||||||
|
expect(result.edges).toBeDefined();
|
||||||
|
expect(Array.isArray(result.nodes)).toBe(true);
|
||||||
|
expect(Array.isArray(result.edges)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return positioned nodes with coordinates', async () => {
|
||||||
|
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||||
|
|
||||||
|
expect(result.nodes.length).toBeGreaterThan(0);
|
||||||
|
result.nodes.forEach((node) => {
|
||||||
|
expect(node.x).toBeDefined();
|
||||||
|
expect(node.y).toBeDefined();
|
||||||
|
expect(typeof node.x).toBe('number');
|
||||||
|
expect(typeof node.y).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return positioned edges with coordinates', async () => {
|
||||||
|
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||||
|
|
||||||
|
expect(result.edges.length).toBeGreaterThan(0);
|
||||||
|
result.edges.forEach((edge) => {
|
||||||
|
expect(edge.startX).toBeDefined();
|
||||||
|
expect(edge.startY).toBeDefined();
|
||||||
|
expect(edge.midX).toBeDefined();
|
||||||
|
expect(edge.midY).toBeDefined();
|
||||||
|
expect(edge.endX).toBeDefined();
|
||||||
|
expect(edge.endY).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty layout data gracefully', async () => {
|
||||||
|
const emptyData: LayoutData = {
|
||||||
|
...mockLayoutData,
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(executeTidyTreeLayout(emptyData)).rejects.toThrow(
|
||||||
|
'No nodes found in layout data'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing nodes', async () => {
|
||||||
|
const invalidData = { ...mockLayoutData, nodes: [] };
|
||||||
|
|
||||||
|
await expect(executeTidyTreeLayout(invalidData)).rejects.toThrow(
|
||||||
|
'No nodes found in layout data'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty edges (single node tree)', async () => {
|
||||||
|
const singleNodeData = {
|
||||||
|
...mockLayoutData,
|
||||||
|
edges: [],
|
||||||
|
nodes: [mockLayoutData.nodes[0]],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await executeTidyTreeLayout(singleNodeData);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toHaveLength(1);
|
||||||
|
expect(result.edges).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create bidirectional dual-tree layout with alternating left/right children', async () => {
|
||||||
|
const result = await executeTidyTreeLayout(mockLayoutData);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toHaveLength(5);
|
||||||
|
|
||||||
|
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||||
|
expect(rootNode).toBeDefined();
|
||||||
|
expect(rootNode!.x).toBe(0);
|
||||||
|
expect(rootNode!.y).toBe(20);
|
||||||
|
|
||||||
|
const child1 = result.nodes.find((node) => node.id === 'child1');
|
||||||
|
const child2 = result.nodes.find((node) => node.id === 'child2');
|
||||||
|
const child3 = result.nodes.find((node) => node.id === 'child3');
|
||||||
|
const child4 = result.nodes.find((node) => node.id === 'child4');
|
||||||
|
|
||||||
|
expect(child1).toBeDefined();
|
||||||
|
expect(child2).toBeDefined();
|
||||||
|
expect(child3).toBeDefined();
|
||||||
|
expect(child4).toBeDefined();
|
||||||
|
|
||||||
|
expect(child1!.x).toBeLessThan(rootNode!.x);
|
||||||
|
expect(child2!.x).toBeGreaterThan(rootNode!.x);
|
||||||
|
expect(child3!.x).toBeLessThan(rootNode!.x);
|
||||||
|
expect(child4!.x).toBeGreaterThan(rootNode!.x);
|
||||||
|
|
||||||
|
expect(child1!.x).toBeLessThan(-100);
|
||||||
|
expect(child3!.x).toBeLessThan(-100);
|
||||||
|
|
||||||
|
expect(child2!.x).toBeGreaterThan(100);
|
||||||
|
expect(child4!.x).toBeGreaterThan(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => {
|
||||||
|
const testData = {
|
||||||
|
...mockLayoutData,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'root',
|
||||||
|
label: 'Root',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect' as const,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tall-child',
|
||||||
|
label: 'Tall Child',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect' as const,
|
||||||
|
width: 80,
|
||||||
|
height: 120,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'short-child',
|
||||||
|
label: 'Short Child',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect' as const,
|
||||||
|
width: 80,
|
||||||
|
height: 30,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'root_tall',
|
||||||
|
start: 'root',
|
||||||
|
end: 'tall-child',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'root_short',
|
||||||
|
start: 'root',
|
||||||
|
end: 'short-child',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await executeTidyTreeLayout(testData);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toHaveLength(3);
|
||||||
|
|
||||||
|
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||||
|
const tallChild = result.nodes.find((node) => node.id === 'tall-child');
|
||||||
|
const shortChild = result.nodes.find((node) => node.id === 'short-child');
|
||||||
|
|
||||||
|
expect(rootNode).toBeDefined();
|
||||||
|
expect(tallChild).toBeDefined();
|
||||||
|
expect(shortChild).toBeDefined();
|
||||||
|
|
||||||
|
expect(tallChild!.x).not.toBe(shortChild!.x);
|
||||||
|
|
||||||
|
expect(tallChild!.width).toBe(80);
|
||||||
|
expect(tallChild!.height).toBe(120);
|
||||||
|
expect(shortChild!.width).toBe(80);
|
||||||
|
expect(shortChild!.height).toBe(30);
|
||||||
|
|
||||||
|
const yDifference = Math.abs(tallChild!.y - shortChild!.y);
|
||||||
|
expect(yDifference).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
629
packages/mermaid-layout-tidy-tree/src/layout.ts
Normal file
629
packages/mermaid-layout-tidy-tree/src/layout.ts
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
import type { LayoutData } from 'mermaid';
|
||||||
|
import type { Bounds, Point } from 'mermaid/src/types.js';
|
||||||
|
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
|
||||||
|
import type {
|
||||||
|
Edge,
|
||||||
|
LayoutResult,
|
||||||
|
Node,
|
||||||
|
PositionedEdge,
|
||||||
|
PositionedNode,
|
||||||
|
TidyTreeNode,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the tidy-tree layout algorithm on generic layout data
|
||||||
|
*
|
||||||
|
* This function takes layout data and uses the non-layered-tidy-tree-layout
|
||||||
|
* algorithm to calculate optimal node positions for tree structures.
|
||||||
|
*
|
||||||
|
* @param data - The layout data containing nodes, edges, and configuration
|
||||||
|
* @param config - Mermaid configuration object
|
||||||
|
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||||
|
*/
|
||||||
|
export function executeTidyTreeLayout(data: LayoutData): Promise<LayoutResult> {
|
||||||
|
let intersectionShift = 50;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
|
||||||
|
throw new Error('No nodes found in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.edges || !Array.isArray(data.edges)) {
|
||||||
|
data.edges = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
|
||||||
|
|
||||||
|
const gap = 20;
|
||||||
|
const bottomPadding = 40;
|
||||||
|
intersectionShift = 30;
|
||||||
|
|
||||||
|
const bb = new BoundingBox(gap, bottomPadding);
|
||||||
|
const layout = new Layout(bb);
|
||||||
|
|
||||||
|
let leftResult = null;
|
||||||
|
let rightResult = null;
|
||||||
|
|
||||||
|
if (leftTree) {
|
||||||
|
const leftLayoutResult = layout.layout(leftTree);
|
||||||
|
leftResult = leftLayoutResult.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTree) {
|
||||||
|
const rightLayoutResult = layout.layout(rightTree);
|
||||||
|
rightResult = rightLayoutResult.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult);
|
||||||
|
const positionedEdges = calculateEdgePositions(
|
||||||
|
data.edges,
|
||||||
|
positionedNodes,
|
||||||
|
intersectionShift
|
||||||
|
);
|
||||||
|
resolve({
|
||||||
|
nodes: positionedNodes,
|
||||||
|
edges: positionedEdges,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert LayoutData to dual-tree format (left and right trees)
|
||||||
|
*
|
||||||
|
* This function builds two separate tree structures from the nodes and edges,
|
||||||
|
* alternating children between left and right trees.
|
||||||
|
*/
|
||||||
|
function convertToDualTreeFormat(data: LayoutData): {
|
||||||
|
leftTree: TidyTreeNode | null;
|
||||||
|
rightTree: TidyTreeNode | null;
|
||||||
|
rootNode: TidyTreeNode;
|
||||||
|
} {
|
||||||
|
const { nodes, edges } = data;
|
||||||
|
|
||||||
|
const nodeMap = new Map<string, Node>();
|
||||||
|
nodes.forEach((node) => nodeMap.set(node.id, node));
|
||||||
|
|
||||||
|
const children = new Map<string, string[]>();
|
||||||
|
const parents = new Map<string, string>();
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
const parentId = edge.start;
|
||||||
|
const childId = edge.end;
|
||||||
|
|
||||||
|
if (parentId && childId) {
|
||||||
|
if (!children.has(parentId)) {
|
||||||
|
children.set(parentId, []);
|
||||||
|
}
|
||||||
|
children.get(parentId)!.push(childId);
|
||||||
|
parents.set(childId, parentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootNodeData = nodes.find((node) => !parents.has(node.id));
|
||||||
|
if (!rootNodeData && nodes.length === 0) {
|
||||||
|
throw new Error('No nodes available to create tree');
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualRoot = rootNodeData ?? nodes[0];
|
||||||
|
|
||||||
|
const rootNode: TidyTreeNode = {
|
||||||
|
id: actualRoot.id,
|
||||||
|
width: actualRoot.width ?? 100,
|
||||||
|
height: actualRoot.height ?? 50,
|
||||||
|
_originalNode: actualRoot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootChildren = children.get(actualRoot.id) ?? [];
|
||||||
|
const leftChildren: string[] = [];
|
||||||
|
const rightChildren: string[] = [];
|
||||||
|
|
||||||
|
rootChildren.forEach((childId, index) => {
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
leftChildren.push(childId);
|
||||||
|
} else {
|
||||||
|
rightChildren.push(childId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
|
||||||
|
|
||||||
|
const rightTree =
|
||||||
|
rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
|
||||||
|
|
||||||
|
return { leftTree, rightTree, rootNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a subtree from a list of root children
|
||||||
|
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
|
||||||
|
*/
|
||||||
|
function buildSubTree(
|
||||||
|
rootChildren: string[],
|
||||||
|
children: Map<string, string[]>,
|
||||||
|
nodeMap: Map<string, Node>
|
||||||
|
): TidyTreeNode {
|
||||||
|
const virtualRoot: TidyTreeNode = {
|
||||||
|
id: `virtual-root-${Math.random()}`,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
children: rootChildren
|
||||||
|
.map((childId) => nodeMap.get(childId))
|
||||||
|
.filter((child): child is Node => child !== undefined)
|
||||||
|
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return virtualRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively convert a node and its children to tidy-tree format
|
||||||
|
* This version transposes width/height for horizontal tree layout
|
||||||
|
*/
|
||||||
|
function convertNodeToTidyTreeTransposed(
|
||||||
|
node: Node,
|
||||||
|
children: Map<string, string[]>,
|
||||||
|
nodeMap: Map<string, Node>
|
||||||
|
): TidyTreeNode {
|
||||||
|
const childIds = children.get(node.id) ?? [];
|
||||||
|
const childNodes = childIds
|
||||||
|
.map((childId) => nodeMap.get(childId))
|
||||||
|
.filter((child): child is Node => child !== undefined)
|
||||||
|
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
width: node.height ?? 50,
|
||||||
|
height: node.width ?? 100,
|
||||||
|
children: childNodes.length > 0 ? childNodes : undefined,
|
||||||
|
_originalNode: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Combine and position the left and right trees around the root node
|
||||||
|
* Creates a bidirectional layout where left tree grows left and right tree grows right
|
||||||
|
*/
|
||||||
|
function combineAndPositionTrees(
|
||||||
|
rootNode: TidyTreeNode,
|
||||||
|
leftResult: TidyTreeNode | null,
|
||||||
|
rightResult: TidyTreeNode | null
|
||||||
|
): PositionedNode[] {
|
||||||
|
const positionedNodes: PositionedNode[] = [];
|
||||||
|
|
||||||
|
const rootX = 0;
|
||||||
|
const rootY = 0;
|
||||||
|
|
||||||
|
const treeSpacing = rootNode.width / 2 + 30;
|
||||||
|
const leftTreeNodes: PositionedNode[] = [];
|
||||||
|
const rightTreeNodes: PositionedNode[] = [];
|
||||||
|
|
||||||
|
if (leftResult?.children) {
|
||||||
|
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightResult?.children) {
|
||||||
|
positionRightTreeBidirectional(
|
||||||
|
rightResult.children,
|
||||||
|
rightTreeNodes,
|
||||||
|
rootX + treeSpacing,
|
||||||
|
rootY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftTreeCenterY = 0;
|
||||||
|
let rightTreeCenterY = 0;
|
||||||
|
|
||||||
|
if (leftTreeNodes.length > 0) {
|
||||||
|
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
|
||||||
|
(a, b) => b - a
|
||||||
|
);
|
||||||
|
const firstLevelLeftX = leftTreeXPositions[0];
|
||||||
|
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
|
||||||
|
|
||||||
|
if (firstLevelLeftNodes.length > 0) {
|
||||||
|
const leftMinY = Math.min(
|
||||||
|
...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||||
|
);
|
||||||
|
const leftMaxY = Math.max(
|
||||||
|
...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||||
|
);
|
||||||
|
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTreeNodes.length > 0) {
|
||||||
|
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
|
||||||
|
(a, b) => a - b
|
||||||
|
);
|
||||||
|
const firstLevelRightX = rightTreeXPositions[0];
|
||||||
|
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
|
||||||
|
|
||||||
|
if (firstLevelRightNodes.length > 0) {
|
||||||
|
const rightMinY = Math.min(
|
||||||
|
...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||||
|
);
|
||||||
|
const rightMaxY = Math.max(
|
||||||
|
...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||||
|
);
|
||||||
|
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftTreeOffset = -leftTreeCenterY;
|
||||||
|
const rightTreeOffset = -rightTreeCenterY;
|
||||||
|
|
||||||
|
positionedNodes.push({
|
||||||
|
id: String(rootNode.id),
|
||||||
|
x: rootX,
|
||||||
|
y: rootY + 20,
|
||||||
|
section: 'root',
|
||||||
|
width: rootNode._originalNode?.width ?? rootNode.width,
|
||||||
|
height: rootNode._originalNode?.height ?? rootNode.height,
|
||||||
|
originalNode: rootNode._originalNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
x: node.x - (node.width ?? 0) / 2,
|
||||||
|
y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
|
||||||
|
section: 'left' as const,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
originalNode: node.originalNode,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
x: node.x + (node.width ?? 0) / 2,
|
||||||
|
y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
|
||||||
|
section: 'right' as const,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
originalNode: node.originalNode,
|
||||||
|
}));
|
||||||
|
|
||||||
|
positionedNodes.push(...leftTreeNodesWithOffset);
|
||||||
|
positionedNodes.push(...rightTreeNodesWithOffset);
|
||||||
|
|
||||||
|
return positionedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position nodes from the left tree in a bidirectional layout (grows to the left)
|
||||||
|
* Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
|
||||||
|
*/
|
||||||
|
function positionLeftTreeBidirectional(
|
||||||
|
nodes: TidyTreeNode[],
|
||||||
|
positionedNodes: PositionedNode[],
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number
|
||||||
|
): void {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const distanceFromRoot = node.y ?? 0;
|
||||||
|
const verticalPosition = node.x ?? 0;
|
||||||
|
|
||||||
|
const originalWidth = node._originalNode?.width ?? 100;
|
||||||
|
const originalHeight = node._originalNode?.height ?? 50;
|
||||||
|
|
||||||
|
const adjustedY = offsetY + verticalPosition;
|
||||||
|
|
||||||
|
positionedNodes.push({
|
||||||
|
id: String(node.id),
|
||||||
|
x: offsetX - distanceFromRoot,
|
||||||
|
y: adjustedY,
|
||||||
|
width: originalWidth,
|
||||||
|
height: originalHeight,
|
||||||
|
originalNode: node._originalNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position nodes from the right tree in a bidirectional layout (grows to the right)
|
||||||
|
* Rotates the tree 90 degrees clockwise so it grows horizontally to the right
|
||||||
|
*/
|
||||||
|
function positionRightTreeBidirectional(
|
||||||
|
nodes: TidyTreeNode[],
|
||||||
|
positionedNodes: PositionedNode[],
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number
|
||||||
|
): void {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const distanceFromRoot = node.y ?? 0;
|
||||||
|
const verticalPosition = node.x ?? 0;
|
||||||
|
|
||||||
|
const originalWidth = node._originalNode?.width ?? 100;
|
||||||
|
const originalHeight = node._originalNode?.height ?? 50;
|
||||||
|
|
||||||
|
const adjustedY = offsetY + verticalPosition;
|
||||||
|
|
||||||
|
positionedNodes.push({
|
||||||
|
id: String(node.id),
|
||||||
|
x: offsetX + distanceFromRoot,
|
||||||
|
y: adjustedY,
|
||||||
|
width: originalWidth,
|
||||||
|
height: originalHeight,
|
||||||
|
originalNode: node._originalNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the intersection point of a line with a circle
|
||||||
|
* @param circle - Circle coordinates and radius
|
||||||
|
* @param lineStart - Starting point of the line
|
||||||
|
* @param lineEnd - Ending point of the line
|
||||||
|
* @returns The intersection point
|
||||||
|
*/
|
||||||
|
function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point {
|
||||||
|
const radius = Math.min(circle.width, circle.height) / 2;
|
||||||
|
|
||||||
|
const dx = lineEnd.x - lineStart.x;
|
||||||
|
const dy = lineEnd.y - lineStart.y;
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
return lineStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nx = dx / length;
|
||||||
|
const ny = dy / length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: circle.x - nx * radius,
|
||||||
|
y: circle.y - ny * radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point {
|
||||||
|
const x = node.x;
|
||||||
|
const y = node.y;
|
||||||
|
|
||||||
|
if (!node.width || !node.height) {
|
||||||
|
return { x: outsidePoint.x, y: outsidePoint.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,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (r === 0) {
|
||||||
|
res.x = outsidePoint.x;
|
||||||
|
res.y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
if (R === 0) {
|
||||||
|
res.x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
res.y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (r === 0) {
|
||||||
|
_x = outsidePoint.x;
|
||||||
|
_y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
if (R === 0) {
|
||||||
|
_x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
_y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: _x, y: _y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate edge positions based on positioned nodes
|
||||||
|
* Now includes tree membership and node dimensions for precise edge calculations
|
||||||
|
* Edges now stop at shape boundaries instead of extending to centers
|
||||||
|
*/
|
||||||
|
function calculateEdgePositions(
|
||||||
|
edges: Edge[],
|
||||||
|
positionedNodes: PositionedNode[],
|
||||||
|
intersectionShift: number
|
||||||
|
): PositionedEdge[] {
|
||||||
|
const nodeInfo = new Map<string, PositionedNode>();
|
||||||
|
positionedNodes.forEach((node) => {
|
||||||
|
nodeInfo.set(node.id, node);
|
||||||
|
});
|
||||||
|
|
||||||
|
return edges.map((edge) => {
|
||||||
|
const sourceNode = nodeInfo.get(edge.start ?? '');
|
||||||
|
const targetNode = nodeInfo.get(edge.end ?? '');
|
||||||
|
|
||||||
|
if (!sourceNode || !targetNode) {
|
||||||
|
return {
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.start ?? '',
|
||||||
|
target: edge.end ?? '',
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
midX: 0,
|
||||||
|
midY: 0,
|
||||||
|
endX: 0,
|
||||||
|
endY: 0,
|
||||||
|
points: [{ x: 0, y: 0 }],
|
||||||
|
sourceSection: undefined,
|
||||||
|
targetSection: undefined,
|
||||||
|
sourceWidth: undefined,
|
||||||
|
sourceHeight: undefined,
|
||||||
|
targetWidth: undefined,
|
||||||
|
targetHeight: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
|
||||||
|
const targetCenter = { x: targetNode.x, y: targetNode.y };
|
||||||
|
|
||||||
|
const isSourceRound = ['circle', 'cloud', 'bang'].includes(
|
||||||
|
sourceNode.originalNode?.shape ?? ''
|
||||||
|
);
|
||||||
|
const isTargetRound = ['circle', 'cloud', 'bang'].includes(
|
||||||
|
targetNode.originalNode?.shape ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
let startPos = isSourceRound
|
||||||
|
? computeCircleEdgeIntersection(
|
||||||
|
{
|
||||||
|
x: sourceNode.x,
|
||||||
|
y: sourceNode.y,
|
||||||
|
width: sourceNode.width ?? 100,
|
||||||
|
height: sourceNode.height ?? 100,
|
||||||
|
},
|
||||||
|
targetCenter,
|
||||||
|
sourceCenter
|
||||||
|
)
|
||||||
|
: intersection(sourceNode, sourceCenter, targetCenter);
|
||||||
|
|
||||||
|
let endPos = isTargetRound
|
||||||
|
? computeCircleEdgeIntersection(
|
||||||
|
{
|
||||||
|
x: targetNode.x,
|
||||||
|
y: targetNode.y,
|
||||||
|
width: targetNode.width ?? 100,
|
||||||
|
height: targetNode.height ?? 100,
|
||||||
|
},
|
||||||
|
sourceCenter,
|
||||||
|
targetCenter
|
||||||
|
)
|
||||||
|
: intersection(targetNode, targetCenter, sourceCenter);
|
||||||
|
|
||||||
|
const midX = (startPos.x + endPos.x) / 2;
|
||||||
|
const midY = (startPos.y + endPos.y) / 2;
|
||||||
|
|
||||||
|
const points = [startPos];
|
||||||
|
if (sourceNode.section === 'left') {
|
||||||
|
points.push({
|
||||||
|
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
|
||||||
|
y: sourceNode.y,
|
||||||
|
});
|
||||||
|
} else if (sourceNode.section === 'right') {
|
||||||
|
points.push({
|
||||||
|
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
|
||||||
|
y: sourceNode.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetNode.section === 'left') {
|
||||||
|
points.push({
|
||||||
|
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
|
||||||
|
y: targetNode.y,
|
||||||
|
});
|
||||||
|
} else if (targetNode.section === 'right') {
|
||||||
|
points.push({
|
||||||
|
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
|
||||||
|
y: targetNode.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push(endPos);
|
||||||
|
|
||||||
|
const secondPoint = points.length > 1 ? points[1] : targetCenter;
|
||||||
|
startPos = isSourceRound
|
||||||
|
? computeCircleEdgeIntersection(
|
||||||
|
{
|
||||||
|
x: sourceNode.x,
|
||||||
|
y: sourceNode.y,
|
||||||
|
width: sourceNode.width ?? 100,
|
||||||
|
height: sourceNode.height ?? 100,
|
||||||
|
},
|
||||||
|
secondPoint,
|
||||||
|
sourceCenter
|
||||||
|
)
|
||||||
|
: intersection(sourceNode, secondPoint, sourceCenter);
|
||||||
|
points[0] = startPos;
|
||||||
|
|
||||||
|
const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
|
||||||
|
endPos = isTargetRound
|
||||||
|
? computeCircleEdgeIntersection(
|
||||||
|
{
|
||||||
|
x: targetNode.x,
|
||||||
|
y: targetNode.y,
|
||||||
|
width: targetNode.width ?? 100,
|
||||||
|
height: targetNode.height ?? 100,
|
||||||
|
},
|
||||||
|
secondLastPoint,
|
||||||
|
targetCenter
|
||||||
|
)
|
||||||
|
: intersection(targetNode, secondLastPoint, targetCenter);
|
||||||
|
points[points.length - 1] = endPos;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.start ?? '',
|
||||||
|
target: edge.end ?? '',
|
||||||
|
startX: startPos.x,
|
||||||
|
startY: startPos.y,
|
||||||
|
midX,
|
||||||
|
midY,
|
||||||
|
endX: endPos.x,
|
||||||
|
endY: endPos.y,
|
||||||
|
points,
|
||||||
|
sourceSection: sourceNode?.section,
|
||||||
|
targetSection: targetNode?.section,
|
||||||
|
sourceWidth: sourceNode?.width,
|
||||||
|
sourceHeight: sourceNode?.height,
|
||||||
|
targetWidth: targetNode?.width,
|
||||||
|
targetHeight: targetNode?.height,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate layout data structure
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns True if data is valid, throws error otherwise
|
||||||
|
*/
|
||||||
|
export function validateLayoutData(data: LayoutData): boolean {
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('Layout data is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.config) {
|
||||||
|
throw new Error('Configuration is required in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(data.nodes)) {
|
||||||
|
throw new Error('Nodes array is required in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(data.edges)) {
|
||||||
|
throw new Error('Edges array is required in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
13
packages/mermaid-layout-tidy-tree/src/layouts.ts
Normal file
13
packages/mermaid-layout-tidy-tree/src/layouts.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||||
|
|
||||||
|
const loader = async () => await import(`./render.js`);
|
||||||
|
|
||||||
|
const tidyTreeLayout: LayoutLoaderDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'tidy-tree',
|
||||||
|
loader,
|
||||||
|
algorithm: 'tidy-tree',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default tidyTreeLayout;
|
||||||
18
packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts
vendored
Normal file
18
packages/mermaid-layout-tidy-tree/src/non-layered-tidy-tree-layout.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
declare module 'non-layered-tidy-tree-layout' {
|
||||||
|
export class BoundingBox {
|
||||||
|
constructor(gap: number, bottomPadding: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Layout {
|
||||||
|
constructor(boundingBox: BoundingBox);
|
||||||
|
layout(data: any): {
|
||||||
|
result: any;
|
||||||
|
boundingBox: {
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
bottom: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
180
packages/mermaid-layout-tidy-tree/src/render.ts
Normal file
180
packages/mermaid-layout-tidy-tree/src/render.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
|
||||||
|
import { executeTidyTreeLayout } from './layout.js';
|
||||||
|
|
||||||
|
interface NodeWithPosition {
|
||||||
|
id: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
domId?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render function for bidirectional tidy-tree layout algorithm
|
||||||
|
*
|
||||||
|
* This follows the same pattern as ELK and dagre renderers:
|
||||||
|
* 1. Insert nodes into DOM to get their actual dimensions
|
||||||
|
* 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
|
||||||
|
* 3. Position the nodes and edges based on layout results
|
||||||
|
*
|
||||||
|
* The bidirectional layout creates two trees that grow horizontally in opposite
|
||||||
|
* directions from a central root node:
|
||||||
|
* - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
|
||||||
|
* - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
|
||||||
|
*/
|
||||||
|
export const render = async (
|
||||||
|
data4Layout: LayoutData,
|
||||||
|
svg: SVG,
|
||||||
|
{
|
||||||
|
insertCluster,
|
||||||
|
insertEdge,
|
||||||
|
insertEdgeLabel,
|
||||||
|
insertMarkers,
|
||||||
|
insertNode,
|
||||||
|
log,
|
||||||
|
positionEdgeLabel,
|
||||||
|
}: InternalHelpers,
|
||||||
|
{ algorithm: _algorithm }: RenderOptions
|
||||||
|
) => {
|
||||||
|
const nodeDb: Record<string, NodeWithPosition> = {};
|
||||||
|
const clusterDb: Record<string, any> = {};
|
||||||
|
|
||||||
|
const element = svg.select('g');
|
||||||
|
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||||
|
|
||||||
|
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
|
||||||
|
const edgePaths = element.insert('g').attr('class', 'edgePaths');
|
||||||
|
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||||
|
const nodes = element.insert('g').attr('class', 'nodes');
|
||||||
|
// Step 1: Insert nodes into DOM to get their actual dimensions
|
||||||
|
log.debug('Inserting nodes into DOM for dimension calculation');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
data4Layout.nodes.map(async (node) => {
|
||||||
|
if (node.isGroup) {
|
||||||
|
const clusterNode: NodeWithPosition = {
|
||||||
|
...node,
|
||||||
|
id: node.id,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
};
|
||||||
|
clusterDb[node.id] = clusterNode;
|
||||||
|
nodeDb[node.id] = clusterNode;
|
||||||
|
|
||||||
|
await insertCluster(subGraphsEl, node);
|
||||||
|
} else {
|
||||||
|
const nodeWithPosition: NodeWithPosition = {
|
||||||
|
...node,
|
||||||
|
id: node.id,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
};
|
||||||
|
nodeDb[node.id] = nodeWithPosition;
|
||||||
|
|
||||||
|
const nodeEl = await insertNode(nodes, node, {
|
||||||
|
config: data4Layout.config,
|
||||||
|
dir: data4Layout.direction || 'TB',
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundingBox = nodeEl.node()!.getBBox();
|
||||||
|
nodeWithPosition.width = boundingBox.width;
|
||||||
|
nodeWithPosition.height = boundingBox.height;
|
||||||
|
nodeWithPosition.domId = nodeEl;
|
||||||
|
|
||||||
|
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Step 2: Run the bidirectional tidy-tree layout algorithm
|
||||||
|
log.debug('Running bidirectional tidy-tree layout algorithm');
|
||||||
|
|
||||||
|
const updatedLayoutData = {
|
||||||
|
...data4Layout,
|
||||||
|
nodes: data4Layout.nodes.map((node) => {
|
||||||
|
const nodeWithDimensions = nodeDb[node.id];
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
width: nodeWithDimensions.width ?? node.width ?? 100,
|
||||||
|
height: nodeWithDimensions.height ?? node.height ?? 50,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
|
||||||
|
// Step 3: Position the nodes based on bidirectional layout results
|
||||||
|
log.debug('Positioning nodes based on bidirectional layout results');
|
||||||
|
|
||||||
|
layoutResult.nodes.forEach((positionedNode) => {
|
||||||
|
const node = nodeDb[positionedNode.id];
|
||||||
|
if (node?.domId) {
|
||||||
|
// Position the node at the calculated coordinates from bidirectional layout
|
||||||
|
// The layout algorithm has already calculated positions for:
|
||||||
|
// - Root node at center (0, 0)
|
||||||
|
// - Left tree nodes with negative x coordinates (growing left)
|
||||||
|
// - Right tree nodes with positive x coordinates (growing right)
|
||||||
|
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||||
|
// Store the final position
|
||||||
|
node.x = positionedNode.x;
|
||||||
|
node.y = positionedNode.y;
|
||||||
|
// Step 3: Position the nodes based on bidirectional layout results
|
||||||
|
log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug('Inserting and positioning edges');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
data4Layout.edges.map(async (edge) => {
|
||||||
|
await insertEdgeLabel(edgeLabels, edge);
|
||||||
|
|
||||||
|
const startNode = nodeDb[edge.start ?? ''];
|
||||||
|
const endNode = nodeDb[edge.end ?? ''];
|
||||||
|
|
||||||
|
if (startNode && endNode) {
|
||||||
|
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||||
|
|
||||||
|
if (positionedEdge) {
|
||||||
|
log.debug('APA01 positionedEdge', positionedEdge);
|
||||||
|
const edgeWithPath = {
|
||||||
|
...edge,
|
||||||
|
points: positionedEdge.points,
|
||||||
|
};
|
||||||
|
const paths = insertEdge(
|
||||||
|
edgePaths,
|
||||||
|
edgeWithPath,
|
||||||
|
clusterDb,
|
||||||
|
data4Layout.type,
|
||||||
|
startNode,
|
||||||
|
endNode,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
|
||||||
|
positionEdgeLabel(edgeWithPath, paths);
|
||||||
|
} else {
|
||||||
|
const edgeWithPath = {
|
||||||
|
...edge,
|
||||||
|
points: [
|
||||||
|
{ x: startNode.x ?? 0, y: startNode.y ?? 0 },
|
||||||
|
{ x: endNode.x ?? 0, y: endNode.y ?? 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const paths = insertEdge(
|
||||||
|
edgePaths,
|
||||||
|
edgeWithPath,
|
||||||
|
clusterDb,
|
||||||
|
data4Layout.type,
|
||||||
|
startNode,
|
||||||
|
endNode,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
positionEdgeLabel(edgeWithPath, paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
log.debug('Bidirectional tidy-tree rendering completed');
|
||||||
|
};
|
||||||
69
packages/mermaid-layout-tidy-tree/src/types.ts
Normal file
69
packages/mermaid-layout-tidy-tree/src/types.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { LayoutData } from 'mermaid';
|
||||||
|
|
||||||
|
export type Node = LayoutData['nodes'][number];
|
||||||
|
export type Edge = LayoutData['edges'][number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positioned node after layout calculation
|
||||||
|
*/
|
||||||
|
export interface PositionedNode {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
section?: 'root' | 'left' | 'right';
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
originalNode?: Node;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positioned edge after layout calculation
|
||||||
|
*/
|
||||||
|
export interface PositionedEdge {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
midX: number;
|
||||||
|
midY: number;
|
||||||
|
endX: number;
|
||||||
|
endY: number;
|
||||||
|
sourceSection?: 'root' | 'left' | 'right';
|
||||||
|
targetSection?: 'root' | 'left' | 'right';
|
||||||
|
sourceWidth?: number;
|
||||||
|
sourceHeight?: number;
|
||||||
|
targetWidth?: number;
|
||||||
|
targetHeight?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of layout algorithm execution
|
||||||
|
*/
|
||||||
|
export interface LayoutResult {
|
||||||
|
nodes: PositionedNode[];
|
||||||
|
edges: PositionedEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tidy-tree node structure compatible with non-layered-tidy-tree-layout
|
||||||
|
*/
|
||||||
|
export interface TidyTreeNode {
|
||||||
|
id: string | number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
children?: TidyTreeNode[];
|
||||||
|
_originalNode?: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tidy-tree layout configuration
|
||||||
|
*/
|
||||||
|
export interface TidyTreeLayoutConfig {
|
||||||
|
gap: number;
|
||||||
|
bottomPadding: number;
|
||||||
|
}
|
||||||
10
packages/mermaid-layout-tidy-tree/tsconfig.json
Normal file
10
packages/mermaid-layout-tidy-tree/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"types": ["vitest/importMeta", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||||
|
"typeRoots": ["./src/types"]
|
||||||
|
}
|
||||||
@@ -229,7 +229,6 @@
|
|||||||
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
|
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
|
||||||
|
|
||||||
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
|
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
|
||||||
|
|
||||||
- Updates the class diagram to the new unified way of rendering.
|
- Updates the class diagram to the new unified way of rendering.
|
||||||
- Includes a new "classBox" shape to be used in diagrams
|
- Includes a new "classBox" shape to be used in diagrams
|
||||||
- Other updates such as:
|
- Other updates such as:
|
||||||
|
|||||||
@@ -123,8 +123,8 @@
|
|||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"start-server-and-test": "^2.0.10",
|
"start-server-and-test": "^2.0.10",
|
||||||
"type-fest": "^4.35.0",
|
"type-fest": "^4.35.0",
|
||||||
"typedoc": "^0.27.8",
|
"typedoc": "^0.28.9",
|
||||||
"typedoc-plugin-markdown": "^4.4.2",
|
"typedoc-plugin-markdown": "^4.8.0",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"unist-util-flatmap": "^1.0.0",
|
"unist-util-flatmap": "^1.0.0",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
|
|||||||
@@ -171,7 +171,9 @@ This Markdown should be kept.
|
|||||||
expect(buildShapeDoc()).toMatchInlineSnapshot(`
|
expect(buildShapeDoc()).toMatchInlineSnapshot(`
|
||||||
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||||
|
| Bang | Bang | \`bang\` | Bang | \`bang\` |
|
||||||
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
|
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
|
||||||
|
| Cloud | Cloud | \`cloud\` | cloud | \`cloud\` |
|
||||||
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
|
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
|
||||||
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
|
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
|
||||||
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
|
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
|
||||||
|
|||||||
@@ -1076,6 +1076,10 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
|
|||||||
export interface MindmapDiagramConfig extends BaseDiagramConfig {
|
export interface MindmapDiagramConfig extends BaseDiagramConfig {
|
||||||
padding?: number;
|
padding?: number;
|
||||||
maxNodeWidth?: number;
|
maxNodeWidth?: number;
|
||||||
|
/**
|
||||||
|
* Layout algorithm to use for positioning mindmap nodes
|
||||||
|
*/
|
||||||
|
layoutAlgorithm?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The object containing configurations specific for kanban diagrams
|
* The object containing configurations specific for kanban diagrams
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// tests to check that comments are removed
|
|
||||||
|
|
||||||
import { cleanupComments } from './comments.js';
|
import { cleanupComments } from './comments.js';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
@@ -10,12 +8,12 @@ describe('comments', () => {
|
|||||||
%% This is a comment
|
%% This is a comment
|
||||||
%% This is another comment
|
%% This is another comment
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
%% This is a comment
|
%% This is a comment
|
||||||
`;
|
`;
|
||||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||||
"graph TD
|
"graph TD
|
||||||
A-->B
|
A-->B
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -29,9 +27,9 @@ graph TD
|
|||||||
%%{ init: {'theme': 'space before init'}}%%
|
%%{ init: {'theme': 'space before init'}}%%
|
||||||
%%{init: {'theme': 'space after ending'}}%%
|
%%{init: {'theme': 'space after ending'}}%%
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
|
|
||||||
B-->C
|
B-->C
|
||||||
%% This is a comment
|
%% This is a comment
|
||||||
`;
|
`;
|
||||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||||
@@ -39,9 +37,9 @@ graph TD
|
|||||||
%%{ init: {'theme': 'space before init'}}%%
|
%%{ init: {'theme': 'space before init'}}%%
|
||||||
%%{init: {'theme': 'space after ending'}}%%
|
%%{init: {'theme': 'space after ending'}}%%
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
|
|
||||||
B-->C
|
B-->C
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -50,14 +48,14 @@ graph TD
|
|||||||
const text = `
|
const text = `
|
||||||
%% This is a comment
|
%% This is a comment
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
%% This is a comment
|
%% This is a comment
|
||||||
C-->D
|
C-->D
|
||||||
`;
|
`;
|
||||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||||
"graph TD
|
"graph TD
|
||||||
A-->B
|
A-->B
|
||||||
C-->D
|
C-->D
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -70,11 +68,11 @@ graph TD
|
|||||||
|
|
||||||
%% This is a comment
|
%% This is a comment
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
`;
|
`;
|
||||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||||
"graph TD
|
"graph TD
|
||||||
A-->B
|
A-->B
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -82,12 +80,12 @@ graph TD
|
|||||||
it('should remove comments at end of text with no newline', () => {
|
it('should remove comments at end of text with no newline', () => {
|
||||||
const text = `
|
const text = `
|
||||||
graph TD
|
graph TD
|
||||||
A-->B
|
A-->B
|
||||||
%% This is a comment`;
|
%% This is a comment`;
|
||||||
|
|
||||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||||
"graph TD
|
"graph TD
|
||||||
A-->B
|
A-->B
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type * as d3 from 'd3';
|
|||||||
import type { SetOptional, SetRequired } from 'type-fest';
|
import type { SetOptional, SetRequired } from 'type-fest';
|
||||||
import type { Diagram } from '../Diagram.js';
|
import type { Diagram } from '../Diagram.js';
|
||||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
||||||
|
import type { DiagramOrientation } from '../diagrams/git/gitGraphTypes.js';
|
||||||
|
|
||||||
export interface DiagramMetadata {
|
export interface DiagramMetadata {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -35,7 +36,8 @@ export interface DiagramDB {
|
|||||||
getAccTitle?: () => string;
|
getAccTitle?: () => string;
|
||||||
setAccDescription?: (description: string) => void;
|
setAccDescription?: (description: string) => void;
|
||||||
getAccDescription?: () => string;
|
getAccDescription?: () => string;
|
||||||
|
getDirection?: () => string | undefined;
|
||||||
|
setDirection?: (dir: DiagramOrientation) => void;
|
||||||
setDisplayMode?: (title: string) => void;
|
setDisplayMode?: (title: string) => void;
|
||||||
bindFunctions?: (element: Element) => void;
|
bindFunctions?: (element: Element) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Position } from 'cytoscape';
|
import type { LayoutOptions, Position } from 'cytoscape';
|
||||||
import cytoscape from 'cytoscape';
|
import cytoscape from 'cytoscape';
|
||||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
|
||||||
import fcose from 'cytoscape-fcose';
|
import fcose from 'cytoscape-fcose';
|
||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||||
@@ -41,7 +40,7 @@ registerIconPacks([
|
|||||||
icons: architectureIcons,
|
icons: architectureIcons,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
cytoscape.use(fcose);
|
cytoscape.use(fcose as any);
|
||||||
|
|
||||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||||
services.forEach((service) => {
|
services.forEach((service) => {
|
||||||
@@ -429,7 +428,7 @@ function layoutArchitecture(
|
|||||||
},
|
},
|
||||||
alignmentConstraint,
|
alignmentConstraint,
|
||||||
relativePlacementConstraint,
|
relativePlacementConstraint,
|
||||||
} as FcoseLayoutOptions);
|
} as LayoutOptions);
|
||||||
|
|
||||||
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
|
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
|
||||||
layout.one('layoutstop', () => {
|
layout.one('layoutstop', () => {
|
||||||
|
|||||||
48
packages/mermaid/src/diagrams/architecture/svgDraw.spec.ts
Normal file
48
packages/mermaid/src/diagrams/architecture/svgDraw.spec.ts
Normal 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');
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
type ArchitectureJunction,
|
type ArchitectureJunction,
|
||||||
type ArchitectureService,
|
type ArchitectureService,
|
||||||
} from './architectureTypes.js';
|
} from './architectureTypes.js';
|
||||||
|
import { getEdgeId } from '../../utils.js';
|
||||||
|
|
||||||
export const drawEdges = async function (
|
export const drawEdges = async function (
|
||||||
edgesEl: D3Element,
|
edgesEl: D3Element,
|
||||||
@@ -91,7 +92,8 @@ export const drawEdges = async function (
|
|||||||
|
|
||||||
g.insert('path')
|
g.insert('path')
|
||||||
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
.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) {
|
if (sourceArrow) {
|
||||||
const xShift = isArchitectureDirectionX(sourceDir)
|
const xShift = isArchitectureDirectionX(sourceDir)
|
||||||
@@ -206,8 +208,9 @@ export const drawGroups = async function (
|
|||||||
if (data.type === 'group') {
|
if (data.type === 'group') {
|
||||||
const { h, w, x1, y1 } = node.boundingBox();
|
const { h, w, x1, y1 } = node.boundingBox();
|
||||||
|
|
||||||
groupsEl
|
const groupsNode = groupsEl.append('rect');
|
||||||
.append('rect')
|
groupsNode
|
||||||
|
.attr('id', `group-${data.id}`)
|
||||||
.attr('x', x1 + halfIconSize)
|
.attr('x', x1 + halfIconSize)
|
||||||
.attr('y', y1 + halfIconSize)
|
.attr('y', y1 + halfIconSize)
|
||||||
.attr('width', w)
|
.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.width = width;
|
||||||
service.height = height;
|
service.height = height;
|
||||||
db.setElementForId(service.id, serviceElem);
|
db.setElementForId(service.id, serviceElem);
|
||||||
|
|||||||
297
packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
Normal file
297
packages/mermaid/src/diagrams/mindmap/mindmapDb.getData.test.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { MindmapDB } from './mindmapDb.js';
|
||||||
|
import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
|
||||||
|
import type { Edge } from '../../rendering-util/types.js';
|
||||||
|
|
||||||
|
// Mock the getConfig function
|
||||||
|
vi.mock('../../diagram-api/diagramAPI.js', () => ({
|
||||||
|
getConfig: vi.fn(() => ({
|
||||||
|
mindmap: {
|
||||||
|
layoutAlgorithm: 'cose-bilkent',
|
||||||
|
padding: 10,
|
||||||
|
maxNodeWidth: 200,
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MindmapDb getData function', () => {
|
||||||
|
let db: MindmapDB;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = new MindmapDB();
|
||||||
|
// Clear the database before each test
|
||||||
|
db.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getData', () => {
|
||||||
|
it('should return empty data when no mindmap is set', () => {
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
expect(result.nodes).toEqual([]);
|
||||||
|
expect(result.edges).toEqual([]);
|
||||||
|
expect(result.config).toBeDefined();
|
||||||
|
expect(result.rootNode).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return structured data for simple mindmap', () => {
|
||||||
|
// Create a simple mindmap structure
|
||||||
|
db.addNode(0, 'root', 'Root Node', 0);
|
||||||
|
db.addNode(1, 'child1', 'Child 1', 0);
|
||||||
|
db.addNode(1, 'child2', 'Child 2', 0);
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
expect(result.nodes).toHaveLength(3);
|
||||||
|
expect(result.edges).toHaveLength(2);
|
||||||
|
expect(result.config).toBeDefined();
|
||||||
|
expect(result.rootNode).toBeDefined();
|
||||||
|
|
||||||
|
// Check root node
|
||||||
|
const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0');
|
||||||
|
expect(rootNode).toBeDefined();
|
||||||
|
expect(rootNode?.label).toBe('Root Node');
|
||||||
|
expect(rootNode?.level).toBe(0);
|
||||||
|
|
||||||
|
// Check child nodes
|
||||||
|
const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1');
|
||||||
|
expect(child1).toBeDefined();
|
||||||
|
expect(child1?.label).toBe('Child 1');
|
||||||
|
expect(child1?.level).toBe(1);
|
||||||
|
|
||||||
|
// Check edges
|
||||||
|
expect(result.edges).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
start: '0',
|
||||||
|
end: '1',
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return structured data for hierarchical mindmap', () => {
|
||||||
|
// Create a hierarchical mindmap structure
|
||||||
|
db.addNode(0, 'root', 'Root Node', 0);
|
||||||
|
db.addNode(1, 'child1', 'Child 1', 0);
|
||||||
|
db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
|
||||||
|
db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
|
||||||
|
db.addNode(1, 'child2', 'Child 2', 0);
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
expect(result.nodes).toHaveLength(5);
|
||||||
|
expect(result.edges).toHaveLength(4);
|
||||||
|
|
||||||
|
// Check that all levels are represented
|
||||||
|
const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
|
||||||
|
expect(levels).toContain(0); // root
|
||||||
|
expect(levels).toContain(1); // children
|
||||||
|
expect(levels).toContain(2); // grandchildren
|
||||||
|
|
||||||
|
// Check edge relationships
|
||||||
|
const edgeRelations = result.edges.map(
|
||||||
|
(e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
|
||||||
|
);
|
||||||
|
expect(edgeRelations).toContain('0->1'); // root to child1
|
||||||
|
expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
|
||||||
|
expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
|
||||||
|
expect(edgeRelations).toContain('0->4'); // root to child2
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve node properties in processed data', () => {
|
||||||
|
// Add a node with specific properties
|
||||||
|
db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
|
||||||
|
|
||||||
|
// Set additional properties
|
||||||
|
const mindmap = db.getMindmap();
|
||||||
|
if (mindmap) {
|
||||||
|
mindmap.width = 150;
|
||||||
|
mindmap.height = 75;
|
||||||
|
mindmap.padding = 15;
|
||||||
|
mindmap.section = 1;
|
||||||
|
mindmap.class = 'custom-class';
|
||||||
|
mindmap.icon = 'star';
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
expect(result.nodes).toHaveLength(1);
|
||||||
|
const node = result.nodes[0] as MindmapLayoutNode;
|
||||||
|
|
||||||
|
expect(node.type).toBe(2);
|
||||||
|
expect(node.width).toBe(150);
|
||||||
|
expect(node.height).toBe(75);
|
||||||
|
expect(node.padding).toBe(15);
|
||||||
|
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||||
|
expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
|
||||||
|
expect(node.icon).toBe('star');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate unique edge IDs', () => {
|
||||||
|
db.addNode(0, 'root', 'Root Node', 0);
|
||||||
|
db.addNode(1, 'child1', 'Child 1', 0);
|
||||||
|
db.addNode(1, 'child2', 'Child 2', 0);
|
||||||
|
db.addNode(1, 'child3', 'Child 3', 0);
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
const edgeIds = result.edges.map((e: Edge) => e.id);
|
||||||
|
const uniqueIds = new Set(edgeIds);
|
||||||
|
|
||||||
|
expect(edgeIds).toHaveLength(3);
|
||||||
|
expect(uniqueIds.size).toBe(3); // All IDs should be unique
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nodes with missing optional properties', () => {
|
||||||
|
db.addNode(0, 'root', 'Root Node', 0);
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
const node = result.nodes[0] as MindmapLayoutNode;
|
||||||
|
|
||||||
|
// Should handle undefined/missing properties gracefully
|
||||||
|
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||||
|
expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
|
||||||
|
expect(node.icon).toBeUndefined();
|
||||||
|
expect(node.x).toBeUndefined();
|
||||||
|
expect(node.y).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assign correct section classes based on sibling position', () => {
|
||||||
|
// Create the example mindmap structure:
|
||||||
|
// A
|
||||||
|
// a0
|
||||||
|
// aa0
|
||||||
|
// a1
|
||||||
|
// aaa
|
||||||
|
// a2
|
||||||
|
db.addNode(0, 'A', 'A', 0); // Root
|
||||||
|
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||||
|
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||||
|
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||||
|
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||||
|
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
// Find nodes by their labels
|
||||||
|
const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||||
|
const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
|
||||||
|
const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
|
||||||
|
const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
|
||||||
|
const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
|
||||||
|
const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
|
||||||
|
|
||||||
|
// Check section assignments
|
||||||
|
expect(nodeA.section).toBeUndefined(); // Root has undefined section
|
||||||
|
expect(nodeA0.section).toBe(0); // First child of root
|
||||||
|
expect(nodeAa0.section).toBe(0); // Inherits from parent a0
|
||||||
|
expect(nodeA1.section).toBe(1); // Second child of root
|
||||||
|
expect(nodeAaa.section).toBe(1); // Inherits from parent a1
|
||||||
|
expect(nodeA2.section).toBe(2); // Third child of root
|
||||||
|
|
||||||
|
// Check CSS classes
|
||||||
|
expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
|
||||||
|
expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
|
||||||
|
expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
|
||||||
|
expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
|
||||||
|
expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
|
||||||
|
expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve custom classes while adding section classes', () => {
|
||||||
|
db.addNode(0, 'root', 'Root Node', 0);
|
||||||
|
db.addNode(1, 'child', 'Child Node', 0);
|
||||||
|
|
||||||
|
// Add custom classes to nodes
|
||||||
|
const mindmap = db.getMindmap();
|
||||||
|
if (mindmap) {
|
||||||
|
mindmap.class = 'custom-root-class';
|
||||||
|
if (mindmap.children?.[0]) {
|
||||||
|
mindmap.children[0].class = 'custom-child-class';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
|
||||||
|
const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
|
||||||
|
|
||||||
|
// Should include both section classes and custom classes
|
||||||
|
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
|
||||||
|
expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create any fake root nodes', () => {
|
||||||
|
// Create a simple mindmap
|
||||||
|
db.addNode(0, 'A', 'A', 0);
|
||||||
|
db.addNode(1, 'a0', 'a0', 0);
|
||||||
|
db.addNode(1, 'a1', 'a1', 0);
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
// Check that we only have the expected nodes
|
||||||
|
expect(result.nodes).toHaveLength(3);
|
||||||
|
expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
|
||||||
|
|
||||||
|
// Check that there's no node with label "mindmap" or any other fake root
|
||||||
|
const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
|
||||||
|
expect(mindmapNode).toBeUndefined();
|
||||||
|
|
||||||
|
// Verify the root node has the correct classes
|
||||||
|
const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||||
|
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
|
||||||
|
expect(rootNode.level).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assign correct section classes to edges', () => {
|
||||||
|
// Create the example mindmap structure:
|
||||||
|
// A
|
||||||
|
// a0
|
||||||
|
// aa0
|
||||||
|
// a1
|
||||||
|
// aaa
|
||||||
|
// a2
|
||||||
|
db.addNode(0, 'A', 'A', 0); // Root
|
||||||
|
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||||
|
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||||
|
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||||
|
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||||
|
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||||
|
|
||||||
|
const result = db.getData();
|
||||||
|
|
||||||
|
// Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
|
||||||
|
expect(result.edges).toHaveLength(5);
|
||||||
|
|
||||||
|
// Find edges by their start and end nodes
|
||||||
|
const edgeA_a0 = result.edges.find(
|
||||||
|
(e) => e.start === '0' && e.end === '1'
|
||||||
|
) as MindmapLayoutEdge;
|
||||||
|
const edgeA0_aa0 = result.edges.find(
|
||||||
|
(e) => e.start === '1' && e.end === '2'
|
||||||
|
) as MindmapLayoutEdge;
|
||||||
|
const edgeA_a1 = result.edges.find(
|
||||||
|
(e) => e.start === '0' && e.end === '3'
|
||||||
|
) as MindmapLayoutEdge;
|
||||||
|
const edgeA1_aaa = result.edges.find(
|
||||||
|
(e) => e.start === '3' && e.end === '4'
|
||||||
|
) as MindmapLayoutEdge;
|
||||||
|
const edgeA_a2 = result.edges.find(
|
||||||
|
(e) => e.start === '0' && e.end === '5'
|
||||||
|
) as MindmapLayoutEdge;
|
||||||
|
|
||||||
|
// Check edge classes
|
||||||
|
expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
|
||||||
|
expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
|
||||||
|
expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
|
||||||
|
expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
|
||||||
|
expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
|
||||||
|
|
||||||
|
// Check section assignments match the child nodes
|
||||||
|
expect(edgeA_a0.section).toBe(0);
|
||||||
|
expect(edgeA0_aa0.section).toBe(0);
|
||||||
|
expect(edgeA_a1.section).toBe(1);
|
||||||
|
expect(edgeA1_aaa.section).toBe(1);
|
||||||
|
expect(edgeA_a2.section).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,26 @@
|
|||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
import type { D3Element } from '../../types.js';
|
import type { D3Element } from '../../types.js';
|
||||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import type { MindmapNode } from './mindmapTypes.js';
|
import type { MindmapNode } from './mindmapTypes.js';
|
||||||
import defaultConfig from '../../defaultConfig.js';
|
import defaultConfig from '../../defaultConfig.js';
|
||||||
|
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
|
||||||
|
import { getUserDefinedConfig } from '../../config.js';
|
||||||
|
|
||||||
|
// Extend Node type for mindmap-specific properties
|
||||||
|
export type MindmapLayoutNode = Node & {
|
||||||
|
level: number;
|
||||||
|
nodeId: string;
|
||||||
|
type: number;
|
||||||
|
section?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend Edge type for mindmap-specific properties
|
||||||
|
export type MindmapLayoutEdge = Edge & {
|
||||||
|
depth: number;
|
||||||
|
section?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const nodeType = {
|
const nodeType = {
|
||||||
DEFAULT: 0,
|
DEFAULT: 0,
|
||||||
@@ -20,6 +37,7 @@ export class MindmapDB {
|
|||||||
private nodes: MindmapNode[] = [];
|
private nodes: MindmapNode[] = [];
|
||||||
private count = 0;
|
private count = 0;
|
||||||
private elements: Record<number, D3Element> = {};
|
private elements: Record<number, D3Element> = {};
|
||||||
|
private baseLevel?: number;
|
||||||
public readonly nodeType: typeof nodeType;
|
public readonly nodeType: typeof nodeType;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -27,7 +45,6 @@ export class MindmapDB {
|
|||||||
this.nodeType = nodeType;
|
this.nodeType = nodeType;
|
||||||
this.clear();
|
this.clear();
|
||||||
this.getType = this.getType.bind(this);
|
this.getType = this.getType.bind(this);
|
||||||
this.getMindmap = this.getMindmap.bind(this);
|
|
||||||
this.getElementById = this.getElementById.bind(this);
|
this.getElementById = this.getElementById.bind(this);
|
||||||
this.getParent = this.getParent.bind(this);
|
this.getParent = this.getParent.bind(this);
|
||||||
this.getMindmap = this.getMindmap.bind(this);
|
this.getMindmap = this.getMindmap.bind(this);
|
||||||
@@ -38,6 +55,7 @@ export class MindmapDB {
|
|||||||
this.nodes = [];
|
this.nodes = [];
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
this.elements = {};
|
this.elements = {};
|
||||||
|
this.baseLevel = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getParent(level: number): MindmapNode | null {
|
public getParent(level: number): MindmapNode | null {
|
||||||
@@ -56,6 +74,17 @@ export class MindmapDB {
|
|||||||
public addNode(level: number, id: string, descr: string, type: number): void {
|
public addNode(level: number, id: string, descr: string, type: number): void {
|
||||||
log.info('addNode', level, id, descr, type);
|
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();
|
const conf = getConfig();
|
||||||
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||||
|
|
||||||
@@ -76,6 +105,7 @@ export class MindmapDB {
|
|||||||
children: [],
|
children: [],
|
||||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||||
padding,
|
padding,
|
||||||
|
isRoot,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parent = this.getParent(level);
|
const parent = this.getParent(level);
|
||||||
@@ -83,7 +113,7 @@ export class MindmapDB {
|
|||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
this.nodes.push(node);
|
this.nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
if (this.nodes.length === 0) {
|
if (isRoot) {
|
||||||
this.nodes.push(node);
|
this.nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -156,6 +186,222 @@ export class MindmapDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign section numbers to nodes based on their position relative to root
|
||||||
|
* @param node - The mindmap node to process
|
||||||
|
* @param sectionNumber - The section number to assign (undefined for root)
|
||||||
|
*/
|
||||||
|
public assignSections(node: MindmapNode, sectionNumber?: number): void {
|
||||||
|
// For root node, section should be undefined (not -1)
|
||||||
|
if (node.level === 0) {
|
||||||
|
node.section = undefined;
|
||||||
|
} else {
|
||||||
|
// For non-root nodes, assign the section number
|
||||||
|
node.section = sectionNumber;
|
||||||
|
}
|
||||||
|
// For root node's children, assign section numbers based on their index
|
||||||
|
// For other nodes, inherit parent's section number
|
||||||
|
if (node.children) {
|
||||||
|
for (const [index, child] of node.children.entries()) {
|
||||||
|
const childSectionNumber = node.level === 0 ? index : sectionNumber;
|
||||||
|
this.assignSections(child, childSectionNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert mindmap tree structure to flat array of nodes
|
||||||
|
* @param node - The mindmap node to process
|
||||||
|
* @param processedNodes - Array to collect processed nodes
|
||||||
|
*/
|
||||||
|
public flattenNodes(node: MindmapNode, processedNodes: MindmapLayoutNode[]): void {
|
||||||
|
// Build CSS classes for the node
|
||||||
|
const cssClasses = ['mindmap-node'];
|
||||||
|
|
||||||
|
if (node.isRoot === true) {
|
||||||
|
// Root node gets special classes
|
||||||
|
cssClasses.push('section-root', 'section--1');
|
||||||
|
} else if (node.section !== undefined) {
|
||||||
|
// Child nodes get section class based on their section number
|
||||||
|
cssClasses.push(`section-${node.section}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any custom classes from the node
|
||||||
|
if (node.class) {
|
||||||
|
cssClasses.push(node.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = cssClasses.join(' ');
|
||||||
|
|
||||||
|
// Map mindmap node type to valid shape name
|
||||||
|
const getShapeFromType = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case nodeType.CIRCLE:
|
||||||
|
return 'mindmapCircle';
|
||||||
|
case nodeType.RECT:
|
||||||
|
return 'rect';
|
||||||
|
case nodeType.ROUNDED_RECT:
|
||||||
|
return 'rounded';
|
||||||
|
case nodeType.CLOUD:
|
||||||
|
return 'cloud';
|
||||||
|
case nodeType.BANG:
|
||||||
|
return 'bang';
|
||||||
|
case nodeType.HEXAGON:
|
||||||
|
return 'hexagon';
|
||||||
|
case nodeType.DEFAULT:
|
||||||
|
return 'defaultMindmapNode';
|
||||||
|
case nodeType.NO_BORDER:
|
||||||
|
default:
|
||||||
|
return 'rect';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedNode: MindmapLayoutNode = {
|
||||||
|
id: node.id.toString(),
|
||||||
|
domId: 'node_' + node.id.toString(),
|
||||||
|
label: node.descr,
|
||||||
|
isGroup: false,
|
||||||
|
shape: getShapeFromType(node.type),
|
||||||
|
width: node.width,
|
||||||
|
height: node.height ?? 0,
|
||||||
|
padding: node.padding,
|
||||||
|
cssClasses: classes,
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
icon: node.icon,
|
||||||
|
x: node.x,
|
||||||
|
y: node.y,
|
||||||
|
// Mindmap-specific properties
|
||||||
|
level: node.level,
|
||||||
|
nodeId: node.nodeId,
|
||||||
|
type: node.type,
|
||||||
|
section: node.section,
|
||||||
|
};
|
||||||
|
|
||||||
|
processedNodes.push(processedNode);
|
||||||
|
|
||||||
|
// Recursively process children
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
this.flattenNodes(child, processedNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate edges from parent-child relationships in mindmap tree
|
||||||
|
* @param node - The mindmap node to process
|
||||||
|
* @param edges - Array to collect edges
|
||||||
|
*/
|
||||||
|
public generateEdges(node: MindmapNode, edges: MindmapLayoutEdge[]): void {
|
||||||
|
if (!node.children) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const child of node.children) {
|
||||||
|
// Build CSS classes for the edge
|
||||||
|
let edgeClasses = 'edge';
|
||||||
|
|
||||||
|
// Add section-specific classes based on the child's section
|
||||||
|
if (child.section !== undefined) {
|
||||||
|
edgeClasses += ` section-edge-${child.section}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add depth class based on the parent's level + 1 (depth of the edge)
|
||||||
|
const edgeDepth = node.level + 1;
|
||||||
|
edgeClasses += ` edge-depth-${edgeDepth}`;
|
||||||
|
|
||||||
|
const edge: MindmapLayoutEdge = {
|
||||||
|
id: `edge_${node.id}_${child.id}`,
|
||||||
|
start: node.id.toString(),
|
||||||
|
end: child.id.toString(),
|
||||||
|
type: 'normal',
|
||||||
|
curve: 'basis',
|
||||||
|
thickness: 'normal',
|
||||||
|
look: 'default',
|
||||||
|
classes: edgeClasses,
|
||||||
|
// Store mindmap-specific data
|
||||||
|
depth: node.level,
|
||||||
|
section: child.section,
|
||||||
|
};
|
||||||
|
|
||||||
|
edges.push(edge);
|
||||||
|
|
||||||
|
// Recursively process child edges
|
||||||
|
this.generateEdges(child, edges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get structured data for layout algorithms
|
||||||
|
* Following the pattern established by ER diagrams
|
||||||
|
* @returns Structured data containing nodes, edges, and config
|
||||||
|
*/
|
||||||
|
public getData(): LayoutData {
|
||||||
|
const mindmapRoot = this.getMindmap();
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
const userDefinedConfig = getUserDefinedConfig();
|
||||||
|
const hasUserDefinedLayout = userDefinedConfig.layout !== undefined;
|
||||||
|
|
||||||
|
const finalConfig = config;
|
||||||
|
if (!hasUserDefinedLayout) {
|
||||||
|
finalConfig.layout = 'cose-bilkent';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mindmapRoot) {
|
||||||
|
return {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
config: finalConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
log.debug('getData: mindmapRoot', mindmapRoot, config);
|
||||||
|
|
||||||
|
// Assign section numbers to all nodes based on their position relative to root
|
||||||
|
this.assignSections(mindmapRoot);
|
||||||
|
|
||||||
|
// Convert tree structure to flat arrays
|
||||||
|
const processedNodes: MindmapLayoutNode[] = [];
|
||||||
|
const processedEdges: MindmapLayoutEdge[] = [];
|
||||||
|
|
||||||
|
this.flattenNodes(mindmapRoot, processedNodes);
|
||||||
|
this.generateEdges(mindmapRoot, processedEdges);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create shapes map for ELK compatibility
|
||||||
|
const shapes = new Map<string, any>();
|
||||||
|
for (const node of processedNodes) {
|
||||||
|
shapes.set(node.id, {
|
||||||
|
shape: node.shape,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
padding: node.padding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: processedNodes,
|
||||||
|
edges: processedEdges,
|
||||||
|
config: finalConfig,
|
||||||
|
// Store the root node for mindmap-specific layout algorithms
|
||||||
|
rootNode: mindmapRoot,
|
||||||
|
// Properties required by dagre layout algorithm
|
||||||
|
markers: ['point'], // Mindmaps don't use markers
|
||||||
|
direction: 'TB', // Top-to-bottom direction for mindmaps
|
||||||
|
nodeSpacing: 50, // Default spacing between nodes
|
||||||
|
rankSpacing: 50, // Default spacing between ranks
|
||||||
|
// Add shapes for ELK compatibility
|
||||||
|
shapes: Object.fromEntries(shapes),
|
||||||
|
// Additional properties that layout algorithms might expect
|
||||||
|
type: 'mindmap',
|
||||||
|
diagramId: 'mindmap-' + v4(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose logger to grammar
|
||||||
public getLogger() {
|
public getLogger() {
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,200 +1,83 @@
|
|||||||
import cytoscape from 'cytoscape';
|
|
||||||
// @ts-expect-error No types available
|
|
||||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
|
||||||
import { select } from 'd3';
|
|
||||||
import type { MermaidConfig } from '../../config.type.js';
|
|
||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
|
||||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import type { D3Element } from '../../types.js';
|
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||||
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
|
import type { LayoutData } from '../../rendering-util/types.js';
|
||||||
import { drawNode, positionNode } from './svgDraw.js';
|
import type { FilledMindMapNode } from './mindmapTypes.js';
|
||||||
import defaultConfig from '../../defaultConfig.js';
|
import defaultConfig from '../../defaultConfig.js';
|
||||||
import type { MindmapDB } from './mindmapDb.js';
|
import type { MindmapDB } from './mindmapDb.js';
|
||||||
// Inject the layout algorithm into cytoscape
|
|
||||||
cytoscape.use(coseBilkent);
|
|
||||||
|
|
||||||
async function drawNodes(
|
/**
|
||||||
db: MindmapDB,
|
* Update the layout data with actual node dimensions after drawing
|
||||||
svg: D3Element,
|
*/
|
||||||
mindmap: FilledMindMapNode,
|
function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) {
|
||||||
section: number,
|
const updateNode = (node: FilledMindMapNode) => {
|
||||||
conf: MermaidConfig
|
// Find the corresponding node in the layout data
|
||||||
) {
|
const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString());
|
||||||
await drawNode(db, svg, mindmap, section, conf);
|
if (layoutNode) {
|
||||||
if (mindmap.children) {
|
// Update with the actual dimensions calculated by drawNode
|
||||||
await Promise.all(
|
layoutNode.width = node.width;
|
||||||
mindmap.children.map((child, index) =>
|
layoutNode.height = node.height;
|
||||||
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height);
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'cytoscape' {
|
|
||||||
interface EdgeSingular {
|
|
||||||
_private: {
|
|
||||||
bodyBounds: unknown;
|
|
||||||
rscratch: {
|
|
||||||
startX: number;
|
|
||||||
startY: number;
|
|
||||||
midX: number;
|
|
||||||
midY: number;
|
|
||||||
endX: number;
|
|
||||||
endY: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
|
||||||
cy.edges().map((edge, id) => {
|
|
||||||
const data = edge.data();
|
|
||||||
if (edge[0]._private.bodyBounds) {
|
|
||||||
const bounds = edge[0]._private.rscratch;
|
|
||||||
log.trace('Edge: ', id, data);
|
|
||||||
edgesEl
|
|
||||||
.insert('path')
|
|
||||||
.attr(
|
|
||||||
'd',
|
|
||||||
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
|
||||||
)
|
|
||||||
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
// Recursively update children
|
||||||
cy.add({
|
node.children?.forEach(updateNode);
|
||||||
group: 'nodes',
|
};
|
||||||
data: {
|
|
||||||
id: mindmap.id.toString(),
|
|
||||||
labelText: mindmap.descr,
|
|
||||||
height: mindmap.height,
|
|
||||||
width: mindmap.width,
|
|
||||||
level: level,
|
|
||||||
nodeId: mindmap.id,
|
|
||||||
padding: mindmap.padding,
|
|
||||||
type: mindmap.type,
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
x: mindmap.x!,
|
|
||||||
y: mindmap.y!,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (mindmap.children) {
|
|
||||||
mindmap.children.forEach((child) => {
|
|
||||||
addNodes(child, cy, conf, level + 1);
|
|
||||||
cy.add({
|
|
||||||
group: 'edges',
|
|
||||||
data: {
|
|
||||||
id: `${mindmap.id}_${child.id}`,
|
|
||||||
source: mindmap.id,
|
|
||||||
target: child.id,
|
|
||||||
depth: level,
|
|
||||||
section: child.section,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
updateNode(mindmapRoot);
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Add temporary render element
|
|
||||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
|
||||||
const cy = cytoscape({
|
|
||||||
container: document.getElementById('cy'), // container to render in
|
|
||||||
style: [
|
|
||||||
{
|
|
||||||
selector: 'edge',
|
|
||||||
style: {
|
|
||||||
'curve-style': 'bezier',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
// Remove element after layout
|
|
||||||
renderEl.remove();
|
|
||||||
addNodes(node, cy, conf, 0);
|
|
||||||
|
|
||||||
// Make cytoscape care about the dimensions of the nodes
|
|
||||||
cy.nodes().forEach(function (n) {
|
|
||||||
n.layoutDimensions = () => {
|
|
||||||
const data = n.data();
|
|
||||||
return { w: data.width, h: data.height };
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.layout({
|
|
||||||
name: 'cose-bilkent',
|
|
||||||
// @ts-ignore Types for cose-bilkent are not correct?
|
|
||||||
quality: 'proof',
|
|
||||||
styleEnabled: false,
|
|
||||||
animate: false,
|
|
||||||
}).run();
|
|
||||||
cy.ready((e) => {
|
|
||||||
log.info('Ready', e);
|
|
||||||
resolve(cy);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
|
||||||
cy.nodes().map((node, id) => {
|
|
||||||
const data = node.data();
|
|
||||||
data.x = node.position().x;
|
|
||||||
data.y = node.position().y;
|
|
||||||
positionNode(db, data);
|
|
||||||
const el = db.getElementById(data.nodeId);
|
|
||||||
log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
|
||||||
el.attr(
|
|
||||||
'transform',
|
|
||||||
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
|
|
||||||
);
|
|
||||||
el.attr('attr', `apa-${id})`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||||
log.debug('Rendering mindmap diagram\n' + text);
|
log.debug('Rendering mindmap diagram\n' + text);
|
||||||
|
|
||||||
|
// Draw the nodes first to get their dimensions, then update the layout data
|
||||||
const db = diagObj.db as MindmapDB;
|
const db = diagObj.db as MindmapDB;
|
||||||
|
|
||||||
|
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||||
|
// into the Layout data format
|
||||||
|
const data4Layout = db.getData();
|
||||||
|
|
||||||
|
// Create the root SVG - the element is the div containing the SVG element
|
||||||
|
const svg = getDiagramElement(id, data4Layout.config.securityLevel);
|
||||||
|
|
||||||
|
data4Layout.type = diagObj.type;
|
||||||
|
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(data4Layout.config.layout, {
|
||||||
|
fallback: 'cose-bilkent',
|
||||||
|
});
|
||||||
|
|
||||||
|
data4Layout.diagramId = id;
|
||||||
|
|
||||||
const mm = db.getMindmap();
|
const mm = db.getMindmap();
|
||||||
if (!mm) {
|
if (!mm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const conf = getConfig();
|
data4Layout.nodes.forEach((node) => {
|
||||||
conf.htmlLabels = false;
|
if (node.shape === 'rounded') {
|
||||||
|
node.radius = 15;
|
||||||
|
node.taper = 15;
|
||||||
|
node.stroke = 'none';
|
||||||
|
node.width = 0;
|
||||||
|
node.padding = 15;
|
||||||
|
} else if (node.shape === 'circle') {
|
||||||
|
node.padding = 10;
|
||||||
|
} else if (node.shape === 'rect') {
|
||||||
|
node.width = 0;
|
||||||
|
node.padding = 10;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const svg = selectSvgElement(id);
|
// Use the unified rendering system
|
||||||
|
await render(data4Layout, svg);
|
||||||
|
|
||||||
// Draw the graph and start with drawing the nodes without proper position
|
// Setup the view box and size of the svg element using config from data4Layout
|
||||||
// this gives us the size of the nodes and we can set the positions later
|
setupViewPortForSVG(
|
||||||
|
|
||||||
const edgesElem = svg.append('g');
|
|
||||||
edgesElem.attr('class', 'mindmap-edges');
|
|
||||||
const nodesElem = svg.append('g');
|
|
||||||
nodesElem.attr('class', 'mindmap-nodes');
|
|
||||||
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
|
||||||
|
|
||||||
// Next step is to layout the mindmap, giving each node a position
|
|
||||||
|
|
||||||
const cy = await layoutMindmap(mm, conf);
|
|
||||||
|
|
||||||
// After this we can draw, first the edges and the then nodes with the correct position
|
|
||||||
drawEdges(edgesElem, cy);
|
|
||||||
positionNodes(db, cy);
|
|
||||||
|
|
||||||
// Setup the view box and size of the svg element
|
|
||||||
setupGraphViewbox(
|
|
||||||
undefined,
|
|
||||||
svg,
|
svg,
|
||||||
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
data4Layout.config.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||||
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
'mindmapDiagram',
|
||||||
|
data4Layout.config.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface MindmapNode {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
|
isRoot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ const getStyles: DiagramStylesProvider = (options) =>
|
|||||||
.section-root text {
|
.section-root text {
|
||||||
fill: ${options.gitBranchLabel0};
|
fill: ${options.gitBranchLabel0};
|
||||||
}
|
}
|
||||||
|
.section-root span {
|
||||||
|
color: ${options.gitBranchLabel0};
|
||||||
|
}
|
||||||
|
.section-2 span {
|
||||||
|
color: ${options.gitBranchLabel0};
|
||||||
|
}
|
||||||
.icon-container {
|
.icon-container {
|
||||||
height:100%;
|
height:100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1368,7 +1368,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
|||||||
it('should handle box without description', async () => {
|
it('should handle box without description', async () => {
|
||||||
const diagram = await Diagram.fromText(`
|
const diagram = await Diagram.fromText(`
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
box Aqua
|
box aqua
|
||||||
participant a as Alice
|
participant a as Alice
|
||||||
participant b as Bob
|
participant b as Bob
|
||||||
end
|
end
|
||||||
@@ -1384,7 +1384,7 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
|||||||
const boxes = diagram.db.getBoxes();
|
const boxes = diagram.db.getBoxes();
|
||||||
expect(boxes[0].name).toBeFalsy();
|
expect(boxes[0].name).toBeFalsy();
|
||||||
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||||
expect(boxes[0].fill).toEqual('Aqua');
|
expect(boxes[0].fill).toEqual('aqua');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle simple actor creation', async () => {
|
it('should handle simple actor creation', async () => {
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ function sidebarConfig() {
|
|||||||
{ text: 'Accessibility', link: '/config/accessibility' },
|
{ text: 'Accessibility', link: '/config/accessibility' },
|
||||||
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
|
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
|
||||||
{ text: 'FAQ', link: '/config/faq' },
|
{ text: 'FAQ', link: '/config/faq' },
|
||||||
|
{ text: 'Layouts', link: '/config/layouts' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import mermaid, { type MermaidConfig } from 'mermaid';
|
import mermaid, { type MermaidConfig } from 'mermaid';
|
||||||
import zenuml from '../../../../../mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
|
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([
|
mermaid.registerIconPacks([
|
||||||
{
|
{
|
||||||
name: 'logos',
|
name: 'logos',
|
||||||
|
|||||||
24
packages/mermaid/src/docs/config/layouts.md
Normal file
24
packages/mermaid/src/docs/config/layouts.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Layouts
|
||||||
|
|
||||||
|
This page lists the available layout algorithms supported in Mermaid diagrams.
|
||||||
|
|
||||||
|
## Supported Layouts
|
||||||
|
|
||||||
|
- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/)
|
||||||
|
- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree)
|
||||||
|
- **cose-bilkent**: Cose Bilkent layout for force-directed graphs
|
||||||
|
- **dagre**: Dagre layout for layered graphs
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
You can specify the layout in your diagram's YAML config or initialization options. For example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: elk
|
||||||
|
---
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
B-->C;
|
||||||
|
```
|
||||||
49
packages/mermaid/src/docs/config/tidy-tree.md
Normal file
49
packages/mermaid/src/docs/config/tidy-tree.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Tidy-tree Layout
|
||||||
|
|
||||||
|
The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Organizes nodes in a tidy, non-overlapping tree
|
||||||
|
- Ideal for mindmaps and hierarchical data
|
||||||
|
- Automatically adjusts spacing for readability
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- Currently, tidy-tree is primarily supported for mindmap diagrams.
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"unocss": "^66.4.2",
|
"unocss": "^66.4.2",
|
||||||
"unplugin-vue-components": "^28.4.0",
|
"unplugin-vue-components": "^28.4.0",
|
||||||
"vite": "^6.1.1",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vitepress": "1.6.3",
|
"vitepress": "1.6.3",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
|
|||||||
@@ -209,3 +209,22 @@ You can also refer the [implementation in the live editor](https://github.com/me
|
|||||||
cspell:locale en,en-gb
|
cspell:locale en,en-gb
|
||||||
cspell:ignore Buzan
|
cspell:ignore Buzan
|
||||||
--->
|
--->
|
||||||
|
|
||||||
|
## Layouts
|
||||||
|
|
||||||
|
Mermaid also supports a Tidy Tree layout for mindmaps.
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
layout: tidy-tree
|
||||||
|
---
|
||||||
|
mindmap
|
||||||
|
root((mindmap is a long thing))
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ Each user journey is split into sections, these describe the part of the task
|
|||||||
the user is trying to complete.
|
the user is trying to complete.
|
||||||
|
|
||||||
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
Tasks syntax is `Task name: <score>: <comma separated list of actors>`
|
||||||
|
|
||||||
|
Score is a number between 1 and 5, inclusive.
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ xychart
|
|||||||
|
|
||||||
## Chart Theme Variables
|
## Chart Theme Variables
|
||||||
|
|
||||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
@@ -151,6 +151,31 @@ config:
|
|||||||
| yAxisLineColor | Color of the y-axis line |
|
| yAxisLineColor | Color of the y-axis line |
|
||||||
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
||||||
|
|
||||||
|
### Setting Colors for Lines and Bars
|
||||||
|
|
||||||
|
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
themeVariables:
|
||||||
|
xyChart:
|
||||||
|
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||||
|
---
|
||||||
|
xychart
|
||||||
|
title "Different Colors in xyChart"
|
||||||
|
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||||
|
y-axis "valuesY" 0 --> 50
|
||||||
|
%% Black line
|
||||||
|
line [10,20,30,40]
|
||||||
|
%% Blue bar
|
||||||
|
bar [20,30,25,35]
|
||||||
|
%% Green bar
|
||||||
|
bar [15,25,20,30]
|
||||||
|
%% Red line
|
||||||
|
line [5,15,25,35]
|
||||||
|
```
|
||||||
|
|
||||||
## Example on config and theme
|
## Example on config and theme
|
||||||
|
|
||||||
```mermaid-example
|
```mermaid-example
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const virtualModuleId = 'virtual:mermaid-config';
|
|||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
// Vite v7 changes the default target and drops old browser support
|
||||||
|
target: 'modules',
|
||||||
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
|
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
|
||||||
// This needs to be excluded from optimization
|
// This needs to be excluded from optimization
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const processFrontmatter = (code: string) => {
|
|||||||
}
|
}
|
||||||
config.gantt.displayMode = displayMode;
|
config.gantt.displayMode = displayMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title, config, text };
|
return { title, config, text };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal file
148
packages/mermaid/src/rendering-util/createGraph.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { insertNode } from './rendering-elements/nodes.js';
|
||||||
|
import type { LayoutData, NonClusterNode } from './types.ts';
|
||||||
|
import type { Selection } from 'd3';
|
||||||
|
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||||
|
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||||
|
|
||||||
|
// Update type:
|
||||||
|
type D3Selection<T extends SVGElement = SVGElement> = Selection<
|
||||||
|
T,
|
||||||
|
unknown,
|
||||||
|
Element | null,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a graph by merging the graph construction and DOM element insertion.
|
||||||
|
*
|
||||||
|
* This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes)
|
||||||
|
* into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions
|
||||||
|
* are computed using each node's bounding box.
|
||||||
|
*
|
||||||
|
* @param element - The D3 selection in which the SVG groups are inserted.
|
||||||
|
* @param data4Layout - The layout data containing nodes and edges.
|
||||||
|
* @returns A promise resolving to an object containing the Graphology graph and the inserted groups.
|
||||||
|
*/
|
||||||
|
export async function createGraphWithElements(
|
||||||
|
element: D3Selection,
|
||||||
|
data4Layout: LayoutData
|
||||||
|
): Promise<{
|
||||||
|
graph: graphlib.Graph;
|
||||||
|
groups: {
|
||||||
|
clusters: D3Selection<SVGGElement>;
|
||||||
|
edgePaths: D3Selection<SVGGElement>;
|
||||||
|
edgeLabels: D3Selection<SVGGElement>;
|
||||||
|
nodes: D3Selection<SVGGElement>;
|
||||||
|
rootGroups: D3Selection<SVGGElement>;
|
||||||
|
};
|
||||||
|
nodeElements: Map<string, D3Selection<SVGElement | SVGGElement>>;
|
||||||
|
}> {
|
||||||
|
// Create a directed, multi graph.
|
||||||
|
const graph = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
});
|
||||||
|
const edgesToProcess = [...data4Layout.edges];
|
||||||
|
const config = getConfig();
|
||||||
|
// Create groups for clusters, edge paths, edge labels, and nodes.
|
||||||
|
const clusters = element.insert('g').attr('class', 'clusters');
|
||||||
|
const edgePaths = element.insert('g').attr('class', 'edges edgePath');
|
||||||
|
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||||
|
const nodesGroup = element.insert('g').attr('class', 'nodes');
|
||||||
|
const rootGroups = element.insert('g').attr('class', 'root');
|
||||||
|
|
||||||
|
const nodeElements = new Map<string, D3Selection<SVGElement | SVGGElement>>();
|
||||||
|
|
||||||
|
// Insert nodes into the DOM and add them to the graph.
|
||||||
|
await Promise.all(
|
||||||
|
data4Layout.nodes.map(async (node) => {
|
||||||
|
if (node.isGroup) {
|
||||||
|
graph.setNode(node.id, { ...node });
|
||||||
|
} else {
|
||||||
|
const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir });
|
||||||
|
const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||||
|
nodeElements.set(node.id, childNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||||
|
node.width = boundingBox.width;
|
||||||
|
node.height = boundingBox.height;
|
||||||
|
graph.setNode(node.id, { ...node });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Add edges to the graph.
|
||||||
|
for (const edge of edgesToProcess) {
|
||||||
|
if (edge.label && edge.label?.length > 0) {
|
||||||
|
// Create a label node for the edge
|
||||||
|
const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`;
|
||||||
|
const labelNode = {
|
||||||
|
id: labelNodeId,
|
||||||
|
label: edge.label,
|
||||||
|
edgeStart: edge.start,
|
||||||
|
edgeEnd: edge.end,
|
||||||
|
shape: 'labelRect',
|
||||||
|
width: 0, // Will be updated after insertion
|
||||||
|
height: 0, // Will be updated after insertion
|
||||||
|
isEdgeLabel: true,
|
||||||
|
isDummy: true,
|
||||||
|
isGroup: false,
|
||||||
|
parentId: edge.parentId,
|
||||||
|
...(edge.dir ? { dir: edge.dir } : {}),
|
||||||
|
} as NonClusterNode;
|
||||||
|
|
||||||
|
// Insert the label node into the DOM
|
||||||
|
const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: edge.dir });
|
||||||
|
const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||||
|
|
||||||
|
// Update node dimensions
|
||||||
|
labelNode.width = boundingBox.width;
|
||||||
|
labelNode.height = boundingBox.height;
|
||||||
|
|
||||||
|
// Add to graph and tracking maps
|
||||||
|
graph.setNode(labelNodeId, { ...labelNode });
|
||||||
|
nodeElements.set(labelNodeId, labelNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||||
|
data4Layout.nodes.push(labelNode);
|
||||||
|
|
||||||
|
// Create two edges to replace the original one
|
||||||
|
const edgeToLabel = {
|
||||||
|
...edge,
|
||||||
|
id: `${edge.id}-to-label`,
|
||||||
|
end: labelNodeId,
|
||||||
|
label: undefined,
|
||||||
|
isLabelEdge: true,
|
||||||
|
arrowTypeEnd: 'none',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
};
|
||||||
|
const edgeFromLabel = {
|
||||||
|
...edge,
|
||||||
|
id: `${edge.id}-from-label`,
|
||||||
|
start: labelNodeId,
|
||||||
|
end: edge.end,
|
||||||
|
label: undefined,
|
||||||
|
isLabelEdge: true,
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
};
|
||||||
|
graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel });
|
||||||
|
graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel });
|
||||||
|
data4Layout.edges.push(edgeToLabel, edgeFromLabel);
|
||||||
|
const edgeIdToRemove = edge.id;
|
||||||
|
data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove);
|
||||||
|
const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id);
|
||||||
|
if (indexInOriginal !== -1) {
|
||||||
|
data4Layout.edges.splice(indexInOriginal, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular edge without label
|
||||||
|
graph.setEdge(edge.id, edge.start, edge.end, { ...edge });
|
||||||
|
const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id);
|
||||||
|
if (!edgeExists) {
|
||||||
|
data4Layout.edges.push(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
graph,
|
||||||
|
groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups },
|
||||||
|
nodeElements,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
addNodes,
|
||||||
|
addEdges,
|
||||||
|
extractPositionedNodes,
|
||||||
|
extractPositionedEdges,
|
||||||
|
} from './cytoscape-setup.js';
|
||||||
|
import type { Node, Edge } from '../../types.js';
|
||||||
|
|
||||||
|
// Mock cytoscape
|
||||||
|
const mockCy = {
|
||||||
|
add: vi.fn(),
|
||||||
|
nodes: vi.fn(),
|
||||||
|
edges: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('cytoscape', () => {
|
||||||
|
const mockCytoscape = vi.fn(() => mockCy) as any;
|
||||||
|
mockCytoscape.use = vi.fn();
|
||||||
|
return {
|
||||||
|
default: mockCytoscape,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cytoscape Setup', () => {
|
||||||
|
let mockNodes: Node[];
|
||||||
|
let mockEdges: Edge[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockNodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
label: 'Root',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
label: 'Child 1',
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 150,
|
||||||
|
y: 150,
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockEdges = [
|
||||||
|
{
|
||||||
|
id: '1_2',
|
||||||
|
start: '1',
|
||||||
|
end: '2',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addNodes', () => {
|
||||||
|
it('should add nodes to cytoscape', () => {
|
||||||
|
addNodes([mockNodes[0]], mockCy as unknown as any);
|
||||||
|
|
||||||
|
expect(mockCy.add).toHaveBeenCalledWith({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: '1',
|
||||||
|
labelText: 'Root',
|
||||||
|
height: 50,
|
||||||
|
width: 100,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add multiple nodes to cytoscape', () => {
|
||||||
|
addNodes(mockNodes, mockCy as unknown as any);
|
||||||
|
|
||||||
|
expect(mockCy.add).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
expect(mockCy.add).toHaveBeenCalledWith({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: '1',
|
||||||
|
labelText: 'Root',
|
||||||
|
height: 50,
|
||||||
|
width: 100,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCy.add).toHaveBeenCalledWith({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: '2',
|
||||||
|
labelText: 'Child 1',
|
||||||
|
height: 40,
|
||||||
|
width: 80,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
cssClasses: '',
|
||||||
|
cssStyles: [],
|
||||||
|
look: 'default',
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 150,
|
||||||
|
y: 150,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addEdges', () => {
|
||||||
|
it('should add edges to cytoscape', () => {
|
||||||
|
addEdges(mockEdges, mockCy as unknown as any);
|
||||||
|
|
||||||
|
expect(mockCy.add).toHaveBeenCalledWith({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: '1_2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
type: 'edge',
|
||||||
|
classes: '',
|
||||||
|
style: [],
|
||||||
|
animate: false,
|
||||||
|
arrowTypeEnd: 'arrow_point',
|
||||||
|
arrowTypeStart: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractPositionedNodes', () => {
|
||||||
|
it('should extract positioned nodes from cytoscape', () => {
|
||||||
|
const mockCytoscapeNodes = [
|
||||||
|
{
|
||||||
|
data: () => ({
|
||||||
|
id: '1',
|
||||||
|
labelText: 'Root',
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
}),
|
||||||
|
position: () => ({ x: 100, y: 100 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: () => ({
|
||||||
|
id: '2',
|
||||||
|
labelText: 'Child 1',
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
}),
|
||||||
|
position: () => ({ x: 150, y: 150 }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCy.nodes.mockReturnValue({
|
||||||
|
map: (fn: unknown) => mockCytoscapeNodes.map(fn as any),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = extractPositionedNodes(mockCy as unknown as any);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
id: '1',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
labelText: 'Root',
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
shape: 'rect',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractPositionedEdges', () => {
|
||||||
|
it('should extract positioned edges from cytoscape', () => {
|
||||||
|
const mockCytoscapeEdges = [
|
||||||
|
{
|
||||||
|
data: () => ({
|
||||||
|
id: '1_2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
type: 'edge',
|
||||||
|
}),
|
||||||
|
_private: {
|
||||||
|
rscratch: {
|
||||||
|
startX: 100,
|
||||||
|
startY: 100,
|
||||||
|
midX: 125,
|
||||||
|
midY: 125,
|
||||||
|
endX: 150,
|
||||||
|
endY: 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCy.edges.mockReturnValue({
|
||||||
|
map: (fn: unknown) => mockCytoscapeEdges.map(fn as any),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = extractPositionedEdges(mockCy as unknown as any);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
id: '1_2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
type: 'edge',
|
||||||
|
startX: 100,
|
||||||
|
startY: 100,
|
||||||
|
midX: 125,
|
||||||
|
midY: 125,
|
||||||
|
endX: 150,
|
||||||
|
endY: 150,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import cytoscape from 'cytoscape';
|
||||||
|
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||||
|
import { select } from 'd3';
|
||||||
|
import { log } from '../../../logger.js';
|
||||||
|
import type { LayoutData, Node, Edge } from '../../types.js';
|
||||||
|
import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js';
|
||||||
|
|
||||||
|
// Inject the layout algorithm into cytoscape
|
||||||
|
cytoscape.use(coseBilkent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare module augmentation for cytoscape edge types
|
||||||
|
*/
|
||||||
|
declare module 'cytoscape' {
|
||||||
|
interface EdgeSingular {
|
||||||
|
_private: {
|
||||||
|
bodyBounds: unknown;
|
||||||
|
rscratch: {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
midX: number;
|
||||||
|
midY: number;
|
||||||
|
endX: number;
|
||||||
|
endY: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add nodes to cytoscape instance from provided node array
|
||||||
|
* This function processes only the nodes provided in the data structure
|
||||||
|
* @param nodes - Array of nodes to add
|
||||||
|
* @param cy - The cytoscape instance
|
||||||
|
*/
|
||||||
|
export function addNodes(nodes: Node[], cy: cytoscape.Core): void {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const nodeData: Record<string, unknown> = {
|
||||||
|
id: node.id,
|
||||||
|
labelText: node.label,
|
||||||
|
height: node.height,
|
||||||
|
width: node.width,
|
||||||
|
padding: node.padding ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add any additional properties from the node
|
||||||
|
Object.keys(node).forEach((key) => {
|
||||||
|
if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) {
|
||||||
|
nodeData[key] = (node as unknown as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.add({
|
||||||
|
group: 'nodes',
|
||||||
|
data: nodeData,
|
||||||
|
position: {
|
||||||
|
x: node.x ?? 0,
|
||||||
|
y: node.y ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add edges to cytoscape instance from provided edge array
|
||||||
|
* This function processes only the edges provided in the data structure
|
||||||
|
* @param edges - Array of edges to add
|
||||||
|
* @param cy - The cytoscape instance
|
||||||
|
*/
|
||||||
|
export function addEdges(edges: Edge[], cy: cytoscape.Core): void {
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
const edgeData: Record<string, unknown> = {
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.start,
|
||||||
|
target: edge.end,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add any additional properties from the edge
|
||||||
|
Object.keys(edge).forEach((key) => {
|
||||||
|
if (!['id', 'start', 'end'].includes(key)) {
|
||||||
|
edgeData[key] = (edge as unknown as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.add({
|
||||||
|
group: 'edges',
|
||||||
|
data: edgeData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and configure cytoscape instance
|
||||||
|
* @param data - Layout data containing nodes and edges
|
||||||
|
* @returns Promise resolving to configured cytoscape instance
|
||||||
|
*/
|
||||||
|
export function createCytoscapeInstance(data: LayoutData): Promise<cytoscape.Core> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Add temporary render element
|
||||||
|
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||||
|
|
||||||
|
const cy = cytoscape({
|
||||||
|
container: document.getElementById('cy'), // container to render in
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove element after layout
|
||||||
|
renderEl.remove();
|
||||||
|
|
||||||
|
// Add all nodes and edges to cytoscape using the generic functions
|
||||||
|
addNodes(data.nodes, cy);
|
||||||
|
addEdges(data.edges, cy);
|
||||||
|
|
||||||
|
// Make cytoscape care about the dimensions of the nodes
|
||||||
|
cy.nodes().forEach(function (n) {
|
||||||
|
n.layoutDimensions = () => {
|
||||||
|
const nodeData = n.data();
|
||||||
|
return { w: nodeData.width, h: nodeData.height };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure and run the cose-bilkent layout
|
||||||
|
const layoutConfig: CytoscapeLayoutConfig = {
|
||||||
|
name: 'cose-bilkent',
|
||||||
|
// @ts-ignore Types for cose-bilkent are not correct?
|
||||||
|
quality: 'proof',
|
||||||
|
styleEnabled: false,
|
||||||
|
animate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.layout(layoutConfig).run();
|
||||||
|
|
||||||
|
cy.ready((e) => {
|
||||||
|
log.info('Cytoscape ready', e);
|
||||||
|
resolve(cy);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract positioned nodes from cytoscape instance
|
||||||
|
* @param cy - The cytoscape instance after layout
|
||||||
|
* @returns Array of positioned nodes
|
||||||
|
*/
|
||||||
|
export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] {
|
||||||
|
return cy.nodes().map((node) => {
|
||||||
|
const data = node.data();
|
||||||
|
const position = node.position();
|
||||||
|
|
||||||
|
// Create a positioned node with all original data plus position
|
||||||
|
const positionedNode: PositionedNode = {
|
||||||
|
id: data.id,
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all other properties from the original data
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (key !== 'id') {
|
||||||
|
positionedNode[key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return positionedNode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract positioned edges from cytoscape instance
|
||||||
|
* @param cy - The cytoscape instance after layout
|
||||||
|
* @returns Array of positioned edges
|
||||||
|
*/
|
||||||
|
export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] {
|
||||||
|
return cy.edges().map((edge) => {
|
||||||
|
const data = edge.data();
|
||||||
|
const rscratch = edge._private.rscratch;
|
||||||
|
|
||||||
|
// Create a positioned edge with all original data plus position
|
||||||
|
const positionedEdge: PositionedEdge = {
|
||||||
|
id: data.id,
|
||||||
|
source: data.source,
|
||||||
|
target: data.target,
|
||||||
|
startX: rscratch.startX,
|
||||||
|
startY: rscratch.startY,
|
||||||
|
midX: rscratch.midX,
|
||||||
|
midY: rscratch.midY,
|
||||||
|
endX: rscratch.endX,
|
||||||
|
endY: rscratch.endY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all other properties from the original data
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (!['id', 'source', 'target'].includes(key)) {
|
||||||
|
positionedEdge[key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return positionedEdge;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { render as renderWithCoseBilkent } from './render.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cose-Bilkent Layout Algorithm for Generic Diagrams
|
||||||
|
*
|
||||||
|
* This module provides a layout algorithm implementation using Cytoscape
|
||||||
|
* with the cose-bilkent algorithm for positioning nodes and edges.
|
||||||
|
*
|
||||||
|
* The algorithm follows the unified rendering pattern and can be used
|
||||||
|
* by any diagram type that provides compatible LayoutData.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render function for the cose-bilkent layout algorithm
|
||||||
|
*
|
||||||
|
* This function follows the unified rendering pattern used by all layout algorithms.
|
||||||
|
* It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
|
||||||
|
* and renders the positioned elements to the SVG.
|
||||||
|
*
|
||||||
|
* @param layoutData - Layout data containing nodes, edges, and configuration
|
||||||
|
* @param svg - SVG element to render to
|
||||||
|
* @param helpers - Internal helper functions for rendering
|
||||||
|
* @param options - Rendering options
|
||||||
|
*/
|
||||||
|
export const render = renderWithCoseBilkent;
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { validateLayoutData, executeCoseBilkentLayout } from './layout.js';
|
||||||
|
import type { LayoutResult } from './types.js';
|
||||||
|
import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js';
|
||||||
|
import type { MermaidConfig } from '../../../config.type.js';
|
||||||
|
import type { LayoutData } from '../../types.js';
|
||||||
|
|
||||||
|
// Mock cytoscape and cytoscape-cose-bilkent before importing the modules
|
||||||
|
|
||||||
|
vi.mock('cytoscape', () => {
|
||||||
|
const mockCy = {
|
||||||
|
add: vi.fn(),
|
||||||
|
nodes: vi.fn(() => ({
|
||||||
|
forEach: vi.fn(),
|
||||||
|
map: vi.fn((fn) => [
|
||||||
|
fn({
|
||||||
|
data: () => ({
|
||||||
|
id: '1',
|
||||||
|
nodeId: '1',
|
||||||
|
labelText: 'Root',
|
||||||
|
level: 0,
|
||||||
|
type: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
}),
|
||||||
|
position: () => ({ x: 100, y: 100 }),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})),
|
||||||
|
edges: vi.fn(() => ({
|
||||||
|
map: vi.fn((fn) => [
|
||||||
|
fn({
|
||||||
|
data: () => ({
|
||||||
|
id: '1_2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
depth: 0,
|
||||||
|
}),
|
||||||
|
_private: {
|
||||||
|
rscratch: {
|
||||||
|
startX: 100,
|
||||||
|
startY: 100,
|
||||||
|
midX: 150,
|
||||||
|
midY: 150,
|
||||||
|
endX: 200,
|
||||||
|
endY: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})),
|
||||||
|
layout: vi.fn(() => ({
|
||||||
|
run: vi.fn(),
|
||||||
|
})),
|
||||||
|
ready: vi.fn((callback) => callback({})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCytoscape = vi.fn(() => mockCy);
|
||||||
|
(mockCytoscape as any).use = vi.fn();
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: mockCytoscape,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cose-Bilkent Layout Algorithm', () => {
|
||||||
|
let mockConfig: MermaidConfig;
|
||||||
|
let mockRootNode: MindmapNode;
|
||||||
|
let mockLayoutData: LayoutData;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockConfig = {
|
||||||
|
mindmap: {
|
||||||
|
layoutAlgorithm: 'cose-bilkent',
|
||||||
|
padding: 10,
|
||||||
|
maxNodeWidth: 200,
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
} as MermaidConfig;
|
||||||
|
|
||||||
|
mockRootNode = {
|
||||||
|
id: 1,
|
||||||
|
nodeId: '1',
|
||||||
|
level: 0,
|
||||||
|
descr: 'Root',
|
||||||
|
type: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
nodeId: '2',
|
||||||
|
level: 1,
|
||||||
|
descr: 'Child 1',
|
||||||
|
type: 0,
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as MindmapNode;
|
||||||
|
|
||||||
|
mockLayoutData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
nodeId: '1',
|
||||||
|
level: 0,
|
||||||
|
descr: 'Root',
|
||||||
|
type: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
nodeId: '2',
|
||||||
|
level: 1,
|
||||||
|
descr: 'Child 1',
|
||||||
|
type: 0,
|
||||||
|
width: 80,
|
||||||
|
height: 40,
|
||||||
|
padding: 10,
|
||||||
|
isGroup: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: '1_2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
depth: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
config: mockConfig,
|
||||||
|
rootNode: mockRootNode,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateLayoutData', () => {
|
||||||
|
it('should validate correct layout data', () => {
|
||||||
|
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing data', () => {
|
||||||
|
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing root node', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, rootNode: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Root node is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for missing config', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, config: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid nodes array', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, nodes: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('No nodes found in layout data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid edges array', () => {
|
||||||
|
const invalidData = { ...mockLayoutData, edges: null as any };
|
||||||
|
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('layout function', () => {
|
||||||
|
it('should execute layout algorithm successfully', async () => {
|
||||||
|
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toBeDefined();
|
||||||
|
expect(result.edges).toBeDefined();
|
||||||
|
expect(Array.isArray(result.nodes)).toBe(true);
|
||||||
|
expect(Array.isArray(result.edges)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return positioned nodes with coordinates', async () => {
|
||||||
|
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
|
||||||
|
|
||||||
|
expect(result.nodes.length).toBeGreaterThan(0);
|
||||||
|
result.nodes.forEach((node) => {
|
||||||
|
expect(node.x).toBeDefined();
|
||||||
|
expect(node.y).toBeDefined();
|
||||||
|
expect(typeof node.x).toBe('number');
|
||||||
|
expect(typeof node.y).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return positioned edges with coordinates', async () => {
|
||||||
|
const result: LayoutResult = await executeCoseBilkentLayout(mockLayoutData, mockConfig);
|
||||||
|
|
||||||
|
expect(result.edges.length).toBeGreaterThan(0);
|
||||||
|
result.edges.forEach((edge) => {
|
||||||
|
expect(edge.startX).toBeDefined();
|
||||||
|
expect(edge.startY).toBeDefined();
|
||||||
|
expect(edge.midX).toBeDefined();
|
||||||
|
expect(edge.midY).toBeDefined();
|
||||||
|
expect(edge.endX).toBeDefined();
|
||||||
|
expect(edge.endY).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty mindmap data gracefully', async () => {
|
||||||
|
const emptyData: LayoutData = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
config: mockConfig,
|
||||||
|
rootNode: mockRootNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: LayoutResult = await executeCoseBilkentLayout(emptyData, mockConfig);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.nodes).toBeDefined();
|
||||||
|
expect(result.edges).toBeDefined();
|
||||||
|
expect(Array.isArray(result.nodes)).toBe(true);
|
||||||
|
expect(Array.isArray(result.edges)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid data', async () => {
|
||||||
|
const invalidData = { ...mockLayoutData, rootNode: null as any };
|
||||||
|
|
||||||
|
await expect(executeCoseBilkentLayout(invalidData, mockConfig)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { MermaidConfig } from '../../../config.type.js';
|
||||||
|
import { log } from '../../../logger.js';
|
||||||
|
import type { LayoutData } from '../../types.js';
|
||||||
|
import type { LayoutResult } from './types.js';
|
||||||
|
import {
|
||||||
|
createCytoscapeInstance,
|
||||||
|
extractPositionedNodes,
|
||||||
|
extractPositionedEdges,
|
||||||
|
} from './cytoscape-setup.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the cose-bilkent layout algorithm on generic layout data
|
||||||
|
*
|
||||||
|
* This function takes layout data and uses Cytoscape with the cose-bilkent
|
||||||
|
* algorithm to calculate optimal node positions and edge paths.
|
||||||
|
*
|
||||||
|
* @param data - The layout data containing nodes, edges, and configuration
|
||||||
|
* @param config - Mermaid configuration object
|
||||||
|
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||||
|
*/
|
||||||
|
export async function executeCoseBilkentLayout(
|
||||||
|
data: LayoutData,
|
||||||
|
_config: MermaidConfig
|
||||||
|
): Promise<LayoutResult> {
|
||||||
|
log.debug('Starting cose-bilkent layout algorithm');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate layout data structure
|
||||||
|
validateLayoutData(data);
|
||||||
|
|
||||||
|
// Create and configure cytoscape instance
|
||||||
|
const cy = await createCytoscapeInstance(data);
|
||||||
|
|
||||||
|
// Extract positioned nodes and edges after layout
|
||||||
|
const positionedNodes = extractPositionedNodes(cy);
|
||||||
|
const positionedEdges = extractPositionedEdges(cy);
|
||||||
|
|
||||||
|
log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: positionedNodes,
|
||||||
|
edges: positionedEdges,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error in cose-bilkent layout algorithm:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate layout data structure
|
||||||
|
* @param data - The data to validate
|
||||||
|
* @returns True if data is valid, throws error otherwise
|
||||||
|
*/
|
||||||
|
export function validateLayoutData(data: LayoutData): boolean {
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('Layout data is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.config) {
|
||||||
|
throw new Error('Configuration is required in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.rootNode) {
|
||||||
|
throw new Error('Root node is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.nodes || !Array.isArray(data.nodes)) {
|
||||||
|
throw new Error('No nodes found in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(data.edges)) {
|
||||||
|
throw new Error('Edges array is required in layout data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
|
||||||
|
import { executeCoseBilkentLayout } from './layout.js';
|
||||||
|
import type { D3Selection } from '../../../types.js';
|
||||||
|
|
||||||
|
type Node = Record<string, unknown>;
|
||||||
|
|
||||||
|
interface NodeWithPosition extends Node {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
domId?: string | SVGGroup | D3Selection<SVGAElement>;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render function for cose-bilkent layout algorithm
|
||||||
|
*
|
||||||
|
* This follows the same pattern as ELK and dagre renderers:
|
||||||
|
* 1. Insert nodes into DOM to get their actual dimensions
|
||||||
|
* 2. Run the layout algorithm to calculate positions
|
||||||
|
* 3. Position the nodes and edges based on layout results
|
||||||
|
*/
|
||||||
|
export const render = async (
|
||||||
|
data4Layout: LayoutData,
|
||||||
|
svg: SVG,
|
||||||
|
{
|
||||||
|
insertCluster,
|
||||||
|
insertEdge,
|
||||||
|
insertEdgeLabel,
|
||||||
|
insertMarkers,
|
||||||
|
insertNode,
|
||||||
|
log,
|
||||||
|
positionEdgeLabel,
|
||||||
|
}: InternalHelpers,
|
||||||
|
{ algorithm: _algorithm }: RenderOptions
|
||||||
|
) => {
|
||||||
|
const nodeDb: Record<string, NodeWithPosition> = {};
|
||||||
|
const clusterDb: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Insert markers for edges
|
||||||
|
const element = svg.select('g');
|
||||||
|
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||||
|
|
||||||
|
// Create container groups
|
||||||
|
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
|
||||||
|
const edgePaths = element.insert('g').attr('class', 'edgePaths');
|
||||||
|
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||||
|
const nodes = element.insert('g').attr('class', 'nodes');
|
||||||
|
|
||||||
|
// Step 1: Insert nodes into DOM to get their actual dimensions
|
||||||
|
log.debug('Inserting nodes into DOM for dimension calculation');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
data4Layout.nodes.map(async (node) => {
|
||||||
|
if (node.isGroup) {
|
||||||
|
// Handle subgraphs/clusters
|
||||||
|
const clusterNode: NodeWithPosition = { ...node };
|
||||||
|
clusterDb[node.id] = clusterNode;
|
||||||
|
nodeDb[node.id] = clusterNode;
|
||||||
|
|
||||||
|
// Insert cluster to get dimensions
|
||||||
|
await insertCluster(subGraphsEl, node);
|
||||||
|
} else {
|
||||||
|
// Handle regular nodes
|
||||||
|
const nodeWithPosition: NodeWithPosition = { ...node };
|
||||||
|
nodeDb[node.id] = nodeWithPosition;
|
||||||
|
|
||||||
|
// Insert node to get actual dimensions
|
||||||
|
const nodeEl = await insertNode(nodes, node, {
|
||||||
|
config: data4Layout.config,
|
||||||
|
dir: data4Layout.direction || 'TB',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the actual bounding box after insertion
|
||||||
|
const boundingBox = nodeEl.node()!.getBBox();
|
||||||
|
nodeWithPosition.width = boundingBox.width;
|
||||||
|
nodeWithPosition.height = boundingBox.height;
|
||||||
|
nodeWithPosition.domId = nodeEl;
|
||||||
|
|
||||||
|
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Run the cose-bilkent layout algorithm
|
||||||
|
log.debug('Running cose-bilkent layout algorithm');
|
||||||
|
|
||||||
|
// Update the layout data with actual dimensions
|
||||||
|
const updatedLayoutData = {
|
||||||
|
...data4Layout,
|
||||||
|
nodes: data4Layout.nodes.map((node) => {
|
||||||
|
const nodeWithDimensions = nodeDb[node.id];
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
width: nodeWithDimensions.width,
|
||||||
|
height: nodeWithDimensions.height,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
|
||||||
|
|
||||||
|
// Step 3: Position the nodes based on layout results
|
||||||
|
log.debug('Positioning nodes based on layout results');
|
||||||
|
|
||||||
|
layoutResult.nodes.forEach((positionedNode) => {
|
||||||
|
const node = nodeDb[positionedNode.id];
|
||||||
|
if (node?.domId) {
|
||||||
|
// Position the node at the calculated coordinates
|
||||||
|
// The positionedNode.x/y represents the center of the node, so use directly
|
||||||
|
(node.domId as D3Selection<SVGAElement>).attr(
|
||||||
|
'transform',
|
||||||
|
`translate(${positionedNode.x}, ${positionedNode.y})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the final position
|
||||||
|
node.x = positionedNode.x;
|
||||||
|
node.y = positionedNode.y;
|
||||||
|
|
||||||
|
log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutResult.edges.forEach((positionedEdge) => {
|
||||||
|
const edge = data4Layout.edges.find((e) => e.id === positionedEdge.id);
|
||||||
|
if (edge) {
|
||||||
|
// Update the edge data with positioned coordinates
|
||||||
|
edge.points = [
|
||||||
|
{ x: positionedEdge.startX, y: positionedEdge.startY },
|
||||||
|
{ x: positionedEdge.midX, y: positionedEdge.midY },
|
||||||
|
{ x: positionedEdge.endX, y: positionedEdge.endY },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4: Insert and position edges
|
||||||
|
log.debug('Inserting and positioning edges');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
data4Layout.edges.map(async (edge) => {
|
||||||
|
// Insert edge label first
|
||||||
|
const _edgeLabel = await insertEdgeLabel(edgeLabels, edge);
|
||||||
|
|
||||||
|
// Get start and end nodes
|
||||||
|
const startNode = nodeDb[edge.start ?? ''];
|
||||||
|
const endNode = nodeDb[edge.end ?? ''];
|
||||||
|
|
||||||
|
if (startNode && endNode) {
|
||||||
|
// Find the positioned edge data
|
||||||
|
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||||
|
|
||||||
|
if (positionedEdge) {
|
||||||
|
log.debug('APA01 positionedEdge', positionedEdge);
|
||||||
|
// Create edge path with positioned coordinates
|
||||||
|
const edgeWithPath = { ...edge };
|
||||||
|
|
||||||
|
// Insert the edge path
|
||||||
|
const paths = insertEdge(
|
||||||
|
edgePaths,
|
||||||
|
edgeWithPath,
|
||||||
|
clusterDb,
|
||||||
|
data4Layout.type,
|
||||||
|
startNode,
|
||||||
|
endNode,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Position the edge label
|
||||||
|
positionEdgeLabel(edgeWithPath, paths);
|
||||||
|
} else {
|
||||||
|
// Fallback: create a simple straight line between nodes
|
||||||
|
const edgeWithPath = {
|
||||||
|
...edge,
|
||||||
|
points: [
|
||||||
|
{ x: startNode.x || 0, y: startNode.y || 0 },
|
||||||
|
{ x: endNode.x || 0, y: endNode.y || 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const paths = insertEdge(
|
||||||
|
edgePaths,
|
||||||
|
edgeWithPath,
|
||||||
|
clusterDb,
|
||||||
|
data4Layout.type,
|
||||||
|
startNode,
|
||||||
|
endNode,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
positionEdgeLabel(edgeWithPath, paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
log.debug('Cose-bilkent rendering completed');
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Positioned node after layout calculation
|
||||||
|
*/
|
||||||
|
export interface PositionedNode {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
[key: string]: unknown; // Allow additional properties
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positioned edge after layout calculation
|
||||||
|
*/
|
||||||
|
export interface PositionedEdge {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
midX: number;
|
||||||
|
midY: number;
|
||||||
|
endX: number;
|
||||||
|
endY: number;
|
||||||
|
[key: string]: unknown; // Allow additional properties
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of layout algorithm execution
|
||||||
|
*/
|
||||||
|
export interface LayoutResult {
|
||||||
|
nodes: PositionedNode[];
|
||||||
|
edges: PositionedEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cytoscape layout configuration
|
||||||
|
*/
|
||||||
|
export interface CytoscapeLayoutConfig {
|
||||||
|
name: 'cose-bilkent';
|
||||||
|
quality: 'proof';
|
||||||
|
styleEnabled: boolean;
|
||||||
|
animate: boolean;
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import { internalHelpers } from '../internals.js';
|
|||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
import type { LayoutData } from './types.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 {
|
export interface RenderOptions {
|
||||||
algorithm?: string;
|
algorithm?: string;
|
||||||
}
|
}
|
||||||
@@ -39,6 +42,14 @@ const registerDefaultLayoutLoaders = () => {
|
|||||||
name: 'dagre',
|
name: 'dagre',
|
||||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||||
},
|
},
|
||||||
|
...(includeLargeFeatures
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'cose-bilkent',
|
||||||
|
loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
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 { log } from '../../logger.js';
|
||||||
import { createText } from '../createText.js';
|
import { createText } from '../createText.js';
|
||||||
import utils from '../../utils.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 { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -25,10 +29,10 @@ import {
|
|||||||
import rough from 'roughjs';
|
import rough from 'roughjs';
|
||||||
import createLabel from './createLabel.js';
|
import createLabel from './createLabel.js';
|
||||||
import { addEdgeMarkers } from './edgeMarker.ts';
|
import { addEdgeMarkers } from './edgeMarker.ts';
|
||||||
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
|
import { isLabelStyle, styles2String } from './shapes/handDrawnShapeStyles.js';
|
||||||
|
|
||||||
const edgeLabels = new Map();
|
export const edgeLabels = new Map();
|
||||||
const terminalLabels = new Map();
|
export const terminalLabels = new Map();
|
||||||
|
|
||||||
export const clear = () => {
|
export const clear = () => {
|
||||||
edgeLabels.clear();
|
edgeLabels.clear();
|
||||||
@@ -43,8 +47,10 @@ export const getLabelStyles = (styleArray) => {
|
|||||||
export const insertEdgeLabel = async (elem, edge) => {
|
export const insertEdgeLabel = async (elem, edge) => {
|
||||||
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||||
|
|
||||||
|
const { labelStyles } = styles2String(edge);
|
||||||
|
edge.labelStyle = labelStyles;
|
||||||
const labelElement = await createText(elem, edge.label, {
|
const labelElement = await createText(elem, edge.label, {
|
||||||
style: getLabelStyles(edge.labelStyle),
|
style: edge.labelStyle,
|
||||||
useHtmlLabels,
|
useHtmlLabels,
|
||||||
addSvgBackground: true,
|
addSvgBackground: true,
|
||||||
isNode: false,
|
isNode: false,
|
||||||
@@ -55,7 +61,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
|||||||
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||||
|
|
||||||
// Create inner g, label, this will be positioned now for centering the text
|
// 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);
|
label.node().appendChild(labelElement);
|
||||||
|
|
||||||
// Center the label
|
// Center the label
|
||||||
@@ -438,8 +444,33 @@ const fixCorners = function (lineData) {
|
|||||||
}
|
}
|
||||||
return newLineData;
|
return newLineData;
|
||||||
};
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
|
// 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();
|
const { handDrawnSeed } = getConfig();
|
||||||
let points = edge.points;
|
let points = edge.points;
|
||||||
let pointsHasChanged = false;
|
let pointsHasChanged = false;
|
||||||
@@ -453,11 +484,12 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
edgeClassStyles.push(edge.cssCompiledStyles[key]);
|
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 = points.slice(1, edge.points.length - 1);
|
||||||
points.unshift(tail.intersect(points[0]));
|
points.unshift(tail.intersect(points[0]));
|
||||||
log.debug(
|
log.debug(
|
||||||
'Last point APA12',
|
'Last point UIO',
|
||||||
edge.start,
|
edge.start,
|
||||||
'-->',
|
'-->',
|
||||||
edge.end,
|
edge.end,
|
||||||
@@ -467,6 +499,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
);
|
);
|
||||||
points.push(head.intersect(points[points.length - 1]));
|
points.push(head.intersect(points[points.length - 1]));
|
||||||
}
|
}
|
||||||
|
const pointsStr = btoa(JSON.stringify(points));
|
||||||
if (edge.toCluster) {
|
if (edge.toCluster) {
|
||||||
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
|
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
|
||||||
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
|
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
|
||||||
@@ -530,6 +563,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
curve = curveBasis;
|
curve = curveBasis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (edge.curve) {
|
||||||
|
// curve = edge.curve;
|
||||||
|
// }
|
||||||
|
|
||||||
const { x, y } = getLineFunctionsWithOffset(edge);
|
const { x, y } = getLineFunctionsWithOffset(edge);
|
||||||
const lineFunction = line().x(x).y(y).curve(curve);
|
const lineFunction = line().x(x).y(y).curve(curve);
|
||||||
|
|
||||||
@@ -561,10 +598,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
strokeClasses += ' edge-pattern-solid';
|
strokeClasses += ' edge-pattern-solid';
|
||||||
}
|
}
|
||||||
let svgPath;
|
let svgPath;
|
||||||
let linePath = lineFunction(lineData);
|
let linePath =
|
||||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : [];
|
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 strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
|
||||||
|
|
||||||
|
let animatedEdge = false;
|
||||||
if (edge.look === 'handDrawn') {
|
if (edge.look === 'handDrawn') {
|
||||||
const rc = rough.svg(elem);
|
const rc = rough.svg(elem);
|
||||||
Object.assign([], lineData);
|
Object.assign([], lineData);
|
||||||
@@ -595,7 +636,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
animationClass = ' edge-animation-' + edge.animation;
|
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
|
svgPath = elem
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', linePath)
|
.attr('d', linePath)
|
||||||
@@ -605,11 +649,39 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
|
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
|
||||||
)
|
)
|
||||||
.attr('style', pathStyle);
|
.attr('style', pathStyle);
|
||||||
|
|
||||||
|
//eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
||||||
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
|
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
|
// MC Special
|
||||||
// adds a red circle at each edge coordinate
|
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) => {
|
// cornerPoints.forEach((point) => {
|
||||||
// elem
|
// elem
|
||||||
// .append('circle')
|
// .append('circle')
|
||||||
@@ -619,19 +691,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
// .attr('cx', point.x)
|
// .attr('cx', point.x)
|
||||||
// .attr('cy', point.y);
|
// .attr('cy', point.y);
|
||||||
// });
|
// });
|
||||||
// lineData.forEach((point) => {
|
if (edge.showPoints) {
|
||||||
// elem
|
lineData.forEach((point) => {
|
||||||
// .append('circle')
|
elem
|
||||||
// .style('stroke', 'blue')
|
.append('circle')
|
||||||
// .style('fill', 'blue')
|
.style('stroke', 'red')
|
||||||
// .attr('r', 3)
|
.style('fill', 'red')
|
||||||
// .attr('cx', point.x)
|
.attr('r', 1)
|
||||||
// .attr('cy', point.y);
|
.attr('cx', point.x)
|
||||||
// });
|
.attr('cy', point.y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let url = '';
|
let url = '';
|
||||||
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
|
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('arrowTypeStart', edge.arrowTypeStart);
|
||||||
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||||
@@ -650,3 +730,134 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
|||||||
paths.originalPath = edge.points;
|
paths.originalPath = edge.points;
|
||||||
return paths;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,64 +2,63 @@
|
|||||||
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
|
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
|
||||||
*/
|
*/
|
||||||
function intersectLine(p1, p2, q1, q2) {
|
function intersectLine(p1, p2, q1, q2) {
|
||||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
{
|
||||||
// p7 and p473.
|
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||||
|
// p7 and p473.
|
||||||
|
|
||||||
var a1, a2, b1, b2, c1, c2;
|
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||||
var r1, r2, r3, r4;
|
// b1 y + c1 = 0.
|
||||||
var denom, offset, num;
|
const a1 = p2.y - p1.y;
|
||||||
var x, y;
|
const b1 = p1.x - p2.x;
|
||||||
|
const c1 = p2.x * p1.y - p1.x * p2.y;
|
||||||
|
|
||||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
// Compute r3 and r4.
|
||||||
// b1 y + c1 = 0.
|
const r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||||
a1 = p2.y - p1.y;
|
const r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||||
b1 = p1.x - p2.x;
|
|
||||||
c1 = p2.x * p1.y - p1.x * p2.y;
|
|
||||||
|
|
||||||
// Compute r3 and r4.
|
const epsilon = 1e-6;
|
||||||
r3 = a1 * q1.x + b1 * q1.y + c1;
|
|
||||||
r4 = a1 * q2.x + b1 * q2.y + c1;
|
|
||||||
|
|
||||||
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||||
// same side of line 1, the line segments do not intersect.
|
// same side of line 1, the line segments do not intersect.
|
||||||
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||||
return /*DON'T_INTERSECT*/;
|
return /*DON'T_INTERSECT*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||||
|
const a2 = q2.y - q1.y;
|
||||||
|
const b2 = q1.x - q2.x;
|
||||||
|
const c2 = q2.x * q1.y - q1.x * q2.y;
|
||||||
|
|
||||||
|
// Compute r1 and r2
|
||||||
|
const r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||||
|
const r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||||
|
|
||||||
|
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||||
|
// on same side of second line segment, the line segments do
|
||||||
|
// not intersect.
|
||||||
|
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
|
||||||
|
return /*DON'T_INTERSECT*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line segments intersect: compute intersection point.
|
||||||
|
const denom = a1 * b2 - a2 * b1;
|
||||||
|
if (denom === 0) {
|
||||||
|
return /*COLLINEAR*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = Math.abs(denom / 2);
|
||||||
|
|
||||||
|
// The denom/2 is to get rounding instead of truncating. It
|
||||||
|
// is added or subtracted to the numerator, depending upon the
|
||||||
|
// sign of the numerator.
|
||||||
|
let num = b1 * c2 - b2 * c1;
|
||||||
|
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||||
|
|
||||||
|
num = a2 * c1 - a1 * c2;
|
||||||
|
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||||
|
|
||||||
|
return { x: x, y: y };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
|
||||||
a2 = q2.y - q1.y;
|
|
||||||
b2 = q1.x - q2.x;
|
|
||||||
c2 = q2.x * q1.y - q1.x * q2.y;
|
|
||||||
|
|
||||||
// Compute r1 and r2
|
|
||||||
r1 = a2 * p1.x + b2 * p1.y + c2;
|
|
||||||
r2 = a2 * p2.x + b2 * p2.y + c2;
|
|
||||||
|
|
||||||
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
|
||||||
// on same side of second line segment, the line segments do
|
|
||||||
// not intersect.
|
|
||||||
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
|
|
||||||
return /*DON'T_INTERSECT*/;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Line segments intersect: compute intersection point.
|
|
||||||
denom = a1 * b2 - a2 * b1;
|
|
||||||
if (denom === 0) {
|
|
||||||
return /*COLLINEAR*/;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset = Math.abs(denom / 2);
|
|
||||||
|
|
||||||
// The denom/2 is to get rounding instead of truncating. It
|
|
||||||
// is added or subtracted to the numerator, depending upon the
|
|
||||||
// sign of the numerator.
|
|
||||||
num = b1 * c2 - b2 * c1;
|
|
||||||
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
|
||||||
|
|
||||||
num = a2 * c1 - a1 * c2;
|
|
||||||
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
|
||||||
|
|
||||||
return { x: x, y: y };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameSign(r1, r2) {
|
function sameSign(r1, r2) {
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ import { erBox } from './shapes/erBox.js';
|
|||||||
import { classBox } from './shapes/classBox.js';
|
import { classBox } from './shapes/classBox.js';
|
||||||
import { requirementBox } from './shapes/requirementBox.js';
|
import { requirementBox } from './shapes/requirementBox.js';
|
||||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||||
|
import { bang } from './shapes/bang.js';
|
||||||
|
import { cloud } from './shapes/cloud.js';
|
||||||
|
import { defaultMindmapNode } from './shapes/defaultMindmapNode.js';
|
||||||
|
import { mindmapCircle } from './shapes/mindmapCircle.js';
|
||||||
|
|
||||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
@@ -135,6 +139,22 @@ export const shapesDefs = [
|
|||||||
aliases: ['circ'],
|
aliases: ['circ'],
|
||||||
handler: circle,
|
handler: circle,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
semanticName: 'Bang',
|
||||||
|
name: 'Bang',
|
||||||
|
shortName: 'bang',
|
||||||
|
description: 'Bang',
|
||||||
|
aliases: ['bang'],
|
||||||
|
handler: bang,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
semanticName: 'Cloud',
|
||||||
|
name: 'Cloud',
|
||||||
|
shortName: 'cloud',
|
||||||
|
description: 'cloud',
|
||||||
|
aliases: ['cloud'],
|
||||||
|
handler: cloud,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
semanticName: 'Decision',
|
semanticName: 'Decision',
|
||||||
name: 'Diamond',
|
name: 'Diamond',
|
||||||
@@ -476,6 +496,9 @@ const generateShapeMap = () => {
|
|||||||
// Kanban diagram
|
// Kanban diagram
|
||||||
kanbanItem,
|
kanbanItem,
|
||||||
|
|
||||||
|
//Mindmap diagram
|
||||||
|
mindmapCircle,
|
||||||
|
defaultMindmapNode,
|
||||||
// class diagram
|
// class diagram
|
||||||
classBox,
|
classBox,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { log } from '../../../logger.js';
|
||||||
|
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '../../types.js';
|
||||||
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import type { D3Selection } from '../../../types.js';
|
||||||
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
|
import type { Bounds, Point } from '../../../types.js';
|
||||||
|
|
||||||
|
export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||||
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
|
node.labelStyle = labelStyles;
|
||||||
|
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||||
|
parent,
|
||||||
|
node,
|
||||||
|
getNodeClasses(node)
|
||||||
|
);
|
||||||
|
|
||||||
|
const w = bbox.width + 10 * halfPadding;
|
||||||
|
const h = bbox.height + 8 * halfPadding;
|
||||||
|
const r = 0.15 * w;
|
||||||
|
const { cssStyles } = node;
|
||||||
|
|
||||||
|
const minWidth = bbox.width + 20;
|
||||||
|
const minHeight = bbox.height + 20;
|
||||||
|
const effectiveWidth = Math.max(w, minWidth);
|
||||||
|
const effectiveHeight = Math.max(h, minHeight);
|
||||||
|
|
||||||
|
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||||
|
|
||||||
|
let bangElem;
|
||||||
|
const path = `M0 0
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1}
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0}
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33}
|
||||||
|
a${r * 0.8},${r * 0.8} 1 0,0 0,${effectiveHeight * 0.34}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.15},${effectiveHeight * 0.33}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${effectiveHeight * 0.15}
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},0
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.25},${-1 * effectiveHeight * 0.15}
|
||||||
|
|
||||||
|
a${r},${r} 1 0,0 ${-1 * effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
|
||||||
|
a${r * 0.8},${r * 0.8} 1 0,0 0,${-1 * effectiveHeight * 0.34}
|
||||||
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
|
||||||
|
H0 V0 Z`;
|
||||||
|
|
||||||
|
if (node.look === 'handDrawn') {
|
||||||
|
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const options = userNodeOverrides(node, {});
|
||||||
|
const roughNode = rc.path(path, options);
|
||||||
|
bangElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
|
bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||||
|
} else {
|
||||||
|
bangElem = shapeSvg
|
||||||
|
.insert('path', ':first-child')
|
||||||
|
.attr('class', 'basic label-container')
|
||||||
|
.attr('style', nodeStyles)
|
||||||
|
.attr('d', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate the path (center the shape)
|
||||||
|
bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`);
|
||||||
|
|
||||||
|
updateNodeBounds(node, bangElem);
|
||||||
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
return intersect.rect(bounds, point);
|
||||||
|
};
|
||||||
|
node.intersect = function (point) {
|
||||||
|
log.info('Bang intersect', node, point);
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import { log } from '../../../logger.js';
|
|
||||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
|
||||||
import intersect from '../intersect/index.js';
|
|
||||||
import type { Node } from '../../types.js';
|
|
||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
|
||||||
import rough from 'roughjs';
|
import rough from 'roughjs';
|
||||||
import type { D3Selection } from '../../../types.js';
|
import { log } from '../../../logger.js';
|
||||||
|
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||||
import { handleUndefinedAttr } from '../../../utils.js';
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
|
import type { MindmapOptions, Node, ShapeRenderOptions } from '../../types.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
|
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
|
||||||
export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
export async function circle<T extends SVGGraphicsElement>(
|
||||||
|
parent: D3Selection<T>,
|
||||||
|
node: Node,
|
||||||
|
options?: MindmapOptions | ShapeRenderOptions
|
||||||
|
) {
|
||||||
const { labelStyles, nodeStyles } = styles2String(node);
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
node.labelStyle = labelStyles;
|
node.labelStyle = labelStyles;
|
||||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
|
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
|
||||||
|
const padding = options?.padding ?? halfPadding;
|
||||||
const radius = bbox.width / 2 + halfPadding;
|
const radius = bbox.width / 2 + padding;
|
||||||
let circleElem;
|
let circleElem;
|
||||||
const { cssStyles } = node;
|
const { cssStyles } = node;
|
||||||
|
|
||||||
@@ -35,7 +39,10 @@ export async function circle<T extends SVGGraphicsElement>(parent: D3Selection<T
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNodeBounds(node, circleElem);
|
updateNodeBounds(node, circleElem);
|
||||||
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
const radius = bounds.width / 2;
|
||||||
|
return intersect.circle(bounds, radius, point);
|
||||||
|
};
|
||||||
node.intersect = function (point) {
|
node.intersect = function (point) {
|
||||||
log.info('Circle intersect', node, radius, point);
|
log.info('Circle intersect', node, radius, point);
|
||||||
return intersect.circle(node, radius, point);
|
return intersect.circle(node, radius, point);
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import rough from 'roughjs';
|
||||||
|
import { log } from '../../../logger.js';
|
||||||
|
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||||
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
|
import type { Node } from '../../types.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
|
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
|
||||||
|
export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||||
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
|
node.labelStyle = labelStyles;
|
||||||
|
|
||||||
|
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||||
|
parent,
|
||||||
|
node,
|
||||||
|
getNodeClasses(node)
|
||||||
|
);
|
||||||
|
|
||||||
|
const w = bbox.width + 2 * halfPadding;
|
||||||
|
const h = bbox.height + 2 * halfPadding;
|
||||||
|
|
||||||
|
// Cloud radii
|
||||||
|
const r1 = 0.15 * w;
|
||||||
|
const r2 = 0.25 * w;
|
||||||
|
const r3 = 0.35 * w;
|
||||||
|
const r4 = 0.2 * w;
|
||||||
|
|
||||||
|
const { cssStyles } = node;
|
||||||
|
let cloudElem;
|
||||||
|
|
||||||
|
// Cloud path
|
||||||
|
const path = `M0 0
|
||||||
|
a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
|
||||||
|
a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
|
||||||
|
a${r2},${r2} 1 0,1 ${w * 0.35},${w * 0.2}
|
||||||
|
|
||||||
|
a${r1},${r1} 1 0,1 ${w * 0.15},${h * 0.35}
|
||||||
|
a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${h * 0.65}
|
||||||
|
|
||||||
|
a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
|
||||||
|
a${r3},${r3} 1 0,1 ${-1 * w * 0.5},0
|
||||||
|
a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
|
||||||
|
|
||||||
|
a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
|
||||||
|
a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
|
||||||
|
H0 V0 Z`;
|
||||||
|
|
||||||
|
if (node.look === 'handDrawn') {
|
||||||
|
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const options = userNodeOverrides(node, {});
|
||||||
|
const roughNode = rc.path(path, options);
|
||||||
|
cloudElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
|
cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||||
|
} else {
|
||||||
|
cloudElem = shapeSvg
|
||||||
|
.insert('path', ':first-child')
|
||||||
|
.attr('class', 'basic label-container')
|
||||||
|
.attr('style', nodeStyles)
|
||||||
|
.attr('d', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||||
|
|
||||||
|
// Center the shape
|
||||||
|
cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
|
||||||
|
|
||||||
|
updateNodeBounds(node, cloudElem);
|
||||||
|
|
||||||
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
return intersect.rect(bounds, point);
|
||||||
|
};
|
||||||
|
node.intersect = function (point) {
|
||||||
|
log.info('Cloud intersect', node, point);
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||||
|
import type { Node } from '../../types.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import { styles2String } from './handDrawnShapeStyles.js';
|
||||||
|
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
|
||||||
|
export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
||||||
|
parent: D3Selection<T>,
|
||||||
|
node: Node
|
||||||
|
) {
|
||||||
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
|
node.labelStyle = labelStyles;
|
||||||
|
|
||||||
|
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
|
||||||
|
parent,
|
||||||
|
node,
|
||||||
|
getNodeClasses(node)
|
||||||
|
);
|
||||||
|
|
||||||
|
const w = bbox.width + 8 * halfPadding;
|
||||||
|
const h = bbox.height + 2 * halfPadding;
|
||||||
|
const rd = 5;
|
||||||
|
|
||||||
|
const rectPath = `
|
||||||
|
M${-w / 2} ${h / 2 - rd}
|
||||||
|
v${-h + 2 * rd}
|
||||||
|
q0,-${rd} ${rd},-${rd}
|
||||||
|
h${w - 2 * rd}
|
||||||
|
q${rd},0 ${rd},${rd}
|
||||||
|
v${h - 2 * rd}
|
||||||
|
q0,${rd} -${rd},${rd}
|
||||||
|
h${-w + 2 * rd}
|
||||||
|
q-${rd},0 -${rd},-${rd}
|
||||||
|
Z
|
||||||
|
`;
|
||||||
|
|
||||||
|
const bg = shapeSvg
|
||||||
|
.append('path')
|
||||||
|
.attr('id', 'node-' + node.id)
|
||||||
|
.attr('class', 'node-bkg node-' + node.type)
|
||||||
|
.attr('style', nodeStyles)
|
||||||
|
.attr('d', rectPath);
|
||||||
|
|
||||||
|
shapeSvg
|
||||||
|
.append('line')
|
||||||
|
.attr('class', 'node-line-')
|
||||||
|
.attr('x1', -w / 2)
|
||||||
|
.attr('y1', h / 2)
|
||||||
|
.attr('x2', w / 2)
|
||||||
|
.attr('y2', h / 2);
|
||||||
|
|
||||||
|
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
||||||
|
shapeSvg.append(() => label.node());
|
||||||
|
|
||||||
|
updateNodeBounds(node, bg);
|
||||||
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
return intersect.rect(bounds, point);
|
||||||
|
};
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
|
|||||||
import rough from 'roughjs';
|
import rough from 'roughjs';
|
||||||
import type { D3Selection } from '../../../types.js';
|
import type { D3Selection } from '../../../types.js';
|
||||||
import { handleUndefinedAttr } from '../../../utils.js';
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
|
import type { Bounds, Point } from '../../../types.js';
|
||||||
|
|
||||||
export async function drawRect<T extends SVGGraphicsElement>(
|
export async function drawRect<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
@@ -62,6 +63,10 @@ export async function drawRect<T extends SVGGraphicsElement>(
|
|||||||
|
|
||||||
updateNodeBounds(node, rect);
|
updateNodeBounds(node, rect);
|
||||||
|
|
||||||
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
return intersect.rect(bounds, point);
|
||||||
|
};
|
||||||
|
|
||||||
node.intersect = function (point) {
|
node.intersect = function (point) {
|
||||||
return intersect.rect(node, point);
|
return intersect.rect(node, point);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ export const compileStyles = (node: Node) => {
|
|||||||
// the array is the styles of node from the classes it is using
|
// 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
|
// 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
|
// 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] };
|
return { stylesMap, stylesArray: [...stylesMap] };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user