Merge pull request #1370 from mermaid-js/feature/1295_generic_rendering_engine
Feature/1295 generic rendering engine
This commit is contained in:
commit
e3b1944e31
|
@ -0,0 +1,358 @@
|
|||
/* eslint-env jest */
|
||||
import { imgSnapshotTest } from '../../helpers/util';
|
||||
|
||||
describe('State diagram', () => {
|
||||
it('should render a simple info', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
info
|
||||
`,
|
||||
{ logLevel: 1 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a simple state diagrams', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
|
||||
[*] --> State1
|
||||
State1 --> [*]
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a long descriptions instead of id when available', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
|
||||
[*] --> S1
|
||||
state "Some long name" as S1
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a long descriptions with additional descriptions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
|
||||
[*] --> S1
|
||||
state "Some long name" as S1: The description
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a single state with short descr', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
state "A long long name" as long1
|
||||
state "A" as longlonglongid
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a transition descrions with new lines', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
|
||||
[*] --> S1
|
||||
S1 --> S2: long line using<br/>should work
|
||||
S1 --> S3: long line using <br>should work
|
||||
S1 --> S4: long line using \\nshould work
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a state with a note', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1: The state with a note
|
||||
note right of State1
|
||||
Important information! You can write
|
||||
notes.
|
||||
end note
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a state with on the left side when so specified', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1: The state with a note with minus - and plus + in it
|
||||
note left of State1
|
||||
Important information! You can write
|
||||
notes with . and in them.
|
||||
end note
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a state with a note together with another state', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1: The state with a note +,-
|
||||
note right of State1
|
||||
Important information! You can write +,-
|
||||
notes.
|
||||
end note
|
||||
State1 --> State2 : With +,-
|
||||
note left of State2 : This is the note +,-<br/>
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a note with multiple lines in it', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1: The state with a note
|
||||
note right of State1
|
||||
Important information! You\ncan write
|
||||
notes with multiple lines...
|
||||
Here is another line...
|
||||
And another line...
|
||||
end note
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should handle multiline notes with different line breaks', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1
|
||||
note right of State1
|
||||
Line1<br>Line2<br/>Line3<br />Line4<br />Line5
|
||||
end note
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a states with descriptions including multi-line descriptions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
State1: This a a single line description
|
||||
State2: This a a multi line description
|
||||
State2: here comes the multi part
|
||||
[*] --> State1
|
||||
State1 --> State2
|
||||
State2 --> [*]
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a simple state diagrams', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
[*] --> State1
|
||||
State1 --> State2
|
||||
State1 --> State3
|
||||
State1 --> [*]
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a simple state diagrams with labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
[*] --> State1
|
||||
State1 --> State2 : Transition 1
|
||||
State1 --> State3 : Transition 2
|
||||
State1 --> State4 : Transition 3
|
||||
State1 --> State5 : Transition 4
|
||||
State2 --> State3 : Transition 5
|
||||
State1 --> [*]
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render state descriptions', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
state "Long state description" as XState1
|
||||
state "Another Long state description" as XState2
|
||||
XState2 : New line
|
||||
XState1 --> XState2
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render composit states', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
[*] --> NotShooting: Pacifist
|
||||
NotShooting --> A
|
||||
NotShooting --> B
|
||||
NotShooting --> C
|
||||
|
||||
state NotShooting {
|
||||
[*] --> Idle: Yet another long long öong öong öong label
|
||||
Idle --> Configuring : EvConfig
|
||||
Configuring --> Idle : EvConfig EvConfig EvConfig EvConfig EvConfig
|
||||
}
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render multiple composit states', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
});
|
||||
it('should render forks in composit states', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
[*]-->TV
|
||||
|
||||
state TV {
|
||||
state fork_state <<fork>>
|
||||
[*] --> fork_state
|
||||
fork_state --> State2
|
||||
fork_state --> State3
|
||||
|
||||
state join_state <<join>>
|
||||
State2 --> join_state
|
||||
State3 --> join_state
|
||||
join_state --> State4
|
||||
State4 --> [*]
|
||||
}
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
});
|
||||
it('should render forks and joins', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
state fork_state <<fork>>
|
||||
[*] --> fork_state
|
||||
fork_state --> State2
|
||||
fork_state --> State3
|
||||
|
||||
state join_state <<join>>
|
||||
State2 --> join_state
|
||||
State3 --> join_state
|
||||
join_state --> State4
|
||||
State4 --> [*]
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render concurrency states', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
[*] --> Active
|
||||
|
||||
state Active {
|
||||
[*] --> NumLockOff
|
||||
NumLockOff --> NumLockOn : EvNumLockPressed
|
||||
NumLockOn --> NumLockOff : EvNumLockPressed
|
||||
--
|
||||
[*] --> CapsLockOff
|
||||
CapsLockOff --> CapsLockOn : EvCapsLockPressed
|
||||
CapsLockOn --> CapsLockOff : EvCapsLockPressed
|
||||
--
|
||||
[*] --> ScrollLockOff
|
||||
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
|
||||
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
|
||||
}
|
||||
`,
|
||||
{ logLevel: 0 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
it('should render a state with states in it', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
state PilotCockpit {
|
||||
state Parent {
|
||||
C
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
logLevel: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
it('Simplest composit state', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
state Parent {
|
||||
C
|
||||
}
|
||||
`,
|
||||
{
|
||||
logLevel: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
it('should handle multiple arrows from one node to another', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
a --> b: Start
|
||||
a --> b: Stop
|
||||
`,
|
||||
{
|
||||
logLevel: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
|
@ -18,6 +18,14 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>info below</h1>
|
||||
<div class="mermaid2" style="width: 100%; height: 20%;">
|
||||
flowchart LR
|
||||
a --> b
|
||||
|
||||
subgraph b [Test]
|
||||
c --> d -->e
|
||||
end
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 20%;">
|
||||
flowchart LR
|
||||
a --> b
|
||||
|
@ -31,30 +39,40 @@
|
|||
G-->H
|
||||
G-->c
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 50%; height: 20%;">
|
||||
stateDiagram-v2
|
||||
[*] --> monkey
|
||||
state monkey {
|
||||
Sitting
|
||||
--
|
||||
Eating
|
||||
}
|
||||
<div class="mermaid" style="width: 50%; height: 20%;">
|
||||
stateDiagram-v2
|
||||
[*] --> Active
|
||||
|
||||
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" 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
|
||||
}
|
||||
stateDiagram-v2
|
||||
[*]-->TV
|
||||
|
||||
state TV {
|
||||
state fork_state <<fork>>
|
||||
[*] --> fork_state
|
||||
fork_state --> State2
|
||||
fork_state --> State3
|
||||
|
||||
state join_state <<join>>
|
||||
State2 --> join_state
|
||||
State3 --> join_state
|
||||
join_state --> State4
|
||||
State4 --> [*]
|
||||
}
|
||||
</div>
|
||||
<div class="mermaid2 mermaid-apa" style="width: 100%; height: 20%;">
|
||||
stateDiagram
|
||||
|
@ -73,8 +91,8 @@
|
|||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
[*] --> First
|
||||
First --> Second
|
||||
% First --> Third
|
||||
First --> Third
|
||||
First --> sec
|
||||
|
||||
state First {
|
||||
[*] --> fir
|
||||
|
@ -83,9 +101,14 @@ stateDiagram-v2
|
|||
state Second {
|
||||
[*] --> sec
|
||||
sec --> [*]
|
||||
}
|
||||
}
|
||||
state Third {
|
||||
[*] --> thi
|
||||
thi --> [*]
|
||||
}
|
||||
thi --> sec
|
||||
</div>
|
||||
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
flowchart TD
|
||||
subgraph A
|
||||
a
|
||||
|
@ -101,7 +124,7 @@ flowchart TD
|
|||
A -- oAo --o B
|
||||
A --> C
|
||||
</div>
|
||||
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
flowchart TD
|
||||
subgraph A
|
||||
a
|
||||
|
@ -133,28 +156,27 @@ stateDiagram-v2
|
|||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
[*]-->TV
|
||||
[*]-->TV
|
||||
|
||||
state TV {
|
||||
[*] --> Off: Off to start with
|
||||
On --> Off : Turn off
|
||||
Off --> On : Turn on
|
||||
}
|
||||
state TV {
|
||||
[*] --> Off: Off to start with
|
||||
On --> Off : Turn off
|
||||
Off --> On : Turn on
|
||||
}
|
||||
|
||||
TV--> Console
|
||||
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
|
||||
}
|
||||
}
|
||||
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%">
|
||||
|
|
|
@ -100,6 +100,20 @@ 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.
|
||||
|
||||
|
||||
Required edgeData for proper rendering:
|
||||
|
||||
| property | description |
|
||||
| ---------- | ---------------------------------------- |
|
||||
| id | Id of the edge |
|
||||
| arrowHead | overlap between arrowHead and arrowType? |
|
||||
| arrowType | overlap between arrowHead and arrowType? |
|
||||
| style | |
|
||||
| labelStyle | |
|
||||
| label | overlap between label and labelText? |
|
||||
| labelPos | |
|
||||
| labelType | overlap between label and labelText? |
|
||||
|
||||
|
||||
# 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.
|
||||
|
|
|
@ -149,7 +149,39 @@ const roundedWithTitle = (parent, node) => {
|
|||
return shapeSvg;
|
||||
};
|
||||
|
||||
const shapes = { rect, roundedWithTitle, noteGroup };
|
||||
const divider = (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');
|
||||
|
||||
const padding = 0 * node.padding;
|
||||
const halfPadding = padding / 2;
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('class', 'divider')
|
||||
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||
.attr('y', node.y - node.height / 2)
|
||||
.attr('width', node.width + padding)
|
||||
.attr('height', node.height + padding);
|
||||
|
||||
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, divider };
|
||||
|
||||
let clusterElems = {};
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
const createLabel = (vertexText, style) => {
|
||||
const createLabel = (vertexText, style, isTitle) => {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
||||
let rows = [];
|
||||
if (vertexText) {
|
||||
rows = vertexText.split(/\n|<br\s*\/?>/gi);
|
||||
if (typeof vertexText === 'string') {
|
||||
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi);
|
||||
} else if (Array.isArray(vertexText)) {
|
||||
rows = vertexText;
|
||||
} else {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
for (let j = 0; j < rows.length; j++) {
|
||||
|
@ -11,6 +15,11 @@ const createLabel = (vertexText, style) => {
|
|||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '0');
|
||||
if (isTitle) {
|
||||
tspan.setAttribute('class', 'title-row');
|
||||
} else {
|
||||
tspan.setAttribute('class', 'row');
|
||||
}
|
||||
tspan.textContent = rows[j].trim();
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ export const insertEdgeLabel = (elem, edge) => {
|
|||
};
|
||||
|
||||
export const positionEdgeLabel = edge => {
|
||||
logger.info('Moving label', edge.id, edge.label, edgeLabels[edge.id]);
|
||||
const el = edgeLabels[edge.id];
|
||||
el.attr('transform', 'translate(' + edge.x + ', ' + edge.y + ')');
|
||||
};
|
||||
|
|
|
@ -14,7 +14,10 @@ import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } f
|
|||
import { logger as log } from '../logger';
|
||||
|
||||
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
||||
log.trace('Graph in recursive render:', graphlib.json.write(graph), parentCluster);
|
||||
log.info('Graph in recursive render:', graphlib.json.write(graph), parentCluster);
|
||||
const dir = graph.graph().rankdir;
|
||||
log.warn('Dir in recursive render - dir:', dir);
|
||||
|
||||
const elem = _elem.insert('g').attr('class', 'root'); // eslint-disable-line
|
||||
if (!graph.nodes()) {
|
||||
log.trace('No nodes found for', graph);
|
||||
|
@ -59,7 +62,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
|||
// insertCluster(clusters, graph.node(v));
|
||||
} else {
|
||||
log.trace('Node - the non recursive path', v, node.id, node);
|
||||
insertNode(nodes, graph.node(v));
|
||||
insertNode(nodes, graph.node(v), dir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -79,14 +82,14 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
|||
});
|
||||
|
||||
graph.edges().forEach(function(e) {
|
||||
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||
});
|
||||
log.trace('#############################################');
|
||||
log.trace('### Layout ###');
|
||||
log.trace('#############################################');
|
||||
log.trace(graph);
|
||||
log.info('#############################################');
|
||||
log.info('### Layout ###');
|
||||
log.info('#############################################');
|
||||
log.info(graph);
|
||||
dagre.layout(graph);
|
||||
log.warn('Graph after layout:', graphlib.json.write(graph));
|
||||
log.trace('Graph after layout:', graphlib.json.write(graph));
|
||||
// Move the nodes to the correct place
|
||||
graph.nodes().forEach(function(v) {
|
||||
const node = graph.node(v);
|
||||
|
@ -119,7 +122,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
|
|||
// Move the edge labels to the correct place after layout
|
||||
graph.edges().forEach(function(e) {
|
||||
const edge = graph.edge(e);
|
||||
log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||
|
||||
insertEdge(edgePaths, edge, clusterDb, diagramtype);
|
||||
positionEdgeLabel(edge);
|
||||
|
@ -138,7 +141,7 @@ export const render = (elem, graph, markers, diagramtype, id) => {
|
|||
log.warn('Graph before:', graphlib.json.write(graph));
|
||||
adjustClustersAndEdges(graph);
|
||||
log.warn('Graph after:', graphlib.json.write(graph));
|
||||
|
||||
log.warn('Graph ever after:', graph.graph());
|
||||
recursiveRender(elem, graph, diagramtype);
|
||||
};
|
||||
|
||||
|
|
|
@ -31,13 +31,17 @@ const isDecendant = (id, ancenstorId) => {
|
|||
};
|
||||
|
||||
const edgeInCluster = (edge, clusterId) => {
|
||||
log.info('Decendants of ', clusterId, ' is ', decendants[clusterId]);
|
||||
log.info('Edge is ', edge);
|
||||
// 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 (edge.v === clusterId) return false;
|
||||
if (edge.w === clusterId) return false;
|
||||
|
||||
if (!decendants[clusterId]) {
|
||||
log.debug('Tilt, ', clusterId, ',not in decendants');
|
||||
return false;
|
||||
}
|
||||
log.info('Here ');
|
||||
|
||||
if (decendants[clusterId].indexOf(edge.v) >= 0) return true;
|
||||
if (isDecendant(edge.v, clusterId)) return true;
|
||||
|
@ -80,17 +84,26 @@ const copy = (clusterId, graph, newGraph, rootId) => {
|
|||
const edges = graph.edges(node);
|
||||
log.debug('Copying Edges', edges);
|
||||
edges.forEach(edge => {
|
||||
log.trace('Edge', edge);
|
||||
log.info('Edge', edge);
|
||||
const data = graph.edge(edge.v, edge.w, edge.name);
|
||||
log.trace('Edge data', data, rootId);
|
||||
log.info('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);
|
||||
log.info('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]));
|
||||
log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
|
||||
} else {
|
||||
log.trace('Skipping copy of edge as ', rootId, edge.v, edge.w, clusterId);
|
||||
log.info(
|
||||
'Skipping copy of edge ',
|
||||
edge.v,
|
||||
'-->',
|
||||
edge.w,
|
||||
' rootId: ',
|
||||
rootId,
|
||||
' clusterId:',
|
||||
clusterId
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
|
@ -316,12 +329,14 @@ export const extractor = (graph, depth) => {
|
|||
depth
|
||||
);
|
||||
|
||||
const graphSettings = graph.graph();
|
||||
|
||||
const clusterGraph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: 'TB',
|
||||
rankdir: graphSettings.rankdir === 'TB' ? 'LR' : 'TB',
|
||||
// Todo: set proper spacing
|
||||
nodesep: 50,
|
||||
ranksep: 50,
|
||||
|
|
|
@ -314,6 +314,34 @@ describe('Graphlib decorations', () => {
|
|||
// expect(edgeData.data).toBe('link2');
|
||||
// expect(validate(g)).toBe(true);
|
||||
});
|
||||
it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB20', function () {
|
||||
/*
|
||||
a --> b
|
||||
subgraph b [Test]
|
||||
c --> d -->e
|
||||
end
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setNode('d', { data: 3 });
|
||||
g.setNode('e', { data: 3 });
|
||||
g.setParent('c', 'b');
|
||||
g.setParent('d', 'b');
|
||||
g.setParent('e', 'b');
|
||||
g.setEdge('a', 'b', { data: 'link1' }, '1');
|
||||
g.setEdge('c', 'd', { data: 'link2' }, '2');
|
||||
g.setEdge('d', 'e', { data: 'link2' }, '2');
|
||||
|
||||
logger.info('Graph before', graphlib.json.write(g))
|
||||
adjustClustersAndEdges(g);
|
||||
const bGraph = g.node('b').graph;
|
||||
// logger.trace('Graph after', graphlib.json.write(g))
|
||||
logger.info('Graph after', graphlib.json.write(bGraph));
|
||||
expect(bGraph.nodes().length).toBe(3);
|
||||
expect(bGraph.edges().length).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
describe('extractDecendants', function () {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import intersect from './intersect/index.js';
|
||||
import { select } from 'd3';
|
||||
import { logger } from '../logger'; // eslint-disable-line
|
||||
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util';
|
||||
import createLabel from './createLabel';
|
||||
import note from './shapes/note';
|
||||
|
||||
const question = (parent, node) => {
|
||||
|
@ -253,6 +255,7 @@ const rect = (parent, node) => {
|
|||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
rect
|
||||
.attr('class', 'basic')
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
|
@ -268,6 +271,74 @@ const rect = (parent, node) => {
|
|||
|
||||
return shapeSvg;
|
||||
};
|
||||
const rectWithTitle = (parent, node) => {
|
||||
// const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);
|
||||
|
||||
let classes;
|
||||
if (!node.classes) {
|
||||
classes = 'node default';
|
||||
} else {
|
||||
classes = 'node ' + node.classes;
|
||||
}
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', classes)
|
||||
.attr('id', node.id);
|
||||
|
||||
// Create the title label and insert it after the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
// const innerRect = shapeSvg.insert('rect');
|
||||
const innerLine = shapeSvg.insert('line');
|
||||
|
||||
const label = shapeSvg.insert('g').attr('class', 'label');
|
||||
|
||||
const text2 = node.labelText.flat();
|
||||
logger.info('Label text', text2[0]);
|
||||
|
||||
const text = label.node().appendChild(createLabel(text2[0], node.labelStyle, true));
|
||||
const textRows = text2.slice(1, text2.length);
|
||||
let titleBox = text.getBBox();
|
||||
const descr = label
|
||||
.node()
|
||||
.appendChild(createLabel(textRows.join('<br/>'), node.labelStyle, true));
|
||||
|
||||
logger.info(descr);
|
||||
const halfPadding = node.padding / 2;
|
||||
select(descr).attr('transform', 'translate( 0' + ', ' + (titleBox.height + halfPadding) + ')');
|
||||
// Get the size of the label
|
||||
|
||||
// Bounding box for title and text
|
||||
const bbox = label.node().getBBox();
|
||||
|
||||
// Center the label
|
||||
label.attr(
|
||||
'transform',
|
||||
'translate(' + -bbox.width / 2 + ', ' + (-bbox.height / 2 - halfPadding + 3) + ')'
|
||||
);
|
||||
|
||||
rect
|
||||
.attr('class', 'outer title-state')
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
.attr('y', -bbox.height / 2 - halfPadding)
|
||||
.attr('width', bbox.width + node.padding)
|
||||
.attr('height', bbox.height + node.padding);
|
||||
|
||||
innerLine
|
||||
.attr('class', 'divider')
|
||||
.attr('x1', -bbox.width / 2 - halfPadding)
|
||||
.attr('x2', bbox.width / 2 + halfPadding)
|
||||
.attr('y1', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding)
|
||||
.attr('y2', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const stadium = (parent, node) => {
|
||||
const { shapeSvg, bbox } = labelHelper(parent, node);
|
||||
|
@ -335,6 +406,41 @@ const start = (parent, node) => {
|
|||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const forkJoin = (parent, node, dir) => {
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'node default')
|
||||
.attr('id', node.id);
|
||||
|
||||
let width = 70;
|
||||
let height = 10;
|
||||
|
||||
if (dir === 'LR') {
|
||||
width = 10;
|
||||
height = 70;
|
||||
}
|
||||
|
||||
const shape = shapeSvg
|
||||
.append('rect')
|
||||
.style('stroke', 'black')
|
||||
.style('fill', 'black')
|
||||
.attr('x', (-1 * width) / 2)
|
||||
.attr('y', (-1 * height) / 2)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('class', 'fork-join');
|
||||
|
||||
updateNodeBounds(node, shape);
|
||||
node.height = node.height + node.padding / 2;
|
||||
node.width = node.width + node.padding / 2;
|
||||
node.intersect = function(point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const end = (parent, node) => {
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
|
@ -367,6 +473,7 @@ const end = (parent, node) => {
|
|||
const shapes = {
|
||||
question,
|
||||
rect,
|
||||
rectWithTitle,
|
||||
circle,
|
||||
stadium,
|
||||
hexagon,
|
||||
|
@ -379,13 +486,15 @@ const shapes = {
|
|||
cylinder,
|
||||
start,
|
||||
end,
|
||||
note
|
||||
note,
|
||||
fork: forkJoin,
|
||||
join: forkJoin
|
||||
};
|
||||
|
||||
let nodeElems = {};
|
||||
|
||||
export const insertNode = (elem, node) => {
|
||||
nodeElems[node.id] = shapes[node.shape](elem, node);
|
||||
export const insertNode = (elem, node, dir) => {
|
||||
nodeElems[node.id] = shapes[node.shape](elem, node, dir);
|
||||
};
|
||||
export const setNodeElem = (elem, node) => {
|
||||
nodeElems[node.id] = elem;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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 insertPatterns = (elem, patternArray, type, id) => {
|
||||
patternArray.forEach(patternName => {
|
||||
patterns[patternName](elem, type, id);
|
||||
});
|
||||
};
|
||||
|
||||
{
|
||||
/* <svg height="10" width="10" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
{' '}
|
||||
<defs>
|
||||
{' '}
|
||||
<pattern id="circles-1" patternUnits="userSpaceOnUse" width="10" height="10">
|
||||
{' '}
|
||||
<image
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+CiAgPHJlY3Qgd2lkdGg9JzEwJyBoZWlnaHQ9JzEwJyBmaWxsPSJ3aGl0ZSIgLz4KICA8Y2lyY2xlIGN4PSIxIiBjeT0iMSIgcj0iMSIgZmlsbD0iYmxhY2siLz4KPC9zdmc+"
|
||||
x="0"
|
||||
y="0"
|
||||
width="10"
|
||||
height="10"
|
||||
>
|
||||
{' '}
|
||||
</image>{' '}
|
||||
</pattern>{' '}
|
||||
</defs>{' '}
|
||||
</svg>; */
|
||||
}
|
||||
|
||||
const dots = (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 patterns = {
|
||||
dots
|
||||
};
|
||||
export default insertPatterns;
|
|
@ -132,6 +132,7 @@ export const addState = function(id, type, doc, descr, note) {
|
|||
}
|
||||
}
|
||||
if (descr) {
|
||||
logger.info('Adding state ', id, descr);
|
||||
if (typeof descr === 'string') addDescription(id, descr.trim());
|
||||
|
||||
if (typeof descr === 'object') {
|
||||
|
|
|
@ -42,6 +42,9 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||
if (node.start === false) {
|
||||
shape = 'end';
|
||||
}
|
||||
if (node.type !== 'default') {
|
||||
shape = node.type;
|
||||
}
|
||||
|
||||
if (!nodeDb[node.id]) {
|
||||
nodeDb[node.id] = {
|
||||
|
@ -52,9 +55,27 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||
};
|
||||
}
|
||||
|
||||
// Description
|
||||
// Build of the array of description strings accordinging
|
||||
if (node.description) {
|
||||
nodeDb[node.id].description = node.description;
|
||||
if (Array.isArray(nodeDb[node.id].description)) {
|
||||
// There already is an array of strings,add to it
|
||||
nodeDb[node.id].shape = 'rectWithTitle';
|
||||
nodeDb[node.id].description.push(node.description);
|
||||
} else {
|
||||
if (nodeDb[node.id].description.length > 0) {
|
||||
// if there is a description already transformit to an array
|
||||
nodeDb[node.id].shape = 'rectWithTitle';
|
||||
if (nodeDb[node.id].description === node.id) {
|
||||
// If the previous description was the is, remove it
|
||||
nodeDb[node.id].description = [node.description];
|
||||
} else {
|
||||
nodeDb[node.id].description = [nodeDb[node.id].description, node.description];
|
||||
}
|
||||
} else {
|
||||
nodeDb[node.id].shape = 'rect';
|
||||
nodeDb[node.id].description = node.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save data for description and group so that for instance a statement without description overwrites
|
||||
|
@ -64,7 +85,7 @@ const setupNode = (g, parent, node, altFlag) => {
|
|||
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].shape = node.type === 'divider' ? 'divider' : 'roundedWithTitle';
|
||||
nodeDb[node.id].classes =
|
||||
nodeDb[node.id].classes +
|
||||
' ' +
|
||||
|
@ -155,10 +176,12 @@ const setupDoc = (g, parent, doc, altFlag) => {
|
|||
setupNode(g, parent, item.state1, altFlag);
|
||||
setupNode(g, parent, item.state2, altFlag);
|
||||
const edgeData = {
|
||||
id: 'edge' + cnt,
|
||||
arrowhead: 'normal',
|
||||
arrowType: 'arrow_barb',
|
||||
style: 'fill:none',
|
||||
labelStyle: '',
|
||||
label: item.description,
|
||||
arrowheadStyle: 'fill: #333',
|
||||
labelpos: 'c',
|
||||
labelType: 'text'
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
|
||||
.edgeLabel {
|
||||
background-color: $edgeLabelBackground;
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,16 @@ g.stateGroup line {
|
|||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.statediagram-cluster rect.outer {
|
||||
rx: 5px;
|
||||
ry: 5px;
|
||||
}
|
||||
.statediagram-state .divider {
|
||||
stroke: $nodeBorder;
|
||||
}
|
||||
|
||||
.statediagram-state .title-state {
|
||||
rx: 5px;
|
||||
ry: 5px;
|
||||
}
|
||||
|
@ -100,10 +110,14 @@ g.stateGroup line {
|
|||
ry:0;
|
||||
}
|
||||
|
||||
.statediagram-state rect {
|
||||
.statediagram-state rect.basic {
|
||||
rx: 5px;
|
||||
ry: 5px;
|
||||
}
|
||||
.statediagram-state rect.divider {
|
||||
stroke-dasharray: 10,10;
|
||||
fill: #efefef;
|
||||
}
|
||||
|
||||
.note-edge {
|
||||
stroke-dasharray: 5;
|
||||
|
|
Loading…
Reference in New Issue