#1602 Update of handling of nested subgraphs

This commit is contained in:
Knut Sveidqvist 2020-09-02 20:34:24 +02:00
parent 2a4344ace3
commit 9106c6ad52
9 changed files with 423 additions and 39 deletions

View File

@ -125,4 +125,32 @@ describe('Flowchart v2', () => {
expect(svg).to.not.have.attr('style');
});
});
it('50: handle nested subgraphs in reverse order', () => {
imgSnapshotTest(
`flowchart LR
a -->b
subgraph A
B
end
subgraph B
b
end
`,
{htmlLabels: true, flowchart: {htmlLabels: true}, securityLevel: 'loose'}
);
});
it('51: handle nested subgraphs in reverse order', () => {
imgSnapshotTest(
`flowchart LR
a -->b
subgraph A
B
end
subgraph B
b
end
`,
{htmlLabels: true, flowchart: {htmlLabels: true}, securityLevel: 'loose'}
);
});
});

View File

@ -75,19 +75,48 @@ stateDiagram-v2
</div>
</div>
<div class="mermaid" style="width: 50%; height: 20%;">
%% this does not produce the desired result
flowchart TB
subgraph container_Beta
process_C-->Process_D
end
subgraph container_Alpha
process_A-->process_B
process_B-->|via_AWSBatch|container_Beta
process_A-->|messages|process_C
end
</div>
<div class="mermaid" style="width: 50%; height: 20%;">
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ff0000'}}}%%
classDiagram
Animal <|-- Duck
flowchart TB
b-->B
a-->c
subgraph O
A
end
subgraph B
c
end
subgraph A
a
b
B
end
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
graph TD
A(Start) --> B[/Another/]
A[/Another/] --> C[End]
subgraph section
B
C
end
flowchart TB
subgraph O
A
end
subgraph A
b-->B
a-->c
end
subgraph B
c
end
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
sequenceDiagram

View File

@ -0,0 +1,129 @@
<html>
<head>
<link
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
rel="stylesheet"
/>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap" rel="stylesheet">
<style>
body {
/* background: rgb(221, 208, 208); */
/* background:#333; */
font-family: 'Arial';
}
h1 { color: grey;}
.mermaid2 {
display: none;
}
</style>
</head>
<body>
<h1>info below</h1>
<div class="flex">
<div class="mermaid2" style="width: 50%; height: 20%;">
flowchart BT
subgraph two
b1
end
subgraph three
c1-->c2
end
c1 --apa apa apa--> b1
two --> c2
</div>
<div class="mermaid2" style="width: 50%; height: 200px;">
sequenceDiagram
Alice->>Bob:Extremely utterly long line of longness which had preivously overflown the actor box as it is much longer than what it should be
Bob->>Alice: I'm short though
</div>
<div class="mermaid2" style="width: 50%; height: 200px;">
%%{init: {'securityLevel': 'loose'}}%%
graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{{Let me think...<br />Do I want something for work,<br />something to spend every free second with,<br />or something to get around?}}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[Car]
click A "index.html#link-clicked" "link test"
click B callback "click test"
classDef someclass fill:#f96;
class A someclass;
class C someclass;
</div>
<div class="mermaid2" style="width: 50%; height: 200px;">
flowchart BT
subgraph a
b1 -- ok --> b2
end
a -- sert --> c
c --> d
b1 --> d
a --asd123 --> d
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
stateDiagram-v2
state A {
B1 --> B2: ok
}
A --> C: sert
C --> D
B1 --> D
A --> D: asd123
</div>
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ff0000'}}}%%
flowchart LR
a -->b
subgraph A
B
end
subgraph B
b
end
</div>
<div class="mermaid" style="width: 50%; height: 20%;">
flowchart TB
subgraph A
b-->B
a-->c
end
subgraph B
c
end
</div>
<div class="mermaid2" style="width: 50%; height: 20%;">
sequenceDiagram
Alice->Bob: Hello Bob, how are you?
Note over Alice,Bob: Looks
Note over Bob,Alice: Looks back
</div>
<script src="./mermaid.js"></script>
<script>
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);
};
mermaid.initialize({
// theme: 'forest',
// themeVariables:{primaryColor: '#ff0000'},
// arrowMarkerAbsolute: true,
// themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}',
logLevel: 0,
flowchart: { curve: 'cardinal', "htmlLabels": false },
// gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50, showSequenceNumbers: true },
// sequenceDiagram: { actorMargin: 300 } // deprecated
fontFamily: '"arial", sans-serif',
curve: 'cardinal',
securityLevel: 'strict'
});
function callback(){alert('It worked');}
</script>
</body>
</html>

View File

@ -6,7 +6,8 @@ import {
clear as clearGraphlib,
clusterDb,
adjustClustersAndEdges,
findNonClusterChild
findNonClusterChild,
sortNodesByHierarchy
} from './mermaid-graphlib';
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes';
import { insertCluster, clear as clearClusters } from './clusters';
@ -14,7 +15,7 @@ import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } f
import { logger as log } from '../logger';
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
log.info('Graph in recursive render:', graphlib.json.write(graph), parentCluster);
log.info('Graph in recursive render: XXX', graphlib.json.write(graph), parentCluster);
const dir = graph.graph().rankdir;
log.warn('Dir in recursive render - dir:', dir);
@ -22,7 +23,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
if (!graph.nodes()) {
log.info('No nodes found for', graph);
} else {
log.info('Recursive render', graph.nodes());
log.info('Recursive render XXX', graph.nodes());
}
if (graph.edges().length > 0) {
log.info('Recursive edges', graph.edge(graph.edges()[0]));
@ -39,11 +40,14 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
if (typeof parentCluster !== 'undefined') {
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
// data.clusterPositioning = true;
log.info('Setting data for cluster', data);
log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster);
graph.setNode(parentCluster.id, data);
graph.setParent(v, parentCluster.id, data);
if (!graph.parent(v)) {
log.warn('Setting parent', v, parentCluster.id);
graph.setParent(v, parentCluster.id, data);
}
}
log.info('(Insert) Node ' + v + ': ' + JSON.stringify(graph.node(v)));
log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v)));
if (node && node.clusterNode) {
// const children = graph.children(v);
log.info('Cluster identified', v, node, graph.node(v));
@ -56,7 +60,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
if (graph.children(v).length > 0) {
// This is a cluster but not to be rendered recusively
// Render as before
log.info('Cluster - the non recursive path', v, node.id, node, graph);
log.info('Cluster - the non recursive path XXX', v, node.id, node, graph);
log.info(findNonClusterChild(node.id, graph));
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
// insertCluster(clusters, graph.node(v));
@ -91,7 +95,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
dagre.layout(graph);
log.info('Graph after layout:', graphlib.json.write(graph));
// Move the nodes to the correct place
graph.nodes().forEach(function(v) {
sortNodesByHierarchy(graph).forEach(function(v) {
const node = graph.node(v);
log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
log.info(
@ -138,10 +142,10 @@ export const render = (elem, graph, markers, diagramtype, id) => {
clearClusters();
clearGraphlib();
log.warn('Graph before:', graphlib.json.write(graph));
log.warn('Graph at first:', graphlib.json.write(graph));
adjustClustersAndEdges(graph);
log.warn('Graph after:', graphlib.json.write(graph));
log.warn('Graph ever after:', graph.graph());
// log.warn('Graph ever after:', graphlib.json.write(graph.node('A').graph));
recursiveRender(elem, graph, diagramtype);
};

View File

@ -52,7 +52,7 @@ const edgeInCluster = (edge, clusterId) => {
};
const copy = (clusterId, graph, newGraph, rootId) => {
log.info(
log.warn(
'Copying children of ',
clusterId,
'root',
@ -68,7 +68,7 @@ const copy = (clusterId, graph, newGraph, rootId) => {
nodes.push(clusterId);
}
log.debug('Copying (nodes) clusterId', clusterId, 'nodes', nodes);
log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes);
nodes.forEach(node => {
if (graph.children(node).length > 0) {
@ -77,8 +77,8 @@ const copy = (clusterId, graph, newGraph, rootId) => {
const data = graph.node(node);
log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
newGraph.setNode(node, data);
log.debug('Setting parent', node, graph.parent(node));
if (rootId !== graph.parent(node)) {
log.warn('Setting parent', node, graph.parent(node));
newGraph.setParent(node, graph.parent(node));
}
@ -245,40 +245,51 @@ export const adjustClustersAndEdges = (graph, depth) => {
// 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]);
log.warn('Edge: ', edge, ' leaves cluster ', id);
log.warn('Decendants of XXX ', id, ': ', decendants[id]);
clusterDb[id].externalConnections = true;
}
}
});
} else {
log.debug('Not a cluster ', id, decendants);
}
});
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)));
log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
log.warn('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]);
log.warn(
'Fix XXX',
clusterDb,
'ids:',
e.v,
e.w,
'Translateing: ',
clusterDb[e.v],
' --- ',
clusterDb[e.w]
);
if (clusterDb[e.v] || clusterDb[e.w]) {
log.warn('Fixing and trixing - removing', e.v, e.w, e.name);
log.warn('Fixing and trixing - removing XXX', 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.warn('Replacing with', v, w, e.name);
log.warn('Fix Replacing with XXX', v, w, e.name);
graph.setEdge(v, w, edge, e.name);
}
});
log.warn('Adjusted Graph', graphlib.json.write(graph));
extractor(graph, 0);
log.trace(clusterDb);
@ -291,7 +302,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
};
export const extractor = (graph, depth) => {
log.debug('extractor - ', depth, graphlib.json.write(graph), graph.children('D'));
log.warn('extractor - ', depth, graphlib.json.write(graph), graph.children('D'));
if (depth > 10) {
log.error('Bailing out');
return;
@ -340,7 +351,7 @@ export const extractor = (graph, depth) => {
graph.children(node) &&
graph.children(node).length > 0
) {
log.debug(
log.warn(
'Cluster without external connections, without a parent and with children',
node,
depth
@ -364,7 +375,7 @@ export const extractor = (graph, depth) => {
return {};
});
log.debug('Old graph before copy', graphlib.json.write(graph));
log.warn('Old graph before copy', graphlib.json.write(graph));
copy(node, graph, clusterGraph, node);
graph.setNode(node, {
clusterNode: true,
@ -373,10 +384,10 @@ export const extractor = (graph, depth) => {
labelText: clusterDb[node].labelText,
graph: clusterGraph
});
log.debug('New graph after copy', graphlib.json.write(clusterGraph));
log.warn('New graph after copy node: (', node, ')', graphlib.json.write(clusterGraph));
log.debug('Old graph after copy', graphlib.json.write(graph));
} else {
log.debug(
log.warn(
'Cluster ** ',
node,
' **not meeting the criteria !externalConnections:',
@ -393,13 +404,27 @@ export const extractor = (graph, depth) => {
}
nodes = graph.nodes();
log.debug('New list of nodes', nodes);
log.warn('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);
log.warn(' Now next level', node, data);
if (data.clusterNode) {
extractor(data.graph, depth + 1);
}
}
};
const sorter = (graph, nodes) => {
if (nodes.length === 0) return [];
let result = Object.assign(nodes);
nodes.forEach(node => {
const children = graph.children(node);
const sorted = sorter(graph, children);
result = result.concat(sorted);
});
return result;
};
export const sortNodesByHierarchy = graph => sorter(graph, graph.children());

View File

@ -1,6 +1,6 @@
import graphlib from 'graphlib';
import dagre from 'dagre';
import { validate, adjustClustersAndEdges, extractDecendants } from './mermaid-graphlib';
import { validate, adjustClustersAndEdges, extractDecendants, sortNodesByHierarchy } from './mermaid-graphlib';
import { setLogLevel, logger } from '../logger';
describe('Graphlib decorations', () => {
@ -373,6 +373,31 @@ describe('Graphlib decorations', () => {
});
});
it('adjustClustersAndEdges should handle nesting GLB77', function () {
/*
flowchart TB
subgraph A
b-->B
a-->c
end
subgraph B
c
end
*/
const exportedGraph = JSON.parse('{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"class":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","classes":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","classes":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}');
const gr = graphlib.json.read(exportedGraph)
logger.info('Graph before', graphlib.json.write(gr))
adjustClustersAndEdges(gr);
const aGraph = gr.node('A').graph;
const bGraph = aGraph.node('B').graph;
logger.info('Graph after', graphlib.json.write(aGraph));
// logger.trace('Graph after', graphlib.json.write(g))
expect(aGraph.parent('c')).toBe('B');
expect(aGraph.parent('B')).toBe(undefined);
});
});
describe('extractDecendants', function () {
let g;
@ -426,3 +451,57 @@ describe('extractDecendants', function () {
expect(d3).toEqual(['c']);
});
});
describe('sortNodesByHierarchy', 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('it should sort proper en nodes are in reverse order', function () {
/*
a -->b
subgraph B
b
end
subgraph A
B
end
*/
g.setNode('a', { data: 1 });
g.setNode('b', { data: 2 });
g.setParent('b', 'B');
g.setParent('B', 'A');
g.setEdge('a', 'b', '1');
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
});
it('it should sort proper en nodes are in correct order', function () {
/*
a -->b
subgraph B
b
end
subgraph A
B
end
*/
g.setNode('a', { data: 1 });
g.setParent('B', 'A');
g.setParent('b', 'B');
g.setNode('b', { data: 2 });
g.setEdge('a', 'b', '1');
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
});
});

View File

@ -421,6 +421,22 @@ export const addSubGraph = function(_id, list, _title) {
title = common.sanitizeText(title, config);
subCount = subCount + 1;
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
/**
* Deletes an id from all subgraphs
*/
const del = _id => {
subGraphs.forEach(sg => {
const pos = sg.nodes.indexOf(_id);
if (pos >= 0) {
console.log(sg.nodes, pos, _id);
sg.nodes.splice(pos, 1);
}
});
};
// Removes the members of this subgraph from any other subgraphs, a node only belong to one subgraph
subGraph.nodes.forEach(_id => del(_id));
console.log(subGraph.nodes);
subGraphs.push(subGraph);
subGraphLookup[id] = subGraph;
return id;

View File

@ -382,11 +382,13 @@ export const draw = function(text, id) {
logger.info(edges);
let i = 0;
for (i = subGraphs.length - 1; i >= 0; i--) {
// for (let i = 0; i < subGraphs.length; i++) {
subG = subGraphs[i];
selectAll('cluster').append('text');
for (let j = 0; j < subG.nodes.length; j++) {
logger.info('Setting up subgraphs', subG.nodes[j], subG.id);
g.setParent(subG.nodes[j], subG.id);
}
}

View File

@ -1,5 +1,6 @@
import flowDb from '../flowDb';
import flow from './flow';
import filter from 'lodash/filter';
import { setConfig } from '../../../config';
setConfig({
@ -238,4 +239,75 @@ describe('when parsing subgraphs', function() {
expect(edges[0].type).toBe('arrow_point');
});
it('should handle nested subgraphs 1', function() {
const res = flow.parser.parse(`flowchart TB
subgraph A
b-->B
a-->c
end
subgraph B
c
end`);
const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(2);
const subgraphA = filter(subgraphs,o => o.id === 'A')[0];
const subgraphB = filter(subgraphs,o => o.id === 'B')[0];
expect(subgraphB.nodes[0]).toBe('c');
expect(subgraphA.nodes).toContain('B');
expect(subgraphA.nodes).toContain('b');
expect(subgraphA.nodes).toContain('a');
expect(subgraphA.nodes).not.toContain('c');
});
it('should handle nested subgraphs 2', function() {
const res = flow.parser.parse(`flowchart TB
b-->B
a-->c
subgraph B
c
end
subgraph A
a
b
B
end`);
const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(2);
const subgraphA = filter(subgraphs,o => o.id === 'A')[0];
const subgraphB = filter(subgraphs,o => o.id === 'B')[0];
expect(subgraphB.nodes[0]).toBe('c');
expect(subgraphA.nodes).toContain('B');
expect(subgraphA.nodes).toContain('b');
expect(subgraphA.nodes).toContain('a');
expect(subgraphA.nodes).not.toContain('c');
});
it('should handle nested subgraphs 3', function() {
console.log('#3');
const res = flow.parser.parse(`flowchart TB
subgraph B
c
end
a-->c
subgraph A
b-->B
a
end`);
const subgraphs = flow.parser.yy.getSubGraphs();
expect(subgraphs.length).toBe(2);
const subgraphA = filter(subgraphs,o => o.id === 'A')[0];
const subgraphB = filter(subgraphs,o => o.id === 'B')[0];
console.log(subgraphB.nodes)
expect(subgraphB.nodes[0]).toBe('c');
expect(subgraphA.nodes).toContain('B');
expect(subgraphA.nodes).toContain('b');
expect(subgraphA.nodes).toContain('a');
expect(subgraphA.nodes).not.toContain('c');
});
});