mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
#1295 Moving graph operations into mermaid-graplib and adding tests
This commit is contained in:
parent
857c860952
commit
8455db6fae
@ -31,7 +31,7 @@
|
||||
G-->H
|
||||
G-->c
|
||||
</div>
|
||||
<div class="mermaid" style="width: 50%; height: 20%;">
|
||||
<div class="mermaid2" style="width: 50%; height: 20%;">
|
||||
stateDiagram-v2
|
||||
[*] --> monkey
|
||||
state monkey {
|
||||
@ -70,7 +70,7 @@
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
</div>
|
||||
<div class="mermaid2" style="width: 100%; height: 100%;">
|
||||
<div class="mermaid" style="width: 100%; height: 100%;">
|
||||
stateDiagram-v2
|
||||
[*] --> First
|
||||
First --> Second
|
||||
|
@ -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.
|
||||
|
@ -1,11 +1,12 @@
|
||||
import dagre from 'dagre';
|
||||
import insertMarkers from './markers';
|
||||
import { clear as cleargraphlib, clusterDb, adjustClustersAndEdges } from './mermaid-graphlib';
|
||||
import { insertNode, positionNode, clear as clearNodes } from './nodes';
|
||||
import { insertCluster, clear as clearClusters } from './clusters';
|
||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
|
||||
import { logger } from '../logger';
|
||||
|
||||
let clusterDb = {};
|
||||
// let clusterDb = {};
|
||||
|
||||
const getAnchorId = id => {
|
||||
// Only insert an achor once
|
||||
@ -50,11 +51,12 @@ const findNonClusterChild = (id, graph) => {
|
||||
|
||||
export const render = (elem, graph, markers, diagramtype, id) => {
|
||||
insertMarkers(elem, markers, diagramtype, id);
|
||||
clusterDb = {};
|
||||
clearNodes();
|
||||
clearEdges();
|
||||
clearClusters();
|
||||
|
||||
adjustClustersAndEdges(graph);
|
||||
|
||||
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');
|
||||
@ -68,13 +70,9 @@ export const render = (elem, graph, markers, diagramtype, id) => {
|
||||
if (node.type !== 'group') {
|
||||
insertNode(nodes, graph.node(v));
|
||||
} else {
|
||||
// const width = getClusterTitleWidth(clusters, node);
|
||||
const children = graph.children(v);
|
||||
|
||||
logger.info('Cluster identified', node.id, children[0], findNonClusterChild(node.id, graph));
|
||||
// nodes2expand.push({ id: children[0], width });
|
||||
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph) };
|
||||
// clusterDb[node.id] = { id: node.id + '_anchor' };
|
||||
// const children = graph.children(v);
|
||||
// logger.info('Cluster identified', node.id, children[0], findNonClusterChild(node.id, graph));
|
||||
// clusterDb[node.id] = { id: findNonClusterChild(node.id, graph) };
|
||||
}
|
||||
});
|
||||
logger.info('Clusters ', clusterDb);
|
||||
|
237
src/dagre-wrapper/mermaid-graphlib.js
Normal file
237
src/dagre-wrapper/mermaid-graphlib.js
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Decorates with functions required by mermaids dagre-wrapper.
|
||||
*/
|
||||
import { logger } from '../logger';
|
||||
import graphlib from 'graphlib';
|
||||
|
||||
export let clusterDb = {};
|
||||
let decendants = {};
|
||||
|
||||
export const clear = () => {
|
||||
decendants = {};
|
||||
clusterDb = {};
|
||||
};
|
||||
|
||||
const copy = (clusterId, graph, newGraph, rootId) => {
|
||||
logger.trace('Copying ', clusterId);
|
||||
const nodes = graph.children(clusterId);
|
||||
nodes.forEach(node => {
|
||||
if (graph.children(node).length > 0) {
|
||||
copy(node, graph, newGraph, rootId);
|
||||
}
|
||||
|
||||
const data = graph.node(node);
|
||||
logger.trace(node, data, ' parent is ', clusterId);
|
||||
newGraph.setNode(node, data);
|
||||
newGraph.setParent(node, clusterId);
|
||||
const edges = graph.edges(node);
|
||||
graph.removeNode(node);
|
||||
logger.trace('Edges', edges);
|
||||
edges.forEach(edge => {
|
||||
const data = graph.edge(edge);
|
||||
// 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.trace('Copying as ', rootId, edge.v, edge.w, clusterId);
|
||||
newGraph.setEdge(edge.v, edge.w, data);
|
||||
} else {
|
||||
logger.trace('Skipping copy of edge as ', rootId, edge.v, edge.w, clusterId);
|
||||
}
|
||||
});
|
||||
});
|
||||
newGraph.setNode(clusterId, graph.node(clusterId));
|
||||
};
|
||||
|
||||
const extractDecendants = (id, graph) => {
|
||||
const children = graph.children(id);
|
||||
let res = [].concat(children);
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
res = res.concat(extractDecendants(children[i], graph));
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const extractGraphFromCluster = (clusterId, graph) => {
|
||||
const clusterGraph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: 'TB',
|
||||
// Todo: set proper spacing
|
||||
nodesep: 10,
|
||||
ranksep: 10,
|
||||
marginx: 8,
|
||||
marginy: 8
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
copy(clusterId, graph, clusterGraph, clusterId);
|
||||
|
||||
return clusterGraph;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
logger.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');
|
||||
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');
|
||||
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
|
||||
*/
|
||||
const findNonClusterChild = (id, graph) => {
|
||||
// const node = graph.node(id);
|
||||
logger.trace('Searching', id);
|
||||
const children = graph.children(id);
|
||||
if (children.length < 1) {
|
||||
logger.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);
|
||||
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 => {
|
||||
// calc decendants, sa
|
||||
|
||||
// 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.trace(
|
||||
'Cluster identified',
|
||||
id,
|
||||
' Replacement id in edges: ',
|
||||
findNonClusterChild(id, graph)
|
||||
);
|
||||
decendants[id] = extractDecendants(id, graph);
|
||||
clusterDb[id] = { id: findNonClusterChild(id, graph) };
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
logger.trace('Cluster identified', id);
|
||||
edges.forEach(edge => {
|
||||
logger.trace('Edge: ', 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.trace('Edge: ', edge, ' leaves cluster ', id);
|
||||
clusterDb[id].externalConnections = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// 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, 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.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]
|
||||
);
|
||||
if (clusterDb[e.v] || clusterDb[e.w]) {
|
||||
logger.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);
|
||||
graph.setEdge(v, w, edge, e.name);
|
||||
}
|
||||
});
|
||||
logger.trace('Graph', graph.nodes());
|
||||
|
||||
logger.trace(clusterDb);
|
||||
};
|
228
src/dagre-wrapper/mermaid-graphlib.spec.js
Normal file
228
src/dagre-wrapper/mermaid-graphlib.spec.js
Normal file
@ -0,0 +1,228 @@
|
||||
import graphlib from 'graphlib';
|
||||
import dagre from 'dagre';
|
||||
import { validate, adjustClustersAndEdges, extractGraphFromCluster } 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 {};
|
||||
});
|
||||
|
||||
// // 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 () {
|
||||
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('It is possible to copy a cluster to a new graph 1', 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);
|
||||
expect(newGraph.nodes().length).toBe(4);
|
||||
expect(newGraph.edges().length).toBe(1);
|
||||
logger.info(newGraph.children('C1'));
|
||||
expect(newGraph.children('C2')).toEqual(['a']);
|
||||
expect(newGraph.children('C1')).toEqual(['b', 'C2']);
|
||||
expect(newGraph.edges('a')).toEqual([{ v: 'a', w: 'b' }]);
|
||||
});
|
||||
|
||||
it('It is possible to extract a clusters to a new graph 2', 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(C1.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', 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' });
|
||||
|
||||
adjustClustersAndEdges(g);
|
||||
logger.info(g.nodes())
|
||||
expect(g.nodes().length).toBe(2);
|
||||
expect(validate(g)).toBe(true);
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import moment from 'moment-mini';
|
||||
|
||||
//
|
||||
export const LEVELS = {
|
||||
debug: 1,
|
||||
info: 2,
|
||||
|
Loading…
x
Reference in New Issue
Block a user