Merge remote-tracking branch 'upstream/develop' into feature/user-journey

This commit is contained in:
Russell Geraghty 2020-04-17 07:30:53 +01:00
commit 249a14f84a
22 changed files with 1211 additions and 187 deletions

View File

@ -211,6 +211,7 @@ pie
- [Command Line Interface](https://github.com/mermaid-js/mermaid.cli)
- [Live Editor](https://github.com/mermaid-js/mermaid-live-editor)
- [HTTP Server](https://github.com/TomWright/mermaid-server)
# Contributors [![Help wanted](https://img.shields.io/github/labels/mermaid-js/mermaid/Help%20wanted!)](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Help+wanted%21%22) [![Contributors](https://img.shields.io/github/contributors/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors) [![Commits](https://img.shields.io/github/commit-activity/m/mermaid-js/mermaid)](https://github.com/mermaid-js/mermaid/graphs/contributors)

View File

@ -7,7 +7,7 @@
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap" rel="stylesheet">
<style>
body {
background: white;
background: rgb(221, 208, 208);
font-family: 'Arial';
}
h1 { color: white;}
@ -32,13 +32,31 @@
G-->c
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
flowchart LR
subgraph id1 [Test]
b
end
a-->id1
stateDiagram-v2
[*] --> monkey
state monkey {
Sitting
--
Eating
}
</div>
<div class="mermaid mermaid-apa" style="width: 100%; height: 20%;">
<div class="mermaid2" style="width: 50%; height: 20%;">
stateDiagram-v2
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
</div>
<div class="mermaid2 mermaid-apa" style="width: 100%; height: 20%;">
stateDiagram
[*] --> Still
Still --> [*]
@ -52,15 +70,66 @@
Moving --> Crash
Crash --> [*]
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> First
First --> Second
% First --> Third
state First {
[*] --> fir
fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
</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.
flowchart TD
subgraph A
a
end
subgraph B
b
end
subgraph C
subgraph D
d
end
end
A -- oAo --o B
A --> C
</div>
<div class="mermaid" style="width: 100%; height: 100%;">
flowchart TD
subgraph A
a
end
subgraph B
b
end
c-->A
c-->B
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> First
First --> Second
First --> Third
state First {
[*] --> fir
fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
state Third {
[*] --> thi
thi --> [*]
}
</div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2

View File

@ -16,11 +16,11 @@ Check out the list of [Integrations and Usages of Mermaid](./integrations.md)
**Mermaid was nominated and won the JS Open Source Awards (2019) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
## New diagrams in 8.4
## New diagrams in 8.5
With version 8.4 class diagrams have got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram type, state diagrams.
With version 8.5 there are some bug fixes and enhancements, plus a new diagram type, entity relationship diagrams.
![Image show the two new diagram types](./img/new-diagrams.png)
![Image showing the new ER diagram type](./img/er.png)
## Special note regarding version 8.2
@ -137,6 +137,18 @@ merge newbranch
![Git graph](./img/git.png)
### Entity Relationship Diagram - :exclamation: experimental
```
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
![ER diagram](./img/simple-er.png)
## Installation
### CDN

View File

@ -9,13 +9,13 @@ Mermaid can render ER diagrams
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid.

View File

@ -5,7 +5,7 @@ pie title NETFLIX
"Time spent looking for movie" : 90
"Time spent watching it" : 10
```
``` mermaid
```mermaid
pie title NETFLIX
"Time spent looking for movie" : 90
"Time spent watching it" : 10
@ -35,8 +35,6 @@ sequenceDiagram
Bob-->Alice: Checking with John...
Alice->John: Yes... John, how are you?
```
```mermaid
sequenceDiagram
Alice ->> Bob: Hello Bob, how are you?
@ -49,9 +47,15 @@ sequenceDiagram
Alice->John: Yes... John, how are you?
```
## Basic flowchart
```
graph LR
A[Square Rect] -- Link text --> B((Circle))
A --> C(Round Rect)
B --> D{Rhombus}
C --> D
```
```mermaid
graph LR
A[Square Rect] -- Link text --> B((Circle))
@ -63,6 +67,29 @@ graph LR
## Larger flowchart with some styling
```
graph TB
sq[Square shape] --> ci((Circle shape))
subgraph A
od>Odd shape]-- Two line<br/>edge comment --> ro
di{Diamond with <br/> line break} -.-> ro(Rounded<br>square<br>shape)
di==>ro2(Rounded square shape)
end
%% Notice that no text in shape are added here instead that is appended further down
e --> od3>Really long text with linebreak<br>in an Odd shape]
%% Comments after double percent signs
e((Inner / circle<br>and some odd <br>special characters)) --> f(,.?!+-*ز)
cyr[Cyrillic]-->cyr2((Circle shape Начало));
classDef green fill:#9f6,stroke:#333,stroke-width:2px;
classDef orange fill:#f96,stroke:#333,stroke-width:4px;
class sq,e green
class di orange
```
```mermaid
graph TB
sq[Square shape] --> ci((Circle shape))
@ -90,6 +117,21 @@ graph TB
## Loops, alt and opt
```
sequenceDiagram
loop Daily query
Alice->>Bob: Hello Bob, how are you?
alt is sick
Bob->>Alice: Not so good :(
else is well
Bob->>Alice: Feeling fresh like a daisy
end
opt Extra response
Bob->>Alice: Thanks for asking
end
end
```
```mermaid
sequenceDiagram
loop Daily query
@ -109,6 +151,19 @@ sequenceDiagram
## Message to self in loop
```
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts<br/>prevail...
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
```
```mermaid
sequenceDiagram
participant Alice

BIN
docs/img/er.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/img/simple-er.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -137,3 +137,4 @@ The following is a list of different integrations and plugins where mermaid is b
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
- [Reveal CK](https://github.com/jedcn/reveal-ck)
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)

View File

@ -1,3 +1,55 @@
# Cluster handling
Dagre does not support edges between nodes and clusters or between clusters to other clusters. In order to remedy this shortcoming the dagre wrapper implements a few work-arounds.
In the diagram below there are two clusters and there are no edges to nodes outside the own cluster.
```mermaid
flowchart
subgraph C1
a --> b
end
subgraph C2
c
end
C1 --> C2
```
In this case the dagre-wrapper will transform the graph to the graph below.
```mermaid
flowchart
C1 --> C2
```
The new nodes C1 and C2 are a special type of nodes, clusterNodes. ClusterNodes have have the nodes in the cluster including the cluster attached in a graph object.
When rendering this diagram it it beeing rendered recursivly. The diagram is rendered by the dagre-mermaid:render function which in turn will be used to render the node C1 and the node C2. The result of those renderings will be inserted as nodes in the "root" diagram. With this recursive approach it would be possible to have different layout direction for each cluster.
```
{ clusterNode: true, graph }
```
*Data for a clusterNode*
When a cluster has edges to or from some of its nodes leading outside the cluster the approach of recursive rendering can not be used as the layout of the graph needs to take responsibility for nodes outside of the cluster.
```mermaid
flowchart
subgraph C1
a
end
subgraph C2
b
end
a --> C2
```
To handle this case a special type of edge is inserted. The edge to/from the cluster is replaced with an edge to/from a node in the cluster which is tagged with toCluster/fromCluster. When rendering this edge the intersection between the edge and the border of the cluster is calculated making the edge start/stop there. In practice this renders like an an edge to/from the cluster.
In the diagram above the root diagram would be rendered with C1 whereas C2 would be rendered recursively.
Of these two approaches the top one renders better and is used when possible. When this is not possible, ie an edge is added crossing the border the non recursive approach is used.
# Graph objects and their properties
Explains the representation of various objects used to render the flow charts and what the properties mean. This ofc from the perspective of the dagre-wrapper.
@ -7,12 +59,10 @@ Explains the representation of various objects used to render the flow charts an
Sample object:
```json
{
"labelType":"svg",
"labelStyle":"",
"shape":"rect",
"label":{},
"labelText":"Test",
"rx":0,"ry":0,
"rx":0,
"ry":0,
"class":"default",
"style":"",
"id":"Test",
@ -24,18 +74,16 @@ This is set by the renderer of the diagram and insert the data that the wrapper
| 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 | ?? |
| labelStyle | Css styles for the label. User for instance for stylling the labels for clusters |
| shape | The shape of the node. |
| 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 |
| rx | The corner radius - maybe part of the shape instead? Used for rects. |
| ry | The corner radius - maybe part of the shape instead? Used for rects. |
| classes | Classes to be set for the shape. Not used |
| 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. |
| type | if set to group then this node indicates *a cluster*. |
| padding | Padding. Passed from the render as this might differ between different diagrams. Maybe obsolete. |
# edge

View File

@ -1,8 +1,10 @@
import intersectRect from './intersect/intersect-rect';
import { logger } from '../logger'; // eslint-disable-line
import { logger as log } from '../logger'; // eslint-disable-line
import createLabel from './createLabel';
const rect = (parent, node) => {
log.trace('Creating subgraph rect for ', node.id, node);
// Add outer g element
const shapeSvg = parent
.insert('g')
@ -23,6 +25,7 @@ const rect = (parent, node) => {
const padding = 0 * node.padding;
const halfPadding = padding / 2;
log.trace('Data ', node, JSON.stringify(node));
// center the rect around its coordinate
rect
.attr('rx', node.rx)
@ -32,9 +35,7 @@ const rect = (parent, node) => {
.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(' +
@ -127,9 +128,7 @@ const roundedWithTitle = (parent, node) => {
.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(' +
@ -155,7 +154,9 @@ const shapes = { rect, roundedWithTitle, noteGroup };
let clusterElems = {};
export const insertCluster = (elem, node) => {
clusterElems[node.id] = shapes[node.shape](elem, node);
log.trace('Inserting cluster');
const shape = node.shape || 'rect';
clusterElems[node.id] = shapes[shape](elem, node);
};
export const getClusterTitleWidth = (elem, node) => {
const label = createLabel(node.labelText, node.labelStyle);
@ -170,6 +171,8 @@ export const clear = () => {
};
export const positionCluster = node => {
log.info('Position cluster');
const el = clusterElems[node.id];
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
};

View File

@ -1,8 +1,10 @@
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);
let rows = [];
if (vertexText) {
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');

View File

@ -63,34 +63,17 @@ const outsideNode = (node, point) => {
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);
logger.trace('intersection o:', outsidePoint, ' i:', 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;
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
const dy = Math.abs(y - insidePoint.y);
const h = node.height / 2;
let q = h - dy;
let q = insidePoint.y < outsidePoint.y ? h - dy : h - dy;
const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x);
@ -105,20 +88,20 @@ const intersection = (node, outsidePoint, insidePoint) => {
};
} else {
q = (Q * r) / R;
r = (R * q) / Q;
return {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x + dx - w,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
};
}
};
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]);
logger.trace('edge', edge);
logger.trace('to cluster', clusterDb[edge.toCluster]);
points = [];
let lastPointOutside;
let isInside = false;
@ -126,13 +109,12 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
const node = clusterDb[edge.toCluster].node;
if (!outsideNode(node, point) && !isInside) {
// logger.info('inside', edge.toCluster, point);
logger.trace('inside', edge.toCluster, point, lastPointOutside);
// First point inside the rect
const insterection = intersection(node, lastPointOutside, point);
// logger.info('intersect', inter.rect(node, lastPointOutside));
logger.trace('intersect', insterection);
points.push(insterection);
// points.push(insterection);
isInside = true;
} else {
if (!isInside) points.push(point);
@ -142,8 +124,8 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
}
if (edge.fromCluster) {
// logger.info('edge', edge);
// logger.info('from cluster', clusterDb[edge.toCluster]);
logger.trace('edge', edge);
logger.trace('from cluster', clusterDb[edge.toCluster]);
const updatedPoints = [];
let lastPointOutside;
let isInside = false;
@ -152,17 +134,17 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
const node = clusterDb[edge.fromCluster].node;
if (!outsideNode(node, point) && !isInside) {
// logger.info('inside', edge.toCluster, point);
logger.trace('inside', edge.toCluster, point);
// First point inside the rect
const insterection = intersection(node, lastPointOutside, point);
// logger.info('intersect', intersection(node, lastPointOutside, point));
// logger.trace('intersect', intersection(node, lastPointOutside, point));
updatedPoints.unshift(insterection);
// points.push(insterection);
isInside = true;
} else {
// at the outside
// logger.info('Outside point', point);
logger.trace('Outside point', point);
if (!isInside) updatedPoints.unshift(point);
}
lastPointOutside = point;
@ -170,10 +152,6 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
points = updatedPoints;
}
// logger.info('Poibts', points);
// logger.info('Edge', edge);
// The data for our line
const lineData = points.filter(p => !Number.isNaN(p.y));
@ -216,7 +194,7 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
}
// logger.info('arrowType', edge.arrowType);
logger.info('arrowType', edge.arrowType);
switch (edge.arrowType) {
case 'arrow_cross':
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');

View File

@ -1,45 +1,66 @@
import dagre from 'dagre';
import graphlib from 'graphlib';
import insertMarkers from './markers';
import { insertNode, positionNode, clear as clearNodes } from './nodes';
import { updateNodeBounds } from './shapes/util';
import {
clear as clearGraphlib,
clusterDb,
adjustClustersAndEdges,
findNonClusterChild
} from './mermaid-graphlib';
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes';
import { insertCluster, clear as clearClusters } from './clusters';
import { 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();
import { logger as log } from '../logger';
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
log.trace('Graph in recursive render:', graphlib.json.write(graph), parentCluster);
const elem = _elem.insert('g').attr('class', 'root'); // eslint-disable-line
if (!graph.nodes()) {
log.trace('No nodes found for', graph);
} else {
log.trace('Recursive render', graph.nodes());
}
if (graph.edges().length > 0) {
log.trace('Recursive edges', graph.edge(graph.edges()[0]));
}
const clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line
const 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));
if (typeof parentCluster !== 'undefined') {
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
// data.clusterPositioning = true;
log.trace('Setting data for cluster', data);
graph.setNode(parentCluster.id, data);
graph.setParent(v, parentCluster.id, data);
}
log.trace('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v)));
if (node.clusterNode) {
// const children = graph.children(v);
log.trace('Cluster identified', v, node, graph.node(v));
const newEl = recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
updateNodeBounds(node, newEl);
setNodeElem(newEl, node);
log.warn('Recursive render complete', newEl, node);
} else {
// 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);
if (graph.children(v).length > 0) {
// This is a cluster but not to be rendered recusively
// Render as before
log.trace('Cluster - the non recursive path', v, node.id, node, graph);
log.trace(findNonClusterChild(node.id, graph));
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
// insertCluster(clusters, graph.node(v));
} else {
log.trace('Node - the non recursive path', v, node.id, node);
insertNode(nodes, graph.node(v));
}
}
});
@ -48,49 +69,77 @@ export const render = (elem, graph, markers, diagramtype, id) => {
// Edges from/to clusters really points to the first child in the cluster.
// 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);
}
const edge = graph.edge(e.v, e.w, e.name);
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e)));
// Check if link is either from or to a cluster
log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
insertEdgeLabel(edgeLabels, edge);
});
graph.edges().forEach(function(e) {
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
});
logger.info('#############################################');
logger.info('### Layout ###');
logger.info('#############################################');
logger.info(graph);
log.trace('#############################################');
log.trace('### Layout ###');
log.trace('#############################################');
log.trace(graph);
dagre.layout(graph);
log.warn('Graph after layout:', graphlib.json.write(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') {
// log.trace('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
log.trace(
'Position ' + v + ': (' + node.x,
',' + node.y,
') width: ',
node.width,
' height: ',
node.height
);
if (node && node.clusterNode) {
// clusterDb[node.id].node = node;
positionNode(node);
} else {
insertCluster(clusters, node);
clusterDb[node.id].node = node;
// Non cluster node
if (graph.children(v).length > 0) {
// A cluster in the non-recurive way
// positionCluster(node);
insertCluster(clusters, node);
clusterDb[node.id].node = node;
} else {
positionNode(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);
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
insertEdge(edgePaths, edge, clusterDb, diagramtype);
positionEdgeLabel(edge);
});
return elem;
};
export const render = (elem, graph, markers, diagramtype, id) => {
insertMarkers(elem, markers, diagramtype, id);
clearNodes();
clearEdges();
clearClusters();
clearGraphlib();
log.warn('Graph before:', graphlib.json.write(graph));
adjustClustersAndEdges(graph);
log.warn('Graph after:', graphlib.json.write(graph));
recursiveRender(elem, graph, diagramtype);
};
// const shapeDefinitions = {};

View File

@ -16,7 +16,7 @@ const extension = (elem, type, id) => {
elem
.append('defs')
.append('marker')
.attr('id', 'extensionStart')
.attr('id', type + '-extensionStart')
.attr('class', 'extension ' + type)
.attr('refX', 0)
.attr('refY', 7)
@ -29,7 +29,7 @@ const extension = (elem, type, id) => {
elem
.append('defs')
.append('marker')
.attr('id', 'extensionEnd ' + type)
.attr('id', type + '-extensionEnd ' + type)
.attr('class', 'extension ' + type)
.attr('refX', 19)
.attr('refY', 7)
@ -44,7 +44,7 @@ const composition = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'compositionStart')
.attr('id', type + '-compositionStart')
.attr('class', 'extension ' + type)
.attr('refX', 0)
.attr('refY', 7)
@ -57,7 +57,7 @@ const composition = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'compositionEnd')
.attr('id', type + '-compositionEnd')
.attr('class', 'extension ' + type)
.attr('refX', 19)
.attr('refY', 7)
@ -71,7 +71,7 @@ const aggregation = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'aggregationStart')
.attr('id', type + '-aggregationStart')
.attr('class', 'extension ' + type)
.attr('refX', 0)
.attr('refY', 7)
@ -84,7 +84,7 @@ const aggregation = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'aggregationEnd')
.attr('id', type + '-aggregationEnd')
.attr('class', type)
.attr('refX', 19)
.attr('refY', 7)
@ -98,7 +98,7 @@ const dependency = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'dependencyStart')
.attr('id', type + '-dependencyStart')
.attr('class', 'extension ' + type)
.attr('refX', 0)
.attr('refY', 7)
@ -111,7 +111,7 @@ const dependency = (elem, type) => {
elem
.append('defs')
.append('marker')
.attr('id', 'dependencyEnd')
.attr('id', type + '-dependencyEnd')
.attr('class', type)
.attr('refX', 19)
.attr('refY', 7)
@ -158,7 +158,7 @@ const point = (elem, type) => {
const circle = (elem, type) => {
elem
.append('marker')
.attr('id', 'circleEnd')
.attr('id', type + '-circleEnd')
.attr('class', type)
.attr('viewBox', '0 0 10 10')
.attr('refX', 11)
@ -177,7 +177,7 @@ const circle = (elem, type) => {
elem
.append('marker')
.attr('id', 'circleStart')
.attr('id', type + '-circleStart')
.attr('class', type)
.attr('viewBox', '0 0 10 10')
.attr('refX', -1)
@ -197,7 +197,7 @@ const circle = (elem, type) => {
const cross = (elem, type) => {
elem
.append('marker')
.attr('id', 'crossEnd')
.attr('id', type + '-crossEnd')
.attr('class', type)
.attr('viewBox', '0 0 11 11')
.attr('refX', 12)
@ -215,7 +215,7 @@ const cross = (elem, type) => {
elem
.append('marker')
.attr('id', 'crossStart')
.attr('id', type + '-crossStart')
.attr('class', type)
.attr('viewBox', '0 0 11 11')
.attr('refX', -1)

View File

@ -0,0 +1,372 @@
/**
* Decorates with functions required by mermaids dagre-wrapper.
*/
import { logger as log } from '../logger';
import graphlib from 'graphlib';
export let clusterDb = {};
let decendants = {};
let parents = {};
export const clear = () => {
decendants = {};
parents = {};
clusterDb = {};
};
const isDecendant = (id, ancenstorId) => {
// if (id === ancenstorId) return true;
log.debug(
'In isDecendant',
ancenstorId,
' ',
id,
' = ',
decendants[ancenstorId].indexOf(id) >= 0
);
if (decendants[ancenstorId].indexOf(id) >= 0) return true;
return false;
};
const edgeInCluster = (edge, clusterId) => {
// Edges to/from the cluster is not in the cluster, they are in the parent
if (!(edge.v === clusterId || edge.w === clusterId)) return false;
if (!decendants[clusterId]) {
log.debug('Tilt, ', clusterId, ',not in decendants');
return false;
}
if (decendants[clusterId].indexOf(edge.v) >= 0) return true;
if (isDecendant(edge.v, clusterId)) return true;
if (isDecendant(edge.w, clusterId)) return true;
if (decendants[clusterId].indexOf(edge.w) >= 0) return true;
return false;
};
const copy = (clusterId, graph, newGraph, rootId) => {
log.trace(
'Copying children of ',
clusterId,
rootId,
' from ',
clusterId,
graph.node(clusterId),
rootId
);
const nodes = graph.children(clusterId) || [];
// Include cluster node if it is not the root
if (clusterId !== rootId) {
nodes.push(clusterId);
}
log.debug('Copying (nodes)', nodes);
nodes.forEach(node => {
if (graph.children(node).length > 0) {
copy(node, graph, newGraph, rootId);
} else {
const data = graph.node(node);
log.trace('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
newGraph.setNode(node, data);
if (clusterId !== rootId && node !== clusterId) {
log.debug('Setting parent', node, clusterId);
newGraph.setParent(node, clusterId);
}
const edges = graph.edges(node);
log.debug('Copying Edges', edges);
edges.forEach(edge => {
log.trace('Edge', edge);
const data = graph.edge(edge.v, edge.w, edge.name);
log.trace('Edge data', data, rootId);
try {
// Do not copy edges in and out of the root cluster, they belong to the parent graph
if (edgeInCluster(edge, rootId)) {
log.trace('Copying as ', edge.v, edge.w, data, edge.name);
newGraph.setEdge(edge.v, edge.w, data, edge.name);
log.trace('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
} else {
log.trace('Skipping copy of edge as ', rootId, edge.v, edge.w, clusterId);
}
} catch (e) {
log.error(e);
}
});
}
log.debug('Removing node', node);
graph.removeNode(node);
});
};
export const extractDecendants = (id, graph) => {
// log.debug('Extracting ', id);
const children = graph.children(id);
let res = [].concat(children);
for (let i = 0; i < children.length; i++) {
parents[children[i]] = id;
res = res.concat(extractDecendants(children[i], graph));
}
return res;
};
/**
* Validates the graph, checking that all parent child relation points to existing nodes and that
* edges between nodes also ia correct. When not correct the function logs the discrepancies.
* @param {graphlib graph} g
*/
export const validate = graph => {
const edges = graph.edges();
log.trace('Edges: ', edges);
for (let i = 0; i < edges.length; i++) {
if (graph.children(edges[i].v).length > 0) {
log.trace('The node ', edges[i].v, ' is part of and edge even though it has children');
return false;
}
if (graph.children(edges[i].w).length > 0) {
log.trace('The node ', edges[i].w, ' is part of and edge even though it has children');
return false;
}
}
return true;
};
/**
* Finds a child that is not a cluster. When faking a edge between a node and a cluster.
* @param {Finds a } id
* @param {*} graph
*/
export const findNonClusterChild = (id, graph) => {
// const node = graph.node(id);
log.trace('Searching', id);
const children = graph.children(id);
if (children.length < 1) {
log.trace('This is a valid node', id);
return id;
}
for (let i = 0; i < children.length; i++) {
const _id = findNonClusterChild(children[i], graph);
if (_id) {
log.trace('Found replacement for', id, ' => ', _id);
return _id;
}
}
};
const getAnchorId = id => {
if (!clusterDb[id]) {
return id;
}
// If the cluster has no external connections
if (!clusterDb[id].externalConnections) {
return id;
}
// Return the replacement node
if (clusterDb[id]) {
return clusterDb[id].id;
}
return id;
};
export const adjustClustersAndEdges = (graph, depth) => {
if (!graph || depth > 10) {
log.debug('Opting out, no graph ');
return;
} else {
log.debug('Opting in, graph ');
}
// Go through the nodes and for each cluster found, save a replacment node, this can be used when
// faking a link to a cluster
graph.nodes().forEach(function(id) {
const children = graph.children(id);
if (children.length > 0) {
log.trace(
'Cluster identified',
id,
' Replacement id in edges: ',
findNonClusterChild(id, graph)
);
decendants[id] = extractDecendants(id, graph);
clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
}
});
// Check incoming and outgoing edges for each cluster
graph.nodes().forEach(function(id) {
const children = graph.children(id);
const edges = graph.edges();
if (children.length > 0) {
log.debug('Cluster identified', id, decendants);
edges.forEach(edge => {
// log.debug('Edge, decendants: ', edge, decendants[id]);
// Check if any edge leaves the cluster (not the actual cluster, thats a link from the box)
if (edge.v !== id && edge.w !== id) {
// Any edge where either the one of the nodes is decending to the cluster but not the other
// if (decendants[id].indexOf(edge.v) < 0 && decendants[id].indexOf(edge.w) < 0) {
const d1 = isDecendant(edge.v, id);
const d2 = isDecendant(edge.w, id);
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
if (d1 ^ d2) {
log.debug('Edge: ', edge, ' leaves cluster ', id);
log.debug('Decendants of ', id, ': ', decendants[id]);
clusterDb[id].externalConnections = true;
}
}
});
}
});
extractor(graph, 0);
// For clusters with incoming and/or outgoing edges translate those edges to a real node
// in the cluster inorder to fake the edge
graph.edges().forEach(function(e) {
const edge = graph.edge(e);
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
let v = e.v;
let w = e.w;
// Check if link is either from or to a cluster
log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
if (clusterDb[e.v] || clusterDb[e.w]) {
log.trace('Fixing and trixing - removing', e.v, e.w, e.name);
v = getAnchorId(e.v);
w = getAnchorId(e.w);
graph.removeEdge(e.v, e.w, e.name);
if (v !== e.v) edge.fromCluster = e.v;
if (w !== e.w) edge.toCluster = e.w;
log.trace('Replacing with', v, w, e.name);
graph.setEdge(v, w, edge, e.name);
}
});
log.debug('Adjusted Graph', graphlib.json.write(graph));
log.trace(clusterDb);
// Remove references to extracted cluster
// graph.edges().forEach(edge => {
// if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) {
// graph.removeEdge(edge);
// }
// });
};
export const extractor = (graph, depth) => {
log.debug('extractor - ', depth, graphlib.json.write(graph), graph.children('D'));
if (depth > 10) {
log.error('Bailing out');
return;
}
// For clusters without incoming and/or outgoing edges, create a new cluster-node
// containing the nodes and edges in the custer in a new graph
// for (let i = 0;)
let nodes = graph.nodes();
let hasChildren = false;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const children = graph.children(node);
hasChildren = hasChildren || children.length > 0;
}
if (!hasChildren) {
log.debug('Done, no node has children', graph.nodes());
return;
}
// const clusters = Object.keys(clusterDb);
// clusters.forEach(clusterId => {
log.debug('Nodes = ', nodes, depth);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
log.debug(
'Extracting node',
node,
clusterDb,
clusterDb[node] && !clusterDb[node].externalConnections,
!graph.parent(node),
graph.node(node),
graph.children('D'),
' Depth ',
depth
);
// Note that the node might have been removed after the Object.keys call so better check
// that it still is in the game
if (!clusterDb[node]) {
// Skip if the node is not a cluster
log.debug('Not a cluster', node, depth);
// break;
} else if (
!clusterDb[node].externalConnections &&
!graph.parent(node) &&
graph.children(node) &&
graph.children(node).length > 0
) {
log.debug(
'Cluster without external connections, without a parent and with children',
node,
depth
);
const clusterGraph = new graphlib.Graph({
multigraph: true,
compound: true
})
.setGraph({
rankdir: 'TB',
// Todo: set proper spacing
nodesep: 50,
ranksep: 50,
marginx: 8,
marginy: 8
})
.setDefaultEdgeLabel(function() {
return {};
});
copy(node, graph, clusterGraph, node);
graph.setNode(node, {
clusterNode: true,
id: node,
clusterData: clusterDb[node].clusterData,
labelText: clusterDb[node].labelText,
graph: clusterGraph
});
log.debug('New graph after copy', graphlib.json.write(clusterGraph));
log.debug('Old graph after copy', graphlib.json.write(graph));
} else {
log.debug(
'Cluster ** ',
node,
' **not meeting the criteria !externalConnections:',
!clusterDb[node].externalConnections,
' no parent: ',
!graph.parent(node),
' children ',
graph.children(node) && graph.children(node).length > 0,
graph.children('D'),
depth
);
log.debug(clusterDb);
}
}
nodes = graph.nodes();
log.debug('New list of nodes', nodes);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const data = graph.node(node);
log.debug(' Now next leveö', node, data);
if (data.clusterNode) {
extractor(data.graph, depth + 1);
}
}
};

View File

@ -0,0 +1,370 @@
import graphlib from 'graphlib';
import dagre from 'dagre';
import { validate, adjustClustersAndEdges, extractDecendants } from './mermaid-graphlib';
import { setLogLevel, logger } from '../logger';
describe('Graphlib decorations', () => {
let g;
beforeEach(function () {
setLogLevel(1);
g = new graphlib.Graph({
multigraph: true,
compound: true
});
g.setGraph({
rankdir: 'TB',
nodesep: 10,
ranksep: 10,
marginx: 8,
marginy: 8
});
g.setDefaultEdgeLabel(function () {
return {};
});
});
describe('validate', function () {
it('Validate should detect edges between clusters', function () {
/*
subgraph C1
a --> b
end
subgraph C2
c
end
C1 --> C2
*/
g.setNode('a', { data:1});
g.setNode('b', { data: 2 });
g.setNode('c', { data: 3 });
g.setParent('a', 'C1');
g.setParent('b', 'C1');
g.setParent('c', 'C2');
g.setEdge('a', 'b');
g.setEdge('C1', 'C2');
console.log(g.nodes())
expect(validate(g)).toBe(false);
});
it('Validate should not detect edges between clusters after adjustment', function () {
/*
subgraph C1
a --> b
end
subgraph C2
c
end
C1 --> C2
*/
g.setNode('a', {});
g.setNode('b', {});
g.setNode('c', {});
g.setParent('a', 'C1');
g.setParent('b', 'C1');
g.setParent('c', 'C2');
g.setEdge('a', 'b');
g.setEdge('C1', 'C2');
console.log(g.nodes())
adjustClustersAndEdges(g);
logger.info(g.edges())
expect(validate(g)).toBe(true);
});
it('Validate should detect edges between clusters and transform clusters GLB4', function () {
/*
a --> b
subgraph C1
subgraph C2
a
end
b
end
C1 --> c
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setNode('c', { data: 3 });
g.setNode('C1', { data: 4 });
g.setNode('C2', { data: 5 });
g.setParent('a', 'C2');
g.setParent('b', 'C1');
g.setParent('C2', 'C1');
g.setEdge('a', 'b', { name: 'C1-internal-link' });
g.setEdge('C1', 'c', { name: 'C1-external-link' });
adjustClustersAndEdges(g);
logger.info(g.nodes())
expect(g.nodes().length).toBe(2);
expect(validate(g)).toBe(true);
});
it('Validate should detect edges between clusters and transform clusters GLB5', function () {
/*
a --> b
subgraph C1
a
end
subgraph C2
b
end
C1 -->
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setParent('a', 'C1');
g.setParent('b', 'C2');
// g.setEdge('a', 'b', { name: 'C1-internal-link' });
g.setEdge('C1', 'C2', { name: 'C1-external-link' });
logger.info(g.nodes())
adjustClustersAndEdges(g);
logger.info(g.nodes())
expect(g.nodes().length).toBe(2);
expect(validate(g)).toBe(true);
});
it('adjustClustersAndEdges GLB6', function () {
/*
subgraph C1
a
end
C1 --> b
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setNode('C1', { data: 3 });
g.setParent('a', 'C1');
g.setEdge('C1', 'b', { data: 'link1' }, '1');
// logger.info(g.edges())
adjustClustersAndEdges(g);
logger.info(g.edges())
expect(g.nodes()).toEqual(['b', 'C1']);
expect(g.edges().length).toBe(1);
expect(validate(g)).toBe(true);
expect(g.node('C1').clusterNode).toBe(true);
const C1Graph = g.node('C1').graph;
expect(C1Graph.nodes()).toEqual(['a']);
});
it('adjustClustersAndEdges GLB7', function () {
/*
subgraph C1
a
end
C1 --> b
C1 --> c
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setNode('c', { data: 3 });
g.setParent('a', 'C1');
g.setNode('C1', { data: 4 });
g.setEdge('C1', 'b', { data: 'link1' }, '1');
g.setEdge('C1', 'c', { data: 'link2' }, '2');
logger.info(g.node('C1'))
adjustClustersAndEdges(g);
logger.info(g.edges())
expect(g.nodes()).toEqual(['b', 'c', 'C1']);
expect(g.nodes().length).toBe(3);
expect(g.edges().length).toBe(2);
expect(g.edges().length).toBe(2);
const edgeData = g.edge(g.edges()[1]);
expect(edgeData.data).toBe('link2');
expect(validate(g)).toBe(true);
const C1Graph = g.node('C1').graph;
expect(C1Graph.nodes()).toEqual(['a']);
});
it('adjustClustersAndEdges GLB8', function () {
/*
subgraph A
a
end
subgraph B
b
end
subgraph C
c
end
A --> B
A --> C
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setNode('c', { data: 3 });
g.setParent('a', 'A');
g.setParent('b', 'B');
g.setParent('c', 'C');
g.setEdge('A', 'B', { data: 'link1' }, '1');
g.setEdge('A', 'C', { data: 'link2' }, '2');
// logger.info(g.edges())
adjustClustersAndEdges(g);
expect(g.nodes()).toEqual(['A', 'B', 'C']);
expect(g.edges().length).toBe(2);
expect(g.edges().length).toBe(2);
const edgeData = g.edge(g.edges()[1]);
expect(edgeData.data).toBe('link2');
expect(validate(g)).toBe(true);
const CGraph = g.node('C').graph;
expect(CGraph.nodes()).toEqual(['c']);
});
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () {
/*
subgraph C
subgraph D
d
end
end
*/
g.setNode('C', { data: 1 });
g.setNode('D', { data: 2 });
g.setNode('d', { data: 3 });
g.setParent('d', 'D');
g.setParent('D', 'C');
// logger.info('Graph before', g.node('D'))
// logger.info('Graph before', graphlib.json.write(g))
adjustClustersAndEdges(g);
// logger.info('Graph after', graphlib.json.write(g), g.node('C').graph)
const CGraph = g.node('C').graph;
const DGraph = CGraph.node('D').graph;
expect(CGraph.nodes()).toEqual(['D']);
expect(DGraph.nodes()).toEqual(['d']);
expect(g.nodes()).toEqual(['C']);
expect(g.nodes().length).toBe(1);
});
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () {
/*
subgraph A
a
end
subgraph B
b
end
subgraph C
subgraph D
d
end
end
A --> B
A --> C
*/
g.setNode('C', { data: 1 });
g.setNode('D', { data: 2 });
g.setNode('d', { data: 3 });
g.setNode('B', { data: 4 });
g.setNode('b', { data: 5 });
g.setNode('A', { data: 6 });
g.setNode('a', { data: 7 });
g.setParent('a', 'A');
g.setParent('b', 'B');
g.setParent('d', 'D');
g.setParent('D', 'C');
g.setEdge('A', 'B', { data: 'link1' }, '1');
g.setEdge('A', 'C', { data: 'link2' }, '2');
logger.info('Graph before', g.node('D'))
logger.info('Graph before', graphlib.json.write(g))
adjustClustersAndEdges(g);
logger.trace('Graph after', graphlib.json.write(g))
expect(g.nodes()).toEqual(['C', 'B', 'A']);
expect(g.nodes().length).toBe(3);
expect(g.edges().length).toBe(2);
const AGraph = g.node('A').graph;
const BGraph = g.node('B').graph;
const CGraph = g.node('C').graph;
// logger.info(CGraph.nodes());
const DGraph = CGraph.node('D').graph;
// logger.info('DG', CGraph.children('D'));
logger.info('A', AGraph.nodes());
expect(AGraph.nodes().length).toBe(1);
expect(AGraph.nodes()).toEqual(['a']);
logger.trace('Nodes', BGraph.nodes())
expect(BGraph.nodes().length).toBe(1);
expect(BGraph.nodes()).toEqual(['b']);
expect(CGraph.nodes()).toEqual(['D']);
expect(CGraph.nodes().length).toEqual(1);
expect(AGraph.edges().length).toBe(0);
expect(BGraph.edges().length).toBe(0);
expect(CGraph.edges().length).toBe(0);
expect(DGraph.nodes()).toEqual(['d']);
expect(DGraph.edges().length).toBe(0);
// expect(CGraph.node('D')).toEqual({ data: 2 });
expect(g.edges().length).toBe(2);
// expect(g.edges().length).toBe(2);
// const edgeData = g.edge(g.edges()[1]);
// expect(edgeData.data).toBe('link2');
// expect(validate(g)).toBe(true);
});
});
});
describe('extractDecendants', function () {
let g;
beforeEach(function () {
setLogLevel(1);
g = new graphlib.Graph({
multigraph: true,
compound: true
});
g.setGraph({
rankdir: 'TB',
nodesep: 10,
ranksep: 10,
marginx: 8,
marginy: 8
});
g.setDefaultEdgeLabel(function () {
return {};
});
});
it('Simple case of one level decendants GLB9', function () {
/*
subgraph A
a
end
subgraph B
b
end
subgraph C
c
end
A --> B
A --> C
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setNode('c', { data: 3 });
g.setParent('a', 'A');
g.setParent('b', 'B');
g.setParent('c', 'C');
g.setEdge('A', 'B', { data: 'link1' }, '1');
g.setEdge('A', 'C', { data: 'link2' }, '2');
// logger.info(g.edges())
const d1 = extractDecendants('A',g)
const d2 = extractDecendants('B',g)
const d3 = extractDecendants('C',g)
expect(d1).toEqual(['a']);
expect(d2).toEqual(['b']);
expect(d3).toEqual(['c']);
});
});

View File

@ -248,7 +248,7 @@ const cylinder = (parent, node) => {
const rect = (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
logger.info('Classes = ', node.classes);
logger.trace('Classes = ', node.classes);
// add the rect
const rect = shapeSvg.insert('rect', ':first-child');
@ -387,11 +387,31 @@ let nodeElems = {};
export const insertNode = (elem, node) => {
nodeElems[node.id] = shapes[node.shape](elem, node);
};
export const setNodeElem = (elem, node) => {
nodeElems[node.id] = elem;
};
export const clear = () => {
nodeElems = {};
};
export const positionNode = node => {
const el = nodeElems[node.id];
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
logger.trace(
'Transforming node',
node,
'translate(' + (node.x - node.width / 2 - 5) + ', ' + (node.y - node.height / 2 - 5) + ')'
);
const padding = 8;
if (node.clusterNode) {
el.attr(
'transform',
'translate(' +
(node.x - node.width / 2 - padding) +
', ' +
(node.y - node.height / 2 - padding) +
')'
);
} else {
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
}
};

View File

@ -1,6 +1,5 @@
import graphlib from 'graphlib';
import * as d3 from 'd3';
import dagre from 'dagre';
import flowDb from './flowDb';
import flow from './parser/flow';
@ -133,10 +132,8 @@ export const addVertices = function(vert, g, svgId) {
}
// Add the node
g.setNode(vertex.id, {
labelType: 'svg',
labelStyle: styles.labelStyle,
shape: _shape,
label: vertexNode,
labelText: vertexText,
rx: radious,
ry: radious,
@ -149,10 +146,8 @@ export const addVertices = function(vert, g, svgId) {
});
logger.info('setNode', {
labelType: 'svg',
labelStyle: styles.labelStyle,
shape: _shape,
label: vertexNode,
labelText: vertexText,
rx: radious,
ry: radious,
@ -193,6 +188,8 @@ export const addEdges = function(edges, g) {
} else {
edgeData.arrowhead = 'normal';
}
logger.info(edgeData, edge);
edgeData.arrowType = edge.type;
let style = '';
@ -321,8 +318,10 @@ export const draw = function(text, id) {
let subG;
const subGraphs = flowDb.getSubGraphs();
logger.info('Subgraphs - ', subGraphs);
for (let i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i];
logger.info('Subgraph - ', subG);
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
}
@ -354,7 +353,7 @@ export const draw = function(text, 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);
// dagre.layout(g);
element.selectAll('g.node').attr('title', function() {
return flowDb.getTooltip(this.id);
@ -385,27 +384,27 @@ export const draw = function(text, id) {
// Index nodes
flowDb.indexNodes('subGraph' + i);
// reposition labels
for (i = 0; i < subGraphs.length; i++) {
subG = subGraphs[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 + '"]');
// 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');
// 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]);
}
}
}
// 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) {

View File

@ -1,4 +1,7 @@
import { logger } from '../../logger';
import { generateId } from '../../utils';
const clone = o => JSON.parse(JSON.stringify(o));
let rootDoc = [];
const setRootDoc = o => {
@ -22,6 +25,34 @@ const docTranslator = (parent, node, first) => {
}
if (node.doc) {
const doc = [];
// Check for concurrency
let i = 0;
let currentDoc = [];
for (i = 0; i < node.doc.length; i++) {
if (node.doc[i].type === 'divider') {
// debugger;
const newNode = clone(node.doc[i]);
newNode.doc = clone(currentDoc);
doc.push(newNode);
currentDoc = [];
} else {
currentDoc.push(node.doc[i]);
}
}
// If any divider was encountered
if (doc.length > 0 && currentDoc.length > 0) {
const newNode = {
stmt: 'state',
id: generateId(),
type: 'divider',
doc: clone(currentDoc)
};
doc.push(clone(newNode));
node.doc = doc;
}
node.doc.forEach(docNode => docTranslator(node, docNode, true));
}
}
@ -31,8 +62,14 @@ const getRootDocV2 = () => {
return { id: 'root', doc: rootDoc };
};
const extract = doc => {
const extract = _doc => {
// const res = { states: [], relations: [] };
let doc;
if (_doc.doc) {
doc = _doc.doc;
} else {
doc = _doc;
}
// let doc = root.doc;
// if (!doc) {
// doc = root;
@ -40,6 +77,8 @@ const extract = doc => {
logger.info(doc);
clear();
logger.info('Extract', doc);
doc.forEach(item => {
if (item.stmt === 'state') {
addState(item.id, item.type, item.doc, item.description, item.note);

View File

@ -72,10 +72,8 @@ const setupNode = (g, parent, node, altFlag) => {
}
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,
@ -87,10 +85,8 @@ const setupNode = (g, parent, node, altFlag) => {
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,
@ -99,10 +95,8 @@ const setupNode = (g, parent, node, altFlag) => {
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,
@ -133,8 +127,7 @@ const setupNode = (g, parent, node, altFlag) => {
classes: 'note-edge',
arrowheadStyle: 'fill: #333',
labelpos: 'c',
labelType: 'text',
label: ''
labelType: 'text'
});
} else {
g.setNode(node.id, nodeData);
@ -143,12 +136,12 @@ const setupNode = (g, parent, node, altFlag) => {
if (parent) {
if (parent.id !== 'root') {
logger.trace('Setting node ', node.id, ' to be child of its parent ', parent.id);
logger.info('Setting node ', node.id, ' to be child of its parent ', parent.id);
g.setParent(node.id, parent.id);
}
}
if (node.doc) {
logger.trace('Adding nodes children ');
logger.info('Adding nodes children ');
setupDoc(g, node, node.doc, !altFlag);
}
};
@ -168,8 +161,7 @@ const setupDoc = (g, parent, doc, altFlag) => {
labelStyle: '',
arrowheadStyle: 'fill: #333',
labelpos: 'c',
labelType: 'text',
label: ''
labelType: 'text'
};
let startId = item.state1.id;
let endId = item.state2.id;
@ -214,7 +206,7 @@ export const draw = function(text, id) {
compound: true
})
.setGraph({
rankdir: 'LR',
rankdir: 'TB',
nodesep: nodeSpacing,
ranksep: rankSpacing,
marginx: 8,
@ -224,8 +216,8 @@ export const draw = function(text, id) {
return {};
});
// logger.info(stateDb.getRootDoc());
stateDb.extract(stateDb.getRootDocV2().doc);
logger.info(stateDb.getRootDocV2());
stateDb.extract(stateDb.getRootDocV2());
logger.info(stateDb.getRootDocV2());
setupNode(g, undefined, stateDb.getRootDocV2(), true);

View File

@ -1,5 +1,5 @@
import moment from 'moment-mini';
//
export const LEVELS = {
debug: 1,
info: 2,

View File

@ -214,6 +214,19 @@ export const getStylesFromArray = arr => {
return { style: style, labelStyle: labelStyle };
};
let cnt = 0;
export const generateId = () => {
cnt++;
return (
'id-' +
Math.random()
.toString(36)
.substr(2, 12) +
'-' +
cnt
);
};
export default {
detectType,
isSubstringInArray,
@ -221,5 +234,6 @@ export default {
calcLabelPosition,
calcCardinalityPosition,
formatUrl,
getStylesFromArray
getStylesFromArray,
generateId
};