mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-17 23:39:26 +02:00
Compare commits
114 Commits
ipsep-cola
...
6623-add-i
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6b64b92b6e | ||
![]() |
41e3d2449a | ||
![]() |
0386ed6b32 | ||
![]() |
97481b4d11 | ||
![]() |
b8b120939e | ||
![]() |
71e4d62153 | ||
![]() |
b867370f1b | ||
![]() |
9f6ee53382 | ||
![]() |
3248bf3da4 | ||
![]() |
bf3ca9d1ef | ||
![]() |
e7a7ff8a2a | ||
![]() |
68fc68c239 | ||
![]() |
e53c17a012 | ||
![]() |
bb2d6973ba | ||
![]() |
769b362005 | ||
![]() |
e4d3aa4610 | ||
![]() |
716548548a | ||
![]() |
9322771b5c | ||
![]() |
2fe3063bf5 | ||
![]() |
4e55a45b1b | ||
![]() |
4bece53a3c | ||
![]() |
3d319824a6 | ||
![]() |
aa5d443a46 | ||
![]() |
356da0b4d7 | ||
![]() |
22530a8bdf | ||
![]() |
e6574ef40c | ||
![]() |
c4eb526162 | ||
![]() |
4d62d59632 | ||
![]() |
5af489d8dd | ||
![]() |
297be4a868 | ||
![]() |
fb6ace73b5 | ||
![]() |
074701e316 | ||
![]() |
bf362673fc | ||
![]() |
d042b21b12 | ||
![]() |
677ff82d13 | ||
![]() |
4b31361506 | ||
![]() |
981829a426 | ||
![]() |
327a5aa9fd | ||
![]() |
165ffefad5 | ||
![]() |
9258b2933b | ||
![]() |
f5445b266e | ||
![]() |
7a1530d911 | ||
![]() |
848f69a75c | ||
![]() |
99dbeba407 | ||
![]() |
d525acc05b | ||
![]() |
b451c66d7c | ||
![]() |
1dd11705d9 | ||
![]() |
29de40478f | ||
![]() |
4915545429 | ||
![]() |
334fe87bc6 | ||
![]() |
283e7810d2 | ||
![]() |
237d01d510 | ||
![]() |
a14cd0e2a1 | ||
![]() |
0823e08a54 | ||
![]() |
5cafe241d0 | ||
![]() |
c847817a54 | ||
![]() |
3ab0961bdc | ||
![]() |
0892870b5d | ||
![]() |
500f90a105 | ||
![]() |
14983158a2 | ||
![]() |
3fea9e8759 | ||
![]() |
e5ea2ed0b1 | ||
![]() |
34e7f9704b | ||
![]() |
1176d30668 | ||
![]() |
6728852b7a | ||
![]() |
15e7c890ed | ||
![]() |
0451e343ef | ||
![]() |
a2dbc8e4b3 | ||
![]() |
aea16eaf7e | ||
![]() |
bb7cd70034 | ||
![]() |
35a92efcdc | ||
![]() |
fce7cabb71 | ||
![]() |
38e6dc497a | ||
![]() |
5f0c53c8a7 | ||
![]() |
f9da4433ff | ||
![]() |
fc07f0d8ab | ||
![]() |
ce7a487dfc | ||
![]() |
8c0d12027d | ||
![]() |
afeb761296 | ||
![]() |
3abcfbb8d2 | ||
![]() |
ee82694645 | ||
![]() |
012530e98e | ||
![]() |
a4a27611dd | ||
![]() |
5055ade44e | ||
![]() |
b61bec8faf | ||
![]() |
1414380181 | ||
![]() |
6686ee9253 | ||
![]() |
7f109c7b94 | ||
![]() |
b9c3375be3 | ||
![]() |
4254bdd473 | ||
![]() |
4012cbf013 | ||
![]() |
76d073b027 | ||
![]() |
814b68b4a9 | ||
![]() |
bfa0eefa32 | ||
![]() |
cc476d59d1 | ||
![]() |
579c22cf5d | ||
![]() |
33e08daf17 | ||
![]() |
24257de8a6 | ||
![]() |
42ac1848dd | ||
![]() |
3920ad442d | ||
![]() |
53d27b771d | ||
![]() |
2c0931da46 | ||
![]() |
8224a81ab6 | ||
![]() |
98442294ed | ||
![]() |
a3d164fde8 | ||
![]() |
9dc987b28b | ||
![]() |
72c0d9df26 | ||
![]() |
cfbce54638 | ||
![]() |
6979aa1013 | ||
![]() |
ea60525988 | ||
![]() |
9da6fb39ae | ||
![]() |
83b9a17277 | ||
![]() |
6cc192680a | ||
![]() |
3dd6107e76 |
5
.changeset/bumpy-shrimps-stay.md
Normal file
5
.changeset/bumpy-shrimps-stay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add custom images and icons support for sequence diagram actors
|
5
.changeset/chatty-lemons-stick.md
Normal file
5
.changeset/chatty-lemons-stick.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Remove the "-beta" suffix from the XYChart, Block, Sankey diagrams to reflect their stable status
|
5
.changeset/cold-sites-accept.md
Normal file
5
.changeset/cold-sites-accept.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Position the edge label in state diagram correctly relative to the edge
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added IpSepCoLa algorithm for layout
|
5
.changeset/empty-clouds-cry.md
Normal file
5
.changeset/empty-clouds-cry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Apply correct dateFormat in Gantt chart to show only day when specified
|
5
.changeset/fuzzy-pears-cough.md
Normal file
5
.changeset/fuzzy-pears-cough.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: handle exclude dates properly in Gantt charts when using dateFormat: 'YYYY-MM-DD HH:mm:ss'
|
5
.changeset/gold-spiders-join.md
Normal file
5
.changeset/gold-spiders-join.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: fixed connection gaps in flowchart for roundedRect, stadium and diamond shape
|
5
.changeset/hungry-baths-glow.md
Normal file
5
.changeset/hungry-baths-glow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Added support for new participant types (`actor`, `boundary`, `control`, `entity`, `database`, `collections`, `queue`) in `sequenceDiagram`.
|
5
.changeset/red-zebras-happen.md
Normal file
5
.changeset/red-zebras-happen.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Allow equals sign in sequenceDiagram labels
|
9
.changeset/seven-papayas-film.md
Normal file
9
.changeset/seven-papayas-film.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
Add validation for negative values in pie charts:
|
||||
|
||||
Prevents crashes during parsing by validating values post-parsing.
|
||||
|
||||
Provides clearer, user-friendly error messages for invalid negative inputs.
|
5
.changeset/six-planets-rescue.md
Normal file
5
.changeset/six-planets-rescue.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: node border style for handdrawn shapes
|
5
.changeset/strong-dryers-pay.md
Normal file
5
.changeset/strong-dryers-pay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: correctly render non-directional lines for '---' in block diagrams
|
@@ -12,7 +12,6 @@ gantt
|
||||
gitgraph
|
||||
gzipped
|
||||
handDrawn
|
||||
ipsepcola
|
||||
kanban
|
||||
marginx
|
||||
marginy
|
||||
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0
|
||||
uses: peter-evans/create-pull-request@1310d7dab503600742045e6fd4b84dda64352858
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
@@ -16,7 +16,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL2: should handle columns statement in sub-blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
id1["Hello"]
|
||||
block
|
||||
columns 3
|
||||
@@ -32,7 +32,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL3: should align block widths and handle columns statement in sub-blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
block
|
||||
columns 1
|
||||
id1
|
||||
@@ -48,7 +48,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL4: should align block widths and handle columns statements in deeper sub-blocks then 1 level', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 1
|
||||
block
|
||||
columns 1
|
||||
@@ -68,7 +68,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL5: should align block widths and handle columns statements in deeper sub-blocks then 1 level (alt)', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 1
|
||||
block
|
||||
id1
|
||||
@@ -87,7 +87,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL6: should handle block arrows and spece statements', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
space:3
|
||||
ida idb idc
|
||||
@@ -106,7 +106,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL7: should handle different types of edges', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
A space:5
|
||||
A --o B
|
||||
@@ -119,7 +119,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL8: should handle sub-blocks without columns statements', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 2
|
||||
C A B
|
||||
block
|
||||
@@ -133,7 +133,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL9: should handle edges from blocks in sub blocks to other blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
B space
|
||||
block
|
||||
@@ -147,7 +147,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL10: should handle edges from composite blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
B space
|
||||
block BL
|
||||
@@ -161,7 +161,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL11: should handle edges to composite blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
B space
|
||||
block BL
|
||||
@@ -175,7 +175,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL12: edges should handle labels', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A
|
||||
space
|
||||
A -- "apa" --> E
|
||||
@@ -186,7 +186,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL13: should handle block arrows in different directions', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
space blockArrowId1<["down"]>(down) space
|
||||
blockArrowId2<["right"]>(right) blockArrowId3<["Sync"]>(x, y) blockArrowId4<["left"]>(left)
|
||||
@@ -199,7 +199,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL14: should style statements and class statements', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A
|
||||
B
|
||||
classDef blue fill:#66f,stroke:#333,stroke-width:2px;
|
||||
@@ -212,7 +212,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL15: width alignment - D and E should share available space', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
block
|
||||
D
|
||||
E
|
||||
@@ -225,7 +225,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL16: width alignment - C should be as wide as the composite block', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
block
|
||||
A("This is the text")
|
||||
B
|
||||
@@ -238,7 +238,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL17: width alignment - blocks should be equal in width', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A("This is the text")
|
||||
B
|
||||
C
|
||||
@@ -249,7 +249,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL18: block types 1 - square, rounded and circle', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A["square"]
|
||||
B("rounded")
|
||||
C(("circle"))
|
||||
@@ -260,7 +260,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL19: block types 2 - odd, diamond and hexagon', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A>"rect_left_inv_arrow"]
|
||||
B{"diamond"}
|
||||
C{{"hexagon"}}
|
||||
@@ -271,7 +271,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL20: block types 3 - stadium', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A(["stadium"])
|
||||
`,
|
||||
{}
|
||||
@@ -280,7 +280,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL21: block types 4 - lean right, lean left, trapezoid and inv trapezoid', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A[/"lean right"/]
|
||||
B[\"lean left"\]
|
||||
C[/"trapezoid"\]
|
||||
@@ -292,7 +292,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL22: block types 1 - square, rounded and circle', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A["square"]
|
||||
B("rounded")
|
||||
C(("circle"))
|
||||
@@ -303,7 +303,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL23: sizing - it should be possible to make a block wider', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
A("rounded"):2
|
||||
B:2
|
||||
C
|
||||
@@ -314,7 +314,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL24: sizing - it should be possible to make a composite block wider', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
block:2
|
||||
A
|
||||
end
|
||||
@@ -326,7 +326,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL25: block in the middle with space on each side', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
space
|
||||
middle["In the middle"]
|
||||
@@ -337,7 +337,7 @@ describe('Block diagram', () => {
|
||||
});
|
||||
it('BL26: space and an edge', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 5
|
||||
A space B
|
||||
A --x B
|
||||
@@ -347,7 +347,7 @@ describe('Block diagram', () => {
|
||||
});
|
||||
it('BL27: block sizes for regular blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
a["A wide one"] b:2 c:2 d
|
||||
`,
|
||||
@@ -356,7 +356,7 @@ describe('Block diagram', () => {
|
||||
});
|
||||
it('BL28: composite block with a set width - f should use the available space', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
a:3
|
||||
block:e:3
|
||||
@@ -370,7 +370,7 @@ describe('Block diagram', () => {
|
||||
|
||||
it('BL29: composite block with a set width - f and g should split the available space', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
`block
|
||||
columns 3
|
||||
a:3
|
||||
block:e:3
|
||||
@@ -393,6 +393,17 @@ describe('Block diagram', () => {
|
||||
overflow:3
|
||||
short:1
|
||||
also_overflow:2
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('BL31: edge without arrow syntax should render with no arrowheads', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
a
|
||||
b
|
||||
a --- b
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
@@ -1113,6 +1113,37 @@ end
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Flowchart Node Shape Rendering', () => {
|
||||
it('should render a stadium-shaped node', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TB
|
||||
A(["Start"]) --> n1["Untitled Node"]
|
||||
A --> n2["Untitled Node"]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a diamond-shaped node using shape config', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart BT
|
||||
n2["Untitled Node"] --> n1["Diamond"]
|
||||
n1@{ shape: diam}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a rounded rectangle and a normal rectangle', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart BT
|
||||
n2["Untitled Node"] --> n1["Rounded Rectangle"]
|
||||
n3["Untitled Node"] --> n1
|
||||
n1@{ shape: rounded}
|
||||
n3@{ shape: rect}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('6617: Per Link Curve Styling using edge Ids', () => {
|
||||
imgSnapshotTest(
|
||||
|
@@ -565,6 +565,18 @@ describe('Gantt diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render only the day when using dateFormat D', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
title Test
|
||||
dateFormat D
|
||||
A :a, 1, 1d
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: fix it
|
||||
//
|
||||
// This test is skipped deliberately
|
||||
@@ -647,6 +659,49 @@ describe('Gantt diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a gantt diagram excluding a specific date in YYYY-MM-DD HH:mm:ss format', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD HH:mm:ss
|
||||
excludes 2025-07-07
|
||||
section Section
|
||||
A task :a1, 2025-07-04 20:30:30, 2025-07-08 10:30:30
|
||||
Another task:after a1, 20h
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a gantt diagram excluding saturday and sunday in YYYY-MM-DD HH:mm:ss format', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD HH:mm:ss
|
||||
excludes weekends
|
||||
weekend saturday
|
||||
section Section
|
||||
A task :a1, 2025-07-04 20:30:30, 2025-07-08 10:30:30
|
||||
Another task:after a1, 20h
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a gantt diagram excluding friday and saturday in YYYY-MM-DD HH:mm:ss format', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD HH:mm:ss
|
||||
excludes weekends
|
||||
weekend friday
|
||||
section Section
|
||||
A task :a1, 2025-07-04 20:30:30, 2025-07-08 10:30:30
|
||||
Another task:after a1, 20h
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it("should render when there's a semicolon in the title", () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
|
@@ -1,153 +0,0 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||
|
||||
describe('Flowchart IPSepCoLa', () => {
|
||||
it('1-ipsepCola: should render a simple flowchart', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('2-ipsepCola: handle bidirectional edges', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
subgraph D
|
||||
A --> B
|
||||
A --> B
|
||||
B --> A
|
||||
B --> A
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
it('3-ipsepCola: handle multiple self loops', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart
|
||||
a --> a
|
||||
a --> a
|
||||
a --> a
|
||||
a --> a
|
||||
`
|
||||
);
|
||||
});
|
||||
it('4-ipsepCola: handle state diagram example', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
`
|
||||
);
|
||||
});
|
||||
it('5-ipsepCola: handle multiple subgraphs with edges between them', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart LR
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
`
|
||||
);
|
||||
});
|
||||
it('6-ipsepCola: handle class diagram example', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class AuthService {
|
||||
+login(username: string, password: string): boolean
|
||||
+logout(): void
|
||||
+register(): void
|
||||
}
|
||||
|
||||
class User {
|
||||
-username: string
|
||||
-password: string
|
||||
-role: Role
|
||||
+changePassword(): void
|
||||
}
|
||||
|
||||
class Role {
|
||||
-name: string
|
||||
-permissions: string[]
|
||||
+hasPermission(): boolean
|
||||
}
|
||||
|
||||
AuthService --> User
|
||||
User --> Role
|
||||
`
|
||||
);
|
||||
});
|
||||
it('7-ipsepCola: should render a decision flowchart', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Start([Start]) --> Prep[Preparation Step]
|
||||
Prep --> Split{Ready to Process?}
|
||||
Split -->|Yes| T1[Task A]
|
||||
Split -->|Yes| T2[Task B]
|
||||
T1 --> Merge
|
||||
T2 --> Merge
|
||||
Merge((Join Results)) --> Finalize[Finalize Process]
|
||||
Finalize --> End([End])
|
||||
`
|
||||
);
|
||||
});
|
||||
it('8-ipsepCola: handle nested subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart LR
|
||||
subgraph main
|
||||
subgraph subcontainer
|
||||
subcontainer-child
|
||||
end
|
||||
subcontainer-child--> subcontainer-sibling
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
@@ -82,4 +82,13 @@ describe('pie chart', () => {
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render pie slices only for non-zero values but shows all legends', () => {
|
||||
imgSnapshotTest(
|
||||
` pie title Pets adopted by volunteers
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 1
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -15,7 +15,7 @@ describe('Sankey Diagram', () => {
|
||||
describe('when given a linkColor', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.wrap(
|
||||
`sankey-beta
|
||||
`sankey
|
||||
a,b,10
|
||||
`
|
||||
).as('graph');
|
||||
@@ -62,7 +62,7 @@ describe('Sankey Diagram', () => {
|
||||
this.beforeAll(() => {
|
||||
cy.wrap(
|
||||
`
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
a,b,8
|
||||
b,c,8
|
||||
|
112
cypress/integration/rendering/sequenceDiagram-icon.spec.ts
Normal file
112
cypress/integration/rendering/sequenceDiagram-icon.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util';
|
||||
|
||||
const looks = ['classic'] as const;
|
||||
|
||||
looks.forEach((look) => {
|
||||
describe(`SequenceDiagram icon participants in ${look} look`, () => {
|
||||
it(`single participant with icon`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
Note over Bob: Icon participant`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`two participants, one icon and one normal`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: Hello`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`two icon participants`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice@{ type: "icon", icon: "fa:user" }
|
||||
Bob->>Alice: Hello
|
||||
Alice-->>Bob: Hi`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with markdown htmlLabels:true content`, () => {
|
||||
// html/markdown in messages/notes (participants themselves don't support label/form/w/h)
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with markdown htmlLabels:false content`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
||||
imgSnapshotTest(diagram, {
|
||||
look,
|
||||
htmlLabels: false,
|
||||
flowchart: { htmlLabels: false },
|
||||
});
|
||||
});
|
||||
|
||||
it(`with styles applied to participant`, () => {
|
||||
// style by participant id
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: Styled participant
|
||||
`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with classDef and class application`, () => {
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: Classed participant
|
||||
`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Colored emoji icon tests (analogous to the flowchart colored icon tests), no direction line.
|
||||
describe('SequenceDiagram colored icon participant', () => {
|
||||
it('colored emoji icon without styles', () => {
|
||||
const icon = 'fluent-emoji:tropical-fish';
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
||||
Note over Bob: colored emoji icon
|
||||
`;
|
||||
imgSnapshotTest(diagram);
|
||||
});
|
||||
|
||||
it('colored emoji icon with styles', () => {
|
||||
const icon = 'fluent-emoji:tropical-fish';
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
||||
`;
|
||||
imgSnapshotTest(diagram);
|
||||
});
|
||||
});
|
||||
|
||||
// Mixed scenario: multiple interactions, still no direction line.
|
||||
describe('SequenceDiagram icon participant with multiple interactions', () => {
|
||||
const icon = 'fa:bell-slash';
|
||||
it('icon participant interacts with two normal participants', () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
||||
participant Alice
|
||||
participant Carol
|
||||
Bob->>Alice: Ping
|
||||
Alice-->>Bob: Pong
|
||||
Bob->>Carol: Notify
|
||||
Note right of Bob: Icon side note`;
|
||||
imgSnapshotTest(diagram);
|
||||
});
|
||||
});
|
88
cypress/integration/rendering/sequenceDiagram-image.spec.ts
Normal file
88
cypress/integration/rendering/sequenceDiagram-image.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util';
|
||||
|
||||
const looks = ['classic'] as const;
|
||||
|
||||
looks.forEach((look) => {
|
||||
describe(`SequenceDiagram image participants in ${look} look`, () => {
|
||||
it(`single participant with image`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
Note over Bob: Image participant`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`two participants, one image and one normal`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: Hello`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`two image participants`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
Bob->>Alice: Hello
|
||||
Alice-->>Bob: Hi`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with markdown htmlLabels:true content`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with markdown htmlLabels:false content`, () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
||||
imgSnapshotTest(diagram, {
|
||||
look,
|
||||
htmlLabels: false,
|
||||
flowchart: { htmlLabels: false },
|
||||
});
|
||||
});
|
||||
|
||||
it(`with styles applied to participant`, () => {
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: Styled participant
|
||||
`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
|
||||
it(`with classDef and class application`, () => {
|
||||
const diagram = `
|
||||
sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: Classed participant
|
||||
`;
|
||||
imgSnapshotTest(diagram, { look });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Mixed scenario: multiple interactions, still no direction line.
|
||||
describe('SequenceDiagram image participant with multiple interactions', () => {
|
||||
const imageUrl = 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg';
|
||||
it('image participant interacts with two normal participants', () => {
|
||||
const diagram = `sequenceDiagram
|
||||
participant Bob@{ type: "image", "image": "${imageUrl}" }
|
||||
participant Alice
|
||||
participant Carol
|
||||
Bob->>Alice: Ping
|
||||
Alice-->>Bob: Pong
|
||||
Bob->>Carol: Notify
|
||||
Note right of Bob: Image side note`;
|
||||
imgSnapshotTest(diagram);
|
||||
});
|
||||
});
|
659
cypress/integration/rendering/sequencediagram-v2.spec.js
Normal file
659
cypress/integration/rendering/sequencediagram-v2.spec.js
Normal file
@@ -0,0 +1,659 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
const looks = ['classic'];
|
||||
const participantTypes = [
|
||||
{ type: 'participant', display: 'participant' },
|
||||
{ type: 'actor', display: 'actor' },
|
||||
{ type: 'boundary', display: 'boundary' },
|
||||
{ type: 'control', display: 'control' },
|
||||
{ type: 'entity', display: 'entity' },
|
||||
{ type: 'database', display: 'database' },
|
||||
{ type: 'collections', display: 'collections' },
|
||||
{ type: 'queue', display: 'queue' },
|
||||
];
|
||||
|
||||
const restrictedTypes = ['boundary', 'control', 'entity', 'database', 'collections', 'queue'];
|
||||
|
||||
const interactionTypes = ['->>', '-->>', '->', '-->', '-x', '--x', '->>+', '-->>+'];
|
||||
|
||||
const notePositions = ['left of', 'right of', 'over'];
|
||||
|
||||
function getParticipantLine(name, type, alias) {
|
||||
if (restrictedTypes.includes(type)) {
|
||||
return ` participant ${name}@{ "type" : "${type}" }\n`;
|
||||
} else if (alias) {
|
||||
return ` participant ${name}@{ "type" : "${type}" } \n`;
|
||||
} else {
|
||||
return ` participant ${name}@{ "type" : "${type}" }\n`;
|
||||
}
|
||||
}
|
||||
|
||||
looks.forEach((look) => {
|
||||
describe(`Sequence Diagram Tests - ${look} look`, () => {
|
||||
it('should render all participant types', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.forEach((pt, index) => {
|
||||
const name = `${pt.display}${index}`;
|
||||
diagramCode += getParticipantLine(name, pt.type);
|
||||
});
|
||||
for (let i = 0; i < participantTypes.length - 1; i++) {
|
||||
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
|
||||
}
|
||||
imgSnapshotTest(diagramCode, { look, sequence: { diagramMarginX: 50, diagramMarginY: 10 } });
|
||||
});
|
||||
|
||||
it('should render all interaction types', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
interactionTypes.forEach((interaction, index) => {
|
||||
diagramCode += ` A ${interaction} B: ${interaction} message ${index}\n`;
|
||||
});
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render participant creation and destruction', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.forEach((pt, index) => {
|
||||
const name = `${pt.display}${index}`;
|
||||
diagramCode += getParticipantLine('A', pt.type);
|
||||
diagramCode += getParticipantLine('B', pt.type);
|
||||
diagramCode += ` create participant ${name}@{ "type" : "${pt.type}" }\n`;
|
||||
diagramCode += ` A ->> ${name}: Hello ${pt.display}\n`;
|
||||
if (index % 2 === 0) {
|
||||
diagramCode += ` destroy ${name}\n`;
|
||||
}
|
||||
});
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render notes in all positions', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
notePositions.forEach((position, index) => {
|
||||
diagramCode += ` Note ${position} A: Note ${position} ${index}\n`;
|
||||
});
|
||||
diagramCode += ` A ->> B: Message with notes\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render parallel interactions', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 4).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` par Parallel actions\n`;
|
||||
for (let i = 0; i < 3; i += 2) {
|
||||
diagramCode += ` ${participantTypes[i].display}${i} ->> ${participantTypes[i + 1].display}${i + 1}: Message ${i}\n`;
|
||||
if (i < participantTypes.length - 2) {
|
||||
diagramCode += ` and\n`;
|
||||
}
|
||||
}
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render alternative flows', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += getParticipantLine('A', 'actor');
|
||||
diagramCode += getParticipantLine('B', 'boundary');
|
||||
diagramCode += ` alt Successful case\n`;
|
||||
diagramCode += ` A ->> B: Request\n`;
|
||||
diagramCode += ` B -->> A: Success\n`;
|
||||
diagramCode += ` else Failure case\n`;
|
||||
diagramCode += ` A ->> B: Request\n`;
|
||||
diagramCode += ` B --x A: Failure\n`;
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render loops', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` loop For each participant\n`;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Message ${i}\n`;
|
||||
}
|
||||
diagramCode += ` end\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render boxes around groups', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
diagramCode += ` box Group 1\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
|
||||
});
|
||||
diagramCode += ` end\n`;
|
||||
diagramCode += ` box rgb(200,220,255) Group 2\n`;
|
||||
participantTypes.slice(3, 6).forEach((pt, index) => {
|
||||
diagramCode += ` ${getParticipantLine(`${pt.display}${index}`, pt.type)}`;
|
||||
});
|
||||
diagramCode += ` end\n`;
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[3].display}0: Cross-group message\n`;
|
||||
imgSnapshotTest(diagramCode, { look });
|
||||
});
|
||||
|
||||
it('should render with different font settings', () => {
|
||||
let diagramCode = `sequenceDiagram\n`;
|
||||
participantTypes.slice(0, 3).forEach((pt, index) => {
|
||||
diagramCode += getParticipantLine(`${pt.display}${index}`, pt.type);
|
||||
});
|
||||
diagramCode += ` ${participantTypes[0].display}0 ->> ${participantTypes[1].display}1: Regular message\n`;
|
||||
diagramCode += ` Note right of ${participantTypes[1].display}1: Regular note\n`;
|
||||
imgSnapshotTest(diagramCode, {
|
||||
look,
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
actorFontSize: 14,
|
||||
messageFontFamily: 'Arial',
|
||||
messageFontSize: 12,
|
||||
noteFontFamily: 'times',
|
||||
noteFontSize: 16,
|
||||
noteAlign: 'left',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Additional tests for specific combinations
|
||||
describe('Sequence Diagram Special Cases', () => {
|
||||
it('should render complex sequence with all features', () => {
|
||||
const diagramCode = `
|
||||
sequenceDiagram
|
||||
box rgb(200,220,255) Authentication
|
||||
actor User
|
||||
participant LoginUI@{ "type": "boundary" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UserDB@{ "type": "database" }
|
||||
end
|
||||
|
||||
box rgb(200,255,220) Order Processing
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
participant AuditLogs@{ "type": "collections" }
|
||||
end
|
||||
|
||||
User ->> LoginUI: Enter credentials
|
||||
LoginUI ->> AuthService: Validate
|
||||
AuthService ->> UserDB: Query user
|
||||
UserDB -->> AuthService: User data
|
||||
alt Valid credentials
|
||||
AuthService -->> LoginUI: Success
|
||||
LoginUI -->> User: Welcome
|
||||
|
||||
par Place order
|
||||
User ->> Order: New order
|
||||
Order ->> OrderQueue: Process
|
||||
and
|
||||
Order ->> AuditLogs: Record
|
||||
end
|
||||
|
||||
loop Until confirmed
|
||||
OrderQueue ->> Order: Update status
|
||||
Order -->> User: Notification
|
||||
end
|
||||
else Invalid credentials
|
||||
AuthService --x LoginUI: Failure
|
||||
LoginUI --x User: Retry
|
||||
end
|
||||
`;
|
||||
imgSnapshotTest(diagramCode, {});
|
||||
});
|
||||
|
||||
it('should render with wrapped messages and notes', () => {
|
||||
const diagramCode = `
|
||||
sequenceDiagram
|
||||
participant A
|
||||
participant B
|
||||
|
||||
A ->> B: This is a very long message that should wrap properly in the diagram rendering
|
||||
Note over A,B: This is a very long note that should also wrap properly when rendered in the diagram
|
||||
|
||||
par Wrapped parallel
|
||||
A ->> B: Parallel message 1<br>with explicit line break
|
||||
and
|
||||
B ->> A: Parallel message 2<br>with explicit line break
|
||||
end
|
||||
|
||||
loop Wrapped loop
|
||||
Note right of B: This is a long note<br>in a loop
|
||||
A ->> B: Message in loop
|
||||
end
|
||||
`;
|
||||
imgSnapshotTest(diagramCode, { sequence: { wrap: true } });
|
||||
});
|
||||
describe('Sequence Diagram Rendering with Different Participant Types', () => {
|
||||
it('should render a sequence diagram with various participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant User@{ "type": "actor" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UI@{ "type": "boundary" }
|
||||
participant OrderController@{ "type": "control" }
|
||||
participant Product@{ "type": "entity" }
|
||||
participant MongoDB@{ "type": "database" }
|
||||
participant Products@{ "type": "collections" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
User ->> UI: Login request
|
||||
UI ->> AuthService: Validate credentials
|
||||
AuthService -->> UI: Authentication token
|
||||
UI ->> OrderController: Place order
|
||||
OrderController ->> Product: Check availability
|
||||
Product -->> OrderController: Available
|
||||
OrderController ->> MongoDB: Save order
|
||||
MongoDB -->> OrderController: Order saved
|
||||
OrderController ->> OrderQueue: Process payment
|
||||
OrderQueue -->> User: Order confirmation
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render participant creation and destruction with different types', () => {
|
||||
imgSnapshotTest(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
Alice->>Bob: Hello Bob, how are you ?
|
||||
Bob->>Alice: Fine, thank you. And you?
|
||||
create participant Carl@{ "type" : "control" }
|
||||
Alice->>Carl: Hi Carl!
|
||||
create actor D as Donald
|
||||
Carl->>D: Hi!
|
||||
destroy Carl
|
||||
Alice-xCarl: We are too many
|
||||
destroy Bob
|
||||
Bob->>Alice: I agree
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex interactions between different participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
box rgb(200,220,255) Authentication
|
||||
participant User@{ "type": "actor" }
|
||||
participant LoginUI@{ "type": "boundary" }
|
||||
participant AuthService@{ "type": "control" }
|
||||
participant UserDB@{ "type": "database" }
|
||||
end
|
||||
|
||||
box rgb(200,255,220) Order Processing
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrderQueue@{ "type": "queue" }
|
||||
participant AuditLogs@{ "type": "collections" }
|
||||
end
|
||||
|
||||
User ->> LoginUI: Enter credentials
|
||||
LoginUI ->> AuthService: Validate
|
||||
AuthService ->> UserDB: Query user
|
||||
UserDB -->> AuthService: User data
|
||||
|
||||
alt Valid credentials
|
||||
AuthService -->> LoginUI: Success
|
||||
LoginUI -->> User: Welcome
|
||||
|
||||
par Place order
|
||||
User ->> Order: New order
|
||||
Order ->> OrderQueue: Process
|
||||
and
|
||||
Order ->> AuditLogs: Record
|
||||
end
|
||||
|
||||
loop Until confirmed
|
||||
OrderQueue ->> Order: Update status
|
||||
Order -->> User: Notification
|
||||
end
|
||||
else Invalid credentials
|
||||
AuthService --x LoginUI: Failure
|
||||
LoginUI --x User: Retry
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: false } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render parallel processes with different participant types', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Customer@{ "type": "actor" }
|
||||
participant Frontend@{ "type": "participant" }
|
||||
participant PaymentService@{ "type": "boundary" }
|
||||
participant InventoryManager@{ "type": "control" }
|
||||
participant Order@{ "type": "entity" }
|
||||
participant OrdersDB@{ "type": "database" }
|
||||
participant NotificationQueue@{ "type": "queue" }
|
||||
|
||||
Customer ->> Frontend: Place order
|
||||
Frontend ->> Order: Create order
|
||||
par Parallel Processing
|
||||
Order ->> PaymentService: Process payment
|
||||
and
|
||||
Order ->> InventoryManager: Reserve items
|
||||
end
|
||||
PaymentService -->> Order: Payment confirmed
|
||||
InventoryManager -->> Order: Items reserved
|
||||
Order ->> OrdersDB: Save finalized order
|
||||
OrdersDB -->> Order: Order saved
|
||||
Order ->> NotificationQueue: Send confirmation
|
||||
NotificationQueue -->> Customer: Order confirmation
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should render different participant types with notes and loops', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Admin
|
||||
participant Dashboard
|
||||
participant AuthService@{ "type" : "boundary" }
|
||||
participant UserManager@{ "type" : "control" }
|
||||
participant UserProfile@{ "type" : "entity" }
|
||||
participant UserDB@{ "type" : "database" }
|
||||
participant Logs@{ "type" : "database" }
|
||||
|
||||
Admin ->> Dashboard: Open user management
|
||||
loop Authentication check
|
||||
Dashboard ->> AuthService: Verify admin rights
|
||||
AuthService ->> Dashboard: Access granted
|
||||
end
|
||||
Dashboard ->> UserManager: List users
|
||||
UserManager ->> UserDB: Query users
|
||||
UserDB ->> UserManager: Return user data
|
||||
Note right of UserDB: Encrypted data<br/>requires decryption
|
||||
UserManager ->> UserProfile: Format profiles
|
||||
UserProfile ->> UserManager: Formatted data
|
||||
UserManager ->> Dashboard: Display users
|
||||
Dashboard ->> Logs: Record access
|
||||
Logs ->> Admin: Audit trail
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render different participant types with alternative flows', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Client
|
||||
participant MobileApp
|
||||
participant CloudService@{ "type" : "boundary" }
|
||||
participant DataProcessor@{ "type" : "control" }
|
||||
participant Transaction@{ "type" : "entity" }
|
||||
participant TransactionsDB@{ "type" : "database" }
|
||||
participant EventBus@{ "type" : "queue" }
|
||||
|
||||
Client ->> MobileApp: Initiate transaction
|
||||
MobileApp ->> CloudService: Authenticate
|
||||
alt Authentication successful
|
||||
CloudService -->> MobileApp: Auth token
|
||||
MobileApp ->> DataProcessor: Process data
|
||||
DataProcessor ->> Transaction: Create transaction
|
||||
Transaction ->> TransactionsDB: Save record
|
||||
TransactionsDB -->> Transaction: Confirmation
|
||||
Transaction ->> EventBus: Publish event
|
||||
EventBus -->> Client: Notification
|
||||
else Authentication failed
|
||||
CloudService -->> MobileApp: Error
|
||||
MobileApp -->> Client: Show error
|
||||
end
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render different participant types with wrapping text', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant B@{ "type" : "boundary" }
|
||||
participant C@{ "type" : "control" }
|
||||
participant E@{ "type" : "entity" }
|
||||
participant DB@{ "type" : "database" }
|
||||
participant COL@{ "type" : "collections" }
|
||||
participant Q@{ "type" : "queue" }
|
||||
|
||||
FE ->> B: Another long message<br/>with explicit<br/>line breaks
|
||||
B -->> FE: Response message that is also quite long and needs to wrap
|
||||
FE ->> C: Process data
|
||||
C ->> E: Validate
|
||||
E -->> C: Validation result
|
||||
C ->> DB: Save
|
||||
DB -->> C: Save result
|
||||
C ->> COL: Log
|
||||
COL -->> Q: Forward
|
||||
Q -->> LongNameUser: Final response with confirmation of all actions taken
|
||||
`,
|
||||
{ sequence: { wrap: true } }
|
||||
);
|
||||
});
|
||||
|
||||
describe('Sequence Diagram - New Participant Types with Long Notes and Messages', () => {
|
||||
it('should render long notes left of boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note left of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes left of control', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note left of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long notes right of entity', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note right of Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes right of database', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note right of Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long notes over collections', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note over Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long notes over queue', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
actor Bob
|
||||
Alice->>Bob: Hola
|
||||
Note over Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render notes over actor and boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Charlie@{ "type" : "boundary" }
|
||||
note over Alice: Some note
|
||||
note over Charlie: Other note
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long messages from database to collections', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob@{ "type" : "collections" }
|
||||
Alice->>Bob: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long messages from control to entity', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob@{ "type" : "entity" }
|
||||
Alice->>Bob:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
Bob->>Alice: I'm short though
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render long messages from queue to boundary', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
Alice->>Bob: I'm short
|
||||
Bob->>Alice: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render wrapped long messages from actor to database', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "database" }
|
||||
Alice->>Bob: I'm short
|
||||
Bob->>Alice:wrap: Extremely utterly long line of longness which had previously overflown the actor box as it is much longer than what it should be
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('svg size', () => {
|
||||
it('should render a sequence diagram when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
participant John@{ "type" : "control" }
|
||||
Alice ->> Bob: Hello Bob, how are you?
|
||||
Bob-->>John: How about you John?
|
||||
Bob--x Alice: I am good thanks!
|
||||
Bob-x John: I am good thanks!
|
||||
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||
Bob-->Alice: Checking with John...
|
||||
alt either this
|
||||
Alice->>John: Yes
|
||||
else or this
|
||||
Alice->>John: No
|
||||
else or this will happen
|
||||
Alice->John: Maybe
|
||||
end
|
||||
par this happens in parallel
|
||||
Alice -->> Bob: Parallel message 1
|
||||
and
|
||||
Alice -->> John: Parallel message 2
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: true } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
expect(svg).to.have.attr('width', '100%');
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
expect(maxWidthValue).to.be.within(820 * 0.95, 820 * 1.05);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a sequence diagram when useMaxWidth is false', () => {
|
||||
renderGraph(
|
||||
`
|
||||
sequenceDiagram
|
||||
actor Alice
|
||||
participant Bob@{ "type" : "boundary" }
|
||||
participant John@{ "type" : "control" }
|
||||
Alice ->> Bob: Hello Bob, how are you?
|
||||
Bob-->>John: How about you John?
|
||||
Bob--x Alice: I am good thanks!
|
||||
Bob-x John: I am good thanks!
|
||||
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||
Bob-->Alice: Checking with John...
|
||||
alt either this
|
||||
Alice->>John: Yes
|
||||
else or this
|
||||
Alice->>John: No
|
||||
else or this will happen
|
||||
Alice->John: Maybe
|
||||
end
|
||||
par this happens in parallel
|
||||
Alice -->> Bob: Parallel message 1
|
||||
and
|
||||
Alice -->> John: Parallel message 2
|
||||
end
|
||||
`,
|
||||
{ sequence: { useMaxWidth: false } }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
expect(width).to.be.within(820 * 0.95, 820 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -602,6 +602,231 @@ State1 --> [*]
|
||||
--
|
||||
55
|
||||
}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render edge labels correctly', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
title: On The Way To Something Something DarkSide
|
||||
config:
|
||||
look: default
|
||||
theme: default
|
||||
---
|
||||
|
||||
stateDiagram-v2
|
||||
|
||||
state State1_____________
|
||||
{
|
||||
c0
|
||||
}
|
||||
|
||||
state State2_____________
|
||||
{
|
||||
c1
|
||||
}
|
||||
|
||||
state State3_____________
|
||||
{
|
||||
c7
|
||||
}
|
||||
|
||||
state State4_____________
|
||||
{
|
||||
c2
|
||||
}
|
||||
|
||||
state State5_____________
|
||||
{
|
||||
c3
|
||||
}
|
||||
|
||||
state State6_____________
|
||||
{
|
||||
c4
|
||||
}
|
||||
|
||||
state State7_____________
|
||||
{
|
||||
c5
|
||||
}
|
||||
|
||||
state State8_____________
|
||||
{
|
||||
c6
|
||||
}
|
||||
|
||||
|
||||
[*] --> State1_____________
|
||||
State1_____________ --> State2_____________ : Transition1_____
|
||||
State2_____________ --> State4_____________ : Transition2_____
|
||||
State2_____________ --> State3_____________ : Transition3_____
|
||||
State3_____________ --> State2_____________
|
||||
State4_____________ --> State2_____________ : Transition5_____
|
||||
State4_____________ --> State5_____________ : Transition6_____
|
||||
State5_____________ --> State6_____________ : Transition7_____
|
||||
State6_____________ --> State4_____________ : Transition8_____
|
||||
State2_____________ --> State7_____________ : Transition4_____
|
||||
State4_____________ --> State7_____________ : Transition4_____
|
||||
State5_____________ --> State7_____________ : Transition4_____
|
||||
State6_____________ --> State7_____________ : Transition4_____
|
||||
State7_____________ --> State1_____________ : Transition9_____
|
||||
State5_____________ --> State8_____________ : Transition10____
|
||||
State8_____________ --> State5_____________ : Transition11____
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render edge labels correctly with multiple transitions', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
title: Multiple Transitions
|
||||
config:
|
||||
look: default
|
||||
theme: default
|
||||
---
|
||||
|
||||
stateDiagram-v2
|
||||
|
||||
state State1_____________
|
||||
{
|
||||
c0
|
||||
}
|
||||
|
||||
state State2_____________
|
||||
{
|
||||
c1
|
||||
}
|
||||
|
||||
state State3_____________
|
||||
{
|
||||
c7
|
||||
}
|
||||
|
||||
state State4_____________
|
||||
{
|
||||
c2
|
||||
}
|
||||
|
||||
state State5_____________
|
||||
{
|
||||
c3
|
||||
}
|
||||
|
||||
state State6_____________
|
||||
{
|
||||
c4
|
||||
}
|
||||
|
||||
state State7_____________
|
||||
{
|
||||
c5
|
||||
}
|
||||
|
||||
state State8_____________
|
||||
{
|
||||
c6
|
||||
}
|
||||
|
||||
state State9_____________
|
||||
{
|
||||
c9
|
||||
}
|
||||
|
||||
[*] --> State1_____________
|
||||
State1_____________ --> State2_____________ : Transition1_____
|
||||
State2_____________ --> State4_____________ : Transition2_____
|
||||
State2_____________ --> State3_____________ : Transition3_____
|
||||
State3_____________ --> State2_____________
|
||||
State4_____________ --> State2_____________ : Transition5_____
|
||||
State4_____________ --> State5_____________ : Transition6_____
|
||||
State5_____________ --> State6_____________ : Transition7_____
|
||||
State6_____________ --> State4_____________ : Transition8_____
|
||||
State2_____________ --> State7_____________ : Transition4_____
|
||||
State4_____________ --> State7_____________ : Transition4_____
|
||||
State5_____________ --> State7_____________ : Transition4_____
|
||||
State6_____________ --> State7_____________ : Transition4_____
|
||||
State7_____________ --> State1_____________ : Transition9_____
|
||||
State5_____________ --> State8_____________ : Transition10____
|
||||
State8_____________ --> State5_____________ : Transition11____
|
||||
State9_____________ --> State8_____________ : Transition12____
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render edge labels correctly with multiple states', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
title: Multiple States
|
||||
config:
|
||||
look: default
|
||||
theme: default
|
||||
---
|
||||
|
||||
stateDiagram-v2
|
||||
|
||||
state State1_____________
|
||||
{
|
||||
c0
|
||||
}
|
||||
|
||||
state State2_____________
|
||||
{
|
||||
c1
|
||||
}
|
||||
|
||||
state State3_____________
|
||||
{
|
||||
c7
|
||||
}
|
||||
|
||||
state State4_____________
|
||||
{
|
||||
c2
|
||||
}
|
||||
|
||||
state State5_____________
|
||||
{
|
||||
c3
|
||||
}
|
||||
|
||||
state State6_____________
|
||||
{
|
||||
c4
|
||||
}
|
||||
|
||||
state State7_____________
|
||||
{
|
||||
c5
|
||||
}
|
||||
|
||||
state State8_____________
|
||||
{
|
||||
c6
|
||||
}
|
||||
|
||||
state State9_____________
|
||||
{
|
||||
c9
|
||||
}
|
||||
|
||||
state State10_____________
|
||||
{
|
||||
c10
|
||||
}
|
||||
|
||||
[*] --> State1_____________
|
||||
State1_____________ --> State2_____________ : Transition1_____
|
||||
State2_____________ --> State3_____________ : Transition2_____
|
||||
State3_____________ --> State4_____________ : Transition3_____
|
||||
State4_____________ --> State5_____________ : Transition4_____
|
||||
State5_____________ --> State6_____________ : Transition5_____
|
||||
State6_____________ --> State7_____________ : Transition6_____
|
||||
State7_____________ --> State8_____________ : Transition7_____
|
||||
State8_____________ --> State9_____________ : Transition8_____
|
||||
State9_____________ --> State10_____________ : Transition9_____
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
describe('XY Chart', () => {
|
||||
it('should render the simplest possible chart', () => {
|
||||
it('should render the simplest possible xy-beta chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
@@ -10,10 +10,19 @@ describe('XY Chart', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render the simplest possible xy chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart
|
||||
line [10, 30, 20]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('Should render a complete chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -26,7 +35,7 @@ describe('XY Chart', () => {
|
||||
it('Should render a chart without title', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
@@ -38,7 +47,7 @@ describe('XY Chart', () => {
|
||||
it('y-axis title not required', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
@@ -50,7 +59,7 @@ describe('XY Chart', () => {
|
||||
it('Should render a chart without y-axis with different range', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
|
||||
line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
|
||||
@@ -61,7 +70,7 @@ describe('XY Chart', () => {
|
||||
it('x axis title not required', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 14000, 3200, 9200, 9900, 3400, 6000]
|
||||
line [2000, 7000, 6500, 9200, 9500, 7500, 11000, 10200, 3200, 8500, 7000, 8800]
|
||||
@@ -72,7 +81,7 @@ describe('XY Chart', () => {
|
||||
it('Multiple plots can be rendered', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
line [23, 46, 77, 34]
|
||||
line [45, 32, 33, 12]
|
||||
bar [87, 54, 99, 85]
|
||||
@@ -86,7 +95,7 @@ describe('XY Chart', () => {
|
||||
it('Decimals and negative numbers are supported', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
y-axis -2.4 --> 3.5
|
||||
line [+1.3, .6, 2.4, -.34]
|
||||
`,
|
||||
@@ -104,7 +113,7 @@ describe('XY Chart', () => {
|
||||
height: 20
|
||||
plotReservedSpacePercent: 100
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
|
||||
`,
|
||||
{}
|
||||
@@ -130,7 +139,7 @@ describe('XY Chart', () => {
|
||||
showTick: false
|
||||
showAxisLine: false
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
|
||||
`,
|
||||
{}
|
||||
@@ -140,7 +149,7 @@ describe('XY Chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
%%{init: {"xyChart": {"width": 1000, "height": 600, "titlePadding": 5, "titleFontSize": 10, "xAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5}, "yAxis": {"labelFontSize": "20", "labelPadding": 10, "titleFontSize": 30, "titlePadding": 20, "tickLength": 10, "tickWidth": 5}, "plotBorderWidth": 5, "chartOrientation": "horizontal", "plotReservedSpacePercent": 60 }}}%%
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -181,7 +190,7 @@ describe('XY Chart', () => {
|
||||
plotReservedSpacePercent: 60
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -202,7 +211,7 @@ describe('XY Chart', () => {
|
||||
yAxis:
|
||||
showTitle: false
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -223,7 +232,7 @@ describe('XY Chart', () => {
|
||||
yAxis:
|
||||
showLabel: false
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -244,7 +253,7 @@ describe('XY Chart', () => {
|
||||
yAxis:
|
||||
showTick: false
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -265,7 +274,7 @@ describe('XY Chart', () => {
|
||||
yAxis:
|
||||
showAxisLine: false
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -294,7 +303,7 @@ describe('XY Chart', () => {
|
||||
xAxisLineColor: "#87ceeb"
|
||||
plotColorPalette: "#008000, #faba63"
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -307,7 +316,7 @@ describe('XY Chart', () => {
|
||||
it('should use the correct distances between data points', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
x-axis 0 --> 2
|
||||
line [0, 1, 0, 1]
|
||||
bar [1, 0, 1, 0]
|
||||
@@ -325,7 +334,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -344,7 +353,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -357,7 +366,7 @@ describe('XY Chart', () => {
|
||||
it('should render vertical bar chart without labels by default', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -375,7 +384,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -393,7 +402,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Multiple Bar Plots"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis "Values" 0 --> 100
|
||||
@@ -412,7 +421,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Multiple Bar Plots"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis "Values" 0 --> 100
|
||||
@@ -430,7 +439,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Single Bar Chart"
|
||||
x-axis Categories [A]
|
||||
y-axis "Value" 0 --> 100
|
||||
@@ -449,7 +458,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Single Bar Chart"
|
||||
x-axis Categories [A]
|
||||
y-axis "Value" 0 --> 100
|
||||
@@ -467,7 +476,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Decimal and Negative Values"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis -10 --> 10
|
||||
@@ -486,7 +495,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Decimal and Negative Values"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis -10 --> 10
|
||||
@@ -504,7 +513,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,b,c]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
@@ -561,7 +570,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,b,c]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
@@ -615,7 +624,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
@@ -672,7 +681,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
@@ -726,7 +735,7 @@ describe('XY Chart', () => {
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan]
|
||||
y-axis "Revenue (in $)" 3000 --> 12000
|
||||
@@ -783,7 +792,7 @@ describe('XY Chart', () => {
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan]
|
||||
y-axis "Revenue (in $)" 3000 --> 12000
|
||||
|
35
cypress/platform/darshan.html
Normal file
35
cypress/platform/darshan.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!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>
|
||||
<h1>Pie chart demos</h1>
|
||||
<pre class="mermaid">
|
||||
pie title Default text position: Animal adoption
|
||||
accTitle: simple pie char demo
|
||||
accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
|
||||
"dogs" : -60.67
|
||||
"rats" : 40.12
|
||||
</pre>
|
||||
|
||||
<hr />
|
||||
<script type="module">
|
||||
import mermaid from '/mermaid.esm.mjs';
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
logLevel: 3,
|
||||
securityLevel: 'loose',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,601 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
/* background: #333; */
|
||||
font-family: 'Arial';
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
border: 3px solid #300;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A --> B
|
||||
subgraph hello
|
||||
C --> D
|
||||
end
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A --> B
|
||||
A --> B
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
A[hello] --> A
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> C
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> c
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
subgraph D
|
||||
A --> B
|
||||
A --> B
|
||||
B --> A
|
||||
B --> A
|
||||
end
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph B2
|
||||
A --> B --> C
|
||||
B --> D
|
||||
end
|
||||
|
||||
B2 --> X
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
classDiagram
|
||||
Animal <|-- Duck
|
||||
Animal <|-- Fish
|
||||
Animal <|-- Zebra
|
||||
Animal : +int age
|
||||
Animal : +String gender
|
||||
Animal: +isMammal()
|
||||
Animal: +mate()
|
||||
class Duck{
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
+bool is_wild
|
||||
+run()
|
||||
}
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% Length of edges
|
||||
flowchart TD
|
||||
L1 --- L2
|
||||
L2 --- C
|
||||
M1 ---> C
|
||||
R1 .-> R2
|
||||
R2 <.-> C
|
||||
C -->|Label 1| E1
|
||||
C <-- Label 2 ---> E2
|
||||
C ----> E3
|
||||
C <-...-> E4
|
||||
C ======> E5
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% Stadium shape
|
||||
flowchart TD
|
||||
A([stadium shape test])
|
||||
A -->|Get money| B([Go shopping])
|
||||
B --> C([Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?])
|
||||
C -->|One| D([Laptop])
|
||||
C -->|Two| E([iPhone])
|
||||
C -->|Three| F([Car<br/>wroom wroom])
|
||||
click A "index.html#link-clicked" "link test"
|
||||
click B testClick "click test"
|
||||
classDef someclass fill:#f96;
|
||||
class A someclass;
|
||||
class C someclass;
|
||||
</pre>
|
||||
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% should render escaped without html labels
|
||||
flowchart TD
|
||||
a["<strong>Haiya</strong>"]---->b
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs in reverse order
|
||||
flowchart LR
|
||||
a -->b
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs in several levels
|
||||
flowchart LR
|
||||
b-->B
|
||||
a-->c
|
||||
subgraph O
|
||||
A
|
||||
end
|
||||
subgraph B
|
||||
c
|
||||
end
|
||||
subgraph A
|
||||
a
|
||||
b
|
||||
B
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with edges in and out
|
||||
flowchart LR
|
||||
internet
|
||||
nat
|
||||
routeur
|
||||
lb1
|
||||
lb2
|
||||
compute1
|
||||
compute2
|
||||
subgraph project
|
||||
routeur
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
lb1
|
||||
end
|
||||
subgraph subnet2
|
||||
compute2
|
||||
lb2
|
||||
end
|
||||
end
|
||||
internet --> routeur
|
||||
routeur --> subnet1 & subnet2
|
||||
subnet1 & subnet2 --> nat --> internet
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links
|
||||
flowchart LR
|
||||
subgraph main
|
||||
subgraph subcontainer
|
||||
subcontainer-child
|
||||
end
|
||||
subcontainer-child--> subcontainer-sibling
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with ingoing links
|
||||
flowchart LR
|
||||
subgraph one[One]
|
||||
subgraph sub_one[Sub One]
|
||||
_sub_one
|
||||
end
|
||||
subgraph sub_two[Sub Two]
|
||||
_sub_two
|
||||
end
|
||||
_one
|
||||
end
|
||||
|
||||
%% here, either the first or the second one
|
||||
sub_one --> sub_two
|
||||
_one --> b
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 3
|
||||
flowchart LR
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 4
|
||||
flowchart LR
|
||||
subgraph A
|
||||
a -->b
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 2
|
||||
flowchart LR
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 5
|
||||
flowchart LR
|
||||
subgraph container_Beta
|
||||
process_C-->Process_D
|
||||
end
|
||||
subgraph container_Alpha
|
||||
process_A-->process_B
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|process_C
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% More subgraphs
|
||||
flowchart LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
|
||||
three --> two
|
||||
two --> c2
|
||||
note[There are two links in this diagram]
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% nested subgraphs with outgoing links 5
|
||||
flowchart LR
|
||||
A[red text] -->|default style| B(blue text)
|
||||
C([red text]) -->|default style| D[[blue text]]
|
||||
E[(red text)] -->|default style| F((blue text))
|
||||
G>red text] -->|default style| H{blue text}
|
||||
I{{red text}} -->|default style| J[/blue text/]
|
||||
K[\\ red text\\] -->|default style| L[/blue text\\]
|
||||
M[\\ red text/] -->|default style| N[blue text];
|
||||
O(((red text))) -->|default style| P(((blue text)));
|
||||
linkStyle default color:Sienna;
|
||||
style A stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style B stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style C stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style D stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style E stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style F stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style G stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style H stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style I stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style J stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style K stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style L stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style M stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style N stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
style O stroke:#ff0000,fill:#ffcccc,color:#ff0000;
|
||||
style P stroke:#0000ff,fill:#ccccff,color:#0000ff;
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1 -- I am a long label --> a2
|
||||
a1 -- Another long label --> a2
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label]
|
||||
a2[I am another node with a mega long label]
|
||||
a3[I am a node with a super long label]
|
||||
a4[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%% labels on edges in a cluster
|
||||
flowchart RL
|
||||
a1[I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. I am a node with a super long label. ]
|
||||
a2[I am another node with a mega long label]
|
||||
a3[I am a node with a super long label]
|
||||
a4[I am another node with a mega long label]
|
||||
a1 & a2 & a3 & a4 --> a5 & a6 & a7 & a8 & a9 & a10
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
subgraph Z
|
||||
subgraph X
|
||||
a --> b
|
||||
end
|
||||
subgraph Y
|
||||
c --> d
|
||||
end
|
||||
end
|
||||
Y --> X
|
||||
X --> P
|
||||
P --> Y
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart
|
||||
|
||||
a --> b
|
||||
b --> c
|
||||
b --> d
|
||||
c --> a
|
||||
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre id="diagram3" class="mermaid">
|
||||
flowchart TD
|
||||
Start([Start]) --> Prep[Preparation Step]
|
||||
Prep --> Split{Ready to Process?}
|
||||
Split -->|Yes| T1[Task A]
|
||||
Split -->|Yes| T2[Task B]
|
||||
T1 --> Merge
|
||||
T2 --> Merge
|
||||
Merge((Join Results)) --> Finalize[Finalize Process]
|
||||
Finalize --> End([End])
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
flowchart TD
|
||||
A[Start Build] --> B[Compile Source]
|
||||
B --> C[Test Suite]
|
||||
C --> D{Tests Passed?}
|
||||
D -->|No| E[Notify Developer]
|
||||
E --> A
|
||||
D -->|Yes| F[Build Docker Image]
|
||||
|
||||
subgraph Deploy Pipeline
|
||||
F --> G[Deploy to Staging]
|
||||
G --> H[Run Integration Tests]
|
||||
H --> I{Tests Passed?}
|
||||
I -->|No| J[Rollback & Alert]
|
||||
I -->|Yes| K[Deploy to Production]
|
||||
end
|
||||
|
||||
K --> L([Success])
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Controller {
|
||||
+handleRequest(): void
|
||||
}
|
||||
|
||||
class View {
|
||||
+render(): void
|
||||
}
|
||||
|
||||
class Model {
|
||||
+getData(): any
|
||||
+setData(data: any): void
|
||||
}
|
||||
|
||||
Controller --> Model
|
||||
Controller --> View
|
||||
Model --> View : notifyChange()
|
||||
</pre
|
||||
>
|
||||
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class AuthService {
|
||||
+login(username: string, password: string): boolean
|
||||
+logout(): void
|
||||
+register(): void
|
||||
}
|
||||
|
||||
class User {
|
||||
-username: string
|
||||
-password: string
|
||||
-role: Role
|
||||
+changePassword(): void
|
||||
}
|
||||
|
||||
class Role {
|
||||
-name: string
|
||||
-permissions: string[]
|
||||
+hasPermission(): boolean
|
||||
}
|
||||
|
||||
AuthService --> User
|
||||
User --> Role
|
||||
</pre
|
||||
>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
|
||||
const staticBellIconPack = {
|
||||
prefix: 'fa6-regular',
|
||||
icons: {
|
||||
bell: {
|
||||
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
|
||||
width: 448,
|
||||
},
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'logos',
|
||||
loader: () =>
|
||||
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
|
||||
},
|
||||
{
|
||||
name: 'fa',
|
||||
loader: () => staticBellIconPack,
|
||||
},
|
||||
]);
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
window.callback = function () {
|
||||
alert('A callback was triggered');
|
||||
};
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
theme: 'redux-dark',
|
||||
// theme: 'default',
|
||||
// theme: 'forest',
|
||||
handDrawnSeed: 12,
|
||||
look: 'classic ',
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
// layout: 'dagre',
|
||||
layout: 'ipsepCola',
|
||||
// layout: 'elk',
|
||||
// layout: 'sugiyama',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'arial',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
kanban: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
fontSize: 12,
|
||||
logLevel: 0,
|
||||
securityLevel: 'loose',
|
||||
callback,
|
||||
});
|
||||
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<h1>Block diagram demos</h1>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -26,7 +26,7 @@ columns 1
|
||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
A1["square"]
|
||||
B1("rounded")
|
||||
C1(("circle"))
|
||||
@@ -36,7 +36,7 @@ block-beta
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
A1(["stadium"])
|
||||
A2[["subroutine"]]
|
||||
B1[("cylinder")]
|
||||
@@ -48,7 +48,7 @@ block-beta
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
block:e:4
|
||||
columns 2
|
||||
f
|
||||
@@ -57,7 +57,7 @@ block-beta
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
block:e:4
|
||||
columns 2
|
||||
f
|
||||
@@ -67,7 +67,7 @@ block-beta
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a:3
|
||||
block:e:3
|
||||
@@ -80,7 +80,7 @@ block-beta
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 4
|
||||
a b c d
|
||||
block:e:4
|
||||
@@ -97,19 +97,19 @@ flowchart LR
|
||||
X-- "a label" -->z
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 5
|
||||
A space B
|
||||
A --x B
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a["A wide one"] b:2 c:2 d
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a b c
|
||||
e:3
|
||||
@@ -117,7 +117,7 @@ columns 3
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block
|
||||
|
||||
A1:3
|
||||
A2:1
|
||||
|
@@ -20,12 +20,14 @@
|
||||
width: 800
|
||||
nodeAlignment: left
|
||||
---
|
||||
sankey-beta
|
||||
Revenue,Expenses,10
|
||||
Revenue,Profit,10
|
||||
Expenses,Manufacturing,5
|
||||
Expenses,Tax,3
|
||||
Expenses,Research,2
|
||||
sankey
|
||||
a,b,8
|
||||
b,c,8
|
||||
c,d,8
|
||||
d,e,8
|
||||
|
||||
x,c,4
|
||||
c,y,4
|
||||
</pre>
|
||||
|
||||
<h2>Energy flow</h2>
|
||||
@@ -40,7 +42,7 @@
|
||||
linkColor: gradient
|
||||
nodeAlignment: justify
|
||||
---
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Agricultural 'waste',Bio-conversion,124.729
|
||||
Bio-conversion,Liquid,0.597
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<body>
|
||||
<h1>XY Charts demos</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue (in $)"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -26,7 +26,7 @@
|
||||
<hr />
|
||||
<h1>XY Charts horizontal</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta horizontal
|
||||
xychart horizontal
|
||||
title "Basic xychart"
|
||||
x-axis "this is x axis" [category1, "category 2", category3, category4]
|
||||
y-axis yaxisText 10 --> 150
|
||||
@@ -36,7 +36,7 @@
|
||||
<hr />
|
||||
<h1>XY Charts only lines and bar</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
line [23, 46, 77, 34]
|
||||
line [45, 32, 33, 12]
|
||||
line [87, 54, 99, 85]
|
||||
@@ -48,13 +48,13 @@
|
||||
<hr />
|
||||
<h1>XY Charts with +ve and -ve numbers</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
line [+1.3, .6, 2.4, -.34]
|
||||
</pre>
|
||||
|
||||
<h1>XY Charts Bar with multiple category</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Basic xychart with many categories"
|
||||
x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
|
||||
y-axis yaxisText 10 --> 150
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
<h1>XY Charts line with multiple category</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Line chart with many category"
|
||||
x-axis "this is x axis" [category1, "category 2", category3, category4, category5, category6, category7]
|
||||
y-axis yaxisText 10 --> 150
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<h1>XY Charts category with large text</h1>
|
||||
<pre class="mermaid">
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Basic xychart with many categories with category overlap"
|
||||
x-axis "this is x axis" [category1, "Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", category3, category4, category5, category6, category7]
|
||||
y-axis yaxisText 10 --> 150
|
||||
@@ -89,7 +89,7 @@ config:
|
||||
height: 20
|
||||
plotReservedSpacePercent: 100
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
line [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
|
||||
</pre>
|
||||
|
||||
@@ -103,7 +103,7 @@ config:
|
||||
height: 20
|
||||
plotReservedSpacePercent: 100
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
bar [5000, 9000, 7500, 6200, 9500, 5500, 11000, 8200, 9200, 9500, 7000, 8800]
|
||||
</pre>
|
||||
|
||||
@@ -136,7 +136,7 @@ config:
|
||||
chartOrientation: horizontal
|
||||
plotReservedSpacePercent: 60
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -162,7 +162,7 @@ config:
|
||||
xAxisLineColor: "#87ceeb"
|
||||
plotColorPalette: "#008000, #faba63"
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutData
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L153)
|
||||
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)
|
||||
|
||||
## Indexable
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:153](https://github.co
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:156](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L156)
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:156](https://github.co
|
||||
|
||||
> **edges**: `Edge`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L155)
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:155](https://github.co
|
||||
|
||||
> **nodes**: `Node`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L154)
|
||||
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)
|
||||
|
@@ -1,156 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/development.md](../../packages/mermaid/src/docs/layouts/development.md).
|
||||
|
||||
# 🛠️ How to Create a New Layout Algorithm in Mermaid
|
||||
|
||||
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
|
||||
|
||||
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
Before starting, ensure the following:
|
||||
|
||||
- You have [Node.js](https://nodejs.org/) installed.
|
||||
- You have [pnpm](https://pnpm.io/) installed globally:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step-by-Step Integration
|
||||
|
||||
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Implementing Your Custom Layout Algorithm
|
||||
|
||||
### 1. Create Your Layout Folder
|
||||
|
||||
Navigate to the relevant source directory and create a folder for your new algorithm:
|
||||
|
||||
```bash
|
||||
cd packages/mermaid/src/layout
|
||||
mkdir myCustomLayout
|
||||
touch myCustomLayout/index.ts
|
||||
```
|
||||
|
||||
> 📁 You can organize supporting files, utils, and types inside this folder.
|
||||
|
||||
### 2. Register the Layout Algorithm
|
||||
|
||||
Open the file:
|
||||
|
||||
```
|
||||
packages/mermaid/src/rendering-util/render.ts
|
||||
```
|
||||
|
||||
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
|
||||
|
||||
```ts
|
||||
registerDefaultLayoutLoaders([
|
||||
...,
|
||||
{
|
||||
id: 'myCustomLayout',
|
||||
loader: () => import('../layout/myCustomLayout'),
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This tells Mermaid how to load your layout dynamically by name (`id`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Your Algorithm
|
||||
|
||||
### 3. Create a Test File
|
||||
|
||||
To visually test your layout implementation, create a test HTML file in:
|
||||
|
||||
```
|
||||
cypress/platform/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
touch cypress/platform/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
Inside the file, load your diagram like this:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module">
|
||||
import mermaid from '/dist/mermaid.esm.mjs';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
layout: 'myCustomLayout', // Use your layout here
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 4. Open in Browser
|
||||
|
||||
After running `pnpm dev`, open your test in the browser:
|
||||
|
||||
```
|
||||
http://localhost:9000/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
You should see your diagram rendered using your new layout engine.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Tips
|
||||
|
||||
- Keep your layout algorithm modular and readable.
|
||||
- Use TypeScript types and helper functions for better structure.
|
||||
- Add comments and constraints where necessary.
|
||||
- If applicable, create a unit test and add a visual test for Cypress.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example File Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
└── mermaid/
|
||||
└── src/
|
||||
└── layout/
|
||||
└── myCustomLayout/
|
||||
├── index.ts
|
||||
├── utils.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
- [ ] All dependencies installed via `pnpm i`
|
||||
- [ ] Layout folder and files created under `src/layout/`
|
||||
- [ ] Entry registered in `registerDefaultLayoutLoaders`
|
||||
- [ ] HTML test file added under `cypress/platform/`
|
||||
- [ ] Diagram renders as expected at `localhost:9000`
|
||||
- [ ] Code is linted and documented
|
||||
|
||||
---
|
||||
|
||||
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
@@ -1,138 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/introduction.md](../../packages/mermaid/src/docs/layouts/introduction.md).
|
||||
|
||||
# 📊 Layout Algorithms in Mermaid
|
||||
|
||||
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Dagre Layout
|
||||
|
||||
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
|
||||
- **Edge routing**: Attempts to reduce edge crossings and bends.
|
||||
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
|
||||
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Works in four stages:
|
||||
1. **Cycle Removal**
|
||||
2. **Layer Assignment**
|
||||
3. **Node Ordering**
|
||||
4. **Coordinate Assignment**
|
||||
- Outputs crisp layouts where edge direction is clear and logical.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- No native support for **grouped or nested structures**.
|
||||
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
|
||||
- Limited edge label placement capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🔸 ELK (Eclipse Layout Kernel)
|
||||
|
||||
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
|
||||
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
|
||||
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
|
||||
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
|
||||
- Different engines are plugged in depending on the chosen layout strategy.
|
||||
- Works well with large, complex, and deeply nested graphs.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- Requires verbose configuration for best results.
|
||||
- Can be slower than Dagre for small or simple diagrams.
|
||||
- More complex to integrate and control dynamically.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 IPSep-CoLa
|
||||
|
||||
### 🌐 Introduction
|
||||
|
||||
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
|
||||
|
||||
It is particularly useful for diagrams where:
|
||||
|
||||
- **Group integrity** is important (e.g., modules, clusters).
|
||||
- **Edge labels** need smart placement.
|
||||
- **Overlaps** must be prevented even under tight space constraints.
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ How IPSep-CoLa Works
|
||||
|
||||
#### 1. **Constraint-Based Force Simulation**:
|
||||
|
||||
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
|
||||
|
||||
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
|
||||
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
|
||||
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
|
||||
|
||||
#### 2. **Incremental Refinement**:
|
||||
|
||||
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
|
||||
|
||||
- Initial layout is produced using a base force simulation.
|
||||
- The layout is iteratively adjusted using **constraint solvers**.
|
||||
- Additional forces (spring, collision avoidance, containment) are incrementally added.
|
||||
|
||||
#### 3. **Edge Label Handling**:
|
||||
|
||||
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
|
||||
|
||||
---
|
||||
|
||||
### 📌 Use Cases
|
||||
|
||||
IPSep-CoLa is ideal for:
|
||||
|
||||
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
|
||||
- **Clustered views** (e.g., social network groupings).
|
||||
- **Diagrams with heavy labeling** where label placement affects readability.
|
||||
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Comparison Table
|
||||
|
||||
| Feature | Dagre | ELK | IPSep-CoLa |
|
||||
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
|
||||
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
|
||||
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
|
||||
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
|
||||
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
|
||||
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Summary
|
||||
|
||||
Each layout engine in Mermaid serves a different purpose:
|
||||
|
||||
- **Dagre** is best for fast, simple, and readable DAGs.
|
||||
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
|
||||
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
|
||||
|
||||
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
|
||||
|
||||
---
|
@@ -1,46 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/implementation.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/implementation.md).
|
||||
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## How IPSep-CoLa built :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Layer Assignment :
|
||||
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
|
||||
|
||||
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
|
||||
|
||||
2. Node ordering:
|
||||
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
|
||||
|
||||
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
|
||||
|
||||
3. AssignInitial positions to node :
|
||||
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 \_ nodeSpacing, 2 \_ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
|
||||
|
||||
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
|
||||
|
||||
4. Force-Directed Simulation with Constraints :
|
||||
|
||||
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
- Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
- Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
5. Incremental Refinement :
|
||||
|
||||
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
6. Adjusting the Final Layout :
|
||||
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
|
||||
|
||||
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.
|
@@ -1,186 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/layouts/ipsepcola/overview.md](../../../packages/mermaid/src/docs/layouts/ipsepcola/overview.md).
|
||||
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## Introduction :
|
||||
|
||||
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
|
||||
|
||||
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
|
||||
|
||||
## How IPSep-CoLa Works :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Graph Preprocessing :
|
||||
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
|
||||
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
|
||||
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
|
||||
|
||||
2. Force-Directed Simulation with Constraints :
|
||||
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
3. Incremental Refinement :
|
||||
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
## Key Features :
|
||||
|
||||
1. Group-Aware Layout: Maintains separation between nested structures.
|
||||
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
|
||||
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
|
||||
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
|
||||
|
||||
## Use Cases :
|
||||
|
||||
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
|
||||
2. Network Visualization (dependency graphs, data pipelines)
|
||||
3. Interactive Graph Editors (real-time layout adjustments)
|
||||
4. Clustered Data Visualization (UML diagrams, biological networks)
|
||||
|
||||
## **Examples**
|
||||
|
||||
### **Example 1**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
CEO --> MKT["Marketing Head"]
|
||||
CEO --> ENG["Engineering Head"]
|
||||
ENG --> DEV["Developer"]
|
||||
ENG --> QA["QA Tester"]
|
||||
```
|
||||
|
||||
### **Example 2**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Start["Start"] --> Red{"Is it red?"}
|
||||
Red -- Yes --> Round{"Is it round?"}
|
||||
Red -- No --> NotApple["❌ Not an Apple"]
|
||||
Round -- Yes --> Apple["✅ It's an Apple"]
|
||||
Round -- No --> NotApple2["❌ Not an Apple"]
|
||||
```
|
||||
|
||||
### **Example 3**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
A[Module A] --> B[Module B]
|
||||
A --> C[Module C]
|
||||
B --> D[Module D]
|
||||
C --> D
|
||||
D --> E[Module E]
|
||||
```
|
||||
|
||||
### **Example 4**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Source1["📦 Raw Data (CSV)"]
|
||||
Source2["🌐 API Data"]
|
||||
|
||||
Source1 --> Clean["🧹 Clean & Format"]
|
||||
Source2 --> Clean
|
||||
|
||||
Clean --> Transform["🔄 Transform Data"]
|
||||
Transform --> Load["📥 Load into Data Warehouse"]
|
||||
Load --> BI["📊 BI Dashboard"]
|
||||
```
|
||||
|
||||
### **Example 5**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class Person {
|
||||
-String name
|
||||
-int age
|
||||
+greet(): void
|
||||
}
|
||||
|
||||
class Employee {
|
||||
-int employeeId
|
||||
+calculateSalary(): float
|
||||
}
|
||||
|
||||
class Manager {
|
||||
-String department
|
||||
+assignTask(): void
|
||||
}
|
||||
|
||||
Person <|-- Employee
|
||||
Employee <|-- Manager
|
||||
```
|
||||
|
||||
### **Example 6**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
|
||||
Leaf --> Glucose["🍬 Glucose"]
|
||||
Leaf --> Oxygen["💨 Oxygen"]
|
||||
```
|
||||
|
||||
### **Example 7**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Internet["🌐 Internet"] --> Router["📡 Router"]
|
||||
Router --> Server1["🖥️ Server A"]
|
||||
Router --> Server2["🖥️ Server B"]
|
||||
Router --> Laptop["💻 Laptop"]
|
||||
|
||||
%% New device joins
|
||||
Router --> Mobile["📱 Mobile"]
|
||||
```
|
||||
|
||||
## Limitations :
|
||||
|
||||
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
|
||||
2. Parameter Tuning: Requires adjustments for different graph types.
|
||||
3. Non-Determinism: Small variations may occur between runs due to force simulation.
|
||||
|
||||
## Conclusion :
|
||||
|
||||
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.
|
@@ -9,7 +9,7 @@
|
||||
## Introduction to Block Diagrams
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -26,7 +26,7 @@ columns 1
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -80,12 +80,12 @@ At its core, a block diagram consists of blocks representing different entities
|
||||
To create a simple block diagram with three blocks labeled 'a', 'b', and 'c', the syntax is as follows:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
a b c
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
a b c
|
||||
```
|
||||
|
||||
@@ -101,13 +101,13 @@ While simple block diagrams are linear and straightforward, more complex systems
|
||||
In scenarios where you need to distribute blocks across multiple columns, you can specify the number of columns and arrange the blocks accordingly. Here's how to create a block diagram with three columns and four blocks, where the fourth block appears in a second row:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a b c d
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a b c d
|
||||
```
|
||||
@@ -130,13 +130,13 @@ In more complex diagrams, you may need blocks that span multiple columns to emph
|
||||
To create a block diagram where one block spans across two columns, you can specify the desired width for each block:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a["A label"] b:2 c:2 d
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a["A label"] b:2 c:2 d
|
||||
```
|
||||
@@ -153,7 +153,7 @@ Composite blocks, or blocks within blocks, are an advanced feature in Mermaid's
|
||||
Creating a composite block involves defining a parent block and then nesting other blocks within it. Here's how to define a composite block with nested elements:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
D
|
||||
end
|
||||
@@ -161,7 +161,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
D
|
||||
end
|
||||
@@ -180,7 +180,7 @@ Mermaid also allows for dynamic adjustment of column widths based on the content
|
||||
In diagrams with varying block sizes, Mermaid automatically adjusts the column widths to fit the largest block in each column. Here's an example:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a:3
|
||||
block:group1:2
|
||||
@@ -195,7 +195,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a:3
|
||||
block:group1:2
|
||||
@@ -215,7 +215,7 @@ This example demonstrates how Mermaid dynamically adjusts the width of the colum
|
||||
In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
columns 1
|
||||
a["A label"] b c d
|
||||
@@ -223,7 +223,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
columns 1
|
||||
a["A label"] b c d
|
||||
@@ -247,12 +247,12 @@ Mermaid supports a range of block shapes to suit different diagramming needs, fr
|
||||
To create a block with round edges, which can be used to represent a softer or more flexible component:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1("This is the text in the box")
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1("This is the text in the box")
|
||||
```
|
||||
|
||||
@@ -261,12 +261,12 @@ block-beta
|
||||
A stadium-shaped block, resembling an elongated circle, can be used for components that are process-oriented:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1(["This is the text in the box"])
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1(["This is the text in the box"])
|
||||
```
|
||||
|
||||
@@ -275,12 +275,12 @@ block-beta
|
||||
For representing subroutines or contained processes, a block with double vertical lines is useful:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[["This is the text in the box"]]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1[["This is the text in the box"]]
|
||||
```
|
||||
|
||||
@@ -289,12 +289,12 @@ block-beta
|
||||
The cylindrical shape is ideal for representing databases or storage components:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[("Database")]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1[("Database")]
|
||||
```
|
||||
|
||||
@@ -303,12 +303,12 @@ block-beta
|
||||
A circle can be used for centralized or pivotal components:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1(("This is the text in the circle"))
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1(("This is the text in the circle"))
|
||||
```
|
||||
|
||||
@@ -319,36 +319,36 @@ For decision points, use a rhombus, and for unique or specialized processes, asy
|
||||
**Asymmetric**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1>"This is the text in the box"]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1>"This is the text in the box"]
|
||||
```
|
||||
|
||||
**Rhombus**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1{"This is the text in the box"}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1{"This is the text in the box"}
|
||||
```
|
||||
|
||||
**Hexagon**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1{{"This is the text in the box"}}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1{{"This is the text in the box"}}
|
||||
```
|
||||
|
||||
@@ -357,7 +357,7 @@ block-beta
|
||||
Parallelogram and trapezoid shapes are perfect for inputs/outputs and transitional processes:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[/"This is the text in the box"/]
|
||||
id2[\"This is the text in the box"\]
|
||||
A[/"Christmas"\]
|
||||
@@ -365,7 +365,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1[/"This is the text in the box"/]
|
||||
id2[\"This is the text in the box"\]
|
||||
A[/"Christmas"\]
|
||||
@@ -377,12 +377,12 @@ block-beta
|
||||
For highlighting critical or high-priority components, a double circle can be effective:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1((("This is the text in the circle")))
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1((("This is the text in the circle")))
|
||||
```
|
||||
|
||||
@@ -395,7 +395,7 @@ Mermaid also offers unique shapes like block arrows and space blocks for directi
|
||||
Block arrows can visually indicate direction or flow within a process:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
blockArrowId<["Label"]>(right)
|
||||
blockArrowId2<["Label"]>(left)
|
||||
blockArrowId3<["Label"]>(up)
|
||||
@@ -406,7 +406,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
blockArrowId<["Label"]>(right)
|
||||
blockArrowId2<["Label"]>(left)
|
||||
blockArrowId3<["Label"]>(up)
|
||||
@@ -421,14 +421,14 @@ block-beta
|
||||
Space blocks can be used to create intentional empty spaces in the diagram, which is useful for layout and readability:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a space b
|
||||
c d e
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a space b
|
||||
c d e
|
||||
@@ -437,12 +437,12 @@ block-beta
|
||||
or
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
ida space:3 idb idc
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
ida space:3 idb idc
|
||||
```
|
||||
|
||||
@@ -467,13 +467,13 @@ The most fundamental aspect of connecting blocks is the use of arrows or links.
|
||||
A simple link with an arrow can be created to show direction or flow from one block to another:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
```
|
||||
@@ -490,13 +490,13 @@ Example - Text with Links
|
||||
To add text to a link, the syntax includes the text within the link definition:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space:2 B
|
||||
A-- "X" -->B
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A space:2 B
|
||||
A-- "X" -->B
|
||||
```
|
||||
@@ -506,7 +506,7 @@ This example show how to add descriptive text to the links, enhancing the inform
|
||||
Example - Edges and Styles:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -523,7 +523,7 @@ columns 1
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -552,7 +552,7 @@ Mermaid enables detailed styling of individual blocks, allowing you to apply var
|
||||
To apply custom styles to a block, you can use the `style` keyword followed by the block identifier and the desired CSS properties:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1 space id2
|
||||
id1("Start")-->id2("Stop")
|
||||
style id1 fill:#636,stroke:#333,stroke-width:4px
|
||||
@@ -560,7 +560,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
id1 space id2
|
||||
id1("Start")-->id2("Stop")
|
||||
style id1 fill:#636,stroke:#333,stroke-width:4px
|
||||
@@ -574,7 +574,7 @@ Mermaid enables applying styling to classes, which could make styling easier if
|
||||
#### Example - Styling a Single Class
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
|
||||
@@ -583,7 +583,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
|
||||
@@ -608,7 +608,7 @@ Combining the elements of structure, linking, and styling, we can create compreh
|
||||
Illustrating a simple software system architecture with interconnected components:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Frontend blockArrowId6<[" "]>(right) Backend
|
||||
space:2 down<[" "]>(down)
|
||||
@@ -621,7 +621,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Frontend blockArrowId6<[" "]>(right) Backend
|
||||
space:2 down<[" "]>(down)
|
||||
@@ -640,7 +640,7 @@ This example shows a basic architecture with a frontend, backend, and database.
|
||||
Representing a business process flow with decision points and multiple stages:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Start(("Start")) space:2
|
||||
down<[" "]>(down) space:2
|
||||
@@ -653,7 +653,7 @@ block-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Start(("Start")) space:2
|
||||
down<[" "]>(down) space:2
|
||||
@@ -682,7 +682,7 @@ Understanding and avoiding common syntax errors is key to a smooth experience wi
|
||||
A common mistake is incorrect linking syntax, which can lead to unexpected results or broken diagrams:
|
||||
|
||||
```
|
||||
block-beta
|
||||
block
|
||||
A - B
|
||||
```
|
||||
|
||||
@@ -690,13 +690,13 @@ block-beta
|
||||
Ensure that links between blocks are correctly specified with arrows (--> or ---) to define the direction and type of connection. Also remember that one of the fundamentals for block diagram is to give the author full control of where the boxes are positioned so in the example you need to add a space between the boxes:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A --> B
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A --> B
|
||||
```
|
||||
@@ -706,13 +706,13 @@ block-beta
|
||||
Applying styles in the wrong context or with incorrect syntax can lead to blocks not being styled as intended:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill#969;
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill#969;
|
||||
```
|
||||
@@ -721,14 +721,14 @@ Applying styles in the wrong context or with incorrect syntax can lead to blocks
|
||||
Correct the syntax by ensuring proper separation of style properties with commas and using the correct CSS property format:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill:#969,stroke:#333;
|
||||
|
||||
```
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill:#969,stroke:#333;
|
||||
|
||||
|
@@ -37,6 +37,11 @@ Drawing a pie chart is really simple in mermaid.
|
||||
- Followed by `:` colon as separator
|
||||
- Followed by `positive numeric value` (supported up to two decimal places)
|
||||
|
||||
**Note:**
|
||||
|
||||
> Pie chart values must be **positive numbers greater than zero**.\
|
||||
> **Negative values are not allowed** and will result in an error.
|
||||
|
||||
\[pie] \[showData] (OPTIONAL)
|
||||
\[title] \[titlevalue] (OPTIONAL)
|
||||
"\[datakey1]" : \[dataValue1]
|
||||
|
@@ -23,7 +23,7 @@ config:
|
||||
sankey:
|
||||
showValues: false
|
||||
---
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Agricultural 'waste',Bio-conversion,124.729
|
||||
Bio-conversion,Liquid,0.597
|
||||
@@ -101,7 +101,7 @@ config:
|
||||
sankey:
|
||||
showValues: false
|
||||
---
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Agricultural 'waste',Bio-conversion,124.729
|
||||
Bio-conversion,Liquid,0.597
|
||||
@@ -175,7 +175,7 @@ Wind,Electricity grid,289.366
|
||||
|
||||
## Syntax
|
||||
|
||||
The idea behind syntax is that a user types `sankey-beta` keyword first, then pastes raw CSV below and get the result.
|
||||
The idea behind syntax is that a user types `sankey` keyword first, then pastes raw CSV below and get the result.
|
||||
|
||||
It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.txt) with subtle **differences**:
|
||||
|
||||
@@ -187,7 +187,7 @@ It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.
|
||||
It is implied that 3 columns inside CSV should represent `source`, `target` and `value` accordingly:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
%% source,target,value
|
||||
Electricity grid,Over generation / exports,104.453
|
||||
@@ -196,7 +196,7 @@ Electricity grid,H2 conversion,27.14
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
%% source,target,value
|
||||
Electricity grid,Over generation / exports,104.453
|
||||
@@ -209,7 +209,7 @@ Electricity grid,H2 conversion,27.14
|
||||
CSV does not support empty lines without comma delimiters by default. But you can add them if needed:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Bio-conversion,Losses,26.862
|
||||
|
||||
@@ -219,7 +219,7 @@ Bio-conversion,Gas,81.144
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Bio-conversion,Losses,26.862
|
||||
|
||||
@@ -233,14 +233,14 @@ Bio-conversion,Gas,81.144
|
||||
If you need to have a comma, wrap it in double quotes:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, homes",193.026
|
||||
Pumped heat,"Heating and cooling, commercial",70.672
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, homes",193.026
|
||||
Pumped heat,"Heating and cooling, commercial",70.672
|
||||
@@ -251,14 +251,14 @@ Pumped heat,"Heating and cooling, commercial",70.672
|
||||
If you need to have double quote, put a pair of them inside quoted string:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, ""homes""",193.026
|
||||
Pumped heat,"Heating and cooling, ""commercial""",70.672
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, ""homes""",193.026
|
||||
Pumped heat,"Heating and cooling, ""commercial""",70.672
|
||||
|
@@ -74,6 +74,166 @@ sequenceDiagram
|
||||
Bob->>Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Boundary
|
||||
|
||||
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
### Queue
|
||||
|
||||
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
### Icon
|
||||
|
||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
||||
participant Bob
|
||||
Alice->>Bob: Icon participant
|
||||
Bob->>Alice: Response to icon
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
||||
participant Bob
|
||||
Alice->>Bob: Icon participant
|
||||
Bob->>Alice: Response to icon
|
||||
```
|
||||
|
||||
### Image
|
||||
|
||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Bob
|
||||
Alice->>Bob: Image participant
|
||||
Bob->>Alice: Response to image
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Bob
|
||||
Alice->>Bob: Image participant
|
||||
Bob->>Alice: Response to image
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
|
@@ -13,7 +13,7 @@
|
||||
## Example
|
||||
|
||||
```mermaid-example
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -22,7 +22,7 @@ xychart-beta
|
||||
```
|
||||
|
||||
```mermaid
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -40,7 +40,7 @@ xychart-beta
|
||||
The chart can be drawn horizontal or vertical, default value is vertical.
|
||||
|
||||
```
|
||||
xychart-beta horizontal
|
||||
xychart horizontal
|
||||
...
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ The title is a short description of the chart and it will always render on top o
|
||||
#### Example
|
||||
|
||||
```
|
||||
xychart-beta
|
||||
xychart
|
||||
title "This is a simple example"
|
||||
...
|
||||
```
|
||||
@@ -98,10 +98,10 @@ A bar chart offers the capability to graphically depict bars.
|
||||
|
||||
#### Simplest example
|
||||
|
||||
The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like
|
||||
The only two things required are the chart name (`xychart`) and one data set. So you will be able to draw a chart with a simple config like
|
||||
|
||||
```
|
||||
xychart-beta
|
||||
xychart
|
||||
line [+1.3, .6, 2.4, -.34]
|
||||
```
|
||||
|
||||
@@ -176,7 +176,7 @@ config:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -195,7 +195,7 @@ config:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
|
@@ -238,13 +238,15 @@ export function edgeTypeStr2Type(typeStr: string): string {
|
||||
}
|
||||
|
||||
export function edgeStrToEdgeData(typeStr: string): string {
|
||||
switch (typeStr.trim()) {
|
||||
case '--x':
|
||||
switch (typeStr.replace(/^[\s-]+|[\s-]+$/g, '')) {
|
||||
case 'x':
|
||||
return 'arrow_cross';
|
||||
case '--o':
|
||||
case 'o':
|
||||
return 'arrow_circle';
|
||||
default:
|
||||
case '>':
|
||||
return 'arrow_point';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-a
|
||||
const id = 'block';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*block-beta/.test(txt);
|
||||
return /^\s*block(-beta)?/.test(txt);
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
|
@@ -36,10 +36,10 @@ CRLF \u000D\u000A
|
||||
|
||||
%%
|
||||
|
||||
"block-beta" { return 'BLOCK_DIAGRAM_KEY'; }
|
||||
"block"\s+ { yy.getLogger().debug('Found space-block'); return 'block';}
|
||||
"block"\n+ { yy.getLogger().debug('Found nl-block'); return 'block';}
|
||||
"block:" { yy.getLogger().debug('Found space-block'); return 'id-block';}
|
||||
"block-beta" { yy.getLogger().debug('Found block-beta'); return 'BLOCK_DIAGRAM_KEY'; }
|
||||
"block:" { yy.getLogger().debug('Found id-block'); return 'id-block'; }
|
||||
"block" { yy.getLogger().debug('Found block'); return 'BLOCK_DIAGRAM_KEY'; }
|
||||
|
||||
// \s*\%\%.* { yy.getLogger().debug('Found comment',yytext); }
|
||||
[\s]+ { yy.getLogger().debug('.', yytext); /* skip all whitespace */ }
|
||||
[\n]+ {yy.getLogger().debug('_', yytext); /* skip all whitespace */ }
|
||||
@@ -240,7 +240,7 @@ columnsStatement
|
||||
|
||||
blockStatement
|
||||
: id-block nodeStatement document end { yy.getLogger().debug('Rule: id-block statement : ', $2, $3); const id2 = yy.generateId(); $$ = { ...$2, type:'composite', children: $3 }; }
|
||||
| block document end { yy.getLogger().debug('Rule: blockStatement : ', $1, $2, $3); const id = yy.generateId(); $$ = { id, type:'composite', label:'', children: $2 }; }
|
||||
| BLOCK_DIAGRAM_KEY document end { yy.getLogger().debug('Rule: blockStatement : ', $1, $2, $3); const id = yy.generateId(); $$ = { id, type:'composite', label:'', children: $2 }; }
|
||||
;
|
||||
|
||||
node
|
||||
|
@@ -23,7 +23,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks[0].label).toBe('id');
|
||||
});
|
||||
it('a node with a square shape and a label', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id["A label"]
|
||||
`;
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks[0].type).toBe('square');
|
||||
});
|
||||
it('a diagram with multiple nodes', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id1
|
||||
id2
|
||||
`;
|
||||
@@ -51,7 +51,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks[1].type).toBe('na');
|
||||
});
|
||||
it('a diagram with multiple nodes', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id1
|
||||
id2
|
||||
id3
|
||||
@@ -72,7 +72,7 @@ describe('Block diagram', function () {
|
||||
});
|
||||
|
||||
it('a node with a square shape and a label', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id["A label"]
|
||||
id2`;
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks[1].type).toBe('na');
|
||||
});
|
||||
it('a diagram with multiple nodes with edges abc123', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id1["first"] --> id2["second"]
|
||||
`;
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('Block diagram', function () {
|
||||
expect(edges[0].arrowTypeEnd).toBe('arrow_point');
|
||||
});
|
||||
it('a diagram with multiple nodes with edges abc123', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
id1["first"] -- "a label" --> id2["second"]
|
||||
`;
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Block diagram', function () {
|
||||
expect(edges[0].label).toBe('a label');
|
||||
});
|
||||
it('a diagram with column statements', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 2
|
||||
block1["Block 1"]
|
||||
`;
|
||||
@@ -127,7 +127,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks.length).toBe(1);
|
||||
});
|
||||
it('a diagram without column statements', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
block1["Block 1"]
|
||||
`;
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('Block diagram', function () {
|
||||
expect(blocks.length).toBe(1);
|
||||
});
|
||||
it('a diagram with auto column statements', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns auto
|
||||
block1["Block 1"]
|
||||
`;
|
||||
@@ -149,7 +149,7 @@ describe('Block diagram', function () {
|
||||
});
|
||||
|
||||
it('blocks next to each other', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 2
|
||||
block1["Block 1"]
|
||||
block2["Block 2"]
|
||||
@@ -163,7 +163,7 @@ describe('Block diagram', function () {
|
||||
});
|
||||
|
||||
it('blocks on top of each other', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 1
|
||||
block1["Block 1"]
|
||||
block2["Block 2"]
|
||||
@@ -177,7 +177,7 @@ describe('Block diagram', function () {
|
||||
});
|
||||
|
||||
it('compound blocks 2', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
block
|
||||
aBlock["ABlock"]
|
||||
bBlock["BBlock"]
|
||||
@@ -205,7 +205,7 @@ describe('Block diagram', function () {
|
||||
expect(bBlock.type).toBe('square');
|
||||
});
|
||||
it('compound blocks of compound blocks', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
block
|
||||
aBlock["ABlock"]
|
||||
block
|
||||
@@ -240,7 +240,7 @@ describe('Block diagram', function () {
|
||||
expect(bBlock.type).toBe('square');
|
||||
});
|
||||
it('compound blocks with title', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
block:compoundBlock["Compound block"]
|
||||
columns 1
|
||||
block2["Block 2"]
|
||||
@@ -265,7 +265,7 @@ describe('Block diagram', function () {
|
||||
expect(block2.type).toBe('square');
|
||||
});
|
||||
it('blocks mixed with compound blocks', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 1
|
||||
block1["Block 1"]
|
||||
|
||||
@@ -292,7 +292,7 @@ describe('Block diagram', function () {
|
||||
});
|
||||
|
||||
it('Arrow blocks', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 3
|
||||
block1["Block 1"]
|
||||
blockArrow<[" "]>(right)
|
||||
@@ -316,7 +316,7 @@ describe('Block diagram', function () {
|
||||
expect(blockArrow.directions).toContain('right');
|
||||
});
|
||||
it('Arrow blocks with multiple points', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 1
|
||||
A
|
||||
blockArrow<[" "]>(up, down)
|
||||
@@ -339,7 +339,7 @@ describe('Block diagram', function () {
|
||||
expect(blockArrow.directions).not.toContain('right');
|
||||
});
|
||||
it('blocks with different widths', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 3
|
||||
one["One Slot"]
|
||||
two["Two slots"]:2
|
||||
@@ -354,7 +354,7 @@ describe('Block diagram', function () {
|
||||
expect(two.widthInColumns).toBe(2);
|
||||
});
|
||||
it('empty blocks', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 3
|
||||
space
|
||||
middle["In the middle"]
|
||||
@@ -373,7 +373,7 @@ describe('Block diagram', function () {
|
||||
expect(middle.label).toBe('In the middle');
|
||||
});
|
||||
it('classDef statements applied to a block', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
classDef black color:#ffffff, fill:#000000;
|
||||
|
||||
mc["Memcache"]
|
||||
@@ -391,7 +391,7 @@ describe('Block diagram', function () {
|
||||
expect(black.styles[0]).toEqual('color:#ffffff');
|
||||
});
|
||||
it('style statements applied to a block', () => {
|
||||
const str = `block-beta
|
||||
const str = `block
|
||||
columns 1
|
||||
B["A wide one in the middle"]
|
||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
||||
@@ -426,9 +426,9 @@ columns 1
|
||||
|
||||
describe('prototype properties', function () {
|
||||
function validateProperty(prop: string) {
|
||||
expect(() => block.parse(`block-beta\n${prop}`)).not.toThrow();
|
||||
expect(() => block.parse(`block\n${prop}`)).not.toThrow();
|
||||
expect(() =>
|
||||
block.parse(`block-beta\nA; classDef ${prop} color:#ffffff,fill:#000000; class A ${prop}`)
|
||||
block.parse(`block\nA; classDef ${prop} color:#ffffff,fill:#000000; class A ${prop}`)
|
||||
).not.toThrow();
|
||||
}
|
||||
|
||||
|
@@ -33,13 +33,13 @@ function setupDompurifyHooks() {
|
||||
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
|
||||
|
||||
DOMPurify.addHook('beforeSanitizeAttributes', (node) => {
|
||||
if (node instanceof Element && node.tagName === 'A' && node.hasAttribute('target')) {
|
||||
if (node.tagName === 'A' && node.hasAttribute('target')) {
|
||||
node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target') ?? '');
|
||||
}
|
||||
});
|
||||
|
||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
||||
if (node instanceof Element && node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
|
||||
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
|
||||
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) ?? '');
|
||||
node.removeAttribute(TEMPORARY_ATTRIBUTE);
|
||||
if (node.getAttribute('target') === '_blank') {
|
||||
|
@@ -167,7 +167,10 @@ export const getTasks = function () {
|
||||
};
|
||||
|
||||
export const isInvalidDate = function (date, dateFormat, excludes, includes) {
|
||||
if (includes.includes(date.format(dateFormat.trim()))) {
|
||||
const formattedDate = date.format(dateFormat.trim());
|
||||
const dateOnly = date.format('YYYY-MM-DD');
|
||||
|
||||
if (includes.includes(formattedDate) || includes.includes(dateOnly)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
@@ -180,7 +183,7 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) {
|
||||
if (excludes.includes(date.format('dddd').toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
return excludes.includes(date.format(dateFormat.trim()));
|
||||
return excludes.includes(formattedDate) || excludes.includes(dateOnly);
|
||||
};
|
||||
|
||||
export const setWeekday = function (txt) {
|
||||
|
@@ -581,17 +581,11 @@ export const draw = function (text, id, version, diagObj) {
|
||||
|
||||
rectangles
|
||||
.append('rect')
|
||||
.attr('id', function (d) {
|
||||
return 'exclude-' + d.start.format('YYYY-MM-DD');
|
||||
})
|
||||
.attr('x', function (d) {
|
||||
return timeScale(d.start) + theSidePad;
|
||||
})
|
||||
.attr('id', (d) => 'exclude-' + d.start.format('YYYY-MM-DD'))
|
||||
.attr('x', (d) => timeScale(d.start.startOf('day')) + theSidePad)
|
||||
.attr('y', conf.gridLineStartPadding)
|
||||
.attr('width', function (d) {
|
||||
const renderEnd = d.end.add(1, 'day');
|
||||
return timeScale(renderEnd) - timeScale(d.start);
|
||||
})
|
||||
.attr('width', (d) => timeScale(d.end.endOf('day')) - timeScale(d.start.startOf('day')))
|
||||
|
||||
.attr('height', h - theTopPad - conf.gridLineStartPadding)
|
||||
.attr('transform-origin', function (d, i) {
|
||||
return (
|
||||
@@ -615,9 +609,20 @@ export const draw = function (text, id, version, diagObj) {
|
||||
* @param h
|
||||
*/
|
||||
function makeGrid(theSidePad, theTopPad, w, h) {
|
||||
const dateFormat = diagObj.db.getDateFormat();
|
||||
const userAxisFormat = diagObj.db.getAxisFormat();
|
||||
let axisFormat;
|
||||
if (userAxisFormat) {
|
||||
axisFormat = userAxisFormat;
|
||||
} else if (dateFormat === 'D') {
|
||||
axisFormat = '%d';
|
||||
} else {
|
||||
axisFormat = conf.axisFormat ?? '%Y-%m-%d';
|
||||
}
|
||||
|
||||
let bottomXAxis = axisBottom(timeScale)
|
||||
.tickSize(-h + theTopPad + conf.gridLineStartPadding)
|
||||
.tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
|
||||
.tickFormat(timeFormat(axisFormat));
|
||||
|
||||
const reTickInterval = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
|
||||
const resultTickInterval = reTickInterval.exec(
|
||||
@@ -669,7 +674,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
if (diagObj.db.topAxisEnabled() || conf.topAxis) {
|
||||
let topXAxis = axisTop(timeScale)
|
||||
.tickSize(-h + theTopPad + conf.gridLineStartPadding)
|
||||
.tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
|
||||
.tickFormat(timeFormat(axisFormat));
|
||||
|
||||
if (resultTickInterval !== null) {
|
||||
const every = resultTickInterval[1];
|
||||
|
@@ -139,6 +139,32 @@ describe('pie', () => {
|
||||
}).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should handle simple pie with zero slice value', async () => {
|
||||
await parser.parse(`pie title Default text position: Animal adoption
|
||||
accTitle: simple pie char demo
|
||||
accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
|
||||
"dogs" : 0
|
||||
"rats" : 40.12
|
||||
`);
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections.get('dogs')).toBe(0);
|
||||
expect(sections.get('rats')).toBe(40.12);
|
||||
});
|
||||
|
||||
it('should handle simple pie with negative slice value', async () => {
|
||||
await expect(async () => {
|
||||
await parser.parse(`pie title Default text position: Animal adoption
|
||||
accTitle: simple pie char demo
|
||||
accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
|
||||
"dogs" : -60.67
|
||||
"rats" : 40.12
|
||||
`);
|
||||
}).rejects.toThrowError(
|
||||
'"dogs" has invalid value: -60.67. Negative values are not allowed in pie charts. All slice values must be >= 0.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unsafe properties', async () => {
|
||||
await expect(
|
||||
parser.parse(`pie title Unsafe props test
|
||||
|
@@ -34,6 +34,11 @@ const clear = (): void => {
|
||||
};
|
||||
|
||||
const addSection = ({ label, value }: D3Section): void => {
|
||||
if (value < 0) {
|
||||
throw new Error(
|
||||
`"${label}" has invalid value: ${value}. Negative values are not allowed in pie charts. All slice values must be >= 0.`
|
||||
);
|
||||
}
|
||||
if (!sections.has(label)) {
|
||||
sections.set(label, value);
|
||||
log.debug(`added new section: ${label}, with value: ${value}`);
|
||||
|
@@ -10,20 +10,14 @@ import { cleanAndMerge, parseFontSize } from '../../utils.js';
|
||||
import type { D3Section, PieDB, Sections } from './pieTypes.js';
|
||||
|
||||
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {
|
||||
// Compute the position of each group on the pie:
|
||||
const sum = [...sections.values()].reduce((acc, val) => acc + val, 0);
|
||||
|
||||
const pieData: D3Section[] = [...sections.entries()]
|
||||
.map((element: [string, number]): D3Section => {
|
||||
return {
|
||||
label: element[0],
|
||||
value: element[1],
|
||||
};
|
||||
})
|
||||
.sort((a: D3Section, b: D3Section): number => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value(
|
||||
(d3Section: D3Section): number => d3Section.value
|
||||
);
|
||||
.map(([label, value]) => ({ label, value }))
|
||||
.filter((d) => (d.value / sum) * 100 >= 1) // Remove values < 1%
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value((d) => d.value);
|
||||
return pie(pieData);
|
||||
};
|
||||
|
||||
@@ -89,13 +83,21 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
||||
themeVariables.pie11,
|
||||
themeVariables.pie12,
|
||||
];
|
||||
let sum = 0;
|
||||
sections.forEach((section) => {
|
||||
sum += section;
|
||||
});
|
||||
|
||||
// Filter out arcs that would render as 0%
|
||||
const filteredArcs = arcs.filter((datum) => ((datum.data.value / sum) * 100).toFixed(0) !== '0');
|
||||
|
||||
// Set the color scale
|
||||
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
|
||||
|
||||
// Build the pie chart: each part of the pie is a path that we build using the arc function.
|
||||
group
|
||||
.selectAll('mySlices')
|
||||
.data(arcs)
|
||||
.data(filteredArcs)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', arcGenerator)
|
||||
@@ -104,15 +106,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
||||
})
|
||||
.attr('class', 'pieCircle');
|
||||
|
||||
let sum = 0;
|
||||
sections.forEach((section) => {
|
||||
sum += section;
|
||||
});
|
||||
// Now add the percentage.
|
||||
// Use the centroid method to get the best coordinates.
|
||||
group
|
||||
.selectAll('mySlices')
|
||||
.data(arcs)
|
||||
.data(filteredArcs)
|
||||
.enter()
|
||||
.append('text')
|
||||
.text((datum: d3.PieArcDatum<D3Section>): string => {
|
||||
@@ -133,15 +131,20 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
||||
.attr('class', 'pieTitleText');
|
||||
|
||||
// Add the legends/annotations for each section
|
||||
const allSectionData: D3Section[] = [...sections.entries()].map(([label, value]) => ({
|
||||
label,
|
||||
value,
|
||||
}));
|
||||
|
||||
const legend = group
|
||||
.selectAll('.legend')
|
||||
.data(color.domain())
|
||||
.data(allSectionData)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', (_datum, index: number): string => {
|
||||
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
|
||||
const offset = (height * color.domain().length) / 2;
|
||||
const offset = (height * allSectionData.length) / 2;
|
||||
const horizontal = 12 * LEGEND_RECT_SIZE;
|
||||
const vertical = index * height - offset;
|
||||
return 'translate(' + horizontal + ',' + vertical + ')';
|
||||
@@ -151,20 +154,18 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
||||
.append('rect')
|
||||
.attr('width', LEGEND_RECT_SIZE)
|
||||
.attr('height', LEGEND_RECT_SIZE)
|
||||
.style('fill', color)
|
||||
.style('stroke', color);
|
||||
.style('fill', (d) => color(d.label))
|
||||
.style('stroke', (d) => color(d.label));
|
||||
|
||||
legend
|
||||
.data(arcs)
|
||||
.append('text')
|
||||
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
|
||||
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
|
||||
.text((datum: d3.PieArcDatum<D3Section>): string => {
|
||||
const { label, value } = datum.data;
|
||||
.text((d) => {
|
||||
if (db.getShowData()) {
|
||||
return `${label} [${value}]`;
|
||||
return `${d.label} [${d.value}]`;
|
||||
}
|
||||
return label;
|
||||
return d.label;
|
||||
});
|
||||
|
||||
const longestTextWidth = Math.max(
|
||||
|
@@ -27,6 +27,7 @@ TEXTDATA [\u0020-\u0021\u0023-\u002B\u002D-\u007E]
|
||||
%%
|
||||
|
||||
<INITIAL>"sankey-beta" { this.pushState('csv'); return 'SANKEY'; }
|
||||
<INITIAL>"sankey" { this.pushState('csv'); return 'SANKEY'; }
|
||||
<INITIAL,csv><<EOF>> { return 'EOF' } // match end of file
|
||||
<INITIAL,csv>({CRLF}|{LF}) { return 'NEWLINE' }
|
||||
<INITIAL,csv>{COMMA} { return 'COMMA' }
|
||||
|
@@ -13,7 +13,7 @@ describe('Sankey diagram', function () {
|
||||
sankey.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('parses csv', () => {
|
||||
it('parses csv with sankey-beta syntax', () => {
|
||||
const csv = path.resolve(__dirname, './energy.csv');
|
||||
const data = fs.readFileSync(csv, 'utf8');
|
||||
const graphDefinition = prepareTextForParsing(cleanupComments('sankey-beta\n\n ' + data));
|
||||
@@ -21,7 +21,15 @@ describe('Sankey diagram', function () {
|
||||
sankey.parser.parse(graphDefinition);
|
||||
});
|
||||
|
||||
it('allows __proto__ as id', function () {
|
||||
it('parses csv with sankey syntax', () => {
|
||||
const csv = path.resolve(__dirname, './energy.csv');
|
||||
const data = fs.readFileSync(csv, 'utf8');
|
||||
const graphDefinition = prepareTextForParsing(cleanupComments('sankey\n\n ' + data));
|
||||
|
||||
sankey.parser.parse(graphDefinition);
|
||||
});
|
||||
|
||||
it('allows __proto__ as id with sankey-beta syntax', function () {
|
||||
sankey.parser.parse(
|
||||
prepareTextForParsing(`sankey-beta
|
||||
__proto__,A,0.597
|
||||
@@ -29,5 +37,14 @@ describe('Sankey diagram', function () {
|
||||
`)
|
||||
);
|
||||
});
|
||||
|
||||
it('allows __proto__ as id with sankey syntax', function () {
|
||||
sankey.parser.parse(
|
||||
prepareTextForParsing(`sankey
|
||||
__proto__,A,0.597
|
||||
A,__proto__,0.403
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-a
|
||||
const id = 'sankey';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*sankey-beta/.test(txt);
|
||||
return /^\s*sankey(-beta)?/.test(txt);
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
// Special states for recognizing aliases
|
||||
// A special state for grabbing text up to the first comment/newline
|
||||
%x ID ALIAS LINE
|
||||
%x ID ALIAS LINE CONFIG CONFIG_DATA
|
||||
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
@@ -28,12 +28,17 @@
|
||||
\%%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */
|
||||
[0-9]+(?=[ \n]+) return 'NUM';
|
||||
<ID>\@\{ { this.begin('CONFIG'); return 'CONFIG_START'; }
|
||||
<CONFIG>[^\}]+ { return 'CONFIG_CONTENT'; }
|
||||
<CONFIG>\} { this.popState(); this.popState(); return 'CONFIG_END'; }
|
||||
<ID>[^\<->\->:\n,;@\s]+(?=\@\{) { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
<ID>[^\<->\->:\n,;@]+?([\-]*[^\<->\->:\n,;@]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
"box" { this.begin('LINE'); return 'box'; }
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||
"create" return 'create';
|
||||
"destroy" { this.begin('ID'); return 'destroy'; }
|
||||
<ID>[^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ID>[^<\->\->:\n,;]+?([\-]*[^<\->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
||||
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
|
||||
"loop" { this.begin('LINE'); return 'loop'; }
|
||||
@@ -73,7 +78,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
"off" return 'off';
|
||||
"," return ',';
|
||||
";" return 'NEWLINE';
|
||||
[^\+\<->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\<->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
[^+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+<\->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; }
|
||||
"->>" return 'SOLID_ARROW';
|
||||
"<<->>" return 'BIDIRECTIONAL_SOLID_ARROW';
|
||||
"-->>" return 'DOTTED_ARROW';
|
||||
@@ -231,6 +236,8 @@ participant_statement
|
||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
|
||||
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
|
||||
| 'participant' actor_with_config 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant'; $$=$2;}
|
||||
|
||||
;
|
||||
|
||||
note_statement
|
||||
@@ -301,6 +308,23 @@ signal
|
||||
{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]}
|
||||
;
|
||||
|
||||
actor_with_config
|
||||
: ACTOR config_object
|
||||
{
|
||||
$$ = {
|
||||
type: 'addParticipant',
|
||||
actor: $1,
|
||||
config: $2
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
config_object
|
||||
: CONFIG_START CONFIG_CONTENT CONFIG_END
|
||||
{
|
||||
$$ = $2.trim();
|
||||
}
|
||||
;
|
||||
// actor
|
||||
// : actor_participant
|
||||
// | actor_actor
|
||||
@@ -313,7 +337,7 @@ signaltype
|
||||
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
|
||||
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
|
||||
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
|
||||
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
|
||||
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
|
||||
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
|
||||
import type { Actor, AddMessageParams, Box, Message, Note, ParticipantMetaData } from './types.js';
|
||||
|
||||
interface SequenceState {
|
||||
prevActor?: string;
|
||||
@@ -75,6 +76,19 @@ const PLACEMENT = {
|
||||
OVER: 2,
|
||||
} as const;
|
||||
|
||||
export const PARTICIPANT_TYPE = {
|
||||
ACTOR: 'actor',
|
||||
BOUNDARY: 'boundary',
|
||||
COLLECTIONS: 'collections',
|
||||
CONTROL: 'control',
|
||||
DATABASE: 'database',
|
||||
ENTITY: 'entity',
|
||||
PARTICIPANT: 'participant',
|
||||
QUEUE: 'queue',
|
||||
ICON: 'icon',
|
||||
IMAGE: 'image',
|
||||
} as const;
|
||||
|
||||
export class SequenceDB implements DiagramDB {
|
||||
private readonly state = new ImperativeState<SequenceState>(() => ({
|
||||
prevActor: undefined,
|
||||
@@ -119,9 +133,22 @@ export class SequenceDB implements DiagramDB {
|
||||
id: string,
|
||||
name: string,
|
||||
description: { text: string; wrap?: boolean | null; type: string },
|
||||
type: string
|
||||
type: string,
|
||||
metadata?: any
|
||||
) {
|
||||
let assignedBox = this.state.records.currentBox;
|
||||
let doc;
|
||||
if (metadata !== undefined) {
|
||||
let yamlData;
|
||||
// detect if shapeData contains a newline character
|
||||
if (!metadata.includes('\n')) {
|
||||
yamlData = '{\n' + metadata + '\n}';
|
||||
} else {
|
||||
yamlData = metadata + '\n';
|
||||
}
|
||||
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as ParticipantMetaData;
|
||||
}
|
||||
type = doc?.type ?? type;
|
||||
const old = this.state.records.actors.get(id);
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
@@ -160,6 +187,7 @@ export class SequenceDB implements DiagramDB {
|
||||
actorCnt: null,
|
||||
rectData: null,
|
||||
type: type ?? 'participant',
|
||||
doc: doc,
|
||||
});
|
||||
if (this.state.records.prevActor) {
|
||||
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
|
||||
@@ -518,7 +546,7 @@ export class SequenceDB implements DiagramDB {
|
||||
});
|
||||
break;
|
||||
case 'addParticipant':
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
break;
|
||||
case 'createParticipant':
|
||||
if (this.state.records.actors.has(param.actor)) {
|
||||
@@ -527,7 +555,7 @@ export class SequenceDB implements DiagramDB {
|
||||
);
|
||||
}
|
||||
this.state.records.lastCreated = param.actor;
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw);
|
||||
this.addActor(param.actor, param.actor, param.description, param.draw, param.config);
|
||||
this.state.records.createdActors.set(param.actor, this.state.records.messages.length);
|
||||
break;
|
||||
case 'destroyParticipant':
|
||||
|
@@ -350,6 +350,26 @@ Bob-->Alice-in-Wonderland:I am good thanks!`);
|
||||
expect(messages[1].from).toBe('Bob');
|
||||
});
|
||||
|
||||
it('should handle equals in participant names', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice=Wonderland
|
||||
participant Bob
|
||||
Alice=Wonderland->Bob:Hello Bob, how are - you?
|
||||
Bob-->Alice=Wonderland:I am good thanks!`);
|
||||
|
||||
const actors = diagram.db.getActors();
|
||||
expect([...actors.keys()]).toEqual(['Alice=Wonderland', 'Bob']);
|
||||
expect(actors.get('Alice=Wonderland').description).toBe('Alice=Wonderland');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
|
||||
expect(messages.length).toBe(2);
|
||||
expect(messages[0].from).toBe('Alice=Wonderland');
|
||||
expect(messages[1].from).toBe('Bob');
|
||||
});
|
||||
|
||||
it('should alias participants', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
@@ -2038,4 +2058,231 @@ Bob->>Alice:Got it!
|
||||
expect(messages[0].from).toBe('Alice');
|
||||
expect(messages[0].to).toBe('Bob');
|
||||
});
|
||||
describe('when parsing extended participant syntax ', () => {
|
||||
it('should parse a message', async () => {
|
||||
const actor1 = 'database';
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob@{ "type" : "database" }
|
||||
participant Carl@{ type: "database" }
|
||||
participant David@{ "type" : 'database' }
|
||||
participant Eve@{ type: 'database' }
|
||||
participant Favela@{ "type" : "database" }
|
||||
Bob->>+Alice: Hi Alice
|
||||
Alice->>+Bob: Hi Bob
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
});
|
||||
|
||||
it('should parse a message', async () => {
|
||||
const actor1 = 'database';
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant lead
|
||||
participant dsa@{ "type" : "queue" }
|
||||
API->>+Database: getUserb
|
||||
Database-->>-API: userb
|
||||
dsa --> Database: hello
|
||||
`);
|
||||
|
||||
const messages = diagram.db.getMessages();
|
||||
});
|
||||
});
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse boundary participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant boundary@{ "type" : "boundary" }
|
||||
boundary->boundary: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('boundary').type).toBe('boundary');
|
||||
expect(actors.get('boundary').description).toBe('boundary');
|
||||
});
|
||||
|
||||
it('should parse control participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
|
||||
it('should parse database participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant D@{ "type" : "database" }
|
||||
D->D: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('D').type).toBe('database');
|
||||
expect(actors.get('D').description).toBe('D');
|
||||
});
|
||||
|
||||
it('should parse collections participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant L@{ "type" : "collections" }
|
||||
L->L: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('L').type).toBe('collections');
|
||||
expect(actors.get('L').description).toBe('L');
|
||||
});
|
||||
|
||||
it('should parse queue participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Q@{ "type" : "queue" }
|
||||
Q->Q: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Q').type).toBe('queue');
|
||||
expect(actors.get('Q').description).toBe('Q');
|
||||
});
|
||||
});
|
||||
|
||||
describe('participant type parsing', () => {
|
||||
it('should parse actor participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant A@{ "type" : "queue" }
|
||||
A->A: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('A').type).toBe('queue');
|
||||
expect(actors.get('A').description).toBe('A');
|
||||
});
|
||||
|
||||
it('should parse participant participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant P@{ "type" : "database" }
|
||||
P->P: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('P').type).toBe('database');
|
||||
expect(actors.get('P').description).toBe('P');
|
||||
});
|
||||
|
||||
it('should parse boundary using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob@{ "type" : "control" }
|
||||
Alice->>Bob: Hello Bob, how are you?
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Alice').type).toBe('collections');
|
||||
expect(actors.get('Bob').type).toBe('control');
|
||||
expect(actors.get('Bob').description).toBe('Bob');
|
||||
});
|
||||
|
||||
it('should parse control using participant keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant C@{ "type" : "control" }
|
||||
C->C: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('C').type).toBe('control');
|
||||
expect(actors.get('C').description).toBe('C');
|
||||
});
|
||||
|
||||
it('should parse entity using actor keyword', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant E@{ "type" : "entity" }
|
||||
E->E: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('E').type).toBe('entity');
|
||||
expect(actors.get('E').description).toBe('E');
|
||||
});
|
||||
});
|
||||
|
||||
describe('image and icon participant parsing', () => {
|
||||
it('should parse image participant with image URL', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
Bob->>Bob: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Bob').type).toBe('image');
|
||||
expect(actors.get('Bob').doc.image).toBe(
|
||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse icon participant with icon name', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type": "icon", "icon": "fa:bell" }
|
||||
Alice->>Alice: test
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Alice').type).toBe('icon');
|
||||
expect(actors.get('Alice').doc.icon).toBe('fa:bell');
|
||||
});
|
||||
|
||||
it('should parse two image participants', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg" }
|
||||
Bob->>Alice: Hello
|
||||
Alice-->>Bob: Hi
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Bob').type).toBe('image');
|
||||
expect(actors.get('Bob').doc.image).toBe(
|
||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
||||
);
|
||||
expect(actors.get('Alice').type).toBe('image');
|
||||
expect(actors.get('Alice').doc.image).toBe(
|
||||
'https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse image participant with normal participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Alice
|
||||
Bob->>Alice: Hello
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Bob').type).toBe('image');
|
||||
expect(actors.get('Alice').type).toBe('participant');
|
||||
});
|
||||
|
||||
it('should parse icon participant with normal participant', async () => {
|
||||
const diagram = await Diagram.fromText(`
|
||||
sequenceDiagram
|
||||
participant Bob@{ "type": "icon", "icon": "fa:bell" }
|
||||
participant Alice
|
||||
Bob->>Alice: Hello
|
||||
`);
|
||||
const actors = diagram.db.getActors();
|
||||
expect(actors.get('Bob').type).toBe('icon');
|
||||
expect(actors.get('Alice').type).toBe('participant');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth.js';
|
||||
import utils from '../../utils.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import { PARTICIPANT_TYPE } from './sequenceDb.js';
|
||||
|
||||
let conf = {};
|
||||
|
||||
@@ -724,11 +725,21 @@ function adjustCreatedDestroyedData(
|
||||
msgModel.startx = msgModel.startx - adjustment;
|
||||
}
|
||||
}
|
||||
const actorArray = [
|
||||
PARTICIPANT_TYPE.ACTOR,
|
||||
PARTICIPANT_TYPE.CONTROL,
|
||||
PARTICIPANT_TYPE.ENTITY,
|
||||
PARTICIPANT_TYPE.DATABASE,
|
||||
PARTICIPANT_TYPE.ICON,
|
||||
PARTICIPANT_TYPE.IMAGE,
|
||||
];
|
||||
|
||||
// if it is a create message
|
||||
if (createdActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
actor.starty = lineStartY - actor.height / 2;
|
||||
bounds.bumpVerticalPos(actor.height / 2);
|
||||
@@ -737,7 +748,7 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.from) == index) {
|
||||
const actor = actors.get(msg.from);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
const adjustment = actorArray.includes(actor.type) ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
|
||||
senderAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -747,7 +758,9 @@ function adjustCreatedDestroyedData(
|
||||
else if (destroyedActors.get(msg.to) == index) {
|
||||
const actor = actors.get(msg.to);
|
||||
if (conf.mirrorActors) {
|
||||
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
|
||||
const adjustment = actorArray.includes(actor.type)
|
||||
? ACTOR_TYPE_WIDTH / 2 + 3
|
||||
: actor.width / 2 + 3;
|
||||
receiverAdjustment(actor, adjustment);
|
||||
}
|
||||
actor.stopy = lineStartY - actor.height / 2;
|
||||
@@ -1065,10 +1078,11 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
for (const box of bounds.models.boxes) {
|
||||
box.height = bounds.getVerticalPos() - box.y;
|
||||
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||
box.startx = box.x;
|
||||
box.starty = box.y;
|
||||
box.stopx = box.startx + box.width;
|
||||
box.stopy = box.starty + box.height;
|
||||
const boxPadding = conf.boxMargin * 2;
|
||||
box.startx = box.x - boxPadding;
|
||||
box.starty = box.y - boxPadding * 0.25;
|
||||
box.stopx = box.startx + box.width + 2 * boxPadding;
|
||||
box.stopy = box.starty + box.height + boxPadding * 0.75;
|
||||
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||
svgDraw.drawBox(diagram, box, conf);
|
||||
}
|
||||
@@ -1333,6 +1347,9 @@ async function calculateActorMargins(
|
||||
return (total += actors.get(aKey).width + (actors.get(aKey).margin || 0));
|
||||
}, 0);
|
||||
|
||||
const standardBoxPadding = conf.boxMargin * 8;
|
||||
totalWidth += standardBoxPadding;
|
||||
|
||||
totalWidth -= 2 * conf.boxTextMargin;
|
||||
if (box.wrap) {
|
||||
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||
|
@@ -12,6 +12,11 @@ const getStyles = (options) =>
|
||||
.actor-line {
|
||||
stroke: ${options.actorLineColor};
|
||||
}
|
||||
|
||||
.innerArc {
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
.messageLine0 {
|
||||
stroke-width: 1.5;
|
||||
@@ -115,6 +120,7 @@ const getStyles = (options) =>
|
||||
fill: ${options.actorBkg};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
@@ -3,6 +3,7 @@ import * as svgDrawCommon from '../common/svgDrawCommon.js';
|
||||
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import * as configApi from '../../config.js';
|
||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||
|
||||
export const ACTOR_TYPE_WIDTH = 18 * 2;
|
||||
const TOP_ACTOR_CLASS = 'actor-top';
|
||||
@@ -318,6 +319,89 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
|
||||
});
|
||||
};
|
||||
|
||||
const drawActorTypeIcon = async function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height / 2;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY + 25)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = 'actor-icon';
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
// Define the size of the square and icon
|
||||
const iconSize = actor.width / 5;
|
||||
const squareX = center - iconSize / 2;
|
||||
const squareY = !isFooter ? actorY + 10 : actorY;
|
||||
|
||||
// Draw a square rectangle for the actor icon background
|
||||
|
||||
actElem
|
||||
.append('rect')
|
||||
.attr('x', squareX)
|
||||
.attr('y', squareY)
|
||||
.attr('width', iconSize)
|
||||
.attr('height', iconSize)
|
||||
.attr('rx', 3) // rounded corners, optional
|
||||
.attr('ry', 3)
|
||||
.attr('fill', 'none'); // light gray background or customize as needed
|
||||
|
||||
// Render icon SVG inside the rectangle
|
||||
const iconGroup = actElem.append('g').attr('transform', `translate(${squareX}, ${squareY})`);
|
||||
|
||||
iconGroup
|
||||
.append('svg')
|
||||
.attr('width', iconSize)
|
||||
.attr('height', iconSize)
|
||||
.html(
|
||||
`<g>${await getIconSVG(actor.doc.icon, {
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
fallbackPrefix: '',
|
||||
})}</g>`
|
||||
);
|
||||
|
||||
// Add text label below icon
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
actor.x,
|
||||
actorY + (!isFooter ? 40 : 30), // positioning below the square icon
|
||||
actor.width,
|
||||
20,
|
||||
{ class: 'actor-icon-text' },
|
||||
conf
|
||||
);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an actor in the diagram with the attached line
|
||||
*
|
||||
@@ -410,6 +494,775 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
||||
|
||||
return height;
|
||||
};
|
||||
const drawActorTypeImage = async function (elem, actor, conf, isFooter) {
|
||||
const img = new Image();
|
||||
img.src = actor.doc.image ?? '';
|
||||
await img.decode();
|
||||
|
||||
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
|
||||
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
|
||||
|
||||
actor.doc.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
||||
|
||||
// Calculate image dimensions with proper sizing logic
|
||||
let imageWidth, imageHeight;
|
||||
|
||||
// Check if custom dimensions are provided and valid
|
||||
const hasValidCustomDimensions =
|
||||
actor.doc.height && actor.doc.height > 10 && actor.doc.width && actor.doc.width > 10;
|
||||
|
||||
if (hasValidCustomDimensions) {
|
||||
if (actor.doc.constraint === 'on') {
|
||||
// Maintain aspect ratio with constraint
|
||||
const customAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
||||
|
||||
if (customAspectRatio > actor.doc.imageAspectRatio) {
|
||||
// Width is the limiting factor
|
||||
imageHeight = actor.doc.height;
|
||||
imageWidth = actor.doc.height * actor.doc.imageAspectRatio;
|
||||
} else {
|
||||
// Height is the limiting factor
|
||||
imageWidth = actor.doc.width;
|
||||
imageHeight = actor.doc.width / actor.doc.imageAspectRatio;
|
||||
}
|
||||
} else {
|
||||
// Use custom dimensions without maintaining aspect ratio
|
||||
imageWidth = actor.doc.width;
|
||||
imageHeight = actor.doc.height;
|
||||
}
|
||||
} else {
|
||||
// Use default sizing based on actor width
|
||||
const defaultImageSize = actor.width / 3.5;
|
||||
|
||||
// Ensure minimum and maximum sizes
|
||||
const minSize = 30;
|
||||
const maxSize = actor.width * 0.8;
|
||||
|
||||
if (actor.doc.imageAspectRatio >= 1) {
|
||||
// Landscape or square image
|
||||
imageWidth = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
||||
} else {
|
||||
// Portrait image
|
||||
imageHeight = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
||||
imageWidth = imageHeight * actor.doc.imageAspectRatio;
|
||||
}
|
||||
|
||||
// Ensure the image doesn't exceed actor bounds
|
||||
if (imageWidth > actor.width * 0.9) {
|
||||
imageWidth = actor.width * 0.9;
|
||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + imageHeight;
|
||||
|
||||
// Calculate positioning
|
||||
const squareX = center - imageWidth / 2;
|
||||
let squareY;
|
||||
|
||||
if (isFooter) {
|
||||
squareY = actorY + (imageHeight - imageHeight * 1);
|
||||
} else {
|
||||
squareY = actorY + 5;
|
||||
}
|
||||
|
||||
// Calculate text position based on image position and size
|
||||
const textY = !isFooter ? squareY + imageHeight : actorY + imageHeight; // Place text below image for header
|
||||
|
||||
// Draw actor line for non-footer elements
|
||||
const x = center;
|
||||
const y = centerY + (isFooter ? 0 : imageHeight / 2); // Adjust line start based on image height
|
||||
const line = elem.append('g').lower();
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', x)
|
||||
.attr('y1', y) // Adjust line start based on image height
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = 'actor-image';
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
// Draw background rectangle for the actor image
|
||||
actElem
|
||||
.append('rect')
|
||||
.attr('x', squareX)
|
||||
.attr('y', squareY)
|
||||
.attr('width', imageWidth)
|
||||
.attr('height', imageHeight)
|
||||
.attr('rx', 3)
|
||||
.attr('ry', 3)
|
||||
.attr('fill', 'white')
|
||||
.attr('stroke', '#ddd')
|
||||
.attr('stroke-width', '1px');
|
||||
|
||||
// Create clipping path for the image
|
||||
const clipId = `clip-actor-${actorCnt || 'footer'}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
actElem
|
||||
.append('defs')
|
||||
.append('clipPath')
|
||||
.attr('id', clipId)
|
||||
.append('rect')
|
||||
.attr('x', squareX)
|
||||
.attr('y', squareY)
|
||||
.attr('width', imageWidth)
|
||||
.attr('height', imageHeight)
|
||||
.attr('rx', 3)
|
||||
.attr('ry', 3);
|
||||
|
||||
// Render image inside the rectangle
|
||||
const imageGroup = actElem.append('g');
|
||||
|
||||
if (actor.doc.image) {
|
||||
imageGroup
|
||||
.append('image')
|
||||
.attr('x', squareX)
|
||||
.attr('y', squareY)
|
||||
.attr('width', imageWidth)
|
||||
.attr('height', imageHeight)
|
||||
.attr('href', img.src)
|
||||
.attr('preserveAspectRatio', actor.doc.constraint === 'on' ? 'xMidYMid meet' : 'none');
|
||||
}
|
||||
|
||||
// Add text label
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
actor.x,
|
||||
textY,
|
||||
actor.width,
|
||||
20,
|
||||
{ class: 'actor-image-text' },
|
||||
conf
|
||||
);
|
||||
|
||||
// Calculate final bounds and update actor height
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws an actor in the diagram with the attached line
|
||||
*
|
||||
* @param {any} elem - The diagram we'll draw to.
|
||||
* @param {any} actor - The actor to draw.
|
||||
* @param {any} conf - DrawText implementation discriminator object
|
||||
* @param {boolean} isFooter - If the actor is the footer one
|
||||
*/
|
||||
const drawActorTypeCollections = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
var g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
var cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
// rect.rx = 3;
|
||||
// rect.ry = 3;
|
||||
rect.name = actor.name;
|
||||
|
||||
// 🔹 DRAW STACKED RECTANGLES
|
||||
const offset = 6;
|
||||
const shadowRect = {
|
||||
...rect,
|
||||
x: rect.x + offset,
|
||||
y: rect.y + (isFooter ? +offset : -offset),
|
||||
class: 'actor',
|
||||
};
|
||||
drawRect(g, shadowRect);
|
||||
const rectElem = drawRect(g, rect); // draw main rectangle on top
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
if (rectElem.node) {
|
||||
const bounds = rectElem.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeQueue = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
const ry = rect.height / 2;
|
||||
const rx = ry / (2.5 + rect.height / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
const cylinderArc = g.append('g');
|
||||
|
||||
// Main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}
|
||||
h ${rect.width - 2 * rx}
|
||||
a ${rx},${ry} 0 0 0 0,-${rect.height}
|
||||
Z
|
||||
`
|
||||
)
|
||||
.attr('class', cssclass);
|
||||
cylinderArc
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 0,${rect.height}`
|
||||
)
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', '1px')
|
||||
.attr('class', cssclass);
|
||||
|
||||
cylinderGroup.attr('transform', `translate(${rx}, ${-(rect.height / 2)})`);
|
||||
cylinderArc.attr('transform', `translate(${rect.width - rx}, ${-rect.height / 2})`);
|
||||
|
||||
actor.rectData = rect;
|
||||
|
||||
if (actor.properties?.icon) {
|
||||
const iconSrc = actor.properties.icon.trim();
|
||||
const iconX = rect.x + rect.width - 20;
|
||||
const iconY = rect.y + 10;
|
||||
if (iconSrc.charAt(0) === '@') {
|
||||
svgDrawCommon.drawEmbeddedImage(g, iconX, iconY, iconSrc.substr(1));
|
||||
} else {
|
||||
svgDrawCommon.drawImage(g, iconX, iconY, iconSrc);
|
||||
}
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
let height = actor.height;
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height;
|
||||
height = bounds.height;
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
const drawActorTypeControl = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + 30;
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'filled-head-control')
|
||||
.attr('refX', 15.5)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', '180')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||
|
||||
// Draw the base circle
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('fill', '#eaeaf7')
|
||||
.attr('stroke', '#666')
|
||||
.attr('stroke-width', 1.2);
|
||||
|
||||
// Draw looping arrow as arc path
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('marker-end', 'url(#filled-head-control)')
|
||||
.attr('transform', `translate(${cx}, ${cy - r})`);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + 2 * (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + 30,
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeEntity = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 75;
|
||||
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
|
||||
const cx = actor.x + actor.width / 2;
|
||||
const cy = actorY + 25;
|
||||
const r = 18;
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', cx)
|
||||
.attr('cy', cy)
|
||||
.attr('r', r)
|
||||
.attr('width', actor.width)
|
||||
.attr('height', actor.height);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('x1', cx - r)
|
||||
.attr('x2', cx + r)
|
||||
.attr('y1', cy + r)
|
||||
.attr('y2', cy + r)
|
||||
.attr('stroke', '#333')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf?.sequence?.labelBoxHeight ?? 0);
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? (cy + r - actorY) / 2 : (cy - actorY) / 2 + r + 2),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(${0}, ${r / 2})`);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeDatabase = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + actor.height + 2 * conf.boxTextMargin;
|
||||
|
||||
const boxplusLineGroup = elem.append('g').lower();
|
||||
let g = boxplusLineGroup;
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
if (Object.keys(actor.links || {}).length && !conf.forceMenus) {
|
||||
g.attr('onclick', popupMenuToggle(`actor${actorCnt}_popup`)).attr('cursor', 'pointer');
|
||||
}
|
||||
g.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
g = boxplusLineGroup.append('g');
|
||||
actor.actorCnt = actorCnt;
|
||||
|
||||
if (actor.links != null) {
|
||||
g.attr('id', 'root-' + actorCnt);
|
||||
}
|
||||
}
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
|
||||
let cssclass = 'actor';
|
||||
if (actor.properties?.class) {
|
||||
cssclass = actor.properties.class;
|
||||
} else {
|
||||
rect.fill = '#eaeaea';
|
||||
}
|
||||
|
||||
if (isFooter) {
|
||||
cssclass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssclass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = cssclass;
|
||||
rect.name = actor.name;
|
||||
|
||||
// Cylinder dimensions
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
const w = rect.width / 4;
|
||||
const h = rect.height * 0.8;
|
||||
const rx = w / 2;
|
||||
const ry = rx / (2.5 + w / 50);
|
||||
|
||||
// Cylinder base group
|
||||
const cylinderGroup = g.append('g');
|
||||
|
||||
const d = `
|
||||
M ${rect.x},${rect.y + ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
a ${rx},${ry} 0 0 0 -${w},0
|
||||
l 0,${h - 2 * ry}
|
||||
a ${rx},${ry} 0 0 0 ${w},0
|
||||
l 0,-${h - 2 * ry}
|
||||
`;
|
||||
// Draw the main cylinder body
|
||||
cylinderGroup
|
||||
.append('path')
|
||||
.attr('d', d)
|
||||
.attr('fill', '#eaeaea')
|
||||
.attr('stroke', '#000')
|
||||
.attr('stroke-width', 1)
|
||||
.attr('class', cssclass);
|
||||
|
||||
if (!isFooter) {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
|
||||
} else {
|
||||
cylinderGroup.attr('transform', `translate(${w * 1.5}, ${rect.height / 4 - 2 * ry})`);
|
||||
}
|
||||
actor.rectData = rect;
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
g,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? rect.height / 2 : h / 2 + ry),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_BOX_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
const lastPath = cylinderGroup.select('path:last-child');
|
||||
if (lastPath.node()) {
|
||||
const bounds = lastPath.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
}
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeBoundary = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
const center = actor.x + actor.width / 2;
|
||||
const centerY = actorY + 80;
|
||||
const radius = 30;
|
||||
const line = elem.append('g').lower();
|
||||
|
||||
if (!isFooter) {
|
||||
actorCnt++;
|
||||
line
|
||||
.append('line')
|
||||
.attr('id', 'actor' + actorCnt)
|
||||
.attr('x1', center)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', center)
|
||||
.attr('y2', 2000)
|
||||
.attr('class', 'actor-line 200')
|
||||
.attr('stroke-width', '0.5px')
|
||||
.attr('stroke', '#999')
|
||||
.attr('name', actor.name);
|
||||
|
||||
actor.actorCnt = actorCnt;
|
||||
}
|
||||
const actElem = elem.append('g');
|
||||
let cssClass = ACTOR_MAN_FIGURE_CLASS;
|
||||
if (isFooter) {
|
||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
||||
} else {
|
||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
||||
}
|
||||
actElem.attr('class', cssClass);
|
||||
actElem.attr('name', actor.name);
|
||||
|
||||
const rect = svgDrawCommon.getNoteRect();
|
||||
rect.x = actor.x;
|
||||
rect.y = actorY;
|
||||
rect.fill = '#eaeaea';
|
||||
rect.width = actor.width;
|
||||
rect.height = actor.height;
|
||||
rect.class = 'actor';
|
||||
rect.rx = 3;
|
||||
rect.ry = 3;
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-torso' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 10)
|
||||
.attr('x2', actor.x + actor.width / 2 - 15)
|
||||
.attr('y2', actorY + 10);
|
||||
|
||||
actElem
|
||||
.append('line')
|
||||
.attr('id', 'actor-man-arms' + actorCnt)
|
||||
.attr('x1', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y1', actorY + 0) // starting Y
|
||||
.attr('x2', actor.x + actor.width / 2 - radius * 2.5)
|
||||
.attr('y2', actorY + 20); // ending Y (26px long, adjust as needed)
|
||||
|
||||
actElem
|
||||
.append('circle')
|
||||
.attr('cx', actor.x + actor.width / 2)
|
||||
.attr('cy', actorY + 10)
|
||||
.attr('r', radius)
|
||||
.attr('width', actor.width);
|
||||
|
||||
const bounds = actElem.node().getBBox();
|
||||
actor.height = bounds.height + (conf.sequence.labelBoxHeight ?? 0);
|
||||
|
||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
||||
actor.description,
|
||||
actElem,
|
||||
rect.x,
|
||||
rect.y + (!isFooter ? radius / 2 : radius / 2),
|
||||
rect.width,
|
||||
rect.height,
|
||||
{ class: `actor ${ACTOR_MAN_FIGURE_CLASS}` },
|
||||
conf
|
||||
);
|
||||
|
||||
if (!isFooter) {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
} else {
|
||||
actElem.attr('transform', `translate(0,${radius / 2 + 7})`);
|
||||
}
|
||||
|
||||
// actElem.attr('transform', `translate(${rect.width / 2}, ${actorY + rect.height / 2})`);
|
||||
|
||||
return actor.height;
|
||||
};
|
||||
|
||||
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
||||
@@ -512,6 +1365,22 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
|
||||
return await drawActorTypeActor(elem, actor, conf, isFooter);
|
||||
case 'participant':
|
||||
return await drawActorTypeParticipant(elem, actor, conf, isFooter);
|
||||
case 'boundary':
|
||||
return await drawActorTypeBoundary(elem, actor, conf, isFooter);
|
||||
case 'control':
|
||||
return await drawActorTypeControl(elem, actor, conf, isFooter);
|
||||
case 'entity':
|
||||
return await drawActorTypeEntity(elem, actor, conf, isFooter);
|
||||
case 'database':
|
||||
return await drawActorTypeDatabase(elem, actor, conf, isFooter);
|
||||
case 'collections':
|
||||
return await drawActorTypeCollections(elem, actor, conf, isFooter);
|
||||
case 'queue':
|
||||
return await drawActorTypeQueue(elem, actor, conf, isFooter);
|
||||
case 'icon':
|
||||
return await drawActorTypeIcon(elem, actor, conf, isFooter);
|
||||
case 'image':
|
||||
return await drawActorTypeImage(elem, actor, conf, isFooter);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -17,6 +17,9 @@ export interface Actor {
|
||||
actorCnt: number | null;
|
||||
rectData: unknown;
|
||||
type: string;
|
||||
doc?: ParticipantMetaData; // For documentation
|
||||
iconName?: string; // For icon type
|
||||
imgSrc?: string; // For img type
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
@@ -90,3 +93,20 @@ export interface Note {
|
||||
message: string;
|
||||
wrap: boolean;
|
||||
}
|
||||
|
||||
export interface ParticipantMetaData {
|
||||
type?:
|
||||
| 'actor'
|
||||
| 'participant'
|
||||
| 'boundary'
|
||||
| 'control'
|
||||
| 'entity'
|
||||
| 'database'
|
||||
| 'collections'
|
||||
| 'queue'
|
||||
| 'icon'
|
||||
| 'img';
|
||||
icon?: string;
|
||||
img?: string;
|
||||
form?: string;
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@
|
||||
<acc_descr_multiline>[^\}]* { return "acc_descr_multiline_value"; }
|
||||
|
||||
"xychart-beta" {return 'XYCHART';}
|
||||
"xychart" {return 'XYCHART';}
|
||||
(?:"vertical"|"horizontal") {return 'CHART_ORIENTATION'}
|
||||
|
||||
"x-axis" { this.pushState("axis_data"); return "X_AXIS"; }
|
||||
|
@@ -33,44 +33,44 @@ describe('Testing xychart jison file', () => {
|
||||
clearMocks();
|
||||
});
|
||||
|
||||
it('should throw error if xychart-beta text is not there', () => {
|
||||
const str = 'xychart-beta-1';
|
||||
it('should throw error if xychart text is not there', () => {
|
||||
const str = 'xychart-1';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw error if only xychart is there', () => {
|
||||
const str = 'xychart-beta';
|
||||
const str = 'xychart';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
});
|
||||
|
||||
it('parse title of the chart within "', () => {
|
||||
const str = 'xychart-beta \n title "This is a title"';
|
||||
const str = 'xychart \n title "This is a title"';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('This is a title');
|
||||
});
|
||||
it('parse title of the chart without "', () => {
|
||||
const str = 'xychart-beta \n title oneLinertitle';
|
||||
const str = 'xychart \n title oneLinertitle';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setDiagramTitle).toHaveBeenCalledWith('oneLinertitle');
|
||||
});
|
||||
|
||||
it('parse chart orientation', () => {
|
||||
const str = 'xychart-beta vertical';
|
||||
const str = 'xychart vertical';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setOrientation).toHaveBeenCalledWith('vertical');
|
||||
});
|
||||
|
||||
it('parse chart orientation with spaces', () => {
|
||||
let str = 'xychart-beta horizontal ';
|
||||
let str = 'xychart horizontal ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setOrientation).toHaveBeenCalledWith('horizontal');
|
||||
|
||||
str = 'xychart-beta abc';
|
||||
str = 'xychart abc';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
|
||||
it('parse x-axis', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName\n';
|
||||
const str = 'xychart \nx-axis xAxisName\n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName',
|
||||
@@ -79,7 +79,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis with axis name without "', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName \n';
|
||||
const str = 'xychart \nx-axis xAxisName \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName',
|
||||
@@ -88,7 +88,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis with axis name with "', () => {
|
||||
const str = 'xychart-beta \n x-axis "xAxisName has space"\n';
|
||||
const str = 'xychart \n x-axis "xAxisName has space"\n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName has space',
|
||||
@@ -97,7 +97,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis with axis name with " with spaces', () => {
|
||||
const str = 'xychart-beta \n x-axis " xAxisName has space " \n';
|
||||
const str = 'xychart \n x-axis " xAxisName has space " \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: ' xAxisName has space ',
|
||||
@@ -106,7 +106,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis with axis name and range data', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName 45.5 --> 33 \n';
|
||||
const str = 'xychart \nx-axis xAxisName 45.5 --> 33 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName',
|
||||
@@ -115,11 +115,11 @@ describe('Testing xychart jison file', () => {
|
||||
expect(mockDB.setXAxisRangeData).toHaveBeenCalledWith(45.5, 33);
|
||||
});
|
||||
it('parse x-axis throw error for invalid range data', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName aaa --> 33 \n';
|
||||
const str = 'xychart \nx-axis xAxisName aaa --> 33 \n';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse x-axis with axis name and range data with only decimal part', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName 45.5 --> .34 \n';
|
||||
const str = 'xychart \nx-axis xAxisName 45.5 --> .34 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName',
|
||||
@@ -129,7 +129,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis without axis name and range data', () => {
|
||||
const str = 'xychart-beta \nx-axis 45.5 --> 1.34 \n';
|
||||
const str = 'xychart \nx-axis 45.5 --> 1.34 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: '',
|
||||
@@ -139,7 +139,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis with axis name and category data', () => {
|
||||
const str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] \n ';
|
||||
const str = 'xychart \nx-axis xAxisName [ "cat1" , cat2a ] \n ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'xAxisName',
|
||||
@@ -155,7 +155,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis without axis name and category data', () => {
|
||||
const str = 'xychart-beta \nx-axis [ "cat1" , cat2a ] \n ';
|
||||
const str = 'xychart \nx-axis [ "cat1" , cat2a ] \n ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({
|
||||
text: '',
|
||||
@@ -171,14 +171,14 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis throw error if unbalanced bracket', () => {
|
||||
let str = 'xychart-beta \nx-axis xAxisName [ "cat1" [ cat2a ] \n ';
|
||||
let str = 'xychart \nx-axis xAxisName [ "cat1" [ cat2a ] \n ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
str = 'xychart-beta \nx-axis xAxisName [ "cat1" , cat2a ] ] \n ';
|
||||
str = 'xychart \nx-axis xAxisName [ "cat1" , cat2a ] ] \n ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
|
||||
it('parse x-axis complete variant 1', () => {
|
||||
const str = `xychart-beta \n x-axis "this is x axis" [category1, "category 2", category3]\n`;
|
||||
const str = `xychart \n x-axis "this is x axis" [category1, "category 2", category3]\n`;
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'this is x axis', type: 'text' });
|
||||
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
|
||||
@@ -189,8 +189,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse x-axis complete variant 2', () => {
|
||||
const str =
|
||||
'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n ';
|
||||
const str = 'xychart \nx-axis xAxisName [ "cat1 with space" , cat2 , cat3] \n ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
|
||||
@@ -202,7 +201,7 @@ describe('Testing xychart jison file', () => {
|
||||
|
||||
it('parse x-axis complete variant 3', () => {
|
||||
const str =
|
||||
'xychart-beta \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n ';
|
||||
'xychart \nx-axis xAxisName [ "cat1 with space" , cat2 asdf , cat3] \n ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisBand).toHaveBeenCalledWith([
|
||||
@@ -213,17 +212,17 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
|
||||
it('parse y-axis with axis name', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName\n';
|
||||
const str = 'xychart \ny-axis yAxisName\n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
});
|
||||
it('parse y-axis with axis name with spaces', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName \n';
|
||||
const str = 'xychart \ny-axis yAxisName \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
});
|
||||
it('parse y-axis with axis name with "', () => {
|
||||
const str = 'xychart-beta \n y-axis "yAxisName has space"\n';
|
||||
const str = 'xychart \n y-axis "yAxisName has space"\n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
|
||||
text: 'yAxisName has space',
|
||||
@@ -231,7 +230,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
});
|
||||
it('parse y-axis with axis name with " and spaces', () => {
|
||||
const str = 'xychart-beta \n y-axis " yAxisName has space " \n';
|
||||
const str = 'xychart \n y-axis " yAxisName has space " \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({
|
||||
text: ' yAxisName has space ',
|
||||
@@ -239,39 +238,39 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
});
|
||||
it('parse y-axis with axis name with range data', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> 33 \n';
|
||||
const str = 'xychart \ny-axis yAxisName 45.5 --> 33 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33);
|
||||
});
|
||||
it('parse y-axis without axis name with range data', () => {
|
||||
const str = 'xychart-beta \ny-axis 45.5 --> 33 \n';
|
||||
const str = 'xychart \ny-axis 45.5 --> 33 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: '', type: 'text' });
|
||||
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 33);
|
||||
});
|
||||
it('parse y-axis with axis name with range data with only decimal part', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> .33 \n';
|
||||
const str = 'xychart \ny-axis yAxisName 45.5 --> .33 \n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setYAxisRangeData).toHaveBeenCalledWith(45.5, 0.33);
|
||||
});
|
||||
it('parse y-axis throw error for invalid number in range data', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName 45.5 --> abc \n';
|
||||
const str = 'xychart \ny-axis yAxisName 45.5 --> abc \n';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse y-axis throws error if range data is passed', () => {
|
||||
const str = 'xychart-beta \ny-axis yAxisName [ 45.3, 33 ] \n';
|
||||
const str = 'xychart \ny-axis yAxisName [ 45.3, 33 ] \n';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse both axis at once', () => {
|
||||
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
});
|
||||
it('parse line Data', () => {
|
||||
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line lineTitle [23, 45, 56.6]';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setLineData).toHaveBeenCalledWith(
|
||||
{ text: 'lineTitle', type: 'text' },
|
||||
@@ -282,7 +281,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
it('parse line Data with spaces and +,- symbols', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -292,8 +291,7 @@ describe('Testing xychart jison file', () => {
|
||||
);
|
||||
});
|
||||
it('parse line Data without title', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] ';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line [ +23 , -45 , 56.6 , .33] ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -304,34 +302,32 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
it('parse line Data throws error unbalanced brackets', () => {
|
||||
let str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 [ -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 [ -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 ] 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -45 ] 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse line Data throws error if data is not provided', () => {
|
||||
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" ';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse line Data throws error if data is empty', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ ] ';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse line Data throws error if , is not in proper', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , , -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , , -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse line Data throws error if not number', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n line "lineTitle with space" [ +23 , -4aa5 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse bar Data', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle [23, 45, 56.6, .22]';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -342,7 +338,7 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
it('parse bar Data spaces and +,- symbol', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -352,8 +348,7 @@ describe('Testing xychart jison file', () => {
|
||||
);
|
||||
});
|
||||
it('parse bar Data without plot title', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] ';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar [ +23 , -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -361,34 +356,34 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
it('parse bar should throw for unbalanced brackets', () => {
|
||||
let str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 [ -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 [ -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 ] 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -45 ] 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse bar should throw error if data is not provided', () => {
|
||||
const str = 'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" ';
|
||||
const str = 'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse bar should throw error if data is empty', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse bar should throw error if comma is not proper', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , , -45 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , , -45 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse bar should throw error if number is not passed', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] ';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar "barTitle with space" [ +23 , -4aa5 , 56.6 ] ';
|
||||
expect(parserFnConstructor(str)).toThrow();
|
||||
});
|
||||
it('parse multiple bar and line variant 1', () => {
|
||||
const str =
|
||||
'xychart-beta\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle1 [23, 45, 56.6] \n line lineTitle1 [11, 45.5, 67, 23] \n bar barTitle2 [13, 42, 56.89] \n line lineTitle2 [45, 99, 012]';
|
||||
'xychart\nx-axis xAxisName\ny-axis yAxisName\n bar barTitle1 [23, 45, 56.6] \n line lineTitle1 [11, 45.5, 67, 23] \n bar barTitle2 [13, 42, 56.89] \n line lineTitle2 [45, 99, 012]';
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
expect(mockDB.setYAxisTitle).toHaveBeenCalledWith({ text: 'yAxisName', type: 'text' });
|
||||
expect(mockDB.setXAxisTitle).toHaveBeenCalledWith({ text: 'xAxisName', type: 'text' });
|
||||
@@ -411,8 +406,8 @@ describe('Testing xychart jison file', () => {
|
||||
});
|
||||
it('parse multiple bar and line variant 2', () => {
|
||||
const str = `
|
||||
xychart-beta horizontal
|
||||
title Basic xychart
|
||||
xychart horizontal
|
||||
title "Basic xychart"
|
||||
x-axis "this is x axis" [category1, "category 2", category3]
|
||||
y-axis yaxisText 10 --> 150
|
||||
bar barTitle1 [23, 45, 56.6]
|
||||
|
@@ -7,7 +7,7 @@ import type {
|
||||
const id = 'xychart';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*xychart-beta/.test(txt);
|
||||
return /^\s*xychart(-beta)?/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
|
@@ -141,7 +141,6 @@ function sidebarAll() {
|
||||
],
|
||||
},
|
||||
...sidebarSyntax(),
|
||||
...sidebarAlgorithms(),
|
||||
...sidebarEcosystem(),
|
||||
...sidebarConfig(),
|
||||
...sidebarCommunity(),
|
||||
@@ -224,27 +223,6 @@ function sidebarEcosystem() {
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarAlgorithms() {
|
||||
return [
|
||||
{
|
||||
text: '🧠 Diagram Algorithms',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/layouts/introduction' },
|
||||
{ text: 'Layout Algorithm Development', link: '/layouts/development' },
|
||||
{
|
||||
text: 'IPSep-Cola',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Overview', link: '/layouts/ipsepcola/overview' },
|
||||
{ text: 'Implementation', link: '/layouts/ipsepcola/implementation' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarCommunity() {
|
||||
return [
|
||||
{
|
||||
|
@@ -1,150 +0,0 @@
|
||||
# 🛠️ How to Create a New Layout Algorithm in Mermaid
|
||||
|
||||
Mermaid supports pluggable layout engines, and contributors can add custom layout algorithms to support specialized rendering needs such as clustered layouts, nested structures, or domain-specific visualizations.
|
||||
|
||||
This guide outlines the steps required to **create and integrate a new layout algorithm** into the Mermaid codebase.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
Before starting, ensure the following:
|
||||
|
||||
- You have [Node.js](https://nodejs.org/) installed.
|
||||
- You have [pnpm](https://pnpm.io/) installed globally:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Step-by-Step Integration
|
||||
|
||||
### Refer [Mermaid Contributing Guide](../community/contributing.md)
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Implementing Your Custom Layout Algorithm
|
||||
|
||||
### 1. Create Your Layout Folder
|
||||
|
||||
Navigate to the relevant source directory and create a folder for your new algorithm:
|
||||
|
||||
```bash
|
||||
cd packages/mermaid/src/layout
|
||||
mkdir myCustomLayout
|
||||
touch myCustomLayout/index.ts
|
||||
```
|
||||
|
||||
> 📁 You can organize supporting files, utils, and types inside this folder.
|
||||
|
||||
### 2. Register the Layout Algorithm
|
||||
|
||||
Open the file:
|
||||
|
||||
```
|
||||
packages/mermaid/src/rendering-util/render.ts
|
||||
```
|
||||
|
||||
Inside the function `registerDefaultLayoutLoaders`, find the `layoutLoaders` array. Add your layout here:
|
||||
|
||||
```ts
|
||||
registerDefaultLayoutLoaders([
|
||||
...,
|
||||
{
|
||||
id: 'myCustomLayout',
|
||||
loader: () => import('../layout/myCustomLayout'),
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This tells Mermaid how to load your layout dynamically by name (`id`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Your Algorithm
|
||||
|
||||
### 3. Create a Test File
|
||||
|
||||
To visually test your layout implementation, create a test HTML file in:
|
||||
|
||||
```
|
||||
cypress/platform/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
touch cypress/platform/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
Inside the file, load your diagram like this:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module">
|
||||
import mermaid from '/dist/mermaid.esm.mjs';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
layout: 'myCustomLayout', // Use your layout here
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mermaid">graph TD A[Node A] --> B[Node B] B --> C[Node C]</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 4. Open in Browser
|
||||
|
||||
After running `pnpm dev`, open your test in the browser:
|
||||
|
||||
```
|
||||
http://localhost:9000/myCustomLayoutTest.html
|
||||
```
|
||||
|
||||
You should see your diagram rendered using your new layout engine.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Tips
|
||||
|
||||
- Keep your layout algorithm modular and readable.
|
||||
- Use TypeScript types and helper functions for better structure.
|
||||
- Add comments and constraints where necessary.
|
||||
- If applicable, create a unit test and add a visual test for Cypress.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Example File Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
└── mermaid/
|
||||
└── src/
|
||||
└── layout/
|
||||
└── myCustomLayout/
|
||||
├── index.ts
|
||||
├── utils.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
- [ ] All dependencies installed via `pnpm i`
|
||||
- [ ] Layout folder and files created under `src/layout/`
|
||||
- [ ] Entry registered in `registerDefaultLayoutLoaders`
|
||||
- [ ] HTML test file added under `cypress/platform/`
|
||||
- [ ] Diagram renders as expected at `localhost:9000`
|
||||
- [ ] Code is linted and documented
|
||||
|
||||
---
|
||||
|
||||
> 💡 You’re now ready to build advanced layout algorithms and contribute to Mermaid's growing visualization capabilities!
|
@@ -1,132 +0,0 @@
|
||||
# 📊 Layout Algorithms in Mermaid
|
||||
|
||||
Mermaid is a popular JavaScript-based diagramming tool that supports auto-layout for graphs using pluggable layout engines. Layout algorithms play a critical role in rendering nodes and edges in a clean, readable, and meaningful way. Mermaid currently uses engines like **Dagre** and **ELK**, and will soon introduce a powerful new layout engine: **IPSep-CoLa**.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Dagre Layout
|
||||
|
||||
**Dagre** is a layout engine inspired by the **Sugiyama algorithm**, optimized for directed acyclic graphs (DAGs). It arranges nodes in layers and computes edge routing to minimize crossings and improve readability.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Layered (Sugiyama-style) layout**: Ideal for top-down or left-to-right flow.
|
||||
- **Edge routing**: Attempts to reduce edge crossings and bends.
|
||||
- **Ranking**: Vertices are assigned ranks to group related elements into the same level.
|
||||
- **Lightweight and fast**: Suitable for small to medium-sized graphs.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Works in four stages:
|
||||
1. **Cycle Removal**
|
||||
2. **Layer Assignment**
|
||||
3. **Node Ordering**
|
||||
4. **Coordinate Assignment**
|
||||
- Outputs crisp layouts where edge direction is clear and logical.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- No native support for **grouped or nested structures**.
|
||||
- Not ideal for graphs with **non-hierarchical** or **dense cyclic connections**.
|
||||
- Limited edge label placement capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🔸 ELK (Eclipse Layout Kernel)
|
||||
|
||||
**ELK** is a modular, extensible layout framework developed as part of the Eclipse ecosystem. It supports a wide variety of graph types and layout strategies.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Multiple layout styles**: Hierarchical, force-based, layered, orthogonal, etc.
|
||||
- **Support for ports**: Allows fine-grained edge anchoring on specific sides of nodes.
|
||||
- **Group and hierarchy awareness**: Ideal for nested and compartmentalized diagrams.
|
||||
- **Rich configuration**: Offers control over spacing, edge routing, direction, padding, and more.
|
||||
|
||||
### Technical Overview:
|
||||
|
||||
- Uses a **model-driven approach** with a well-defined intermediate representation (ELK Graph Model).
|
||||
- Different engines are plugged in depending on the chosen layout strategy.
|
||||
- Works well with large, complex, and deeply nested graphs.
|
||||
|
||||
### Limitations:
|
||||
|
||||
- Requires verbose configuration for best results.
|
||||
- Can be slower than Dagre for small or simple diagrams.
|
||||
- More complex to integrate and control dynamically.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 IPSep-CoLa
|
||||
|
||||
### 🌐 Introduction
|
||||
|
||||
**IPSep-CoLa** stands for **Incremental Procedure for Separation Constraint Layout**, a next-generation layout algorithm tailored for **grouped, nested, and labeled graphs**. It is an enhancement over standard force-directed layouts, offering constraint enforcement and iterative refinement.
|
||||
|
||||
It is particularly useful for diagrams where:
|
||||
|
||||
- **Group integrity** is important (e.g., modules, clusters).
|
||||
- **Edge labels** need smart placement.
|
||||
- **Overlaps** must be prevented even under tight space constraints.
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ How IPSep-CoLa Works
|
||||
|
||||
#### 1. **Constraint-Based Force Simulation**:
|
||||
|
||||
It builds on top of standard force-directed approaches (like CoLa), but adds **constraints** to influence the final positions of nodes:
|
||||
|
||||
- **Separation constraints**: Minimum distances between nodes, edge labels, and groups.
|
||||
- **Containment constraints**: Child nodes must stay within the bounds of parent groups.
|
||||
- **Alignment constraints**: Nodes can be aligned in rows or columns if desired.
|
||||
|
||||
#### 2. **Incremental Refinement**:
|
||||
|
||||
Unlike one-pass algorithms, IPSep-CoLa works in **phases**:
|
||||
|
||||
- Initial layout is produced using a base force simulation.
|
||||
- The layout is iteratively adjusted using **constraint solvers**.
|
||||
- Additional forces (spring, collision avoidance, containment) are incrementally added.
|
||||
|
||||
#### 3. **Edge Label Handling**:
|
||||
|
||||
One of the distinguishing features of IPSep-CoLa is its support for **multi-segment edge routing with mid-edge label positioning**, ensuring labels do not clutter or overlap.
|
||||
|
||||
---
|
||||
|
||||
### 📌 Use Cases
|
||||
|
||||
IPSep-CoLa is ideal for:
|
||||
|
||||
- **Hierarchical graphs** with complex nesting (e.g., software architecture, UML diagrams).
|
||||
- **Clustered views** (e.g., social network groupings).
|
||||
- **Diagrams with heavy labeling** where label placement affects readability.
|
||||
- **Diagrams with strict visual structure** needs — maintaining boundaries, margins, or padding.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Comparison Table
|
||||
|
||||
| Feature | Dagre | ELK | IPSep-CoLa |
|
||||
| ------------------------- | ----------- | ------------------- | ------------------------------ |
|
||||
| Layout Type | Layered DAG | Modular (varied) | Constraint-driven force layout |
|
||||
| Edge Labeling | ⚠️ Basic | ✅ Yes | ✅ Smart Placement |
|
||||
| Overlap Avoidance | ⚠️ Partial | ✅ Configurable | ✅ Automatic |
|
||||
| Layout Performance | ✅ Fast | ⚠️ Medium | ⚠️ Medium |
|
||||
| Customization Flexibility | ⚠️ Limited | ✅ Extensive | ✅ Moderate to High |
|
||||
| Best For | Simple DAGs | Complex hierarchies | Grouped and labeled graphs |
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Summary
|
||||
|
||||
Each layout engine in Mermaid serves a different purpose:
|
||||
|
||||
- **Dagre** is best for fast, simple, and readable DAGs.
|
||||
- **ELK** is powerful for modular, layered, or port-based diagrams with a need for rich customization.
|
||||
- **IPSep-CoLa** will soon offer a flexible, constraint-respecting layout engine that excels at **visual clarity in grouped and complex diagrams**.
|
||||
|
||||
The addition of IPSep-CoLa to Mermaid's layout stack represents a significant leap forward in layout control and quality — making it easier than ever to visualize rich, structured, and annotated graphs.
|
||||
|
||||
---
|
@@ -1,40 +0,0 @@
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## How IPSep-CoLa built :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Layer Assignment :
|
||||
The layer assignment algorithm organizes nodes into hierarchical layers to create a structured layout for directed graphs. It begins by detecting and temporarily removing cyclic edges using a depth-first search (DFS) approach, ensuring the graph becomes a Directed Acyclic Graph (DAG) for proper layering. The algorithm then performs a topological sort using Kahn's method, calculating node ranks (layers) based on in-degree counts. Each node's layer is determined by its position in the topological order, with parent nodes always appearing in higher layers than their children to maintain proper flow direction.
|
||||
|
||||
The implementation handles special cases like nested nodes by considering parent-child relationships when calculating layers. Nodes without dependencies are placed in layer 0, while subsequent nodes are assigned to layers one level below their nearest parent. The algorithm efficiently processes nodes using a queue system, decrementing in-degrees as it progresses, and ultimately stores the layer information directly in the node objects. Though cyclic edges are removed during processing, they could potentially be reintroduced after layer assignment if needed for visualization purposes.
|
||||
|
||||
2. Node ordering:
|
||||
After assigning layers to nodes, this step organizes nodes horizontally within each layer to minimize edge crossings and create a clean, readable layout. It uses the barycenter method—a technique that positions each node based on the average position of its connected neighbors (either incoming or outgoing). Nodes with no connections are pushed to the end of their layer.
|
||||
|
||||
The algorithm works in multiple passes (iterations) to refine the order: first adjusting nodes based on their incoming connections (from the layer above), then outgoing connections (to the layer below). Group nodes (like containers) are handled separately—their position is determined by averaging the positions of their children, ensuring they stay properly aligned with their contents. This approach keeps the layout structured while reducing visual clutter.
|
||||
|
||||
3. AssignInitial positions to node :
|
||||
This step calculates the starting (x, y) positions for each node based on its assigned layer (vertical level) and order (horizontal position). Nodes are spaced evenly—horizontally using nodeSpacing and vertically using layerHeight. For example, a node in layer 2 with order 3 will be placed at (3 _ nodeSpacing, 2 _ layerHeight). This creates a grid-like structure where nodes align neatly in rows (layers) and columns (orders).
|
||||
|
||||
The initial positioning is simple but crucial—it provides a structured starting point before more advanced adjustments (like reducing edge crossings or compacting the layout) are applied. Group nodes follow the same logic, ensuring they align with their children. This method ensures a readable, organized foundation for further refinement.
|
||||
|
||||
4. Force-Directed Simulation with Constraints :
|
||||
|
||||
- Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
- Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
- Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
- Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
5. Incremental Refinement :
|
||||
|
||||
- Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
- Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
- Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
6. Adjusting the Final Layout :
|
||||
This step takes the calculated node positions and applies them to the visual elements of the graph. Nodes are placed at their assigned (x, y) coordinates—regular nodes are positioned directly, while group nodes (clusters) are rendered as containers that may include other nodes. Edges (connections between nodes) are drawn based on their start and end points, ensuring they follow the structured layout.
|
||||
|
||||
The adjustment phase bridges the mathematical layout with the actual rendering, updating the SVG or canvas elements to reflect the computed positions. This ensures that the graph is not only logically organized but also visually coherent, with proper spacing, alignment, and connections. The result is a clean, readable diagram ready for display.
|
@@ -1,180 +0,0 @@
|
||||
## IPSEPCOLA Documentation :
|
||||
|
||||
IPSep-CoLa: An Incremental Procedure for Separation Constraint Layout of Graphs
|
||||
|
||||
## Introduction :
|
||||
|
||||
IPSep-CoLa (Incremental Procedure for Separation Constraint Layout) is an advanced graph layout algorithm designed to handle complex diagrams with separation constraints, such as grouped nodes, edge labels, and hierarchical structures. Unlike traditional force-directed algorithms, IPSep-CoLa incrementally refines node positions while enforcing geometric constraints to prevent overlaps, maintain group cohesion, and optimize edge routing.
|
||||
|
||||
The algorithm is particularly effective for visualizing nested and clustered graphs, where maintaining clear separation between elements is crucial. It combines techniques from force-directed layout, constraint satisfaction, and incremental refinement to produce readable and aesthetically pleasing diagrams.
|
||||
|
||||
## How IPSep-CoLa Works :
|
||||
|
||||
IPSep-CoLa follows a multi-stage process to compute a well-structured layout:
|
||||
|
||||
1. Graph Preprocessing :
|
||||
Cycle Removal: Detects and temporarily removes cyclic dependencies to enable proper layering.
|
||||
Layer Assignment: Assigns nodes to hierarchical layers using topological sorting.
|
||||
Node Ordering: Uses the barycenter heuristic to minimize edge crossings within layers.
|
||||
|
||||
2. Force-Directed Simulation with Constraints :
|
||||
Spring Forces: Attracts connected nodes to maintain desired edge lengths.
|
||||
Repulsion Forces: Pushes nodes apart to prevent overlaps.
|
||||
Group Constraints: Ensures child nodes stay near their parent groups.
|
||||
Cooling Factor: Gradually reduces movement to stabilize the layout.
|
||||
|
||||
3. Incremental Refinement :
|
||||
Overlap Resolution: Iteratively adjusts node positions to eliminate overlaps.
|
||||
Edge Routing: Computes smooth paths for edges, including curved paths for parallel edges and self-loops.
|
||||
Group Boundary Adjustment: Dynamically resizes group containers to fit nested elements.
|
||||
|
||||
## Key Features :
|
||||
|
||||
1. Group-Aware Layout: Maintains separation between nested structures.
|
||||
2. Edge Label Placement: Uses edge labels as virtual nodes and automatically positions labels inside their parent groups.
|
||||
3. Stable Convergence: Uses cooling factors and incremental updates for smooth refinement.
|
||||
4. Support for Self-Loops & Parallel Edges: Avoids visual clutter with intelligent edge routing.
|
||||
|
||||
## Use Cases :
|
||||
|
||||
1. Hierarchical Diagrams (org charts, flowcharts, decision trees)
|
||||
2. Network Visualization (dependency graphs, data pipelines)
|
||||
3. Interactive Graph Editors (real-time layout adjustments)
|
||||
4. Clustered Data Visualization (UML diagrams, biological networks)
|
||||
|
||||
## **Examples**
|
||||
|
||||
### **Example 1**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
CEO --> MKT["Marketing Head"]
|
||||
CEO --> ENG["Engineering Head"]
|
||||
ENG --> DEV["Developer"]
|
||||
ENG --> QA["QA Tester"]
|
||||
```
|
||||
|
||||
### **Example 2**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Start["Start"] --> Red{"Is it red?"}
|
||||
Red -- Yes --> Round{"Is it round?"}
|
||||
Red -- No --> NotApple["❌ Not an Apple"]
|
||||
Round -- Yes --> Apple["✅ It's an Apple"]
|
||||
Round -- No --> NotApple2["❌ Not an Apple"]
|
||||
```
|
||||
|
||||
### **Example 3**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
A[Module A] --> B[Module B]
|
||||
A --> C[Module C]
|
||||
B --> D[Module D]
|
||||
C --> D
|
||||
D --> E[Module E]
|
||||
```
|
||||
|
||||
### **Example 4**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
Source1["📦 Raw Data (CSV)"]
|
||||
Source2["🌐 API Data"]
|
||||
|
||||
Source1 --> Clean["🧹 Clean & Format"]
|
||||
Source2 --> Clean
|
||||
|
||||
Clean --> Transform["🔄 Transform Data"]
|
||||
Transform --> Load["📥 Load into Data Warehouse"]
|
||||
Load --> BI["📊 BI Dashboard"]
|
||||
```
|
||||
|
||||
### **Example 5**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
classDiagram
|
||||
class Person {
|
||||
-String name
|
||||
-int age
|
||||
+greet(): void
|
||||
}
|
||||
|
||||
class Employee {
|
||||
-int employeeId
|
||||
+calculateSalary(): float
|
||||
}
|
||||
|
||||
class Manager {
|
||||
-String department
|
||||
+assignTask(): void
|
||||
}
|
||||
|
||||
Person <|-- Employee
|
||||
Employee <|-- Manager
|
||||
```
|
||||
|
||||
### **Example 6**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Sunlight["☀️ Sunlight"] --> Leaf["🌿 Leaf"]
|
||||
Leaf --> Glucose["🍬 Glucose"]
|
||||
Leaf --> Oxygen["💨 Oxygen"]
|
||||
```
|
||||
|
||||
### **Example 7**
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: ipsepCola
|
||||
---
|
||||
flowchart TD
|
||||
Internet["🌐 Internet"] --> Router["📡 Router"]
|
||||
Router --> Server1["🖥️ Server A"]
|
||||
Router --> Server2["🖥️ Server B"]
|
||||
Router --> Laptop["💻 Laptop"]
|
||||
|
||||
%% New device joins
|
||||
Router --> Mobile["📱 Mobile"]
|
||||
```
|
||||
|
||||
## Limitations :
|
||||
|
||||
1. Computational Cost: More iterations may be needed for large graphs (>1000 nodes).
|
||||
2. Parameter Tuning: Requires adjustments for different graph types.
|
||||
3. Non-Determinism: Small variations may occur between runs due to force simulation.
|
||||
|
||||
## Conclusion :
|
||||
|
||||
IPSep-CoLa provides a robust solution for constraint-based graph layout, particularly for structured and clustered diagrams. By combining incremental refinement with separation constraints, it achieves readable and well-organized visualizations. Future improvements could include GPU acceleration and adaptive parameter tuning for large-scale graphs.
|
@@ -8,7 +8,7 @@ outline: 'deep' # shows all h3 headings in outline in Vitepress
|
||||
## Introduction to Block Diagrams
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -62,7 +62,7 @@ At its core, a block diagram consists of blocks representing different entities
|
||||
To create a simple block diagram with three blocks labeled 'a', 'b', and 'c', the syntax is as follows:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
a b c
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ While simple block diagrams are linear and straightforward, more complex systems
|
||||
In scenarios where you need to distribute blocks across multiple columns, you can specify the number of columns and arrange the blocks accordingly. Here's how to create a block diagram with three columns and four blocks, where the fourth block appears in a second row:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a b c d
|
||||
```
|
||||
@@ -101,7 +101,7 @@ In more complex diagrams, you may need blocks that span multiple columns to emph
|
||||
To create a block diagram where one block spans across two columns, you can specify the desired width for each block:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a["A label"] b:2 c:2 d
|
||||
```
|
||||
@@ -118,7 +118,7 @@ Composite blocks, or blocks within blocks, are an advanced feature in Mermaid's
|
||||
Creating a composite block involves defining a parent block and then nesting other blocks within it. Here's how to define a composite block with nested elements:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
D
|
||||
end
|
||||
@@ -137,7 +137,7 @@ Mermaid also allows for dynamic adjustment of column widths based on the content
|
||||
In diagrams with varying block sizes, Mermaid automatically adjusts the column widths to fit the largest block in each column. Here's an example:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a:3
|
||||
block:group1:2
|
||||
@@ -157,7 +157,7 @@ This example demonstrates how Mermaid dynamically adjusts the width of the colum
|
||||
In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
block
|
||||
columns 1
|
||||
a["A label"] b c d
|
||||
@@ -181,7 +181,7 @@ Mermaid supports a range of block shapes to suit different diagramming needs, fr
|
||||
To create a block with round edges, which can be used to represent a softer or more flexible component:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1("This is the text in the box")
|
||||
```
|
||||
|
||||
@@ -190,7 +190,7 @@ block-beta
|
||||
A stadium-shaped block, resembling an elongated circle, can be used for components that are process-oriented:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1(["This is the text in the box"])
|
||||
```
|
||||
|
||||
@@ -199,7 +199,7 @@ block-beta
|
||||
For representing subroutines or contained processes, a block with double vertical lines is useful:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[["This is the text in the box"]]
|
||||
```
|
||||
|
||||
@@ -208,7 +208,7 @@ block-beta
|
||||
The cylindrical shape is ideal for representing databases or storage components:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[("Database")]
|
||||
```
|
||||
|
||||
@@ -217,7 +217,7 @@ block-beta
|
||||
A circle can be used for centralized or pivotal components:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1(("This is the text in the circle"))
|
||||
```
|
||||
|
||||
@@ -228,21 +228,21 @@ For decision points, use a rhombus, and for unique or specialized processes, asy
|
||||
**Asymmetric**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1>"This is the text in the box"]
|
||||
```
|
||||
|
||||
**Rhombus**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1{"This is the text in the box"}
|
||||
```
|
||||
|
||||
**Hexagon**
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1{{"This is the text in the box"}}
|
||||
```
|
||||
|
||||
@@ -251,7 +251,7 @@ block-beta
|
||||
Parallelogram and trapezoid shapes are perfect for inputs/outputs and transitional processes:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1[/"This is the text in the box"/]
|
||||
id2[\"This is the text in the box"\]
|
||||
A[/"Christmas"\]
|
||||
@@ -263,7 +263,7 @@ block-beta
|
||||
For highlighting critical or high-priority components, a double circle can be effective:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1((("This is the text in the circle")))
|
||||
```
|
||||
|
||||
@@ -276,7 +276,7 @@ Mermaid also offers unique shapes like block arrows and space blocks for directi
|
||||
Block arrows can visually indicate direction or flow within a process:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
blockArrowId<["Label"]>(right)
|
||||
blockArrowId2<["Label"]>(left)
|
||||
blockArrowId3<["Label"]>(up)
|
||||
@@ -291,7 +291,7 @@ block-beta
|
||||
Space blocks can be used to create intentional empty spaces in the diagram, which is useful for layout and readability:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
a space b
|
||||
c d e
|
||||
@@ -300,7 +300,7 @@ block-beta
|
||||
or
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
ida space:3 idb idc
|
||||
```
|
||||
|
||||
@@ -325,7 +325,7 @@ The most fundamental aspect of connecting blocks is the use of arrows or links.
|
||||
A simple link with an arrow can be created to show direction or flow from one block to another:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
```
|
||||
@@ -342,7 +342,7 @@ Example - Text with Links
|
||||
To add text to a link, the syntax includes the text within the link definition:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space:2 B
|
||||
A-- "X" -->B
|
||||
```
|
||||
@@ -352,7 +352,7 @@ This example show how to add descriptive text to the links, enhancing the inform
|
||||
Example - Edges and Styles:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
@@ -381,7 +381,7 @@ Mermaid enables detailed styling of individual blocks, allowing you to apply var
|
||||
To apply custom styles to a block, you can use the `style` keyword followed by the block identifier and the desired CSS properties:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
id1 space id2
|
||||
id1("Start")-->id2("Stop")
|
||||
style id1 fill:#636,stroke:#333,stroke-width:4px
|
||||
@@ -395,7 +395,7 @@ Mermaid enables applying styling to classes, which could make styling easier if
|
||||
#### Example - Styling a Single Class
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A-->B
|
||||
classDef blue fill:#6e6ce6,stroke:#333,stroke-width:4px;
|
||||
@@ -420,7 +420,7 @@ Combining the elements of structure, linking, and styling, we can create compreh
|
||||
Illustrating a simple software system architecture with interconnected components:
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Frontend blockArrowId6<[" "]>(right) Backend
|
||||
space:2 down<[" "]>(down)
|
||||
@@ -439,7 +439,7 @@ This example shows a basic architecture with a frontend, backend, and database.
|
||||
Representing a business process flow with decision points and multiple stages:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
columns 3
|
||||
Start(("Start")) space:2
|
||||
down<[" "]>(down) space:2
|
||||
@@ -468,7 +468,7 @@ Understanding and avoiding common syntax errors is key to a smooth experience wi
|
||||
A common mistake is incorrect linking syntax, which can lead to unexpected results or broken diagrams:
|
||||
|
||||
```
|
||||
block-beta
|
||||
block
|
||||
A - B
|
||||
```
|
||||
|
||||
@@ -476,7 +476,7 @@ block-beta
|
||||
Ensure that links between blocks are correctly specified with arrows (--> or ---) to define the direction and type of connection. Also remember that one of the fundamentals for block diagram is to give the author full control of where the boxes are positioned so in the example you need to add a space between the boxes:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A space B
|
||||
A --> B
|
||||
```
|
||||
@@ -486,7 +486,7 @@ block-beta
|
||||
Applying styles in the wrong context or with incorrect syntax can lead to blocks not being styled as intended:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill#969;
|
||||
```
|
||||
@@ -495,7 +495,7 @@ Applying styles in the wrong context or with incorrect syntax can lead to blocks
|
||||
Correct the syntax by ensuring proper separation of style properties with commas and using the correct CSS property format:
|
||||
|
||||
```mermaid-example
|
||||
block-beta
|
||||
block
|
||||
A
|
||||
style A fill:#969,stroke:#333;
|
||||
|
||||
|
@@ -24,6 +24,11 @@ Drawing a pie chart is really simple in mermaid.
|
||||
- Followed by `:` colon as separator
|
||||
- Followed by `positive numeric value` (supported up to two decimal places)
|
||||
|
||||
**Note:**
|
||||
|
||||
> Pie chart values must be **positive numbers greater than zero**.
|
||||
> **Negative values are not allowed** and will result in an error.
|
||||
|
||||
[pie] [showData] (OPTIONAL)
|
||||
[title] [titlevalue] (OPTIONAL)
|
||||
"[datakey1]" : [dataValue1]
|
||||
|
@@ -18,7 +18,7 @@ config:
|
||||
sankey:
|
||||
showValues: false
|
||||
---
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Agricultural 'waste',Bio-conversion,124.729
|
||||
Bio-conversion,Liquid,0.597
|
||||
@@ -92,7 +92,7 @@ Wind,Electricity grid,289.366
|
||||
|
||||
## Syntax
|
||||
|
||||
The idea behind syntax is that a user types `sankey-beta` keyword first, then pastes raw CSV below and get the result.
|
||||
The idea behind syntax is that a user types `sankey` keyword first, then pastes raw CSV below and get the result.
|
||||
|
||||
It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.txt) with subtle **differences**:
|
||||
|
||||
@@ -104,7 +104,7 @@ It implements CSV standard as [described here](https://www.ietf.org/rfc/rfc4180.
|
||||
It is implied that 3 columns inside CSV should represent `source`, `target` and `value` accordingly:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
%% source,target,value
|
||||
Electricity grid,Over generation / exports,104.453
|
||||
@@ -117,7 +117,7 @@ Electricity grid,H2 conversion,27.14
|
||||
CSV does not support empty lines without comma delimiters by default. But you can add them if needed:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Bio-conversion,Losses,26.862
|
||||
|
||||
@@ -131,7 +131,7 @@ Bio-conversion,Gas,81.144
|
||||
If you need to have a comma, wrap it in double quotes:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, homes",193.026
|
||||
Pumped heat,"Heating and cooling, commercial",70.672
|
||||
@@ -142,7 +142,7 @@ Pumped heat,"Heating and cooling, commercial",70.672
|
||||
If you need to have double quote, put a pair of them inside quoted string:
|
||||
|
||||
```mermaid-example
|
||||
sankey-beta
|
||||
sankey
|
||||
|
||||
Pumped heat,"Heating and cooling, ""homes""",193.026
|
||||
Pumped heat,"Heating and cooling, ""commercial""",70.672
|
||||
|
@@ -46,6 +46,102 @@ sequenceDiagram
|
||||
Bob->>Alice: Hi Alice
|
||||
```
|
||||
|
||||
### Boundary
|
||||
|
||||
If you want to use the boundary symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "boundary" }
|
||||
participant Bob
|
||||
Alice->>Bob: Request from boundary
|
||||
Bob->>Alice: Response to boundary
|
||||
```
|
||||
|
||||
### Control
|
||||
|
||||
If you want to use the control symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "control" }
|
||||
participant Bob
|
||||
Alice->>Bob: Control request
|
||||
Bob->>Alice: Control response
|
||||
```
|
||||
|
||||
### Entity
|
||||
|
||||
If you want to use the entity symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "entity" }
|
||||
participant Bob
|
||||
Alice->>Bob: Entity request
|
||||
Bob->>Alice: Entity response
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
If you want to use the database symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "database" }
|
||||
participant Bob
|
||||
Alice->>Bob: DB query
|
||||
Bob->>Alice: DB result
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
If you want to use the collections symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "collections" }
|
||||
participant Bob
|
||||
Alice->>Bob: Collections request
|
||||
Bob->>Alice: Collections response
|
||||
```
|
||||
|
||||
### Queue
|
||||
|
||||
If you want to use the queue symbol for a participant, use the JSON configuration syntax as shown below.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "queue" }
|
||||
participant Bob
|
||||
Alice->>Bob: Queue message
|
||||
Bob->>Alice: Queue response
|
||||
```
|
||||
|
||||
### Icon
|
||||
|
||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
||||
participant Bob
|
||||
Alice->>Bob: Icon participant
|
||||
Bob->>Alice: Response to icon
|
||||
```
|
||||
|
||||
### Image
|
||||
|
||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
||||
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
||||
participant Bob
|
||||
Alice->>Bob: Image participant
|
||||
Bob->>Alice: Response to image
|
||||
```
|
||||
|
||||
### Aliases
|
||||
|
||||
The actor can have a convenient identifier and a descriptive label.
|
||||
|
@@ -7,7 +7,7 @@
|
||||
## Example
|
||||
|
||||
```mermaid-example
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
@@ -26,7 +26,7 @@ All text values that contain only one word can be written without `"`. If a text
|
||||
The chart can be drawn horizontal or vertical, default value is vertical.
|
||||
|
||||
```
|
||||
xychart-beta horizontal
|
||||
xychart horizontal
|
||||
...
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ The title is a short description of the chart and it will always render on top o
|
||||
#### Example
|
||||
|
||||
```
|
||||
xychart-beta
|
||||
xychart
|
||||
title "This is a simple example"
|
||||
...
|
||||
```
|
||||
@@ -86,10 +86,10 @@ A bar chart offers the capability to graphically depict bars.
|
||||
|
||||
#### Simplest example
|
||||
|
||||
The only two things required are the chart name (`xychart-beta`) and one data set. So you will be able to draw a chart with a simple config like
|
||||
The only two things required are the chart name (`xychart`) and one data set. So you will be able to draw a chart with a simple config like
|
||||
|
||||
```
|
||||
xychart-beta
|
||||
xychart
|
||||
line [+1.3, .6, 2.4, -.34]
|
||||
```
|
||||
|
||||
@@ -164,7 +164,7 @@ config:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
---
|
||||
xychart-beta
|
||||
xychart
|
||||
title "Sales Revenue"
|
||||
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
|
@@ -755,6 +755,11 @@ graph TD;A--x|text including URL space|B;`)
|
||||
expectedType: 'xychart',
|
||||
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
|
||||
},
|
||||
{
|
||||
textDiagramType: 'xychart',
|
||||
expectedType: 'xychart',
|
||||
content: 'x-axis "Attempts" 10000 --> 10000\ny-axis "Passing tests" 1 --> 1\nbar [1]',
|
||||
},
|
||||
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
|
||||
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
||||
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
||||
|
@@ -1,148 +0,0 @@
|
||||
import type { Selection } from 'd3';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import type { LayoutData, NonClusterNode } from './types.js';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import { insertNode } from './rendering-elements/nodes.js';
|
||||
|
||||
type D3Selection<T extends SVGElement = SVGElement> = Selection<
|
||||
T,
|
||||
unknown,
|
||||
Element | null,
|
||||
unknown
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a graph by merging the graph construction and DOM element insertion.
|
||||
*
|
||||
* This function creates the graph, inserts the SVG groups (clusters, edgePaths, edgeLabels, nodes)
|
||||
* into the provided element, and uses `insertNode` to add nodes to the diagram. Node dimensions
|
||||
* are computed using each node's bounding box.
|
||||
*
|
||||
* @param element - The D3 selection in which the SVG groups are inserted.
|
||||
* @param data4Layout - The layout data containing nodes and edges.
|
||||
* @returns A promise resolving to an object containing the graph and the inserted groups.
|
||||
*/
|
||||
export async function createGraphWithElements(
|
||||
element: D3Selection,
|
||||
data4Layout: LayoutData
|
||||
): Promise<{
|
||||
graph: graphlib.Graph;
|
||||
groups: {
|
||||
clusters: D3Selection<SVGGElement>;
|
||||
edgePaths: D3Selection<SVGGElement>;
|
||||
edgeLabels: D3Selection<SVGGElement>;
|
||||
nodes: D3Selection<SVGGElement>;
|
||||
rootGroups: D3Selection<SVGGElement>;
|
||||
};
|
||||
nodeElements: Map<string, D3Selection<SVGElement | SVGGElement>>;
|
||||
}> {
|
||||
const graph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
const edgesToProcess = [...data4Layout.edges];
|
||||
const config = getConfig();
|
||||
// Create groups for clusters, edge paths, edge labels, and nodes.
|
||||
const rootGroups = element.insert('g').attr('class', 'root');
|
||||
const clusters = rootGroups.insert('g').attr('class', 'clusters');
|
||||
const edgePaths = rootGroups.insert('g').attr('class', 'edges edgePath');
|
||||
const edgeLabels = rootGroups.insert('g').attr('class', 'edgeLabels');
|
||||
const nodesGroup = rootGroups.insert('g').attr('class', 'nodes');
|
||||
|
||||
const nodeElements = new Map<string, D3Selection<SVGElement | SVGGElement>>();
|
||||
|
||||
// Insert nodes into the DOM and add them to the graph.
|
||||
for (const node of data4Layout.nodes) {
|
||||
if (node.isGroup) {
|
||||
graph.setNode(node.id, { ...node });
|
||||
} else {
|
||||
const childNodeEl = await insertNode(nodesGroup, node, { config, dir: node.dir });
|
||||
const boundingBox = childNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||
nodeElements.set(node.id, childNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||
node.width = boundingBox.width;
|
||||
node.height = boundingBox.height;
|
||||
graph.setNode(node.id, { ...node });
|
||||
}
|
||||
}
|
||||
|
||||
// Add edges to the graph.
|
||||
for (const edge of edgesToProcess) {
|
||||
if (edge.label && edge.label?.length > 0) {
|
||||
// Create a label node for the edge
|
||||
const startNode = data4Layout.nodes.find((n) => n.id == edge.start);
|
||||
const labelNodeId = `edge-label-${edge.start}-${edge.end}-${edge.id}`;
|
||||
const labelNode: NonClusterNode = {
|
||||
id: labelNodeId,
|
||||
label: edge.label,
|
||||
edgeStart: edge.start ?? '',
|
||||
edgeEnd: edge.end ?? '',
|
||||
shape: 'labelRect',
|
||||
width: 0,
|
||||
height: 0,
|
||||
isEdgeLabel: true,
|
||||
isDummy: true,
|
||||
parentId: undefined,
|
||||
isGroup: false,
|
||||
layer: 0,
|
||||
order: 0,
|
||||
...(startNode?.dir ? { dir: startNode.dir } : {}),
|
||||
};
|
||||
|
||||
// Insert the label node into the DOM
|
||||
const labelNodeEl = await insertNode(nodesGroup, labelNode, { config, dir: startNode?.dir });
|
||||
const boundingBox = labelNodeEl.node()?.getBBox() ?? { width: 0, height: 0 };
|
||||
|
||||
// Update node dimensions
|
||||
labelNode.width = boundingBox.width;
|
||||
labelNode.height = boundingBox.height;
|
||||
|
||||
// Add to graph and tracking maps
|
||||
graph.setNode(labelNodeId, { ...labelNode });
|
||||
nodeElements.set(labelNodeId, labelNodeEl as D3Selection<SVGElement | SVGGElement>);
|
||||
data4Layout.nodes.push(labelNode);
|
||||
|
||||
// Create two edges to replace the original one
|
||||
const edgeToLabel = {
|
||||
...edge,
|
||||
id: `${edge.id}-to-label`,
|
||||
end: labelNodeId,
|
||||
label: undefined,
|
||||
isLabelEdge: true,
|
||||
arrowTypeEnd: 'none',
|
||||
arrowTypeStart: 'none',
|
||||
};
|
||||
const edgeFromLabel = {
|
||||
...edge,
|
||||
id: `${edge.id}-from-label`,
|
||||
start: labelNodeId,
|
||||
end: edge.end,
|
||||
label: undefined,
|
||||
isLabelEdge: true,
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
};
|
||||
graph.setEdge(edgeToLabel.id, edgeToLabel.start, edgeToLabel.end, { ...edgeToLabel });
|
||||
graph.setEdge(edgeFromLabel.id, edgeFromLabel.start, edgeFromLabel.end, { ...edgeFromLabel });
|
||||
data4Layout.edges.push(edgeToLabel, edgeFromLabel);
|
||||
const edgeIdToRemove = edge.id;
|
||||
data4Layout.edges = data4Layout.edges.filter((edge) => edge.id !== edgeIdToRemove);
|
||||
const indexInOriginal = data4Layout.edges.findIndex((e) => e.id === edge.id);
|
||||
if (indexInOriginal !== -1) {
|
||||
data4Layout.edges.splice(indexInOriginal, 1);
|
||||
}
|
||||
} else {
|
||||
// Regular edge without label
|
||||
graph.setEdge(edge.id, edge.start, edge.end, { ...edge });
|
||||
const edgeExists = data4Layout.edges.some((existingEdge) => existingEdge.id === edge.id);
|
||||
if (!edgeExists) {
|
||||
data4Layout.edges.push(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
graph,
|
||||
groups: { clusters, edgePaths, edgeLabels, nodes: nodesGroup, rootGroups },
|
||||
nodeElements,
|
||||
};
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
import type { LayoutData } from '../../types.ts';
|
||||
import type { D3Selection } from '../../../types.ts';
|
||||
import { insertCluster } from '../../rendering-elements/clusters.js';
|
||||
import { insertEdge } from '../../rendering-elements/edges.js';
|
||||
import { positionNode } from '../../rendering-elements/nodes.js';
|
||||
|
||||
export async function adjustLayout(
|
||||
data4Layout: LayoutData,
|
||||
groups: {
|
||||
edgePaths: D3Selection<SVGGElement>;
|
||||
rootGroups: D3Selection<SVGGElement>;
|
||||
[key: string]: D3Selection<SVGGElement>;
|
||||
}
|
||||
): Promise<void> {
|
||||
for (const node of data4Layout.nodes) {
|
||||
if (node.isGroup) {
|
||||
await insertCluster(groups.clusters, node);
|
||||
} else {
|
||||
positionNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
for (const edge of data4Layout.edges) {
|
||||
insertEdge(
|
||||
groups.edgePaths,
|
||||
edge,
|
||||
{},
|
||||
data4Layout.type,
|
||||
edge.start,
|
||||
edge.end,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,238 +0,0 @@
|
||||
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
|
||||
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
|
||||
import type { Node } from '../../types.ts';
|
||||
import { assignInitialPositions } from './assignInitialPositions.js';
|
||||
import { layerAssignment } from './layerAssignment.js';
|
||||
import { assignNodeOrder } from './nodeOrdering.js';
|
||||
|
||||
describe('assignInitialPositioning', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should correctly assign initial positioning to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
A --> B --> C
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(50, layoutData);
|
||||
|
||||
const firstNode = layoutData.nodes.find((node: Node) => node.id === 'A');
|
||||
const secondNode = layoutData.nodes.find((node: Node) => node.id === 'B');
|
||||
const thirdNode = layoutData.nodes.find((node: Node) => node.id === 'C');
|
||||
// Call Initial Position Assignment for the graph
|
||||
assignInitialPositions(100, 130, layoutData);
|
||||
|
||||
const node1 = layoutData.nodes.find((node: Node) => node.id === 'A');
|
||||
const node2 = layoutData.nodes.find((node: Node) => node.id === 'B');
|
||||
const node3 = layoutData.nodes.find((node: Node) => node.id === 'C');
|
||||
expect(node1.x).toEqual(firstNode.order * 100);
|
||||
expect(node1.y).toEqual(firstNode.layer * 130);
|
||||
expect(node2.x).toEqual(secondNode.order * 100);
|
||||
expect(node2.y).toEqual(secondNode.layer * 130);
|
||||
expect(node3.x).toEqual(thirdNode.order * 100);
|
||||
expect(node3.y).toEqual(thirdNode.layer * 130);
|
||||
});
|
||||
it('should correctly assign initial positioning to nodes in subgraphs', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
three --> two
|
||||
two --> c2
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(50, layoutData);
|
||||
|
||||
const twoNode = layoutData.nodes.find((node: Node) => node.id === 'two');
|
||||
const b1Node = layoutData.nodes.find((node: Node) => node.id === 'b1');
|
||||
const threeNode = layoutData.nodes.find((node: Node) => node.id === 'three');
|
||||
const c2Node = layoutData.nodes.find((node: Node) => node.id === 'c2');
|
||||
|
||||
// Call Initial Position Assignment for the graph
|
||||
assignInitialPositions(100, 130, layoutData);
|
||||
|
||||
expect(twoNode.x).toEqual(twoNode.order * 100);
|
||||
expect(twoNode.y).toEqual(twoNode.layer * 130);
|
||||
expect(b1Node.x).toEqual(b1Node.order * 100);
|
||||
expect(b1Node.y).toEqual(b1Node.layer * 130);
|
||||
expect(threeNode.x).toEqual(threeNode.order * 100);
|
||||
expect(threeNode.y).toEqual(threeNode.layer * 130);
|
||||
expect(c2Node.x).toEqual(c2Node.order * 100);
|
||||
expect(c2Node.y).toEqual(c2Node.layer * 130);
|
||||
});
|
||||
|
||||
it('should correctly assign initial positioning to nodes in complex subgraph', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(50, layoutData);
|
||||
|
||||
const oneNode = layoutData.nodes.find((node: Node) => node.id === 'one');
|
||||
const a1Node = layoutData.nodes.find((node: Node) => node.id === 'a1');
|
||||
const a2Node = layoutData.nodes.find((node: Node) => node.id === 'a2');
|
||||
const a3Node = layoutData.nodes.find((node: Node) => node.id === 'a3');
|
||||
const a4Node = layoutData.nodes.find((node: Node) => node.id === 'a4');
|
||||
|
||||
// Call Initial Position Assignment for the graph
|
||||
assignInitialPositions(100, 130, layoutData);
|
||||
|
||||
expect(oneNode.x).toEqual(oneNode.order * 100);
|
||||
expect(oneNode.y).toEqual(oneNode.layer * 130);
|
||||
expect(a1Node.x).toEqual(a1Node.order * 100);
|
||||
expect(a1Node.y).toEqual(a1Node.layer * 130);
|
||||
expect(a2Node.x).toEqual(a2Node.order * 100);
|
||||
expect(a2Node.y).toEqual(a2Node.layer * 130);
|
||||
expect(a3Node.x).toEqual(a3Node.order * 100);
|
||||
expect(a3Node.y).toEqual(a3Node.layer * 130);
|
||||
expect(a4Node.x).toEqual(a4Node.order * 100);
|
||||
expect(a4Node.y).toEqual(a4Node.layer * 130);
|
||||
});
|
||||
|
||||
it('should correctly assign initial positioning to nodes in TD subgraphs', async () => {
|
||||
const flowchart = `
|
||||
flowchart TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(50, layoutData);
|
||||
|
||||
const p1Node = layoutData.nodes.find((node: Node) => node.id === 'P1');
|
||||
const p15Node = layoutData.nodes.find((node: Node) => node.id === 'P1.5');
|
||||
const p2Node = layoutData.nodes.find((node: Node) => node.id === 'P2');
|
||||
const p25Node = layoutData.nodes.find((node: Node) => node.id === 'P2.5');
|
||||
const p3Node = layoutData.nodes.find((node: Node) => node.id === 'P3');
|
||||
const p4Node = layoutData.nodes.find((node: Node) => node.id === 'P4');
|
||||
const p5Node = layoutData.nodes.find((node: Node) => node.id === 'P5');
|
||||
const p6Node = layoutData.nodes.find((node: Node) => node.id === 'P6');
|
||||
|
||||
// Call Initial Position Assignment for the graph
|
||||
assignInitialPositions(100, 130, layoutData);
|
||||
|
||||
expect(p1Node.x).toEqual(p1Node.order * 100);
|
||||
expect(p1Node.y).toEqual(p1Node.layer * 130);
|
||||
expect(p15Node.x).toEqual(p15Node.order * 100);
|
||||
expect(p15Node.y).toEqual(p15Node.layer * 130);
|
||||
expect(p2Node.x).toEqual(p2Node.order * 100);
|
||||
expect(p2Node.y).toEqual(p2Node.layer * 130);
|
||||
expect(p25Node.x).toEqual(p25Node.order * 100);
|
||||
expect(p25Node.y).toEqual(p25Node.layer * 130);
|
||||
expect(p3Node.x).toEqual(p3Node.order * 100);
|
||||
expect(p3Node.y).toEqual(p3Node.layer * 130);
|
||||
expect(p4Node.x).toEqual(p4Node.order * 100);
|
||||
expect(p4Node.y).toEqual(p4Node.layer * 130);
|
||||
expect(p5Node.x).toEqual(p5Node.order * 100);
|
||||
expect(p5Node.y).toEqual(p5Node.layer * 130);
|
||||
expect(p6Node.x).toEqual(p6Node.order * 100);
|
||||
expect(p6Node.y).toEqual(p6Node.layer * 130);
|
||||
});
|
||||
it('should correctly assign initial positioning to nodes in TD subgraphs', async () => {
|
||||
const flowchart = `
|
||||
flowchart
|
||||
subgraph Z
|
||||
subgraph X
|
||||
a --> b
|
||||
end
|
||||
subgraph Y
|
||||
c --> d
|
||||
end
|
||||
end
|
||||
Y --> X
|
||||
X --> P
|
||||
P --> Y
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(50, layoutData);
|
||||
|
||||
const zNode = layoutData.nodes.find((node: Node) => node.id === 'Z');
|
||||
const yNode = layoutData.nodes.find((node: Node) => node.id === 'Y');
|
||||
const xNode = layoutData.nodes.find((node: Node) => node.id === 'X');
|
||||
const aNode = layoutData.nodes.find((node: Node) => node.id === 'a');
|
||||
const bNode = layoutData.nodes.find((node: Node) => node.id === 'b');
|
||||
const cNode = layoutData.nodes.find((node: Node) => node.id === 'c');
|
||||
const dNode = layoutData.nodes.find((node: Node) => node.id === 'd');
|
||||
const pNode = layoutData.nodes.find((node: Node) => node.id === 'P');
|
||||
|
||||
// Call Initial Position Assignment for the graph
|
||||
assignInitialPositions(100, 130, layoutData);
|
||||
|
||||
expect(zNode.x).toEqual(zNode.order * 100);
|
||||
expect(zNode.y).toEqual(zNode.layer * 130);
|
||||
expect(yNode.x).toEqual(yNode.order * 100);
|
||||
expect(yNode.y).toEqual(yNode.layer * 130);
|
||||
expect(xNode.x).toEqual(xNode.order * 100);
|
||||
expect(xNode.y).toEqual(xNode.layer * 130);
|
||||
expect(aNode.x).toEqual(aNode.order * 100);
|
||||
expect(aNode.y).toEqual(aNode.layer * 130);
|
||||
expect(bNode.x).toEqual(bNode.order * 100);
|
||||
expect(bNode.y).toEqual(bNode.layer * 130);
|
||||
expect(cNode.x).toEqual(cNode.order * 100);
|
||||
expect(cNode.y).toEqual(cNode.layer * 130);
|
||||
expect(dNode.x).toEqual(dNode.order * 100);
|
||||
expect(dNode.y).toEqual(dNode.layer * 130);
|
||||
expect(pNode.x).toEqual(pNode.order * 100);
|
||||
expect(pNode.y).toEqual(pNode.layer * 130);
|
||||
});
|
||||
});
|
@@ -1,27 +0,0 @@
|
||||
import type { LayoutData, Node } from '../../types.ts';
|
||||
|
||||
/**
|
||||
* Assigns initial x and y positions to each node
|
||||
* based on its rank and order.
|
||||
*
|
||||
* @param nodeSpacing - Horizontal spacing between nodes
|
||||
* @param layerHeight - Vertical spacing between layers
|
||||
* @param data4Layout - Layout data used to update node positions
|
||||
*/
|
||||
|
||||
export function assignInitialPositions(
|
||||
nodeSpacing: number,
|
||||
layerHeight: number,
|
||||
data4Layout: LayoutData
|
||||
): void {
|
||||
data4Layout.nodes.forEach((node: Node) => {
|
||||
const layer = node.layer ?? 0;
|
||||
const order = node.order ?? 0;
|
||||
|
||||
const x = order * nodeSpacing;
|
||||
const y = layer * layerHeight;
|
||||
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
});
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
import insertMarkers from '../../rendering-elements/markers.js';
|
||||
import { clear as clearGraphlib } from '../dagre/mermaid-graphlib.js';
|
||||
import { clear as clearNodes } from '../../rendering-elements/nodes.js';
|
||||
import { clear as clearClusters } from '../../rendering-elements/clusters.js';
|
||||
import { clear as clearEdges } from '../../rendering-elements/edges.js';
|
||||
import type { LayoutData, Node } from '../../types.ts';
|
||||
import type { D3Selection } from '../../../types.ts';
|
||||
import type { SVG } from '../../../mermaid.ts';
|
||||
import { adjustLayout } from './adjustLayout.js';
|
||||
import { createGraphWithElements } from '../../createGraph.js';
|
||||
import { layerAssignment } from './layerAssignment.js';
|
||||
import { assignNodeOrder } from './nodeOrdering.js';
|
||||
import { assignInitialPositions } from './assignInitialPositions.js';
|
||||
import { applyCola } from './applyCola.js';
|
||||
|
||||
export async function render(data4Layout: LayoutData, svg: SVG): Promise<void> {
|
||||
const element = svg.select('g') as unknown as D3Selection<SVGElement>;
|
||||
// Insert markers and clear previous elements
|
||||
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||
clearNodes();
|
||||
clearEdges();
|
||||
clearClusters();
|
||||
clearGraphlib();
|
||||
// Create the graph and insert the SVG groups and nodes
|
||||
const { groups } = await createGraphWithElements(element, data4Layout);
|
||||
|
||||
// layer assignment
|
||||
layerAssignment(data4Layout);
|
||||
|
||||
// assign node order using barycenter heuristic method
|
||||
assignNodeOrder(1, data4Layout);
|
||||
|
||||
// assign initial coordinates
|
||||
assignInitialPositions(100, 130, data4Layout);
|
||||
|
||||
const iteration = calculateIterations(data4Layout);
|
||||
|
||||
applyCola(
|
||||
{
|
||||
iterations: iteration * 4,
|
||||
springLength: 80,
|
||||
springStrength: 0.1,
|
||||
repulsionStrength: 70000,
|
||||
},
|
||||
data4Layout
|
||||
);
|
||||
data4Layout.nodes = sortGroupNodesToEnd(data4Layout.nodes);
|
||||
await adjustLayout(data4Layout, groups);
|
||||
}
|
||||
|
||||
function sortGroupNodesToEnd(nodes: Node[]): Node[] {
|
||||
const nonGroupNodes = nodes.filter((n) => !n.isGroup);
|
||||
const groupNodes = nodes
|
||||
.filter((n) => n.isGroup)
|
||||
.map((n) => {
|
||||
const width = typeof n.width === 'number' ? n.width : 0;
|
||||
const height = typeof n.height === 'number' ? n.height : 0;
|
||||
return {
|
||||
...n,
|
||||
_area: width * height,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b._area - a._area)
|
||||
.map((n, idx) => {
|
||||
const { _area, ...cleanNode } = n;
|
||||
cleanNode.order = nonGroupNodes.length + idx;
|
||||
return cleanNode;
|
||||
});
|
||||
|
||||
return [...nonGroupNodes, ...groupNodes];
|
||||
}
|
||||
|
||||
function calculateIterations(data4Layout: LayoutData) {
|
||||
const nodesCount = data4Layout.nodes.length;
|
||||
const edgesCount = data4Layout.edges.length;
|
||||
|
||||
const groupNodes = data4Layout.nodes.filter((node) => {
|
||||
if (node.isGroup) {
|
||||
return node;
|
||||
}
|
||||
});
|
||||
|
||||
let iteration = nodesCount + edgesCount;
|
||||
if (groupNodes.length > 0) {
|
||||
iteration = iteration * 5;
|
||||
}
|
||||
|
||||
return iteration;
|
||||
}
|
@@ -1,161 +0,0 @@
|
||||
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
|
||||
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
|
||||
import type { Node } from '../../types.ts';
|
||||
import { layerAssignment } from './layerAssignment.js';
|
||||
|
||||
describe('layerAssignment', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
A --> B --> C
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'A').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'B').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'C').layer).toEqual(3);
|
||||
});
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
three --> two
|
||||
two --> c2
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'two').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'b1').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'three').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'c2').layer).toEqual(3);
|
||||
|
||||
// expect(graph.getNodeAttributes('B').layer).toEqual(2);
|
||||
// expect(graph.getNodeAttributes('C').layer).toEqual(3);
|
||||
});
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'one').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a1').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a2').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a3').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a4').layer).toEqual(2);
|
||||
});
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P1').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P1.5').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P2').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P2.5').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P3').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P4').layer).toEqual(3);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P5').layer).toEqual(3);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P6').layer).toEqual(3);
|
||||
});
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart
|
||||
subgraph Z
|
||||
subgraph X
|
||||
a --> b
|
||||
end
|
||||
subgraph Y
|
||||
c --> d
|
||||
end
|
||||
end
|
||||
Y --> X
|
||||
X --> P
|
||||
P --> Y
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'Z').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'Y').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'X').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'b').layer).toEqual(3);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'c').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'd').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P').layer).toEqual(3);
|
||||
});
|
||||
it('should correctly assign the layers to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
A --|Test Label|--> B --> C
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'A').layer).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'B').layer).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'C').layer).toEqual(3);
|
||||
});
|
||||
});
|
@@ -1,90 +0,0 @@
|
||||
import type { Edge, LayoutData } from '../../types.ts';
|
||||
|
||||
export function layerAssignment(data4Layout: LayoutData): void {
|
||||
const removedEdges: Edge[] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
function dfs(nodeId: string): void {
|
||||
visited.add(nodeId);
|
||||
visiting.add(nodeId);
|
||||
|
||||
const outbound = data4Layout.edges.filter((e) => e.start === nodeId);
|
||||
|
||||
for (const edge of outbound) {
|
||||
const neighbor = edge.end!;
|
||||
if (!visited.has(neighbor)) {
|
||||
dfs(neighbor);
|
||||
} else if (visiting.has(neighbor)) {
|
||||
// Cycle detected: temporarily remove this edge
|
||||
removedEdges.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(nodeId);
|
||||
}
|
||||
|
||||
// Remove cycles using DFS
|
||||
for (const node of data4Layout.nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
dfs(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out removed edges temporarily
|
||||
const workingEdges = data4Layout.edges.filter((e) => !removedEdges.includes(e));
|
||||
|
||||
// Build in-degree map
|
||||
const inDegree: Record<string, number> = {};
|
||||
for (const node of data4Layout.nodes) {
|
||||
inDegree[node.id] = 0;
|
||||
}
|
||||
for (const edge of workingEdges) {
|
||||
if (edge.end) {
|
||||
inDegree[edge.end]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue of nodes with in-degree 0
|
||||
const queue: string[] = [];
|
||||
for (const nodeId in inDegree) {
|
||||
if (inDegree[nodeId] === 0) {
|
||||
queue.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Map to store calculated ranks/layers
|
||||
const ranks: Record<string, number> = {};
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift()!;
|
||||
const parents = workingEdges.filter((e) => e.end === nodeId).map((e) => e.start!);
|
||||
const layoutNode = data4Layout.nodes.find((n) => n.id === nodeId);
|
||||
if (layoutNode?.parentId && parents.length == 0) {
|
||||
const parentNode = data4Layout.nodes.find((n) => n.id === layoutNode.parentId);
|
||||
if (!parentNode?.layer) {
|
||||
parents.push(parentNode?.id ?? '');
|
||||
}
|
||||
}
|
||||
const parentRanks = parents.map((p) => ranks[p] ?? 0);
|
||||
const rank = parentRanks.length ? Math.min(...parentRanks) + 1 : 0;
|
||||
|
||||
ranks[nodeId] = rank;
|
||||
|
||||
// Update layer in data4Layout.nodes
|
||||
|
||||
if (layoutNode) {
|
||||
layoutNode.layer = rank + 1;
|
||||
}
|
||||
|
||||
// Decrement in-degree of children
|
||||
for (const edge of workingEdges) {
|
||||
if (edge.start === nodeId && edge.end) {
|
||||
inDegree[edge.end]--;
|
||||
if (inDegree[edge.end] === 0) {
|
||||
queue.push(edge.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
import { FlowDB } from '../../../diagrams/flowchart/flowDb.js';
|
||||
import flow from '../../../diagrams/flowchart/parser/flowParser.js';
|
||||
import type { Node } from '../../types.ts';
|
||||
import { layerAssignment } from './layerAssignment.js';
|
||||
import { assignNodeOrder } from './nodeOrdering.js';
|
||||
|
||||
describe('nodeOrdering', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should correctly assign the orders to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
A --> B --> C
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(1, layoutData);
|
||||
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'A').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'B').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'C').order).toEqual(0);
|
||||
});
|
||||
it('should correctly assign the orders to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph two
|
||||
b1
|
||||
end
|
||||
subgraph three
|
||||
c2
|
||||
end
|
||||
three --> two
|
||||
two --> c2
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(1, layoutData);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'two').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'b1').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'three').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'c2').order).toEqual(0);
|
||||
});
|
||||
|
||||
it('should correctly assign the orders to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart LR
|
||||
subgraph one
|
||||
a1[Iam a node with a super long label] -- I am a long label --> a2[I am another node with a mega long label]
|
||||
a1 -- Another long label --> a2
|
||||
a3 --> a1 & a2 & a3 & a4
|
||||
a1 --> a4
|
||||
end
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(1, layoutData);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'one').order).toEqual(0.75);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a1').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a2').order).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a3').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a4').order).toEqual(2);
|
||||
});
|
||||
it('should correctly assign the orders to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart TD
|
||||
P1
|
||||
P1 -->P1.5
|
||||
subgraph P1.5
|
||||
P2
|
||||
P2.5(( A ))
|
||||
P3
|
||||
end
|
||||
P2 --> P4
|
||||
P3 --> P6
|
||||
P1.5 --> P5
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(1, layoutData);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P1').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P1.5').order).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P2').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P2.5').order).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P3').order).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P4').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P5').order).toEqual(2);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P6').order).toEqual(1);
|
||||
});
|
||||
|
||||
it('should correctly assign the orders to node', async () => {
|
||||
const flowchart = `
|
||||
flowchart
|
||||
subgraph Z
|
||||
subgraph X
|
||||
a --> b
|
||||
end
|
||||
subgraph Y
|
||||
c --> d
|
||||
end
|
||||
end
|
||||
Y --> X
|
||||
X --> P
|
||||
P --> Y
|
||||
`;
|
||||
|
||||
// Get layout data from flowDb
|
||||
await flow.parse(flowchart);
|
||||
const layoutData = flow.parser.yy.getData();
|
||||
|
||||
// Call Layer Assignment for the graph
|
||||
layerAssignment(layoutData);
|
||||
|
||||
// Call Node order Assignment for the graph
|
||||
assignNodeOrder(1, layoutData);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'Z').order).toEqual(8);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'Y').order).toEqual(0.5);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'X').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'a').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'b').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'c').order).toEqual(0);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'd').order).toEqual(1);
|
||||
expect(layoutData.nodes.find((node: Node) => node.id === 'P').order).toEqual(1);
|
||||
});
|
||||
});
|
@@ -1,129 +0,0 @@
|
||||
import type { Edge, LayoutData, Node } from '../../types.ts';
|
||||
|
||||
type LayerMap = Record<number, Node[]>;
|
||||
|
||||
function groupNodesByLayer(nodes: Node[]): LayerMap {
|
||||
const layers: LayerMap = {};
|
||||
nodes.forEach((node: Node) => {
|
||||
if (node.isGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = node.layer ?? 0;
|
||||
if (!layers[layer]) {
|
||||
layers[layer] = [];
|
||||
}
|
||||
layers[layer].push(node);
|
||||
});
|
||||
return layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign horizontal ordering to nodes, excluding group nodes from ordering.
|
||||
* Groups are assigned `order` after real nodes are sorted.
|
||||
*/
|
||||
export function assignNodeOrder(iterations: number, data4Layout: LayoutData): void {
|
||||
const nodes = data4Layout.nodes;
|
||||
const edges = data4Layout.edges;
|
||||
const nodeMap = new Map<string, Node>(nodes.map((n) => [n.id, n]));
|
||||
|
||||
const isLayered = nodes.some((n) => n.layer !== undefined);
|
||||
if (isLayered) {
|
||||
const layers = groupNodesByLayer(nodes);
|
||||
const sortedLayers = Object.keys(layers)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
// Initial order
|
||||
for (const layer of sortedLayers) {
|
||||
layers[layer].forEach((node, index) => {
|
||||
node.order = index;
|
||||
});
|
||||
}
|
||||
|
||||
// Barycenter iterations
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
for (let l = 1; l < sortedLayers.length; l++) {
|
||||
sortLayerByBarycenter(layers[sortedLayers[l]], 'inbound', edges, nodeMap);
|
||||
}
|
||||
for (let l = sortedLayers.length - 2; l >= 0; l--) {
|
||||
sortLayerByBarycenter(layers[sortedLayers[l]], 'outbound', edges, nodeMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign order to group nodes at the end
|
||||
for (const node of nodes) {
|
||||
if (node.isGroup) {
|
||||
const childOrders = nodes
|
||||
.filter((n) => n.parentId === node.id)
|
||||
.map((n) => nodeMap.get(n.id)?.order)
|
||||
.filter((o): o is number => typeof o === 'number');
|
||||
|
||||
node.order = childOrders.length
|
||||
? childOrders.reduce((a, b) => a + b, 0) / childOrders.length
|
||||
: nodes.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortLayerByBarycenter(
|
||||
layerNodes: Node[],
|
||||
direction: 'inbound' | 'outbound' | 'both',
|
||||
edges: Edge[],
|
||||
nodeMap: Map<string, Node>
|
||||
): void {
|
||||
const edgeMap = new Map<string, Set<string>>();
|
||||
edges.forEach((e: Edge) => {
|
||||
if (e.start && e.end) {
|
||||
if (!edgeMap.has(e.start)) {
|
||||
edgeMap.set(e.start, new Set());
|
||||
}
|
||||
edgeMap.get(e.start)?.add(e.end);
|
||||
}
|
||||
});
|
||||
|
||||
const baryCenters = layerNodes.map((node, originalIndex) => {
|
||||
const neighborOrders: number[] = [];
|
||||
|
||||
edges.forEach((edge) => {
|
||||
if (direction === 'inbound' && edge.end === node.id) {
|
||||
const source = nodeMap.get(edge.start ?? '');
|
||||
if (source?.order !== undefined) {
|
||||
neighborOrders.push(source.order);
|
||||
}
|
||||
} else if (direction === 'outbound' && edge.start === node.id) {
|
||||
const target = nodeMap.get(edge.end ?? '');
|
||||
if (target?.order !== undefined) {
|
||||
neighborOrders.push(target.order);
|
||||
}
|
||||
} else if (direction === 'both' && (edge.start === node.id || edge.end === node.id)) {
|
||||
const neighborId = edge.start === node.id ? edge.end : edge.start;
|
||||
const neighbor = nodeMap.get(neighborId ?? '');
|
||||
if (neighbor?.order !== undefined) {
|
||||
neighborOrders.push(neighbor.order);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const barycenter =
|
||||
neighborOrders.length === 0
|
||||
? Infinity // Push unconnected nodes to the end
|
||||
: neighborOrders.reduce((sum, o) => sum + o, 0) / neighborOrders.length;
|
||||
|
||||
return { node, barycenter, originalIndex };
|
||||
});
|
||||
|
||||
baryCenters.sort((a, b) => {
|
||||
if (a.barycenter !== b.barycenter) {
|
||||
return a.barycenter - b.barycenter;
|
||||
}
|
||||
|
||||
// Stable tie-breaker based on original index
|
||||
return a.originalIndex - b.originalIndex;
|
||||
});
|
||||
|
||||
baryCenters.forEach((entry, index) => {
|
||||
entry.node.order = index;
|
||||
});
|
||||
}
|
@@ -39,10 +39,6 @@ const registerDefaultLayoutLoaders = () => {
|
||||
name: 'dagre',
|
||||
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||
},
|
||||
{
|
||||
name: 'ipsepCola',
|
||||
loader: async () => await import('./layout-algorithms/ipsepCola/index.js'),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
|
@@ -637,6 +637,11 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||
|
||||
addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor);
|
||||
const midIndex = Math.floor(points.length / 2);
|
||||
const point = points[midIndex];
|
||||
if (!utils.isLabelCoordinateInPath(point, svgPath.attr('d'))) {
|
||||
pointsHasChanged = true;
|
||||
}
|
||||
|
||||
let paths = {};
|
||||
if (pointsHasChanged) {
|
||||
|
@@ -104,8 +104,23 @@ export const userNodeOverrides = (node: Node, options: any) => {
|
||||
seed: handDrawnSeed,
|
||||
strokeWidth: stylesMap.get('stroke-width')?.replace('px', '') || 1.3,
|
||||
fillLineDash: [0, 0],
|
||||
strokeLineDash: getStrokeDashArray(stylesMap.get('stroke-dasharray')),
|
||||
},
|
||||
options
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
const getStrokeDashArray = (strokeDasharrayStyle?: string) => {
|
||||
if (!strokeDasharrayStyle) {
|
||||
return [0, 0];
|
||||
}
|
||||
const dashArray = strokeDasharrayStyle.trim().split(/\s+/).map(Number);
|
||||
if (dashArray.length === 1) {
|
||||
const val = isNaN(dashArray[0]) ? 0 : dashArray[0];
|
||||
return [val, val];
|
||||
}
|
||||
const first = isNaN(dashArray[0]) ? 0 : dashArray[0];
|
||||
const second = isNaN(dashArray[1]) ? 0 : dashArray[1];
|
||||
return [first, second];
|
||||
};
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } 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 { insertPolygonShape } from './insertPolygonShape.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
|
||||
export const createHexagonPathD = (
|
||||
@@ -29,42 +28,50 @@ export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection<
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
const f = 4;
|
||||
const h = bbox.height + node.padding;
|
||||
const m = h / f;
|
||||
const w = bbox.width + 2 * m + node.padding;
|
||||
const points = [
|
||||
{ x: m, y: 0 },
|
||||
{ x: w - m, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w - m, y: -h },
|
||||
{ x: m, y: -h },
|
||||
{ x: 0, y: -h / 2 },
|
||||
];
|
||||
|
||||
let polygon: D3Selection<SVGGElement> | Awaited<ReturnType<typeof insertPolygonShape>>;
|
||||
const h = bbox.height + (node.padding ?? 0);
|
||||
const w = bbox.width + (node.padding ?? 0) * 2.5;
|
||||
const { cssStyles } = node;
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
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 pathData = createHexagonPathD(0, 0, w, h, m);
|
||||
const roughNode = rc.path(pathData, options);
|
||||
|
||||
polygon = shapeSvg
|
||||
.insert(() => roughNode, ':first-child')
|
||||
.attr('transform', `translate(${-w / 2}, ${h / 2})`);
|
||||
|
||||
if (cssStyles) {
|
||||
polygon.attr('style', cssStyles);
|
||||
}
|
||||
} else {
|
||||
polygon = insertPolygonShape(shapeSvg, w, h, points);
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
if (nodeStyles) {
|
||||
polygon.attr('style', nodeStyles);
|
||||
let halfWidth = w / 2;
|
||||
const m = halfWidth / 6; // Margin for label
|
||||
halfWidth = halfWidth + m; // Adjusted half width for hexagon
|
||||
|
||||
const halfHeight = h / 2;
|
||||
|
||||
const fixedLength = halfHeight / 2;
|
||||
const deducedWidth = halfWidth - fixedLength;
|
||||
|
||||
const points = [
|
||||
{ x: -deducedWidth, y: -halfHeight },
|
||||
{ x: 0, y: -halfHeight },
|
||||
{ x: deducedWidth, y: -halfHeight },
|
||||
{ x: halfWidth, y: 0 },
|
||||
{ x: deducedWidth, y: halfHeight },
|
||||
{ x: 0, y: halfHeight },
|
||||
{ x: -deducedWidth, y: halfHeight },
|
||||
{ x: -halfWidth, y: 0 },
|
||||
];
|
||||
|
||||
const pathData = createPathFromPoints(points);
|
||||
const shapeNode = rc.path(pathData, options);
|
||||
|
||||
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
|
||||
polygon.attr('class', 'basic label-container');
|
||||
|
||||
if (cssStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', cssStyles);
|
||||
}
|
||||
|
||||
if (nodeStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', nodeStyles);
|
||||
}
|
||||
|
||||
node.width = w;
|
||||
|
@@ -25,6 +25,7 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const s = w + h;
|
||||
const adjustment = 0.5;
|
||||
|
||||
const points = [
|
||||
{ x: s / 2, y: 0 },
|
||||
@@ -45,13 +46,14 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
|
||||
|
||||
polygon = shapeSvg
|
||||
.insert(() => roughNode, ':first-child')
|
||||
.attr('transform', `translate(${-s / 2}, ${s / 2})`);
|
||||
.attr('transform', `translate(${-s / 2 + adjustment}, ${s / 2})`);
|
||||
|
||||
if (cssStyles) {
|
||||
polygon.attr('style', cssStyles);
|
||||
}
|
||||
} else {
|
||||
polygon = insertPolygonShape(shapeSvg, s, s, points);
|
||||
polygon.attr('transform', `translate(${-s / 2 + adjustment}, ${s / 2})`);
|
||||
}
|
||||
|
||||
if (nodeStyles) {
|
||||
|
@@ -1,18 +1,160 @@
|
||||
import type { Node, RectOptions } from '../../types.js';
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } 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 { drawRect } from './drawRect.js';
|
||||
|
||||
/**
|
||||
* Generates evenly spaced points along an elliptical arc connecting two points.
|
||||
*
|
||||
* @param x1 - x-coordinate of the start point of the arc
|
||||
* @param y1 - y-coordinate of the start point of the arc
|
||||
* @param x2 - x-coordinate of the end point of the arc
|
||||
* @param y2 - y-coordinate of the end point of the arc
|
||||
* @param rx - horizontal radius of the ellipse
|
||||
* @param ry - vertical radius of the ellipse
|
||||
* @param clockwise - direction of the arc; true for clockwise, false for counterclockwise
|
||||
* @returns Array of points `{ x, y }` along the elliptical arc
|
||||
*
|
||||
* @throws Error if the given radii are too small to draw an arc between the points
|
||||
*/
|
||||
export function generateArcPoints(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
rx: number,
|
||||
ry: number,
|
||||
clockwise: boolean
|
||||
) {
|
||||
const numPoints = 20;
|
||||
// Calculate midpoint
|
||||
const midX = (x1 + x2) / 2;
|
||||
const midY = (y1 + y2) / 2;
|
||||
|
||||
// Calculate the angle of the line connecting the points
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
|
||||
// Calculate transformed coordinates for the ellipse
|
||||
const dx = (x2 - x1) / 2;
|
||||
const dy = (y2 - y1) / 2;
|
||||
|
||||
// Scale to unit circle
|
||||
const transformedX = dx / rx;
|
||||
const transformedY = dy / ry;
|
||||
|
||||
// Calculate the distance between points on the unit circle
|
||||
const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2);
|
||||
|
||||
// Check if the ellipse can be drawn with the given radii
|
||||
if (distance > 1) {
|
||||
throw new Error('The given radii are too small to create an arc between the points.');
|
||||
}
|
||||
|
||||
// Calculate the distance from the midpoint to the center of the ellipse
|
||||
const scaledCenterDistance = Math.sqrt(1 - distance ** 2);
|
||||
|
||||
// Calculate the center of the ellipse
|
||||
const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1);
|
||||
const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1);
|
||||
|
||||
// Calculate the start and end angles on the ellipse
|
||||
const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx);
|
||||
const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx);
|
||||
|
||||
// Adjust angles for clockwise/counterclockwise
|
||||
let angleRange = endAngle - startAngle;
|
||||
if (clockwise && angleRange < 0) {
|
||||
angleRange += 2 * Math.PI;
|
||||
}
|
||||
if (!clockwise && angleRange > 0) {
|
||||
angleRange -= 2 * Math.PI;
|
||||
}
|
||||
|
||||
// Generate points
|
||||
const points = [];
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const t = i / (numPoints - 1);
|
||||
const angle = startAngle + t * angleRange;
|
||||
const x = centerX + rx * Math.cos(angle);
|
||||
const y = centerY + ry * Math.sin(angle);
|
||||
points.push({ x, y });
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
export async function roundedRect<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const options = {
|
||||
rx: 5,
|
||||
ry: 5,
|
||||
classes: '',
|
||||
labelPaddingX: (node?.padding || 0) * 1,
|
||||
labelPaddingY: (node?.padding || 0) * 1,
|
||||
} as RectOptions;
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
|
||||
|
||||
return drawRect(parent, node, options);
|
||||
const labelPaddingX = node?.padding ?? 0;
|
||||
const labelPaddingY = node?.padding ?? 0;
|
||||
|
||||
const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
|
||||
const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
|
||||
const radius = 5;
|
||||
const taper = 5; // Taper width for the rounded corners
|
||||
const { cssStyles } = node;
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
const points = [
|
||||
// Top edge (left to right)
|
||||
{ x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1)
|
||||
{ x: w / 2 - taper, y: -h / 2 }, // Top-right corner start (2)
|
||||
|
||||
...generateArcPoints(w / 2 - taper, -h / 2, w / 2, -h / 2 + taper, radius, radius, true), // Top-left arc (2 to 3)
|
||||
|
||||
// Right edge (top to bottom)
|
||||
{ x: w / 2, y: -h / 2 + taper }, // Top-right taper point (3)
|
||||
{ x: w / 2, y: h / 2 - taper }, // Bottom-right taper point (4)
|
||||
|
||||
...generateArcPoints(w / 2, h / 2 - taper, w / 2 - taper, h / 2, radius, radius, true), // Top-left arc (4 to 5)
|
||||
|
||||
// Bottom edge (right to left)
|
||||
{ x: w / 2 - taper, y: h / 2 }, // Bottom-right corner start (5)
|
||||
{ x: -w / 2 + taper, y: h / 2 }, // Bottom-left corner start (6)
|
||||
|
||||
...generateArcPoints(-w / 2 + taper, h / 2, -w / 2, h / 2 - taper, radius, radius, true), // Top-left arc (4 to 5)
|
||||
|
||||
// Left edge (bottom to top)
|
||||
{ x: -w / 2, y: h / 2 - taper }, // Bottom-left taper point (7)
|
||||
{ x: -w / 2, y: -h / 2 + taper }, // Top-left taper point (8)
|
||||
...generateArcPoints(-w / 2, -h / 2 + taper, -w / 2 + taper, -h / 2, radius, radius, true), // Top-left arc (4 to 5)
|
||||
];
|
||||
|
||||
const pathData = createPathFromPoints(points);
|
||||
const shapeNode = rc.path(pathData, options);
|
||||
|
||||
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
|
||||
polygon.attr('class', 'basic label-container outer-path');
|
||||
|
||||
if (cssStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', cssStyles);
|
||||
}
|
||||
|
||||
if (nodeStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', nodeStyles);
|
||||
}
|
||||
|
||||
updateNodeBounds(node, polygon);
|
||||
|
||||
node.intersect = function (point) {
|
||||
const pos = intersect.polygon(node, points, point);
|
||||
return pos;
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
|
@@ -1,11 +1,15 @@
|
||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import {
|
||||
labelHelper,
|
||||
updateNodeBounds,
|
||||
getNodeClasses,
|
||||
generateCirclePoints,
|
||||
createPathFromPoints,
|
||||
} 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 { createRoundedRectPathD } from './roundedRectPath.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { handleUndefinedAttr } from '../../../utils.js';
|
||||
|
||||
export const createStadiumPathD = (
|
||||
x: number,
|
||||
@@ -60,36 +64,44 @@ export async function stadium<T extends SVGGraphicsElement>(parent: D3Selection<
|
||||
const h = bbox.height + node.padding;
|
||||
const w = bbox.width + h / 4 + node.padding;
|
||||
|
||||
let rect;
|
||||
const radius = h / 2;
|
||||
const { cssStyles } = node;
|
||||
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, {});
|
||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
const pathData = createRoundedRectPathD(-w / 2, -h / 2, w, h, h / 2);
|
||||
const roughNode = rc.path(pathData, options);
|
||||
|
||||
rect = shapeSvg.insert(() => roughNode, ':first-child');
|
||||
rect.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||
} else {
|
||||
rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
rect
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', nodeStyles)
|
||||
.attr('rx', h / 2)
|
||||
.attr('ry', h / 2)
|
||||
.attr('x', -w / 2)
|
||||
.attr('y', -h / 2)
|
||||
.attr('width', w)
|
||||
.attr('height', h);
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
const points = [
|
||||
{ x: -w / 2 + radius, y: -h / 2 },
|
||||
{ x: w / 2 - radius, y: -h / 2 },
|
||||
...generateCirclePoints(-w / 2 + radius, 0, radius, 50, 90, 270),
|
||||
{ x: w / 2 - radius, y: h / 2 },
|
||||
...generateCirclePoints(w / 2 - radius, 0, radius, 50, 270, 450),
|
||||
];
|
||||
|
||||
const pathData = createPathFromPoints(points);
|
||||
const shapeNode = rc.path(pathData, options);
|
||||
|
||||
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
|
||||
polygon.attr('class', 'basic label-container outer-path');
|
||||
|
||||
if (cssStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', cssStyles);
|
||||
}
|
||||
|
||||
if (nodeStyles && node.look !== 'handDrawn') {
|
||||
polygon.selectChildren('path').attr('style', nodeStyles);
|
||||
}
|
||||
|
||||
updateNodeBounds(node, polygon);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
const pos = intersect.polygon(node, points, point);
|
||||
return pos;
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
|
@@ -72,12 +72,6 @@ interface BaseNode {
|
||||
defaultWidth?: number;
|
||||
imageAspectRatio?: number;
|
||||
constraint?: 'on' | 'off';
|
||||
isEdgeLabel?: boolean;
|
||||
edgeStart?: string;
|
||||
edgeEnd?: string;
|
||||
layer?: number;
|
||||
order?: number;
|
||||
isDummy?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +126,6 @@ export interface Edge {
|
||||
thickness?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
||||
look?: string;
|
||||
isUserDefinedId?: boolean;
|
||||
isLabelEdge?: boolean;
|
||||
points?: { x: number; y: number }[];
|
||||
}
|
||||
|
||||
export interface RectOptions {
|
||||
|
@@ -884,6 +884,7 @@ export default {
|
||||
runFunc,
|
||||
entityDecode,
|
||||
insertTitle,
|
||||
isLabelCoordinateInPath,
|
||||
parseFontSize,
|
||||
InitIDGenerator,
|
||||
};
|
||||
@@ -960,3 +961,23 @@ export function handleUndefinedAttr(
|
||||
) {
|
||||
return attrValue ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the x or y coordinate of the edge label
|
||||
* appears in the given SVG path data string.
|
||||
*
|
||||
* @param point - The Point object with x and y properties to check.
|
||||
* @param dAttr - SVG path data string (the 'd' attribute of an SVG path element).
|
||||
* @returns - True if the rounded x or y coordinate of the edge label is found
|
||||
* in the sanitized path data string; otherwise, false.
|
||||
*/
|
||||
export function isLabelCoordinateInPath(point: Point, dAttr: string) {
|
||||
const roundedX = Math.round(point.x);
|
||||
const roundedY = Math.round(point.y);
|
||||
|
||||
const sanitizedD = dAttr.replace(/(\d+\.\d+)/g, (match) =>
|
||||
Math.round(parseFloat(match)).toString()
|
||||
);
|
||||
|
||||
return sanitizedD.includes(roundedX.toString()) || sanitizedD.includes(roundedY.toString());
|
||||
}
|
||||
|
@@ -12,5 +12,9 @@ entry Pie:
|
||||
;
|
||||
|
||||
PieSection:
|
||||
label=STRING ":" value=NUMBER EOL
|
||||
label=STRING ":" value=NUMBER_PIE EOL
|
||||
;
|
||||
|
||||
terminal FLOAT_PIE returns number: /-?[0-9]+\.[0-9]+(?!\.)/;
|
||||
terminal INT_PIE returns number: /-?(0|[1-9][0-9]*)(?!\.)/;
|
||||
terminal NUMBER_PIE returns number: FLOAT_PIE | INT_PIE;
|
Reference in New Issue
Block a user