diff --git a/cypress/platform/current.html b/cypress/platform/current.html index 50cb3ed3c..dd32f0be0 100644 --- a/cypress/platform/current.html +++ b/cypress/platform/current.html @@ -87,9 +87,19 @@ stateDiagram-v2
flowchart TD - subgraph Apa - a --> b + subgraph A + a end + subgraph B + b + end + subgraph C + subgraph D + d + end + end + A -- oAo --> B + A --> C
stateDiagram-v2 diff --git a/src/dagre-wrapper/clusters.js b/src/dagre-wrapper/clusters.js index 02da85c58..ff65496a1 100644 --- a/src/dagre-wrapper/clusters.js +++ b/src/dagre-wrapper/clusters.js @@ -24,8 +24,6 @@ const rect = (parent, node) => { const padding = 0 * node.padding; const halfPadding = padding / 2; - const width = node.width || 50; - const height = node.height || 50; log.info('Data ', node, JSON.stringify(node)); // center the rect around its coordinate diff --git a/src/dagre-wrapper/index.js b/src/dagre-wrapper/index.js index f3dd6c885..c1b2b7933 100644 --- a/src/dagre-wrapper/index.js +++ b/src/dagre-wrapper/index.js @@ -9,7 +9,7 @@ import { findNonClusterChild } from './mermaid-graphlib'; import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes'; -import { insertCluster, clear as clearClusters, positionCluster } from './clusters'; +import { insertCluster, clear as clearClusters } from './clusters'; import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges'; import { logger as log } from '../logger'; @@ -24,11 +24,12 @@ const getAnchorId = id => { }; const recursiveRender = (_elem, graph, diagramtype) => { + log.info('Graph in recursive render:', graphlib.json.write(graph)); const elem = _elem.insert('g').attr('class', 'root'); // eslint-disable-line if (!graph.nodes()) { log.info('No nodes found for', graph); } else { - log.info('Recursive render', graph.edges()); + log.info('Recursive render', graph.nodes()); } if (graph.edges().length > 0) { log.info('Recursive edges', graph.edge(graph.edges()[0])); @@ -50,7 +51,7 @@ const recursiveRender = (_elem, graph, diagramtype) => { updateNodeBounds(node, newEl); setNodeElem(newEl, node); - log.info('Recursice render complete', newEl, node); + log.warn('Recursive render complete', newEl, node); } else { if (graph.children(v).length > 0) { // This is a cluster but not to be rendered recusively @@ -65,7 +66,6 @@ const recursiveRender = (_elem, graph, diagramtype) => { } } }); - log.info('Clusters ', clusterDb); // Insert labels, this will insert them into the dom so that the width can be calculated // Also figure out which edges point to/from clusters and adjust them accordingly @@ -74,27 +74,24 @@ const recursiveRender = (_elem, graph, diagramtype) => { graph.edges().forEach(function(e) { const edge = graph.edge(e.v, e.w, e.name); log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - log.info( - 'Edge ' + e.v + ' -> ' + e.w + ': ', - e, - ' ', - +JSON.stringify(graph.edge(e.v, e.w, e.name)) - ); + log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e))); let v = e.v; let w = e.w; // Check if link is either from or to a cluster log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]); - if (clusterDb[e.v] || clusterDb[e.w]) { - log.info('Fixing and trixing - removing', e.v, e.w, e.name); - v = getAnchorId(e.v, graph, nodes); - w = getAnchorId(e.w, graph, nodes); - 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.info('Fixing Replacing with', v, w, e.name); - graph.setEdge(v, w, edge, e.name); - } + // Todo handle case with links + + // if (clusterDb[e.v] || clusterDb[e.w]) { + // log.info('Fixing and trixing - removing', e.v, e.w, e.name); + // v = getAnchorId(e.v, graph, nodes); + // w = getAnchorId(e.w, graph, nodes); + // 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.info('Fixing Replacing with', v, w, e.name); + // graph.setEdge(v, w, edge, e.name); + // } insertEdgeLabel(edgeLabels, edge); }); @@ -106,14 +103,22 @@ const recursiveRender = (_elem, graph, diagramtype) => { log.info('#############################################'); log.info(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); - log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); + // log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); + log.info( + 'Position ' + v + ': (' + node.x, + ',' + node.y, + ') width: ', + node.width, + ' height: ', + node.height + ); if (node && node.clusterNode) { // clusterDb[node.id].node = node; - // positionNode(node); + positionNode(node); } else { // Non cluster node if (graph.children(v).length > 0) { @@ -130,7 +135,7 @@ const recursiveRender = (_elem, graph, diagramtype) => { // 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); diff --git a/src/dagre-wrapper/mermaid-graphlib.js b/src/dagre-wrapper/mermaid-graphlib.js index a232c09db..30f69dbd9 100644 --- a/src/dagre-wrapper/mermaid-graphlib.js +++ b/src/dagre-wrapper/mermaid-graphlib.js @@ -1,58 +1,152 @@ /** * Decorates with functions required by mermaids dagre-wrapper. */ -import { logger } from '../logger'; +import { logger as log } from '../logger'; import graphlib from 'graphlib'; export let clusterDb = {}; let decendants = {}; +let parents = {}; +let graphs = {}; export const clear = () => { decendants = {}; + parents = {}; clusterDb = {}; + graphs = {}; }; -const copy = (clusterId, graph, newGraph, rootId) => { - logger.info('Copying ', clusterId, graph.node(clusterId)); +const isDecendant = (id, ancenstorId) => { + // if (id === ancenstorId) return true; + + log.info('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.info('Tilt, ', clustedId, ',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 copyOld = (clusterId, graph, newGraph, rootId) => { + log.info('Copying to', rootId, ' from ', clusterId, graph.node(clusterId), rootId); const nodes = graph.children(clusterId); + log.info('Copying (nodes)', nodes); + if (nodes) { + nodes.forEach(node => { + if (graph.children(node).length > 0) { + copy(node, graph, newGraph, rootId); + } else { + // if (clusterId === rootId) { + const data = graph.node(node); + log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId); + newGraph.setNode(node, data); + newGraph.setParent(node, clusterId); + const edges = graph.edges(node); + log.trace('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); + } + }); + // } else { + // log.info('Skipping leaf as root ', rootId, ' !== ', clusterId, ' leaf id = ', node); + // } + } + // log.info('Removing node', node, graphlib.json.write(graph)); + log.info('Removing node', node); + graph.removeNode(node); + }); + } + // newGraph.setNode(clusterId, graph.node(clusterId)); +}; +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.info('Copying (nodes)', nodes); + nodes.forEach(node => { if (graph.children(node).length > 0) { copy(node, graph, newGraph, rootId); - } - - const data = graph.node(node); - logger.info(node, data, ' parent is ', clusterId); - newGraph.setNode(node, data); - newGraph.setParent(node, clusterId); - const edges = graph.edges(node); - logger.info('Copying Edges', edges); - edges.forEach(edge => { - logger.info('Edge', edge); - const data = graph.edge(edge.v, edge.w, edge.name); - logger.info('Edge data', data, rootId); - try { - // Do not copy edges in and out of the root cluster, they belong to the parent graph - if (!(edge.v === rootId || edge.w === rootId)) { - logger.info('Copying as ', edge.v, edge.w, data, edge.name); - newGraph.setEdge(edge.v, edge.w, data, edge.name); - logger.info('newgrapg edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0])); - } else { - logger.info('Skipping copy of edge as ', rootId, edge.v, edge.w, clusterId); - } - } catch (e) { - logger.error(e); + } 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.info('Setting parent', node, clusterId); + newGraph.setParent(node, clusterId); } - }); + const edges = graph.edges(node); + log.info('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.info('Removing node', node); graph.removeNode(node); }); - newGraph.setNode(clusterId, graph.node(clusterId)); }; - -const extractDecendants = (id, graph) => { +export const extractDecendants = (id, graph) => { + // log.info('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)); } @@ -60,6 +154,7 @@ const extractDecendants = (id, graph) => { }; export const extractGraphFromCluster = (clusterId, graph) => { + log.info('Extracting graph ', clusterId); const clusterGraph = new graphlib.Graph({ multigraph: true, compound: true @@ -96,8 +191,25 @@ export const extractGraphFromCluster = (clusterId, graph) => { // return {}; // }); + log.trace('Extracting before copy', graphlib.json.write(graph)); + log.trace('Extracting before copy', graphlib.json.write(graph)); copy(clusterId, graph, clusterGraph, clusterId); + log.trace('Extracting after copy', graphlib.json.write(graph)); + log.trace('Extracting after copy', clusterGraph.nodes()); + graphs[clusterId] = clusterGraph; + // Remove references to extracted cluster + // graph.edges().forEach(edge => { + // if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) { + // graph.removeEdge(edge); + // } + // }); + // graph.nodes().forEach(node => { + // if (isDecendant(node, clusterId)) { + // log.info('Removing ', node, ' from ', clusterId); + // graph.removeNode(node); + // } + // }); return clusterGraph; }; @@ -108,14 +220,14 @@ export const extractGraphFromCluster = (clusterId, graph) => { */ export const validate = graph => { const edges = graph.edges(); - logger.trace('Edges: ', edges); + log.trace('Edges: ', edges); for (let i = 0; i < edges.length; i++) { if (graph.children(edges[i].v).length > 0) { - logger.trace('The node ', edges[i].v, ' is part of and edge even though it has children'); + 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) { - logger.trace('The node ', edges[i].w, ' is part of and edge even though it has children'); + log.trace('The node ', edges[i].w, ' is part of and edge even though it has children'); return false; } } @@ -129,16 +241,16 @@ export const validate = graph => { */ export const findNonClusterChild = (id, graph) => { // const node = graph.node(id); - logger.trace('Searching', id); + log.trace('Searching', id); const children = graph.children(id); if (children.length < 1) { - logger.trace('This is a valid node', id); + 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) { - logger.trace('Found replacement for', id, ' => ', _id); + log.trace('Found replacement for', id, ' => ', _id); return _id; } } @@ -160,15 +272,19 @@ const getAnchorId = id => { return id; }; -export const adjustClustersAndEdges = graph => { - // calc decendants, sa - +export const adjustClustersAndEdges = (graph, depth) => { + if (!graph || depth > 10) { + log.info('Opting out, no graph '); + return; + } else { + log.info('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) { - logger.info( + log.trace( 'Cluster identified', id, ' Replacement id in edges: ', @@ -184,13 +300,22 @@ export const adjustClustersAndEdges = graph => { const children = graph.children(id); const edges = graph.edges(); if (children.length > 0) { - logger.info('Cluster identified', id); + log.info('Cluster identified', id, decendants); edges.forEach(edge => { - logger.info('Edge: ', edge, decendants[id]); + // log.info('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) { - if (decendants[id].indexOf(edge.v) < 0 || decendants[id].indexOf(edge.w) < 0) { - logger.info('Edge: ', edge, ' leaves cluster ', 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.info('Edge: ', edge, ' leaves cluster ', id); + log.info('Decendants of ', id, ': ', decendants[id]); clusterDb[id].externalConnections = true; } } @@ -198,73 +323,265 @@ export const adjustClustersAndEdges = graph => { } }); - // 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;) - const clusters = Object.keys(clusterDb); - // clusters.forEach(clusterId => { - for (let i = 0; i < clusters.length; i++) { - const clusterId = clusters[i]; - if (!clusterDb[clusterId].externalConnections) { - logger.trace('Cluster without external connections', clusterId); - const edges = graph.nodeEdges(clusterId); + extractor(graph, 0); - // New graph with the nodes in the cluster - const clusterGraph = extractGraphFromCluster(clusterId, graph); - logger.trace('Cluster graph', clusterGraph.nodes()); - logger.trace('Graph', graph.nodes()); - - // Create a new node in the original graph, this new node is not a cluster - // but a regular node with the cluster dontent as a new attached graph - graph.setNode(clusterId, { - clusterNode: true, - id: clusterId, - clusterData: clusterDb[clusterId], - labelText: clusterDb[clusterId].labelText, - graph: clusterGraph - }); - - // The original edges in and out of the cluster is applied - edges.forEach(edge => { - logger.trace('Setting edge', edge); - const data = graph.edge(edge); - graph.setEdge(edge.v, edge.w, data); - }); - } - } - logger.trace('Graph', graph.nodes()); // 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); - logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); - logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); + log.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + 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 - logger.trace( - 'Fix', - clusterDb, - 'ids:', - e.v, - e.w, - 'Translateing: ', - clusterDb[e.v], - clusterDb[e.w] - ); + log.trace('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]); if (clusterDb[e.v] || clusterDb[e.w]) { - logger.trace('Fixing and trixing - removing', e.v, e.w, e.name); + 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; - logger.trace('Replacing with', v, w, e.name); + log.trace('Replacing with', v, w, e.name); graph.setEdge(v, w, edge, e.name); } }); - logger.trace('Graph', graph.nodes()); + log.info('Adjusted Graph', graphlib.json.write(graph)); - logger.trace(clusterDb); + 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 transformClustersToNodes = (graph, depth) => { + log.info('transformClustersToNodes - ', depth); + 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;) + const 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.info('Done, no node has children', graph.nodes()); + return; + } + // const clusters = Object.keys(clusterDb); + // clusters.forEach(clusterId => { + log.info('Nodes = ', nodes); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + log.info( + 'Handling node', + node, + clusterDb, + clusterDb[node] && !clusterDb[node].externalConnections, + !graph.parent(node), + graph.node(node) + ); + // 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]) { + if ( + !clusterDb[node].externalConnections && + !graph.parent(node) && + graph.children(node) && + graph.children(node).length > 0 + ) { + log.info('Cluster without external connections', node); + // const parentGraph = parent && graphs[parent] ? graphs[parent] : graph; + // New graph with the nodes in the cluster + log.info('before Extracting ', node, ' parent is ', graph.parent(node)); + const clusterGraph = extractGraphFromCluster(node, graph); + + if (clusterGraph) { + log.trace('Cluster graph', clusterGraph.nodes()); + log.trace('Graph', graph.edges()); + + log.info('Creating node in original', node, clusterGraph); + + // Create a new node in the original graph, this new node is not a cluster + // but a regular node with the cluster content as a new attached graph + graph.setNode(node, { + clusterNode: true, + id: node, + clusterData: clusterDb[node], + labelText: clusterDb[node].labelText, + graph: clusterGraph + }); + + // if any node in the clusterGraph still has children + transformClustersToNodes(clusterGraph, depth + 1); + } + + // The original edges in and out of the cluster is applied + // edges.forEach(edge => { + // log.info('Setting edge', edge); + // const data = graph.edge(edge); + // graph.setEdge(edge.v, edge.w, data); + // }); + } + } else { + log.info('Not a cluster ', node); + } + } +}; + +export const extractor = (graph, depth) => { + log.info('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.info('Done, no node has children', graph.nodes()); + return; + } + // const clusters = Object.keys(clusterDb); + // clusters.forEach(clusterId => { + log.info('Nodes = ', nodes, depth); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + log.info( + '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.info('Not a cluster', node, depth); + // break; + } else if ( + !clusterDb[node].externalConnections && + !graph.parent(node) && + graph.children(node) && + graph.children(node).length > 0 + ) { + log.info( + '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], + labelText: clusterDb[node].labelText, + graph: clusterGraph + }); + log.info('New graph after copy', graphlib.json.write(clusterGraph)); + log.info('Old graph after copy', graphlib.json.write(graph)); + + /* + // New graph with the nodes in the cluster + log.info('before Extracting ', node, ' parent is ', graph.parent(node)); + const clusterGraph = extractGraphFromCluster(node, graph); + + if (clusterGraph) { + log.trace('Cluster graph', clusterGraph.nodes()); + log.trace('Graph', graph.edges()); + + log.info('Creating node in original', node, clusterGraph); + + // Create a new node in the original graph, this new node is not a cluster + // but a regular node with the cluster content as a new attached graph + graph.setNode(node, { + clusterNode: true, + id: node, + clusterData: clusterDb[node], + labelText: clusterDb[node].labelText, + graph: clusterGraph + }); + + // if any node in the clusterGraph still has children + transformClustersToNodes(clusterGraph, depth + 1); + } + + // The original edges in and out of the cluster is applied + // edges.forEach(edge => { + // log.info('Setting edge', edge); + // const data = graph.edge(edge); + // graph.setEdge(edge.v, edge.w, data); + // }); + */ + } else { + log.info( + '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.info(clusterDb); + } + } + + nodes = graph.nodes(); + log.info('New list of nodes', nodes); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const data = graph.node(node); + log.info(' Now next leveƶ', node, data); + if (data.clusterNode) { + extractor(data.graph, depth + 1); + } + } }; diff --git a/src/dagre-wrapper/mermaid-graphlib.spec.js b/src/dagre-wrapper/mermaid-graphlib.spec.js index cd0c29b34..ad483eea7 100644 --- a/src/dagre-wrapper/mermaid-graphlib.spec.js +++ b/src/dagre-wrapper/mermaid-graphlib.spec.js @@ -1,6 +1,6 @@ import graphlib from 'graphlib'; import dagre from 'dagre'; -import { validate, adjustClustersAndEdges, extractGraphFromCluster } from './mermaid-graphlib'; +import { validate, adjustClustersAndEdges, extractGraphFromCluster, extractDecendants } from './mermaid-graphlib'; import { setLogLevel, logger } from '../logger'; describe('Graphlib decorations', () => { @@ -21,40 +21,6 @@ describe('Graphlib decorations', () => { g.setDefaultEdgeLabel(function () { return {}; }); - -// // Add node 'a' to the graph with no label -// g.setNode('a'); - -// // Add node 'b' to the graph with a String label -// g.setNode('b', 'b's value'); - -// // Add node 'c' to the graph with an Object label -// g.setNode('c', { k: 123 }); - -// // What nodes are in the graph? -// g.nodes(); -// // => `[ 'a', 'b', 'c' ]` - -// // Add a directed edge from 'a' to 'b', but assign no label -// g.setEdge('a', 'b'); - -// // Add a directed edge from 'c' to 'd' with an Object label. -// // Since 'd' did not exist prior to this call it is automatically -// // created with an undefined label. -// g.setEdge('c', 'd', { k: 456 }); - -// // What edges are in the graph? -// g.edges(); -// // => `[ { v: 'a', w: 'b' }, -// // { v: 'c', w: 'd' } ]`. - -// // Which edges leave node 'a'? -// g.outEdges('a'); -// // => `[ { v: 'a', w: 'b' } ]` - -// // Which edges enter and leave node 'd'? -// g.nodeEdges('d'); -// // => `[ { v: 'c', w: 'd' } ]` }); describe('validate', function () { @@ -135,71 +101,6 @@ describe('Graphlib decorations', () => { expect(newGraph.edges('a')).toEqual([{ v: 'a', w: 'b' }]); }); - it('It is possible to extract a clusters to a new graph 2 GLB1', 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.setNode('c', { data: 3 }); - g.setParent('a', 'C1'); - g.setParent('b', 'C1'); - g.setParent('c', 'C2'); - g.setEdge('a', 'b', { name: 'C1-internal-link' }); - g.setEdge('C1', 'C2', { name: 'C1-external-link' }); - - const C1 = extractGraphFromCluster('C1', g); - const C2 = extractGraphFromCluster('C2', g); - - expect(g.nodes()).toEqual(['C1', 'C2']); - expect(g.children('C1')).toEqual([]); - expect(g.children('C2')).toEqual([]); - expect(g.edges()).toEqual([{ v: 'C1', w: 'C2' }]); - - logger.info(g.nodes()); - expect(C1.nodes()).toEqual(['a', 'C1', 'b']); - expect(C1.children('C1')).toEqual(['a', 'b']); - expect(C1.edges()).toEqual([{ v: 'a', w: 'b' }]); - - expect(C2.nodes()).toEqual(['c', 'C2']); - expect(C2.edges()).toEqual([]); - }); - - it('It is possible to extract a cluster from a graph so that the nodes are removed from original graph', 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.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' }); - - const newGraph = extractGraphFromCluster('C1', g); - logger.info(g.nodes()); - expect(g.nodes()).toEqual(['c','C1']); - expect(g.edges().length).toBe(1); - expect(g.children()).toEqual(['c','C1']); - expect(g.children('C1')).toEqual([]); - }); - }); it('Validate should detect edges between clusters and transform clusters GLB4', function () { /* a --> b @@ -214,6 +115,8 @@ describe('Graphlib decorations', () => { 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'); @@ -240,7 +143,6 @@ describe('Graphlib decorations', () => { g.setNode('b', { data: 2 }); g.setParent('a', 'C1'); g.setParent('b', 'C2'); - g.setParent('C1', 'C2'); // g.setEdge('a', 'b', { name: 'C1-internal-link' }); g.setEdge('C1', 'C2', { name: 'C1-external-link' }); @@ -250,4 +152,248 @@ describe('Graphlib decorations', () => { 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']); + }); }); diff --git a/src/dagre-wrapper/nodes.js b/src/dagre-wrapper/nodes.js index 47be56991..ec45764e7 100644 --- a/src/dagre-wrapper/nodes.js +++ b/src/dagre-wrapper/nodes.js @@ -396,5 +396,7 @@ export const clear = () => { export const positionNode = node => { const el = nodeElems[node.id]; + logger.debug('Transforming node', node); el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); + // el.attr('transform', 'translate(' + node.x / 2 + ', ' + 0 + ')'); }; diff --git a/src/diagrams/flowchart/flowRenderer-v2.js b/src/diagrams/flowchart/flowRenderer-v2.js index b76ff361a..9e781a2b5 100644 --- a/src/diagrams/flowchart/flowRenderer-v2.js +++ b/src/diagrams/flowchart/flowRenderer-v2.js @@ -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';