mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
commit
366f9db8a8
3
.gitignore
vendored
3
.gitignore
vendored
@ -18,4 +18,5 @@ dist/classTest.html
|
||||
dist/sequenceTest.html
|
||||
|
||||
.vscode/
|
||||
cypress/platform/current.html
|
||||
cypress/platform/current.html
|
||||
cypress/platform/experimental.html
|
@ -20,7 +20,7 @@ For more information and help in getting started, please view our [documentation
|
||||
|
||||
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.
|
||||
|
||||
![Image show the two new diagram types](.docs/img/new-diagrams.png)
|
||||
![Image show the two new diagram types](./docs/img/new-diagrams.png)
|
||||
|
||||
## Special note regarding version 8.2
|
||||
|
||||
|
91
cypress/integration/rendering/erDiagram.spec.js
Normal file
91
cypress/integration/rendering/erDiagram.spec.js
Normal file
@ -0,0 +1,91 @@
|
||||
/* eslint-env jest */
|
||||
import { imgSnapshotTest } from '../../helpers/util';
|
||||
|
||||
describe('Entity Relationship Diagram', () => {
|
||||
it('should render a simple ER diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render an ER diagram with a recursive relationship', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||..o{ CUSTOMER : refers
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render an ER diagram with multiple relationships between the same two entities', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--|{ ADDRESS : "invoiced at"
|
||||
CUSTOMER ||--|{ ADDRESS : "receives goods at"
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render a cyclical ER diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
A ||--|{ B : likes
|
||||
B ||--|{ C : likes
|
||||
C ||--|{ A : likes
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render a not-so-simple ER diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
CUSTOMER ||--o{ INVOICE : "liable for"
|
||||
DELIVERY-ADDRESS ||--o{ ORDER : receives
|
||||
INVOICE ||--|{ ORDER : covers
|
||||
ORDER ||--|{ ORDER-ITEM : includes
|
||||
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
|
||||
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render multiple ER diagrams', () => {
|
||||
imgSnapshotTest(
|
||||
[
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`
|
||||
],
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
});
|
@ -8,29 +8,169 @@
|
||||
<style>
|
||||
body {
|
||||
background: white;
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
font-family: 'Arial';
|
||||
}
|
||||
h1 { color: white;}
|
||||
.arrowheadPath {fill: red;}
|
||||
|
||||
.edgePath .path {stroke: red;}
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>info below</h1>
|
||||
<div style="display: flex;width: 100%; height: 100%">
|
||||
<div class="mermaid" style="width: 100%; height: 100%">
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
<div class="mermaid2" style="width: 100%; height: 20%;">
|
||||
flowchart LR
|
||||
a --> b
|
||||
|
||||
subgraph id1 [Test]
|
||||
a --apa--> c
|
||||
b
|
||||
c-->b
|
||||
b-->H
|
||||
end
|
||||
G-->H
|
||||
G-->c
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 50%; height: 20%;">
|
||||
flowchart LR
|
||||
subgraph id1 [Test]
|
||||
b
|
||||
end
|
||||
a-->id1
|
||||
</div>
|
||||
<div class="mermaid mermaid-apa" style="width: 100%; height: 20%;">
|
||||
stateDiagram
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
</div>
|
||||
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
State1: The state with a note
|
||||
note right of State1
|
||||
Important information! You can write
|
||||
notes.
|
||||
end note
|
||||
State1 --> State2
|
||||
note left of State2 : This is the note to the left.
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
[*]-->TV
|
||||
|
||||
state TV {
|
||||
[*] --> Off: Off to start with
|
||||
On --> Off : Turn off
|
||||
Off --> On : Turn on
|
||||
}
|
||||
|
||||
TV--> Console
|
||||
|
||||
state Console {
|
||||
[*] --> Off2: Off to start with
|
||||
On2--> Off2 : Turn off
|
||||
Off2 --> On2 : Turn on
|
||||
On2-->Playing
|
||||
|
||||
state Playing {
|
||||
Alive --> Dead
|
||||
Dead-->Alive
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: flex;flex-direction:column;width: 100%; height: 100%">
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
state apa {
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
flowchart TB
|
||||
a --> b
|
||||
|
||||
subgraph id1 [Test]
|
||||
a --apa--> c
|
||||
b
|
||||
c-->b
|
||||
b-->H
|
||||
end
|
||||
G-->H
|
||||
G-->id1
|
||||
id1 --> I
|
||||
I --> G
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
flowchart RL
|
||||
a --> b
|
||||
|
||||
subgraph id1 [Test]
|
||||
a --apa--> c
|
||||
b
|
||||
c-->b
|
||||
b-->H
|
||||
end
|
||||
G-->H
|
||||
G-->id1
|
||||
id1 --> I
|
||||
I --> G
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
flowchart RL
|
||||
|
||||
subgraph id1 [Test]
|
||||
a
|
||||
end
|
||||
b-->id1
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
flowchart RL
|
||||
|
||||
subgraph id1 [Test1]
|
||||
a
|
||||
end
|
||||
subgraph id2 [Test2]
|
||||
b
|
||||
end
|
||||
a --> id2
|
||||
a --> b
|
||||
b-->id1
|
||||
id1 --> id2
|
||||
</div>
|
||||
new:
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
flowchart LR
|
||||
a <--> b
|
||||
b o--o c
|
||||
c x--x d
|
||||
a21([In the box]) --> b2
|
||||
b2((b2)) --o c2
|
||||
c2(c2) --x d2 --> id1{{This is the text in the box}} --> A[(cylindrical<br />shape<br />test)]
|
||||
</div>
|
||||
old:
|
||||
<div class="mermaid2" style="width: 100%; height: 100%">
|
||||
graph LR
|
||||
a((a)) --> b --> id1{{This is the text in the box}}
|
||||
A[(cylindrical<br />shape<br />test)]
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
@ -43,7 +183,8 @@
|
||||
// gantt: { axisFormat: '%m/%d/%Y' },
|
||||
sequence: { actorMargin: 50, showSequenceNumbers: true },
|
||||
// sequenceDiagram: { actorMargin: 300 } // deprecated
|
||||
fontFamily: '"Noto Sans SC", sans-serif'
|
||||
fontFamily: '"arial", sans-serif',
|
||||
curve: 'linear',
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
80
docs/entityRelationshipDiagram.md
Normal file
80
docs/entityRelationshipDiagram.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Entity Relationship Diagrams
|
||||
|
||||
> 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.
|
||||
|
||||
Mermaid can render ER diagrams
|
||||
```
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
||||
```
|
||||
```mermaid
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
||||
```
|
||||
|
||||
Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid.
|
||||
|
||||
Relationships between entities are represented by lines with end markers representing cardinality. Mermaid uses the most popular crow's foot notation. The crow's foot intuitively conveys the possibility of many instances of the entity that it connects to.
|
||||
|
||||
## 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.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Entities and Relationships
|
||||
|
||||
Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to label the relationship. Each statement consists of the following parts, all of which are mandatory:
|
||||
```
|
||||
<first-entity> <relationship> <second-entity> : <relationship-label>
|
||||
```
|
||||
Where:
|
||||
|
||||
- `first-entity` is the name of an entity. Names must begin with an alphabetic character and may also contain digits and hyphens
|
||||
- `relationship` describes the way that both entities inter-relate. See below.
|
||||
- `second-entity` is the name of the other entity
|
||||
- `relationship-label` describes the relationship from the perspective of the first entity.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
PROPERTY ||--|{ ROOM : contains
|
||||
```
|
||||
|
||||
This statement can be read as *a property contains one or more rooms, and a room is part of one and only one property*. You can see that the label here is from the first entity's perspective: a property contains a room, but a room does not contain a property. When considered from the perspective of the second entity, the equivalent label is usually very easy to infer. (Some ER diagrams label relationships from both perspectives, but this is not supported here, and is usually superfluous).
|
||||
|
||||
### Relationship Syntax
|
||||
|
||||
The `relationship` part of each statement can be broken down into three sub-components:
|
||||
|
||||
- the cardinality of the first entity with respect to the second,
|
||||
- whether the relationship confers identity on a 'child' entity
|
||||
- the cardinality of the second entity with respect to the first
|
||||
|
||||
Cardinality is a property that describes how many elements of another entity can be related to the entity in question. In the above example a `PROPERTY` can have one or more `ROOM` instances associated to it, whereas a `ROOM` can only be associated with one `PROPERTY`. In each cardinality marker there are two characters. The outermost character represents a maximum value, and the innermost character represents a minimum value. The table below summarises possible cardinalities.
|
||||
|
||||
| Value (left) | Value (right) | Meaning |
|
||||
|:------------:|:-------------:|--------------------------------------------------------|
|
||||
| `|o` | `o|` | Zero or one |
|
||||
| `||` | `||` | Exactly one |
|
||||
| `}o` | `o{` | Zero or more (no upper limit) |
|
||||
| `}|` | `|{` | One or more (no upper limit) |
|
||||
|
||||
### Identification
|
||||
|
||||
Relationships may be classified as either *identifying* or *non-identifying* and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
|
||||
|
||||
```
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
```
|
||||
### Other Things
|
||||
|
||||
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
|
||||
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
|
@ -409,6 +409,22 @@ graph TB
|
||||
end
|
||||
```
|
||||
|
||||
You can also set an excplicit id for the subgraph.
|
||||
|
||||
```
|
||||
graph TB
|
||||
c1-->a2
|
||||
subgraph ide1 [one]
|
||||
a1-->a2
|
||||
end
|
||||
```
|
||||
```mermaid
|
||||
graph TB
|
||||
c1-->a2
|
||||
subgraph id1 [one]
|
||||
a1-->a2
|
||||
end
|
||||
```
|
||||
|
||||
## Interaction
|
||||
|
||||
@ -425,10 +441,10 @@ Examples of tooltip usage below:
|
||||
|
||||
```
|
||||
<script>
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
<script>
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```
|
||||
@ -448,28 +464,30 @@ graph LR;
|
||||
```
|
||||
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
|
||||
|
||||
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/s37cjoau/3/).
|
||||
|
||||
Beginners tip, a full example using interactive links in a html context:
|
||||
```
|
||||
<body>
|
||||
<div class="mermaid">
|
||||
graph LR;
|
||||
A-->B;
|
||||
click A callback "Tooltip"
|
||||
click B "http://www.github.com" "This is a link"
|
||||
A-->B;
|
||||
click A callback "Tooltip"
|
||||
click B "http://www.github.com" "This is a link"
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var callback = function(){
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
var config = {
|
||||
startOnLoad:true,
|
||||
flowchart:{
|
||||
useMaxWidth:true,
|
||||
htmlLabels:true,
|
||||
curve:'cardinal',
|
||||
},
|
||||
securityLevel:'loose',
|
||||
startOnLoad:true,
|
||||
flowchart:{
|
||||
useMaxWidth:true,
|
||||
htmlLabels:true,
|
||||
curve:'cardinal',
|
||||
},
|
||||
securityLevel:'loose',
|
||||
};
|
||||
|
||||
mermaid.initialize(config);
|
||||
|
@ -1,8 +1,19 @@
|
||||
# Gantt diagrams
|
||||
|
||||
> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule. Gantt charts illustrate the start and finish dates of the terminal elements and summary elements of a project.
|
||||
|
||||
Mermaid can render Gantt diagrams.
|
||||
> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule and the amount of time it would take for any one project to finish. Gantt charts illustrate number of days between the start and finish dates of the terminal elements and summary elements of a project.
|
||||
|
||||
## A note to users
|
||||
Gannt Charts will record each scheduled task as one continuous bar that extends from the left to the right. The x axis represents time and the y records the different tasks and the order in which they are to be completed.
|
||||
|
||||
It is important to remember that when a date, day or collection of dates specific to a task are "excluded", the Gannt Chart will accomodate those changes by extending an equal number of day, towards the right, not by creating a gap inside the task.
|
||||
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-excluded-days-within.png)
|
||||
|
||||
However, if the excluded date/s is between two tasks that are set to start consecutively, the excluded dates will be skipped graphically and left blank, and the following task will begin after the end of the excluded date/s.
|
||||
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-long-weekend-look.png)
|
||||
|
||||
A Gantt chart is useful for tracking the amount of time it would take before a project is finished, but it can also be used to graphically represent "non-working days, with a few tweaks.
|
||||
|
||||
Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pasted into docs.
|
||||
|
||||
```
|
||||
gantt
|
||||
@ -30,9 +41,12 @@ gantt
|
||||
|
||||
```
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Adding GANTT diagram functionality to mermaid
|
||||
|
||||
|
||||
dateFormat :YYYY-MM-DD
|
||||
title :Adding GANTT diagram functionality to mermaid
|
||||
excludes :excludes the named dates/days from being included in a charted task..
|
||||
(Accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
section A section
|
||||
Completed task :done, des1, 2014-01-06,2014-01-08
|
||||
Active task :active, des2, 2014-01-09, 3d
|
||||
@ -118,7 +132,7 @@ Tbd
|
||||
|
||||
### Date format
|
||||
|
||||
The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example:
|
||||
The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example: 2020-3-7
|
||||
|
||||
```
|
||||
dateFormat YYYY MM DD
|
||||
|
BIN
docs/img/Gantt-excluded-days-within.png
Normal file
BIN
docs/img/Gantt-excluded-days-within.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
BIN
docs/img/Gantt-long-weekend-look.png
Normal file
BIN
docs/img/Gantt-long-weekend-look.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
@ -174,6 +174,14 @@ margin around notes.
|
||||
Space between messages.
|
||||
**Default value 35**.
|
||||
|
||||
### messageAlign
|
||||
|
||||
Multiline message alignment. Possible values are:
|
||||
|
||||
- left
|
||||
- center **default**
|
||||
- right
|
||||
|
||||
### mirrorActors
|
||||
|
||||
mirror actors under diagram.
|
||||
|
74
src/dagre-wrapper/GraphObjects.md
Normal file
74
src/dagre-wrapper/GraphObjects.md
Normal file
@ -0,0 +1,74 @@
|
||||
# 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.
|
||||
|
||||
## node
|
||||
|
||||
Sample object:
|
||||
```json
|
||||
{
|
||||
"labelType":"svg",
|
||||
"labelStyle":"",
|
||||
"shape":"rect",
|
||||
"label":{},
|
||||
"labelText":"Test",
|
||||
"rx":0,"ry":0,
|
||||
"class":"default",
|
||||
"style":"",
|
||||
"id":"Test",
|
||||
"type":"group",
|
||||
"padding":15}
|
||||
```
|
||||
|
||||
This is set by the renderer of the diagram and insert the data that the wrapper neds for rendering.
|
||||
|
||||
| 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. Not currently used. |
|
||||
| shape | The shape of the node. Currently on rect is suppoerted. This will change. |
|
||||
| label | ?? |
|
||||
| labelText | The text on the label |
|
||||
| rx | The corner radius - maybe part of the shape instead? |
|
||||
| ry | The corner radius - maybe part of the shape instead? |
|
||||
| class | Class to be set for the shape |
|
||||
| style | Css styles for the actual shape |
|
||||
| id | id of the shape |
|
||||
| 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. |
|
||||
|
||||
|
||||
# edge
|
||||
|
||||
arrowType sets the type of arrows to use. The following arrow types are currently supported:
|
||||
|
||||
arrow_cross
|
||||
double_arrow_cross
|
||||
arrow_point
|
||||
double_arrow_point
|
||||
arrow_circle
|
||||
double_arrow_circle
|
||||
|
||||
Lets try to make these types semantic free so that diagram type semantics does not find its way in to this more generic layer.
|
||||
|
||||
|
||||
# Markers
|
||||
|
||||
Define what markers that should be included in the diagram with the insert markers function. The function takes two arguments, first the element in which the markers should be included and a list of the markers that should be added.
|
||||
|
||||
Ex:
|
||||
insertMarkers(el, ['point', 'circle'])
|
||||
|
||||
The example above adds the markers point and cross. This means that edges with the arrowTypes arrow_cross, double_arrow_cross, arrow_point and double_arrow_cross will get the corresponding markers but arrowType arrow_cross will have no impact.
|
||||
|
||||
Current markers:
|
||||
* point - the standard arrow from flowcharts
|
||||
* circle - Arrows ending with circle
|
||||
* cross - arrows starting and ending with a cross
|
||||
|
||||
|
||||
// Todo - in case of common renderer
|
||||
# Common functions used by the renderer to be implemented by the Db
|
||||
|
||||
getDirection
|
||||
getClasses
|
175
src/dagre-wrapper/clusters.js
Normal file
175
src/dagre-wrapper/clusters.js
Normal file
@ -0,0 +1,175 @@
|
||||
import intersectRect from './intersect/intersect-rect';
|
||||
import { logger } from '../logger'; // eslint-disable-line
|
||||
import createLabel from './createLabel';
|
||||
|
||||
const rect = (parent, node) => {
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'cluster')
|
||||
.attr('id', node.id);
|
||||
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
|
||||
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle));
|
||||
|
||||
// Get the size of the label
|
||||
const bbox = text.getBBox();
|
||||
|
||||
const padding = 0 * node.padding;
|
||||
const halfPadding = padding / 2;
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||
.attr('y', node.y - node.height / 2 - halfPadding)
|
||||
.attr('width', node.width + padding)
|
||||
.attr('height', node.height + padding);
|
||||
|
||||
// logger.info('bbox', bbox.width, node.x, node.width);
|
||||
// Center the label
|
||||
// label.attr('transform', 'translate(' + adj + ', ' + (node.y - node.height / 2) + ')');
|
||||
label.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(node.x - bbox.width / 2) +
|
||||
', ' +
|
||||
(node.y - node.height / 2 - node.padding / 3 + 3) +
|
||||
')'
|
||||
);
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
/**
|
||||
* Non visiable cluster where the note is group with its
|
||||
*/
|
||||
const noteGroup = (parent, node) => {
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'note-cluster')
|
||||
.attr('id', node.id);
|
||||
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
const padding = 0 * node.padding;
|
||||
const halfPadding = padding / 2;
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||
.attr('y', node.y - node.height / 2 - halfPadding)
|
||||
.attr('width', node.width + padding)
|
||||
.attr('height', node.height + padding)
|
||||
.attr('fill', 'none');
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const roundedWithTitle = (parent, node) => {
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', node.classes)
|
||||
.attr('id', node.id);
|
||||
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||
const innerRect = shapeSvg.append('rect');
|
||||
|
||||
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle));
|
||||
|
||||
// Get the size of the label
|
||||
const bbox = text.getBBox();
|
||||
|
||||
const padding = 0 * node.padding;
|
||||
const halfPadding = padding / 2;
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('class', 'outer')
|
||||
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||
.attr('y', node.y - node.height / 2 - halfPadding)
|
||||
.attr('width', node.width + padding)
|
||||
.attr('height', node.height + padding);
|
||||
innerRect
|
||||
.attr('class', 'inner')
|
||||
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||
.attr('y', node.y - node.height / 2 - halfPadding + bbox.height - 1)
|
||||
.attr('width', node.width + padding)
|
||||
.attr('height', node.height + padding - bbox.height - 3);
|
||||
|
||||
// logger.info('bbox', bbox.width, node.x, node.width);
|
||||
// Center the label
|
||||
// label.attr('transform', 'translate(' + adj + ', ' + (node.y - node.height / 2) + ')');
|
||||
label.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(node.x - bbox.width / 2) +
|
||||
', ' +
|
||||
(node.y - node.height / 2 - node.padding / 3 + 3) +
|
||||
')'
|
||||
);
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const shapes = { rect, roundedWithTitle, noteGroup };
|
||||
|
||||
let clusterElems = {};
|
||||
|
||||
export const insertCluster = (elem, node) => {
|
||||
clusterElems[node.id] = shapes[node.shape](elem, node);
|
||||
};
|
||||
export const getClusterTitleWidth = (elem, node) => {
|
||||
const label = createLabel(node.labelText, node.labelStyle);
|
||||
elem.node().appendChild(label);
|
||||
const width = label.getBBox().width;
|
||||
elem.node().removeChild(label);
|
||||
return width;
|
||||
};
|
||||
|
||||
export const clear = () => {
|
||||
clusterElems = {};
|
||||
};
|
||||
|
||||
export const positionCluster = node => {
|
||||
const el = clusterElems[node.id];
|
||||
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||
};
|
18
src/dagre-wrapper/createLabel.js
Normal file
18
src/dagre-wrapper/createLabel.js
Normal file
@ -0,0 +1,18 @@
|
||||
const createLabel = (vertexText, style) => {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(/\n|<br\s*\/?>/gi);
|
||||
|
||||
for (let j = 0; j < rows.length; j++) {
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '0');
|
||||
tspan.textContent = rows[j].trim();
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
return svgLabel;
|
||||
};
|
||||
|
||||
export default createLabel;
|
251
src/dagre-wrapper/edges.js
Normal file
251
src/dagre-wrapper/edges.js
Normal file
@ -0,0 +1,251 @@
|
||||
import { logger } from '../logger'; // eslint-disable-line
|
||||
import createLabel from './createLabel';
|
||||
import * as d3 from 'd3';
|
||||
import { getConfig } from '../config';
|
||||
|
||||
let edgeLabels = {};
|
||||
|
||||
export const clear = () => {
|
||||
edgeLabels = {};
|
||||
};
|
||||
|
||||
export const insertEdgeLabel = (elem, edge) => {
|
||||
// Create the actual text element
|
||||
const labelElement = createLabel(edge.label, edge.labelStyle);
|
||||
|
||||
// Create outer g, edgeLabel, this will be positioned after graph layout
|
||||
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||
|
||||
// Create inner g, label, this will be positioned now for centering the text
|
||||
const label = edgeLabel.insert('g').attr('class', 'label');
|
||||
label.node().appendChild(labelElement);
|
||||
|
||||
// Center the label
|
||||
const bbox = labelElement.getBBox();
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
|
||||
// Make element accessible by id for positioning
|
||||
edgeLabels[edge.id] = edgeLabel;
|
||||
|
||||
// Update the abstract data of the edge with the new information about its width and height
|
||||
edge.width = bbox.width;
|
||||
edge.height = bbox.height;
|
||||
};
|
||||
|
||||
export const positionEdgeLabel = edge => {
|
||||
const el = edgeLabels[edge.id];
|
||||
el.attr('transform', 'translate(' + edge.x + ', ' + edge.y + ')');
|
||||
};
|
||||
|
||||
// const getRelationType = function(type) {
|
||||
// switch (type) {
|
||||
// case stateDb.relationType.AGGREGATION:
|
||||
// return 'aggregation';
|
||||
// case stateDb.relationType.EXTENSION:
|
||||
// return 'extension';
|
||||
// case stateDb.relationType.COMPOSITION:
|
||||
// return 'composition';
|
||||
// case stateDb.relationType.DEPENDENCY:
|
||||
// return 'dependency';
|
||||
// }
|
||||
// };
|
||||
|
||||
const outsideNode = (node, point) => {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
const dx = Math.abs(point.x - x);
|
||||
const dy = Math.abs(point.y - y);
|
||||
const w = node.width / 2;
|
||||
const h = node.height / 2;
|
||||
if (dx > w || dy > h) {
|
||||
return true;
|
||||
}
|
||||
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) => {
|
||||
// logger.info('intersection', outsidePoint, insidePoint, node);
|
||||
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;
|
||||
let q = h - dy;
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h || false) { // eslint-disable-line
|
||||
// Intersection is top or bottom of rect.
|
||||
|
||||
r = (R * q) / Q;
|
||||
|
||||
return {
|
||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
|
||||
y: insidePoint.y + q
|
||||
};
|
||||
} else {
|
||||
q = (Q * r) / R;
|
||||
|
||||
return {
|
||||
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
|
||||
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const insertEdge = function(elem, edge, clusterDb, diagramType) {
|
||||
logger.info('\n\n\n\n');
|
||||
let points = edge.points;
|
||||
if (edge.toCluster) {
|
||||
// logger.info('edge', edge);
|
||||
// logger.info('to cluster', clusterDb[edge.toCluster]);
|
||||
points = [];
|
||||
let lastPointOutside;
|
||||
let isInside = false;
|
||||
edge.points.forEach(point => {
|
||||
const node = clusterDb[edge.toCluster].node;
|
||||
|
||||
if (!outsideNode(node, point) && !isInside) {
|
||||
// logger.info('inside', edge.toCluster, point);
|
||||
|
||||
// First point inside the rect
|
||||
const insterection = intersection(node, lastPointOutside, point);
|
||||
// logger.info('intersect', inter.rect(node, lastPointOutside));
|
||||
points.push(insterection);
|
||||
// points.push(insterection);
|
||||
isInside = true;
|
||||
} else {
|
||||
if (!isInside) points.push(point);
|
||||
}
|
||||
lastPointOutside = point;
|
||||
});
|
||||
}
|
||||
|
||||
if (edge.fromCluster) {
|
||||
// logger.info('edge', edge);
|
||||
// logger.info('from cluster', clusterDb[edge.toCluster]);
|
||||
const updatedPoints = [];
|
||||
let lastPointOutside;
|
||||
let isInside = false;
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
const point = points[i];
|
||||
const node = clusterDb[edge.fromCluster].node;
|
||||
|
||||
if (!outsideNode(node, point) && !isInside) {
|
||||
// logger.info('inside', edge.toCluster, point);
|
||||
|
||||
// First point inside the rect
|
||||
const insterection = intersection(node, lastPointOutside, point);
|
||||
// logger.info('intersect', intersection(node, lastPointOutside, point));
|
||||
updatedPoints.unshift(insterection);
|
||||
// points.push(insterection);
|
||||
isInside = true;
|
||||
} else {
|
||||
// at the outside
|
||||
// logger.info('Outside point', point);
|
||||
if (!isInside) updatedPoints.unshift(point);
|
||||
}
|
||||
lastPointOutside = point;
|
||||
}
|
||||
points = updatedPoints;
|
||||
}
|
||||
|
||||
// logger.info('Poibts', points);
|
||||
|
||||
// logger.info('Edge', edge);
|
||||
|
||||
// The data for our line
|
||||
const lineData = points.filter(p => !Number.isNaN(p.y));
|
||||
|
||||
// This is the accessor function we talked about above
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.x(function(d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function(d) {
|
||||
return d.y;
|
||||
})
|
||||
.curve(d3.curveBasis);
|
||||
|
||||
const svgPath = elem
|
||||
.append('path')
|
||||
.attr('d', lineFunction(lineData))
|
||||
.attr('id', edge.id)
|
||||
.attr('class', 'transition' + (edge.classes ? ' ' + edge.classes : ''));
|
||||
|
||||
// DEBUG code, adds a red circle at each edge coordinate
|
||||
// edge.points.forEach(point => {
|
||||
// elem
|
||||
// .append('circle')
|
||||
// .style('stroke', 'red')
|
||||
// .style('fill', 'red')
|
||||
// .attr('r', 1)
|
||||
// .attr('cx', point.x)
|
||||
// .attr('cy', point.y);
|
||||
// });
|
||||
|
||||
let url = '';
|
||||
if (getConfig().state.arrowMarkerAbsolute) {
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(');
|
||||
url = url.replace(/\)/g, '\\)');
|
||||
}
|
||||
// logger.info('arrowType', edge.arrowType);
|
||||
switch (edge.arrowType) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||
break;
|
||||
case 'double_arrow_cross':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
|
||||
break;
|
||||
case 'double_arrow_point':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
|
||||
break;
|
||||
case 'double_arrow_barb':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barnEnd' + ')');
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
|
||||
break;
|
||||
case 'double_arrow_circle':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
104
src/dagre-wrapper/index.js
Normal file
104
src/dagre-wrapper/index.js
Normal file
@ -0,0 +1,104 @@
|
||||
import dagre from 'dagre';
|
||||
import insertMarkers from './markers';
|
||||
import { insertNode, positionNode, clear as clearNodes } from './nodes';
|
||||
import { insertCluster, clear as clearClusters } from './clusters';
|
||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
|
||||
import { logger } 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 clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line
|
||||
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
|
||||
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
|
||||
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
|
||||
// to the abstract node and is later used by dagre for the layout
|
||||
graph.nodes().forEach(function(v) {
|
||||
const node = graph.node(v);
|
||||
logger.warn('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||
if (node.type !== 'group') {
|
||||
insertNode(nodes, graph.node(v));
|
||||
} else {
|
||||
// const width = getClusterTitleWidth(clusters, node);
|
||||
const children = graph.children(v);
|
||||
logger.info('Cluster identified', node.id, children[0]);
|
||||
// nodes2expand.push({ id: children[0], width });
|
||||
clusterDb[node.id] = { id: children[0] };
|
||||
logger.info('Clusters ', clusterDb);
|
||||
}
|
||||
});
|
||||
|
||||
// Insert labels, this will insert them into the dom so that the width can be calculated
|
||||
// Also figure out which edges point to/from clusters and adjust them accordingly
|
||||
// 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
|
||||
graph.edges().forEach(function(e) {
|
||||
const edge = graph.edge(e);
|
||||
logger.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
|
||||
const v = translateClusterId(e.v);
|
||||
const w = translateClusterId(e.w);
|
||||
if (v !== e.v || w !== 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);
|
||||
});
|
||||
|
||||
graph.edges().forEach(function(e) {
|
||||
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||
});
|
||||
logger.info('#############################################');
|
||||
logger.info('### Layout ###');
|
||||
logger.info('#############################################');
|
||||
logger.info(graph);
|
||||
dagre.layout(graph);
|
||||
|
||||
// Move the nodes to the correct place
|
||||
graph.nodes().forEach(function(v) {
|
||||
const node = graph.node(v);
|
||||
logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||
if (node.type !== 'group') {
|
||||
positionNode(node);
|
||||
} else {
|
||||
insertCluster(clusters, node);
|
||||
clusterDb[node.id].node = node;
|
||||
}
|
||||
});
|
||||
|
||||
// Move the edge labels to the correct place after layout
|
||||
graph.edges().forEach(function(e) {
|
||||
const edge = graph.edge(e);
|
||||
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||
|
||||
insertEdge(edgePaths, edge, clusterDb, diagramtype);
|
||||
positionEdgeLabel(edge);
|
||||
});
|
||||
};
|
||||
|
||||
// const shapeDefinitions = {};
|
||||
// export const addShape = ({ shapeType: fun }) => {
|
||||
// shapeDefinitions[shapeType] = fun;
|
||||
// };
|
||||
|
||||
// const arrowDefinitions = {};
|
||||
// export const addArrow = ({ arrowType: fun }) => {
|
||||
// arrowDefinitions[arrowType] = fun;
|
||||
// };
|
7
src/dagre-wrapper/intersect.js
Normal file
7
src/dagre-wrapper/intersect.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
node: require('./intersect-node'),
|
||||
circle: require('./intersect-circle'),
|
||||
ellipse: require('./intersect-ellipse'),
|
||||
polygon: require('./intersect-polygon'),
|
||||
rect: require('./intersect-rect')
|
||||
};
|
17
src/dagre-wrapper/intersect/index.js
Normal file
17
src/dagre-wrapper/intersect/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Borrowed with love from from dagrge-d3. Many thanks to cpettitt!
|
||||
*/
|
||||
|
||||
import node from './intersect-node.js';
|
||||
import circle from './intersect-circle.js';
|
||||
import ellipse from './intersect-ellipse.js';
|
||||
import polygon from './intersect-polygon.js';
|
||||
import rect from './intersect-rect.js';
|
||||
|
||||
export default {
|
||||
node,
|
||||
circle,
|
||||
ellipse,
|
||||
polygon,
|
||||
rect
|
||||
};
|
7
src/dagre-wrapper/intersect/intersect-circle.js
Normal file
7
src/dagre-wrapper/intersect/intersect-circle.js
Normal file
@ -0,0 +1,7 @@
|
||||
import intersectEllipse from './intersect-ellipse';
|
||||
|
||||
function intersectCircle(node, rx, point) {
|
||||
return intersectEllipse(node, rx, rx, point);
|
||||
}
|
||||
|
||||
export default intersectCircle;
|
24
src/dagre-wrapper/intersect/intersect-ellipse.js
Normal file
24
src/dagre-wrapper/intersect/intersect-ellipse.js
Normal file
@ -0,0 +1,24 @@
|
||||
function intersectEllipse(node, rx, ry, point) {
|
||||
// Formulae from: http://mathworld.wolfram.com/Ellipse-LineIntersection.html
|
||||
|
||||
var cx = node.x;
|
||||
var cy = node.y;
|
||||
|
||||
var px = cx - point.x;
|
||||
var py = cy - point.y;
|
||||
|
||||
var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);
|
||||
|
||||
var dx = Math.abs((rx * ry * px) / det);
|
||||
if (point.x < cx) {
|
||||
dx = -dx;
|
||||
}
|
||||
var dy = Math.abs((rx * ry * py) / det);
|
||||
if (point.y < cy) {
|
||||
dy = -dy;
|
||||
}
|
||||
|
||||
return { x: cx + dx, y: cy + dy };
|
||||
}
|
||||
|
||||
export default intersectEllipse;
|
70
src/dagre-wrapper/intersect/intersect-line.js
Normal file
70
src/dagre-wrapper/intersect/intersect-line.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Returns the point at which two lines, p and q, intersect or returns
|
||||
* undefined if they do not intersect.
|
||||
*/
|
||||
function intersectLine(p1, p2, q1, q2) {
|
||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||
// p7 and p473.
|
||||
|
||||
var a1, a2, b1, b2, c1, c2;
|
||||
var r1, r2, r3, r4;
|
||||
var denom, offset, num;
|
||||
var x, y;
|
||||
|
||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||
// b1 y + c1 = 0.
|
||||
a1 = p2.y - p1.y;
|
||||
b1 = p1.x - p2.x;
|
||||
c1 = p2.x * p1.y - p1.x * p2.y;
|
||||
|
||||
// Compute r3 and r4.
|
||||
r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||
r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||
|
||||
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||
// same side of line 1, the line segments do not intersect.
|
||||
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||
return /*DONT_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||
a2 = q2.y - q1.y;
|
||||
b2 = q1.x - q2.x;
|
||||
c2 = q2.x * q1.y - q1.x * q2.y;
|
||||
|
||||
// Compute r1 and r2
|
||||
r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||
r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||
|
||||
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||
// on same side of second line segment, the line segments do
|
||||
// not intersect.
|
||||
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
|
||||
return /*DONT_INTERSECT*/;
|
||||
}
|
||||
|
||||
// Line segments intersect: compute intersection point.
|
||||
denom = a1 * b2 - a2 * b1;
|
||||
if (denom === 0) {
|
||||
return /*COLLINEAR*/;
|
||||
}
|
||||
|
||||
offset = Math.abs(denom / 2);
|
||||
|
||||
// The denom/2 is to get rounding instead of truncating. It
|
||||
// is added or subtracted to the numerator, depending upon the
|
||||
// sign of the numerator.
|
||||
num = b1 * c2 - b2 * c1;
|
||||
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
num = a2 * c1 - a1 * c2;
|
||||
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
function sameSign(r1, r2) {
|
||||
return r1 * r2 > 0;
|
||||
}
|
||||
|
||||
export default intersectLine;
|
5
src/dagre-wrapper/intersect/intersect-node.js
Normal file
5
src/dagre-wrapper/intersect/intersect-node.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = intersectNode;
|
||||
|
||||
function intersectNode(node, point) {
|
||||
return node.intersect(point);
|
||||
}
|
61
src/dagre-wrapper/intersect/intersect-polygon.js
Normal file
61
src/dagre-wrapper/intersect/intersect-polygon.js
Normal file
@ -0,0 +1,61 @@
|
||||
/* eslint "no-console": off */
|
||||
|
||||
import intersectLine from './intersect-line';
|
||||
|
||||
export default intersectPolygon;
|
||||
|
||||
/*
|
||||
* Returns the point ({x, y}) at which the point argument intersects with the
|
||||
* node argument assuming that it has the shape specified by polygon.
|
||||
*/
|
||||
function intersectPolygon(node, polyPoints, point) {
|
||||
var x1 = node.x;
|
||||
var y1 = node.y;
|
||||
|
||||
var intersections = [];
|
||||
|
||||
var minX = Number.POSITIVE_INFINITY;
|
||||
var minY = Number.POSITIVE_INFINITY;
|
||||
polyPoints.forEach(function(entry) {
|
||||
minX = Math.min(minX, entry.x);
|
||||
minY = Math.min(minY, entry.y);
|
||||
});
|
||||
|
||||
var left = x1 - node.width / 2 - minX;
|
||||
var top = y1 - node.height / 2 - minY;
|
||||
|
||||
for (var i = 0; i < polyPoints.length; i++) {
|
||||
var p1 = polyPoints[i];
|
||||
var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
|
||||
var intersect = intersectLine(
|
||||
node,
|
||||
point,
|
||||
{ x: left + p1.x, y: top + p1.y },
|
||||
{ x: left + p2.x, y: top + p2.y }
|
||||
);
|
||||
if (intersect) {
|
||||
intersections.push(intersect);
|
||||
}
|
||||
}
|
||||
|
||||
if (!intersections.length) {
|
||||
console.log('NO INTERSECTION FOUND, RETURN NODE CENTER', node);
|
||||
return node;
|
||||
}
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// More intersections, find the one nearest to edge end point
|
||||
intersections.sort(function(p, q) {
|
||||
var pdx = p.x - point.x;
|
||||
var pdy = p.y - point.y;
|
||||
var distp = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||
|
||||
var qdx = q.x - point.x;
|
||||
var qdy = q.y - point.y;
|
||||
var distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
||||
|
||||
return distp < distq ? -1 : distp === distq ? 0 : 1;
|
||||
});
|
||||
}
|
||||
return intersections[0];
|
||||
}
|
32
src/dagre-wrapper/intersect/intersect-rect.js
Normal file
32
src/dagre-wrapper/intersect/intersect-rect.js
Normal file
@ -0,0 +1,32 @@
|
||||
const intersectRect = (node, point) => {
|
||||
var x = node.x;
|
||||
var y = node.y;
|
||||
|
||||
// Rectangle intersection algorithm from:
|
||||
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
||||
var dx = point.x - x;
|
||||
var dy = point.y - y;
|
||||
var w = node.width / 2;
|
||||
var h = node.height / 2;
|
||||
|
||||
var sx, sy;
|
||||
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
if (dy < 0) {
|
||||
h = -h;
|
||||
}
|
||||
sx = dy === 0 ? 0 : (h * dx) / dy;
|
||||
sy = h;
|
||||
} else {
|
||||
// Intersection is left or right of rect.
|
||||
if (dx < 0) {
|
||||
w = -w;
|
||||
}
|
||||
sx = w;
|
||||
sy = dx === 0 ? 0 : (w * dy) / dx;
|
||||
}
|
||||
|
||||
return { x: x + sx, y: y + sy };
|
||||
};
|
||||
|
||||
export default intersectRect;
|
260
src/dagre-wrapper/markers.js
Normal file
260
src/dagre-wrapper/markers.js
Normal file
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Setup arrow head and define the marker. The result is appended to the svg.
|
||||
*/
|
||||
|
||||
import { logger } from '../logger';
|
||||
|
||||
// Only add the number of markers that the diagram needs
|
||||
const insertMarkers = (elem, markerArray, type, id) => {
|
||||
markerArray.forEach(markerName => {
|
||||
markers[markerName](elem, type, id);
|
||||
});
|
||||
};
|
||||
|
||||
const extension = (elem, type, id) => {
|
||||
logger.trace('Making markers for ', id);
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'extensionStart')
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 1,7 L18,13 V 1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'extensionEnd ' + type)
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
const composition = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'compositionStart')
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'compositionEnd')
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const aggregation = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'aggregationStart')
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'aggregationEnd')
|
||||
.attr('class', type)
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const dependency = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'dependencyStart')
|
||||
.attr('class', 'extension ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'dependencyEnd')
|
||||
.attr('class', type)
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||
};
|
||||
const point = (elem, type) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-pointEnd')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 10)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 8)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-pointStart')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 8)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 5 L 10 10 L 10 0 z')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const circle = (elem, type) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', 'circleEnd')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 11)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 7)
|
||||
.attr('markerHeight', 7)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('cx', '5')
|
||||
.attr('cy', '5')
|
||||
.attr('r', '5')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', 'circleStart')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', -1)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 7)
|
||||
.attr('markerHeight', 7)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('cx', '5')
|
||||
.attr('cy', '5')
|
||||
.attr('r', '5')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const cross = (elem, type) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', 'crossEnd')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 11 11')
|
||||
.attr('refX', 12)
|
||||
.attr('refY', 5.2)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 7)
|
||||
.attr('markerHeight', 7)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', 'black')
|
||||
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 2)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', 'crossStart')
|
||||
.attr('class', type)
|
||||
.attr('viewBox', '0 0 11 11')
|
||||
.attr('refX', -1)
|
||||
.attr('refY', 5.2)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 7)
|
||||
.attr('markerHeight', 7)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', 'black')
|
||||
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
|
||||
.attr('class', 'arrowMarkerPath')
|
||||
.style('stroke-width', 2)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const barb = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-barbEnd')
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 14)
|
||||
.attr('markerUnits', 0)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z');
|
||||
};
|
||||
|
||||
// TODO rename the class diagram markers to something shape descriptive and semanitc free
|
||||
const markers = {
|
||||
extension,
|
||||
composition,
|
||||
aggregation,
|
||||
dependency,
|
||||
point,
|
||||
circle,
|
||||
cross,
|
||||
barb
|
||||
};
|
||||
export default insertMarkers;
|
397
src/dagre-wrapper/nodes.js
Normal file
397
src/dagre-wrapper/nodes.js
Normal file
@ -0,0 +1,397 @@
|
||||
import intersect from './intersect/index.js';
|
||||
import { logger } from '../logger'; // eslint-disable-line
|
||||
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util';
|
||||
import note from './shapes/note';
|
||||
|
||||
const question = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const s = (w + h) * 0.9;
|
||||
const points = [
|
||||
{ x: s / 2, y: 0 },
|
||||
{ x: s, y: -s / 2 },
|
||||
{ x: s / 2, y: -s },
|
||||
{ x: 0, y: -s / 2 }
|
||||
];
|
||||
|
||||
const questionElem = insertPolygonShape(shapeSvg, s, s, points);
|
||||
updateNodeBounds(node, questionElem);
|
||||
node.intersect = function(point) {
|
||||
return intersect.polugon(node, points, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const hexagon = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const f = 4;
|
||||
const h = bbox.height + node.padding;
|
||||
const m = h / f;
|
||||
const w = bbox.width + 2 * m + node.padding;
|
||||
const points = [
|
||||
{ x: m, y: 0 },
|
||||
{ x: w - m, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w - m, y: -h },
|
||||
{ x: m, y: -h },
|
||||
{ x: 0, y: -h / 2 }
|
||||
];
|
||||
const hex = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, hex);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect_left_inv_arrow = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: -h / 2, y: 0 },
|
||||
{ x: w, y: 0 },
|
||||
{ x: w, y: -h },
|
||||
{ x: -h / 2, y: -h },
|
||||
{ x: 0, y: -h / 2 }
|
||||
];
|
||||
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const lean_right = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: (-2 * h) / 6, y: 0 },
|
||||
{ x: w - h / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: -h },
|
||||
{ x: h / 6, y: -h }
|
||||
];
|
||||
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const lean_left = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: (2 * h) / 6, y: 0 },
|
||||
{ x: w + h / 6, y: 0 },
|
||||
{ x: w - (2 * h) / 6, y: -h },
|
||||
{ x: -h / 6, y: -h }
|
||||
];
|
||||
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const trapezoid = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: (-2 * h) / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: 0 },
|
||||
{ x: w - h / 6, y: -h },
|
||||
{ x: h / 6, y: -h }
|
||||
];
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const inv_trapezoid = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: h / 6, y: 0 },
|
||||
{ x: w - h / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: -h },
|
||||
{ x: (-2 * h) / 6, y: -h }
|
||||
];
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const rect_right_inv_arrow = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const points = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: w + h / 2, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w + h / 2, y: -h },
|
||||
{ x: 0, y: -h }
|
||||
];
|
||||
const el = insertPolygonShape(shapeSvg, w, h, points);
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.polygon(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const cylinder = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const rx = w / 2;
|
||||
const ry = rx / (2.5 + w / 50);
|
||||
const h = bbox.height + ry + node.padding;
|
||||
|
||||
const shape =
|
||||
'M 0,' +
|
||||
ry +
|
||||
' a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
w +
|
||||
' 0 a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
-w +
|
||||
' 0 l 0,' +
|
||||
h +
|
||||
' a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
w +
|
||||
' 0 l 0,' +
|
||||
-h;
|
||||
|
||||
const el = shapeSvg
|
||||
.attr('label-offset-y', ry)
|
||||
.insert('path', ':first-child')
|
||||
.attr('d', shape)
|
||||
.attr('transform', 'translate(' + -w / 2 + ',' + -(h / 2 + ry) + ')');
|
||||
|
||||
updateNodeBounds(node, el);
|
||||
|
||||
node.intersect = function(point) {
|
||||
const pos = intersect.rect(node, point);
|
||||
const x = pos.x - node.x;
|
||||
|
||||
if (
|
||||
rx != 0 &&
|
||||
(Math.abs(x) < node.width / 2 ||
|
||||
(Math.abs(x) == node.width / 2 && Math.abs(pos.y - node.y) > node.height / 2 - ry))
|
||||
) {
|
||||
// ellipsis equation: x*x / a*a + y*y / b*b = 1
|
||||
// solve for y to get adjustion value for pos.y
|
||||
let y = ry * ry * (1 - (x * x) / (rx * rx));
|
||||
if (y != 0) y = Math.sqrt(y);
|
||||
y = ry - y;
|
||||
if (point.y - node.y > 0) y = -y;
|
||||
|
||||
pos.y += y;
|
||||
}
|
||||
|
||||
return pos;
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
|
||||
|
||||
logger.info('Classes = ', node.classes);
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
rect
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
.attr('y', -bbox.height / 2 - halfPadding)
|
||||
.attr('width', bbox.width + node.padding)
|
||||
.attr('height', bbox.height + node.padding);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const stadium = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
||||
const h = bbox.height + node.padding;
|
||||
const w = bbox.width + h / 4 + node.padding;
|
||||
|
||||
// add the rect
|
||||
const rect = shapeSvg
|
||||
.insert('rect', ':first-child')
|
||||
.attr('rx', h / 2)
|
||||
.attr('ry', h / 2)
|
||||
.attr('x', -w / 2)
|
||||
.attr('y', -h / 2)
|
||||
.attr('width', w)
|
||||
.attr('height', h);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const circle = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node);
|
||||
const circle = shapeSvg.insert('circle', ':first-child');
|
||||
|
||||
// center the circle around its coordinate
|
||||
circle
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('r', bbox.width / 2 + halfPadding)
|
||||
.attr('width', bbox.width + node.padding)
|
||||
.attr('height', bbox.height + node.padding);
|
||||
|
||||
updateNodeBounds(node, circle);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.circle(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const start = (parent, node) => {
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'node default')
|
||||
.attr('id', node.id);
|
||||
const circle = shapeSvg.insert('circle', ':first-child');
|
||||
|
||||
// center the circle around its coordinate
|
||||
circle
|
||||
.attr('class', 'state-start')
|
||||
.attr('r', 7)
|
||||
.attr('width', 14)
|
||||
.attr('height', 14);
|
||||
|
||||
updateNodeBounds(node, circle);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.circle(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
const end = (parent, node) => {
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'node default')
|
||||
.attr('id', node.id);
|
||||
const innerCircle = shapeSvg.insert('circle', ':first-child');
|
||||
const circle = shapeSvg.insert('circle', ':first-child');
|
||||
|
||||
circle
|
||||
.attr('class', 'state-start')
|
||||
.attr('r', 7)
|
||||
.attr('width', 14)
|
||||
.attr('height', 14);
|
||||
|
||||
innerCircle
|
||||
.attr('class', 'state-end')
|
||||
.attr('r', 5)
|
||||
.attr('width', 10)
|
||||
.attr('height', 10);
|
||||
|
||||
updateNodeBounds(node, circle);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.circle(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const shapes = {
|
||||
question,
|
||||
rect,
|
||||
circle,
|
||||
stadium,
|
||||
hexagon,
|
||||
rect_left_inv_arrow,
|
||||
lean_right,
|
||||
lean_left,
|
||||
trapezoid,
|
||||
inv_trapezoid,
|
||||
rect_right_inv_arrow,
|
||||
cylinder,
|
||||
start,
|
||||
end,
|
||||
note
|
||||
};
|
||||
|
||||
let nodeElems = {};
|
||||
|
||||
export const insertNode = (elem, node) => {
|
||||
nodeElems[node.id] = shapes[node.shape](elem, node);
|
||||
};
|
||||
export const clear = () => {
|
||||
nodeElems = {};
|
||||
};
|
||||
|
||||
export const positionNode = node => {
|
||||
const el = nodeElems[node.id];
|
||||
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||
};
|
29
src/dagre-wrapper/shapes/note.js
Normal file
29
src/dagre-wrapper/shapes/note.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { updateNodeBounds, labelHelper } from './util';
|
||||
import { logger } from '../../logger'; // eslint-disable-line
|
||||
import intersect from '../intersect/index.js';
|
||||
|
||||
const note = (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
|
||||
|
||||
logger.info('Classes = ', node.classes);
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
rect
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
.attr('y', -bbox.height / 2 - halfPadding)
|
||||
.attr('width', bbox.width + node.padding)
|
||||
.attr('height', bbox.height + node.padding);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
export default note;
|
50
src/dagre-wrapper/shapes/util.js
Normal file
50
src/dagre-wrapper/shapes/util.js
Normal file
@ -0,0 +1,50 @@
|
||||
import createLabel from '../createLabel';
|
||||
|
||||
export const labelHelper = (parent, node, _classes) => {
|
||||
let classes;
|
||||
if (!_classes) {
|
||||
classes = 'node default';
|
||||
} else {
|
||||
classes = _classes;
|
||||
}
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', classes)
|
||||
.attr('id', node.id);
|
||||
|
||||
// Create the label and insert it after the rect
|
||||
const label = shapeSvg.insert('g').attr('class', 'label');
|
||||
|
||||
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle));
|
||||
|
||||
// Get the size of the label
|
||||
const bbox = text.getBBox();
|
||||
|
||||
const halfPadding = node.padding / 2;
|
||||
|
||||
// Center the label
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
|
||||
return { shapeSvg, bbox, halfPadding, label };
|
||||
};
|
||||
|
||||
export const updateNodeBounds = (node, element) => {
|
||||
const bbox = element.node().getBBox();
|
||||
node.width = bbox.width;
|
||||
node.height = bbox.height;
|
||||
};
|
||||
|
||||
export function insertPolygonShape(parent, w, h, points) {
|
||||
return parent
|
||||
.insert('polygon', ':first-child')
|
||||
.attr(
|
||||
'points',
|
||||
points
|
||||
.map(function(d) {
|
||||
return d.x + ',' + d.y;
|
||||
})
|
||||
.join(' ')
|
||||
)
|
||||
.attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')');
|
||||
}
|
@ -9,8 +9,7 @@
|
||||
%x string generic struct
|
||||
|
||||
%%
|
||||
\%\%[^\n]*\n* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
\%\%[^\n]*\n* /* do nothing */
|
||||
\n+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
"classDiagram" return 'CLASS_DIAGRAM';
|
||||
|
77
src/diagrams/er/erDb.js
Normal file
77
src/diagrams/er/erDb.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
import { logger } from '../../logger';
|
||||
|
||||
let entities = {};
|
||||
let relationships = [];
|
||||
let title = '';
|
||||
|
||||
const Cardinality = {
|
||||
ZERO_OR_ONE: 'ZERO_OR_ONE',
|
||||
ZERO_OR_MORE: 'ZERO_OR_MORE',
|
||||
ONE_OR_MORE: 'ONE_OR_MORE',
|
||||
ONLY_ONE: 'ONLY_ONE'
|
||||
};
|
||||
|
||||
const Identification = {
|
||||
NON_IDENTIFYING: 'NON_IDENTIFYING',
|
||||
IDENTIFYING: 'IDENTIFYING'
|
||||
};
|
||||
|
||||
const addEntity = function(name) {
|
||||
if (typeof entities[name] === 'undefined') {
|
||||
entities[name] = name;
|
||||
logger.debug('Added new entity :', name);
|
||||
}
|
||||
};
|
||||
|
||||
const getEntities = () => entities;
|
||||
|
||||
/**
|
||||
* Add a relationship
|
||||
* @param entA The first entity in the relationship
|
||||
* @param rolA The role played by the first entity in relation to the second
|
||||
* @param entB The second entity in the relationship
|
||||
* @param rSpec The details of the relationship between the two entities
|
||||
*/
|
||||
const addRelationship = function(entA, rolA, entB, rSpec) {
|
||||
let rel = {
|
||||
entityA: entA,
|
||||
roleA: rolA,
|
||||
entityB: entB,
|
||||
relSpec: rSpec
|
||||
};
|
||||
|
||||
relationships.push(rel);
|
||||
logger.debug('Added new relationship :', rel);
|
||||
};
|
||||
|
||||
const getRelationships = () => relationships;
|
||||
|
||||
// Keep this - TODO: revisit...allow the diagram to have a title
|
||||
const setTitle = function(txt) {
|
||||
title = txt;
|
||||
};
|
||||
|
||||
const getTitle = function() {
|
||||
return title;
|
||||
};
|
||||
|
||||
const clear = function() {
|
||||
entities = {};
|
||||
relationships = [];
|
||||
title = '';
|
||||
};
|
||||
|
||||
export default {
|
||||
Cardinality,
|
||||
Identification,
|
||||
addEntity,
|
||||
getEntities,
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
clear,
|
||||
setTitle,
|
||||
getTitle
|
||||
};
|
168
src/diagrams/er/erMarkers.js
Normal file
168
src/diagrams/er/erMarkers.js
Normal file
@ -0,0 +1,168 @@
|
||||
const ERMarkers = {
|
||||
ONLY_ONE_START: 'ONLY_ONE_START',
|
||||
ONLY_ONE_END: 'ONLY_ONE_END',
|
||||
ZERO_OR_ONE_START: 'ZERO_OR_ONE_START',
|
||||
ZERO_OR_ONE_END: 'ZERO_OR_ONE_END',
|
||||
ONE_OR_MORE_START: 'ONE_OR_MORE_START',
|
||||
ONE_OR_MORE_END: 'ONE_OR_MORE_END',
|
||||
ZERO_OR_MORE_START: 'ZERO_OR_MORE_START',
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END'
|
||||
};
|
||||
|
||||
/**
|
||||
* Put the markers into the svg DOM for later use with edge paths
|
||||
*/
|
||||
const insertMarkers = function(elem, conf) {
|
||||
let marker;
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18 M15,0 L15,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_END)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,0 L3,18 M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 21)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_END)
|
||||
.attr('refX', 30)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,0 L21,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONE_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONE_OR_MORE_END)
|
||||
.attr('refX', 27)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 48)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_END)
|
||||
.attr('refX', 39)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default {
|
||||
ERMarkers,
|
||||
insertMarkers
|
||||
};
|
346
src/diagrams/er/erRenderer.js
Normal file
346
src/diagrams/er/erRenderer.js
Normal file
@ -0,0 +1,346 @@
|
||||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
import erDb from './erDb';
|
||||
import erParser from './parser/erDiagram';
|
||||
import dagre from 'dagre';
|
||||
import { getConfig } from '../../config';
|
||||
import { logger } from '../../logger';
|
||||
import erMarkers from './erMarkers';
|
||||
|
||||
const conf = {};
|
||||
|
||||
/**
|
||||
* Allows the top-level API module to inject config specific to this renderer,
|
||||
* storing it in the local conf object. Note that generic config still needs to be
|
||||
* retrieved using getConfig() imported from the config module
|
||||
*/
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Use D3 to construct the svg elements for the entities
|
||||
* @param svgNode the svg node that contains the diagram
|
||||
* @param entities The entities to be drawn
|
||||
* @param graph The graph that contains the vertex and edge definitions post-layout
|
||||
* @return The first entity that was inserted
|
||||
*/
|
||||
const drawEntities = function(svgNode, entities, graph) {
|
||||
const keys = Object.keys(entities);
|
||||
let firstOne;
|
||||
|
||||
keys.forEach(function(id) {
|
||||
// Create a group for each entity
|
||||
const groupNode = svgNode.append('g').attr('id', id);
|
||||
|
||||
firstOne = firstOne === undefined ? id : firstOne;
|
||||
|
||||
// Label the entity - this is done first so that we can get the bounding box
|
||||
// which then determines the size of the rectangle
|
||||
const textId = 'entity-' + id;
|
||||
const textNode = groupNode
|
||||
.append('text')
|
||||
.attr('id', textId)
|
||||
.attr('x', 0)
|
||||
.attr('y', (conf.fontSize + 2 * conf.entityPadding) / 2)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(id);
|
||||
|
||||
// Calculate the width and height of the entity
|
||||
const textBBox = textNode.node().getBBox();
|
||||
const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2);
|
||||
const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2);
|
||||
|
||||
// Make sure the text gets centred relative to the entity box
|
||||
textNode.attr('transform', 'translate(' + entityWidth / 2 + ',' + entityHeight / 2 + ')');
|
||||
|
||||
// Draw the rectangle - insert it before the text so that the text is not obscured
|
||||
const rectNode = groupNode
|
||||
.insert('rect', '#' + textId)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', entityWidth)
|
||||
.attr('height', entityHeight);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(id, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: id
|
||||
});
|
||||
});
|
||||
return firstOne;
|
||||
}; // drawEntities
|
||||
|
||||
const adjustEntities = function(svgNode, graph) {
|
||||
graph.nodes().forEach(function(v) {
|
||||
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(graph.node(v).x - graph.node(v).width / 2) +
|
||||
',' +
|
||||
(graph.node(v).y - graph.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const getEdgeName = function(rel) {
|
||||
return (rel.entityA + rel.roleA + rel.entityB).replace(/\s/g, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add each relationship to the graph
|
||||
* @param relationships the relationships to be added
|
||||
* @param g the graph
|
||||
* @return {Array} The array of relationships
|
||||
*/
|
||||
const addRelationships = function(relationships, g) {
|
||||
relationships.forEach(function(r) {
|
||||
g.setEdge(r.entityA, r.entityB, { relationship: r }, getEdgeName(r));
|
||||
});
|
||||
return relationships;
|
||||
}; // addRelationships
|
||||
|
||||
let relCnt = 0;
|
||||
/**
|
||||
* Draw a relationship using edge information from the graph
|
||||
* @param svg the svg node
|
||||
* @param rel the relationship to draw in the svg
|
||||
* @param g the graph containing the edge information
|
||||
* @param insert the insertion point in the svg DOM (because relationships have markers that need to sit 'behind' opaque entity boxes)
|
||||
*/
|
||||
const drawRelationshipFromLayout = function(svg, rel, g, insert) {
|
||||
relCnt++;
|
||||
|
||||
// Find the edge relating to this relationship
|
||||
const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel));
|
||||
|
||||
// Get a function that will generate the line path
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.x(function(d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function(d) {
|
||||
return d.y;
|
||||
})
|
||||
.curve(d3.curveBasis);
|
||||
|
||||
// Insert the line at the right place
|
||||
const svgPath = svg
|
||||
.insert('path', '#' + insert)
|
||||
.attr('d', lineFunction(edge.points))
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none');
|
||||
|
||||
// ...and with dashes if necessary
|
||||
if (rel.relSpec.relType === erDb.Identification.NON_IDENTIFYING) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// TODO: Understand this better
|
||||
let url = '';
|
||||
if (conf.arrowMarkerAbsolute) {
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(');
|
||||
url = url.replace(/\)/g, '\\)');
|
||||
}
|
||||
|
||||
// Decide which start and end markers it needs. It may be possible to be more concise here
|
||||
// by reversing a start marker to make an end marker...but this will do for now
|
||||
|
||||
// Note that the 'A' entity's marker is at the end of the relationship and the 'B' entity's marker is at the start
|
||||
switch (rel.relSpec.cardA) {
|
||||
case erDb.Cardinality.ZERO_OR_ONE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (rel.relSpec.cardB) {
|
||||
case erDb.Cardinality.ZERO_OR_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
|
||||
// Find the half-way point
|
||||
const len = svgPath.node().getTotalLength();
|
||||
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
|
||||
|
||||
// Append a text node containing the label
|
||||
const labelId = 'rel' + relCnt;
|
||||
|
||||
const labelNode = svg
|
||||
.append('text')
|
||||
.attr('id', labelId)
|
||||
.attr('x', labelPoint.x)
|
||||
.attr('y', labelPoint.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(rel.roleA);
|
||||
|
||||
// Figure out how big the opaque 'container' rectangle needs to be
|
||||
const labelBBox = labelNode.node().getBBox();
|
||||
|
||||
// Insert the opaque rectangle before the text label
|
||||
svg
|
||||
.insert('rect', '#' + labelId)
|
||||
.attr('x', labelPoint.x - labelBBox.width / 2)
|
||||
.attr('y', labelPoint.y - labelBBox.height / 2)
|
||||
.attr('width', labelBBox.width)
|
||||
.attr('height', labelBBox.height)
|
||||
.attr('fill', 'white')
|
||||
.attr('fill-opacity', '85%');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw en E-R diagram in the tag with id: id based on the text definition of the diagram
|
||||
* @param text the text of the diagram
|
||||
* @param id the unique id of the DOM node that contains the diagram
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing ER diagram');
|
||||
erDb.clear();
|
||||
const parser = erParser.parser;
|
||||
parser.yy = erDb;
|
||||
|
||||
// Parse the text to populate erDb
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Get a reference to the svg node that contains the text
|
||||
const svg = d3.select(`[id='${id}']`);
|
||||
|
||||
// Add cardinality marker definitions to the svg
|
||||
erMarkers.insertMarkers(svg, conf);
|
||||
|
||||
// Now we have to construct the diagram in a specific way:
|
||||
// ---
|
||||
// 1. Create all the entities in the svg node at 0,0, but with the correct dimensions (allowing for text content)
|
||||
// 2. Make sure they are all added to the graph
|
||||
// 3. Add all the edges (relationships) to the graph aswell
|
||||
// 4. Let dagre do its magic to layout the graph. This assigns:
|
||||
// - the centre co-ordinates for each node, bearing in mind the dimensions and edge relationships
|
||||
// - the path co-ordinates for each edge
|
||||
// But it has no impact on the svg child nodes - the diagram remains with every entity rooted at 0,0
|
||||
// 5. Now assign a transform to each entity in the svg node so that it gets drawn in the correct place, as determined by
|
||||
// its centre point, which is obtained from the graph, and it's width and height
|
||||
// 6. And finally, create all the edges in the svg node using information from the graph
|
||||
// ---
|
||||
|
||||
// Create the graph
|
||||
let g;
|
||||
|
||||
// TODO: Explore directed vs undirected graphs, and how the layout is affected
|
||||
// An E-R diagram could be said to be undirected, but there is merit in setting
|
||||
// the direction from parent to child in a one-to-many as this influences graphlib to
|
||||
// put the parent above the child (does it?), which is intuitive. Most relationships
|
||||
// in ER diagrams are one-to-many.
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
directed: true,
|
||||
compound: false
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: conf.layoutDirection,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
nodesep: 100,
|
||||
edgesep: 100,
|
||||
ranksep: 100
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Draw the entities (at 0,0), returning the first svg node that got
|
||||
// inserted - this represents the insertion point for relationship paths
|
||||
const firstEntity = drawEntities(svg, erDb.getEntities(), g);
|
||||
|
||||
// TODO: externalise the addition of entities to the graph - it's a bit 'buried' in the above
|
||||
|
||||
// Add all the relationships to the graph
|
||||
const relationships = addRelationships(erDb.getRelationships(), g);
|
||||
|
||||
dagre.layout(g); // Node and edge positions will be updated
|
||||
|
||||
// Adjust the positions of the entities so that they adhere to the layout
|
||||
adjustEntities(svg, g);
|
||||
|
||||
// Draw the relationships
|
||||
relationships.forEach(function(rel) {
|
||||
drawRelationshipFromLayout(svg, rel, g, firstEntity);
|
||||
});
|
||||
|
||||
const padding = conf.diagramPadding;
|
||||
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 2;
|
||||
const height = svgBounds.height + padding * 2;
|
||||
|
||||
svg.attr('height', height);
|
||||
svg.attr('width', '100%');
|
||||
svg.attr('style', `max-width: ${width}px;`);
|
||||
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
|
||||
}; // draw
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw
|
||||
};
|
79
src/diagrams/er/parser/erDiagram.jison
Normal file
79
src/diagrams/er/parser/erDiagram.jison
Normal file
@ -0,0 +1,79 @@
|
||||
%lex
|
||||
|
||||
%x string
|
||||
%options case-insensitive
|
||||
|
||||
%%
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
["] { this.begin("string");}
|
||||
<string>["] { this.popState(); }
|
||||
<string>[^"]* { return 'STR'; }
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
\|o return 'ZERO_OR_ONE';
|
||||
\}o return 'ZERO_OR_MORE';
|
||||
\}\| return 'ONE_OR_MORE';
|
||||
\|\| return 'ONLY_ONE';
|
||||
o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
\.\- return 'NON_IDENTIFYING';
|
||||
\-\. return 'NON_IDENTIFYING';
|
||||
[A-Za-z][A-Za-z0-9\-]* return 'ALPHANUM';
|
||||
. return yytext[0];
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
| document statement
|
||||
;
|
||||
|
||||
statement
|
||||
: entityName relSpec entityName ':' role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
||||
};
|
||||
|
||||
entityName
|
||||
: 'ALPHANUM' { $$ = $1; /*console.log('Entity: ' + $1);*/ }
|
||||
;
|
||||
|
||||
relSpec
|
||||
: cardinality relType cardinality
|
||||
{
|
||||
$$ = { cardA: $3, relType: $2, cardB: $1 };
|
||||
/*console.log('relSpec: ' + $3 + $2 + $1);*/
|
||||
}
|
||||
;
|
||||
|
||||
cardinality
|
||||
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||
| 'ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE; }
|
||||
| 'ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE; }
|
||||
;
|
||||
|
||||
relType
|
||||
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
|
||||
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
|
||||
;
|
||||
|
||||
role
|
||||
: 'STR' { $$ = $1; }
|
||||
| 'ALPHANUM' { $$ = $1; }
|
||||
;
|
||||
%%
|
255
src/diagrams/er/parser/erDiagram.spec.js
Normal file
255
src/diagrams/er/parser/erDiagram.spec.js
Normal file
@ -0,0 +1,255 @@
|
||||
import erDb from '../erDb';
|
||||
import erDiagram from './erDiagram';
|
||||
import { setConfig } from '../../../config';
|
||||
import logger from '../../../logger';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict'
|
||||
});
|
||||
|
||||
describe('when parsing ER diagram it...', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
erDiagram.parser.yy = erDb;
|
||||
erDiagram.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should associate two entities correctly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nCAR ||--o{ DRIVER : "insured for"');
|
||||
const entities = erDb.getEntities();
|
||||
const relationships = erDb.getRelationships();
|
||||
const carEntity = entities.CAR;
|
||||
const driverEntity = entities.DRIVER;
|
||||
|
||||
expect(carEntity).toBe('CAR');
|
||||
expect(driverEntity).toBe('DRIVER');
|
||||
expect(relationships.length).toBe(1);
|
||||
expect(relationships[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(relationships[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(relationships[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should not create duplicate entities', function() {
|
||||
const line1 = 'CAR ||--o{ DRIVER : "insured for"';
|
||||
const line2 = 'DRIVER ||--|| LICENSE : has';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(3);
|
||||
});
|
||||
|
||||
it('should create the role specified', function() {
|
||||
const teacherRole = 'is teacher of';
|
||||
const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`;
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}`);
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(rels[0].roleA).toBe(`${teacherRole}`);
|
||||
});
|
||||
|
||||
it('should allow recursive relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nNODE ||--o{ NODE : "leads to"');
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow more than one relationship between the same two entities', function() {
|
||||
const line1 = 'CAR ||--o{ PERSON : "insured for"';
|
||||
const line2 = 'CAR }o--|| PERSON : "owned by"';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(2);
|
||||
expect(rels.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should limit the number of relationships between the same two entities', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
it ('should not allow multiple relationships between the same two entities unless the roles are different', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
|
||||
it('should handle only-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|--|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||..|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||--o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should represent identifying relationships properly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nHOUSE ||--|{ ROOM : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should represent non-identifying relationships properly', function() {
|
||||
erDiagram.parser.parse('erDiagram\n PERSON ||..o{ POSSESSION : owns');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.NON_IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should not accept a syntax error', function() {
|
||||
const doc = 'erDiagram\nA xxx B : has';
|
||||
expect(() => {
|
||||
erDiagram.parser.parse(doc);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
});
|
@ -242,6 +242,31 @@ export function addToRender(render) {
|
||||
render.shapes().rect_right_inv_arrow = rect_right_inv_arrow;
|
||||
}
|
||||
|
||||
export function addToRenderV2(addShape) {
|
||||
addShape({ question });
|
||||
addShape({ hexagon });
|
||||
addShape({ stadium });
|
||||
addShape({ cylinder });
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
addShape({ rect_left_inv_arrow });
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
addShape({ lean_right });
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
addShape({ lean_left });
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
addShape({ trapezoid });
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
addShape({ inv_trapezoid });
|
||||
|
||||
// Add custom shape for box with inverted arrow on right side
|
||||
addShape({ rect_right_inv_arrow });
|
||||
}
|
||||
|
||||
function insertPolygonShape(parent, w, h, points) {
|
||||
return parent
|
||||
.insert('polygon', ':first-child')
|
||||
@ -257,5 +282,6 @@ function insertPolygonShape(parent, w, h, points) {
|
||||
}
|
||||
|
||||
export default {
|
||||
addToRender
|
||||
addToRender,
|
||||
addToRenderV2
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as d3 from 'd3';
|
||||
import { logger } from '../../logger';
|
||||
import { logger } from '../../logger'; // eslint-disable-line
|
||||
import utils from '../../utils';
|
||||
import { getConfig } from '../../config';
|
||||
import common from '../common/common';
|
||||
@ -88,7 +88,7 @@ export const addSingleLink = function(_start, _end, type, linktext) {
|
||||
let end = _end;
|
||||
if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start;
|
||||
if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end;
|
||||
logger.info('Got edge...', start, end);
|
||||
// logger.info('Got edge...', start, end);
|
||||
|
||||
const edge = { start: start, end: end, type: undefined, text: '' };
|
||||
linktext = type.text;
|
||||
@ -496,19 +496,19 @@ const destructStartLink = _str => {
|
||||
|
||||
switch (str) {
|
||||
case '<--':
|
||||
return { type: 'arrow', stroke: 'normal' };
|
||||
return { type: 'arrow_point', stroke: 'normal' };
|
||||
case 'x--':
|
||||
return { type: 'arrow_cross', stroke: 'normal' };
|
||||
case 'o--':
|
||||
return { type: 'arrow_circle', stroke: 'normal' };
|
||||
case '<-.':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
return { type: 'arrow_point', stroke: 'dotted' };
|
||||
case 'x-.':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case 'o-.':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '<==':
|
||||
return { type: 'arrow', stroke: 'thick' };
|
||||
return { type: 'arrow_point', stroke: 'thick' };
|
||||
case 'x==':
|
||||
return { type: 'arrow_cross', stroke: 'thick' };
|
||||
case 'o==':
|
||||
@ -529,7 +529,7 @@ const destructEndLink = _str => {
|
||||
case '--x':
|
||||
return { type: 'arrow_cross', stroke: 'normal' };
|
||||
case '-->':
|
||||
return { type: 'arrow', stroke: 'normal' };
|
||||
return { type: 'arrow_point', stroke: 'normal' };
|
||||
case '<-->':
|
||||
return { type: 'double_arrow_point', stroke: 'normal' };
|
||||
case 'x--x':
|
||||
@ -561,7 +561,7 @@ const destructEndLink = _str => {
|
||||
case '-.-x':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case '-.->':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
return { type: 'arrow_point', stroke: 'dotted' };
|
||||
case '-.-o':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '-.-':
|
||||
@ -569,7 +569,7 @@ const destructEndLink = _str => {
|
||||
case '.-x':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case '.->':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
return { type: 'arrow_point', stroke: 'dotted' };
|
||||
case '.-o':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '.-':
|
||||
@ -577,7 +577,7 @@ const destructEndLink = _str => {
|
||||
case '==x':
|
||||
return { type: 'arrow_cross', stroke: 'thick' };
|
||||
case '==>':
|
||||
return { type: 'arrow', stroke: 'thick' };
|
||||
return { type: 'arrow_point', stroke: 'thick' };
|
||||
case '==o':
|
||||
return { type: 'arrow_circle', stroke: 'thick' };
|
||||
case '===':
|
||||
|
468
src/diagrams/flowchart/flowRenderer-v2.js
Normal file
468
src/diagrams/flowchart/flowRenderer-v2.js
Normal file
@ -0,0 +1,468 @@
|
||||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
import dagre from 'dagre';
|
||||
|
||||
import flowDb from './flowDb';
|
||||
import flow from './parser/flow';
|
||||
import { getConfig } from '../../config';
|
||||
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
||||
import { logger } from '../../logger';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
* @param vert Object containing the vertices.
|
||||
* @param g The graph that is to be drawn.
|
||||
*/
|
||||
export const addVertices = function(vert, g, svgId) {
|
||||
const svg = d3.select(`[id="${svgId}"]`);
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function(id) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
|
||||
const styles = getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText.replace(
|
||||
/fa[lrsb]?:fa-[\w-]+/g,
|
||||
s => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
)
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(/<br\s*\/?>/gi);
|
||||
|
||||
for (let j = 0; j < rows.length; j++) {
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '1');
|
||||
tspan.textContent = rows[j];
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
|
||||
let radious = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
_shape = 'question';
|
||||
break;
|
||||
case 'hexagon':
|
||||
_shape = 'hexagon';
|
||||
break;
|
||||
case 'odd':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
_shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
_shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
_shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
_shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'odd_right':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
_shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
_shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
_shape = 'stadium';
|
||||
break;
|
||||
case 'cylinder':
|
||||
_shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelType: 'svg',
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
label: vertexNode,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
padding: getConfig().flowchart.padding
|
||||
});
|
||||
|
||||
logger.info('setNode', {
|
||||
labelType: 'svg',
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
label: vertexNode,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
padding: getConfig().flowchart.padding
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph defninition
|
||||
* @param {Object} edges The edges to add to the graph
|
||||
* @param {Object} g The graph object
|
||||
*/
|
||||
export const addEdges = function(edges, g) {
|
||||
let cnt = 0;
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
if (typeof edges.defaultStyle !== 'undefined') {
|
||||
const defaultStyles = getStylesFromArray(edges.defaultStyle);
|
||||
defaultStyle = defaultStyles.style;
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
edges.forEach(function(edge) {
|
||||
cnt++;
|
||||
const edgeData = {};
|
||||
edgeData.id = 'id' + cnt;
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
edgeData.arrowType = edge.type;
|
||||
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
style = styles.style;
|
||||
labelStyle = styles.labelStyle;
|
||||
} else {
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none';
|
||||
if (typeof defaultStyle !== 'undefined') {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (typeof defaultLabelStyle !== 'undefined') {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
break;
|
||||
case 'dotted':
|
||||
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
break;
|
||||
case 'thick':
|
||||
style = ' stroke-width: 3.5px;fill:none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
edgeData.style = style;
|
||||
edgeData.labelStyle = labelStyle;
|
||||
|
||||
if (typeof edge.interpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear);
|
||||
} else if (typeof edges.defaultInterpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear);
|
||||
} else {
|
||||
edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear);
|
||||
}
|
||||
|
||||
if (typeof edge.text === 'undefined') {
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
edgeData.label = edge.text.replace(/<br\s*\/?>/gi, '\n');
|
||||
|
||||
if (typeof edge.style === 'undefined') {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
}
|
||||
}
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.start, edge.end, edgeData, cnt);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
* @returns {object} classDef styles
|
||||
*/
|
||||
export const getClasses = function(text) {
|
||||
logger.info('Extracting classes');
|
||||
flowDb.clear();
|
||||
const parser = flow.parser;
|
||||
parser.yy = flowDb;
|
||||
|
||||
// Parse the graph definition
|
||||
parser.parse(text);
|
||||
return flowDb.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
* @param text
|
||||
* @param id
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing flowchart');
|
||||
flowDb.clear();
|
||||
const parser = flow.parser;
|
||||
parser.yy = flowDb;
|
||||
|
||||
// Parse the graph definition
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = flowDb.getDirection();
|
||||
if (typeof dir === 'undefined') {
|
||||
dir = 'TD';
|
||||
}
|
||||
|
||||
const conf = getConfig().flowchart;
|
||||
const nodeSpacing = conf.nodeSpacing || 50;
|
||||
const rankSpacing = conf.rankSpacing || 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: dir,
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
let subG;
|
||||
const subGraphs = flowDb.getSubGraphs();
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
||||
}
|
||||
|
||||
// Fetch the verices/nodes and edges/links from the parsed graph definition
|
||||
const vert = flowDb.getVertices();
|
||||
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
logger.info(edges);
|
||||
let i = 0;
|
||||
for (i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
d3.selectAll('cluster').append('text');
|
||||
|
||||
for (let j = 0; j < subG.nodes.length; j++) {
|
||||
g.setParent(subG.nodes[j], subG.id);
|
||||
}
|
||||
}
|
||||
addVertices(vert, g, id);
|
||||
addEdges(edges, g);
|
||||
|
||||
// Add custom shapes
|
||||
// flowChartShapes.addToRenderV2(addShape);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
const svg = d3.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = d3.select('#' + id + ' g');
|
||||
render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);
|
||||
dagre.layout(g);
|
||||
|
||||
element.selectAll('g.node').attr('title', function() {
|
||||
return flowDb.getTooltip(this.id);
|
||||
});
|
||||
|
||||
const padding = 8;
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + 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) {
|
||||
svg.attr('width', '100%');
|
||||
svg.attr('style', `max-width: ${width}px;`);
|
||||
} else {
|
||||
svg.attr('height', height);
|
||||
svg.attr('width', width);
|
||||
}
|
||||
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
svg
|
||||
.select('g')
|
||||
.attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`);
|
||||
|
||||
// Index nodes
|
||||
flowDb.indexNodes('subGraph' + i);
|
||||
|
||||
// reposition labels
|
||||
for (i = 0; i < subGraphs.length; i++) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
if (subG.title !== 'undefined') {
|
||||
const clusterRects = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"] rect');
|
||||
const clusterEl = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"]');
|
||||
|
||||
const xPos = clusterRects[0].x.baseVal.value;
|
||||
const yPos = clusterRects[0].y.baseVal.value;
|
||||
const width = clusterRects[0].width.baseVal.value;
|
||||
const cluster = d3.select(clusterEl[0]);
|
||||
const te = cluster.select('.label');
|
||||
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
|
||||
te.attr('id', id + 'Text');
|
||||
|
||||
for (let j = 0; j < subG.classes.length; j++) {
|
||||
clusterEl[0].classList.add(subG.classes[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (let k = 0; k < labels.length; k++) {
|
||||
const label = labels[k];
|
||||
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
rect.setAttribute('style', 'fill:#e8e8e8;');
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
const keys = Object.keys(vert);
|
||||
keys.forEach(function(key) {
|
||||
const vertex = vert[key];
|
||||
|
||||
if (vertex.link) {
|
||||
const node = d3.select('#' + id + ' [id="' + key + '"]');
|
||||
if (node) {
|
||||
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
|
||||
const linkNode = node.insert(function() {
|
||||
return link;
|
||||
}, ':first-child');
|
||||
|
||||
const shape = node.select('.label-container');
|
||||
if (shape) {
|
||||
linkNode.append(function() {
|
||||
return shape.node();
|
||||
});
|
||||
}
|
||||
|
||||
const label = node.select('.label');
|
||||
if (label) {
|
||||
linkNode.append(function() {
|
||||
return label.node();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
addVertices,
|
||||
addEdges,
|
||||
getClasses,
|
||||
draw
|
||||
};
|
@ -23,7 +23,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -59,7 +59,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -77,7 +77,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -95,7 +95,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -110,7 +110,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -125,7 +125,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -140,7 +140,7 @@ describe('[Arrows] when parsing', () => {
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[1].start).toBe('B');
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -38,7 +38,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -53,7 +53,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -68,7 +68,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -98,7 +98,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -113,7 +113,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -128,7 +128,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
@ -145,7 +145,7 @@ describe('[Comments] when parsing', () => {
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
});
|
||||
|
@ -19,7 +19,7 @@ describe('[Text] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges.length).toBe(47917);
|
||||
console.log(vert);
|
||||
expect(Object.keys(vert).length).toBe(2);
|
||||
|
@ -273,7 +273,7 @@ describe('[Style] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle multi-numbered style definitons with more then 1 digit in a row', function() {
|
||||
@ -297,7 +297,7 @@ describe('[Style] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle classDefs with style in classes', function() {
|
||||
@ -306,7 +306,7 @@ describe('[Style] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle classDefs with % in classes', function() {
|
||||
@ -317,6 +317,6 @@ describe('[Style] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
});
|
||||
|
@ -74,7 +74,7 @@ describe('[Text] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('text including URL space and send');
|
||||
});
|
||||
it('should handle space and send', function() {
|
||||
@ -83,7 +83,7 @@ describe('[Text] when parsing', () => {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('text including URL space and send');
|
||||
});
|
||||
|
||||
@ -380,7 +380,7 @@ describe('[Text] when parsing', () => {
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow_circle');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(vert['C'].id).toBe('C');
|
||||
|
@ -27,11 +27,11 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('B');
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
});
|
||||
it('should handle chaining of vertices', function() {
|
||||
@ -49,11 +49,11 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('C');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('B');
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
});
|
||||
it('should multiple vertices in link statement in the begining', function() {
|
||||
@ -71,11 +71,11 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('A');
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
});
|
||||
it('should multiple vertices in link statement at the end', function() {
|
||||
@ -94,19 +94,19 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(4);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('C');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('A');
|
||||
expect(edges[1].end).toBe('D');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
expect(edges[2].start).toBe('B');
|
||||
expect(edges[2].end).toBe('C');
|
||||
expect(edges[2].type).toBe('arrow');
|
||||
expect(edges[2].type).toBe('arrow_point');
|
||||
expect(edges[2].text).toBe('');
|
||||
expect(edges[3].start).toBe('B');
|
||||
expect(edges[3].end).toBe('D');
|
||||
expect(edges[3].type).toBe('arrow');
|
||||
expect(edges[3].type).toBe('arrow_point');
|
||||
expect(edges[3].text).toBe('');
|
||||
});
|
||||
it('should handle chaining of vertices at both ends at once', function() {
|
||||
@ -125,19 +125,19 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(4);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('C');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('A');
|
||||
expect(edges[1].end).toBe('D');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
expect(edges[2].start).toBe('B');
|
||||
expect(edges[2].end).toBe('C');
|
||||
expect(edges[2].type).toBe('arrow');
|
||||
expect(edges[2].type).toBe('arrow_point');
|
||||
expect(edges[2].text).toBe('');
|
||||
expect(edges[3].start).toBe('B');
|
||||
expect(edges[3].end).toBe('D');
|
||||
expect(edges[3].type).toBe('arrow');
|
||||
expect(edges[3].type).toBe('arrow_point');
|
||||
expect(edges[3].text).toBe('');
|
||||
});
|
||||
it('should handle chaining and multiple nodes in in link statement FVC ', function() {
|
||||
@ -157,27 +157,27 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(6);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[1].start).toBe('A');
|
||||
expect(edges[1].end).toBe('B2');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('');
|
||||
expect(edges[2].start).toBe('A');
|
||||
expect(edges[2].end).toBe('C');
|
||||
expect(edges[2].type).toBe('arrow');
|
||||
expect(edges[2].type).toBe('arrow_point');
|
||||
expect(edges[2].text).toBe('');
|
||||
expect(edges[3].start).toBe('B');
|
||||
expect(edges[3].end).toBe('D2');
|
||||
expect(edges[3].type).toBe('arrow');
|
||||
expect(edges[3].type).toBe('arrow_point');
|
||||
expect(edges[3].text).toBe('');
|
||||
expect(edges[4].start).toBe('B2');
|
||||
expect(edges[4].end).toBe('D2');
|
||||
expect(edges[4].type).toBe('arrow');
|
||||
expect(edges[4].type).toBe('arrow_point');
|
||||
expect(edges[4].text).toBe('');
|
||||
expect(edges[5].start).toBe('C');
|
||||
expect(edges[5].end).toBe('D2');
|
||||
expect(edges[5].type).toBe('arrow');
|
||||
expect(edges[5].type).toBe('arrow_point');
|
||||
expect(edges[5].text).toBe('');
|
||||
});
|
||||
it('should handle chaining and multiple nodes in in link statement with extra info in statements', function() {
|
||||
@ -203,19 +203,19 @@ describe('when parsing flowcharts', function() {
|
||||
expect(edges.length).toBe(4);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('hello');
|
||||
expect(edges[1].start).toBe('A');
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[1].type).toBe('arrow');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('hello');
|
||||
expect(edges[2].start).toBe('B');
|
||||
expect(edges[2].end).toBe('D');
|
||||
expect(edges[2].type).toBe('arrow');
|
||||
expect(edges[2].type).toBe('arrow_point');
|
||||
expect(edges[2].text).toBe('');
|
||||
expect(edges[3].start).toBe('C');
|
||||
expect(edges[3].end).toBe('D');
|
||||
expect(edges[3].type).toBe('arrow');
|
||||
expect(edges[3].type).toBe('arrow_point');
|
||||
expect(edges[3].text).toBe('');
|
||||
});
|
||||
});
|
||||
|
@ -10,8 +10,7 @@
|
||||
%x dir
|
||||
%x vertex
|
||||
%%
|
||||
\%\%[^\n]*\n* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
\%\%[^\n]*\n* /* do nothing */
|
||||
["] this.begin("string");
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
@ -22,7 +21,8 @@
|
||||
"classDef" return 'CLASSDEF';
|
||||
"class" return 'CLASS';
|
||||
"click" return 'CLICK';
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"subgraph" return 'subgraph';
|
||||
"end"\b\s* return 'end';
|
||||
<dir>\s*"LR" { this.popState(); return 'DIR'; }
|
||||
|
@ -23,7 +23,7 @@ describe('when parsing ', function() {
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
expect(edges[0].text).toBe('');
|
||||
});
|
||||
|
||||
|
@ -93,7 +93,7 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with title in quotes', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph "title in quotes";c-->d;end;');
|
||||
@ -107,7 +107,7 @@ describe('when parsing subgraphs', function() {
|
||||
|
||||
expect(subgraph.title).toBe('title in quotes');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs in old style that was broken', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph old style that is broken;c-->d;end;');
|
||||
@ -121,7 +121,7 @@ describe('when parsing subgraphs', function() {
|
||||
|
||||
expect(subgraph.title).toBe('old style that is broken');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with dashes in the title', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph a-b-c;c-->d;end;');
|
||||
@ -135,7 +135,7 @@ describe('when parsing subgraphs', function() {
|
||||
|
||||
expect(subgraph.title).toBe('a-b-c');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with id and title in brackets', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph uid1[text of doom];c-->d;end;');
|
||||
@ -150,7 +150,7 @@ describe('when parsing subgraphs', function() {
|
||||
expect(subgraph.title).toBe('text of doom');
|
||||
expect(subgraph.id).toBe('uid1');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with id and title in brackets and quotes', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph uid2["text of doom"];c-->d;end;');
|
||||
@ -165,7 +165,7 @@ describe('when parsing subgraphs', function() {
|
||||
expect(subgraph.title).toBe('text of doom');
|
||||
expect(subgraph.id).toBe('uid2');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with id and title in brackets without spaces', function() {
|
||||
const res = flow.parser.parse('graph TD;A-->B;subgraph uid2[textofdoom];c-->d;end;');
|
||||
@ -180,7 +180,7 @@ describe('when parsing subgraphs', function() {
|
||||
expect(subgraph.title).toBe('textofdoom');
|
||||
expect(subgraph.id).toBe('uid2');
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle subgraphs2', function() {
|
||||
@ -189,7 +189,7 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle subgraphs3', function() {
|
||||
@ -198,7 +198,7 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle nested subgraphs', function() {
|
||||
@ -219,7 +219,7 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
|
||||
it('should handle subgraphs5', function() {
|
||||
@ -228,7 +228,7 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
it('should handle subgraphs with multi node statements in it', function() {
|
||||
const res = flow.parser.parse('graph TD\nA-->B\nsubgraph myTitle\na & b --> c & e\n end;');
|
||||
@ -236,6 +236,6 @@ describe('when parsing subgraphs', function() {
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].type).toBe('arrow');
|
||||
expect(edges[0].type).toBe('arrow_point');
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,6 @@
|
||||
\s+ /* skip whitespace */
|
||||
\#[^\n]* /* skip comments */
|
||||
\%%[^\n]* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
|
@ -18,7 +18,6 @@
|
||||
\s+ /* skip all whitespace */
|
||||
\#[^\n]* /* skip comments */
|
||||
\%%[^\n]* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
"gitGraph" return 'GG';
|
||||
"commit" return 'COMMIT';
|
||||
"branch" return 'BRANCH';
|
||||
|
@ -12,8 +12,7 @@
|
||||
%}
|
||||
|
||||
%%
|
||||
\%\%[^\n]* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
\%\%[^\n]* /* do nothing */
|
||||
\s+ /* skip whitespace */
|
||||
"pie" return 'pie' ;
|
||||
[\s\n\r]+ return 'NL' ;
|
||||
|
@ -26,7 +26,6 @@
|
||||
<ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */
|
||||
<INITIAL,ID,ALIAS,LINE>\#[^\n]* /* skip comments */
|
||||
\%%[^\n]* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
"participant" { this.begin('ID'); return 'participant'; }
|
||||
<ID>[^\->:\n,;]+?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
||||
|
@ -24,6 +24,8 @@ const conf = {
|
||||
noteMargin: 10,
|
||||
// Space between messages
|
||||
messageMargin: 35,
|
||||
// Multiline message alignment
|
||||
messageAlign: 'center',
|
||||
// mirror actors under diagram
|
||||
mirrorActors: false,
|
||||
// Depending on css styling this might need adjustment
|
||||
@ -230,24 +232,38 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
||||
const g = elem.append('g');
|
||||
const txtCenter = startx + (stopx - startx) / 2;
|
||||
|
||||
let textElem;
|
||||
let textElems = [];
|
||||
let counterBreaklines = 0;
|
||||
let breaklineOffset = 17;
|
||||
const breaklines = msg.message.split(/<br\s*\/?>/gi);
|
||||
for (const breakline of breaklines) {
|
||||
textElem = g
|
||||
.append('text') // text label for the x axis
|
||||
.attr('x', txtCenter)
|
||||
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('class', 'messageText')
|
||||
.text(breakline.trim());
|
||||
textElems.push(
|
||||
g
|
||||
.append('text') // text label for the x axis
|
||||
.attr('x', txtCenter)
|
||||
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('class', 'messageText')
|
||||
.text(breakline.trim())
|
||||
);
|
||||
counterBreaklines++;
|
||||
}
|
||||
const offsetLineCounter = counterBreaklines - 1;
|
||||
const totalOffset = offsetLineCounter * breaklineOffset;
|
||||
|
||||
let textWidth = (textElem._groups || textElem)[0][0].getBBox().width;
|
||||
let textWidths = textElems.map(function(textElem) {
|
||||
return (textElem._groups || textElem)[0][0].getBBox().width;
|
||||
});
|
||||
let textWidth = Math.max(...textWidths);
|
||||
for (const textElem of textElems) {
|
||||
if (conf.messageAlign === 'left') {
|
||||
textElem.attr('x', txtCenter - textWidth / 2).style('text-anchor', 'start');
|
||||
} else if (conf.messageAlign === 'right') {
|
||||
textElem.attr('x', txtCenter + textWidth / 2).style('text-anchor', 'end');
|
||||
}
|
||||
}
|
||||
|
||||
bounds.bumpVerticalPos(totalOffset);
|
||||
|
||||
let line;
|
||||
if (startx === stopx) {
|
||||
@ -295,9 +311,9 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
||||
} else {
|
||||
line = g.append('line');
|
||||
line.attr('x1', startx);
|
||||
line.attr('y1', verticalPos);
|
||||
line.attr('y1', verticalPos + totalOffset);
|
||||
line.attr('x2', stopx);
|
||||
line.attr('y2', verticalPos);
|
||||
line.attr('y2', verticalPos + totalOffset);
|
||||
bounds.insert(
|
||||
startx,
|
||||
bounds.getVerticalPos() - 10 + totalOffset,
|
||||
|
@ -33,11 +33,10 @@
|
||||
%%
|
||||
|
||||
[\n]+ return 'NL';
|
||||
\s+ /* skip all whitespace */
|
||||
\s+ /* skip all whitespace */
|
||||
<ID,STATE,struct,LINE>((?!\n)\s)+ /* skip same-line whitespace */
|
||||
<INITIAL,ID,STATE,struct,LINE>\#[^\n]* /* skip comments */
|
||||
\%%[^\n]* /* skip comments */
|
||||
\%\%\*((.|\n)*)\*\%\% /* multiline skip comments */
|
||||
\%%[^\n]* /* skip comments */
|
||||
|
||||
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
|
||||
<SCALE>\d+ return 'WIDTH';
|
||||
@ -72,6 +71,7 @@
|
||||
<NOTE_TEXT>\s*[^:;]+"end note" { this.popState();/*console.log('Got NOTE_TEXT for note',yytext);*/yytext = yytext.slice(0,-8).trim();return 'NOTE_TEXT';}
|
||||
|
||||
"stateDiagram"\s+ { /*console.log('Got state diagram', yytext,'#');*/return 'SD'; }
|
||||
"stateDiagram-v2"\s+ { /*console.log('Got state diagram', yytext,'#');*/return 'SD'; }
|
||||
"hide empty description" { /*console.log('HIDE_EMPTY', yytext,'#');*/return 'HIDE_EMPTY'; }
|
||||
<INITIAL,struct>"[*]" { /*console.log('EDGE_STATE=',yytext);*/ return 'EDGE_STATE';}
|
||||
<INITIAL,struct>[^:\n\s\-\{]+ { /*console.log('=>ID=',yytext);*/ return 'ID';}
|
||||
@ -114,7 +114,7 @@ line
|
||||
|
||||
statement
|
||||
: idStatement { /*console.warn('got id and descr', $1);*/$$={ stmt: 'state', id: $1, type: 'default', description: ''};}
|
||||
| idStatement DESCR { /*console.warn('got id and descr', $1, $2.trim());*/$$={ stmt: 'state', id: $1, type: 'default', description: $2.trim()};}
|
||||
| idStatement DESCR { /*console.warn('got id and descr', $1, $2.trim());*/$$={ stmt: 'state', id: $1, type: 'default', description: yy.trimColon($2)};}
|
||||
| idStatement '-->' idStatement
|
||||
{
|
||||
/*console.warn('got id', $1);yy.addRelation($1, $3);*/
|
||||
|
@ -4,6 +4,7 @@ import stateDb from './stateDb';
|
||||
import utils from '../../utils';
|
||||
import common from '../common/common';
|
||||
import { getConfig } from '../../config';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
// let conf;
|
||||
|
||||
@ -456,6 +457,9 @@ export const drawEdge = function(elem, path, relation) {
|
||||
|
||||
let titleHeight = 0;
|
||||
const titleRows = [];
|
||||
let maxWidth = 0;
|
||||
let minX = 0;
|
||||
|
||||
for (let i = 0; i <= rows.length; i++) {
|
||||
const title = label
|
||||
.append('text')
|
||||
@ -464,27 +468,39 @@ export const drawEdge = function(elem, path, relation) {
|
||||
.attr('x', x)
|
||||
.attr('y', y + titleHeight);
|
||||
|
||||
const boundstmp = title.node().getBBox();
|
||||
maxWidth = Math.max(maxWidth, boundstmp.width);
|
||||
minX = Math.min(minX, boundstmp.x);
|
||||
|
||||
logger.info(boundstmp.x, x, y + titleHeight);
|
||||
|
||||
if (titleHeight === 0) {
|
||||
const titleBox = title.node().getBBox();
|
||||
titleHeight = titleBox.height;
|
||||
logger.info('Title height', titleHeight, y);
|
||||
}
|
||||
titleRows.push(title);
|
||||
}
|
||||
|
||||
let boxHeight = titleHeight * rows.length;
|
||||
if (rows.length > 1) {
|
||||
const heightAdj = rows.length * titleHeight * 0.25;
|
||||
const heightAdj = (rows.length - 1) * titleHeight * 0.5;
|
||||
|
||||
titleRows.forEach((title, i) => title.attr('y', y + i * titleHeight - heightAdj));
|
||||
boxHeight = titleHeight * rows.length;
|
||||
}
|
||||
|
||||
const bounds = label.node().getBBox();
|
||||
|
||||
label
|
||||
.insert('rect', ':first-child')
|
||||
.attr('class', 'box')
|
||||
.attr('x', bounds.x - getConfig().state.padding / 2)
|
||||
.attr('y', bounds.y - getConfig().state.padding / 2)
|
||||
.attr('width', bounds.width + getConfig().state.padding)
|
||||
.attr('height', bounds.height + getConfig().state.padding);
|
||||
.attr('x', x - maxWidth / 2 - getConfig().state.padding / 2)
|
||||
.attr('y', y - boxHeight / 2 - getConfig().state.padding / 2 - 3.5)
|
||||
.attr('width', maxWidth + getConfig().state.padding)
|
||||
.attr('height', boxHeight + getConfig().state.padding);
|
||||
|
||||
logger.info(bounds);
|
||||
|
||||
//label.attr('transform', '0 -' + (bounds.y / 2));
|
||||
|
||||
|
@ -3,13 +3,41 @@ import { logger } from '../../logger';
|
||||
let rootDoc = [];
|
||||
const setRootDoc = o => {
|
||||
logger.info('Setting root doc', o);
|
||||
// rootDoc = { id: 'root', doc: o };
|
||||
rootDoc = o;
|
||||
};
|
||||
|
||||
const getRootDoc = () => rootDoc;
|
||||
|
||||
const docTranslator = (parent, node, first) => {
|
||||
if (node.stmt === 'relation') {
|
||||
docTranslator(parent, node.state1, true);
|
||||
docTranslator(parent, node.state2, false);
|
||||
} else {
|
||||
if (node.stmt === 'state') {
|
||||
if (node.id === '[*]') {
|
||||
node.id = first ? parent.id + '_start' : parent.id + '_end';
|
||||
node.start = first;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.doc) {
|
||||
node.doc.forEach(docNode => docTranslator(node, docNode, true));
|
||||
}
|
||||
}
|
||||
};
|
||||
const getRootDocV2 = () => {
|
||||
docTranslator({ id: 'root' }, { id: 'root', doc: rootDoc }, true);
|
||||
return { id: 'root', doc: rootDoc };
|
||||
};
|
||||
|
||||
const extract = doc => {
|
||||
// const res = { states: [], relations: [] };
|
||||
// let doc = root.doc;
|
||||
// if (!doc) {
|
||||
// doc = root;
|
||||
// }
|
||||
logger.info(doc);
|
||||
clear();
|
||||
|
||||
doc.forEach(item => {
|
||||
@ -145,6 +173,12 @@ const getDividerId = () => {
|
||||
return 'divider-id-' + dividerCnt;
|
||||
};
|
||||
|
||||
const classes = [];
|
||||
|
||||
const getClasses = () => classes;
|
||||
|
||||
const getDirection = () => 'TB';
|
||||
|
||||
export const relationType = {
|
||||
AGGREGATION: 0,
|
||||
EXTENSION: 1,
|
||||
@ -152,12 +186,16 @@ export const relationType = {
|
||||
DEPENDENCY: 3
|
||||
};
|
||||
|
||||
const trimColon = str => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
|
||||
|
||||
export default {
|
||||
addState,
|
||||
clear,
|
||||
getState,
|
||||
getStates,
|
||||
getRelations,
|
||||
getClasses,
|
||||
getDirection,
|
||||
addRelation,
|
||||
getDividerId,
|
||||
// addDescription,
|
||||
@ -167,5 +205,7 @@ export default {
|
||||
logDocuments,
|
||||
getRootDoc,
|
||||
setRootDoc,
|
||||
extract
|
||||
getRootDocV2,
|
||||
extract,
|
||||
trimColon
|
||||
};
|
||||
|
304
src/diagrams/state/stateRenderer-v2.js
Normal file
304
src/diagrams/state/stateRenderer-v2.js
Normal file
@ -0,0 +1,304 @@
|
||||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
import stateDb from './stateDb';
|
||||
import state from './parser/stateDiagram';
|
||||
import { getConfig } from '../../config';
|
||||
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
const nodeDb = {};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
* @returns {object} classDef styles
|
||||
*/
|
||||
export const getClasses = function(text) {
|
||||
logger.trace('Extracting classes');
|
||||
stateDb.clear();
|
||||
const parser = state.parser;
|
||||
parser.yy = stateDb;
|
||||
|
||||
// Parse the graph definition
|
||||
parser.parse(text);
|
||||
return stateDb.getClasses();
|
||||
};
|
||||
|
||||
const setupNode = (g, parent, node, altFlag) => {
|
||||
// Add the node
|
||||
if (node.id !== 'root') {
|
||||
let shape = 'rect';
|
||||
if (node.start === true) {
|
||||
shape = 'start';
|
||||
}
|
||||
if (node.start === false) {
|
||||
shape = 'end';
|
||||
}
|
||||
|
||||
if (!nodeDb[node.id]) {
|
||||
nodeDb[node.id] = {
|
||||
id: node.id,
|
||||
shape,
|
||||
description: node.id,
|
||||
classes: 'statediagram-state'
|
||||
};
|
||||
}
|
||||
|
||||
// Description
|
||||
if (node.description) {
|
||||
nodeDb[node.id].description = node.description;
|
||||
}
|
||||
|
||||
// Save data for description and group so that for instance a statement without description overwrites
|
||||
// one with description
|
||||
|
||||
// group
|
||||
if (!nodeDb[node.id].type && node.doc) {
|
||||
logger.info('Setting cluser for ', node.id);
|
||||
nodeDb[node.id].type = 'group';
|
||||
nodeDb[node.id].shape = 'roundedWithTitle';
|
||||
nodeDb[node.id].classes =
|
||||
nodeDb[node.id].classes +
|
||||
' ' +
|
||||
(altFlag ? 'statediagram-cluster statediagram-cluster-alt' : 'statediagram-cluster');
|
||||
}
|
||||
|
||||
const nodeData = {
|
||||
labelType: 'svg',
|
||||
labelStyle: '',
|
||||
shape: nodeDb[node.id].shape,
|
||||
label: node.id,
|
||||
labelText: nodeDb[node.id].description,
|
||||
classes: nodeDb[node.id].classes, //classStr,
|
||||
style: '', //styles.style,
|
||||
id: node.id,
|
||||
type: nodeDb[node.id].type,
|
||||
padding: 15 //getConfig().flowchart.padding
|
||||
};
|
||||
|
||||
if (node.note) {
|
||||
// Todo: set random id
|
||||
const noteData = {
|
||||
labelType: 'svg',
|
||||
labelStyle: '',
|
||||
shape: 'note',
|
||||
label: node.id,
|
||||
labelText: node.note.text,
|
||||
classes: 'statediagram-note', //classStr,
|
||||
style: '', //styles.style,
|
||||
id: node.id + '----note',
|
||||
type: nodeDb[node.id].type,
|
||||
padding: 15 //getConfig().flowchart.padding
|
||||
};
|
||||
const groupData = {
|
||||
labelType: 'svg',
|
||||
labelStyle: '',
|
||||
shape: 'noteGroup',
|
||||
label: node.id + '----parent',
|
||||
labelText: node.note.text,
|
||||
classes: nodeDb[node.id].classes, //classStr,
|
||||
style: '', //styles.style,
|
||||
id: node.id + '----parent',
|
||||
type: 'group',
|
||||
padding: 0 //getConfig().flowchart.padding
|
||||
};
|
||||
g.setNode(node.id + '----parent', groupData);
|
||||
|
||||
g.setNode(noteData.id, noteData);
|
||||
g.setNode(node.id, nodeData);
|
||||
|
||||
g.setParent(node.id, node.id + '----parent');
|
||||
g.setParent(noteData.id, node.id + '----parent');
|
||||
|
||||
let from = node.id;
|
||||
let to = noteData.id;
|
||||
|
||||
if (node.note.position === 'left of') {
|
||||
from = noteData.id;
|
||||
to = node.id;
|
||||
}
|
||||
g.setEdge(from, to, {
|
||||
arrowhead: 'none',
|
||||
arrowType: '',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
classes: 'note-edge',
|
||||
arrowheadStyle: 'fill: #333',
|
||||
labelpos: 'c',
|
||||
labelType: 'text',
|
||||
label: ''
|
||||
});
|
||||
} else {
|
||||
g.setNode(node.id, nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
if (parent.id !== 'root') {
|
||||
logger.trace('Setting node ', node.id, ' to be child of its parent ', parent.id);
|
||||
g.setParent(node.id, parent.id);
|
||||
}
|
||||
}
|
||||
if (node.doc) {
|
||||
logger.trace('Adding nodes children ');
|
||||
setupDoc(g, node, node.doc, !altFlag);
|
||||
}
|
||||
};
|
||||
let cnt = 0;
|
||||
const setupDoc = (g, parent, doc, altFlag) => {
|
||||
logger.trace('items', doc);
|
||||
doc.forEach(item => {
|
||||
if (item.stmt === 'state' || item.stmt === 'default') {
|
||||
setupNode(g, parent, item, altFlag);
|
||||
} else if (item.stmt === 'relation') {
|
||||
setupNode(g, parent, item.state1, altFlag);
|
||||
setupNode(g, parent, item.state2, altFlag);
|
||||
const edgeData = {
|
||||
arrowhead: 'normal',
|
||||
arrowType: 'arrow_barb',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
arrowheadStyle: 'fill: #333',
|
||||
labelpos: 'c',
|
||||
labelType: 'text',
|
||||
label: ''
|
||||
};
|
||||
let startId = item.state1.id;
|
||||
let endId = item.state2.id;
|
||||
|
||||
g.setEdge(startId, endId, edgeData, cnt);
|
||||
cnt++;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
* @param text
|
||||
* @param id
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing state diagram (v2)', id);
|
||||
stateDb.clear();
|
||||
const parser = state.parser;
|
||||
parser.yy = stateDb;
|
||||
|
||||
// Parse the graph definition
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = stateDb.getDirection();
|
||||
if (typeof dir === 'undefined') {
|
||||
dir = 'LR';
|
||||
}
|
||||
|
||||
const conf = getConfig().state;
|
||||
const nodeSpacing = conf.nodeSpacing || 50;
|
||||
const rankSpacing = conf.rankSpacing || 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: 'LR',
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
// logger.info(stateDb.getRootDoc());
|
||||
stateDb.extract(stateDb.getRootDocV2().doc);
|
||||
logger.info(stateDb.getRootDocV2());
|
||||
setupNode(g, undefined, stateDb.getRootDocV2(), true);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
const svg = d3.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = d3.select('#' + id + ' g');
|
||||
render(element, g, ['barb'], 'statediagram', id);
|
||||
|
||||
const padding = 8;
|
||||
// const svgBounds = svg.node().getBBox();
|
||||
// const width = svgBounds.width + 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) {
|
||||
// svg.attr('width', '100%');
|
||||
// svg.attr('style', `max-width: ${width}px;`);
|
||||
// } else {
|
||||
// svg.attr('height', height);
|
||||
// svg.attr('width', width);
|
||||
// }
|
||||
|
||||
// svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
// svg
|
||||
// .select('g')
|
||||
// .attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`);
|
||||
|
||||
const bounds = svg.node().getBBox();
|
||||
|
||||
const width = bounds.width + padding * 2;
|
||||
const height = bounds.height + padding * 2;
|
||||
|
||||
// diagram.attr('height', '100%');
|
||||
// diagram.attr('style', `width: ${bounds.width * 3 + conf.padding * 2};`);
|
||||
// diagram.attr('height', height);
|
||||
|
||||
// Zoom in a bit
|
||||
svg.attr('width', width * 1.75);
|
||||
svg.attr('class', 'statediagram');
|
||||
// diagram.attr('height', bounds.height * 3 + conf.padding * 2);
|
||||
svg.attr(
|
||||
'viewBox',
|
||||
`${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height
|
||||
);
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (let k = 0; k < labels.length; k++) {
|
||||
const label = labels[k];
|
||||
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
rect.setAttribute('style', 'fill:#e8e8e8;');
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
getClasses,
|
||||
draw
|
||||
};
|
@ -75,10 +75,6 @@ export const draw = function(text, id) {
|
||||
const width = bounds.width + padding * 2;
|
||||
const height = bounds.height + padding * 2;
|
||||
|
||||
// diagram.attr('height', '100%');
|
||||
// diagram.attr('style', `width: ${bounds.width * 3 + conf.padding * 2};`);
|
||||
// diagram.attr('height', height);
|
||||
|
||||
// Zoom in a bit
|
||||
diagram.attr('width', width * 1.75);
|
||||
// diagram.attr('height', bounds.height * 3 + conf.padding * 2);
|
||||
@ -86,15 +82,6 @@ export const draw = function(text, id) {
|
||||
'viewBox',
|
||||
`${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height
|
||||
);
|
||||
// diagram.attr('transform', `translate(, 0)`);
|
||||
|
||||
// diagram.attr(
|
||||
// 'viewBox',
|
||||
// `${conf.padding * -1} ${conf.padding * -1} ` +
|
||||
// (bounds.width * 1.5 + conf.padding * 2) +
|
||||
// ' ' +
|
||||
// (bounds.height + conf.padding * 5)
|
||||
// );
|
||||
};
|
||||
const getLabelWidth = text => {
|
||||
return text ? text.length * conf.fontSizeFactor : 1;
|
||||
|
@ -17,6 +17,7 @@ export const logger = {
|
||||
};
|
||||
|
||||
export const setLogLevel = function(level) {
|
||||
logger.trace = () => {};
|
||||
logger.debug = () => {};
|
||||
logger.info = () => {};
|
||||
logger.warn = () => {};
|
||||
|
@ -6,7 +6,6 @@ import he from 'he';
|
||||
|
||||
import mermaidAPI from './mermaidAPI';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* ## init
|
||||
* Function that goes through the document to find the chart definitions in there and render them.
|
||||
|
@ -17,6 +17,7 @@ import { setConfig, getConfig } from './config';
|
||||
import { logger, setLogLevel } from './logger';
|
||||
import utils from './utils';
|
||||
import flowRenderer from './diagrams/flowchart/flowRenderer';
|
||||
import flowRendererV2 from './diagrams/flowchart/flowRenderer-v2';
|
||||
import flowParser from './diagrams/flowchart/parser/flow';
|
||||
import flowDb from './diagrams/flowchart/flowDb';
|
||||
import sequenceRenderer from './diagrams/sequence/sequenceRenderer';
|
||||
@ -29,6 +30,7 @@ import classRenderer from './diagrams/class/classRenderer';
|
||||
import classParser from './diagrams/class/parser/classDiagram';
|
||||
import classDb from './diagrams/class/classDb';
|
||||
import stateRenderer from './diagrams/state/stateRenderer';
|
||||
import stateRendererV2 from './diagrams/state/stateRenderer-v2';
|
||||
import stateParser from './diagrams/state/parser/stateDiagram';
|
||||
import stateDb from './diagrams/state/stateDb';
|
||||
import gitGraphRenderer from './diagrams/git/gitGraphRenderer';
|
||||
@ -40,6 +42,9 @@ import infoDb from './diagrams/info/infoDb';
|
||||
import pieRenderer from './diagrams/pie/pieRenderer';
|
||||
import pieParser from './diagrams/pie/parser/pie';
|
||||
import pieDb from './diagrams/pie/pieDb';
|
||||
import erDb from './diagrams/er/erDb';
|
||||
import erParser from './diagrams/er/parser/erDiagram';
|
||||
import erRenderer from './diagrams/er/erRenderer';
|
||||
|
||||
const themes = {};
|
||||
for (const themeName of ['default', 'forest', 'dark', 'neutral']) {
|
||||
@ -164,7 +169,10 @@ const config = {
|
||||
* * linear **default**
|
||||
* * cardinal
|
||||
*/
|
||||
curve: 'linear'
|
||||
curve: 'linear',
|
||||
// Only used in new experimental rendering
|
||||
// repreesents the padding between the labels and the shape
|
||||
padding: 15
|
||||
},
|
||||
|
||||
/**
|
||||
@ -225,6 +233,14 @@ const config = {
|
||||
*/
|
||||
messageMargin: 35,
|
||||
|
||||
/**
|
||||
* Multiline message alignment. Possible values are:
|
||||
* * left
|
||||
* * center **default**
|
||||
* * right
|
||||
*/
|
||||
messageAlign: 'center',
|
||||
|
||||
/**
|
||||
* mirror actors under diagram.
|
||||
* **Default value true**.
|
||||
@ -342,6 +358,52 @@ const config = {
|
||||
edgeLengthFactor: '20',
|
||||
compositTitleSize: 35,
|
||||
radius: 5
|
||||
},
|
||||
|
||||
/**
|
||||
* The object containing configurations specific for entity relationship diagrams
|
||||
*/
|
||||
er: {
|
||||
/**
|
||||
* The amount of padding around the diagram as a whole so that embedded diagrams have margins, expressed in pixels
|
||||
*/
|
||||
diagramPadding: 20,
|
||||
|
||||
/**
|
||||
* Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL',
|
||||
* where T = top, B = bottom, L = left, and R = right.
|
||||
*/
|
||||
layoutDirection: 'TB',
|
||||
|
||||
/**
|
||||
* The mimimum width of an entity box, expressed in pixels
|
||||
*/
|
||||
minEntityWidth: 100,
|
||||
|
||||
/**
|
||||
* The minimum height of an entity box, expressed in pixels
|
||||
*/
|
||||
minEntityHeight: 75,
|
||||
|
||||
/**
|
||||
* The minimum internal padding between the text in an entity box and the enclosing box borders, expressed in pixels
|
||||
*/
|
||||
entityPadding: 15,
|
||||
|
||||
/**
|
||||
* Stroke color of box edges and lines
|
||||
*/
|
||||
stroke: 'gray',
|
||||
|
||||
/**
|
||||
* Fill color of entity boxes
|
||||
*/
|
||||
fill: 'honeydew',
|
||||
|
||||
/**
|
||||
* Font size
|
||||
*/
|
||||
fontSize: '12px'
|
||||
}
|
||||
};
|
||||
|
||||
@ -363,6 +425,11 @@ function parse(text) {
|
||||
parser = flowParser;
|
||||
parser.parser.yy = flowDb;
|
||||
break;
|
||||
case 'flowchart-v2':
|
||||
flowDb.clear();
|
||||
parser = flowRendererV2;
|
||||
parser.parser.yy = flowDb;
|
||||
break;
|
||||
case 'sequence':
|
||||
parser = sequenceParser;
|
||||
parser.parser.yy = sequenceDb;
|
||||
@ -379,6 +446,10 @@ function parse(text) {
|
||||
parser = stateParser;
|
||||
parser.parser.yy = stateDb;
|
||||
break;
|
||||
case 'stateDiagram':
|
||||
parser = stateParser;
|
||||
parser.parser.yy = stateDb;
|
||||
break;
|
||||
case 'info':
|
||||
logger.debug('info info info');
|
||||
parser = infoParser;
|
||||
@ -389,6 +460,11 @@ function parse(text) {
|
||||
parser = pieParser;
|
||||
parser.parser.yy = pieDb;
|
||||
break;
|
||||
case 'er':
|
||||
logger.debug('er');
|
||||
parser = erParser;
|
||||
parser.parser.yy = erDb;
|
||||
break;
|
||||
}
|
||||
|
||||
parser.parser.yy.parseError = (str, hash) => {
|
||||
@ -568,6 +644,11 @@ const render = function(id, _txt, cb, container) {
|
||||
flowRenderer.setConf(config.flowchart);
|
||||
flowRenderer.draw(txt, id, false);
|
||||
break;
|
||||
case 'flowchart-v2':
|
||||
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
flowRendererV2.setConf(config.flowchart);
|
||||
flowRendererV2.draw(txt, id, false);
|
||||
break;
|
||||
case 'sequence':
|
||||
config.sequence.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
if (config.sequenceDiagram) {
|
||||
@ -596,6 +677,11 @@ const render = function(id, _txt, cb, container) {
|
||||
stateRenderer.setConf(config.state);
|
||||
stateRenderer.draw(txt, id);
|
||||
break;
|
||||
case 'stateDiagram':
|
||||
// config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
stateRendererV2.setConf(config.state);
|
||||
stateRendererV2.draw(txt, id);
|
||||
break;
|
||||
case 'info':
|
||||
config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
infoRenderer.setConf(config.class);
|
||||
@ -606,6 +692,10 @@ const render = function(id, _txt, cb, container) {
|
||||
pieRenderer.setConf(config.class);
|
||||
pieRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
case 'er':
|
||||
erRenderer.setConf(config.er);
|
||||
erRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
}
|
||||
|
||||
d3.select(`[id="${id}"]`)
|
||||
@ -738,6 +828,7 @@ export default mermaidAPI;
|
||||
* boxTextMargin:5,
|
||||
* noteMargin:10,
|
||||
* messageMargin:35,
|
||||
* messageAlign:'center',
|
||||
* mirrorActors:true,
|
||||
* bottomMarginAdj:1,
|
||||
* useMaxWidth:true,
|
||||
|
@ -67,3 +67,52 @@ g.stateGroup line {
|
||||
font-family: 'trebuchet ms', verdana, arial;
|
||||
font-family: var(--mermaid-font-family);
|
||||
}
|
||||
|
||||
.node circle.state-start {
|
||||
fill: black;
|
||||
stroke: black;
|
||||
}
|
||||
.node circle.state-end {
|
||||
fill: black;
|
||||
stroke: white;
|
||||
stroke-width: 1.5
|
||||
}
|
||||
#statediagram-barbEnd {
|
||||
fill: $nodeBorder
|
||||
}
|
||||
|
||||
.statediagram-cluster rect {
|
||||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1px;
|
||||
rx: 5px;
|
||||
ry: 5px;
|
||||
}
|
||||
.statediagram-cluster.statediagram-cluster .inner {
|
||||
fill: white;
|
||||
}
|
||||
.statediagram-cluster.statediagram-cluster-alt .inner {
|
||||
fill: #e0e0e0;
|
||||
}
|
||||
|
||||
.statediagram-cluster .inner {
|
||||
rx:0;
|
||||
ry:0;
|
||||
}
|
||||
|
||||
.statediagram-state rect {
|
||||
rx: 5px;
|
||||
ry: 5px;
|
||||
}
|
||||
|
||||
.note-edge {
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
|
||||
.statediagram-note rect {
|
||||
fill: $noteBkgColor;
|
||||
stroke: $noteBorderColor;
|
||||
stroke-width: 1px;
|
||||
rx: 0;
|
||||
ry: 0;
|
||||
}
|
||||
|
10
src/utils.js
10
src/utils.js
@ -33,6 +33,9 @@ export const detectType = function(text) {
|
||||
if (text.match(/^\s*classDiagram/)) {
|
||||
return 'class';
|
||||
}
|
||||
if (text.match(/^\s*stateDiagram-v2/)) {
|
||||
return 'stateDiagram';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*stateDiagram/)) {
|
||||
return 'state';
|
||||
@ -41,6 +44,9 @@ export const detectType = function(text) {
|
||||
if (text.match(/^\s*gitGraph/)) {
|
||||
return 'git';
|
||||
}
|
||||
if (text.match(/^\s*flowchart/)) {
|
||||
return 'flowchart-v2';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*info/)) {
|
||||
return 'info';
|
||||
@ -49,6 +55,10 @@ export const detectType = function(text) {
|
||||
return 'pie';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*erDiagram/)) {
|
||||
return 'er';
|
||||
}
|
||||
|
||||
return 'flowchart';
|
||||
};
|
||||
|
||||
|
@ -1681,9 +1681,9 @@ acorn-walk@^6.0.1:
|
||||
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
|
||||
|
||||
acorn@^5.2.1, acorn@^5.5.3:
|
||||
version "5.7.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
|
||||
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
|
||||
version "5.7.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
||||
|
||||
acorn@^6.0.1, acorn@^6.2.1:
|
||||
version "6.4.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user