mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
commit
3d185dc622
@ -188,6 +188,7 @@ pie
|
|||||||
|
|
||||||
- [Command Line Interface](https://github.com/mermaid-js/mermaid.cli)
|
- [Command Line Interface](https://github.com/mermaid-js/mermaid.cli)
|
||||||
- [Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
|
- [Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
|
||||||
|
- [HTTP Server](https://github.com/TomWright/mermaid-server)
|
||||||
|
|
||||||
# Contributors [![Help wanted](https://img.shields.io/github/labels/mermaid-js/mermaid/Help%20wanted!)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Help+wanted%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors)
|
# Contributors [![Help wanted](https://img.shields.io/github/labels/mermaid-js/mermaid/Help%20wanted!)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Help+wanted%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors)
|
||||||
|
|
||||||
|
@ -640,4 +640,35 @@ describe('Flowchart', () => {
|
|||||||
{ flowchart: { htmlLabels: false } }
|
{ flowchart: { htmlLabels: false } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('31: should not slice off edges that are to the left of the left-most vertex', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`graph TD
|
||||||
|
work --> sleep
|
||||||
|
sleep --> work
|
||||||
|
eat --> sleep
|
||||||
|
work --> eat
|
||||||
|
`,
|
||||||
|
{ flowchart: { htmlLabels: false } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('32: Render Subroutine shape', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`graph LR
|
||||||
|
A[[subroutine 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;
|
||||||
|
`,
|
||||||
|
{ flowchart: { htmlLabels: false } }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-env jest */
|
/* eslint-env jest */
|
||||||
import { imgSnapshotTest } from '../../helpers/util.js';
|
import { imgSnapshotTest } from '../../helpers/util.js';
|
||||||
|
|
||||||
describe('Sequencediagram', () => {
|
describe('Gantt diagram', () => {
|
||||||
it('should render a gantt chart', () => {
|
it('should render a gantt chart', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`
|
||||||
@ -130,4 +130,34 @@ describe('Sequencediagram', () => {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should hide today marker', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
gantt
|
||||||
|
title Hide today marker (vertical line should not be visible)
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
axisFormat %d
|
||||||
|
todayMarker off
|
||||||
|
section Section1
|
||||||
|
Today: 1, -1h
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should style today marker', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
gantt
|
||||||
|
title Style today marker (vertical line should be 5px wide and half-transparent blue)
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
axisFormat %d
|
||||||
|
todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
|
||||||
|
section Section1
|
||||||
|
Today: 1, -1h
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -182,5 +182,18 @@ context('Sequence diagram', () => {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('should render autonumber with different line breaks', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
Alice->>John: Hello John,<br>how are you?
|
||||||
|
Alice->>John: John,<br/>can you hear me?
|
||||||
|
John-->>Alice: Hi Alice,<br />I can hear you!
|
||||||
|
John-->>Alice: I feel great!
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: white;
|
background: rgb(221, 208, 208);
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
}
|
}
|
||||||
h1 { color: white;}
|
h1 { color: white;}
|
||||||
@ -32,13 +32,31 @@
|
|||||||
G-->c
|
G-->c
|
||||||
</div>
|
</div>
|
||||||
<div class="mermaid2" style="width: 50%; height: 20%;">
|
<div class="mermaid2" style="width: 50%; height: 20%;">
|
||||||
flowchart LR
|
stateDiagram-v2
|
||||||
subgraph id1 [Test]
|
[*] --> monkey
|
||||||
b
|
state monkey {
|
||||||
end
|
Sitting
|
||||||
a-->id1
|
--
|
||||||
|
Eating
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mermaid mermaid-apa" style="width: 100%; height: 20%;">
|
<div class="mermaid2" style="width: 50%; height: 20%;">
|
||||||
|
stateDiagram-v2
|
||||||
|
state Active {
|
||||||
|
[*] --> NumLockOff
|
||||||
|
NumLockOff --> NumLockOn : EvNumLockPressed
|
||||||
|
NumLockOn --> NumLockOff : EvNumLockPressed
|
||||||
|
--
|
||||||
|
[*] --> CapsLockOff
|
||||||
|
CapsLockOff --> CapsLockOn : EvCapsLockPressed
|
||||||
|
CapsLockOn --> CapsLockOff : EvCapsLockPressed
|
||||||
|
--
|
||||||
|
[*] --> ScrollLockOff
|
||||||
|
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
|
||||||
|
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mermaid2 mermaid-apa" style="width: 100%; height: 20%;">
|
||||||
stateDiagram
|
stateDiagram
|
||||||
[*] --> Still
|
[*] --> Still
|
||||||
Still --> [*]
|
Still --> [*]
|
||||||
@ -52,15 +70,66 @@
|
|||||||
Moving --> Crash
|
Moving --> Crash
|
||||||
Crash --> [*]
|
Crash --> [*]
|
||||||
</div>
|
</div>
|
||||||
<div class="mermaid" style="width: 100%; height: 100%;">
|
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||||
stateDiagram-v2
|
stateDiagram-v2
|
||||||
State1: The state with a note
|
[*] --> First
|
||||||
note right of State1
|
First --> Second
|
||||||
Important information! You can write
|
% First --> Third
|
||||||
notes.
|
|
||||||
end note
|
state First {
|
||||||
State1 --> State2
|
[*] --> fir
|
||||||
note left of State2 : This is the note to the left.
|
fir --> [*]
|
||||||
|
}
|
||||||
|
state Second {
|
||||||
|
[*] --> sec
|
||||||
|
sec --> [*]
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||||
|
flowchart TD
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
subgraph D
|
||||||
|
d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
A -- oAo --o B
|
||||||
|
A --> C
|
||||||
|
</div>
|
||||||
|
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||||
|
flowchart TD
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
c-->A
|
||||||
|
c-->B
|
||||||
|
</div>
|
||||||
|
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> First
|
||||||
|
First --> Second
|
||||||
|
First --> Third
|
||||||
|
|
||||||
|
state First {
|
||||||
|
[*] --> fir
|
||||||
|
fir --> [*]
|
||||||
|
}
|
||||||
|
state Second {
|
||||||
|
[*] --> sec
|
||||||
|
sec --> [*]
|
||||||
|
}
|
||||||
|
state Third {
|
||||||
|
[*] --> thi
|
||||||
|
thi --> [*]
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||||
stateDiagram-v2
|
stateDiagram-v2
|
||||||
|
47
dist/index.html
vendored
47
dist/index.html
vendored
@ -16,6 +16,9 @@
|
|||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
info
|
info
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
gantt
|
gantt
|
||||||
title Exclusive end dates (Manual date should end on 3d)
|
title Exclusive end dates (Manual date should end on 3d)
|
||||||
@ -25,7 +28,6 @@
|
|||||||
2 Days: 1, 2019-01-01,2d
|
2 Days: 1, 2019-01-01,2d
|
||||||
Manual Date: 2, 2019-01-01,2019-01-03
|
Manual Date: 2, 2019-01-01,2019-01-03
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
gantt
|
gantt
|
||||||
title Inclusive end dates (Manual date should end on 4th)
|
title Inclusive end dates (Manual date should end on 4th)
|
||||||
@ -36,6 +38,27 @@
|
|||||||
2 Days: 1, 2019-01-01,2d
|
2 Days: 1, 2019-01-01,2d
|
||||||
Manual Date: 2, 2019-01-01,2019-01-03
|
Manual Date: 2, 2019-01-01,2019-01-03
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mermaid">
|
||||||
|
gantt
|
||||||
|
title Hide today marker (vertical line should not be visible)
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
axisFormat %d
|
||||||
|
todayMarker off
|
||||||
|
section Section1
|
||||||
|
Today: 1, -1h
|
||||||
|
</div>
|
||||||
|
<div class="mermaid">
|
||||||
|
gantt
|
||||||
|
title Style today marker (vertical line should be 5px wide and half-transparent blue)
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
axisFormat %d
|
||||||
|
todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
|
||||||
|
section Section1
|
||||||
|
Today: 1, -1h
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
graph LR
|
graph LR
|
||||||
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["
|
sid-B3655226-6C29-4D00-B685-3D5C734DC7E1["
|
||||||
@ -315,6 +338,20 @@ graph TB
|
|||||||
class A someclass;
|
class A someclass;
|
||||||
class C someclass;
|
class C someclass;
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mermaid">
|
||||||
|
graph LR
|
||||||
|
A[[subroutine 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;
|
||||||
|
</div>
|
||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
graph LR
|
graph LR
|
||||||
A[(cylindrical<br />shape<br />test)]
|
A[(cylindrical<br />shape<br />test)]
|
||||||
@ -441,6 +478,14 @@ end
|
|||||||
4->>1: multiline<br />using #lt;br /#gt;
|
4->>1: multiline<br />using #lt;br /#gt;
|
||||||
note right of 1: multiline<br />using #lt;br /#gt;
|
note right of 1: multiline<br />using #lt;br /#gt;
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mermaid">
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
Alice->>John: Hello John,<br>how are you?
|
||||||
|
Alice->>John: John,<br/>can you hear me?
|
||||||
|
John-->>Alice: Hi Alice,<br />I can hear you!
|
||||||
|
John-->>Alice: I feel great!
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
|
@ -16,11 +16,11 @@ Check out the list of [Integrations and Usages of Mermaid](./integrations.md)
|
|||||||
|
|
||||||
**Mermaid was nominated and won the JS Open Source Awards (2019) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
|
**Mermaid was nominated and won the JS Open Source Awards (2019) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
|
||||||
|
|
||||||
## New diagrams in 8.4
|
## New diagrams in 8.5
|
||||||
|
|
||||||
With version 8.4 class diagrams have got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram type, state diagrams.
|
With version 8.5 there are some bug fixes and enhancements, plus a new diagram type, entity relationship diagrams.
|
||||||
|
|
||||||
![Image show the two new diagram types](./img/new-diagrams.png)
|
![Image showing the new ER diagram type](./img/er.png)
|
||||||
|
|
||||||
## Special note regarding version 8.2
|
## Special note regarding version 8.2
|
||||||
|
|
||||||
@ -137,6 +137,18 @@ merge newbranch
|
|||||||
|
|
||||||
![Git graph](./img/git.png)
|
![Git graph](./img/git.png)
|
||||||
|
|
||||||
|
### Entity Relationship Diagram - :exclamation: experimental
|
||||||
|
|
||||||
|
```
|
||||||
|
erDiagram
|
||||||
|
CUSTOMER ||--o{ ORDER : places
|
||||||
|
ORDER ||--|{ LINE-ITEM : contains
|
||||||
|
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
![ER diagram](./img/simple-er.png)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### CDN
|
### CDN
|
||||||
|
@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
> An entity–relationship model (or ER model) describes interrelated things of interest in a specific domain of knowledge. A basic ER model is composed of entity types (which classify the things of interest) and specifies relationships that can exist between entities (instances of those entity types). Wikipedia.
|
> An entity–relationship model (or ER model) describes interrelated things of interest in a specific domain of knowledge. A basic ER model is composed of entity types (which classify the things of interest) and specifies relationships that can exist between entities (instances of those entity types). Wikipedia.
|
||||||
|
|
||||||
Note that practitioners of ER modelling almost always refer to entity types simply as entities. For example the CUSTOMER entity type would be referred to simply as the CUSTOMER entity. This is so common it would be inadvisable to do anything else, but technically an entity is an abstract *instance* of an entity type, and this is what an ER diagram shows - abstract instances, and the relationships between them. This is why entities are always named using singular nouns.
|
Note that practitioners of ER modelling almost always refer to *entity types* simply as *entities*. For example the CUSTOMER entity type would be referred to simply as the CUSTOMER entity. This is so common it would be inadvisable to do anything else, but technically an entity is an abstract *instance* of an entity type, and this is what an ER diagram shows - abstract instances, and the relationships between them. This is why entities are always named using singular nouns.
|
||||||
|
|
||||||
Mermaid can render ER diagrams
|
Mermaid can render ER diagrams
|
||||||
```
|
```
|
||||||
erDiagram
|
erDiagram
|
||||||
CUSTOMER ||--o{ ORDER : places
|
CUSTOMER ||--o{ ORDER : places
|
||||||
ORDER ||--|{ LINE-ITEM : contains
|
ORDER ||--|{ LINE-ITEM : contains
|
||||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
||||||
```
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
CUSTOMER ||--o{ ORDER : places
|
CUSTOMER ||--o{ ORDER : places
|
||||||
ORDER ||--|{ LINE-ITEM : contains
|
ORDER ||--|{ LINE-ITEM : contains
|
||||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
||||||
```
|
```
|
||||||
|
|
||||||
Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid.
|
Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid.
|
||||||
@ -24,7 +24,7 @@ Relationships between entities are represented by lines with end markers represe
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
ER diagrams are a new feature in Mermaid and are **experimental**. There are likely to be a few bugs and constraints, and enhancements will be made in due course.
|
ER diagrams are a new feature in Mermaid and are **experimental**. There are likely to be a few bugs and constraints, and enhancements will be made in due course. Currently you can only define entities and relationships, but not attributes.
|
||||||
|
|
||||||
## Syntax
|
## Syntax
|
||||||
|
|
||||||
@ -61,10 +61,10 @@ Cardinality is a property that describes how many elements of another entity can
|
|||||||
|
|
||||||
| Value (left) | Value (right) | Meaning |
|
| Value (left) | Value (right) | Meaning |
|
||||||
|:------------:|:-------------:|--------------------------------------------------------|
|
|:------------:|:-------------:|--------------------------------------------------------|
|
||||||
| `|o` | `o|` | Zero or one |
|
| `\|o` | `o\|` | Zero or one |
|
||||||
| `||` | `||` | Exactly one |
|
| `\|\|` | `\|\|` | Exactly one |
|
||||||
| `}o` | `o{` | Zero or more (no upper limit) |
|
| `}o` | `o{` | Zero or more (no upper limit) |
|
||||||
| `}|` | `|{` | One or more (no upper limit) |
|
| `}\|` | `\|{` | One or more (no upper limit) |
|
||||||
|
|
||||||
### Identification
|
### Identification
|
||||||
|
|
||||||
|
@ -35,8 +35,6 @@ sequenceDiagram
|
|||||||
Bob-->Alice: Checking with John...
|
Bob-->Alice: Checking with John...
|
||||||
Alice->John: Yes... John, how are you?
|
Alice->John: Yes... John, how are you?
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice ->> Bob: Hello Bob, how are you?
|
Alice ->> Bob: Hello Bob, how are you?
|
||||||
@ -49,9 +47,15 @@ sequenceDiagram
|
|||||||
Alice->John: Yes... John, how are you?
|
Alice->John: Yes... John, how are you?
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Basic flowchart
|
## Basic flowchart
|
||||||
|
|
||||||
|
```
|
||||||
|
graph LR
|
||||||
|
A[Square Rect] -- Link text --> B((Circle))
|
||||||
|
A --> C(Round Rect)
|
||||||
|
B --> D{Rhombus}
|
||||||
|
C --> D
|
||||||
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
A[Square Rect] -- Link text --> B((Circle))
|
A[Square Rect] -- Link text --> B((Circle))
|
||||||
@ -63,6 +67,29 @@ graph LR
|
|||||||
|
|
||||||
## Larger flowchart with some styling
|
## Larger flowchart with some styling
|
||||||
|
|
||||||
|
```
|
||||||
|
graph TB
|
||||||
|
sq[Square shape] --> ci((Circle shape))
|
||||||
|
|
||||||
|
subgraph A
|
||||||
|
od>Odd shape]-- Two line<br/>edge comment --> ro
|
||||||
|
di{Diamond with <br/> line break} -.-> ro(Rounded<br>square<br>shape)
|
||||||
|
di==>ro2(Rounded square shape)
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Notice that no text in shape are added here instead that is appended further down
|
||||||
|
e --> od3>Really long text with linebreak<br>in an Odd shape]
|
||||||
|
|
||||||
|
%% Comments after double percent signs
|
||||||
|
e((Inner / circle<br>and some odd <br>special characters)) --> f(,.?!+-*ز)
|
||||||
|
|
||||||
|
cyr[Cyrillic]-->cyr2((Circle shape Начало));
|
||||||
|
|
||||||
|
classDef green fill:#9f6,stroke:#333,stroke-width:2px;
|
||||||
|
classDef orange fill:#f96,stroke:#333,stroke-width:4px;
|
||||||
|
class sq,e green
|
||||||
|
class di orange
|
||||||
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TB
|
graph TB
|
||||||
sq[Square shape] --> ci((Circle shape))
|
sq[Square shape] --> ci((Circle shape))
|
||||||
@ -90,6 +117,21 @@ graph TB
|
|||||||
|
|
||||||
## Loops, alt and opt
|
## Loops, alt and opt
|
||||||
|
|
||||||
|
```
|
||||||
|
sequenceDiagram
|
||||||
|
loop Daily query
|
||||||
|
Alice->>Bob: Hello Bob, how are you?
|
||||||
|
alt is sick
|
||||||
|
Bob->>Alice: Not so good :(
|
||||||
|
else is well
|
||||||
|
Bob->>Alice: Feeling fresh like a daisy
|
||||||
|
end
|
||||||
|
|
||||||
|
opt Extra response
|
||||||
|
Bob->>Alice: Thanks for asking
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
loop Daily query
|
loop Daily query
|
||||||
@ -109,6 +151,19 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Message to self in loop
|
## Message to self in loop
|
||||||
|
|
||||||
|
```
|
||||||
|
sequenceDiagram
|
||||||
|
participant Alice
|
||||||
|
participant Bob
|
||||||
|
Alice->>John: Hello John, how are you?
|
||||||
|
loop Healthcheck
|
||||||
|
John->>John: Fight against hypochondria
|
||||||
|
end
|
||||||
|
Note right of John: Rational thoughts<br/>prevail...
|
||||||
|
John-->>Alice: Great!
|
||||||
|
John->>Bob: How about you?
|
||||||
|
Bob-->>John: Jolly good!
|
||||||
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Alice
|
participant Alice
|
||||||
|
@ -89,6 +89,17 @@ graph LR
|
|||||||
id1([This is the text in the box])
|
id1([This is the text in the box])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### A node in a subroutine shape
|
||||||
|
|
||||||
|
```
|
||||||
|
graph LR
|
||||||
|
id1[[This is the text in the box]]
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
id1[[This is the text in the box]]
|
||||||
|
```
|
||||||
|
|
||||||
### A node in a cylindrical shape
|
### A node in a cylindrical shape
|
||||||
|
|
||||||
```
|
```
|
||||||
|
BIN
docs/img/er.png
Normal file
BIN
docs/img/er.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
docs/img/simple-er.png
Normal file
BIN
docs/img/simple-er.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
@ -7,7 +7,7 @@
|
|||||||
<meta name="description" content="Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.">
|
<meta name="description" content="Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||||
<script src="//cdn.jsdelivr.net/npm/mermaid@8.4.7/dist/mermaid.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/mermaid@8.5.0/dist/mermaid.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
@ -137,3 +137,4 @@ The following is a list of different integrations and plugins where mermaid is b
|
|||||||
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
|
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
|
||||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||||
|
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
|
||||||
|
@ -263,6 +263,43 @@ The number of alternating section styles.
|
|||||||
Datetime format of the axis. This might need adjustment to match your locale and preferences
|
Datetime format of the axis. This might need adjustment to match your locale and preferences
|
||||||
**Default value '%Y-%m-%d'**.
|
**Default value '%Y-%m-%d'**.
|
||||||
|
|
||||||
|
## er
|
||||||
|
|
||||||
|
The object containing configurations specific for entity relationship diagrams
|
||||||
|
|
||||||
|
### diagramPadding
|
||||||
|
|
||||||
|
The amount of padding around the diagram as a whole so that embedded diagrams have margins, expressed in pixels
|
||||||
|
|
||||||
|
### layoutDirection
|
||||||
|
|
||||||
|
Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL',
|
||||||
|
where T = top, B = bottom, L = left, and R = right.
|
||||||
|
|
||||||
|
### minEntityWidth
|
||||||
|
|
||||||
|
The mimimum width of an entity box, expressed in pixels
|
||||||
|
|
||||||
|
### minEntityHeight
|
||||||
|
|
||||||
|
The minimum height of an entity box, expressed in pixels
|
||||||
|
|
||||||
|
### entityPadding
|
||||||
|
|
||||||
|
The minimum internal padding between the text in an entity box and the enclosing box borders, expressed in pixels
|
||||||
|
|
||||||
|
### stroke
|
||||||
|
|
||||||
|
Stroke color of box edges and lines
|
||||||
|
|
||||||
|
### fill
|
||||||
|
|
||||||
|
Fill color of entity boxes
|
||||||
|
|
||||||
|
### fontSize
|
||||||
|
|
||||||
|
Font size
|
||||||
|
|
||||||
## render
|
## render
|
||||||
|
|
||||||
Function that renders an svg with a graph from a chart definition. Usage example below.
|
Function that renders an svg with a graph from a chart definition. Usage example below.
|
||||||
@ -318,6 +355,7 @@ mermaidAPI.initialize({
|
|||||||
boxTextMargin:5,
|
boxTextMargin:5,
|
||||||
noteMargin:10,
|
noteMargin:10,
|
||||||
messageMargin:35,
|
messageMargin:35,
|
||||||
|
messageAlign:'center',
|
||||||
mirrorActors:true,
|
mirrorActors:true,
|
||||||
bottomMarginAdj:1,
|
bottomMarginAdj:1,
|
||||||
useMaxWidth:true,
|
useMaxWidth:true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mermaid",
|
"name": "mermaid",
|
||||||
"version": "8.4.7",
|
"version": "8.5.0",
|
||||||
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||||
"main": "dist/mermaid.core.js",
|
"main": "dist/mermaid.core.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"git graph"
|
"git graph"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --progress --colors",
|
"build": "webpack --progress --colors -p",
|
||||||
"postbuild": "documentation build src/mermaidAPI.js --shallow -f md --markdown-toc false -o docs/mermaidAPI.md",
|
"postbuild": "documentation build src/mermaidAPI.js --shallow -f md --markdown-toc false -o docs/mermaidAPI.md",
|
||||||
"build:watch": "yarn build --watch",
|
"build:watch": "yarn build --watch",
|
||||||
"minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js",
|
"minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js",
|
||||||
|
@ -1,3 +1,55 @@
|
|||||||
|
# Cluster handling
|
||||||
|
|
||||||
|
Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few work-arounds.
|
||||||
|
|
||||||
|
In the diagram below there are two clusters and there are no edges to nodes outside the own cluster.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart
|
||||||
|
subgraph C1
|
||||||
|
a --> b
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
c
|
||||||
|
end
|
||||||
|
C1 --> C2
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case the dagre-wrapper will transform the graph to the graph below.
|
||||||
|
```mermaid
|
||||||
|
flowchart
|
||||||
|
C1 --> C2
|
||||||
|
```
|
||||||
|
|
||||||
|
The new nodes C1 and C2 are a special type of nodes, clusterNodes. ClusterNodes have have the nodes in the cluster including the cluster attached in a graph object.
|
||||||
|
|
||||||
|
When rendering this diagram it it beeing rendered recursivly. The diagram is rendered by the dagre-mermaid:render function which in turn will be used to render the node C1 and the node C2. The result of those renderings will be inserted as nodes in the "root" diagram. With this recursive approach it would be possible to have different layout direction for each cluster.
|
||||||
|
|
||||||
|
```
|
||||||
|
{ clusterNode: true, graph }
|
||||||
|
```
|
||||||
|
*Data for a clusterNode*
|
||||||
|
|
||||||
|
When a cluster has edges to or from some of its nodes leading outside the cluster the approach of recursive rendering can not be used as the layout of the graph needs to take responsibility for nodes outside of the cluster.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
b
|
||||||
|
end
|
||||||
|
a --> C2
|
||||||
|
```
|
||||||
|
|
||||||
|
To handle this case a special type of edge is inserted. The edge to/from the cluster is replaced with an edge to/from a node in the cluster which is tagged with toCluster/fromCluster. When rendering this edge the intersection between the edge and the border of the cluster is calculated making the edge start/stop there. In practice this renders like an an edge to/from the cluster.
|
||||||
|
|
||||||
|
In the diagram above the root diagram would be rendered with C1 whereas C2 would be rendered recursively.
|
||||||
|
|
||||||
|
Of these two approaches the top one renders better and is used when possible. When this is not possible, ie an edge is added crossing the border the non recursive approach is used.
|
||||||
|
|
||||||
|
|
||||||
# Graph objects and their properties
|
# Graph objects and their properties
|
||||||
|
|
||||||
Explains the representation of various objects used to render the flow charts and what the properties mean. This ofc from the perspective of the dagre-wrapper.
|
Explains the representation of various objects used to render the flow charts and what the properties mean. This ofc from the perspective of the dagre-wrapper.
|
||||||
@ -7,12 +59,10 @@ Explains the representation of various objects used to render the flow charts an
|
|||||||
Sample object:
|
Sample object:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"labelType":"svg",
|
|
||||||
"labelStyle":"",
|
|
||||||
"shape":"rect",
|
"shape":"rect",
|
||||||
"label":{},
|
|
||||||
"labelText":"Test",
|
"labelText":"Test",
|
||||||
"rx":0,"ry":0,
|
"rx":0,
|
||||||
|
"ry":0,
|
||||||
"class":"default",
|
"class":"default",
|
||||||
"style":"",
|
"style":"",
|
||||||
"id":"Test",
|
"id":"Test",
|
||||||
@ -24,18 +74,16 @@ This is set by the renderer of the diagram and insert the data that the wrapper
|
|||||||
|
|
||||||
| property | description |
|
| property | description |
|
||||||
| ---------- | ----------------------------------------------------------------------------------------------------------- |
|
| ---------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
| labelType | If the label should be html label or a svg label. Should we continue to support both? |
|
| labelStyle | Css styles for the label. User for instance for stylling the labels for clusters |
|
||||||
| labelStyle | Css styles for the label. Not currently used. |
|
| shape | The shape of the node. |
|
||||||
| shape | The shape of the node. Currently on rect is suppoerted. This will change. |
|
|
||||||
| label | ?? |
|
|
||||||
| labelText | The text on the label |
|
| labelText | The text on the label |
|
||||||
| rx | The corner radius - maybe part of the shape instead? |
|
| rx | The corner radius - maybe part of the shape instead? Used for rects. |
|
||||||
| ry | The corner radius - maybe part of the shape instead? |
|
| ry | The corner radius - maybe part of the shape instead? Used for rects. |
|
||||||
| class | Class to be set for the shape |
|
| classes | Classes to be set for the shape. Not used |
|
||||||
| style | Css styles for the actual shape |
|
| style | Css styles for the actual shape |
|
||||||
| id | id of the shape |
|
| id | id of the shape |
|
||||||
| type | if set to group then this node indicates *a cluster*. |
|
| type | if set to group then this node indicates *a cluster*. |
|
||||||
| padding | Padding. Passed from the renderr as this might differ between react for different diagrams. Maybe obsolete. |
|
| padding | Padding. Passed from the render as this might differ between different diagrams. Maybe obsolete. |
|
||||||
|
|
||||||
|
|
||||||
# edge
|
# edge
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import intersectRect from './intersect/intersect-rect';
|
import intersectRect from './intersect/intersect-rect';
|
||||||
import { logger } from '../logger'; // eslint-disable-line
|
import { logger as log } from '../logger'; // eslint-disable-line
|
||||||
import createLabel from './createLabel';
|
import createLabel from './createLabel';
|
||||||
|
|
||||||
const rect = (parent, node) => {
|
const rect = (parent, node) => {
|
||||||
|
log.trace('Creating subgraph rect for ', node.id, node);
|
||||||
|
|
||||||
// Add outer g element
|
// Add outer g element
|
||||||
const shapeSvg = parent
|
const shapeSvg = parent
|
||||||
.insert('g')
|
.insert('g')
|
||||||
@ -23,6 +25,7 @@ const rect = (parent, node) => {
|
|||||||
const padding = 0 * node.padding;
|
const padding = 0 * node.padding;
|
||||||
const halfPadding = padding / 2;
|
const halfPadding = padding / 2;
|
||||||
|
|
||||||
|
log.trace('Data ', node, JSON.stringify(node));
|
||||||
// center the rect around its coordinate
|
// center the rect around its coordinate
|
||||||
rect
|
rect
|
||||||
.attr('rx', node.rx)
|
.attr('rx', node.rx)
|
||||||
@ -32,9 +35,7 @@ const rect = (parent, node) => {
|
|||||||
.attr('width', node.width + padding)
|
.attr('width', node.width + padding)
|
||||||
.attr('height', node.height + padding);
|
.attr('height', node.height + padding);
|
||||||
|
|
||||||
// logger.info('bbox', bbox.width, node.x, node.width);
|
|
||||||
// Center the label
|
// Center the label
|
||||||
// label.attr('transform', 'translate(' + adj + ', ' + (node.y - node.height / 2) + ')');
|
|
||||||
label.attr(
|
label.attr(
|
||||||
'transform',
|
'transform',
|
||||||
'translate(' +
|
'translate(' +
|
||||||
@ -127,9 +128,7 @@ const roundedWithTitle = (parent, node) => {
|
|||||||
.attr('width', node.width + padding)
|
.attr('width', node.width + padding)
|
||||||
.attr('height', node.height + padding - bbox.height - 3);
|
.attr('height', node.height + padding - bbox.height - 3);
|
||||||
|
|
||||||
// logger.info('bbox', bbox.width, node.x, node.width);
|
|
||||||
// Center the label
|
// Center the label
|
||||||
// label.attr('transform', 'translate(' + adj + ', ' + (node.y - node.height / 2) + ')');
|
|
||||||
label.attr(
|
label.attr(
|
||||||
'transform',
|
'transform',
|
||||||
'translate(' +
|
'translate(' +
|
||||||
@ -155,7 +154,9 @@ const shapes = { rect, roundedWithTitle, noteGroup };
|
|||||||
let clusterElems = {};
|
let clusterElems = {};
|
||||||
|
|
||||||
export const insertCluster = (elem, node) => {
|
export const insertCluster = (elem, node) => {
|
||||||
clusterElems[node.id] = shapes[node.shape](elem, node);
|
log.trace('Inserting cluster');
|
||||||
|
const shape = node.shape || 'rect';
|
||||||
|
clusterElems[node.id] = shapes[shape](elem, node);
|
||||||
};
|
};
|
||||||
export const getClusterTitleWidth = (elem, node) => {
|
export const getClusterTitleWidth = (elem, node) => {
|
||||||
const label = createLabel(node.labelText, node.labelStyle);
|
const label = createLabel(node.labelText, node.labelStyle);
|
||||||
@ -170,6 +171,8 @@ export const clear = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const positionCluster = node => {
|
export const positionCluster = node => {
|
||||||
|
log.info('Position cluster');
|
||||||
const el = clusterElems[node.id];
|
const el = clusterElems[node.id];
|
||||||
|
|
||||||
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
const createLabel = (vertexText, style) => {
|
const createLabel = (vertexText, style) => {
|
||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
||||||
|
let rows = [];
|
||||||
const rows = vertexText.split(/\n|<br\s*\/?>/gi);
|
if (vertexText) {
|
||||||
|
rows = vertexText.split(/\n|<br\s*\/?>/gi);
|
||||||
|
}
|
||||||
|
|
||||||
for (let j = 0; j < rows.length; j++) {
|
for (let j = 0; j < rows.length; j++) {
|
||||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
|
@ -63,34 +63,17 @@ const outsideNode = (node, point) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const intersection = (node, outsidePoint, insidePoint) => {
|
|
||||||
// const x = node.x;
|
|
||||||
// const y = node.y;
|
|
||||||
|
|
||||||
// const dx = Math.abs(x - insidePoint.x);
|
|
||||||
// const w = node.width / 2;
|
|
||||||
// let r = w - dx;
|
|
||||||
// const dy = Math.abs(y - insidePoint.y);
|
|
||||||
// const h = node.height / 2;
|
|
||||||
// const q = h - dy;
|
|
||||||
|
|
||||||
// const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
|
||||||
// const R = Math.abs(outsidePoint.x - insidePoint.x);
|
|
||||||
// r = (R * q) / Q;
|
|
||||||
|
|
||||||
// return { x: insidePoint.x + r, y: insidePoint.y + q };
|
|
||||||
// };
|
|
||||||
const intersection = (node, outsidePoint, insidePoint) => {
|
const intersection = (node, outsidePoint, insidePoint) => {
|
||||||
// logger.info('intersection', outsidePoint, insidePoint, node);
|
logger.trace('intersection o:', outsidePoint, ' i:', insidePoint, node);
|
||||||
const x = node.x;
|
const x = node.x;
|
||||||
const y = node.y;
|
const y = node.y;
|
||||||
|
|
||||||
const dx = Math.abs(x - insidePoint.x);
|
const dx = Math.abs(x - insidePoint.x);
|
||||||
const w = node.width / 2;
|
const w = node.width / 2;
|
||||||
let r = w - dx;
|
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||||
const dy = Math.abs(y - insidePoint.y);
|
const dy = Math.abs(y - insidePoint.y);
|
||||||
const h = node.height / 2;
|
const h = node.height / 2;
|
||||||
let q = h - dy;
|
let q = insidePoint.y < outsidePoint.y ? h - dy : h - dy;
|
||||||
|
|
||||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||||
@ -105,20 +88,20 @@ const intersection = (node, outsidePoint, insidePoint) => {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
q = (Q * r) / R;
|
q = (Q * r) / R;
|
||||||
|
r = (R * q) / Q;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
|
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x + dx - w,
|
||||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
|
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
||||||
logger.info('\n\n\n\n');
|
|
||||||
let points = edge.points;
|
let points = edge.points;
|
||||||
if (edge.toCluster) {
|
if (edge.toCluster) {
|
||||||
// logger.info('edge', edge);
|
logger.trace('edge', edge);
|
||||||
// logger.info('to cluster', clusterDb[edge.toCluster]);
|
logger.trace('to cluster', clusterDb[edge.toCluster]);
|
||||||
points = [];
|
points = [];
|
||||||
let lastPointOutside;
|
let lastPointOutside;
|
||||||
let isInside = false;
|
let isInside = false;
|
||||||
@ -126,13 +109,12 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
|||||||
const node = clusterDb[edge.toCluster].node;
|
const node = clusterDb[edge.toCluster].node;
|
||||||
|
|
||||||
if (!outsideNode(node, point) && !isInside) {
|
if (!outsideNode(node, point) && !isInside) {
|
||||||
// logger.info('inside', edge.toCluster, point);
|
logger.trace('inside', edge.toCluster, point, lastPointOutside);
|
||||||
|
|
||||||
// First point inside the rect
|
// First point inside the rect
|
||||||
const insterection = intersection(node, lastPointOutside, point);
|
const insterection = intersection(node, lastPointOutside, point);
|
||||||
// logger.info('intersect', inter.rect(node, lastPointOutside));
|
logger.trace('intersect', insterection);
|
||||||
points.push(insterection);
|
points.push(insterection);
|
||||||
// points.push(insterection);
|
|
||||||
isInside = true;
|
isInside = true;
|
||||||
} else {
|
} else {
|
||||||
if (!isInside) points.push(point);
|
if (!isInside) points.push(point);
|
||||||
@ -142,8 +124,8 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edge.fromCluster) {
|
if (edge.fromCluster) {
|
||||||
// logger.info('edge', edge);
|
logger.trace('edge', edge);
|
||||||
// logger.info('from cluster', clusterDb[edge.toCluster]);
|
logger.trace('from cluster', clusterDb[edge.toCluster]);
|
||||||
const updatedPoints = [];
|
const updatedPoints = [];
|
||||||
let lastPointOutside;
|
let lastPointOutside;
|
||||||
let isInside = false;
|
let isInside = false;
|
||||||
@ -152,17 +134,17 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
|||||||
const node = clusterDb[edge.fromCluster].node;
|
const node = clusterDb[edge.fromCluster].node;
|
||||||
|
|
||||||
if (!outsideNode(node, point) && !isInside) {
|
if (!outsideNode(node, point) && !isInside) {
|
||||||
// logger.info('inside', edge.toCluster, point);
|
logger.trace('inside', edge.toCluster, point);
|
||||||
|
|
||||||
// First point inside the rect
|
// First point inside the rect
|
||||||
const insterection = intersection(node, lastPointOutside, point);
|
const insterection = intersection(node, lastPointOutside, point);
|
||||||
// logger.info('intersect', intersection(node, lastPointOutside, point));
|
// logger.trace('intersect', intersection(node, lastPointOutside, point));
|
||||||
updatedPoints.unshift(insterection);
|
updatedPoints.unshift(insterection);
|
||||||
// points.push(insterection);
|
// points.push(insterection);
|
||||||
isInside = true;
|
isInside = true;
|
||||||
} else {
|
} else {
|
||||||
// at the outside
|
// at the outside
|
||||||
// logger.info('Outside point', point);
|
logger.trace('Outside point', point);
|
||||||
if (!isInside) updatedPoints.unshift(point);
|
if (!isInside) updatedPoints.unshift(point);
|
||||||
}
|
}
|
||||||
lastPointOutside = point;
|
lastPointOutside = point;
|
||||||
@ -170,10 +152,6 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
|||||||
points = updatedPoints;
|
points = updatedPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
// logger.info('Poibts', points);
|
|
||||||
|
|
||||||
// logger.info('Edge', edge);
|
|
||||||
|
|
||||||
// The data for our line
|
// The data for our line
|
||||||
const lineData = points.filter(p => !Number.isNaN(p.y));
|
const lineData = points.filter(p => !Number.isNaN(p.y));
|
||||||
|
|
||||||
@ -216,7 +194,7 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
|||||||
url = url.replace(/\(/g, '\\(');
|
url = url.replace(/\(/g, '\\(');
|
||||||
url = url.replace(/\)/g, '\\)');
|
url = url.replace(/\)/g, '\\)');
|
||||||
}
|
}
|
||||||
// logger.info('arrowType', edge.arrowType);
|
logger.info('arrowType', edge.arrowType);
|
||||||
switch (edge.arrowType) {
|
switch (edge.arrowType) {
|
||||||
case 'arrow_cross':
|
case 'arrow_cross':
|
||||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||||
|
@ -1,45 +1,66 @@
|
|||||||
import dagre from 'dagre';
|
import dagre from 'dagre';
|
||||||
|
import graphlib from 'graphlib';
|
||||||
import insertMarkers from './markers';
|
import insertMarkers from './markers';
|
||||||
import { insertNode, positionNode, clear as clearNodes } from './nodes';
|
import { updateNodeBounds } from './shapes/util';
|
||||||
|
import {
|
||||||
|
clear as clearGraphlib,
|
||||||
|
clusterDb,
|
||||||
|
adjustClustersAndEdges,
|
||||||
|
findNonClusterChild
|
||||||
|
} from './mermaid-graphlib';
|
||||||
|
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes';
|
||||||
import { insertCluster, clear as clearClusters } from './clusters';
|
import { insertCluster, clear as clearClusters } from './clusters';
|
||||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
|
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
|
||||||
import { logger } from '../logger';
|
import { logger as log } from '../logger';
|
||||||
|
|
||||||
let clusterDb = {};
|
|
||||||
|
|
||||||
const translateClusterId = id => {
|
|
||||||
if (clusterDb[id]) return clusterDb[id].id;
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const render = (elem, graph, markers, diagramtype, id) => {
|
|
||||||
insertMarkers(elem, markers, diagramtype, id);
|
|
||||||
clusterDb = {};
|
|
||||||
clearNodes();
|
|
||||||
clearEdges();
|
|
||||||
clearClusters();
|
|
||||||
|
|
||||||
|
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
||||||
|
log.trace('Graph in recursive render:', graphlib.json.write(graph), parentCluster);
|
||||||
|
const elem = _elem.insert('g').attr('class', 'root'); // eslint-disable-line
|
||||||
|
if (!graph.nodes()) {
|
||||||
|
log.trace('No nodes found for', graph);
|
||||||
|
} else {
|
||||||
|
log.trace('Recursive render', graph.nodes());
|
||||||
|
}
|
||||||
|
if (graph.edges().length > 0) {
|
||||||
|
log.trace('Recursive edges', graph.edge(graph.edges()[0]));
|
||||||
|
}
|
||||||
const clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line
|
const clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line
|
||||||
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
|
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
|
||||||
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
|
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
|
||||||
const nodes = elem.insert('g').attr('class', 'nodes');
|
const nodes = elem.insert('g').attr('class', 'nodes');
|
||||||
|
|
||||||
logger.warn('graph', graph);
|
|
||||||
|
|
||||||
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
|
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
|
||||||
// to the abstract node and is later used by dagre for the layout
|
// to the abstract node and is later used by dagre for the layout
|
||||||
graph.nodes().forEach(function(v) {
|
graph.nodes().forEach(function(v) {
|
||||||
const node = graph.node(v);
|
const node = graph.node(v);
|
||||||
logger.warn('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
|
if (typeof parentCluster !== 'undefined') {
|
||||||
if (node.type !== 'group') {
|
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
|
||||||
insertNode(nodes, graph.node(v));
|
// data.clusterPositioning = true;
|
||||||
|
log.trace('Setting data for cluster', data);
|
||||||
|
graph.setNode(parentCluster.id, data);
|
||||||
|
graph.setParent(v, parentCluster.id, data);
|
||||||
|
}
|
||||||
|
log.trace('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||||
|
if (node.clusterNode) {
|
||||||
|
// const children = graph.children(v);
|
||||||
|
log.trace('Cluster identified', v, node, graph.node(v));
|
||||||
|
const newEl = recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
|
||||||
|
updateNodeBounds(node, newEl);
|
||||||
|
setNodeElem(newEl, node);
|
||||||
|
|
||||||
|
log.warn('Recursive render complete', newEl, node);
|
||||||
} else {
|
} else {
|
||||||
// const width = getClusterTitleWidth(clusters, node);
|
if (graph.children(v).length > 0) {
|
||||||
const children = graph.children(v);
|
// This is a cluster but not to be rendered recusively
|
||||||
logger.info('Cluster identified', node.id, children[0]);
|
// Render as before
|
||||||
// nodes2expand.push({ id: children[0], width });
|
log.trace('Cluster - the non recursive path', v, node.id, node, graph);
|
||||||
clusterDb[node.id] = { id: children[0] };
|
log.trace(findNonClusterChild(node.id, graph));
|
||||||
logger.info('Clusters ', clusterDb);
|
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
|
||||||
|
// insertCluster(clusters, graph.node(v));
|
||||||
|
} else {
|
||||||
|
log.trace('Node - the non recursive path', v, node.id, node);
|
||||||
|
insertNode(nodes, graph.node(v));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,49 +69,77 @@ export const render = (elem, graph, markers, diagramtype, id) => {
|
|||||||
// Edges from/to clusters really points to the first child in the cluster.
|
// Edges from/to clusters really points to the first child in the cluster.
|
||||||
// TODO: pick optimal child in the cluster to us as link anchor
|
// TODO: pick optimal child in the cluster to us as link anchor
|
||||||
graph.edges().forEach(function(e) {
|
graph.edges().forEach(function(e) {
|
||||||
const edge = graph.edge(e);
|
const edge = graph.edge(e.v, e.w, e.name);
|
||||||
logger.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e)));
|
||||||
const v = translateClusterId(e.v);
|
|
||||||
const w = translateClusterId(e.w);
|
// Check if link is either from or to a cluster
|
||||||
if (v !== e.v || w !== e.w) {
|
log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
|
||||||
graph.removeEdge(e.v, e.w, e.name);
|
|
||||||
if (v !== e.v) edge.fromCluster = e.v;
|
|
||||||
if (w !== e.w) edge.toCluster = e.w;
|
|
||||||
graph.setEdge(v, w, edge, e.name);
|
|
||||||
}
|
|
||||||
insertEdgeLabel(edgeLabels, edge);
|
insertEdgeLabel(edgeLabels, edge);
|
||||||
});
|
});
|
||||||
|
|
||||||
graph.edges().forEach(function(e) {
|
graph.edges().forEach(function(e) {
|
||||||
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
});
|
});
|
||||||
logger.info('#############################################');
|
log.trace('#############################################');
|
||||||
logger.info('### Layout ###');
|
log.trace('### Layout ###');
|
||||||
logger.info('#############################################');
|
log.trace('#############################################');
|
||||||
logger.info(graph);
|
log.trace(graph);
|
||||||
dagre.layout(graph);
|
dagre.layout(graph);
|
||||||
|
log.warn('Graph after layout:', graphlib.json.write(graph));
|
||||||
// Move the nodes to the correct place
|
// Move the nodes to the correct place
|
||||||
graph.nodes().forEach(function(v) {
|
graph.nodes().forEach(function(v) {
|
||||||
const node = graph.node(v);
|
const node = graph.node(v);
|
||||||
logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
|
// log.trace('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||||
if (node.type !== 'group') {
|
log.trace(
|
||||||
|
'Position ' + v + ': (' + node.x,
|
||||||
|
',' + node.y,
|
||||||
|
') width: ',
|
||||||
|
node.width,
|
||||||
|
' height: ',
|
||||||
|
node.height
|
||||||
|
);
|
||||||
|
if (node && node.clusterNode) {
|
||||||
|
// clusterDb[node.id].node = node;
|
||||||
|
|
||||||
positionNode(node);
|
positionNode(node);
|
||||||
} else {
|
} else {
|
||||||
|
// Non cluster node
|
||||||
|
if (graph.children(v).length > 0) {
|
||||||
|
// A cluster in the non-recurive way
|
||||||
|
// positionCluster(node);
|
||||||
insertCluster(clusters, node);
|
insertCluster(clusters, node);
|
||||||
clusterDb[node.id].node = node;
|
clusterDb[node.id].node = node;
|
||||||
|
} else {
|
||||||
|
positionNode(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move the edge labels to the correct place after layout
|
// Move the edge labels to the correct place after layout
|
||||||
graph.edges().forEach(function(e) {
|
graph.edges().forEach(function(e) {
|
||||||
const edge = graph.edge(e);
|
const edge = graph.edge(e);
|
||||||
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||||
|
|
||||||
insertEdge(edgePaths, edge, clusterDb, diagramtype);
|
insertEdge(edgePaths, edge, clusterDb, diagramtype);
|
||||||
positionEdgeLabel(edge);
|
positionEdgeLabel(edge);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const render = (elem, graph, markers, diagramtype, id) => {
|
||||||
|
insertMarkers(elem, markers, diagramtype, id);
|
||||||
|
clearNodes();
|
||||||
|
clearEdges();
|
||||||
|
clearClusters();
|
||||||
|
clearGraphlib();
|
||||||
|
|
||||||
|
log.warn('Graph before:', graphlib.json.write(graph));
|
||||||
|
adjustClustersAndEdges(graph);
|
||||||
|
log.warn('Graph after:', graphlib.json.write(graph));
|
||||||
|
|
||||||
|
recursiveRender(elem, graph, diagramtype);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const shapeDefinitions = {};
|
// const shapeDefinitions = {};
|
||||||
|
@ -16,7 +16,7 @@ const extension = (elem, type, id) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'extensionStart')
|
.attr('id', type + '-extensionStart')
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 0)
|
.attr('refX', 0)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -29,7 +29,7 @@ const extension = (elem, type, id) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'extensionEnd ' + type)
|
.attr('id', type + '-extensionEnd ' + type)
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 19)
|
.attr('refX', 19)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -44,7 +44,7 @@ const composition = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'compositionStart')
|
.attr('id', type + '-compositionStart')
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 0)
|
.attr('refX', 0)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -57,7 +57,7 @@ const composition = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'compositionEnd')
|
.attr('id', type + '-compositionEnd')
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 19)
|
.attr('refX', 19)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -71,7 +71,7 @@ const aggregation = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'aggregationStart')
|
.attr('id', type + '-aggregationStart')
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 0)
|
.attr('refX', 0)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -84,7 +84,7 @@ const aggregation = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'aggregationEnd')
|
.attr('id', type + '-aggregationEnd')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('refX', 19)
|
.attr('refX', 19)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -98,7 +98,7 @@ const dependency = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'dependencyStart')
|
.attr('id', type + '-dependencyStart')
|
||||||
.attr('class', 'extension ' + type)
|
.attr('class', 'extension ' + type)
|
||||||
.attr('refX', 0)
|
.attr('refX', 0)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -111,7 +111,7 @@ const dependency = (elem, type) => {
|
|||||||
elem
|
elem
|
||||||
.append('defs')
|
.append('defs')
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'dependencyEnd')
|
.attr('id', type + '-dependencyEnd')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('refX', 19)
|
.attr('refX', 19)
|
||||||
.attr('refY', 7)
|
.attr('refY', 7)
|
||||||
@ -158,7 +158,7 @@ const point = (elem, type) => {
|
|||||||
const circle = (elem, type) => {
|
const circle = (elem, type) => {
|
||||||
elem
|
elem
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'circleEnd')
|
.attr('id', type + '-circleEnd')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('viewBox', '0 0 10 10')
|
.attr('viewBox', '0 0 10 10')
|
||||||
.attr('refX', 11)
|
.attr('refX', 11)
|
||||||
@ -177,7 +177,7 @@ const circle = (elem, type) => {
|
|||||||
|
|
||||||
elem
|
elem
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'circleStart')
|
.attr('id', type + '-circleStart')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('viewBox', '0 0 10 10')
|
.attr('viewBox', '0 0 10 10')
|
||||||
.attr('refX', -1)
|
.attr('refX', -1)
|
||||||
@ -197,7 +197,7 @@ const circle = (elem, type) => {
|
|||||||
const cross = (elem, type) => {
|
const cross = (elem, type) => {
|
||||||
elem
|
elem
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'crossEnd')
|
.attr('id', type + '-crossEnd')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('viewBox', '0 0 11 11')
|
.attr('viewBox', '0 0 11 11')
|
||||||
.attr('refX', 12)
|
.attr('refX', 12)
|
||||||
@ -215,7 +215,7 @@ const cross = (elem, type) => {
|
|||||||
|
|
||||||
elem
|
elem
|
||||||
.append('marker')
|
.append('marker')
|
||||||
.attr('id', 'crossStart')
|
.attr('id', type + '-crossStart')
|
||||||
.attr('class', type)
|
.attr('class', type)
|
||||||
.attr('viewBox', '0 0 11 11')
|
.attr('viewBox', '0 0 11 11')
|
||||||
.attr('refX', -1)
|
.attr('refX', -1)
|
||||||
|
372
src/dagre-wrapper/mermaid-graphlib.js
Normal file
372
src/dagre-wrapper/mermaid-graphlib.js
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
/**
|
||||||
|
* Decorates with functions required by mermaids dagre-wrapper.
|
||||||
|
*/
|
||||||
|
import { logger as log } from '../logger';
|
||||||
|
import graphlib from 'graphlib';
|
||||||
|
|
||||||
|
export let clusterDb = {};
|
||||||
|
let decendants = {};
|
||||||
|
let parents = {};
|
||||||
|
|
||||||
|
export const clear = () => {
|
||||||
|
decendants = {};
|
||||||
|
parents = {};
|
||||||
|
clusterDb = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDecendant = (id, ancenstorId) => {
|
||||||
|
// if (id === ancenstorId) return true;
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'In isDecendant',
|
||||||
|
ancenstorId,
|
||||||
|
' ',
|
||||||
|
id,
|
||||||
|
' = ',
|
||||||
|
decendants[ancenstorId].indexOf(id) >= 0
|
||||||
|
);
|
||||||
|
if (decendants[ancenstorId].indexOf(id) >= 0) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeInCluster = (edge, clusterId) => {
|
||||||
|
// Edges to/from the cluster is not in the cluster, they are in the parent
|
||||||
|
if (!(edge.v === clusterId || edge.w === clusterId)) return false;
|
||||||
|
|
||||||
|
if (!decendants[clusterId]) {
|
||||||
|
log.debug('Tilt, ', clusterId, ',not in decendants');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decendants[clusterId].indexOf(edge.v) >= 0) return true;
|
||||||
|
if (isDecendant(edge.v, clusterId)) return true;
|
||||||
|
if (isDecendant(edge.w, clusterId)) return true;
|
||||||
|
if (decendants[clusterId].indexOf(edge.w) >= 0) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (clusterId, graph, newGraph, rootId) => {
|
||||||
|
log.trace(
|
||||||
|
'Copying children of ',
|
||||||
|
clusterId,
|
||||||
|
rootId,
|
||||||
|
' from ',
|
||||||
|
clusterId,
|
||||||
|
graph.node(clusterId),
|
||||||
|
rootId
|
||||||
|
);
|
||||||
|
const nodes = graph.children(clusterId) || [];
|
||||||
|
|
||||||
|
// Include cluster node if it is not the root
|
||||||
|
if (clusterId !== rootId) {
|
||||||
|
nodes.push(clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Copying (nodes)', nodes);
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (graph.children(node).length > 0) {
|
||||||
|
copy(node, graph, newGraph, rootId);
|
||||||
|
} else {
|
||||||
|
const data = graph.node(node);
|
||||||
|
log.trace('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
|
||||||
|
newGraph.setNode(node, data);
|
||||||
|
if (clusterId !== rootId && node !== clusterId) {
|
||||||
|
log.debug('Setting parent', node, clusterId);
|
||||||
|
newGraph.setParent(node, clusterId);
|
||||||
|
}
|
||||||
|
const edges = graph.edges(node);
|
||||||
|
log.debug('Copying Edges', edges);
|
||||||
|
edges.forEach(edge => {
|
||||||
|
log.trace('Edge', edge);
|
||||||
|
const data = graph.edge(edge.v, edge.w, edge.name);
|
||||||
|
log.trace('Edge data', data, rootId);
|
||||||
|
try {
|
||||||
|
// Do not copy edges in and out of the root cluster, they belong to the parent graph
|
||||||
|
if (edgeInCluster(edge, rootId)) {
|
||||||
|
log.trace('Copying as ', edge.v, edge.w, data, edge.name);
|
||||||
|
newGraph.setEdge(edge.v, edge.w, data, edge.name);
|
||||||
|
log.trace('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
|
||||||
|
} else {
|
||||||
|
log.trace('Skipping copy of edge as ', rootId, edge.v, edge.w, clusterId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.debug('Removing node', node);
|
||||||
|
graph.removeNode(node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const extractDecendants = (id, graph) => {
|
||||||
|
// log.debug('Extracting ', id);
|
||||||
|
const children = graph.children(id);
|
||||||
|
let res = [].concat(children);
|
||||||
|
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
parents[children[i]] = id;
|
||||||
|
res = res.concat(extractDecendants(children[i], graph));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the graph, checking that all parent child relation points to existing nodes and that
|
||||||
|
* edges between nodes also ia correct. When not correct the function logs the discrepancies.
|
||||||
|
* @param {graphlib graph} g
|
||||||
|
*/
|
||||||
|
export const validate = graph => {
|
||||||
|
const edges = graph.edges();
|
||||||
|
log.trace('Edges: ', edges);
|
||||||
|
for (let i = 0; i < edges.length; i++) {
|
||||||
|
if (graph.children(edges[i].v).length > 0) {
|
||||||
|
log.trace('The node ', edges[i].v, ' is part of and edge even though it has children');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (graph.children(edges[i].w).length > 0) {
|
||||||
|
log.trace('The node ', edges[i].w, ' is part of and edge even though it has children');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a child that is not a cluster. When faking a edge between a node and a cluster.
|
||||||
|
* @param {Finds a } id
|
||||||
|
* @param {*} graph
|
||||||
|
*/
|
||||||
|
export const findNonClusterChild = (id, graph) => {
|
||||||
|
// const node = graph.node(id);
|
||||||
|
log.trace('Searching', id);
|
||||||
|
const children = graph.children(id);
|
||||||
|
if (children.length < 1) {
|
||||||
|
log.trace('This is a valid node', id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const _id = findNonClusterChild(children[i], graph);
|
||||||
|
if (_id) {
|
||||||
|
log.trace('Found replacement for', id, ' => ', _id);
|
||||||
|
return _id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnchorId = id => {
|
||||||
|
if (!clusterDb[id]) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
// If the cluster has no external connections
|
||||||
|
if (!clusterDb[id].externalConnections) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the replacement node
|
||||||
|
if (clusterDb[id]) {
|
||||||
|
return clusterDb[id].id;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adjustClustersAndEdges = (graph, depth) => {
|
||||||
|
if (!graph || depth > 10) {
|
||||||
|
log.debug('Opting out, no graph ');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.debug('Opting in, graph ');
|
||||||
|
}
|
||||||
|
// Go through the nodes and for each cluster found, save a replacment node, this can be used when
|
||||||
|
// faking a link to a cluster
|
||||||
|
graph.nodes().forEach(function(id) {
|
||||||
|
const children = graph.children(id);
|
||||||
|
if (children.length > 0) {
|
||||||
|
log.trace(
|
||||||
|
'Cluster identified',
|
||||||
|
id,
|
||||||
|
' Replacement id in edges: ',
|
||||||
|
findNonClusterChild(id, graph)
|
||||||
|
);
|
||||||
|
decendants[id] = extractDecendants(id, graph);
|
||||||
|
clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check incoming and outgoing edges for each cluster
|
||||||
|
graph.nodes().forEach(function(id) {
|
||||||
|
const children = graph.children(id);
|
||||||
|
const edges = graph.edges();
|
||||||
|
if (children.length > 0) {
|
||||||
|
log.debug('Cluster identified', id, decendants);
|
||||||
|
edges.forEach(edge => {
|
||||||
|
// log.debug('Edge, decendants: ', edge, decendants[id]);
|
||||||
|
|
||||||
|
// Check if any edge leaves the cluster (not the actual cluster, thats a link from the box)
|
||||||
|
if (edge.v !== id && edge.w !== id) {
|
||||||
|
// Any edge where either the one of the nodes is decending to the cluster but not the other
|
||||||
|
// if (decendants[id].indexOf(edge.v) < 0 && decendants[id].indexOf(edge.w) < 0) {
|
||||||
|
|
||||||
|
const d1 = isDecendant(edge.v, id);
|
||||||
|
const d2 = isDecendant(edge.w, id);
|
||||||
|
|
||||||
|
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
|
||||||
|
if (d1 ^ d2) {
|
||||||
|
log.debug('Edge: ', edge, ' leaves cluster ', id);
|
||||||
|
log.debug('Decendants of ', id, ': ', decendants[id]);
|
||||||
|
clusterDb[id].externalConnections = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
extractor(graph, 0);
|
||||||
|
|
||||||
|
// For clusters with incoming and/or outgoing edges translate those edges to a real node
|
||||||
|
// in the cluster inorder to fake the edge
|
||||||
|
graph.edges().forEach(function(e) {
|
||||||
|
const edge = graph.edge(e);
|
||||||
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
|
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
|
||||||
|
|
||||||
|
let v = e.v;
|
||||||
|
let w = e.w;
|
||||||
|
// Check if link is either from or to a cluster
|
||||||
|
log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
|
||||||
|
if (clusterDb[e.v] || clusterDb[e.w]) {
|
||||||
|
log.trace('Fixing and trixing - removing', e.v, e.w, e.name);
|
||||||
|
v = getAnchorId(e.v);
|
||||||
|
w = getAnchorId(e.w);
|
||||||
|
graph.removeEdge(e.v, e.w, e.name);
|
||||||
|
if (v !== e.v) edge.fromCluster = e.v;
|
||||||
|
if (w !== e.w) edge.toCluster = e.w;
|
||||||
|
log.trace('Replacing with', v, w, e.name);
|
||||||
|
graph.setEdge(v, w, edge, e.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.debug('Adjusted Graph', graphlib.json.write(graph));
|
||||||
|
|
||||||
|
log.trace(clusterDb);
|
||||||
|
|
||||||
|
// Remove references to extracted cluster
|
||||||
|
// graph.edges().forEach(edge => {
|
||||||
|
// if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) {
|
||||||
|
// graph.removeEdge(edge);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractor = (graph, depth) => {
|
||||||
|
log.debug('extractor - ', depth, graphlib.json.write(graph), graph.children('D'));
|
||||||
|
if (depth > 10) {
|
||||||
|
log.error('Bailing out');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// For clusters without incoming and/or outgoing edges, create a new cluster-node
|
||||||
|
// containing the nodes and edges in the custer in a new graph
|
||||||
|
// for (let i = 0;)
|
||||||
|
let nodes = graph.nodes();
|
||||||
|
let hasChildren = false;
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const children = graph.children(node);
|
||||||
|
hasChildren = hasChildren || children.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChildren) {
|
||||||
|
log.debug('Done, no node has children', graph.nodes());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// const clusters = Object.keys(clusterDb);
|
||||||
|
// clusters.forEach(clusterId => {
|
||||||
|
log.debug('Nodes = ', nodes, depth);
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'Extracting node',
|
||||||
|
node,
|
||||||
|
clusterDb,
|
||||||
|
clusterDb[node] && !clusterDb[node].externalConnections,
|
||||||
|
!graph.parent(node),
|
||||||
|
graph.node(node),
|
||||||
|
graph.children('D'),
|
||||||
|
' Depth ',
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
// Note that the node might have been removed after the Object.keys call so better check
|
||||||
|
// that it still is in the game
|
||||||
|
if (!clusterDb[node]) {
|
||||||
|
// Skip if the node is not a cluster
|
||||||
|
log.debug('Not a cluster', node, depth);
|
||||||
|
// break;
|
||||||
|
} else if (
|
||||||
|
!clusterDb[node].externalConnections &&
|
||||||
|
!graph.parent(node) &&
|
||||||
|
graph.children(node) &&
|
||||||
|
graph.children(node).length > 0
|
||||||
|
) {
|
||||||
|
log.debug(
|
||||||
|
'Cluster without external connections, without a parent and with children',
|
||||||
|
node,
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
|
||||||
|
const clusterGraph = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
})
|
||||||
|
.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
// Todo: set proper spacing
|
||||||
|
nodesep: 50,
|
||||||
|
ranksep: 50,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
})
|
||||||
|
.setDefaultEdgeLabel(function() {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
copy(node, graph, clusterGraph, node);
|
||||||
|
graph.setNode(node, {
|
||||||
|
clusterNode: true,
|
||||||
|
id: node,
|
||||||
|
clusterData: clusterDb[node].clusterData,
|
||||||
|
labelText: clusterDb[node].labelText,
|
||||||
|
graph: clusterGraph
|
||||||
|
});
|
||||||
|
log.debug('New graph after copy', graphlib.json.write(clusterGraph));
|
||||||
|
log.debug('Old graph after copy', graphlib.json.write(graph));
|
||||||
|
} else {
|
||||||
|
log.debug(
|
||||||
|
'Cluster ** ',
|
||||||
|
node,
|
||||||
|
' **not meeting the criteria !externalConnections:',
|
||||||
|
!clusterDb[node].externalConnections,
|
||||||
|
' no parent: ',
|
||||||
|
!graph.parent(node),
|
||||||
|
' children ',
|
||||||
|
graph.children(node) && graph.children(node).length > 0,
|
||||||
|
graph.children('D'),
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
log.debug(clusterDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = graph.nodes();
|
||||||
|
log.debug('New list of nodes', nodes);
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const data = graph.node(node);
|
||||||
|
log.debug(' Now next leveö', node, data);
|
||||||
|
if (data.clusterNode) {
|
||||||
|
extractor(data.graph, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
370
src/dagre-wrapper/mermaid-graphlib.spec.js
Normal file
370
src/dagre-wrapper/mermaid-graphlib.spec.js
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import graphlib from 'graphlib';
|
||||||
|
import dagre from 'dagre';
|
||||||
|
import { validate, adjustClustersAndEdges, extractDecendants } from './mermaid-graphlib';
|
||||||
|
import { setLogLevel, logger } from '../logger';
|
||||||
|
|
||||||
|
describe('Graphlib decorations', () => {
|
||||||
|
let g;
|
||||||
|
beforeEach(function () {
|
||||||
|
setLogLevel(1);
|
||||||
|
g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 10,
|
||||||
|
ranksep: 10,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate', function () {
|
||||||
|
it('Validate should detect edges between clusters', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a --> b
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
c
|
||||||
|
end
|
||||||
|
C1 --> C2
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data:1});
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('c', 'C2');
|
||||||
|
g.setEdge('a', 'b');
|
||||||
|
g.setEdge('C1', 'C2');
|
||||||
|
|
||||||
|
console.log(g.nodes())
|
||||||
|
|
||||||
|
expect(validate(g)).toBe(false);
|
||||||
|
});
|
||||||
|
it('Validate should not detect edges between clusters after adjustment', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a --> b
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
c
|
||||||
|
end
|
||||||
|
C1 --> C2
|
||||||
|
*/
|
||||||
|
g.setNode('a', {});
|
||||||
|
g.setNode('b', {});
|
||||||
|
g.setNode('c', {});
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('c', 'C2');
|
||||||
|
g.setEdge('a', 'b');
|
||||||
|
g.setEdge('C1', 'C2');
|
||||||
|
|
||||||
|
console.log(g.nodes())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.info(g.edges())
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Validate should detect edges between clusters and transform clusters GLB4', function () {
|
||||||
|
/*
|
||||||
|
a --> b
|
||||||
|
subgraph C1
|
||||||
|
subgraph C2
|
||||||
|
a
|
||||||
|
end
|
||||||
|
b
|
||||||
|
end
|
||||||
|
C1 --> c
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setNode('C1', { data: 4 });
|
||||||
|
g.setNode('C2', { data: 5 });
|
||||||
|
g.setParent('a', 'C2');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('C2', 'C1');
|
||||||
|
g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||||
|
g.setEdge('C1', 'c', { name: 'C1-external-link' });
|
||||||
|
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.info(g.nodes())
|
||||||
|
expect(g.nodes().length).toBe(2);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
it('Validate should detect edges between clusters and transform clusters GLB5', function () {
|
||||||
|
/*
|
||||||
|
a --> b
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
b
|
||||||
|
end
|
||||||
|
C1 -->
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C2');
|
||||||
|
// g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||||
|
g.setEdge('C1', 'C2', { name: 'C1-external-link' });
|
||||||
|
|
||||||
|
logger.info(g.nodes())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.info(g.nodes())
|
||||||
|
expect(g.nodes().length).toBe(2);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB6', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
C1 --> b
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('C1', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||||
|
|
||||||
|
// logger.info(g.edges())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.info(g.edges())
|
||||||
|
expect(g.nodes()).toEqual(['b', 'C1']);
|
||||||
|
expect(g.edges().length).toBe(1);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
expect(g.node('C1').clusterNode).toBe(true);
|
||||||
|
|
||||||
|
const C1Graph = g.node('C1').graph;
|
||||||
|
expect(C1Graph.nodes()).toEqual(['a']);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB7', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
C1 --> b
|
||||||
|
C1 --> c
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setNode('C1', { data: 4 });
|
||||||
|
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('C1', 'c', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
logger.info(g.node('C1'))
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.info(g.edges())
|
||||||
|
expect(g.nodes()).toEqual(['b', 'c', 'C1']);
|
||||||
|
expect(g.nodes().length).toBe(3);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
const edgeData = g.edge(g.edges()[1]);
|
||||||
|
expect(edgeData.data).toBe('link2');
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
|
||||||
|
const C1Graph = g.node('C1').graph;
|
||||||
|
expect(C1Graph.nodes()).toEqual(['a']);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB8', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
c
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('c', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
// logger.info(g.edges())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
expect(g.nodes()).toEqual(['A', 'B', 'C']);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
const edgeData = g.edge(g.edges()[1]);
|
||||||
|
expect(edgeData.data).toBe('link2');
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
expect(CGraph.nodes()).toEqual(['c']);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () {
|
||||||
|
/*
|
||||||
|
subgraph C
|
||||||
|
subgraph D
|
||||||
|
d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
|
||||||
|
g.setNode('C', { data: 1 });
|
||||||
|
g.setNode('D', { data: 2 });
|
||||||
|
g.setNode('d', { data: 3 });
|
||||||
|
g.setParent('d', 'D');
|
||||||
|
g.setParent('D', 'C');
|
||||||
|
|
||||||
|
// logger.info('Graph before', g.node('D'))
|
||||||
|
// logger.info('Graph before', graphlib.json.write(g))
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
// logger.info('Graph after', graphlib.json.write(g), g.node('C').graph)
|
||||||
|
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
const DGraph = CGraph.node('D').graph;
|
||||||
|
|
||||||
|
expect(CGraph.nodes()).toEqual(['D']);
|
||||||
|
expect(DGraph.nodes()).toEqual(['d']);
|
||||||
|
|
||||||
|
expect(g.nodes()).toEqual(['C']);
|
||||||
|
expect(g.nodes().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
subgraph D
|
||||||
|
d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
|
||||||
|
g.setNode('C', { data: 1 });
|
||||||
|
g.setNode('D', { data: 2 });
|
||||||
|
g.setNode('d', { data: 3 });
|
||||||
|
g.setNode('B', { data: 4 });
|
||||||
|
g.setNode('b', { data: 5 });
|
||||||
|
g.setNode('A', { data: 6 });
|
||||||
|
g.setNode('a', { data: 7 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('d', 'D');
|
||||||
|
g.setParent('D', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
logger.info('Graph before', g.node('D'))
|
||||||
|
logger.info('Graph before', graphlib.json.write(g))
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
logger.trace('Graph after', graphlib.json.write(g))
|
||||||
|
expect(g.nodes()).toEqual(['C', 'B', 'A']);
|
||||||
|
expect(g.nodes().length).toBe(3);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
const AGraph = g.node('A').graph;
|
||||||
|
const BGraph = g.node('B').graph;
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
// logger.info(CGraph.nodes());
|
||||||
|
const DGraph = CGraph.node('D').graph;
|
||||||
|
// logger.info('DG', CGraph.children('D'));
|
||||||
|
|
||||||
|
logger.info('A', AGraph.nodes());
|
||||||
|
expect(AGraph.nodes().length).toBe(1);
|
||||||
|
expect(AGraph.nodes()).toEqual(['a']);
|
||||||
|
logger.trace('Nodes', BGraph.nodes())
|
||||||
|
expect(BGraph.nodes().length).toBe(1);
|
||||||
|
expect(BGraph.nodes()).toEqual(['b']);
|
||||||
|
expect(CGraph.nodes()).toEqual(['D']);
|
||||||
|
expect(CGraph.nodes().length).toEqual(1);
|
||||||
|
|
||||||
|
expect(AGraph.edges().length).toBe(0);
|
||||||
|
expect(BGraph.edges().length).toBe(0);
|
||||||
|
expect(CGraph.edges().length).toBe(0);
|
||||||
|
expect(DGraph.nodes()).toEqual(['d']);
|
||||||
|
expect(DGraph.edges().length).toBe(0);
|
||||||
|
// expect(CGraph.node('D')).toEqual({ data: 2 });
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
// expect(g.edges().length).toBe(2);
|
||||||
|
// const edgeData = g.edge(g.edges()[1]);
|
||||||
|
// expect(edgeData.data).toBe('link2');
|
||||||
|
// expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('extractDecendants', function () {
|
||||||
|
let g;
|
||||||
|
beforeEach(function () {
|
||||||
|
setLogLevel(1);
|
||||||
|
g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 10,
|
||||||
|
ranksep: 10,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Simple case of one level decendants GLB9', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
c
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('c', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
// logger.info(g.edges())
|
||||||
|
const d1 = extractDecendants('A',g)
|
||||||
|
const d2 = extractDecendants('B',g)
|
||||||
|
const d3 = extractDecendants('C',g)
|
||||||
|
|
||||||
|
expect(d1).toEqual(['a']);
|
||||||
|
expect(d2).toEqual(['b']);
|
||||||
|
expect(d3).toEqual(['c']);
|
||||||
|
});
|
||||||
|
});
|
@ -248,7 +248,7 @@ const cylinder = (parent, node) => {
|
|||||||
const rect = (parent, node) => {
|
const rect = (parent, node) => {
|
||||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
|
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
|
||||||
|
|
||||||
logger.info('Classes = ', node.classes);
|
logger.trace('Classes = ', node.classes);
|
||||||
// add the rect
|
// add the rect
|
||||||
const rect = shapeSvg.insert('rect', ':first-child');
|
const rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
@ -387,11 +387,31 @@ let nodeElems = {};
|
|||||||
export const insertNode = (elem, node) => {
|
export const insertNode = (elem, node) => {
|
||||||
nodeElems[node.id] = shapes[node.shape](elem, node);
|
nodeElems[node.id] = shapes[node.shape](elem, node);
|
||||||
};
|
};
|
||||||
|
export const setNodeElem = (elem, node) => {
|
||||||
|
nodeElems[node.id] = elem;
|
||||||
|
};
|
||||||
export const clear = () => {
|
export const clear = () => {
|
||||||
nodeElems = {};
|
nodeElems = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const positionNode = node => {
|
export const positionNode = node => {
|
||||||
const el = nodeElems[node.id];
|
const el = nodeElems[node.id];
|
||||||
|
logger.trace(
|
||||||
|
'Transforming node',
|
||||||
|
node,
|
||||||
|
'translate(' + (node.x - node.width / 2 - 5) + ', ' + (node.y - node.height / 2 - 5) + ')'
|
||||||
|
);
|
||||||
|
const padding = 8;
|
||||||
|
if (node.clusterNode) {
|
||||||
|
el.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' +
|
||||||
|
(node.x - node.width / 2 - padding) +
|
||||||
|
', ' +
|
||||||
|
(node.y - node.height / 2 - padding) +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -25,8 +25,10 @@ export const sanitizeText = (text, config) => {
|
|||||||
return txt;
|
return txt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const lineBreakRegex = /<br\s*\/?>/gi;
|
||||||
|
|
||||||
const breakToPlaceholder = s => {
|
const breakToPlaceholder = s => {
|
||||||
return s.replace(/<br\s*\/?>/gi, '#br#');
|
return s.replace(lineBreakRegex, '#br#');
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeholderToBreak = s => {
|
const placeholderToBreak = s => {
|
||||||
@ -35,5 +37,6 @@ const placeholderToBreak = s => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
getRows,
|
getRows,
|
||||||
sanitizeText
|
sanitizeText,
|
||||||
|
lineBreakRegex
|
||||||
};
|
};
|
||||||
|
@ -154,6 +154,28 @@ function stadium(parent, bbox, node) {
|
|||||||
return shapeSvg;
|
return shapeSvg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function subroutine(parent, bbox, node) {
|
||||||
|
const w = bbox.width;
|
||||||
|
const h = bbox.height;
|
||||||
|
const points = [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: w, y: 0 },
|
||||||
|
{ x: w, y: -h },
|
||||||
|
{ x: 0, y: -h },
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: -8, y: 0 },
|
||||||
|
{ x: w + 8, y: 0 },
|
||||||
|
{ x: w + 8, y: -h },
|
||||||
|
{ x: -8, y: -h },
|
||||||
|
{ x: -8, y: 0 }
|
||||||
|
];
|
||||||
|
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||||
|
node.intersect = function(point) {
|
||||||
|
return dagreD3.intersect.polygon(node, points, point);
|
||||||
|
};
|
||||||
|
return shapeSvg;
|
||||||
|
}
|
||||||
|
|
||||||
function cylinder(parent, bbox, node) {
|
function cylinder(parent, bbox, node) {
|
||||||
const w = bbox.width;
|
const w = bbox.width;
|
||||||
const rx = w / 2;
|
const rx = w / 2;
|
||||||
@ -221,6 +243,7 @@ export function addToRender(render) {
|
|||||||
render.shapes().question = question;
|
render.shapes().question = question;
|
||||||
render.shapes().hexagon = hexagon;
|
render.shapes().hexagon = hexagon;
|
||||||
render.shapes().stadium = stadium;
|
render.shapes().stadium = stadium;
|
||||||
|
render.shapes().subroutine = subroutine;
|
||||||
render.shapes().cylinder = cylinder;
|
render.shapes().cylinder = cylinder;
|
||||||
|
|
||||||
// Add custom shape for box with inverted arrow on left side
|
// Add custom shape for box with inverted arrow on left side
|
||||||
@ -246,6 +269,7 @@ export function addToRenderV2(addShape) {
|
|||||||
addShape({ question });
|
addShape({ question });
|
||||||
addShape({ hexagon });
|
addShape({ hexagon });
|
||||||
addShape({ stadium });
|
addShape({ stadium });
|
||||||
|
addShape({ subroutine });
|
||||||
addShape({ cylinder });
|
addShape({ cylinder });
|
||||||
|
|
||||||
// Add custom shape for box with inverted arrow on left side
|
// Add custom shape for box with inverted arrow on left side
|
||||||
|
@ -65,7 +65,8 @@ describe('flowchart shapes', function() {
|
|||||||
['lean_right', 4, useWidth, useHeight],
|
['lean_right', 4, useWidth, useHeight],
|
||||||
['lean_left', 4, useWidth, useHeight],
|
['lean_left', 4, useWidth, useHeight],
|
||||||
['trapezoid', 4, useWidth, useHeight],
|
['trapezoid', 4, useWidth, useHeight],
|
||||||
['inv_trapezoid', 4, useWidth, useHeight]
|
['inv_trapezoid', 4, useWidth, useHeight],
|
||||||
|
['subroutine', 10, useWidth, useHeight],
|
||||||
].forEach(function([shapeType, expectedPointCount, getW, getH]) {
|
].forEach(function([shapeType, expectedPointCount, getW, getH]) {
|
||||||
it(`should add a ${shapeType} shape that renders a properly translated polygon element`, function() {
|
it(`should add a ${shapeType} shape that renders a properly translated polygon element`, function() {
|
||||||
const mockRender = MockRender();
|
const mockRender = MockRender();
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import graphlib from 'graphlib';
|
import graphlib from 'graphlib';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import dagre from 'dagre';
|
|
||||||
|
|
||||||
import flowDb from './flowDb';
|
import flowDb from './flowDb';
|
||||||
import flow from './parser/flow';
|
import flow from './parser/flow';
|
||||||
@ -9,6 +8,7 @@ import { getConfig } from '../../config';
|
|||||||
import { render } from '../../dagre-wrapper/index.js';
|
import { render } from '../../dagre-wrapper/index.js';
|
||||||
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
|
import common from '../common/common';
|
||||||
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
||||||
|
|
||||||
const conf = {};
|
const conf = {};
|
||||||
@ -62,7 +62,7 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||||
|
|
||||||
const rows = vertexText.split(/<br\s*\/?>/gi);
|
const rows = vertexText.split(common.lineBreakRegex);
|
||||||
|
|
||||||
for (let j = 0; j < rows.length; j++) {
|
for (let j = 0; j < rows.length; j++) {
|
||||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
@ -119,6 +119,9 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
case 'stadium':
|
case 'stadium':
|
||||||
_shape = 'stadium';
|
_shape = 'stadium';
|
||||||
break;
|
break;
|
||||||
|
case 'subroutine':
|
||||||
|
_shape = 'subroutine';
|
||||||
|
break;
|
||||||
case 'cylinder':
|
case 'cylinder':
|
||||||
_shape = 'cylinder';
|
_shape = 'cylinder';
|
||||||
break;
|
break;
|
||||||
@ -130,10 +133,8 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
}
|
}
|
||||||
// Add the node
|
// Add the node
|
||||||
g.setNode(vertex.id, {
|
g.setNode(vertex.id, {
|
||||||
labelType: 'svg',
|
|
||||||
labelStyle: styles.labelStyle,
|
labelStyle: styles.labelStyle,
|
||||||
shape: _shape,
|
shape: _shape,
|
||||||
label: vertexNode,
|
|
||||||
labelText: vertexText,
|
labelText: vertexText,
|
||||||
rx: radious,
|
rx: radious,
|
||||||
ry: radious,
|
ry: radious,
|
||||||
@ -146,10 +147,8 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info('setNode', {
|
logger.info('setNode', {
|
||||||
labelType: 'svg',
|
|
||||||
labelStyle: styles.labelStyle,
|
labelStyle: styles.labelStyle,
|
||||||
shape: _shape,
|
shape: _shape,
|
||||||
label: vertexNode,
|
|
||||||
labelText: vertexText,
|
labelText: vertexText,
|
||||||
rx: radious,
|
rx: radious,
|
||||||
ry: radious,
|
ry: radious,
|
||||||
@ -190,6 +189,8 @@ export const addEdges = function(edges, g) {
|
|||||||
} else {
|
} else {
|
||||||
edgeData.arrowhead = 'normal';
|
edgeData.arrowhead = 'normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(edgeData, edge);
|
||||||
edgeData.arrowType = edge.type;
|
edgeData.arrowType = edge.type;
|
||||||
|
|
||||||
let style = '';
|
let style = '';
|
||||||
@ -243,7 +244,7 @@ export const addEdges = function(edges, g) {
|
|||||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||||
} else {
|
} else {
|
||||||
edgeData.labelType = 'text';
|
edgeData.labelType = 'text';
|
||||||
edgeData.label = edge.text.replace(/<br\s*\/?>/gi, '\n');
|
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||||
|
|
||||||
if (typeof edge.style === 'undefined') {
|
if (typeof edge.style === 'undefined') {
|
||||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||||
@ -318,8 +319,10 @@ export const draw = function(text, id) {
|
|||||||
|
|
||||||
let subG;
|
let subG;
|
||||||
const subGraphs = flowDb.getSubGraphs();
|
const subGraphs = flowDb.getSubGraphs();
|
||||||
|
logger.info('Subgraphs - ', subGraphs);
|
||||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||||
subG = subGraphs[i];
|
subG = subGraphs[i];
|
||||||
|
logger.info('Subgraph - ', subG);
|
||||||
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +354,7 @@ export const draw = function(text, id) {
|
|||||||
// Run the renderer. This is what draws the final graph.
|
// Run the renderer. This is what draws the final graph.
|
||||||
const element = d3.select('#' + id + ' g');
|
const element = d3.select('#' + id + ' g');
|
||||||
render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);
|
render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);
|
||||||
dagre.layout(g);
|
// dagre.layout(g);
|
||||||
|
|
||||||
element.selectAll('g.node').attr('title', function() {
|
element.selectAll('g.node').attr('title', function() {
|
||||||
return flowDb.getTooltip(this.id);
|
return flowDb.getTooltip(this.id);
|
||||||
@ -382,27 +385,27 @@ export const draw = function(text, id) {
|
|||||||
// Index nodes
|
// Index nodes
|
||||||
flowDb.indexNodes('subGraph' + i);
|
flowDb.indexNodes('subGraph' + i);
|
||||||
|
|
||||||
// reposition labels
|
// // reposition labels
|
||||||
for (i = 0; i < subGraphs.length; i++) {
|
// for (i = 0; i < subGraphs.length; i++) {
|
||||||
subG = subGraphs[i];
|
// subG = subGraphs[i];
|
||||||
|
|
||||||
if (subG.title !== 'undefined') {
|
// if (subG.title !== 'undefined') {
|
||||||
const clusterRects = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"] rect');
|
// const clusterRects = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"] rect');
|
||||||
const clusterEl = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"]');
|
// const clusterEl = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"]');
|
||||||
|
|
||||||
const xPos = clusterRects[0].x.baseVal.value;
|
// const xPos = clusterRects[0].x.baseVal.value;
|
||||||
const yPos = clusterRects[0].y.baseVal.value;
|
// const yPos = clusterRects[0].y.baseVal.value;
|
||||||
const width = clusterRects[0].width.baseVal.value;
|
// const width = clusterRects[0].width.baseVal.value;
|
||||||
const cluster = d3.select(clusterEl[0]);
|
// const cluster = d3.select(clusterEl[0]);
|
||||||
const te = cluster.select('.label');
|
// const te = cluster.select('.label');
|
||||||
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
|
// te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
|
||||||
te.attr('id', id + 'Text');
|
// te.attr('id', id + 'Text');
|
||||||
|
|
||||||
for (let j = 0; j < subG.classes.length; j++) {
|
// for (let j = 0; j < subG.classes.length; j++) {
|
||||||
clusterEl[0].classList.add(subG.classes[j]);
|
// clusterEl[0].classList.add(subG.classes[j]);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Add label rects for non html labels
|
// Add label rects for non html labels
|
||||||
if (!conf.htmlLabels) {
|
if (!conf.htmlLabels) {
|
||||||
|
@ -8,6 +8,7 @@ import { getConfig } from '../../config';
|
|||||||
import dagreD3 from 'dagre-d3';
|
import dagreD3 from 'dagre-d3';
|
||||||
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
|
import common from '../common/common';
|
||||||
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
||||||
import flowChartShapes from './flowChartShapes';
|
import flowChartShapes from './flowChartShapes';
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||||
|
|
||||||
const rows = vertexText.split(/<br\s*\/?>/gi);
|
const rows = vertexText.split(common.lineBreakRegex);
|
||||||
|
|
||||||
for (let j = 0; j < rows.length; j++) {
|
for (let j = 0; j < rows.length; j++) {
|
||||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
@ -119,6 +120,9 @@ export const addVertices = function(vert, g, svgId) {
|
|||||||
case 'stadium':
|
case 'stadium':
|
||||||
_shape = 'stadium';
|
_shape = 'stadium';
|
||||||
break;
|
break;
|
||||||
|
case 'subroutine':
|
||||||
|
_shape = 'subroutine';
|
||||||
|
break;
|
||||||
case 'cylinder':
|
case 'cylinder':
|
||||||
_shape = 'cylinder';
|
_shape = 'cylinder';
|
||||||
break;
|
break;
|
||||||
@ -222,7 +226,7 @@ export const addEdges = function(edges, g) {
|
|||||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||||
} else {
|
} else {
|
||||||
edgeData.labelType = 'text';
|
edgeData.labelType = 'text';
|
||||||
edgeData.label = edge.text.replace(/<br\s*\/?>/gi, '\n');
|
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||||
|
|
||||||
if (typeof edge.style === 'undefined') {
|
if (typeof edge.style === 'undefined') {
|
||||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||||
@ -380,10 +384,6 @@ export const draw = function(text, id) {
|
|||||||
const svgBounds = svg.node().getBBox();
|
const svgBounds = svg.node().getBBox();
|
||||||
const width = svgBounds.width + padding * 2;
|
const width = svgBounds.width + padding * 2;
|
||||||
const height = svgBounds.height + padding * 2;
|
const height = svgBounds.height + padding * 2;
|
||||||
logger.debug(
|
|
||||||
`new ViewBox 0 0 ${width} ${height}`,
|
|
||||||
`translate(${padding - g._label.marginx}, ${padding - g._label.marginy})`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conf.useMaxWidth) {
|
if (conf.useMaxWidth) {
|
||||||
svg.attr('width', '100%');
|
svg.attr('width', '100%');
|
||||||
@ -393,10 +393,10 @@ export const draw = function(text, id) {
|
|||||||
svg.attr('width', width);
|
svg.attr('width', width);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
// Ensure the viewBox includes the whole svgBounds area with extra space for padding
|
||||||
svg
|
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
|
||||||
.select('g')
|
logger.debug(`viewBox ${vBox}`);
|
||||||
.attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`);
|
svg.attr('viewBox', vBox);
|
||||||
|
|
||||||
// Index nodes
|
// Index nodes
|
||||||
flowDb.indexNodes('subGraph' + i);
|
flowDb.indexNodes('subGraph' + i);
|
||||||
|
@ -23,6 +23,7 @@ describe('the flowchart renderer', function() {
|
|||||||
['circle', 'circle'],
|
['circle', 'circle'],
|
||||||
['ellipse', 'ellipse'],
|
['ellipse', 'ellipse'],
|
||||||
['stadium', 'stadium'],
|
['stadium', 'stadium'],
|
||||||
|
['subroutine', 'subroutine'],
|
||||||
['cylinder', 'cylinder'],
|
['cylinder', 'cylinder'],
|
||||||
['group', 'rect']
|
['group', 'rect']
|
||||||
].forEach(function([type, expectedShape, expectedRadios = 0]) {
|
].forEach(function([type, expectedShape, expectedRadios = 0]) {
|
||||||
|
@ -87,6 +87,8 @@
|
|||||||
"-)" return '-)';
|
"-)" return '-)';
|
||||||
"([" return 'STADIUMSTART';
|
"([" return 'STADIUMSTART';
|
||||||
"])" return 'STADIUMEND';
|
"])" return 'STADIUMEND';
|
||||||
|
"[[" return 'SUBROUTINESTART';
|
||||||
|
"]]" return 'SUBROUTINEEND';
|
||||||
"[(" return 'CYLINDERSTART';
|
"[(" return 'CYLINDERSTART';
|
||||||
")]" return 'CYLINDEREND';
|
")]" return 'CYLINDEREND';
|
||||||
\- return 'MINUS';
|
\- return 'MINUS';
|
||||||
@ -316,6 +318,8 @@ vertex: idString SQS text SQE
|
|||||||
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
|
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
|
||||||
| idString STADIUMSTART text STADIUMEND
|
| idString STADIUMSTART text STADIUMEND
|
||||||
{$$ = $1;yy.addVertex($1,$3,'stadium');}
|
{$$ = $1;yy.addVertex($1,$3,'stadium');}
|
||||||
|
| idString SUBROUTINESTART text SUBROUTINEEND
|
||||||
|
{$$ = $1;yy.addVertex($1,$3,'subroutine');}
|
||||||
| idString CYLINDERSTART text CYLINDEREND
|
| idString CYLINDERSTART text CYLINDEREND
|
||||||
{$$ = $1;yy.addVertex($1,$3,'cylinder');}
|
{$$ = $1;yy.addVertex($1,$3,'cylinder');}
|
||||||
| idString PS text PE
|
| idString PS text PE
|
||||||
@ -474,5 +478,5 @@ alphaNumToken : PUNCTUATION | AMP | UNICODE_TEXT | NUM| ALPHA | COLON | COMMA |
|
|||||||
|
|
||||||
idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION | AMP;
|
idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION | AMP;
|
||||||
|
|
||||||
graphCodeTokens: STADIUMSTART | STADIUMEND | CYLINDERSTART | CYLINDEREND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI;
|
graphCodeTokens: STADIUMSTART | STADIUMEND | SUBROUTINESTART | SUBROUTINEEND | CYLINDERSTART | CYLINDEREND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI;
|
||||||
%%
|
%%
|
||||||
|
@ -6,6 +6,7 @@ import { getConfig } from '../../config';
|
|||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
let dateFormat = '';
|
let dateFormat = '';
|
||||||
let axisFormat = '';
|
let axisFormat = '';
|
||||||
|
let todayMarker = '';
|
||||||
let excludes = [];
|
let excludes = [];
|
||||||
let title = '';
|
let title = '';
|
||||||
let sections = [];
|
let sections = [];
|
||||||
@ -27,6 +28,7 @@ export const clear = function() {
|
|||||||
rawTasks = [];
|
rawTasks = [];
|
||||||
dateFormat = '';
|
dateFormat = '';
|
||||||
axisFormat = '';
|
axisFormat = '';
|
||||||
|
todayMarker = '';
|
||||||
excludes = [];
|
excludes = [];
|
||||||
inclusiveEndDates = false;
|
inclusiveEndDates = false;
|
||||||
};
|
};
|
||||||
@ -39,6 +41,14 @@ export const getAxisFormat = function() {
|
|||||||
return axisFormat;
|
return axisFormat;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setTodayMarker = function(txt) {
|
||||||
|
todayMarker = txt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTodayMarker = function() {
|
||||||
|
return todayMarker;
|
||||||
|
};
|
||||||
|
|
||||||
export const setDateFormat = function(txt) {
|
export const setDateFormat = function(txt) {
|
||||||
dateFormat = txt;
|
dateFormat = txt;
|
||||||
};
|
};
|
||||||
@ -572,6 +582,8 @@ export default {
|
|||||||
endDatesAreInclusive,
|
endDatesAreInclusive,
|
||||||
setAxisFormat,
|
setAxisFormat,
|
||||||
getAxisFormat,
|
getAxisFormat,
|
||||||
|
setTodayMarker,
|
||||||
|
getTodayMarker,
|
||||||
setTitle,
|
setTitle,
|
||||||
getTitle,
|
getTitle,
|
||||||
addSection,
|
addSection,
|
||||||
|
@ -21,6 +21,7 @@ describe('when using the ganttDb', function() {
|
|||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||||
ganttDb.enableInclusiveEndDates();
|
ganttDb.enableInclusiveEndDates();
|
||||||
|
ganttDb.setTodayMarker('off');
|
||||||
ganttDb.setExcludes('weekends 2019-02-06,friday');
|
ganttDb.setExcludes('weekends 2019-02-06,friday');
|
||||||
ganttDb.addSection('weekends skip test');
|
ganttDb.addSection('weekends skip test');
|
||||||
ganttDb.addTask('test1', 'id1,2019-02-01,1d');
|
ganttDb.addTask('test1', 'id1,2019-02-01,1d');
|
||||||
@ -34,6 +35,7 @@ describe('when using the ganttDb', function() {
|
|||||||
${'getTitle'} | ${''}
|
${'getTitle'} | ${''}
|
||||||
${'getDateFormat'} | ${''}
|
${'getDateFormat'} | ${''}
|
||||||
${'getAxisFormat'} | ${''}
|
${'getAxisFormat'} | ${''}
|
||||||
|
${'getTodayMarker'} | ${''}
|
||||||
${'getExcludes'} | ${[]}
|
${'getExcludes'} | ${[]}
|
||||||
${'getSections'} | ${[]}
|
${'getSections'} | ${[]}
|
||||||
${'endDatesAreInclusive'} | ${false}
|
${'endDatesAreInclusive'} | ${false}
|
||||||
@ -216,4 +218,13 @@ describe('when using the ganttDb', function() {
|
|||||||
expect(tasks[1].task).toEqual('test2');
|
expect(tasks[1].task).toEqual('test2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
type | expected
|
||||||
|
${'hide'} | ${'off'}
|
||||||
|
${'style'} | ${'stoke:stroke-width:5px,stroke:#00f,opacity:0.5'}
|
||||||
|
`('should ${type} today marker', ({ expected }) => {
|
||||||
|
ganttDb.setTodayMarker(expected);
|
||||||
|
expect(ganttDb.getTodayMarker()).toEqual(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
|
|
||||||
import { parser } from './parser/gantt';
|
import { parser } from './parser/gantt';
|
||||||
|
import common from '../common/common';
|
||||||
import ganttDb from './ganttDb';
|
import ganttDb from './ganttDb';
|
||||||
|
|
||||||
parser.yy = ganttDb;
|
parser.yy = ganttDb;
|
||||||
@ -358,7 +359,7 @@ export const draw = function(text, id) {
|
|||||||
.data(numOccurances)
|
.data(numOccurances)
|
||||||
.enter()
|
.enter()
|
||||||
.append(function(d) {
|
.append(function(d) {
|
||||||
const rows = d[0].split(/<br\s*\/?>/gi);
|
const rows = d[0].split(common.lineBreakRegex);
|
||||||
const dy = -(rows.length - 1) / 2;
|
const dy = -(rows.length - 1) / 2;
|
||||||
|
|
||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
@ -396,17 +397,25 @@ export const draw = function(text, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawToday(theSidePad, theTopPad, w, h) {
|
function drawToday(theSidePad, theTopPad, w, h) {
|
||||||
|
const todayMarker = ganttDb.getTodayMarker();
|
||||||
|
if (todayMarker === 'off') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const todayG = svg.append('g').attr('class', 'today');
|
const todayG = svg.append('g').attr('class', 'today');
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
const todayLine = todayG.append('line');
|
||||||
|
|
||||||
todayG
|
todayLine
|
||||||
.append('line')
|
|
||||||
.attr('x1', timeScale(today) + theSidePad)
|
.attr('x1', timeScale(today) + theSidePad)
|
||||||
.attr('x2', timeScale(today) + theSidePad)
|
.attr('x2', timeScale(today) + theSidePad)
|
||||||
.attr('y1', conf.titleTopMargin)
|
.attr('y1', conf.titleTopMargin)
|
||||||
.attr('y2', h - conf.titleTopMargin)
|
.attr('y2', h - conf.titleTopMargin)
|
||||||
.attr('class', 'today');
|
.attr('class', 'today');
|
||||||
|
|
||||||
|
if (todayMarker !== '') {
|
||||||
|
todayLine.attr('style', todayMarker.replace(/,/g, ';'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
|
// from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
|
||||||
|
@ -58,6 +58,7 @@ that id.
|
|||||||
"inclusiveEndDates" return 'inclusiveEndDates';
|
"inclusiveEndDates" return 'inclusiveEndDates';
|
||||||
"axisFormat"\s[^#\n;]+ return 'axisFormat';
|
"axisFormat"\s[^#\n;]+ return 'axisFormat';
|
||||||
"excludes"\s[^#\n;]+ return 'excludes';
|
"excludes"\s[^#\n;]+ return 'excludes';
|
||||||
|
"todayMarker"\s[^\n;]+ return 'todayMarker';
|
||||||
\d\d\d\d"-"\d\d"-"\d\d return 'date';
|
\d\d\d\d"-"\d\d"-"\d\d return 'date';
|
||||||
"title"\s[^#\n;]+ return 'title';
|
"title"\s[^#\n;]+ return 'title';
|
||||||
"section"\s[^#:\n;]+ return 'section';
|
"section"\s[^#:\n;]+ return 'section';
|
||||||
@ -96,6 +97,7 @@ statement
|
|||||||
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
|
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
|
||||||
| axisFormat {yy.setAxisFormat($1.substr(11));$$=$1.substr(11);}
|
| axisFormat {yy.setAxisFormat($1.substr(11));$$=$1.substr(11);}
|
||||||
| excludes {yy.setExcludes($1.substr(9));$$=$1.substr(9);}
|
| excludes {yy.setExcludes($1.substr(9));$$=$1.substr(9);}
|
||||||
|
| todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);}
|
||||||
| title {yy.setTitle($1.substr(6));$$=$1.substr(6);}
|
| title {yy.setTitle($1.substr(6));$$=$1.substr(6);}
|
||||||
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
|
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
|
||||||
| clickStatement
|
| clickStatement
|
||||||
|
@ -37,6 +37,14 @@ describe('when parsing a gantt diagram it', function() {
|
|||||||
|
|
||||||
expect(parserFnConstructor(str)).not.toThrow();
|
expect(parserFnConstructor(str)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
it('should handle a todayMarker definition', function() {
|
||||||
|
spyOn(ganttDb, 'setTodayMarker');
|
||||||
|
const str =
|
||||||
|
'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid\nexcludes weekdays 2019-02-01\ntodayMarker off';
|
||||||
|
|
||||||
|
expect(parserFnConstructor(str)).not.toThrow();
|
||||||
|
expect(ganttDb.setTodayMarker).toHaveBeenCalledWith('off');
|
||||||
|
});
|
||||||
it('should handle a section definition', function() {
|
it('should handle a section definition', function() {
|
||||||
const str =
|
const str =
|
||||||
'gantt\n' +
|
'gantt\n' +
|
||||||
|
@ -3,6 +3,7 @@ import * as d3 from 'd3';
|
|||||||
import svgDraw from './svgDraw';
|
import svgDraw from './svgDraw';
|
||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
import { parser } from './parser/sequenceDiagram';
|
import { parser } from './parser/sequenceDiagram';
|
||||||
|
import common from '../common/common';
|
||||||
import sequenceDb from './sequenceDb';
|
import sequenceDb from './sequenceDb';
|
||||||
|
|
||||||
parser.yy = sequenceDb;
|
parser.yy = sequenceDb;
|
||||||
@ -170,7 +171,7 @@ export const bounds = {
|
|||||||
|
|
||||||
const _drawLongText = (text, x, y, g, width) => {
|
const _drawLongText = (text, x, y, g, width) => {
|
||||||
let textHeight = 0;
|
let textHeight = 0;
|
||||||
const lines = text.split(/<br\s*\/?>/gi);
|
const lines = text.split(common.lineBreakRegex);
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const textObj = svgDraw.getTextObj();
|
const textObj = svgDraw.getTextObj();
|
||||||
textObj.x = x;
|
textObj.x = x;
|
||||||
@ -235,7 +236,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
|||||||
let textElems = [];
|
let textElems = [];
|
||||||
let counterBreaklines = 0;
|
let counterBreaklines = 0;
|
||||||
let breaklineOffset = 17;
|
let breaklineOffset = 17;
|
||||||
const breaklines = msg.message.split(/<br\s*\/?>/gi);
|
const breaklines = msg.message.split(common.lineBreakRegex);
|
||||||
for (const breakline of breaklines) {
|
for (const breakline of breaklines) {
|
||||||
textElems.push(
|
textElems.push(
|
||||||
g
|
g
|
||||||
@ -362,7 +363,7 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
|||||||
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
line.attr('marker-start', 'url(' + url + '#sequencenumber)');
|
||||||
g.append('text')
|
g.append('text')
|
||||||
.attr('x', startx)
|
.attr('x', startx)
|
||||||
.attr('y', verticalPos + 4)
|
.attr('y', verticalPos + 4 + totalOffset)
|
||||||
.attr('font-family', 'sans-serif')
|
.attr('font-family', 'sans-serif')
|
||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import common from '../common/common';
|
||||||
|
|
||||||
export const drawRect = function(elem, rectData) {
|
export const drawRect = function(elem, rectData) {
|
||||||
const rectElem = elem.append('rect');
|
const rectElem = elem.append('rect');
|
||||||
rectElem.attr('x', rectData.x);
|
rectElem.attr('x', rectData.x);
|
||||||
@ -18,7 +20,7 @@ export const drawRect = function(elem, rectData) {
|
|||||||
|
|
||||||
export const drawText = function(elem, textData) {
|
export const drawText = function(elem, textData) {
|
||||||
// Remove and ignore br:s
|
// Remove and ignore br:s
|
||||||
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
|
const nText = textData.text.replace(common.lineBreakRegex, ' ');
|
||||||
|
|
||||||
const textElem = elem.append('text');
|
const textElem = elem.append('text');
|
||||||
textElem.attr('x', textData.x);
|
textElem.attr('x', textData.x);
|
||||||
@ -321,7 +323,7 @@ const _drawTextCandidateFunc = (function() {
|
|||||||
function byTspan(content, g, x, y, width, height, textAttrs, conf) {
|
function byTspan(content, g, x, y, width, height, textAttrs, conf) {
|
||||||
const { actorFontSize, actorFontFamily } = conf;
|
const { actorFontSize, actorFontFamily } = conf;
|
||||||
|
|
||||||
const lines = content.split(/<br\s*\/?>/gi);
|
const lines = content.split(common.lineBreakRegex);
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2;
|
const dy = i * actorFontSize - (actorFontSize * (lines.length - 1)) / 2;
|
||||||
const text = g
|
const text = g
|
||||||
|
@ -282,7 +282,7 @@ const drawForkJoinState = (g, stateDef) => {
|
|||||||
|
|
||||||
export const drawText = function(elem, textData) {
|
export const drawText = function(elem, textData) {
|
||||||
// Remove and ignore br:s
|
// Remove and ignore br:s
|
||||||
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
|
const nText = textData.text.replace(common.lineBreakRegex, ' ');
|
||||||
|
|
||||||
const textElem = elem.append('text');
|
const textElem = elem.append('text');
|
||||||
textElem.attr('x', textData.x);
|
textElem.attr('x', textData.x);
|
||||||
@ -310,7 +310,7 @@ const _drawLongText = (_text, x, y, g) => {
|
|||||||
|
|
||||||
let text = _text.replace(/\r\n/g, '<br/>');
|
let text = _text.replace(/\r\n/g, '<br/>');
|
||||||
text = text.replace(/\n/g, '<br/>');
|
text = text.replace(/\n/g, '<br/>');
|
||||||
const lines = text.split(/<br\s*\/?>/gi);
|
const lines = text.split(common.lineBreakRegex);
|
||||||
|
|
||||||
let tHeight = 1.25 * getConfig().state.noteMargin;
|
let tHeight = 1.25 * getConfig().state.noteMargin;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { logger } from '../../logger';
|
import { logger } from '../../logger';
|
||||||
|
import { generateId } from '../../utils';
|
||||||
|
|
||||||
|
const clone = o => JSON.parse(JSON.stringify(o));
|
||||||
|
|
||||||
let rootDoc = [];
|
let rootDoc = [];
|
||||||
const setRootDoc = o => {
|
const setRootDoc = o => {
|
||||||
@ -22,6 +25,34 @@ const docTranslator = (parent, node, first) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.doc) {
|
if (node.doc) {
|
||||||
|
const doc = [];
|
||||||
|
// Check for concurrency
|
||||||
|
let i = 0;
|
||||||
|
let currentDoc = [];
|
||||||
|
for (i = 0; i < node.doc.length; i++) {
|
||||||
|
if (node.doc[i].type === 'divider') {
|
||||||
|
// debugger;
|
||||||
|
const newNode = clone(node.doc[i]);
|
||||||
|
newNode.doc = clone(currentDoc);
|
||||||
|
doc.push(newNode);
|
||||||
|
currentDoc = [];
|
||||||
|
} else {
|
||||||
|
currentDoc.push(node.doc[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any divider was encountered
|
||||||
|
if (doc.length > 0 && currentDoc.length > 0) {
|
||||||
|
const newNode = {
|
||||||
|
stmt: 'state',
|
||||||
|
id: generateId(),
|
||||||
|
type: 'divider',
|
||||||
|
doc: clone(currentDoc)
|
||||||
|
};
|
||||||
|
doc.push(clone(newNode));
|
||||||
|
node.doc = doc;
|
||||||
|
}
|
||||||
|
|
||||||
node.doc.forEach(docNode => docTranslator(node, docNode, true));
|
node.doc.forEach(docNode => docTranslator(node, docNode, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,8 +62,14 @@ const getRootDocV2 = () => {
|
|||||||
return { id: 'root', doc: rootDoc };
|
return { id: 'root', doc: rootDoc };
|
||||||
};
|
};
|
||||||
|
|
||||||
const extract = doc => {
|
const extract = _doc => {
|
||||||
// const res = { states: [], relations: [] };
|
// const res = { states: [], relations: [] };
|
||||||
|
let doc;
|
||||||
|
if (_doc.doc) {
|
||||||
|
doc = _doc.doc;
|
||||||
|
} else {
|
||||||
|
doc = _doc;
|
||||||
|
}
|
||||||
// let doc = root.doc;
|
// let doc = root.doc;
|
||||||
// if (!doc) {
|
// if (!doc) {
|
||||||
// doc = root;
|
// doc = root;
|
||||||
@ -40,6 +77,8 @@ const extract = doc => {
|
|||||||
logger.info(doc);
|
logger.info(doc);
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
|
logger.info('Extract', doc);
|
||||||
|
|
||||||
doc.forEach(item => {
|
doc.forEach(item => {
|
||||||
if (item.stmt === 'state') {
|
if (item.stmt === 'state') {
|
||||||
addState(item.id, item.type, item.doc, item.description, item.note);
|
addState(item.id, item.type, item.doc, item.description, item.note);
|
||||||
|
@ -72,10 +72,8 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nodeData = {
|
const nodeData = {
|
||||||
labelType: 'svg',
|
|
||||||
labelStyle: '',
|
labelStyle: '',
|
||||||
shape: nodeDb[node.id].shape,
|
shape: nodeDb[node.id].shape,
|
||||||
label: node.id,
|
|
||||||
labelText: nodeDb[node.id].description,
|
labelText: nodeDb[node.id].description,
|
||||||
classes: nodeDb[node.id].classes, //classStr,
|
classes: nodeDb[node.id].classes, //classStr,
|
||||||
style: '', //styles.style,
|
style: '', //styles.style,
|
||||||
@ -87,10 +85,8 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||||||
if (node.note) {
|
if (node.note) {
|
||||||
// Todo: set random id
|
// Todo: set random id
|
||||||
const noteData = {
|
const noteData = {
|
||||||
labelType: 'svg',
|
|
||||||
labelStyle: '',
|
labelStyle: '',
|
||||||
shape: 'note',
|
shape: 'note',
|
||||||
label: node.id,
|
|
||||||
labelText: node.note.text,
|
labelText: node.note.text,
|
||||||
classes: 'statediagram-note', //classStr,
|
classes: 'statediagram-note', //classStr,
|
||||||
style: '', //styles.style,
|
style: '', //styles.style,
|
||||||
@ -99,10 +95,8 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||||||
padding: 15 //getConfig().flowchart.padding
|
padding: 15 //getConfig().flowchart.padding
|
||||||
};
|
};
|
||||||
const groupData = {
|
const groupData = {
|
||||||
labelType: 'svg',
|
|
||||||
labelStyle: '',
|
labelStyle: '',
|
||||||
shape: 'noteGroup',
|
shape: 'noteGroup',
|
||||||
label: node.id + '----parent',
|
|
||||||
labelText: node.note.text,
|
labelText: node.note.text,
|
||||||
classes: nodeDb[node.id].classes, //classStr,
|
classes: nodeDb[node.id].classes, //classStr,
|
||||||
style: '', //styles.style,
|
style: '', //styles.style,
|
||||||
@ -133,8 +127,7 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||||||
classes: 'note-edge',
|
classes: 'note-edge',
|
||||||
arrowheadStyle: 'fill: #333',
|
arrowheadStyle: 'fill: #333',
|
||||||
labelpos: 'c',
|
labelpos: 'c',
|
||||||
labelType: 'text',
|
labelType: 'text'
|
||||||
label: ''
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
g.setNode(node.id, nodeData);
|
g.setNode(node.id, nodeData);
|
||||||
@ -143,12 +136,12 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (parent.id !== 'root') {
|
if (parent.id !== 'root') {
|
||||||
logger.trace('Setting node ', node.id, ' to be child of its parent ', parent.id);
|
logger.info('Setting node ', node.id, ' to be child of its parent ', parent.id);
|
||||||
g.setParent(node.id, parent.id);
|
g.setParent(node.id, parent.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (node.doc) {
|
if (node.doc) {
|
||||||
logger.trace('Adding nodes children ');
|
logger.info('Adding nodes children ');
|
||||||
setupDoc(g, node, node.doc, !altFlag);
|
setupDoc(g, node, node.doc, !altFlag);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -168,8 +161,7 @@ const setupDoc = (g, parent, doc, altFlag) => {
|
|||||||
labelStyle: '',
|
labelStyle: '',
|
||||||
arrowheadStyle: 'fill: #333',
|
arrowheadStyle: 'fill: #333',
|
||||||
labelpos: 'c',
|
labelpos: 'c',
|
||||||
labelType: 'text',
|
labelType: 'text'
|
||||||
label: ''
|
|
||||||
};
|
};
|
||||||
let startId = item.state1.id;
|
let startId = item.state1.id;
|
||||||
let endId = item.state2.id;
|
let endId = item.state2.id;
|
||||||
@ -214,7 +206,7 @@ export const draw = function(text, id) {
|
|||||||
compound: true
|
compound: true
|
||||||
})
|
})
|
||||||
.setGraph({
|
.setGraph({
|
||||||
rankdir: 'LR',
|
rankdir: 'TB',
|
||||||
nodesep: nodeSpacing,
|
nodesep: nodeSpacing,
|
||||||
ranksep: rankSpacing,
|
ranksep: rankSpacing,
|
||||||
marginx: 8,
|
marginx: 8,
|
||||||
@ -224,8 +216,8 @@ export const draw = function(text, id) {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
// logger.info(stateDb.getRootDoc());
|
logger.info(stateDb.getRootDocV2());
|
||||||
stateDb.extract(stateDb.getRootDocV2().doc);
|
stateDb.extract(stateDb.getRootDocV2());
|
||||||
logger.info(stateDb.getRootDocV2());
|
logger.info(stateDb.getRootDocV2());
|
||||||
setupNode(g, undefined, stateDb.getRootDocV2(), true);
|
setupNode(g, undefined, stateDb.getRootDocV2(), true);
|
||||||
|
|
||||||
|
@ -75,8 +75,13 @@ export const draw = function(text, id) {
|
|||||||
const width = bounds.width + padding * 2;
|
const width = bounds.width + padding * 2;
|
||||||
const height = bounds.height + padding * 2;
|
const height = bounds.height + padding * 2;
|
||||||
|
|
||||||
|
if (conf.useMaxWidth) {
|
||||||
|
diagram.attr('width', '100%');
|
||||||
|
diagram.attr('style', `max-width: ${width * 1.75}px;`);
|
||||||
|
} else {
|
||||||
// Zoom in a bit
|
// Zoom in a bit
|
||||||
diagram.attr('width', width * 1.75);
|
diagram.attr('width', width * 1.75);
|
||||||
|
}
|
||||||
// diagram.attr('height', bounds.height * 3 + conf.padding * 2);
|
// diagram.attr('height', bounds.height * 3 + conf.padding * 2);
|
||||||
diagram.attr(
|
diagram.attr(
|
||||||
'viewBox',
|
'viewBox',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import moment from 'moment-mini';
|
import moment from 'moment-mini';
|
||||||
|
//
|
||||||
export const LEVELS = {
|
export const LEVELS = {
|
||||||
debug: 1,
|
debug: 1,
|
||||||
info: 2,
|
info: 2,
|
||||||
|
16
src/utils.js
16
src/utils.js
@ -210,6 +210,19 @@ export const getStylesFromArray = arr => {
|
|||||||
return { style: style, labelStyle: labelStyle };
|
return { style: style, labelStyle: labelStyle };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cnt = 0;
|
||||||
|
export const generateId = () => {
|
||||||
|
cnt++;
|
||||||
|
return (
|
||||||
|
'id-' +
|
||||||
|
Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 12) +
|
||||||
|
'-' +
|
||||||
|
cnt
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
detectType,
|
detectType,
|
||||||
isSubstringInArray,
|
isSubstringInArray,
|
||||||
@ -217,5 +230,6 @@ export default {
|
|||||||
calcLabelPosition,
|
calcLabelPosition,
|
||||||
calcCardinalityPosition,
|
calcCardinalityPosition,
|
||||||
formatUrl,
|
formatUrl,
|
||||||
getStylesFromArray
|
getStylesFromArray,
|
||||||
|
generateId
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user